ux: pull-to-refresh на всех списках (Этап 2 п.12)
Раньше свайп-обновление было только в Файлах. Добавлено (Material3 PullToRefreshBox): Почта (входящие/исходящие, force-обновление), Конференции (список бесед — заменяет убранную кнопку ↻), Контакты (force + CTag — дёшево), Задачи (список), Карточки (доски и открытая доска). Контакты: refresh() получил параметр force (pull-to-refresh форсит sync мимо TTL; с CTag-проверкой это 1 лёгкий PROPFIND).
This commit is contained in:
+11
-8
@@ -21,6 +21,8 @@ import androidx.compose.foundation.text.BasicTextField
|
|||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
@@ -51,6 +53,7 @@ import ru.forbion.f7cloud.core.designsystem.F7Colors
|
|||||||
import ru.forbion.f7cloud.core.designsystem.F7CreateButton
|
import ru.forbion.f7cloud.core.designsystem.F7CreateButton
|
||||||
import ru.forbion.f7cloud.core.designsystem.F7ModuleScreen
|
import ru.forbion.f7cloud.core.designsystem.F7ModuleScreen
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ContactsScreen(
|
fun ContactsScreen(
|
||||||
session: AuthSession,
|
session: AuthSession,
|
||||||
@@ -94,14 +97,13 @@ fun ContactsScreen(
|
|||||||
)
|
)
|
||||||
F7CreateButton(onClick = vm::openAddSheet, size = 40.dp)
|
F7CreateButton(onClick = vm::openAddSheet, size = 40.dp)
|
||||||
}
|
}
|
||||||
if (state.syncing && state.contacts.isNotEmpty()) {
|
PullToRefreshBox(
|
||||||
Text(
|
isRefreshing = state.syncing && state.contacts.isNotEmpty(),
|
||||||
"Обновление…",
|
onRefresh = { vm.refresh(session, force = true) },
|
||||||
modifier = Modifier.padding(horizontal = 4.dp, vertical = 2.dp),
|
modifier = Modifier
|
||||||
style = MaterialTheme.typography.labelSmall,
|
.weight(1f)
|
||||||
color = F7Colors.TextSecondary,
|
.fillMaxWidth(),
|
||||||
)
|
) {
|
||||||
}
|
|
||||||
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
||||||
items(state.filteredContacts, key = { "${it.uid}|${it.email}" }) { contact ->
|
items(state.filteredContacts, key = { "${it.uid}|${it.email}" }) { contact ->
|
||||||
ContactListRow(
|
ContactListRow(
|
||||||
@@ -112,6 +114,7 @@ fun ContactsScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
state.selectedContact?.let { contact ->
|
state.selectedContact?.let { contact ->
|
||||||
ContactDetailSheet(
|
ContactDetailSheet(
|
||||||
|
|||||||
+4
-2
@@ -99,14 +99,16 @@ class ContactsViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun refresh(session: AuthSession, showLoading: Boolean = false) {
|
fun refresh(session: AuthSession, showLoading: Boolean = false, force: Boolean = false) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
if (showLoading && _state.value.contacts.isEmpty()) {
|
if (showLoading && _state.value.contacts.isEmpty()) {
|
||||||
_state.update { it.copy(loading = true, error = null) }
|
_state.update { it.copy(loading = true, error = null) }
|
||||||
} else {
|
} else {
|
||||||
_state.update { it.copy(syncing = true, error = null) }
|
_state.update { it.copy(syncing = true, error = null) }
|
||||||
}
|
}
|
||||||
runCatching { repository.syncContacts(session) }
|
// force (pull-to-refresh) с CTag-проверкой дешёвый: 1 лёгкий PROPFIND,
|
||||||
|
// полная закачка vCard — только если адресные книги реально менялись
|
||||||
|
runCatching { repository.syncContacts(session, force = force) }
|
||||||
.onFailure { t ->
|
.onFailure { t ->
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
package ru.forbion.f7cloud.feature.deck
|
package ru.forbion.f7cloud.feature.deck
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
@@ -17,6 +20,7 @@ import ru.forbion.f7cloud.core.designsystem.F7ModuleScreen
|
|||||||
import ru.forbion.f7cloud.core.designsystem.F7OverlayDismissHandler
|
import ru.forbion.f7cloud.core.designsystem.F7OverlayDismissHandler
|
||||||
import ru.forbion.f7cloud.core.designsystem.F7SecondaryButton
|
import ru.forbion.f7cloud.core.designsystem.F7SecondaryButton
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun DeckScreen(
|
fun DeckScreen(
|
||||||
session: AuthSession,
|
session: AuthSession,
|
||||||
@@ -57,6 +61,15 @@ fun DeckScreen(
|
|||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
val detail = state.boardDetail
|
val detail = state.boardDetail
|
||||||
|
PullToRefreshBox(
|
||||||
|
isRefreshing = state.loading && (state.boards.isNotEmpty() || detail != null),
|
||||||
|
onRefresh = {
|
||||||
|
if (detail != null) vm.openBoard(session, detail.boardId) else vm.load(session)
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
) {
|
||||||
if (detail != null) {
|
if (detail != null) {
|
||||||
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
||||||
items(detail.stacks, key = { it.id }) { stack ->
|
items(detail.stacks, key = { it.id }) { stack ->
|
||||||
@@ -79,4 +92,5 @@ fun DeckScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ import androidx.compose.foundation.verticalScroll
|
|||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
@@ -485,6 +487,8 @@ fun MailScreen(
|
|||||||
onOutboxRetry = { vm.retryOutboxMessage(session, it) },
|
onOutboxRetry = { vm.retryOutboxMessage(session, it) },
|
||||||
onOutboxDelete = { vm.deleteOutboxMessage(session, it) },
|
onOutboxDelete = { vm.deleteOutboxMessage(session, it) },
|
||||||
onLoadMore = { vm.loadMore(session) },
|
onLoadMore = { vm.loadMore(session) },
|
||||||
|
refreshing = state.loading,
|
||||||
|
onRefresh = { vm.load(session, forceRefresh = true) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
MailNavigationSidebar(
|
MailNavigationSidebar(
|
||||||
@@ -520,6 +524,7 @@ fun MailScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun MailInboxScreen(
|
private fun MailInboxScreen(
|
||||||
session: AuthSession,
|
session: AuthSession,
|
||||||
@@ -542,6 +547,8 @@ private fun MailInboxScreen(
|
|||||||
onOutboxRetry: (Int) -> Unit,
|
onOutboxRetry: (Int) -> Unit,
|
||||||
onOutboxDelete: (Int) -> Unit,
|
onOutboxDelete: (Int) -> Unit,
|
||||||
onLoadMore: () -> Unit,
|
onLoadMore: () -> Unit,
|
||||||
|
refreshing: Boolean = false,
|
||||||
|
onRefresh: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val listItems = remember(messages) { buildMailInboxListItems(messages) }
|
val listItems = remember(messages) { buildMailInboxListItems(messages) }
|
||||||
val folderTitle = mailFolderListTitle(selectedFolder, searchQuery, searchParams)
|
val folderTitle = mailFolderListTitle(selectedFolder, searchQuery, searchParams)
|
||||||
@@ -595,11 +602,14 @@ private fun MailInboxScreen(
|
|||||||
if (!error.isNullOrBlank()) {
|
if (!error.isNullOrBlank()) {
|
||||||
Text(text = error, color = F7Colors.Error, style = MaterialTheme.typography.bodyMedium)
|
Text(text = error, color = F7Colors.Error, style = MaterialTheme.typography.bodyMedium)
|
||||||
}
|
}
|
||||||
Box(
|
PullToRefreshBox(
|
||||||
|
isRefreshing = refreshing && !loading,
|
||||||
|
onRefresh = onRefresh,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
if (loading) {
|
if (loading) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
@@ -706,6 +716,7 @@ private fun MailInboxScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
@@ -318,11 +320,14 @@ fun TalkScreen(
|
|||||||
onCreateRoom = vm::openCreateRoom,
|
onCreateRoom = vm::openCreateRoom,
|
||||||
onToggleFavorite = { vm.toggleFavorite(session, it.token) },
|
onToggleFavorite = { vm.toggleFavorite(session, it.token) },
|
||||||
onMarkUnread = { vm.markRoomUnread(session, it.token) },
|
onMarkUnread = { vm.markRoomUnread(session, it.token) },
|
||||||
|
refreshing = state.loading && state.rooms.isNotEmpty(),
|
||||||
|
onRefresh = { vm.load(session) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun RoomListContent(
|
private fun RoomListContent(
|
||||||
session: AuthSession,
|
session: AuthSession,
|
||||||
@@ -337,6 +342,8 @@ private fun RoomListContent(
|
|||||||
onCreateRoom: () -> Unit,
|
onCreateRoom: () -> Unit,
|
||||||
onToggleFavorite: (TalkRoom) -> Unit,
|
onToggleFavorite: (TalkRoom) -> Unit,
|
||||||
onMarkUnread: (TalkRoom) -> Unit,
|
onMarkUnread: (TalkRoom) -> Unit,
|
||||||
|
refreshing: Boolean = false,
|
||||||
|
onRefresh: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
@@ -351,6 +358,13 @@ private fun RoomListContent(
|
|||||||
onCreateClick = onCreateRoom,
|
onCreateClick = onCreateRoom,
|
||||||
)
|
)
|
||||||
TalkFilterChips(selected = roomFilter, onClear = onClearFilter)
|
TalkFilterChips(selected = roomFilter, onClear = onClearFilter)
|
||||||
|
PullToRefreshBox(
|
||||||
|
isRefreshing = refreshing,
|
||||||
|
onRefresh = onRefresh,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
) {
|
||||||
if (rooms.isEmpty()) {
|
if (rooms.isEmpty()) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -392,6 +406,7 @@ private fun RoomListContent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import androidx.compose.foundation.lazy.LazyColumn
|
|||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
@@ -143,6 +145,7 @@ fun TasksScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun TasksMainContent(
|
private fun TasksMainContent(
|
||||||
session: AuthSession,
|
session: AuthSession,
|
||||||
@@ -190,6 +193,13 @@ private fun TasksMainContent(
|
|||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
PullToRefreshBox(
|
||||||
|
isRefreshing = state.loading,
|
||||||
|
onRefresh = onReload,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
@@ -231,4 +241,5 @@ private fun TasksMainContent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user