ReadYou/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsPage.kt

370 lines
16 KiB
Kotlin

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.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.windowInsetsBottomHeight
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.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.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.runtime.snapshots.SnapshotStateMap
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import androidx.work.WorkInfo
import me.ash.reader.R
import me.ash.reader.infrastructure.preference.LocalFeedsFilterBarFilled
import me.ash.reader.infrastructure.preference.LocalFeedsFilterBarPadding
import me.ash.reader.infrastructure.preference.LocalFeedsFilterBarStyle
import me.ash.reader.infrastructure.preference.LocalFeedsFilterBarTonalElevation
import me.ash.reader.infrastructure.preference.LocalFeedsGroupListExpand
import me.ash.reader.infrastructure.preference.LocalFeedsGroupListTonalElevation
import me.ash.reader.infrastructure.preference.LocalFeedsTopBarTonalElevation
import me.ash.reader.infrastructure.preference.LocalNewVersionNumber
import me.ash.reader.infrastructure.preference.LocalSkipVersionNumber
import me.ash.reader.ui.component.FilterBar
import me.ash.reader.ui.component.base.Banner
import me.ash.reader.ui.component.base.DisplayText
import me.ash.reader.ui.component.base.FeedbackIconButton
import me.ash.reader.ui.component.base.RYScaffold
import me.ash.reader.ui.component.base.Subtitle
import me.ash.reader.ui.ext.alphaLN
import me.ash.reader.ui.ext.collectAsStateValue
import me.ash.reader.ui.ext.currentAccountId
import me.ash.reader.ui.ext.findActivity
import me.ash.reader.ui.ext.getCurrentVersion
import me.ash.reader.ui.page.common.RouteName
import me.ash.reader.ui.page.home.FilterState
import me.ash.reader.ui.page.home.HomeViewModel
import me.ash.reader.ui.page.home.feeds.accounts.AccountsTab
import me.ash.reader.ui.page.home.feeds.drawer.feed.FeedOptionDrawer
import me.ash.reader.ui.page.home.feeds.drawer.group.GroupOptionDrawer
import me.ash.reader.ui.page.home.feeds.subscribe.SubscribeDialog
import me.ash.reader.ui.page.home.feeds.subscribe.SubscribeViewModel
import me.ash.reader.ui.page.settings.accounts.AccountViewModel
import kotlin.collections.set
import kotlin.math.ln
@OptIn(
androidx.compose.foundation.ExperimentalFoundationApi::class
)
@Composable
fun FeedsPage(
navController: NavHostController,
accountViewModel: AccountViewModel = hiltViewModel(),
feedsViewModel: FeedsViewModel = hiltViewModel(),
subscribeViewModel: SubscribeViewModel = hiltViewModel(),
homeViewModel: HomeViewModel,
) {
var accountTabVisible by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
val context = LocalContext.current
val topBarTonalElevation = LocalFeedsTopBarTonalElevation.current
val groupListTonalElevation = LocalFeedsGroupListTonalElevation.current
val groupListExpand = LocalFeedsGroupListExpand.current
val filterBarStyle = LocalFeedsFilterBarStyle.current
val filterBarFilled = LocalFeedsFilterBarFilled.current
val filterBarPadding = LocalFeedsFilterBarPadding.current
val filterBarTonalElevation = LocalFeedsFilterBarTonalElevation.current
val accounts = accountViewModel.accounts.collectAsStateValue(initial = emptyList())
val feedsUiState = feedsViewModel.feedsUiState.collectAsStateValue()
val filterUiState = homeViewModel.filterUiState.collectAsStateValue()
val importantSum =
feedsUiState.importantSum.collectAsStateValue(initial = stringResource(R.string.loading))
val groupWithFeedList =
feedsUiState.groupWithFeedList.collectAsStateValue(initial = emptyList())
val groupsVisible: SnapshotStateMap<String, Boolean> = feedsUiState.groupsVisible
val newVersion = LocalNewVersionNumber.current
val skipVersion = LocalSkipVersionNumber.current
val currentVersion = remember { context.getCurrentVersion() }
val listState = if (groupWithFeedList.isNotEmpty()) feedsUiState.listState else rememberLazyListState()
val owner = LocalLifecycleOwner.current
var isSyncing by remember { mutableStateOf(false) }
DisposableEffect(owner) {
homeViewModel.syncWorkLiveData.observe(owner) { workInfoList ->
workInfoList.let {
isSyncing = it.any { workInfo -> workInfo.state == WorkInfo.State.RUNNING }
}
}
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 {
derivedStateOf {
groupListTonalElevation.value.dp.alphaLN(
weight = 1.4f
)
}
}
LaunchedEffect(Unit) {
feedsViewModel.fetchAccount()
}
LaunchedEffect(filterUiState, isSyncing) {
snapshotFlow { filterUiState }.collect {
feedsViewModel.pullFeeds(it)
}
}
BackHandler(true) {
context.findActivity()?.moveTaskToBack(false)
}
RYScaffold(
topBarTonalElevation = topBarTonalElevation.value.dp,
containerTonalElevation = groupListTonalElevation.value.dp,
navigationIcon = {
FeedbackIconButton(
modifier = Modifier.size(20.dp),
imageVector = Icons.Outlined.Settings,
contentDescription = stringResource(R.string.settings),
tint = MaterialTheme.colorScheme.onSurface,
showBadge = newVersion.whetherNeedUpdate(currentVersion, skipVersion),
) {
navController.navigate(RouteName.SETTINGS) {
launchSingleTop = true
}
}
},
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,
contentDescription = stringResource(R.string.subscribe),
tint = MaterialTheme.colorScheme.onSurface,
) {
subscribeViewModel.showDrawer()
}
}
},
content = {
LazyColumn (
state = listState
) {
item {
DisplayText(
modifier = Modifier.clickable {
accountTabVisible = true
},
text = feedsUiState.account?.name ?: "",
desc = if (isSyncing) stringResource(R.string.syncing) else "",
)
}
item {
Banner(
title = filterUiState.filter.toName(),
desc = importantSum,
icon = filterUiState.filter.iconOutline,
action = {
Icon(
imageVector = Icons.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))
Subtitle(
modifier = Modifier.padding(start = 26.dp),
text = stringResource(R.string.feeds)
)
Spacer(modifier = Modifier.height(8.dp))
}
itemsIndexed(groupWithFeedList) { index, groupWithFeed ->
when (groupWithFeed) {
is GroupFeedsView.Group -> {
if (index != 0) {
Spacer(modifier = Modifier.height(16.dp))
}
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] = groupsVisible.getOrPut(
groupWithFeed.group.id,
groupListExpand::value
).not()
}
) {
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
)
},
) {
filterChange(
navController = navController,
homeViewModel = homeViewModel,
filterState = filterUiState.copy(
group = null,
feed = groupWithFeed.feed,
)
)
}
}
}
}
item {
Spacer(modifier = Modifier.height(128.dp))
Spacer(modifier = Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars))
}
}
},
bottomBar = {
FilterBar(
filter = filterUiState.filter,
filterBarStyle = filterBarStyle.value,
filterBarFilled = filterBarFilled.value,
filterBarPadding = filterBarPadding.dp,
filterBarTonalElevation = filterBarTonalElevation.value.dp,
) {
filterChange(
navController = navController,
homeViewModel = homeViewModel,
filterState = filterUiState.copy(filter = it),
isNavigate = false,
)
}
}
)
SubscribeDialog(subscribeViewModel = subscribeViewModel)
GroupOptionDrawer()
FeedOptionDrawer()
AccountsTab(
visible = accountTabVisible,
accounts = accounts,
onAccountSwitch = {
accountViewModel.switchAccount(it) {
accountTabVisible = false
navController.navigate(RouteName.SETTINGS)
navController.navigate(RouteName.FEEDS) {
launchSingleTop = true
restoreState = true
}
}
},
onClickSettings = {
accountTabVisible = false
navController.navigate("${RouteName.ACCOUNT_DETAILS}/${context.currentAccountId}")
},
onClickManage = {
accountTabVisible = false
navController.navigate(RouteName.ACCOUNTS)
},
onDismissRequest = {
accountTabVisible = false
},
)
}
private fun filterChange(
navController: NavHostController,
homeViewModel: HomeViewModel,
filterState: FilterState,
isNavigate: Boolean = true,
) {
homeViewModel.changeFilter(filterState)
if (isNavigate) {
navController.navigate(RouteName.FLOW) {
launchSingleTop = true
}
}
}