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..bed1c717 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,16 @@ 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 +46,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 +88,7 @@ import kotlin.collections.set import kotlin.math.ln @OptIn( - androidx.compose.foundation.ExperimentalFoundationApi::class + ExperimentalMaterial3Api::class ) @Composable fun FeedsPage( @@ -130,7 +128,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 +149,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 +220,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 +231,162 @@ fun FeedsPage( } }, content = { - LazyColumn( - state = listState - ) { - 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, - ) - ) - } - } - 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, + PullToRefreshBox( + state = syncingState, + isRefreshing = isSyncing, + onRefresh = doSync + ) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + state = listState + ) { + item { + DisplayText( + text = feedsUiState.account?.name ?: "", + desc = "", + ) { 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), + ) + }, ) { - 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/ArticleItem.kt b/app/src/main/java/me/ash/reader/ui/page/home/flow/ArticleItem.kt index c63c9ea0..2c4bfba1 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,6 +1,14 @@ 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 import androidx.compose.foundation.combinedClickable 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 b9dddb70..d86d8f7c 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 @@ -56,13 +59,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( @@ -97,7 +99,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 -> @@ -253,14 +264,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, @@ -273,7 +281,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)) 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..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,9 +1,13 @@ 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 import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding @@ -11,13 +15,16 @@ 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.platform.LocalContext +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 @@ -42,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 @@ -60,18 +68,18 @@ fun Content( } } else { - SelectionContainer { - LazyColumn( - modifier = modifier - .fillMaxSize() - .drawVerticalScrollbar(listState), - state = listState, - ) { - item { + when (renderer) { + ReadingRendererPreference.WebView -> { + Column( + modifier = modifier + .padding(top = contentPadding.calculateTopPadding()) + .fillMaxSize() + .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) @@ -86,21 +94,50 @@ fun Content( ) } } - Spacer(modifier = Modifier.height(22.dp)) + + RYWebView( + modifier = Modifier.fillMaxSize(), + content = content, + refererDomain = link.extractDomain(), + onImageClick = onImageClick, + ) + Spacer(modifier = Modifier.height(128.dp)) + Spacer(modifier = Modifier.height(contentPadding.calculateBottomPadding())) + + } - 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(contentPadding.calculateTopPadding())) + Column( + modifier = Modifier + .padding(horizontal = 12.dp) + ) { + DisableSelection { + Metadata( + feedName = feedName, + title = title, + author = author, + link = link, + publishedDate = publishedDate, + ) + } + } + } - ReadingRendererPreference.NativeComponent -> { Reader( context = context, subheadUpperCase = subheadUpperCase.value, @@ -111,14 +148,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.height(contentPadding.calculateBottomPadding())) + } + } } } } + + } } 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 } 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 73f73c27..ce228095 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 @@ -181,7 +181,6 @@ fun ReadingPage( ) { Content( modifier = Modifier - .padding(paddings) .pullToLoad( state = state, onScroll = { f -> @@ -190,6 +189,7 @@ fun ReadingPage( }, enabled = isPullToSwitchArticleEnabled ), + contentPadding = paddings, content = content.text ?: "", feedName = feedName, title = title.toString(),