From d1d61d0cf8728b7a77a4fa40408c6dd7c80eabfa Mon Sep 17 00:00:00 2001 From: b-dev-mobile Date: Thu, 9 Jul 2026 12:48:08 +0000 Subject: [PATCH] =?UTF-8?q?ux:=20pull-to-refresh=20=D0=BD=D0=B0=20=D0=B2?= =?UTF-8?q?=D1=81=D0=B5=D1=85=20=D1=81=D0=BF=D0=B8=D1=81=D0=BA=D0=B0=D1=85?= =?UTF-8?q?=20(=D0=AD=D1=82=D0=B0=D0=BF=202=20=D0=BF.12)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Раньше свайп-обновление было только в Файлах. Добавлено (Material3 PullToRefreshBox): Почта (входящие/исходящие, force-обновление), Конференции (список бесед — заменяет убранную кнопку ↻), Контакты (force + CTag — дёшево), Задачи (список), Карточки (доски и открытая доска). Контакты: refresh() получил параметр force (pull-to-refresh форсит sync мимо TTL; с CTag-проверкой это 1 лёгкий PROPFIND). --- .../feature/contacts/ContactsScreen.kt | 33 ++++++++------- .../feature/contacts/ContactsViewModel.kt | 6 ++- .../f7cloud/feature/deck/DeckScreen.kt | 42 ++++++++++++------- .../f7cloud/feature/mail/MailScreen.kt | 13 +++++- .../f7cloud/feature/talk/TalkScreen.kt | 15 +++++++ .../f7cloud/feature/tasks/TasksScreen.kt | 11 +++++ 6 files changed, 88 insertions(+), 32 deletions(-) diff --git a/feature/contacts/src/main/java/ru/forbion/f7cloud/feature/contacts/ContactsScreen.kt b/feature/contacts/src/main/java/ru/forbion/f7cloud/feature/contacts/ContactsScreen.kt index d553ee9..4852e87 100644 --- a/feature/contacts/src/main/java/ru/forbion/f7cloud/feature/contacts/ContactsScreen.kt +++ b/feature/contacts/src/main/java/ru/forbion/f7cloud/feature/contacts/ContactsScreen.kt @@ -21,6 +21,8 @@ import androidx.compose.foundation.text.BasicTextField import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.material3.Text import androidx.compose.runtime.Composable 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.F7ModuleScreen +@OptIn(ExperimentalMaterial3Api::class) @Composable fun ContactsScreen( session: AuthSession, @@ -94,21 +97,21 @@ fun ContactsScreen( ) F7CreateButton(onClick = vm::openAddSheet, size = 40.dp) } - if (state.syncing && state.contacts.isNotEmpty()) { - Text( - "Обновление…", - modifier = Modifier.padding(horizontal = 4.dp, vertical = 2.dp), - style = MaterialTheme.typography.labelSmall, - color = F7Colors.TextSecondary, - ) - } - LazyColumn(modifier = Modifier.fillMaxSize()) { - items(state.filteredContacts, key = { "${it.uid}|${it.email}" }) { contact -> - ContactListRow( - contact = contact, - onClick = { vm.openContact(contact) }, - ) - HorizontalDivider(color = F7Colors.Border.copy(alpha = 0.6f)) + PullToRefreshBox( + isRefreshing = state.syncing && state.contacts.isNotEmpty(), + onRefresh = { vm.refresh(session, force = true) }, + modifier = Modifier + .weight(1f) + .fillMaxWidth(), + ) { + LazyColumn(modifier = Modifier.fillMaxSize()) { + items(state.filteredContacts, key = { "${it.uid}|${it.email}" }) { contact -> + ContactListRow( + contact = contact, + onClick = { vm.openContact(contact) }, + ) + HorizontalDivider(color = F7Colors.Border.copy(alpha = 0.6f)) + } } } } diff --git a/feature/contacts/src/main/java/ru/forbion/f7cloud/feature/contacts/ContactsViewModel.kt b/feature/contacts/src/main/java/ru/forbion/f7cloud/feature/contacts/ContactsViewModel.kt index d347d66..6c99c38 100644 --- a/feature/contacts/src/main/java/ru/forbion/f7cloud/feature/contacts/ContactsViewModel.kt +++ b/feature/contacts/src/main/java/ru/forbion/f7cloud/feature/contacts/ContactsViewModel.kt @@ -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) { if (showLoading && _state.value.contacts.isEmpty()) { _state.update { it.copy(loading = true, error = null) } } else { _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 -> _state.update { it.copy( diff --git a/feature/deck/src/main/java/ru/forbion/f7cloud/feature/deck/DeckScreen.kt b/feature/deck/src/main/java/ru/forbion/f7cloud/feature/deck/DeckScreen.kt index 8ec4959..34b1cbc 100644 --- a/feature/deck/src/main/java/ru/forbion/f7cloud/feature/deck/DeckScreen.kt +++ b/feature/deck/src/main/java/ru/forbion/f7cloud/feature/deck/DeckScreen.kt @@ -1,10 +1,13 @@ package ru.forbion.f7cloud.feature.deck import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.MaterialTheme 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.LaunchedEffect 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.F7SecondaryButton +@OptIn(ExperimentalMaterial3Api::class) @Composable fun DeckScreen( session: AuthSession, @@ -57,23 +61,33 @@ fun DeckScreen( }, ) { val detail = state.boardDetail - if (detail != null) { - LazyColumn(modifier = Modifier.fillMaxSize()) { - items(detail.stacks, key = { it.id }) { stack -> - F7ListCard { - Text(stack.title, style = MaterialTheme.typography.titleSmall) - stack.cards.forEach { card -> - val prefix = if (card.done) "✓ " else "• " - Text(prefix + card.title, style = MaterialTheme.typography.bodyMedium) + 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) { + LazyColumn(modifier = Modifier.fillMaxSize()) { + items(detail.stacks, key = { it.id }) { stack -> + F7ListCard { + Text(stack.title, style = MaterialTheme.typography.titleSmall) + stack.cards.forEach { card -> + val prefix = if (card.done) "✓ " else "• " + Text(prefix + card.title, style = MaterialTheme.typography.bodyMedium) + } } } } - } - } else { - LazyColumn(modifier = Modifier.fillMaxSize()) { - items(state.boards, key = { it.id }) { board -> - F7ListCard(onClick = { vm.openBoard(session, board.id) }) { - Text(board.title, style = MaterialTheme.typography.titleSmall) + } else { + LazyColumn(modifier = Modifier.fillMaxSize()) { + items(state.boards, key = { it.id }) { board -> + F7ListCard(onClick = { vm.openBoard(session, board.id) }) { + Text(board.title, style = MaterialTheme.typography.titleSmall) + } } } } diff --git a/feature/mail/src/main/java/ru/forbion/f7cloud/feature/mail/MailScreen.kt b/feature/mail/src/main/java/ru/forbion/f7cloud/feature/mail/MailScreen.kt index de74ad1..24cb42b 100644 --- a/feature/mail/src/main/java/ru/forbion/f7cloud/feature/mail/MailScreen.kt +++ b/feature/mail/src/main/java/ru/forbion/f7cloud/feature/mail/MailScreen.kt @@ -24,6 +24,8 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme 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.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -485,6 +487,8 @@ fun MailScreen( onOutboxRetry = { vm.retryOutboxMessage(session, it) }, onOutboxDelete = { vm.deleteOutboxMessage(session, it) }, onLoadMore = { vm.loadMore(session) }, + refreshing = state.loading, + onRefresh = { vm.load(session, forceRefresh = true) }, ) } MailNavigationSidebar( @@ -520,6 +524,7 @@ fun MailScreen( } } +@OptIn(ExperimentalMaterial3Api::class) @Composable private fun MailInboxScreen( session: AuthSession, @@ -542,6 +547,8 @@ private fun MailInboxScreen( onOutboxRetry: (Int) -> Unit, onOutboxDelete: (Int) -> Unit, onLoadMore: () -> Unit, + refreshing: Boolean = false, + onRefresh: () -> Unit = {}, ) { val listItems = remember(messages) { buildMailInboxListItems(messages) } val folderTitle = mailFolderListTitle(selectedFolder, searchQuery, searchParams) @@ -595,11 +602,14 @@ private fun MailInboxScreen( if (!error.isNullOrBlank()) { Text(text = error, color = F7Colors.Error, style = MaterialTheme.typography.bodyMedium) } - Box( + PullToRefreshBox( + isRefreshing = refreshing && !loading, + onRefresh = onRefresh, modifier = Modifier .weight(1f) .fillMaxWidth(), ) { + Box(modifier = Modifier.fillMaxSize()) { if (loading) { Box( modifier = Modifier.fillMaxSize(), @@ -705,6 +715,7 @@ private fun MailInboxScreen( } } } + } } } diff --git a/feature/talk/src/main/java/ru/forbion/f7cloud/feature/talk/TalkScreen.kt b/feature/talk/src/main/java/ru/forbion/f7cloud/feature/talk/TalkScreen.kt index 6e0aac8..33ae3fd 100644 --- a/feature/talk/src/main/java/ru/forbion/f7cloud/feature/talk/TalkScreen.kt +++ b/feature/talk/src/main/java/ru/forbion/f7cloud/feature/talk/TalkScreen.kt @@ -29,6 +29,8 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text 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.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -318,11 +320,14 @@ fun TalkScreen( onCreateRoom = vm::openCreateRoom, onToggleFavorite = { vm.toggleFavorite(session, it.token) }, onMarkUnread = { vm.markRoomUnread(session, it.token) }, + refreshing = state.loading && state.rooms.isNotEmpty(), + onRefresh = { vm.load(session) }, ) } } } +@OptIn(ExperimentalMaterial3Api::class) @Composable private fun RoomListContent( session: AuthSession, @@ -337,6 +342,8 @@ private fun RoomListContent( onCreateRoom: () -> Unit, onToggleFavorite: (TalkRoom) -> Unit, onMarkUnread: (TalkRoom) -> Unit, + refreshing: Boolean = false, + onRefresh: () -> Unit = {}, ) { Column( modifier = Modifier.fillMaxSize(), @@ -351,6 +358,13 @@ private fun RoomListContent( onCreateClick = onCreateRoom, ) TalkFilterChips(selected = roomFilter, onClear = onClearFilter) + PullToRefreshBox( + isRefreshing = refreshing, + onRefresh = onRefresh, + modifier = Modifier + .weight(1f) + .fillMaxWidth(), + ) { if (rooms.isEmpty()) { Column( modifier = Modifier @@ -391,6 +405,7 @@ private fun RoomListContent( } } } + } } } diff --git a/feature/tasks/src/main/java/ru/forbion/f7cloud/feature/tasks/TasksScreen.kt b/feature/tasks/src/main/java/ru/forbion/f7cloud/feature/tasks/TasksScreen.kt index 57fe77a..a7d1d74 100644 --- a/feature/tasks/src/main/java/ru/forbion/f7cloud/feature/tasks/TasksScreen.kt +++ b/feature/tasks/src/main/java/ru/forbion/f7cloud/feature/tasks/TasksScreen.kt @@ -11,6 +11,8 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.CircularProgressIndicator 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.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -143,6 +145,7 @@ fun TasksScreen( ) } +@OptIn(ExperimentalMaterial3Api::class) @Composable private fun TasksMainContent( session: AuthSession, @@ -190,6 +193,13 @@ private fun TasksMainContent( modifier = Modifier.fillMaxWidth(), ) } else { + PullToRefreshBox( + isRefreshing = state.loading, + onRefresh = onReload, + modifier = Modifier + .weight(1f) + .fillMaxWidth(), + ) { LazyColumn( modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(8.dp), @@ -229,6 +239,7 @@ private fun TasksMainContent( } } } + } } } }