From 5eb0edcc786c423573e1f3316a99a266890a6f72 Mon Sep 17 00:00:00 2001 From: junkfood <69683722+JunkFood02@users.noreply.github.com> Date: Mon, 30 Sep 2024 01:02:41 +0900 Subject: [PATCH 1/4] Migrate to Compose 1.7 (#845) * refactor: migrate to compose 1.7 (wip) * feat(ui): migrate to swipe --- app/build.gradle.kts | 4 + .../ash/reader/ui/page/home/feeds/FeedItem.kt | 7 +- .../reader/ui/page/home/feeds/FeedsPage.kt | 59 ++- .../reader/ui/page/home/feeds/GroupItem.kt | 4 +- .../home/feeds/drawer/feed/ClearFeedDialog.kt | 3 +- .../feeds/drawer/feed/DeleteFeedDialog.kt | 3 +- .../feeds/drawer/feed/FeedOptionDrawer.kt | 24 +- .../feeds/drawer/feed/FeedOptionViewModel.kt | 25 +- .../group/AllAllowNotificationDialog.kt | 5 +- .../drawer/group/AllMoveToGroupDialog.kt | 4 +- .../drawer/group/AllParseFullContentDialog.kt | 6 +- .../feeds/drawer/group/ClearGroupDialog.kt | 3 +- .../feeds/drawer/group/DeleteGroupDialog.kt | 3 +- .../feeds/drawer/group/GroupOptionDrawer.kt | 34 +- .../drawer/group/GroupOptionViewModel.kt | 15 +- .../reader/ui/page/home/flow/ArticleItem.kt | 369 +++++++------- .../reader/ui/page/home/flow/ArticleList.kt | 4 +- .../ash/reader/ui/page/home/flow/FlowPage.kt | 10 +- .../ui/page/home/flow/SwipeToDismissBox.kt | 471 ------------------ .../home/reading/drawer/FeedOptionDrawer.kt | 11 +- gradle/libs.versions.toml | 4 +- 21 files changed, 319 insertions(+), 749 deletions(-) delete mode 100644 app/src/main/java/me/ash/reader/ui/page/home/flow/SwipeToDismissBox.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8a7cafee..3b8e0009 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -80,6 +80,9 @@ android { proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") signingConfig = signingConfigs.getByName("release") } + all { + signingConfig = signingConfigs.getByName("release") + } } applicationVariants.all { outputs.all { @@ -151,6 +154,7 @@ dependencies { implementation(libs.readability4j) implementation(libs.rome) implementation(libs.telephoto) + implementation(libs.swipe) implementation(libs.okhttp) implementation(libs.okhttp.coroutines) implementation(libs.retrofit) diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedItem.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedItem.kt index da0601e5..03f8240f 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedItem.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedItem.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.platform.LocalView import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel +import kotlinx.coroutines.launch import me.ash.reader.domain.model.feed.Feed import me.ash.reader.ui.component.FeedIcon import me.ash.reader.ui.component.base.RYExtensibleVisibility @@ -40,6 +41,7 @@ fun FeedItem( isExpanded: () -> Boolean, feedOptionViewModel: FeedOptionViewModel = hiltViewModel(), onClick: () -> Unit = {}, + onLongClick: () -> Unit = {} ) { val view = LocalView.current val scope = rememberCoroutineScope() @@ -56,8 +58,11 @@ fun FeedItem( onClick() }, onLongClick = { + onLongClick() view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) - feedOptionViewModel.showDrawer(scope, feed.id) + scope.launch { + feedOptionViewModel.fetchFeed(feedId = feed.id) + } } ) .padding(horizontal = 14.dp) diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsPage.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsPage.kt index 03f05004..a2a624a9 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsPage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsPage.kt @@ -21,6 +21,7 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.ModalBottomSheetValue import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.KeyboardArrowRight import androidx.compose.material.icons.outlined.Settings @@ -28,6 +29,7 @@ import androidx.compose.material.icons.rounded.Add import androidx.compose.material.icons.rounded.Refresh import androidx.compose.material.icons.rounded.UnfoldLess import androidx.compose.material.icons.rounded.UnfoldMore +import androidx.compose.material.rememberModalBottomSheetState import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -53,6 +55,7 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavHostController import androidx.work.WorkInfo +import kotlinx.coroutines.launch import me.ash.reader.R import me.ash.reader.infrastructure.preference.LocalFeedsFilterBarFilled import me.ash.reader.infrastructure.preference.LocalFeedsFilterBarPadding @@ -123,7 +126,8 @@ fun FeedsPage( val newVersion = LocalNewVersionNumber.current val skipVersion = LocalSkipVersionNumber.current val currentVersion = remember { context.getCurrentVersion() } - val listState = if (groupWithFeedList.isNotEmpty()) feedsUiState.listState else rememberLazyListState() + val listState = + if (groupWithFeedList.isNotEmpty()) feedsUiState.listState else rememberLazyListState() val owner = LocalLifecycleOwner.current var isSyncing by remember { mutableStateOf(false) } @@ -162,6 +166,7 @@ fun FeedsPage( is GroupFeedsView.Group -> { groupsVisible[groupWithFeed.group.id] = true } + else -> {} } } @@ -174,12 +179,17 @@ fun FeedsPage( is GroupFeedsView.Group -> { groupsVisible[groupWithFeed.group.id] = false } + else -> {} } } hasGroupVisible = false } + val groupDrawerState = + rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden) + val feedDrawerState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden) + LaunchedEffect(Unit) { feedsViewModel.fetchAccount() } @@ -231,7 +241,7 @@ fun FeedsPage( } }, content = { - LazyColumn ( + LazyColumn( state = listState ) { item { @@ -303,7 +313,7 @@ fun FeedsPage( is GroupFeedsView.Group -> { Spacer(modifier = Modifier.height(16.dp)) - if (groupWithFeed.group.id != defaultGroupId || groupWithFeed.group.feeds > 0) { + if (groupWithFeed.group.id != defaultGroupId || groupWithFeed.group.feeds > 0) { GroupItem( isExpanded = { groupsVisible.getOrPut( @@ -321,10 +331,16 @@ fun FeedsPage( groupWithFeed.group.id, groupListExpand::value ).not() - hasGroupVisible = if (groupsVisible[groupWithFeed.group.id] == true) { - true - } else { - groupsVisible.any { it.value } + hasGroupVisible = + if (groupsVisible[groupWithFeed.group.id] == true) { + true + } else { + groupsVisible.any { it.value } + } + }, + onLongClick = { + scope.launch { + groupDrawerState.show() } } ) { @@ -351,17 +367,21 @@ fun FeedsPage( groupWithFeed.feed.groupId, groupListExpand::value ) - }, - ) { - filterChange( - navController = navController, - homeViewModel = homeViewModel, - filterState = filterUiState.copy( - group = null, - feed = groupWithFeed.feed, + }, onClick = { + filterChange( + navController = navController, + homeViewModel = homeViewModel, + filterState = filterUiState.copy( + group = null, + feed = groupWithFeed.feed, + ) ) - ) - } + }, onLongClick = { + scope.launch { + feedDrawerState.show() + } + } + ) } } } @@ -390,8 +410,9 @@ fun FeedsPage( ) SubscribeDialog(subscribeViewModel = subscribeViewModel) - GroupOptionDrawer() - FeedOptionDrawer() + + GroupOptionDrawer(drawerState = groupDrawerState) + FeedOptionDrawer(drawerState = feedDrawerState) AccountsTab( visible = accountTabVisible, diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/GroupItem.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/GroupItem.kt index f22a42a2..fb6e07c5 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/GroupItem.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/GroupItem.kt @@ -39,6 +39,7 @@ fun GroupItem( isExpanded: () -> Boolean, groupOptionViewModel: GroupOptionViewModel = hiltViewModel(), onExpanded: () -> Unit = {}, + onLongClick: () -> Unit = {}, groupOnClick: () -> Unit = {}, ) { val view = LocalView.current @@ -57,7 +58,8 @@ fun GroupItem( }, onLongClick = { view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) - groupOptionViewModel.showDrawer(scope, group.id) + groupOptionViewModel.fetchGroup(groupId = group.id) + onLongClick() } ) .padding(top = 22.dp) diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/feed/ClearFeedDialog.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/feed/ClearFeedDialog.kt index 0144eb70..180dcab3 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/feed/ClearFeedDialog.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/feed/ClearFeedDialog.kt @@ -19,6 +19,7 @@ import me.ash.reader.ui.ext.showToast fun ClearFeedDialog( feedName: String, feedOptionViewModel: FeedOptionViewModel = hiltViewModel(), + onConfirm: () -> Unit = {} ) { val context = LocalContext.current val feedOptionUiState = feedOptionViewModel.feedOptionUiState.collectAsStateValue() @@ -47,7 +48,7 @@ fun ClearFeedDialog( onClick = { feedOptionViewModel.clearFeed { feedOptionViewModel.hideClearDialog() - feedOptionViewModel.hideDrawer(scope) + onConfirm() context.showToast(toastString) } } diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/feed/DeleteFeedDialog.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/feed/DeleteFeedDialog.kt index 1bd1f1dc..fc554807 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/feed/DeleteFeedDialog.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/feed/DeleteFeedDialog.kt @@ -19,6 +19,7 @@ import me.ash.reader.ui.ext.showToast fun DeleteFeedDialog( feedName: String, feedOptionViewModel: FeedOptionViewModel = hiltViewModel(), + onConfirm: () -> Unit, ) { val context = LocalContext.current val feedOptionUiState = feedOptionViewModel.feedOptionUiState.collectAsStateValue() @@ -47,7 +48,7 @@ fun DeleteFeedDialog( onClick = { feedOptionViewModel.delete { feedOptionViewModel.hideDeleteDialog() - feedOptionViewModel.hideDrawer(scope) + onConfirm() context.showToast(toastString) } } diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/feed/FeedOptionDrawer.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/feed/FeedOptionDrawer.kt index ab9bee9a..702d2c43 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/feed/FeedOptionDrawer.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/feed/FeedOptionDrawer.kt @@ -10,8 +10,11 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.ModalBottomSheetState +import androidx.compose.material.ModalBottomSheetValue import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.CreateNewFolder +import androidx.compose.material.rememberModalBottomSheetState import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -39,9 +42,9 @@ import me.ash.reader.ui.ext.roundClick import me.ash.reader.ui.ext.showToast import me.ash.reader.ui.page.home.feeds.FeedOptionView -@OptIn(ExperimentalMaterialApi::class) @Composable fun FeedOptionDrawer( + drawerState: ModalBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden), feedOptionViewModel: FeedOptionViewModel = hiltViewModel(), content: @Composable () -> Unit = {}, ) { @@ -54,14 +57,15 @@ fun FeedOptionDrawer( val feed = feedOptionUiState.feed val toastString = stringResource(R.string.rename_toast, feedOptionUiState.newName) - BackHandler(feedOptionUiState.drawerState.isVisible) { + + BackHandler(drawerState.isVisible) { scope.launch { - feedOptionUiState.drawerState.hide() + drawerState.hide() } } BottomDrawer( - drawerState = feedOptionUiState.drawerState, + drawerState = drawerState, sheetContent = { Column(modifier = Modifier.navigationBarsPadding()) { Column( @@ -132,9 +136,13 @@ fun FeedOptionDrawer( content() } - DeleteFeedDialog(feedName = feed?.name ?: "") + DeleteFeedDialog( + feedName = feed?.name ?: "", + onConfirm = { scope.launch { drawerState.hide() } }) - ClearFeedDialog(feedName = feed?.name ?: "") + ClearFeedDialog( + feedName = feed?.name ?: "", + onConfirm = { scope.launch { drawerState.hide() } }) TextFieldDialog( visible = feedOptionUiState.newGroupDialogVisible, @@ -164,7 +172,7 @@ fun FeedOptionDrawer( }, onConfirm = { feedOptionViewModel.renameFeed() - feedOptionViewModel.hideDrawer(scope) + scope.launch { drawerState.hide() } context.showToast(toastString) } ) @@ -180,7 +188,7 @@ fun FeedOptionDrawer( }, onConfirm = { feedOptionViewModel.changeFeedUrl() - feedOptionViewModel.hideDrawer(scope) + scope.launch { drawerState.hide() } } ) } diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/feed/FeedOptionViewModel.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/feed/FeedOptionViewModel.kt index 5eda96ee..d2949794 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/feed/FeedOptionViewModel.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/feed/FeedOptionViewModel.kt @@ -3,6 +3,7 @@ package me.ash.reader.ui.page.home.feeds.drawer.feed import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ModalBottomSheetState import androidx.compose.material.ModalBottomSheetValue +import androidx.compose.ui.unit.Density import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel @@ -49,7 +50,7 @@ class FeedOptionViewModel @Inject constructor( } } - private suspend fun fetchFeed(feedId: String) { + suspend fun fetchFeed(feedId: String) { val feed = rssService.get().findFeedById(feedId) _feedOptionUiState.update { it.copy( @@ -59,17 +60,6 @@ class FeedOptionViewModel @Inject constructor( } } - fun showDrawer(scope: CoroutineScope, feedId: String) { - scope.launch { - fetchFeed(feedId) - _feedOptionUiState.value.drawerState.show() - } - } - - fun hideDrawer(scope: CoroutineScope) { - scope.launch { _feedOptionUiState.value.drawerState.hide() } - } - fun showNewGroupDialog() { _feedOptionUiState.update { it.copy( @@ -95,9 +85,12 @@ class FeedOptionViewModel @Inject constructor( fun addNewGroup() { if (_feedOptionUiState.value.newGroupContent.isNotBlank()) { applicationScope.launch { - selectedGroup(rssService.get().addGroup( - destFeed = _feedOptionUiState.value.feed, - newGroupName = _feedOptionUiState.value.newGroupContent)) + selectedGroup( + rssService.get().addGroup( + destFeed = _feedOptionUiState.value.feed, + newGroupName = _feedOptionUiState.value.newGroupContent + ) + ) hideNewGroupDialog() } } @@ -244,9 +237,7 @@ class FeedOptionViewModel @Inject constructor( } } -@OptIn(ExperimentalMaterialApi::class) data class FeedOptionUiState( - var drawerState: ModalBottomSheetState = ModalBottomSheetState(ModalBottomSheetValue.Hidden), val feed: Feed? = null, val selectedGroupId: String = "", val newGroupContent: String = "", diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/group/AllAllowNotificationDialog.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/group/AllAllowNotificationDialog.kt index afe10688..2280be16 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/group/AllAllowNotificationDialog.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/group/AllAllowNotificationDialog.kt @@ -19,6 +19,7 @@ import me.ash.reader.ui.ext.showToast fun AllAllowNotificationDialog( groupName: String, groupOptionViewModel: GroupOptionViewModel = hiltViewModel(), + onConfirm: () -> Unit, ) { val context = LocalContext.current val groupOptionUiState = groupOptionViewModel.groupOptionUiState.collectAsStateValue() @@ -48,7 +49,7 @@ fun AllAllowNotificationDialog( onClick = { groupOptionViewModel.allAllowNotification(true) { groupOptionViewModel.hideAllAllowNotificationDialog() - groupOptionViewModel.hideDrawer(scope) + onConfirm() context.showToast(allowToastString) } } @@ -63,7 +64,7 @@ fun AllAllowNotificationDialog( onClick = { groupOptionViewModel.allAllowNotification(false) { groupOptionViewModel.hideAllAllowNotificationDialog() - groupOptionViewModel.hideDrawer(scope) + onConfirm() context.showToast(denyToastString) } } diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/group/AllMoveToGroupDialog.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/group/AllMoveToGroupDialog.kt index 35077741..f6956ed1 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/group/AllMoveToGroupDialog.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/group/AllMoveToGroupDialog.kt @@ -2,7 +2,6 @@ package me.ash.reader.ui.page.home.feeds.drawer.group import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.DriveFileMove -import androidx.compose.material.icons.outlined.DriveFileMove import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -20,6 +19,7 @@ import me.ash.reader.ui.ext.showToast fun AllMoveToGroupDialog( groupName: String, groupOptionViewModel: GroupOptionViewModel = hiltViewModel(), + onConfirm: () -> Unit, ) { val context = LocalContext.current val groupOptionUiState = groupOptionViewModel.groupOptionUiState.collectAsStateValue() @@ -57,7 +57,7 @@ fun AllMoveToGroupDialog( onClick = { groupOptionViewModel.allMoveToGroup { groupOptionViewModel.hideAllMoveToGroupDialog() - groupOptionViewModel.hideDrawer(scope) + onConfirm() context.showToast(toastString) } } diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/group/AllParseFullContentDialog.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/group/AllParseFullContentDialog.kt index efeccd94..0a118152 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/group/AllParseFullContentDialog.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/group/AllParseFullContentDialog.kt @@ -2,7 +2,6 @@ package me.ash.reader.ui.page.home.feeds.drawer.group import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.Article -import androidx.compose.material.icons.outlined.Article import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -20,6 +19,7 @@ import me.ash.reader.ui.ext.showToast fun AllParseFullContentDialog( groupName: String, groupOptionViewModel: GroupOptionViewModel = hiltViewModel(), + onConfirm: () -> Unit, ) { val context = LocalContext.current val groupOptionUiState = groupOptionViewModel.groupOptionUiState.collectAsStateValue() @@ -49,7 +49,7 @@ fun AllParseFullContentDialog( onClick = { groupOptionViewModel.allParseFullContent(true) { groupOptionViewModel.hideAllParseFullContentDialog() - groupOptionViewModel.hideDrawer(scope) + onConfirm() context.showToast(allowToastString) } } @@ -64,7 +64,7 @@ fun AllParseFullContentDialog( onClick = { groupOptionViewModel.allParseFullContent(false) { groupOptionViewModel.hideAllParseFullContentDialog() - groupOptionViewModel.hideDrawer(scope) + onConfirm() context.showToast(denyToastString) } } diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/group/ClearGroupDialog.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/group/ClearGroupDialog.kt index d21c7497..15d9e967 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/group/ClearGroupDialog.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/group/ClearGroupDialog.kt @@ -19,6 +19,7 @@ import me.ash.reader.ui.ext.showToast fun ClearGroupDialog( groupName: String, groupOptionViewModel: GroupOptionViewModel = hiltViewModel(), + onConfirm: () -> Unit, ) { val context = LocalContext.current val groupOptionUiState = groupOptionViewModel.groupOptionUiState.collectAsStateValue() @@ -47,7 +48,7 @@ fun ClearGroupDialog( onClick = { groupOptionViewModel.clear { groupOptionViewModel.hideClearDialog() - groupOptionViewModel.hideDrawer(scope) + onConfirm() context.showToast(toastString) } } diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/group/DeleteGroupDialog.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/group/DeleteGroupDialog.kt index 9809fec5..02b1c7ee 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/group/DeleteGroupDialog.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/group/DeleteGroupDialog.kt @@ -19,6 +19,7 @@ import me.ash.reader.ui.ext.showToast fun DeleteGroupDialog( groupName: String, groupOptionViewModel: GroupOptionViewModel = hiltViewModel(), + onConfirm: () -> Unit, ) { val context = LocalContext.current val groupOptionUiState = groupOptionViewModel.groupOptionUiState.collectAsStateValue() @@ -47,7 +48,7 @@ fun DeleteGroupDialog( onClick = { groupOptionViewModel.delete { groupOptionViewModel.hideDeleteDialog() - groupOptionViewModel.hideDrawer(scope) + onConfirm() context.showToast(toastString) } } diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/group/GroupOptionDrawer.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/group/GroupOptionDrawer.kt index ad7ecbeb..7e4493ee 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/group/GroupOptionDrawer.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/group/GroupOptionDrawer.kt @@ -11,7 +11,6 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.Article -import androidx.compose.material.icons.outlined.Article import androidx.compose.material.icons.outlined.Folder import androidx.compose.material.icons.outlined.Notifications import androidx.compose.material3.Icon @@ -28,6 +27,9 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.compose.foundation.layout.FlowRow +import androidx.compose.material.ModalBottomSheetState +import androidx.compose.material.ModalBottomSheetValue +import androidx.compose.material.rememberModalBottomSheetState import kotlinx.coroutines.launch import me.ash.reader.R import me.ash.reader.domain.model.group.Group @@ -37,9 +39,9 @@ import me.ash.reader.ui.component.base.RYSelectionChip import me.ash.reader.ui.component.base.Subtitle import me.ash.reader.ui.ext.* -@OptIn(ExperimentalMaterialApi::class) @Composable fun GroupOptionDrawer( + drawerState: ModalBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden), viewModel: GroupOptionViewModel = hiltViewModel(), content: @Composable () -> Unit = {}, ) { @@ -49,14 +51,14 @@ fun GroupOptionDrawer( val group = groupOptionUiState.group val toastString = stringResource(R.string.rename_toast, groupOptionUiState.newName) - BackHandler(groupOptionUiState.drawerState.isVisible) { + BackHandler(drawerState.isVisible) { scope.launch { - groupOptionUiState.drawerState.hide() + drawerState.hide() } } BottomDrawer( - drawerState = groupOptionUiState.drawerState, + drawerState = drawerState, sheetContent = { Column(modifier = Modifier.navigationBarsPadding()) { Column( @@ -125,11 +127,21 @@ fun GroupOptionDrawer( content() } - ClearGroupDialog(groupName = group?.name ?: "") - DeleteGroupDialog(groupName = group?.name ?: "") - AllAllowNotificationDialog(groupName = group?.name ?: "") - AllParseFullContentDialog(groupName = group?.name ?: "") - AllMoveToGroupDialog(groupName = group?.name ?: "") + ClearGroupDialog( + groupName = group?.name ?: "", + onConfirm = { scope.launch { drawerState.hide() } }) + DeleteGroupDialog( + groupName = group?.name ?: "", + onConfirm = { scope.launch { drawerState.hide() } }) + AllAllowNotificationDialog( + groupName = group?.name ?: "", + onConfirm = { scope.launch { drawerState.hide() } }) + AllParseFullContentDialog( + groupName = group?.name ?: "", + onConfirm = { scope.launch { drawerState.hide() } }) + AllMoveToGroupDialog( + groupName = group?.name ?: "", + onConfirm = { scope.launch { drawerState.hide() } }) RenameDialog( visible = groupOptionUiState.renameDialogVisible, value = groupOptionUiState.newName, @@ -141,7 +153,7 @@ fun GroupOptionDrawer( }, onConfirm = { viewModel.rename() - viewModel.hideDrawer(scope) + scope.launch { drawerState.hide() } context.showToast(toastString) } ) diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/group/GroupOptionViewModel.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/group/GroupOptionViewModel.kt index 1ada59df..1f9bbfdb 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/group/GroupOptionViewModel.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/group/GroupOptionViewModel.kt @@ -8,6 +8,7 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -44,16 +45,15 @@ class GroupOptionViewModel @Inject constructor( } } - fun showDrawer(scope: CoroutineScope, groupId: String) { - scope.launch { - _groupOptionUiState.update { it.copy(group = rssService.get().findGroupById(groupId)) } - _groupOptionUiState.value.drawerState.show() + fun fetchGroup(groupId: String) { + viewModelScope.launch(Dispatchers.IO) { + val group = rssService.get().findGroupById(groupId) + withContext(Dispatchers.Main) { + _groupOptionUiState.update { it.copy(group = group) } + } } } - fun hideDrawer(scope: CoroutineScope) { - scope.launch { _groupOptionUiState.value.drawerState.hide() } - } fun allAllowNotification(isNotification: Boolean, callback: () -> Unit = {}) { _groupOptionUiState.value.group?.let { @@ -196,7 +196,6 @@ class GroupOptionViewModel @Inject constructor( @OptIn(ExperimentalMaterialApi::class) data class GroupOptionUiState( - var drawerState: ModalBottomSheetState = ModalBottomSheetState(ModalBottomSheetValue.Hidden), val group: Group? = null, val targetGroup: Group? = null, val groups: List = emptyList(), diff --git a/app/src/main/java/me/ash/reader/ui/page/home/flow/ArticleItem.kt b/app/src/main/java/me/ash/reader/ui/page/home/flow/ArticleItem.kt index f7d97d38..4f12a582 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/flow/ArticleItem.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/flow/ArticleItem.kt @@ -1,9 +1,12 @@ package me.ash.reader.ui.page.home.flow +import android.util.Log import android.view.HapticFeedbackConstants import androidx.compose.animation.Animatable import androidx.compose.animation.core.AnimationSpec import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.exponentialDecay +import androidx.compose.animation.core.snap import androidx.compose.animation.core.spring import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background @@ -53,6 +56,8 @@ import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalDensity @@ -90,6 +95,10 @@ import me.ash.reader.ui.page.settings.color.flow.generateArticleWithFeedPreview import me.ash.reader.ui.theme.Shape20 import me.ash.reader.ui.theme.applyTextDirection import me.ash.reader.ui.theme.palette.onDark +import me.saket.swipe.SwipeAction +import me.saket.swipe.SwipeableActionsBox + +private const val TAG = "ArticleItem" @Composable fun ArticleItem( @@ -273,10 +282,13 @@ fun ArticleItem( } } -private const val PositionalThresholdFraction = 0.15f +private const val PositionalThresholdFraction = 0.4f private const val SwipeActionDelay = 300L -@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class) +@OptIn( + ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class, + ExperimentalFoundationApi::class +) @Composable fun SwipeableArticleItem( articleWithFeed: ArticleWithFeed, @@ -285,78 +297,16 @@ fun SwipeableArticleItem( onClick: (ArticleWithFeed) -> Unit = {}, isSwipeEnabled: () -> Boolean = { false }, isMenuEnabled: Boolean = true, - onToggleStarred: (ArticleWithFeed, Long) -> Unit = { _, _ -> }, - onToggleRead: (ArticleWithFeed, Long) -> Unit = { _, _ -> }, + onToggleStarred: (ArticleWithFeed) -> Unit = { }, + onToggleRead: (ArticleWithFeed) -> Unit = { }, onMarkAboveAsRead: ((ArticleWithFeed) -> Unit)? = null, onMarkBelowAsRead: ((ArticleWithFeed) -> Unit)? = null, onShare: ((ArticleWithFeed) -> Unit)? = null, ) { - val swipeToStartAction = LocalArticleListSwipeStartAction.current - val swipeToEndAction = LocalArticleListSwipeEndAction.current - val onSwipeEndToStart = when (swipeToStartAction) { - SwipeStartActionPreference.None -> null - SwipeStartActionPreference.ToggleRead -> onToggleRead - SwipeStartActionPreference.ToggleStarred -> onToggleStarred - } - val onSwipeStartToEnd = when (swipeToEndAction) { - SwipeEndActionPreference.None -> null - SwipeEndActionPreference.ToggleRead -> onToggleRead - SwipeEndActionPreference.ToggleStarred -> onToggleStarred - } - val density = LocalDensity.current - val confirmValueChange: (SwipeToDismissBoxValue) -> Boolean = { - when (it) { - SwipeToDismissBoxValue.StartToEnd -> { - onSwipeStartToEnd?.invoke(articleWithFeed, SwipeActionDelay) - swipeToEndAction == SwipeEndActionPreference.ToggleRead && isFilterUnread - } - - SwipeToDismissBoxValue.EndToStart -> { - onSwipeEndToStart?.invoke(articleWithFeed, SwipeActionDelay) - swipeToStartAction == SwipeStartActionPreference.ToggleRead && isFilterUnread - } - - SwipeToDismissBoxValue.Settled -> { - true - } - } - } - val positionalThreshold: (totalDistance: Float) -> Float = { - it * PositionalThresholdFraction - } - val velocityThreshold: () -> Float = { Float.POSITIVE_INFINITY } - val animationSpec: AnimationSpec = spring(stiffness = Spring.StiffnessMediumLow) - val swipeState = rememberSaveable( - articleWithFeed.article, saver = SwipeToDismissBoxState.Saver( - confirmValueChange = confirmValueChange, - density = density, - animationSpec = animationSpec, - velocityThreshold = velocityThreshold, - positionalThreshold = positionalThreshold - ) - ) { - SwipeToDismissBoxState( - initialValue = SwipeToDismissBoxValue.Settled, - density = density, - animationSpec = animationSpec, - confirmValueChange = confirmValueChange, - positionalThreshold = positionalThreshold, - velocityThreshold = velocityThreshold - ) - } val view = LocalView.current - var isThresholdPassed by remember(articleWithFeed) { mutableStateOf(false) } - - LaunchedEffect(swipeState.progress > PositionalThresholdFraction) { - if (swipeState.progress > PositionalThresholdFraction && swipeState.targetValue != SwipeToDismissBoxValue.Settled) { - isThresholdPassed = true - view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) - } else { - isThresholdPassed = false - } - } + val density = LocalDensity.current var expanded by remember { mutableStateOf(false) } @@ -371,104 +321,158 @@ fun SwipeableArticleItem( } var menuOffset by remember { mutableStateOf(Offset(0f, 0f)) } - SwipeToDismissBox( - state = swipeState, - enabled = !isSwipeEnabled(), - /*** create dismiss alert background box */ - backgroundContent = { - SwipeToDismissBoxBackgroundContent( - direction = swipeState.dismissDirection, - isActive = isThresholdPassed, - isStarred = articleWithFeed.article.isStarred, - isRead = !articleWithFeed.article.isUnread - ) - }, - /**** Dismiss Content */ - content = { - Box( - modifier = Modifier - .fillMaxSize() - .pointerInput(expanded) { - awaitEachGesture { - while (true) { - awaitFirstDown(requireUnconsumed = false).let { - menuOffset = it.position - } + SwipeActionBox( + articleWithFeed = articleWithFeed, + isRead = !articleWithFeed.article.isUnread, + isStarred = articleWithFeed.article.isStarred, + onToggleStarred = onToggleStarred, + onToggleRead = onToggleRead + ) { + Box( + modifier = Modifier + .fillMaxSize() + .pointerInput(expanded) { + awaitEachGesture { + while (true) { + awaitFirstDown(requireUnconsumed = false).let { + menuOffset = it.position } } } - .background( - MaterialTheme.colorScheme.surfaceColorAtElevation( - articleListTonalElevation.dp - ) onDark MaterialTheme.colorScheme.surface - ) - .wrapContentSize() - ) { - ArticleItem( - articleWithFeed = articleWithFeed, - onClick = onClick, - onLongClick = onLongClick + } + .background( + MaterialTheme.colorScheme.surfaceColorAtElevation( + articleListTonalElevation.dp + ) onDark MaterialTheme.colorScheme.surface ) - with(articleWithFeed.article) { - if (isMenuEnabled) { - AnimatedDropdownMenu( - modifier = Modifier.padding(12.dp), - expanded = expanded, - onDismissRequest = { expanded = false }, - offset = density.run { - DpOffset(menuOffset.x.toDp(), 0.dp) - }, - ) { - ArticleItemMenuContent( - articleWithFeed = articleWithFeed, - isStarred = isStarred, - isRead = !isUnread, - onToggleStarred = onToggleStarred, - onToggleRead = onToggleRead, - onMarkAboveAsRead = onMarkAboveAsRead, - onMarkBelowAsRead = onMarkBelowAsRead, - onShare = onShare - ) { expanded = false } - } + .wrapContentSize() + ) { + ArticleItem( + articleWithFeed = articleWithFeed, + onClick = onClick, + onLongClick = onLongClick + ) + with(articleWithFeed.article) { + if (isMenuEnabled) { + AnimatedDropdownMenu( + modifier = Modifier.padding(12.dp), + expanded = expanded, + onDismissRequest = { expanded = false }, + offset = density.run { + DpOffset(menuOffset.x.toDp(), 0.dp) + }, + ) { + ArticleItemMenuContent( + articleWithFeed = articleWithFeed, + isStarred = isStarred, + isRead = !isUnread, + onToggleStarred = onToggleStarred, + onToggleRead = onToggleRead, + onMarkAboveAsRead = onMarkAboveAsRead, + onMarkBelowAsRead = onMarkBelowAsRead, + onShare = onShare + ) { expanded = false } } } } - }, - /*** Set Direction to dismiss */ - enableDismissFromEndToStart = onSwipeEndToStart != null, - enableDismissFromStartToEnd = onSwipeStartToEnd != null - ) + } + } + + +} + +private enum class SwipeDirection { + StartToEnd, EndToStart } -@OptIn(ExperimentalMaterial3Api::class) @Composable -private fun RowScope.SwipeToDismissBoxBackgroundContent( - direction: SwipeToDismissBoxValue, - isActive: Boolean, +private fun SwipeActionBox( + modifier: Modifier = Modifier, + articleWithFeed: ArticleWithFeed, isStarred: Boolean, isRead: Boolean, + onToggleStarred: (ArticleWithFeed) -> Unit, + onToggleRead: (ArticleWithFeed) -> Unit, + content: @Composable () -> Unit ) { - val containerColor = MaterialTheme.colorScheme.surface - val containerColorElevated = MaterialTheme.colorScheme.tertiaryContainer - val backgroundColor = remember { Animatable(containerColor) } + val containerColor = MaterialTheme.colorScheme.tertiaryContainer - LaunchedEffect(isActive) { - backgroundColor.animateTo( - if (isActive) { - containerColorElevated - } else { - containerColor - } + val swipeToStartAction = LocalArticleListSwipeStartAction.current + val swipeToEndAction = LocalArticleListSwipeEndAction.current + + val onSwipeEndToStart = when (swipeToStartAction) { + SwipeStartActionPreference.None -> null + SwipeStartActionPreference.ToggleRead -> onToggleRead + SwipeStartActionPreference.ToggleStarred -> onToggleStarred + } + + val onSwipeStartToEnd = when (swipeToEndAction) { + SwipeEndActionPreference.None -> null + SwipeEndActionPreference.ToggleRead -> onToggleRead + SwipeEndActionPreference.ToggleStarred -> onToggleStarred + } + + if (onSwipeStartToEnd == null && onSwipeEndToStart == null) { + content() + return + } + + val startAction = + SwipeAction( + icon = { + swipeActionIcon( + direction = SwipeDirection.StartToEnd, + isStarred = isStarred, + isRead = isRead + )?.let { + Icon( + imageVector = it, + contentDescription = null, + tint = MaterialTheme.colorScheme.tertiary, + modifier = Modifier.padding(horizontal = 24.dp) + ) + } + }, + background = containerColor, + isUndo = false, + onSwipe = { onSwipeStartToEnd?.invoke(articleWithFeed) } ) - } - // FIXME: Remove this once SwipeToDismissBox has proper RTL support - val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl - val alignment = when (direction) { - SwipeToDismissBoxValue.StartToEnd -> if (isRtl) Alignment.CenterEnd else Alignment.CenterStart - SwipeToDismissBoxValue.EndToStart -> if (isRtl) Alignment.CenterStart else Alignment.CenterEnd - SwipeToDismissBoxValue.Settled -> Alignment.Center + val endAction = SwipeAction( + icon = { + swipeActionIcon( + direction = SwipeDirection.EndToStart, + isStarred = isStarred, + isRead = isRead + )?.let { + Icon( + imageVector = it, + contentDescription = null, + tint = MaterialTheme.colorScheme.tertiary, + modifier = Modifier.padding(horizontal = 24.dp) + ) + } + }, + background = containerColor, + isUndo = false, + onSwipe = { onSwipeEndToStart?.invoke(articleWithFeed) } + ) + + SwipeableActionsBox( + modifier = modifier, + startActions = listOf(startAction), + endActions = listOf(endAction), + backgroundUntilSwipeThreshold = MaterialTheme.colorScheme.surface, + ) { + content.invoke() } +} + +@Composable +private fun swipeActionIcon( + direction: SwipeDirection, isStarred: Boolean, + isRead: Boolean, +): ImageVector? { val swipeToStartAction = LocalArticleListSwipeStartAction.current val swipeToEndAction = LocalArticleListSwipeEndAction.current @@ -478,15 +482,9 @@ private fun RowScope.SwipeToDismissBoxBackgroundContent( val readImageVector = remember(isRead) { if (isRead) Icons.Outlined.Circle else Icons.Rounded.CheckCircleOutline } - val starText = - stringResource(if (isStarred) R.string.mark_as_unstar else R.string.mark_as_starred) - - val readText = - stringResource(if (isRead) R.string.mark_as_unread else R.string.mark_as_read) - - val imageVector = remember(direction) { + return remember(direction) { when (direction) { - SwipeToDismissBoxValue.StartToEnd -> { + SwipeDirection.StartToEnd -> { when (swipeToEndAction) { SwipeEndActionPreference.None -> null @@ -495,57 +493,48 @@ private fun RowScope.SwipeToDismissBoxBackgroundContent( } } - SwipeToDismissBoxValue.EndToStart -> { + SwipeDirection.EndToStart -> { when (swipeToStartAction) { SwipeStartActionPreference.None -> null SwipeStartActionPreference.ToggleRead -> readImageVector SwipeStartActionPreference.ToggleStarred -> starImageVector } } - - SwipeToDismissBoxValue.Settled -> null } } +} - val text = remember(direction) { +@Composable +private fun swipeActionText( + direction: SwipeDirection, isStarred: Boolean, + isRead: Boolean, +): String { + val swipeToStartAction = LocalArticleListSwipeStartAction.current + val swipeToEndAction = LocalArticleListSwipeEndAction.current + + val starText = + stringResource(if (isStarred) R.string.mark_as_unstar else R.string.mark_as_starred) + + val readText = + stringResource(if (isRead) R.string.mark_as_unread else R.string.mark_as_read) + + return remember(direction) { when (direction) { - SwipeToDismissBoxValue.StartToEnd -> { + SwipeDirection.StartToEnd -> { when (swipeToEndAction) { - SwipeEndActionPreference.None -> null + SwipeEndActionPreference.None -> "null" SwipeEndActionPreference.ToggleRead -> readText SwipeEndActionPreference.ToggleStarred -> starText } } - SwipeToDismissBoxValue.EndToStart -> { + SwipeDirection.EndToStart -> { when (swipeToStartAction) { - SwipeStartActionPreference.None -> null + SwipeStartActionPreference.None -> "null" SwipeStartActionPreference.ToggleRead -> readText SwipeStartActionPreference.ToggleStarred -> starText } } - - SwipeToDismissBoxValue.Settled -> null - } - } - - - Box( - modifier = Modifier - .fillMaxSize() - .drawBehind { drawRect(backgroundColor.value) }, - ) { - Column(modifier = Modifier.align(alignment = alignment)) { - imageVector?.let { - Icon( - imageVector = it, - contentDescription = text, - tint = MaterialTheme.colorScheme.tertiary, - modifier = Modifier - .align(Alignment.CenterHorizontally) - .padding(horizontal = 24.dp) - ) - } } } } @@ -557,8 +546,8 @@ fun ArticleItemMenuContent( iconSize: DpSize = DpSize(width = 20.dp, height = 20.dp), isStarred: Boolean = false, isRead: Boolean = false, - onToggleStarred: (ArticleWithFeed, Long) -> Unit = { _, _ -> }, - onToggleRead: (ArticleWithFeed, Long) -> Unit = { _, _ -> }, + onToggleStarred: (ArticleWithFeed) -> Unit = { }, + onToggleRead: (ArticleWithFeed) -> Unit = { }, onMarkAboveAsRead: ((ArticleWithFeed) -> Unit)? = null, onMarkBelowAsRead: ((ArticleWithFeed) -> Unit)? = null, onShare: ((ArticleWithFeed) -> Unit)? = null, @@ -577,7 +566,7 @@ fun ArticleItemMenuContent( stringResource(if (isRead) R.string.mark_as_unread else R.string.mark_as_read) DropdownMenuItem(text = { Text(text = readText) }, onClick = { - onToggleRead(articleWithFeed, 0) + onToggleRead(articleWithFeed) onItemClick?.invoke() }, leadingIcon = { Icon( @@ -589,7 +578,7 @@ fun ArticleItemMenuContent( DropdownMenuItem( text = { Text(text = starText) }, onClick = { - onToggleStarred(articleWithFeed, 0) + onToggleStarred(articleWithFeed) onItemClick?.invoke() }, leadingIcon = { diff --git a/app/src/main/java/me/ash/reader/ui/page/home/flow/ArticleList.kt b/app/src/main/java/me/ash/reader/ui/page/home/flow/ArticleList.kt index 18ff0376..c0b67877 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/flow/ArticleList.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/flow/ArticleList.kt @@ -23,8 +23,8 @@ fun LazyListScope.ArticleList( isSwipeEnabled: () -> Boolean = { false }, isMenuEnabled: Boolean = true, onClick: (ArticleWithFeed) -> Unit = {}, - onToggleStarred: (ArticleWithFeed, Long) -> Unit = { _, _ -> }, - onToggleRead: (ArticleWithFeed, Long) -> Unit = { _, _ -> }, + onToggleStarred: (ArticleWithFeed) -> Unit = { }, + onToggleRead: (ArticleWithFeed) -> Unit = { }, onMarkAboveAsRead: ((ArticleWithFeed) -> Unit)? = null, onMarkBelowAsRead: ((ArticleWithFeed) -> Unit)? = null, onShare: ((ArticleWithFeed) -> Unit)? = null, diff --git a/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowPage.kt b/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowPage.kt index 30223b09..a376385c 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowPage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowPage.kt @@ -105,25 +105,23 @@ fun FlowPage( onDispose { homeViewModel.syncWorkLiveData.removeObservers(owner) } } - val onToggleStarred: (ArticleWithFeed, Long) -> Unit = remember { - { article, delay -> + val onToggleStarred: (ArticleWithFeed) -> Unit = remember { + { article -> flowViewModel.updateStarredStatus( articleId = article.article.id, isStarred = !article.article.isStarred, - withDelay = delay ) } } - val onToggleRead: (ArticleWithFeed, Long) -> Unit = remember { - { article, delay -> + val onToggleRead: (ArticleWithFeed) -> Unit = remember { + { article -> flowViewModel.updateReadStatus( groupId = null, feedId = null, articleId = article.article.id, conditions = MarkAsReadConditions.All, isUnread = !article.article.isUnread, - withDelay = delay ) } } diff --git a/app/src/main/java/me/ash/reader/ui/page/home/flow/SwipeToDismissBox.kt b/app/src/main/java/me/ash/reader/ui/page/home/flow/SwipeToDismissBox.kt deleted file mode 100644 index 4eb74e4d..00000000 --- a/app/src/main/java/me/ash/reader/ui/page/home/flow/SwipeToDismissBox.kt +++ /dev/null @@ -1,471 +0,0 @@ -package me.ash.reader.ui.page.home.flow - -/* - * Copyright 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import androidx.annotation.FloatRange -import androidx.compose.animation.core.AnimationSpec -import androidx.compose.animation.core.spring -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.gestures.AnchoredDraggableState -import androidx.compose.foundation.gestures.DraggableAnchors -import androidx.compose.foundation.gestures.Orientation -import androidx.compose.foundation.gestures.anchoredDraggable -import androidx.compose.foundation.gestures.animateTo -import androidx.compose.foundation.gestures.snapTo -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.RowScope -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.SwipeToDismissBoxState.Companion.Saver -import androidx.compose.runtime.Composable -import androidx.compose.runtime.saveable.Saver -import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.Measurable -import androidx.compose.ui.layout.MeasureResult -import androidx.compose.ui.layout.MeasureScope -import androidx.compose.ui.node.LayoutModifierNode -import androidx.compose.ui.node.ModifierNodeElement -import androidx.compose.ui.platform.InspectorInfo -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.LocalLayoutDirection -import androidx.compose.ui.platform.debugInspectorInfo -import androidx.compose.ui.unit.Constraints -import androidx.compose.ui.unit.Density -import androidx.compose.ui.unit.LayoutDirection -import androidx.compose.ui.unit.dp -import kotlinx.coroutines.CancellationException -import kotlin.math.roundToInt - -/** - * The directions in which a [SwipeToDismissBox] can be dismissed. - */ -@ExperimentalMaterial3Api -enum class SwipeToDismissBoxValue { - /** - * Can be dismissed by swiping in the reading direction. - */ - StartToEnd, - - /** - * Can be dismissed by swiping in the reverse of the reading direction. - */ - EndToStart, - - /** - * Cannot currently be dismissed. - */ - Settled -} - -/** - * State of the [SwipeToDismissBox] composable. - * - * @param initialValue The initial value of the state. - * @param density The density that this state can use to convert values to and from dp. - * @param confirmValueChange Optional callback invoked to confirm or veto a pending state change. - * @param positionalThreshold The positional threshold to be used when calculating the target state - * while a swipe is in progress and when settling after the swipe ends. This is the distance from - * the start of a transition. It will be, depending on the direction of the interaction, added or - * subtracted from/to the origin offset. It should always be a positive value. - */ -@OptIn(ExperimentalFoundationApi::class) -@ExperimentalMaterial3Api -class SwipeToDismissBoxState( - initialValue: SwipeToDismissBoxValue, - internal val density: Density, - animationSpec: AnimationSpec = spring(), - confirmValueChange: (SwipeToDismissBoxValue) -> Boolean = { true }, - velocityThreshold: () -> Float = { with(density) { DismissThreshold.toPx() } }, - positionalThreshold: (totalDistance: Float) -> Float -) { - internal val anchoredDraggableState = AnchoredDraggableState( - initialValue = initialValue, - animationSpec = animationSpec, - confirmValueChange = confirmValueChange, - positionalThreshold = positionalThreshold, - velocityThreshold = velocityThreshold - ) - - internal val offset: Float get() = anchoredDraggableState.offset - - /** - * Require the current offset. - * - * @throws IllegalStateException If the offset has not been initialized yet - */ - fun requireOffset(): Float = anchoredDraggableState.requireOffset() - - /** - * The current state value of the [SwipeToDismissBoxState]. - */ - val currentValue: SwipeToDismissBoxValue get() = anchoredDraggableState.currentValue - - /** - * The target state. This is the closest state to the current offset (taking into account - * positional thresholds). If no interactions like animations or drags are in progress, this - * will be the current state. - */ - val targetValue: SwipeToDismissBoxValue get() = anchoredDraggableState.targetValue - - /** - * The fraction of the progress going from currentValue to targetValue, within [0f..1f] bounds. - */ - @get:FloatRange(from = 0.0, to = 1.0) - val progress: Float get() = anchoredDraggableState.progress - - /** - * The direction (if any) in which the composable has been or is being dismissed. - * - * Use this to change the background of the [SwipeToDismissBox] if you want different actions on each - * side. - */ - val dismissDirection: SwipeToDismissBoxValue - get() = if (offset == 0f || offset.isNaN()) - SwipeToDismissBoxValue.Settled - else if (offset > 0f) - SwipeToDismissBoxValue.StartToEnd else SwipeToDismissBoxValue.EndToStart - - /** - * Whether the component has been dismissed in the given [direction]. - * - * @param direction The dismiss direction. - */ - @Deprecated( - message = "DismissDirection is no longer used by SwipeToDismissBoxState. Please compare " + - "currentValue against SwipeToDismissValue instead.", - level = DeprecationLevel.HIDDEN - ) - @Suppress("DEPRECATION") - fun isDismissed(direction: DismissDirection): Boolean { - return currentValue == ( - if (direction == DismissDirection.StartToEnd) { - SwipeToDismissBoxValue.StartToEnd - } else { - SwipeToDismissBoxValue.EndToStart - } - ) - } - - /** - * Set the state without any animation and suspend until it's set - * - * @param targetValue The new target value - */ - suspend fun snapTo(targetValue: SwipeToDismissBoxValue) { - anchoredDraggableState.snapTo(targetValue) - } - - /** - * Reset the component to the default position with animation and suspend until it if fully - * reset or animation has been cancelled. This method will throw [CancellationException] if - * the animation is interrupted - * - * @return the reason the reset animation ended - */ - suspend fun reset() = anchoredDraggableState.animateTo( - targetValue = SwipeToDismissBoxValue.Settled - ) - - /** - * Dismiss the component in the given [direction], with an animation and suspend. This method - * will throw [CancellationException] if the animation is interrupted - * - * @param direction The dismiss direction. - */ - suspend fun dismiss(direction: SwipeToDismissBoxValue) { - anchoredDraggableState.animateTo(targetValue = direction) - } - - companion object { - - /** - * The default [Saver] implementation for [SwipeToDismissBoxState]. - */ - fun Saver( - confirmValueChange: (SwipeToDismissBoxValue) -> Boolean, - positionalThreshold: (totalDistance: Float) -> Float, - velocityThreshold: () -> Float, - animationSpec: AnimationSpec, - density: Density - ) = Saver( - save = { it.currentValue }, - restore = { - SwipeToDismissBoxState( - it, - density, - animationSpec, - confirmValueChange, - velocityThreshold, - positionalThreshold - ) - } - ) - } -} - -/** - * A composable that can be dismissed by swiping left or right. - * - * @sample androidx.compose.material3.samples.SwipeToDismissListItems - * - * @param state The state of this component. - * @param background A composable that is stacked behind the content and is exposed when the - * content is swiped. You can/should use the [state] to have different backgrounds on each side. - * @param dismissContent The content that can be dismissed. - * @param modifier Optional [Modifier] for this component. - * @param directions The set of directions in which the component can be dismissed. - */ -@Composable -@Deprecated( - level = DeprecationLevel.WARNING, - message = "Use SwipeToDismissBox instead", - replaceWith = - ReplaceWith( - "SwipeToDismissBox(state, background, modifier, " + - "enableDismissFromStartToEnd, enableDismissFromEndToStart, dismissContent)" - ) -) -@ExperimentalMaterial3Api -fun SwipeToDismiss( - state: SwipeToDismissBoxState, - background: @Composable RowScope.() -> Unit, - dismissContent: @Composable RowScope.() -> Unit, - modifier: Modifier = Modifier, - directions: Set = setOf( - SwipeToDismissBoxValue.EndToStart, - SwipeToDismissBoxValue.StartToEnd - ), -) = SwipeToDismissBox( - state = state, - backgroundContent = background, - modifier = modifier, - enableDismissFromStartToEnd = SwipeToDismissBoxValue.StartToEnd in directions, - enableDismissFromEndToStart = SwipeToDismissBoxValue.EndToStart in directions, - content = dismissContent -) - -/** - * A composable that can be dismissed by swiping left or right. - * - * @sample androidx.compose.material3.samples.SwipeToDismissListItems - * - * @param state The state of this component. - * @param backgroundContent A composable that is stacked behind the [content] and is exposed when the - * content is swiped. You can/should use the [state] to have different backgrounds on each side. - * @param modifier Optional [Modifier] for this component. - * @param enableDismissFromStartToEnd Whether SwipeToDismissBox can be dismissed from start to end. - * @param enableDismissFromEndToStart Whether SwipeToDismissBox can be dismissed from end to start. - * @param content The content that can be dismissed. - */ -@OptIn(ExperimentalFoundationApi::class) -@Composable -@ExperimentalMaterial3Api -fun SwipeToDismissBox( - state: SwipeToDismissBoxState, - backgroundContent: @Composable RowScope.() -> Unit, - modifier: Modifier = Modifier, - enabled: Boolean = true, - enableDismissFromStartToEnd: Boolean = true, - enableDismissFromEndToStart: Boolean = true, - content: @Composable RowScope.() -> Unit, -) { -// val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl - - Box( - modifier - .anchoredDraggable( - state = state.anchoredDraggableState, - orientation = Orientation.Horizontal, - enabled = enabled && state.currentValue == SwipeToDismissBoxValue.Settled, -// reverseDirection = isRtl, - ), - propagateMinConstraints = true - ) { - Row( - content = backgroundContent, - modifier = Modifier.matchParentSize() - ) - Row( - content = content, - modifier = Modifier.swipeToDismissBoxAnchors( - state, - enableDismissFromStartToEnd, - enableDismissFromEndToStart - ) - ) - } -} - -/** Contains default values for [SwipeToDismissBox] and [SwipeToDismissBoxState]. */ -@ExperimentalMaterial3Api -object SwipeToDismissBoxDefaults { - /** Default positional threshold of 56.dp for [SwipeToDismissBoxState]. */ - val positionalThreshold: (totalDistance: Float) -> Float - @Composable get() = with(LocalDensity.current) { - { 56.dp.toPx() } - } -} - -/** - * The directions in which a [SwipeToDismissBox] can be dismissed. - */ -@ExperimentalMaterial3Api -@Deprecated( - message = "Dismiss direction is no longer used by SwipeToDismissBoxState. Please use " + - "SwipeToDismissBoxValue instead.", - level = DeprecationLevel.WARNING -) -enum class DismissDirection { - /** - * Can be dismissed by swiping in the reading direction. - */ - StartToEnd, - - /** - * Can be dismissed by swiping in the reverse of the reading direction. - */ - EndToStart, -} - -/** - * Possible values of [SwipeToDismissBoxState]. - */ -@ExperimentalMaterial3Api -@Deprecated( - message = "DismissValue is no longer used by SwipeToDismissBoxState. Please use " + - "SwipeToDismissBoxValue instead.", - level = DeprecationLevel.WARNING -) -enum class DismissValue { - /** - * Indicates the component has not been dismissed yet. - */ - Default, - - /** - * Indicates the component has been dismissed in the reading direction. - */ - DismissedToEnd, - - /** - * Indicates the component has been dismissed in the reverse of the reading direction. - */ - DismissedToStart -} - -private val DismissThreshold = 125.dp - -@OptIn(ExperimentalMaterial3Api::class) -private fun Modifier.swipeToDismissBoxAnchors( - state: SwipeToDismissBoxState, - enableDismissFromStartToEnd: Boolean, - enableDismissFromEndToStart: Boolean -) = this then SwipeToDismissAnchorsElement( - state, - enableDismissFromStartToEnd, - enableDismissFromEndToStart -) - -@OptIn(ExperimentalMaterial3Api::class) -private class SwipeToDismissAnchorsElement( - private val state: SwipeToDismissBoxState, - private val enableDismissFromStartToEnd: Boolean, - private val enableDismissFromEndToStart: Boolean, -) : ModifierNodeElement() { - - override fun create() = SwipeToDismissAnchorsNode( - state, - enableDismissFromStartToEnd, - enableDismissFromEndToStart, - ) - - override fun update(node: SwipeToDismissAnchorsNode) { - node.state = state - node.enableDismissFromStartToEnd = enableDismissFromStartToEnd - node.enableDismissFromEndToStart = enableDismissFromEndToStart - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - other as SwipeToDismissAnchorsElement - if (state != other.state) return false - if (enableDismissFromStartToEnd != other.enableDismissFromStartToEnd) return false - if (enableDismissFromEndToStart != other.enableDismissFromEndToStart) return false - return true - } - - override fun hashCode(): Int { - var result = state.hashCode() - result = 31 * result + enableDismissFromStartToEnd.hashCode() - result = 31 * result + enableDismissFromEndToStart.hashCode() - return result - } - - override fun InspectorInfo.inspectableProperties() { - debugInspectorInfo { - properties["state"] = state - properties["enableDismissFromStartToEnd"] = enableDismissFromStartToEnd - properties["enableDismissFromEndToStart"] = enableDismissFromEndToStart - } - } -} - -@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) -private class SwipeToDismissAnchorsNode( - var state: SwipeToDismissBoxState, - var enableDismissFromStartToEnd: Boolean, - var enableDismissFromEndToStart: Boolean, -) : Modifier.Node(), LayoutModifierNode { - private var didLookahead: Boolean = false - - override fun onDetach() { - didLookahead = false - } - - override fun MeasureScope.measure( - measurable: Measurable, - constraints: Constraints - ): MeasureResult { - val placeable = measurable.measure(constraints) - // If we are in a lookahead pass, we only want to update the anchors here and not in - // post-lookahead. If there is no lookahead happening (!isLookingAhead && !didLookahead), - // update the anchors in the main pass. - if (isLookingAhead || !didLookahead) { - val width = placeable.width.toFloat() - val newAnchors = DraggableAnchors { - SwipeToDismissBoxValue.Settled at 0f - if (enableDismissFromStartToEnd) { - SwipeToDismissBoxValue.StartToEnd at width - } - if (enableDismissFromEndToStart) { - SwipeToDismissBoxValue.EndToStart at -width - } - } - state.anchoredDraggableState.updateAnchors(newAnchors) - } - didLookahead = isLookingAhead || didLookahead - return layout(placeable.width, placeable.height) { - // In a lookahead pass, we use the position of the current target as this is where any - // ongoing animations would move. If SwipeToDismissBox is in a settled state, lookahead - // and post-lookahead will converge. - val xOffset = if (isLookingAhead) { - state.anchoredDraggableState.anchors.positionOf(state.targetValue) - } else state.requireOffset() - placeable.place(xOffset.roundToInt(), 0) - } - } -} diff --git a/app/src/main/java/me/ash/reader/ui/page/home/reading/drawer/FeedOptionDrawer.kt b/app/src/main/java/me/ash/reader/ui/page/home/reading/drawer/FeedOptionDrawer.kt index a4b3470a..7de86ccd 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/reading/drawer/FeedOptionDrawer.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/reading/drawer/FeedOptionDrawer.kt @@ -5,7 +5,11 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.material.BottomDrawerState import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.ModalBottomSheetState +import androidx.compose.material.ModalBottomSheetValue +import androidx.compose.material.rememberModalBottomSheetState import androidx.compose.material3.Tab import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope @@ -25,6 +29,7 @@ import me.ash.reader.ui.page.home.feeds.drawer.feed.FeedOptionViewModel @OptIn(ExperimentalMaterialApi::class) @Composable fun StyleOptionDrawer( + drawerState: ModalBottomSheetState, feedOptionViewModel: FeedOptionViewModel = hiltViewModel(), content: @Composable () -> Unit = {}, ) { @@ -35,14 +40,14 @@ fun StyleOptionDrawer( val feed = feedOptionUiState.feed val toastString = stringResource(R.string.rename_toast, feedOptionUiState.newName) - BackHandler(feedOptionUiState.drawerState.isVisible) { + BackHandler(drawerState.isVisible) { scope.launch { - feedOptionUiState.drawerState.hide() + drawerState.hide() } } BottomDrawer( - drawerState = feedOptionUiState.drawerState, + drawerState = drawerState, sheetContent = { Info() } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f2a74521..ca38138c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -27,7 +27,7 @@ room = "2.6.1" work = "2.9.0" # Compose -composeBom = "2024.05.00" +composeBom = "2024.09.02" composeCompiler = "1.5.8" composeHtml = "1.0.2" @@ -53,6 +53,7 @@ readability4j = "1.0.8" retrofit2 = "2.9.0" rome = "1.18.0" telephoto = "0.7.1" +swipe = "1.3.0" [libraries] # AboutLibraries @@ -94,6 +95,7 @@ opml-parser = { group = "be.ceau", name = "opml-parser", version.ref = "opmlPars readability4j = { group = "net.dankito.readability4j", name = "readability4j", version.ref = "readability4j" } rome = { group = "com.rometools", name = "rome", version.ref = "rome" } telephoto = { group = "me.saket.telephoto", name = "zoomable", version.ref = "telephoto" } +swipe = { group = "me.saket.swipe", name = "swipe", version.ref = "swipe" } okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } okhttp-coroutines = { group = "com.squareup.okhttp3", name = "okhttp-coroutines-jvm", version.ref = "okhttp" } retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit2" } From 438cee569421bba1455c1786725f7356821f3c74 Mon Sep 17 00:00:00 2001 From: junkfood <69683722+JunkFood02@users.noreply.github.com> Date: Mon, 30 Sep 2024 01:02:57 +0900 Subject: [PATCH 2/4] fix(ui): webview renderer nested scrolling (#847) --- .../reader/ui/page/home/reading/Content.kt | 116 +++++++++++------- .../reader/ui/page/home/reading/PullToLoad.kt | 4 +- 2 files changed, 77 insertions(+), 43 deletions(-) diff --git a/app/src/main/java/me/ash/reader/ui/page/home/reading/Content.kt b/app/src/main/java/me/ash/reader/ui/page/home/reading/Content.kt index 1c75b56f..54d45331 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/reading/Content.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/reading/Content.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding @@ -11,13 +12,17 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.windowInsetsBottomHeight import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.selection.DisableSelection import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.rememberNestedScrollInteropConnection import androidx.compose.ui.unit.dp import me.ash.reader.infrastructure.preference.LocalOpenLink import me.ash.reader.infrastructure.preference.LocalOpenLinkSpecificBrowser @@ -60,47 +65,74 @@ fun Content( } } else { - SelectionContainer { - LazyColumn( - modifier = modifier - .fillMaxSize() - .drawVerticalScrollbar(listState), - state = listState, - ) { - item { - // Top bar height - Spacer(modifier = Modifier.height(64.dp)) - // padding - Spacer(modifier = Modifier.height(22.dp)) - Column( - modifier = Modifier - .padding(horizontal = 12.dp) - ) { - DisableSelection { - Metadata( - feedName = feedName, - title = title, - author = author, - link = link, - publishedDate = publishedDate, - ) + when (renderer) { + ReadingRendererPreference.WebView -> { + Column(modifier = modifier) { + Column(modifier = Modifier.verticalScroll(rememberScrollState())) { + // Top bar height + Spacer(modifier = Modifier.height(64.dp)) + // padding + Spacer(modifier = Modifier.height(22.dp)) + Column( + modifier = Modifier + .padding(horizontal = 12.dp) + ) { + DisableSelection { + Metadata( + feedName = feedName, + title = title, + author = author, + link = link, + publishedDate = publishedDate, + ) + } } + Spacer(modifier = Modifier.height(22.dp)) + + RYWebView( + modifier = Modifier.fillMaxWidth(), + content = content, + refererDomain = link.extractDomain(), + onImageClick = onImageClick, + ) + Spacer(modifier = Modifier.height(128.dp)) + Spacer(modifier = Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars)) + } - Spacer(modifier = Modifier.height(22.dp)) } - when (renderer) { - ReadingRendererPreference.WebView -> { - item { - RYWebView( - content = content, - refererDomain = link.extractDomain(), - onImageClick = onImageClick, - ) - } - } + } + + ReadingRendererPreference.NativeComponent -> { + SelectionContainer { + LazyColumn( + modifier = modifier + .fillMaxSize() + .drawVerticalScrollbar(listState), + state = listState, + ) { + item { + // Top bar height + Spacer(modifier = Modifier.height(64.dp)) + // padding + Spacer(modifier = Modifier.height(22.dp)) + Column( + modifier = Modifier + .padding(horizontal = 12.dp) + ) { + DisableSelection { + Metadata( + feedName = feedName, + title = title, + author = author, + link = link, + publishedDate = publishedDate, + ) + } + } + Spacer(modifier = Modifier.height(22.dp)) + } - ReadingRendererPreference.NativeComponent -> { Reader( context = context, subheadUpperCase = subheadUpperCase.value, @@ -111,14 +143,16 @@ fun Content( context.openURL(it, openLink, openLinkSpecificBrowser) } ) - } - } - item { - Spacer(modifier = Modifier.height(128.dp)) - Spacer(modifier = Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars)) + item { + Spacer(modifier = Modifier.height(128.dp)) + Spacer(modifier = Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars)) + } + } } } } + + } } diff --git a/app/src/main/java/me/ash/reader/ui/page/home/reading/PullToLoad.kt b/app/src/main/java/me/ash/reader/ui/page/home/reading/PullToLoad.kt index 261ea549..1bd54659 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/reading/PullToLoad.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/reading/PullToLoad.kt @@ -65,7 +65,7 @@ private class ReaderNestedScrollConnection( !enabled || available.y == 0f -> Offset.Zero // Scroll down to reduce the progress when the offset is currently pulled up, same for the opposite - source == Drag -> { + source == NestedScrollSource.UserInput -> { Offset(0f, onPreScroll(available.y)) } @@ -78,7 +78,7 @@ private class ReaderNestedScrollConnection( consumed: Offset, available: Offset, source: NestedScrollSource ): Offset = when { !enabled -> Offset.Zero - source == Drag -> Offset(0f, onPostScroll(available.y)) // Pull to load + source == NestedScrollSource.UserInput -> Offset(0f, onPostScroll(available.y)) // Pull to load else -> Offset.Zero } From 006396c31071fc3ac71acc2dfd2775e1db293429 Mon Sep 17 00:00:00 2001 From: junkfood <69683722+JunkFood02@users.noreply.github.com> Date: Mon, 30 Sep 2024 00:53:10 +0800 Subject: [PATCH 3/4] fix(ui): egde to edge --- .../reader/ui/page/home/reading/Content.kt | 75 ++++++++++--------- .../ui/page/home/reading/ReadingPage.kt | 2 +- 2 files changed, 41 insertions(+), 36 deletions(-) diff --git a/app/src/main/java/me/ash/reader/ui/page/home/reading/Content.kt b/app/src/main/java/me/ash/reader/ui/page/home/reading/Content.kt index 54d45331..f7ba366e 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/reading/Content.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/reading/Content.kt @@ -1,8 +1,11 @@ package me.ash.reader.ui.page.home.reading import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -20,9 +23,8 @@ import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.rememberNestedScrollInteropConnection +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.dp import me.ash.reader.infrastructure.preference.LocalOpenLink import me.ash.reader.infrastructure.preference.LocalOpenLinkSpecificBrowser @@ -47,6 +49,7 @@ fun Content( publishedDate: Date, listState: LazyListState, isLoading: Boolean, + contentPadding: PaddingValues = PaddingValues(), onImageClick: ((imgUrl: String, altText: String) -> Unit)? = null, ) { val context = LocalContext.current @@ -67,38 +70,41 @@ fun Content( when (renderer) { ReadingRendererPreference.WebView -> { - Column(modifier = modifier) { - Column(modifier = Modifier.verticalScroll(rememberScrollState())) { - // Top bar height - Spacer(modifier = Modifier.height(64.dp)) - // padding - Spacer(modifier = Modifier.height(22.dp)) - Column( - modifier = Modifier - .padding(horizontal = 12.dp) - ) { - DisableSelection { - Metadata( - feedName = feedName, - title = title, - author = author, - link = link, - publishedDate = publishedDate, - ) - } + Column( + modifier = modifier + .padding(top = contentPadding.calculateTopPadding()) + .fillMaxSize() + .verticalScroll(rememberScrollState()) + + ) { + // Top bar height + Spacer(modifier = Modifier.height(64.dp)) + // padding + Column( + modifier = Modifier + .padding(horizontal = 12.dp) + ) { + DisableSelection { + Metadata( + feedName = feedName, + title = title, + author = author, + link = link, + publishedDate = publishedDate, + ) } - Spacer(modifier = Modifier.height(22.dp)) - - RYWebView( - modifier = Modifier.fillMaxWidth(), - content = content, - refererDomain = link.extractDomain(), - onImageClick = onImageClick, - ) - Spacer(modifier = Modifier.height(128.dp)) - Spacer(modifier = Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars)) - } + + RYWebView( + modifier = Modifier.fillMaxSize(), + content = content, + refererDomain = link.extractDomain(), + onImageClick = onImageClick, + ) + Spacer(modifier = Modifier.height(128.dp)) + Spacer(modifier = Modifier.height(contentPadding.calculateBottomPadding())) + + } } @@ -115,7 +121,7 @@ fun Content( // Top bar height Spacer(modifier = Modifier.height(64.dp)) // padding - Spacer(modifier = Modifier.height(22.dp)) + Spacer(modifier = Modifier.height(contentPadding.calculateTopPadding())) Column( modifier = Modifier .padding(horizontal = 12.dp) @@ -130,7 +136,6 @@ fun Content( ) } } - Spacer(modifier = Modifier.height(22.dp)) } Reader( @@ -146,7 +151,7 @@ fun Content( item { Spacer(modifier = Modifier.height(128.dp)) - Spacer(modifier = Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars)) + Spacer(modifier = Modifier.height(contentPadding.calculateBottomPadding())) } } } diff --git a/app/src/main/java/me/ash/reader/ui/page/home/reading/ReadingPage.kt b/app/src/main/java/me/ash/reader/ui/page/home/reading/ReadingPage.kt index 5ac4040b..72419bce 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/reading/ReadingPage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/reading/ReadingPage.kt @@ -177,7 +177,6 @@ fun ReadingPage( ) { Content( modifier = Modifier - .padding(paddings) .pullToLoad( state = state, onScroll = { f -> @@ -186,6 +185,7 @@ fun ReadingPage( }, enabled = isPullToSwitchArticleEnabled ), + contentPadding = paddings, content = content.text ?: "", feedName = feedName, title = title.toString(), From 24087803b1211b269253238dd0f6cb6af1a577b4 Mon Sep 17 00:00:00 2001 From: Will McCall Date: Mon, 14 Oct 2024 11:57:22 +0100 Subject: [PATCH 4/4] Replace deprecated SwipeRefresh with recomended PullRefreshIndicator and implement pull to refresh for Feeds Page. (#850) * Flow Page: replace SwipeRefresh with PullRefreshIndicator. * Feed Page: use PullRefreshIndicator for pull down refresh. * SwipeRefresh: Removed due to deprecation. * Switch from MD2 PullRefresh to MD3 PullToRefresh --------- Co-authored-by: Will McCall <532b81611e7e@mail.fordibben.zone> --- .../reader/ui/component/base/SwipeRefresh.kt | 31 -- .../reader/ui/page/home/feeds/FeedsPage.kt | 298 +++++++++--------- .../ash/reader/ui/page/home/flow/FlowPage.kt | 28 +- 3 files changed, 165 insertions(+), 192 deletions(-) delete mode 100644 app/src/main/java/me/ash/reader/ui/component/base/SwipeRefresh.kt diff --git a/app/src/main/java/me/ash/reader/ui/component/base/SwipeRefresh.kt b/app/src/main/java/me/ash/reader/ui/component/base/SwipeRefresh.kt deleted file mode 100644 index 5e2d311a..00000000 --- a/app/src/main/java/me/ash/reader/ui/component/base/SwipeRefresh.kt +++ /dev/null @@ -1,31 +0,0 @@ -package me.ash.reader.ui.component.base - -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import com.google.accompanist.swiperefresh.SwipeRefreshIndicator -import com.google.accompanist.swiperefresh.rememberSwipeRefreshState -import me.ash.reader.ui.theme.palette.onDark - -@Composable -fun SwipeRefresh( - isRefresh: Boolean = false, - onRefresh: () -> Unit = {}, - content: @Composable () -> Unit = {}, -) { - com.google.accompanist.swiperefresh.SwipeRefresh( - state = rememberSwipeRefreshState(isRefresh), - onRefresh = onRefresh, - indicator = { state, trigger -> - SwipeRefreshIndicator( - state = state, - refreshTriggerDistance = trigger, - fade = true, - scale = true, - contentColor = MaterialTheme.colorScheme.primary, - backgroundColor = MaterialTheme.colorScheme.surface onDark MaterialTheme.colorScheme.surfaceVariant, - ) - } - ) { - content() - } -} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsPage.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsPage.kt index a2a624a9..db6addbd 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsPage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsPage.kt @@ -1,16 +1,12 @@ package me.ash.reader.ui.page.home.feeds import androidx.activity.compose.BackHandler -import androidx.compose.animation.core.LinearEasing -import androidx.compose.animation.core.animateFloat -import androidx.compose.animation.core.infiniteRepeatable -import androidx.compose.animation.core.rememberInfiniteTransition -import androidx.compose.animation.core.tween import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBars @@ -26,13 +22,15 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.KeyboardArrowRight import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.rounded.Add -import androidx.compose.material.icons.rounded.Refresh import androidx.compose.material.icons.rounded.UnfoldLess import androidx.compose.material.icons.rounded.UnfoldMore import androidx.compose.material.rememberModalBottomSheetState +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text +import androidx.compose.material3.pulltorefresh.PullToRefreshBox +import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect @@ -47,7 +45,6 @@ import androidx.compose.runtime.snapshots.SnapshotStateMap import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.rotate import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.stringResource @@ -90,7 +87,7 @@ import kotlin.collections.set import kotlin.math.ln @OptIn( - androidx.compose.foundation.ExperimentalFoundationApi::class + ExperimentalMaterial3Api::class ) @Composable fun FeedsPage( @@ -130,7 +127,17 @@ fun FeedsPage( if (groupWithFeedList.isNotEmpty()) feedsUiState.listState else rememberLazyListState() val owner = LocalLifecycleOwner.current + var isSyncing by remember { mutableStateOf(false) } + val syncingState = rememberPullToRefreshState() + val syncingScope = rememberCoroutineScope() + val doSync:() -> Unit = { + isSyncing = true + syncingScope.launch { + + homeViewModel.sync() + } + } DisposableEffect(owner) { homeViewModel.syncWorkLiveData.observe(owner) { workInfoList -> @@ -141,15 +148,6 @@ fun FeedsPage( onDispose { homeViewModel.syncWorkLiveData.removeObservers(owner) } } - val infiniteTransition = rememberInfiniteTransition() - val angle by infiniteTransition.animateFloat( - initialValue = 0f, - targetValue = 360f, - animationSpec = infiniteRepeatable( - animation = tween(1000, easing = LinearEasing) - ) - ) - val feedBadgeAlpha by remember { derivedStateOf { (ln(groupListTonalElevation.value + 1.4f) + 2f) / 100f } } val groupAlpha by remember { derivedStateOf { groupListTonalElevation.value.dp.alphaLN(weight = 1.2f) } } val groupIndicatorAlpha by remember { @@ -221,15 +219,6 @@ fun FeedsPage( } }, actions = { - FeedbackIconButton( - modifier = Modifier.rotate(if (isSyncing) angle else 0f), - imageVector = Icons.Rounded.Refresh, - contentDescription = stringResource(R.string.refresh), - tint = MaterialTheme.colorScheme.onSurface, - enabled = !isSyncing - ) { - if (!isSyncing) homeViewModel.sync() - } if (subscribeViewModel.rssService.get().addSubscription) { FeedbackIconButton( imageVector = Icons.Rounded.Add, @@ -241,153 +230,160 @@ fun FeedsPage( } }, content = { - LazyColumn( - state = listState + PullToRefreshBox( + state=syncingState, + isRefreshing = isSyncing, + onRefresh = doSync ) { - item { - DisplayText( - text = feedsUiState.account?.name ?: "", - desc = if (isSyncing) stringResource(R.string.syncing) else "", - ) { accountTabVisible = true } - } - item { - Banner( - title = filterUiState.filter.toName(), - desc = importantSum, - icon = filterUiState.filter.iconOutline, - action = { - Icon( - imageVector = Icons.AutoMirrored.Outlined.KeyboardArrowRight, - contentDescription = stringResource(R.string.go_to), - ) - }, - ) { - filterChange( - navController = navController, - homeViewModel = homeViewModel, - filterState = filterUiState.copy( - group = null, - feed = null, - ) - ) + LazyColumn( + modifier=Modifier.fillMaxSize(), + state = listState + ) { + item { + DisplayText( + text = feedsUiState.account?.name ?: "", + desc = "", + ) { accountTabVisible = true } } - } - - item { - Spacer(modifier = Modifier.height(24.dp)) - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 26.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = stringResource(R.string.feeds), - color = MaterialTheme.colorScheme.primary, - style = MaterialTheme.typography.labelLarge, - ) - Row( - modifier = Modifier - .padding(end = 12.dp) - .size(20.dp) - .clip(CircleShape) - .clickable { if (hasGroupVisible) collapseAllGroups() else expandAllGroups() }, - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, + item { + Banner( + title = filterUiState.filter.toName(), + desc = importantSum, + icon = filterUiState.filter.iconOutline, + action = { + Icon( + imageVector = Icons.AutoMirrored.Outlined.KeyboardArrowRight, + contentDescription = stringResource(R.string.go_to), + ) + }, ) { - Icon( - imageVector = if (hasGroupVisible) Icons.Rounded.UnfoldLess else Icons.Rounded.UnfoldMore, - contentDescription = stringResource(R.string.unfold_less), - tint = MaterialTheme.colorScheme.primary, + filterChange( + navController = navController, + homeViewModel = homeViewModel, + filterState = filterUiState.copy( + group = null, + feed = null, + ) ) } } - Spacer(modifier = Modifier.height(8.dp)) - } - val defaultGroupId = context.currentAccountId.getDefaultGroupId() + item { + Spacer(modifier = Modifier.height(24.dp)) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 26.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(R.string.feeds), + color = MaterialTheme.colorScheme.primary, + style = MaterialTheme.typography.labelLarge, + ) + Row( + modifier = Modifier + .padding(end = 12.dp) + .size(20.dp) + .clip(CircleShape) + .clickable { if (hasGroupVisible) collapseAllGroups() else expandAllGroups() }, + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = if (hasGroupVisible) Icons.Rounded.UnfoldLess else Icons.Rounded.UnfoldMore, + contentDescription = stringResource(R.string.unfold_less), + tint = MaterialTheme.colorScheme.primary, + ) + } + } + Spacer(modifier = Modifier.height(8.dp)) + } - itemsIndexed(groupWithFeedList) { index, groupWithFeed -> - when (groupWithFeed) { - is GroupFeedsView.Group -> { - Spacer(modifier = Modifier.height(16.dp)) + val defaultGroupId = context.currentAccountId.getDefaultGroupId() - if (groupWithFeed.group.id != defaultGroupId || groupWithFeed.group.feeds > 0) { - GroupItem( - isExpanded = { - groupsVisible.getOrPut( - groupWithFeed.group.id, - groupListExpand::value - ) - }, - group = groupWithFeed.group, - alpha = groupAlpha, - indicatorAlpha = groupIndicatorAlpha, - roundedBottomCorner = { index == groupWithFeedList.lastIndex || groupWithFeed.group.feeds == 0 }, - onExpanded = { - groupsVisible[groupWithFeed.group.id] = + itemsIndexed(groupWithFeedList) { index, groupWithFeed -> + when (groupWithFeed) { + is GroupFeedsView.Group -> { + Spacer(modifier = Modifier.height(16.dp)) + + if (groupWithFeed.group.id != defaultGroupId || groupWithFeed.group.feeds > 0) { + GroupItem( + isExpanded = { groupsVisible.getOrPut( groupWithFeed.group.id, groupListExpand::value - ).not() - hasGroupVisible = - if (groupsVisible[groupWithFeed.group.id] == true) { - true - } else { - groupsVisible.any { it.value } + ) + }, + group = groupWithFeed.group, + alpha = groupAlpha, + indicatorAlpha = groupIndicatorAlpha, + roundedBottomCorner = { index == groupWithFeedList.lastIndex || groupWithFeed.group.feeds == 0 }, + onExpanded = { + groupsVisible[groupWithFeed.group.id] = + groupsVisible.getOrPut( + groupWithFeed.group.id, + groupListExpand::value + ).not() + hasGroupVisible = + if (groupsVisible[groupWithFeed.group.id] == true) { + true + } else { + groupsVisible.any { it.value } + } + }, + onLongClick = { + scope.launch { + groupDrawerState.show() } - }, - onLongClick = { - scope.launch { - groupDrawerState.show() } - } - ) { - filterChange( - navController = navController, - homeViewModel = homeViewModel, - filterState = filterUiState.copy( - group = groupWithFeed.group, - feed = null, + ) { + filterChange( + navController = navController, + homeViewModel = homeViewModel, + filterState = filterUiState.copy( + group = groupWithFeed.group, + feed = null, + ) ) - ) + } } } - } - is GroupFeedsView.Feed -> { - FeedItem( - feed = groupWithFeed.feed, - alpha = groupAlpha, - badgeAlpha = feedBadgeAlpha, - isEnded = { index == groupWithFeedList.lastIndex || groupWithFeedList[index + 1] is GroupFeedsView.Group }, - isExpanded = { - groupsVisible.getOrPut( - groupWithFeed.feed.groupId, - groupListExpand::value - ) - }, onClick = { - filterChange( - navController = navController, - homeViewModel = homeViewModel, - filterState = filterUiState.copy( - group = null, - feed = groupWithFeed.feed, + is GroupFeedsView.Feed -> { + FeedItem( + feed = groupWithFeed.feed, + alpha = groupAlpha, + badgeAlpha = feedBadgeAlpha, + isEnded = { index == groupWithFeedList.lastIndex || groupWithFeedList[index + 1] is GroupFeedsView.Group }, + isExpanded = { + groupsVisible.getOrPut( + groupWithFeed.feed.groupId, + groupListExpand::value ) - ) - }, onLongClick = { - scope.launch { - feedDrawerState.show() + }, onClick = { + filterChange( + navController = navController, + homeViewModel = homeViewModel, + filterState = filterUiState.copy( + group = null, + feed = groupWithFeed.feed, + ) + ) + }, onLongClick = { + scope.launch { + feedDrawerState.show() + } } - } - ) + ) + } } } - } - item { - Spacer(modifier = Modifier.height(128.dp)) - Spacer(modifier = Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars)) + item { + Spacer(modifier = Modifier.height(128.dp)) + Spacer(modifier = Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars)) + } } } }, diff --git a/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowPage.kt b/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowPage.kt index a376385c..fd93e09f 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowPage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowPage.kt @@ -14,7 +14,10 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.ArrowBack import androidx.compose.material.icons.rounded.DoneAll import androidx.compose.material.icons.rounded.Search +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.pulltorefresh.PullToRefreshBox +import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect @@ -55,13 +58,12 @@ import me.ash.reader.ui.component.base.DisplayText import me.ash.reader.ui.component.base.FeedbackIconButton import me.ash.reader.ui.component.base.RYExtensibleVisibility import me.ash.reader.ui.component.base.RYScaffold -import me.ash.reader.ui.component.base.SwipeRefresh import me.ash.reader.ui.ext.collectAsStateValue import me.ash.reader.ui.page.common.RouteName import me.ash.reader.ui.page.home.HomeViewModel @OptIn( - androidx.compose.ui.ExperimentalComposeUiApi::class, + ExperimentalMaterial3Api::class, ) @Composable fun FlowPage( @@ -94,7 +96,16 @@ fun FlowPage( var onSearch by remember { mutableStateOf(false) } val owner = LocalLifecycleOwner.current + var isSyncing by remember { mutableStateOf(false) } + val syncingState = rememberPullToRefreshState() + val syncingScope = rememberCoroutineScope() + val doSync: () -> Unit = { + isSyncing = true + syncingScope.launch { + flowViewModel.sync() + } + } DisposableEffect(owner) { homeViewModel.syncWorkLiveData.observe(owner) { workInfoList -> @@ -242,14 +253,11 @@ fun FlowPage( } }, content = { - SwipeRefresh( - onRefresh = { - if (!isSyncing) { - flowViewModel.sync() - } - } + PullToRefreshBox( + state = syncingState, + isRefreshing = isSyncing, + onRefresh = doSync ) { - var showMenu by remember { mutableStateOf(false) } LazyColumn( modifier = Modifier.fillMaxSize(), state = listState, @@ -262,7 +270,7 @@ fun FlowPage( filterUiState.feed != null -> filterUiState.feed.name else -> filterUiState.filter.toName() }, - desc = if (isSyncing) stringResource(R.string.syncing) else "", + desc = "", ) RYExtensibleVisibility(visible = markAsRead) { Spacer(modifier = Modifier.height((56 + 24 + 10).dp))