Merge branch 'main' into adaptive

This commit is contained in:
junkfood 2024-10-14 19:16:42 +08:00
commit d3f6fa63aa
No known key found for this signature in database
GPG Key ID: 2EA5B648DB112A34
7 changed files with 245 additions and 222 deletions

View File

@ -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()
}
}

View File

@ -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))
}
}
}
},

View File

@ -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

View File

@ -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))

View File

@ -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()))
}
}
}
}
}
}
}

View File

@ -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
}

View File

@ -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(),