Separate ViewPager

This commit is contained in:
Ash 2022-04-25 10:49:06 +08:00
parent 1b56bff6ca
commit 64f81c696f
27 changed files with 318 additions and 450 deletions

View File

@ -1,18 +1,14 @@
package me.ash.reader.data.entity package me.ash.reader.data.entity
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.FiberManualRecord
import androidx.compose.material.icons.outlined.FiberManualRecord import androidx.compose.material.icons.outlined.FiberManualRecord
import androidx.compose.material.icons.rounded.Star
import androidx.compose.material.icons.rounded.StarOutline import androidx.compose.material.icons.rounded.StarOutline
import androidx.compose.material.icons.rounded.Subject import androidx.compose.material.icons.rounded.Subject
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
class Filter( class Filter(
var index: Int, var index: Int,
var important: Int,
var icon: ImageVector, var icon: ImageVector,
var filledIcon: ImageVector,
) { ) {
fun isStarred(): Boolean = this == Starred fun isStarred(): Boolean = this == Starred
fun isUnread(): Boolean = this == Unread fun isUnread(): Boolean = this == Unread
@ -21,21 +17,15 @@ class Filter(
companion object { companion object {
val Starred = Filter( val Starred = Filter(
index = 0, index = 0,
important = 666,
icon = Icons.Rounded.StarOutline, icon = Icons.Rounded.StarOutline,
filledIcon = Icons.Rounded.Star,
) )
val Unread = Filter( val Unread = Filter(
index = 1, index = 1,
important = 666,
icon = Icons.Outlined.FiberManualRecord, icon = Icons.Outlined.FiberManualRecord,
filledIcon = Icons.Filled.FiberManualRecord,
) )
val All = Filter( val All = Filter(
index = 2, index = 2,
important = 666,
icon = Icons.Rounded.Subject, icon = Icons.Rounded.Subject,
filledIcon = Icons.Rounded.Subject,
) )
} }
} }

View File

@ -10,6 +10,6 @@ class StringsRepository @Inject constructor(
@ApplicationContext @ApplicationContext
private val context: Context, private val context: Context,
) { ) {
fun getString(resId: Int) = context.getString(resId) fun getString(resId: Int, vararg formatArgs: Any) = context.getString(resId, *formatArgs)
fun formatAsString(date: Date?) = date?.formatAsString(context) fun formatAsString(date: Date?) = date?.formatAsString(context)
} }

View File

@ -10,6 +10,8 @@ package me.ash.reader.ui.component
import android.view.SoundEffectConstants import android.view.SoundEffectConstants
import androidx.compose.animation.Crossfade import androidx.compose.animation.Crossfade
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
@ -84,6 +86,7 @@ fun Banner(
) )
desc?.let { desc?.let {
Text( Text(
modifier = Modifier.animateContentSize(tween()),
text = it, text = it,
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = (MaterialTheme.colorScheme.onSurface alwaysLight true).copy(alpha = 0.7f), color = (MaterialTheme.colorScheme.onSurface alwaysLight true).copy(alpha = 0.7f),

View File

@ -1,6 +1,7 @@
package me.ash.reader.ui.component package me.ash.reader.ui.component
import androidx.compose.animation.* import androidx.compose.animation.*
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
@ -30,7 +31,9 @@ fun DisplayText(
) )
) { ) {
Text( Text(
modifier = Modifier.height(44.dp), modifier = Modifier
.height(44.dp)
.animateContentSize(tween()),
text = text, text = text,
style = MaterialTheme.typography.displaySmall.copy( style = MaterialTheme.typography.displaySmall.copy(
baselineShift = BaselineShift.Superscript baselineShift = BaselineShift.Superscript

View File

@ -70,7 +70,12 @@ fun WebView(
): Boolean { ): Boolean {
if (null == request?.url) return false if (null == request?.url) return false
val url = request.url.toString() val url = request.url.toString()
if (url.isNotEmpty()) context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) if (url.isNotEmpty()) context.startActivity(
Intent(
Intent.ACTION_VIEW,
Uri.parse(url)
)
)
return true return true
} }

View File

@ -11,10 +11,3 @@ fun Filter.getName(): String = when (this) {
Filter.Starred -> stringResource(R.string.starred) Filter.Starred -> stringResource(R.string.starred)
else -> stringResource(R.string.all) else -> stringResource(R.string.all)
} }
@Composable
fun Filter.getDesc(): String = when (this) {
Filter.Unread -> stringResource(R.string.unread_desc, this.important)
Filter.Starred -> stringResource(R.string.starred_desc, this.important)
else -> stringResource(R.string.all_desc, this.important)
}

View File

@ -3,16 +3,24 @@ package me.ash.reader.ui.page.common
import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.paging.compose.collectAsLazyPagingItems
import com.google.accompanist.navigation.animation.AnimatedNavHost import com.google.accompanist.navigation.animation.AnimatedNavHost
import com.google.accompanist.navigation.animation.rememberAnimatedNavController import com.google.accompanist.navigation.animation.rememberAnimatedNavController
import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.google.accompanist.systemuicontroller.rememberSystemUiController
import me.ash.reader.ui.ext.animatedComposable import me.ash.reader.ui.ext.animatedComposable
import me.ash.reader.ui.ext.collectAsStateValue
import me.ash.reader.ui.ext.findActivity
import me.ash.reader.ui.ext.isFirstLaunch import me.ash.reader.ui.ext.isFirstLaunch
import me.ash.reader.ui.page.home.HomePage import me.ash.reader.ui.page.home.HomeViewModel
import me.ash.reader.ui.page.home.feeds.FeedsPage
import me.ash.reader.ui.page.home.flow.FlowPage
import me.ash.reader.ui.page.home.read.ReadPage
import me.ash.reader.ui.page.settings.ColorAndStyle import me.ash.reader.ui.page.settings.ColorAndStyle
import me.ash.reader.ui.page.settings.SettingsPage import me.ash.reader.ui.page.settings.SettingsPage
import me.ash.reader.ui.page.settings.TipsAndSupport import me.ash.reader.ui.page.settings.TipsAndSupport
@ -22,12 +30,33 @@ import me.ash.reader.ui.theme.LocalUseDarkTheme
@OptIn(ExperimentalAnimationApi::class, androidx.compose.material.ExperimentalMaterialApi::class) @OptIn(ExperimentalAnimationApi::class, androidx.compose.material.ExperimentalMaterialApi::class)
@Composable @Composable
fun HomeEntry() { fun HomeEntry(
homeViewModel: HomeViewModel = hiltViewModel(),
) {
val viewState = homeViewModel.viewState.collectAsStateValue()
val pagingItems = viewState.pagingData.collectAsLazyPagingItems()
AppTheme { AppTheme {
val context = LocalContext.current val context = LocalContext.current
val useDarkTheme = LocalUseDarkTheme.current val useDarkTheme = LocalUseDarkTheme.current
val navController = rememberAnimatedNavController() val navController = rememberAnimatedNavController()
val intent by rememberSaveable { mutableStateOf(context.findActivity()?.intent) }
var openArticleId by rememberSaveable {
mutableStateOf(intent?.extras?.get(ExtraName.ARTICLE_ID)?.toString() ?: "")
}.also {
intent?.replaceExtras(null)
}
LaunchedEffect(openArticleId) {
if (openArticleId.isNotEmpty()) {
navController.navigate("${RouteName.READING}/${openArticleId}") {
popUpTo(RouteName.FEEDS)
}
openArticleId = ""
}
}
rememberSystemUiController().run { rememberSystemUiController().run {
setStatusBarColor(Color.Transparent, !useDarkTheme) setStatusBarColor(Color.Transparent, !useDarkTheme)
setSystemBarsColor(Color.Transparent, !useDarkTheme) setSystemBarsColor(Color.Transparent, !useDarkTheme)
@ -37,13 +66,23 @@ fun HomeEntry() {
AnimatedNavHost( AnimatedNavHost(
modifier = Modifier.background(MaterialTheme.colorScheme.surface), modifier = Modifier.background(MaterialTheme.colorScheme.surface),
navController = navController, navController = navController,
startDestination = if (context.isFirstLaunch) RouteName.STARTUP else RouteName.HOME, startDestination = if (context.isFirstLaunch) RouteName.STARTUP else RouteName.FEEDS,
) { ) {
animatedComposable(route = RouteName.STARTUP) { animatedComposable(route = RouteName.STARTUP) {
StartupPage(navController) StartupPage(navController)
} }
animatedComposable(route = RouteName.HOME) { animatedComposable(route = RouteName.FEEDS) {
HomePage(navController) FeedsPage(navController = navController, homeViewModel = homeViewModel)
}
animatedComposable(route = RouteName.FLOW) {
FlowPage(
navController = navController,
homeViewModel = homeViewModel,
pagingItems = pagingItems
)
}
animatedComposable(route = "${RouteName.READING}/{articleId}") {
ReadPage(navController = navController)
} }
animatedComposable(route = RouteName.SETTINGS) { animatedComposable(route = RouteName.SETTINGS) {
SettingsPage(navController) SettingsPage(navController)

View File

@ -2,10 +2,9 @@ package me.ash.reader.ui.page.common
object RouteName { object RouteName {
const val STARTUP = "startup" const val STARTUP = "startup"
const val HOME = "home" const val FEEDS = "feeds"
const val FEED = "feed" const val FLOW = "flow"
const val ARTICLE = "article" const val READING = "reading"
const val READ = "read"
const val SETTINGS = "settings" const val SETTINGS = "settings"
const val COLOR_AND_STYLE = "color_and_style" const val COLOR_AND_STYLE = "color_and_style"
const val TIPS_AND_SUPPORT = "tips_and_support" const val TIPS_AND_SUPPORT = "tips_and_support"

View File

@ -1,156 +0,0 @@
package me.ash.reader.ui.page.home
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import com.google.accompanist.pager.ExperimentalPagerApi
import me.ash.reader.ui.component.ViewPager
import me.ash.reader.ui.ext.collectAsStateValue
import me.ash.reader.ui.ext.findActivity
import me.ash.reader.ui.page.common.ExtraName
import me.ash.reader.ui.page.home.feeds.FeedsPage
import me.ash.reader.ui.page.home.feeds.option.feed.FeedOptionDrawer
import me.ash.reader.ui.page.home.feeds.option.feed.FeedOptionViewAction
import me.ash.reader.ui.page.home.feeds.option.feed.FeedOptionViewModel
import me.ash.reader.ui.page.home.feeds.option.group.GroupOptionDrawer
import me.ash.reader.ui.page.home.flow.FlowPage
import me.ash.reader.ui.page.home.read.ReadPage
import me.ash.reader.ui.page.home.read.ReadViewAction
import me.ash.reader.ui.page.home.read.ReadViewModel
@OptIn(ExperimentalPagerApi::class, androidx.compose.material.ExperimentalMaterialApi::class)
@Composable
fun HomePage(
navController: NavHostController,
homeViewModel: HomeViewModel = hiltViewModel(),
readViewModel: ReadViewModel = hiltViewModel(),
feedOptionViewModel: FeedOptionViewModel = hiltViewModel(),
) {
val context = LocalContext.current
val intent = remember { context.findActivity()?.intent }
val scope = rememberCoroutineScope()
val viewState = homeViewModel.viewState.collectAsStateValue()
val filterState = homeViewModel.filterState.collectAsStateValue()
var openArticleId by rememberSaveable {
mutableStateOf(intent?.extras?.get(ExtraName.ARTICLE_ID)?.toString() ?: "")
}.also {
intent?.replaceExtras(null)
}
LaunchedEffect(openArticleId) {
if (openArticleId.isNotEmpty()) {
readViewModel.dispatch(ReadViewAction.InitData(openArticleId))
readViewModel.dispatch(ReadViewAction.ScrollToItem(0))
homeViewModel.dispatch(HomeViewAction.ScrollToPage(scope, 2))
openArticleId = ""
}
}
BackHandler(true) {
val currentPage = viewState.pagerState.currentPage
if (currentPage == 0) {
context.findActivity()?.moveTaskToBack(false)
return@BackHandler
}
homeViewModel.dispatch(
HomeViewAction.ScrollToPage(
scope = scope,
targetPage = when (currentPage) {
2 -> 1
else -> 0
},
callback = {
if (currentPage == 2) {
readViewModel.dispatch(ReadViewAction.ClearArticle)
}
if (currentPage == 0) {
feedOptionViewModel.dispatch(FeedOptionViewAction.Hide(scope))
}
}
)
)
}
Column{
ViewPager(
modifier = Modifier.weight(1f),
state = viewState.pagerState,
composableList = listOf(
{
FeedsPage(
navController = navController,
syncWorkLiveData = homeViewModel.syncWorkLiveData,
filterState = filterState,
onSyncClick = {
homeViewModel.dispatch(HomeViewAction.Sync)
},
onFilterChange = {
homeViewModel.dispatch(HomeViewAction.ChangeFilter(it))
},
onScrollToPage = {
homeViewModel.dispatch(
HomeViewAction.ScrollToPage(
scope = scope,
targetPage = it,
)
)
}
)
},
{
FlowPage(
navController = navController,
syncWorkLiveData = homeViewModel.syncWorkLiveData,
filterState = filterState,
onScrollToPage = {
homeViewModel.dispatch(
HomeViewAction.ScrollToPage(
scope = scope,
targetPage = it,
)
)
},
onFilterChange = {
homeViewModel.dispatch(HomeViewAction.ChangeFilter(it))
},
onItemClick = {
readViewModel.dispatch(ReadViewAction.ScrollToItem(0))
readViewModel.dispatch(ReadViewAction.InitData(it.article.id))
if (it.feed.isFullContent) readViewModel.dispatch(ReadViewAction.RenderFullContent)
else readViewModel.dispatch(ReadViewAction.RenderDescriptionContent)
readViewModel.dispatch(ReadViewAction.RenderDescriptionContent)
homeViewModel.dispatch(
HomeViewAction.ScrollToPage(
scope = scope,
targetPage = 2,
)
)
}
)
},
{
ReadPage(
navController = navController,
onScrollToPage = { targetPage, callback ->
homeViewModel.dispatch(
HomeViewAction.ScrollToPage(
scope = scope,
targetPage = targetPage,
callback = callback
),
)
})
},
),
)
}
FeedOptionDrawer()
GroupOptionDrawer()
}

View File

@ -1,27 +1,30 @@
package me.ash.reader.ui.page.home package me.ash.reader.ui.page.home
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.paging.*
import androidx.work.WorkManager import androidx.work.WorkManager
import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.PagerState import com.google.accompanist.pager.PagerState
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import me.ash.reader.data.entity.Feed import me.ash.reader.data.entity.Feed
import me.ash.reader.data.entity.Filter import me.ash.reader.data.entity.Filter
import me.ash.reader.data.entity.Group import me.ash.reader.data.entity.Group
import me.ash.reader.data.module.ApplicationScope
import me.ash.reader.data.repository.RssRepository import me.ash.reader.data.repository.RssRepository
import me.ash.reader.data.repository.StringsRepository
import me.ash.reader.data.repository.SyncWorker import me.ash.reader.data.repository.SyncWorker
import me.ash.reader.ui.ext.animateScrollToPage import me.ash.reader.ui.page.home.flow.FlowItemView
import javax.inject.Inject import javax.inject.Inject
@OptIn(ExperimentalPagerApi::class) @OptIn(ExperimentalPagerApi::class)
@HiltViewModel @HiltViewModel
class HomeViewModel @Inject constructor( class HomeViewModel @Inject constructor(
private val rssRepository: RssRepository, private val rssRepository: RssRepository,
private val stringsRepository: StringsRepository,
@ApplicationScope
private val applicationScope: CoroutineScope,
workManager: WorkManager, workManager: WorkManager,
) : ViewModel() { ) : ViewModel() {
@ -37,11 +40,8 @@ class HomeViewModel @Inject constructor(
when (action) { when (action) {
is HomeViewAction.Sync -> sync() is HomeViewAction.Sync -> sync()
is HomeViewAction.ChangeFilter -> changeFilter(action.filterState) is HomeViewAction.ChangeFilter -> changeFilter(action.filterState)
is HomeViewAction.ScrollToPage -> scrollToPage( is HomeViewAction.FetchArticles -> fetchArticles()
action.scope, is HomeViewAction.InputSearchContent -> inputSearchContent(action.content)
action.targetPage,
action.callback
)
} }
} }
@ -57,10 +57,53 @@ class HomeViewModel @Inject constructor(
filter = filterState.filter, filter = filterState.filter,
) )
} }
fetchArticles()
} }
private fun scrollToPage(scope: CoroutineScope, targetPage: Int, callback: () -> Unit = {}) { private fun fetchArticles() {
_viewState.value.pagerState.animateScrollToPage(scope, targetPage, callback) _viewState.update {
it.copy(
pagingData = Pager(PagingConfig(pageSize = 10)) {
if (_viewState.value.searchContent.isNotBlank()) {
rssRepository.get().searchArticles(
content = _viewState.value.searchContent.trim(),
groupId = _filterState.value.group?.id,
feedId = _filterState.value.feed?.id,
isStarred = _filterState.value.filter.isStarred(),
isUnread = _filterState.value.filter.isUnread(),
)
} else {
rssRepository.get().pullArticles(
groupId = _filterState.value.group?.id,
feedId = _filterState.value.feed?.id,
isStarred = _filterState.value.filter.isStarred(),
isUnread = _filterState.value.filter.isUnread(),
)
}
}.flow.map {
it.map { FlowItemView.Article(it) }.insertSeparators { before, after ->
val beforeDate =
stringsRepository.formatAsString(before?.articleWithFeed?.article?.date)
val afterDate =
stringsRepository.formatAsString(after?.articleWithFeed?.article?.date)
if (beforeDate != afterDate) {
afterDate?.let { FlowItemView.Date(it, beforeDate != null) }
} else {
null
}
}
}.cachedIn(applicationScope)
)
}
}
private fun inputSearchContent(content: String) {
_viewState.update {
it.copy(
searchContent = content,
)
}
fetchArticles()
} }
} }
@ -73,6 +116,8 @@ data class FilterState(
@OptIn(ExperimentalPagerApi::class) @OptIn(ExperimentalPagerApi::class)
data class HomeViewState( data class HomeViewState(
val pagerState: PagerState = PagerState(0), val pagerState: PagerState = PagerState(0),
val pagingData: Flow<PagingData<FlowItemView>> = emptyFlow(),
val searchContent: String = "",
) )
sealed class HomeViewAction { sealed class HomeViewAction {
@ -82,9 +127,9 @@ sealed class HomeViewAction {
val filterState: FilterState val filterState: FilterState
) : HomeViewAction() ) : HomeViewAction()
data class ScrollToPage( object FetchArticles : HomeViewAction()
val scope: CoroutineScope,
val targetPage: Int, data class InputSearchContent(
val callback: () -> Unit = {}, val content: String,
) : HomeViewAction() ) : HomeViewAction()
} }

View File

@ -1,6 +1,7 @@
package me.ash.reader.ui.page.home.feeds package me.ash.reader.ui.page.home.feeds
import android.annotation.SuppressLint import android.annotation.SuppressLint
import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.core.* import androidx.compose.animation.core.*
@ -24,12 +25,9 @@ import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.LiveData
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.work.WorkInfo
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import me.ash.reader.R import me.ash.reader.R
import me.ash.reader.data.entity.Version
import me.ash.reader.data.entity.toVersion import me.ash.reader.data.entity.toVersion
import me.ash.reader.data.repository.SyncWorker.Companion.getIsSyncing import me.ash.reader.data.repository.SyncWorker.Companion.getIsSyncing
import me.ash.reader.ui.component.Banner import me.ash.reader.ui.component.Banner
@ -40,6 +38,10 @@ import me.ash.reader.ui.ext.*
import me.ash.reader.ui.page.common.RouteName import me.ash.reader.ui.page.common.RouteName
import me.ash.reader.ui.page.home.FilterBar import me.ash.reader.ui.page.home.FilterBar
import me.ash.reader.ui.page.home.FilterState import me.ash.reader.ui.page.home.FilterState
import me.ash.reader.ui.page.home.HomeViewAction
import me.ash.reader.ui.page.home.HomeViewModel
import me.ash.reader.ui.page.home.feeds.option.feed.FeedOptionDrawer
import me.ash.reader.ui.page.home.feeds.option.group.GroupOptionDrawer
import me.ash.reader.ui.page.home.feeds.subscribe.SubscribeDialog import me.ash.reader.ui.page.home.feeds.subscribe.SubscribeDialog
import me.ash.reader.ui.page.home.feeds.subscribe.SubscribeViewAction import me.ash.reader.ui.page.home.feeds.subscribe.SubscribeViewAction
import me.ash.reader.ui.page.home.feeds.subscribe.SubscribeViewModel import me.ash.reader.ui.page.home.feeds.subscribe.SubscribeViewModel
@ -51,18 +53,14 @@ import me.ash.reader.ui.page.home.feeds.subscribe.SubscribeViewModel
) )
@Composable @Composable
fun FeedsPage( fun FeedsPage(
modifier: Modifier = Modifier,
navController: NavHostController, navController: NavHostController,
feedsViewModel: FeedsViewModel = hiltViewModel(), feedsViewModel: FeedsViewModel = hiltViewModel(),
syncWorkLiveData: LiveData<WorkInfo>,
filterState: FilterState,
subscribeViewModel: SubscribeViewModel = hiltViewModel(), subscribeViewModel: SubscribeViewModel = hiltViewModel(),
onSyncClick: () -> Unit = {}, homeViewModel: HomeViewModel,
onFilterChange: (filterState: FilterState) -> Unit = {},
onScrollToPage: (targetPage: Int) -> Unit = {},
) { ) {
val context = LocalContext.current val context = LocalContext.current
val viewState = feedsViewModel.viewState.collectAsStateValue() val feedsViewState = feedsViewModel.viewState.collectAsStateValue()
val filterState = homeViewModel.filterState.collectAsStateValue()
val skipVersion = context.dataStore.data val skipVersion = context.dataStore.data
.map { it[DataStoreKeys.SkipVersionNumber.key] ?: "" } .map { it[DataStoreKeys.SkipVersionNumber.key] ?: "" }
@ -78,7 +76,7 @@ fun FeedsPage(
val owner = LocalLifecycleOwner.current val owner = LocalLifecycleOwner.current
var isSyncing by remember { mutableStateOf(false) } var isSyncing by remember { mutableStateOf(false) }
syncWorkLiveData.observe(owner) { homeViewModel.syncWorkLiveData.observe(owner) {
it?.let { isSyncing = it.progress.getIsSyncing() } it?.let { isSyncing = it.progress.getIsSyncing() }
} }
@ -108,13 +106,13 @@ fun FeedsPage(
} }
LaunchedEffect(filterState) { LaunchedEffect(filterState) {
feedsViewModel.dispatch(FeedsViewAction.FetchData(filterState)) snapshotFlow { filterState }.collect {
feedsViewModel.dispatch(FeedsViewAction.FetchData(it))
}
} }
LaunchedEffect(isSyncing) { BackHandler(true) {
if (!isSyncing) { context.findActivity()?.moveTaskToBack(false)
feedsViewModel.dispatch(FeedsViewAction.FetchData(filterState))
}
} }
Scaffold( Scaffold(
@ -133,7 +131,9 @@ fun FeedsPage(
tint = MaterialTheme.colorScheme.onSurface, tint = MaterialTheme.colorScheme.onSurface,
showBadge = latestVersion.whetherNeedUpdate(currentVersion, skipVersion), showBadge = latestVersion.whetherNeedUpdate(currentVersion, skipVersion),
) { ) {
navController.navigate(RouteName.SETTINGS) navController.navigate(RouteName.SETTINGS) {
popUpTo(RouteName.FEEDS)
}
} }
}, },
actions = { actions = {
@ -143,9 +143,7 @@ fun FeedsPage(
contentDescription = stringResource(R.string.refresh), contentDescription = stringResource(R.string.refresh),
tint = MaterialTheme.colorScheme.onSurface, tint = MaterialTheme.colorScheme.onSurface,
) { ) {
if (!isSyncing) { if (!isSyncing) homeViewModel.dispatch(HomeViewAction.Sync)
onSyncClick()
}
} }
FeedbackIconButton( FeedbackIconButton(
imageVector = Icons.Rounded.Add, imageVector = Icons.Rounded.Add,
@ -158,7 +156,6 @@ fun FeedsPage(
) )
}, },
content = { content = {
SubscribeDialog()
LazyColumn { LazyColumn {
item { item {
DisplayText( DisplayText(
@ -169,14 +166,14 @@ fun FeedsPage(
} }
) )
}, },
text = viewState.account?.name ?: stringResource(R.string.unknown), text = feedsViewState.account?.name ?: "",
desc = if (isSyncing) stringResource(R.string.syncing) else "", desc = if (isSyncing) stringResource(R.string.syncing) else "",
) )
} }
item { item {
Banner( Banner(
title = filterState.filter.getName(), title = filterState.filter.getName(),
desc = filterState.filter.getDesc(), desc = feedsViewState.importantCount,
icon = filterState.filter.icon, icon = filterState.filter.icon,
action = { action = {
Icon( Icon(
@ -185,13 +182,14 @@ fun FeedsPage(
) )
}, },
) { ) {
onFilterChange( filterChange(
filterState.copy( navController = navController,
homeViewModel = homeViewModel,
filterState = filterState.copy(
group = null, group = null,
feed = null feed = null,
) )
) )
onScrollToPage(1)
} }
} }
item { item {
@ -202,32 +200,34 @@ fun FeedsPage(
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
} }
itemsIndexed(viewState.groupWithFeedList) { index, groupWithFeed -> itemsIndexed(feedsViewState.groupWithFeedList) { index, groupWithFeed ->
// Crossfade(targetState = groupWithFeed) { groupWithFeed -> // Crossfade(targetState = groupWithFeed) { groupWithFeed ->
Column { Column {
GroupItem( GroupItem(
group = groupWithFeed.group, group = groupWithFeed.group,
feeds = groupWithFeed.feeds, feeds = groupWithFeed.feeds,
groupOnClick = { groupOnClick = {
onFilterChange( filterChange(
filterState.copy( navController = navController,
homeViewModel = homeViewModel,
filterState = filterState.copy(
group = groupWithFeed.group, group = groupWithFeed.group,
feed = null feed = null,
) )
) )
onScrollToPage(1)
}, },
feedOnClick = { feed -> feedOnClick = { feed ->
onFilterChange( filterChange(
filterState.copy( navController = navController,
homeViewModel = homeViewModel,
filterState = filterState.copy(
group = null, group = null,
feed = feed feed = feed,
) )
) )
onScrollToPage(1)
} }
) )
if (index != viewState.groupWithFeedList.lastIndex) { if (index != feedsViewState.groupWithFeedList.lastIndex) {
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
} }
} }
@ -246,14 +246,32 @@ fun FeedsPage(
.fillMaxWidth(), .fillMaxWidth(),
filter = filterState.filter, filter = filterState.filter,
filterOnClick = { filterOnClick = {
onFilterChange( filterChange(
filterState.copy( navController = navController,
filter = it homeViewModel = homeViewModel,
) filterState = filterState.copy(filter = it),
isNavigate = false,
) )
}, },
) )
} }
) )
SubscribeDialog()
GroupOptionDrawer()
FeedOptionDrawer()
} }
private fun filterChange(
navController: NavHostController,
homeViewModel: HomeViewModel,
filterState: FilterState,
isNavigate: Boolean = true,
) {
homeViewModel.dispatch(HomeViewAction.ChangeFilter(filterState))
if (isNavigate) {
navController.navigate(RouteName.FLOW) {
popUpTo(RouteName.FEEDS)
}
}
}

View File

@ -8,12 +8,13 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.ash.reader.R
import me.ash.reader.data.entity.Account import me.ash.reader.data.entity.Account
import me.ash.reader.data.entity.Filter
import me.ash.reader.data.entity.GroupWithFeed import me.ash.reader.data.entity.GroupWithFeed
import me.ash.reader.data.repository.AccountRepository import me.ash.reader.data.repository.AccountRepository
import me.ash.reader.data.repository.OpmlRepository import me.ash.reader.data.repository.OpmlRepository
import me.ash.reader.data.repository.RssRepository import me.ash.reader.data.repository.RssRepository
import me.ash.reader.data.repository.StringsRepository
import me.ash.reader.ui.page.home.FilterState import me.ash.reader.ui.page.home.FilterState
import javax.inject.Inject import javax.inject.Inject
@ -22,6 +23,7 @@ class FeedsViewModel @Inject constructor(
private val accountRepository: AccountRepository, private val accountRepository: AccountRepository,
private val rssRepository: RssRepository, private val rssRepository: RssRepository,
private val opmlRepository: OpmlRepository, private val opmlRepository: OpmlRepository,
private val stringsRepository: StringsRepository,
) : ViewModel() { ) : ViewModel() {
private val _viewState = MutableStateFlow(FeedsViewState()) private val _viewState = MutableStateFlow(FeedsViewState())
val viewState: StateFlow<FeedsViewState> = _viewState.asStateFlow() val viewState: StateFlow<FeedsViewState> = _viewState.asStateFlow()
@ -105,19 +107,19 @@ class FeedsViewModel @Inject constructor(
}.onEach { groupWithFeedList -> }.onEach { groupWithFeedList ->
_viewState.update { _viewState.update {
it.copy( it.copy(
filter = when { importantCount = groupWithFeedList.sumOf { it.group.important ?: 0 }.run {
isStarred -> Filter.Starred when {
isUnread -> Filter.Unread isStarred -> stringsRepository.getString(R.string.unread_desc, this)
else -> Filter.All isUnread -> stringsRepository.getString(R.string.starred_desc, this)
}.apply { else -> stringsRepository.getString(R.string.all_desc, this)
important = groupWithFeedList.sumOf { it.group.important ?: 0 } }
}, },
groupWithFeedList = groupWithFeedList, groupWithFeedList = groupWithFeedList,
feedsVisible = List(groupWithFeedList.size, init = { true }) feedsVisible = List(groupWithFeedList.size, init = { true })
) )
} }
}.catch { }.catch() {
Log.e("RLog", "catch in articleRepository.pullFeeds(): $this") Log.e("RLog", "catch in articleRepository.pullFeeds(): ${it.message}")
}.flowOn(Dispatchers.Default).collect() }.flowOn(Dispatchers.Default).collect()
} }
@ -130,7 +132,7 @@ class FeedsViewModel @Inject constructor(
data class FeedsViewState( data class FeedsViewState(
val account: Account? = null, val account: Account? = null,
val filter: Filter = Filter.All, val importantCount: String = "",
val groupWithFeedList: List<GroupWithFeed> = emptyList(), val groupWithFeedList: List<GroupWithFeed> = emptyList(),
val feedsVisible: List<Boolean> = emptyList(), val feedsVisible: List<Boolean> = emptyList(),
val listState: LazyListState = LazyListState(), val listState: LazyListState = LazyListState(),

View File

@ -1,6 +1,5 @@
package me.ash.reader.ui.page.home.feeds.option.feed package me.ash.reader.ui.page.home.feeds.option.feed
import android.widget.Toast
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ExperimentalMaterialApi

View File

@ -1,5 +1,6 @@
package me.ash.reader.ui.page.home.flow package me.ash.reader.ui.page.home.flow
import android.util.Log
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.animation.* import androidx.compose.animation.*
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
@ -23,63 +24,55 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.LiveData
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.paging.LoadState import androidx.paging.LoadState
import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.compose.LazyPagingItems
import androidx.work.WorkInfo
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.ash.reader.R import me.ash.reader.R
import me.ash.reader.data.entity.ArticleWithFeed
import me.ash.reader.data.repository.SyncWorker.Companion.getIsSyncing import me.ash.reader.data.repository.SyncWorker.Companion.getIsSyncing
import me.ash.reader.ui.component.DisplayText import me.ash.reader.ui.component.DisplayText
import me.ash.reader.ui.component.FeedbackIconButton import me.ash.reader.ui.component.FeedbackIconButton
import me.ash.reader.ui.component.SwipeRefresh import me.ash.reader.ui.component.SwipeRefresh
import me.ash.reader.ui.ext.collectAsStateValue import me.ash.reader.ui.ext.collectAsStateValue
import me.ash.reader.ui.ext.getName import me.ash.reader.ui.ext.getName
import me.ash.reader.ui.page.common.RouteName
import me.ash.reader.ui.page.home.FilterBar import me.ash.reader.ui.page.home.FilterBar
import me.ash.reader.ui.page.home.FilterState import me.ash.reader.ui.page.home.FilterState
import me.ash.reader.ui.page.home.HomeViewAction
import me.ash.reader.ui.page.home.HomeViewModel
@OptIn( @OptIn(
ExperimentalMaterial3Api::class, ExperimentalMaterial3Api::class,
ExperimentalFoundationApi::class, com.google.accompanist.pager.ExperimentalPagerApi::class, ExperimentalFoundationApi::class,
com.google.accompanist.pager.ExperimentalPagerApi::class,
androidx.compose.ui.ExperimentalComposeUiApi::class, androidx.compose.ui.ExperimentalComposeUiApi::class,
) )
@Composable @Composable
fun FlowPage( fun FlowPage(
modifier: Modifier = Modifier,
navController: NavHostController, navController: NavHostController,
flowViewModel: FlowViewModel = hiltViewModel(), flowViewModel: FlowViewModel = hiltViewModel(),
syncWorkLiveData: LiveData<WorkInfo>, homeViewModel: HomeViewModel,
filterState: FilterState, pagingItems: LazyPagingItems<FlowItemView>,
onFilterChange: (filterState: FilterState) -> Unit = {},
onScrollToPage: (targetPage: Int) -> Unit = {},
onItemClick: (item: ArticleWithFeed) -> Unit = {},
) { ) {
val keyboardController = LocalSoftwareKeyboardController.current val keyboardController = LocalSoftwareKeyboardController.current
val focusRequester = remember { FocusRequester() }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val focusRequester = remember { FocusRequester() }
var markAsRead by remember { mutableStateOf(false) } var markAsRead by remember { mutableStateOf(false) }
var onSearch by remember { mutableStateOf(false) } var onSearch by remember { mutableStateOf(false) }
val viewState = flowViewModel.viewState.collectAsStateValue() val viewState = flowViewModel.viewState.collectAsStateValue()
val pagingItems = viewState.pagingData.collectAsLazyPagingItems() val filterState = homeViewModel.filterState.collectAsStateValue()
val homeViewState = homeViewModel.viewState.collectAsStateValue()
val listState = if (pagingItems.itemCount > 0) viewState.listState else rememberLazyListState() val listState = if (pagingItems.itemCount > 0) viewState.listState else rememberLazyListState()
val owner = LocalLifecycleOwner.current val owner = LocalLifecycleOwner.current
var isSyncing by remember { mutableStateOf(false) } var isSyncing by remember { mutableStateOf(false) }
syncWorkLiveData.observe(owner) { homeViewModel.syncWorkLiveData.observe(owner) {
it?.let { isSyncing = it.progress.getIsSyncing() } it?.let { isSyncing = it.progress.getIsSyncing() }
} }
LaunchedEffect(filterState) {
snapshotFlow { filterState }.collect {
flowViewModel.dispatch(
FlowViewAction.FetchData(it)
)
}
}
LaunchedEffect(onSearch) { LaunchedEffect(onSearch) {
snapshotFlow { onSearch }.collect { snapshotFlow { onSearch }.collect {
if (it) { if (it) {
@ -87,8 +80,8 @@ fun FlowPage(
focusRequester.requestFocus() focusRequester.requestFocus()
} else { } else {
keyboardController?.hide() keyboardController?.hide()
if (viewState.searchContent.isNotBlank()) { if (homeViewState.searchContent.isNotBlank()) {
flowViewModel.dispatch(FlowViewAction.InputSearchContent("")) homeViewModel.dispatch(HomeViewAction.InputSearchContent(""))
} }
} }
} }
@ -96,6 +89,7 @@ fun FlowPage(
LaunchedEffect(viewState.listState) { LaunchedEffect(viewState.listState) {
snapshotFlow { viewState.listState.firstVisibleItemIndex }.collect { snapshotFlow { viewState.listState.firstVisibleItemIndex }.collect {
Log.i("RLog", "FlowPage: ${it}")
if (it > 0) { if (it > 0) {
keyboardController?.hide() keyboardController?.hide()
} }
@ -121,7 +115,7 @@ fun FlowPage(
tint = MaterialTheme.colorScheme.onSurface tint = MaterialTheme.colorScheme.onSurface
) { ) {
onSearch = false onSearch = false
onScrollToPage(0) navController.popBackStack()
} }
}, },
actions = { actions = {
@ -215,7 +209,7 @@ fun FlowPage(
exit = fadeOut() + shrinkVertically(), exit = fadeOut() + shrinkVertically(),
) { ) {
SearchBar( SearchBar(
value = viewState.searchContent, value = homeViewState.searchContent,
placeholder = when { placeholder = when {
filterState.group != null -> stringResource( filterState.group != null -> stringResource(
R.string.search_for_in, R.string.search_for_in,
@ -234,11 +228,11 @@ fun FlowPage(
}, },
focusRequester = focusRequester, focusRequester = focusRequester,
onValueChange = { onValueChange = {
flowViewModel.dispatch(FlowViewAction.InputSearchContent(it)) homeViewModel.dispatch(HomeViewAction.InputSearchContent(it))
}, },
onClose = { onClose = {
onSearch = false onSearch = false
flowViewModel.dispatch(FlowViewAction.InputSearchContent("")) homeViewModel.dispatch(HomeViewAction.InputSearchContent(""))
} }
) )
Spacer(modifier = Modifier.height((56 + 24 + 10).dp)) Spacer(modifier = Modifier.height((56 + 24 + 10).dp))
@ -248,7 +242,9 @@ fun FlowPage(
pagingItems = pagingItems, pagingItems = pagingItems,
) { ) {
onSearch = false onSearch = false
onItemClick(it) navController.navigate("${RouteName.READING}/${it.article.id}") {
popUpTo(RouteName.FLOW)
}
} }
item { item {
Spacer(modifier = Modifier.height(64.dp)) Spacer(modifier = Modifier.height(64.dp))
@ -266,7 +262,9 @@ fun FlowPage(
.fillMaxWidth(), .fillMaxWidth(),
filter = filterState.filter, filter = filterState.filter,
filterOnClick = { filterOnClick = {
onFilterChange(filterState.copy(filter = it)) flowViewModel.dispatch(FlowViewAction.ScrollToItem(0))
homeViewModel.dispatch(HomeViewAction.ChangeFilter(filterState.copy(filter = it)))
homeViewModel.dispatch(HomeViewAction.FetchArticles)
}, },
) )
} }

View File

@ -3,21 +3,20 @@ package me.ash.reader.ui.page.home.flow
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.paging.*
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.ash.reader.data.entity.ArticleWithFeed import me.ash.reader.data.entity.ArticleWithFeed
import me.ash.reader.data.repository.RssRepository import me.ash.reader.data.repository.RssRepository
import me.ash.reader.data.repository.StringsRepository
import me.ash.reader.ui.page.home.FilterState
import java.util.* import java.util.*
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class FlowViewModel @Inject constructor( class FlowViewModel @Inject constructor(
private val rssRepository: RssRepository, private val rssRepository: RssRepository,
private val stringsRepository: StringsRepository,
) : ViewModel() { ) : ViewModel() {
private val _viewState = MutableStateFlow(ArticleViewState()) private val _viewState = MutableStateFlow(ArticleViewState())
val viewState: StateFlow<ArticleViewState> = _viewState.asStateFlow() val viewState: StateFlow<ArticleViewState> = _viewState.asStateFlow()
@ -25,7 +24,6 @@ class FlowViewModel @Inject constructor(
fun dispatch(action: FlowViewAction) { fun dispatch(action: FlowViewAction) {
when (action) { when (action) {
is FlowViewAction.Sync -> sync() is FlowViewAction.Sync -> sync()
is FlowViewAction.FetchData -> fetchData(action.filterState)
is FlowViewAction.ChangeIsBack -> changeIsBack(action.isBack) is FlowViewAction.ChangeIsBack -> changeIsBack(action.isBack)
is FlowViewAction.ScrollToItem -> scrollToItem(action.index) is FlowViewAction.ScrollToItem -> scrollToItem(action.index)
is FlowViewAction.MarkAsRead -> markAsRead( is FlowViewAction.MarkAsRead -> markAsRead(
@ -34,7 +32,6 @@ class FlowViewModel @Inject constructor(
action.articleId, action.articleId,
action.markAsReadBefore, action.markAsReadBefore,
) )
is FlowViewAction.InputSearchContent -> inputSearchContent(action.content)
} }
} }
@ -42,77 +39,6 @@ class FlowViewModel @Inject constructor(
rssRepository.get().doSync() rssRepository.get().doSync()
} }
private fun fetchData(filterState: FilterState? = null) {
// viewModelScope.launch(Dispatchers.Default) {
// rssRepository.get().pullImportant(filterState.filter.isStarred(), true)
// .collect { importantList ->
// _viewState.update {
// it.copy(
// filterImportant = importantList.sumOf { it.important },
// )
// }
// }
// }
if (_viewState.value.searchContent.isNotBlank()) {
_viewState.update {
it.copy(
filterState = filterState,
pagingData = Pager(PagingConfig(pageSize = 10)) {
rssRepository.get().searchArticles(
content = _viewState.value.searchContent.trim(),
groupId = _viewState.value.filterState?.group?.id,
feedId = _viewState.value.filterState?.feed?.id,
isStarred = _viewState.value.filterState?.filter?.isStarred() ?: false,
isUnread = _viewState.value.filterState?.filter?.isUnread() ?: false,
)
}.flow.map {
it.map {
FlowItemView.Article(it)
}.insertSeparators { before, after ->
val beforeDate =
stringsRepository.formatAsString(before?.articleWithFeed?.article?.date)
val afterDate =
stringsRepository.formatAsString(after?.articleWithFeed?.article?.date)
if (beforeDate != afterDate) {
afterDate?.let { FlowItemView.Date(it, beforeDate != null) }
} else {
null
}
}
}.cachedIn(viewModelScope)
)
}
} else if (filterState != null) {
_viewState.update {
it.copy(
filterState = filterState,
pagingData = Pager(PagingConfig(pageSize = 10)) {
rssRepository.get().pullArticles(
groupId = filterState.group?.id,
feedId = filterState.feed?.id,
isStarred = filterState.filter.isStarred(),
isUnread = filterState.filter.isUnread(),
)
}.flow.map {
it.map {
FlowItemView.Article(it)
}.insertSeparators { before, after ->
val beforeDate =
stringsRepository.formatAsString(before?.articleWithFeed?.article?.date)
val afterDate =
stringsRepository.formatAsString(after?.articleWithFeed?.article?.date)
if (beforeDate != afterDate) {
afterDate?.let { FlowItemView.Date(it, beforeDate != null) }
} else {
null
}
}
}.cachedIn(viewModelScope)
)
}
}
}
private fun scrollToItem(index: Int) { private fun scrollToItem(index: Int) {
viewModelScope.launch { viewModelScope.launch {
_viewState.value.listState.scrollToItem(index) _viewState.value.listState.scrollToItem(index)
@ -155,34 +81,18 @@ class FlowViewModel @Inject constructor(
) )
} }
} }
private fun inputSearchContent(content: String) {
_viewState.update {
it.copy(
searchContent = content,
)
}
fetchData(_viewState.value.filterState)
}
} }
data class ArticleViewState( data class ArticleViewState(
val filterState: FilterState? = null,
val filterImportant: Int = 0, val filterImportant: Int = 0,
val listState: LazyListState = LazyListState(), val listState: LazyListState = LazyListState(),
val isBack: Boolean = false, val isBack: Boolean = false,
val pagingData: Flow<PagingData<FlowItemView>> = emptyFlow(),
val syncWorkInfo: String = "", val syncWorkInfo: String = "",
val searchContent: String = "",
) )
sealed class FlowViewAction { sealed class FlowViewAction {
object Sync : FlowViewAction() object Sync : FlowViewAction()
data class FetchData(
val filterState: FilterState,
) : FlowViewAction()
data class ChangeIsBack( data class ChangeIsBack(
val isBack: Boolean val isBack: Boolean
) : FlowViewAction() ) : FlowViewAction()
@ -197,10 +107,6 @@ sealed class FlowViewAction {
val articleId: String?, val articleId: String?,
val markAsReadBefore: MarkAsReadBefore val markAsReadBefore: MarkAsReadBefore
) : FlowViewAction() ) : FlowViewAction()
data class InputSearchContent(
val content: String,
) : FlowViewAction()
} }
enum class MarkAsReadBefore { enum class MarkAsReadBefore {

View File

@ -31,13 +31,19 @@ import me.ash.reader.ui.ext.collectAsStateValue
@Composable @Composable
fun ReadPage( fun ReadPage(
navController: NavHostController, navController: NavHostController,
modifier: Modifier = Modifier,
readViewModel: ReadViewModel = hiltViewModel(), readViewModel: ReadViewModel = hiltViewModel(),
onScrollToPage: (targetPage: Int, callback: () -> Unit) -> Unit = { _, _ -> },
) { ) {
val viewState = readViewModel.viewState.collectAsStateValue() val viewState = readViewModel.viewState.collectAsStateValue()
var isScrollDown by remember { mutableStateOf(false) } var isScrollDown by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
navController.currentBackStackEntryFlow.collect {
it.arguments?.getString("articleId")?.let {
readViewModel.dispatch(ReadViewAction.InitData(it))
}
}
}
if (viewState.listState.isScrollInProgress) { if (viewState.listState.isScrollInProgress) {
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
Log.i("RLog", "scroll: start") Log.i("RLog", "scroll: start")
@ -84,10 +90,9 @@ fun ReadPage(
TopBar( TopBar(
isShow = viewState.articleWithFeed == null || !isScrollDown, isShow = viewState.articleWithFeed == null || !isScrollDown,
isShowActions = viewState.articleWithFeed != null, isShowActions = viewState.articleWithFeed != null,
onScrollToPage = onScrollToPage, onClose = {
onClearArticle = { navController.popBackStack()
readViewModel.dispatch(ReadViewAction.ClearArticle) },
}
) )
} }
Content( Content(
@ -127,8 +132,7 @@ fun ReadPage(
private fun TopBar( private fun TopBar(
isShow: Boolean, isShow: Boolean,
isShowActions: Boolean = false, isShowActions: Boolean = false,
onScrollToPage: (targetPage: Int, callback: () -> Unit) -> Unit = { _, _ -> }, onClose: () -> Unit = {},
onClearArticle: () -> Unit = {},
) { ) {
AnimatedVisibility( AnimatedVisibility(
visible = isShow, visible = isShow,
@ -147,9 +151,7 @@ private fun TopBar(
contentDescription = stringResource(R.string.close), contentDescription = stringResource(R.string.close),
tint = MaterialTheme.colorScheme.onSurface tint = MaterialTheme.colorScheme.onSurface
) { ) {
onScrollToPage(1) { onClose()
onClearArticle()
}
} }
}, },
actions = { actions = {

View File

@ -32,7 +32,9 @@ fun SettingItem(
action: (@Composable () -> Unit)? = null action: (@Composable () -> Unit)? = null
) { ) {
Surface( Surface(
modifier = modifier.clickable { onClick() }.alpha(if (enable) 1f else 0.5f), modifier = modifier
.clickable { onClick() }
.alpha(if (enable) 1f else 0.5f),
color = Color.Unspecified color = Color.Unspecified
) { ) {
Row( Row(

View File

@ -65,7 +65,7 @@ fun SettingsPage(
contentDescription = stringResource(R.string.back), contentDescription = stringResource(R.string.back),
tint = MaterialTheme.colorScheme.onSurface tint = MaterialTheme.colorScheme.onSurface
) { ) {
navController.navigate(RouteName.HOME) navController.popBackStack()
} }
}, },
actions = {} actions = {}
@ -119,7 +119,9 @@ fun SettingsPage(
desc = stringResource(R.string.color_and_style_desc), desc = stringResource(R.string.color_and_style_desc),
icon = Icons.Outlined.Palette, icon = Icons.Outlined.Palette,
) { ) {
navController.navigate(RouteName.COLOR_AND_STYLE) navController.navigate(RouteName.COLOR_AND_STYLE) {
popUpTo(RouteName.SETTINGS)
}
} }
} }
item { item {
@ -144,7 +146,9 @@ fun SettingsPage(
desc = stringResource(R.string.tips_and_support_desc), desc = stringResource(R.string.tips_and_support_desc),
icon = Icons.Outlined.TipsAndUpdates, icon = Icons.Outlined.TipsAndUpdates,
) { ) {
navController.navigate(RouteName.TIPS_AND_SUPPORT) navController.navigate(RouteName.TIPS_AND_SUPPORT) {
popUpTo(RouteName.SETTINGS)
}
} }
} }
} }

View File

@ -102,7 +102,7 @@ fun StartupPage(
floatingActionButton = { floatingActionButton = {
ExtendedFloatingActionButton( ExtendedFloatingActionButton(
onClick = { onClick = {
navController.navigate(route = RouteName.HOME) navController.navigate(RouteName.FEEDS)
scope.launch { scope.launch {
context.dataStore.put(DataStoreKeys.IsFirstLaunch, false) context.dataStore.put(DataStoreKeys.IsFirstLaunch, false)
} }

View File

@ -67,7 +67,9 @@ data class Jzazbz(
z, z,
) )
).map { ).map {
((c_1 + c_2 * (it / 10000.0).pow(n)) / (1.0 + c_3 * (it / 10000.0).pow(n))).pow(p) ((c_1 + c_2 * (it / 10000.0).pow(n)) / (1.0 + c_3 * (it / 10000.0).pow(n))).pow(
p
)
}.toDoubleArray() }.toDoubleArray()
return Jzazbz( return Jzazbz(
Jz = (1.0 + d) * Iz / (1.0 + d * Iz) - d_0, Jz = (1.0 + d) * Iz / (1.0 + d * Iz) - d_0,

View File

@ -22,7 +22,8 @@ data class Rgb(
fun isInGamut(): Boolean = rgb.map { it in colorSpace.componentRange }.all { it } fun isInGamut(): Boolean = rgb.map { it in colorSpace.componentRange }.all { it }
fun clamp(): Rgb = rgb.map { it.coerceIn(colorSpace.componentRange) }.toDoubleArray().asRgb(colorSpace) fun clamp(): Rgb =
rgb.map { it.coerceIn(colorSpace.componentRange) }.toDoubleArray().asRgb(colorSpace)
fun toXyz(luminance: Double): CieXyz = ( fun toXyz(luminance: Double): CieXyz = (
colorSpace.rgbToXyzMatrix * rgb.map { colorSpace.rgbToXyzMatrix * rgb.map {
@ -40,6 +41,7 @@ data class Rgb(
.map { colorSpace.transferFunction.OETF(it) } .map { colorSpace.transferFunction.OETF(it) }
.toDoubleArray().asRgb(colorSpace) .toDoubleArray().asRgb(colorSpace)
internal fun DoubleArray.asRgb(colorSpace: RgbColorSpace): Rgb = Rgb(this[0], this[1], this[2], colorSpace) internal fun DoubleArray.asRgb(colorSpace: RgbColorSpace): Rgb =
Rgb(this[0], this[1], this[2], colorSpace)
} }
} }

View File

@ -22,9 +22,11 @@ class PQTransferFunction : TransferFunction {
} }
override fun EOTF(x: Double): Double = override fun EOTF(x: Double): Double =
10000.0 * ((x.pow(1.0 / m_2).coerceAtLeast(0.0)) / (c_2 - c_3 * x.pow(1.0 / m_2))).pow(1.0 / m_1) 10000.0 * ((x.pow(1.0 / m_2)
.coerceAtLeast(0.0)) / (c_2 - c_3 * x.pow(1.0 / m_2))).pow(1.0 / m_1)
override fun OETF(x: Double): Double = ((c_1 + c_2 * x / 10000.0) / (1 + c_3 * x / 10000.0)).pow( override fun OETF(x: Double): Double =
((c_1 + c_2 * x / 10000.0) / (1 + c_3 * x / 10000.0)).pow(
m_2 m_2
) )
} }

View File

@ -98,7 +98,8 @@ data class Zcam(
if (!current.toIzazbz().toXyz().toRgb(cond.luminance, colorSpace).isInGamut()) { if (!current.toIzazbz().toXyz().toRgb(cond.luminance, colorSpace).isInGamut()) {
high = mid high = mid
} else { } else {
val next = current.copy(Cz = mid + error).toIzazbz().toXyz().toRgb(cond.luminance, colorSpace) val next = current.copy(Cz = mid + error).toIzazbz().toXyz()
.toRgb(cond.luminance, colorSpace)
if (next.isInGamut()) { if (next.isInGamut()) {
low = mid low = mid
} else { } else {
@ -124,14 +125,17 @@ data class Zcam(
val F_b = sqrt(Y_b / Y_w) val F_b = sqrt(Y_b / Y_w)
val F_L = 0.171 * L_a.pow(1.0 / 3.0) * (1 - exp(-48.0 / 9.0 * L_a)) val F_L = 0.171 * L_a.pow(1.0 / 3.0) * (1 - exp(-48.0 / 9.0 * L_a))
val Izw = absoluteWhitePoint.toIzazbz().Iz val Izw = absoluteWhitePoint.toIzazbz().Iz
val Qzw = 2700.0 * Izw.pow(1.6 * F_s / F_b.pow(0.12)) * F_s.pow(2.2) * sqrt(F_b) * F_L.pow(0.2) val Qzw =
2700.0 * Izw.pow(1.6 * F_s / F_b.pow(0.12)) * F_s.pow(2.2) * sqrt(F_b) * F_L.pow(0.2)
} }
fun Izazbz.toZcam(cond: ViewingConditions): Zcam { fun Izazbz.toZcam(cond: ViewingConditions): Zcam {
with(cond) { with(cond) {
val hz = atan2(bz, az).toDegrees().mod(360.0) // hue angle val hz = atan2(bz, az).toDegrees().mod(360.0) // hue angle
val Qz = val Qz =
2700.0 * Iz.pow(1.6 * F_s / F_b.pow(0.12)) * F_s.pow(2.2) * sqrt(F_b) * F_L.pow(0.2) // brightness 2700.0 * Iz.pow(1.6 * F_s / F_b.pow(0.12)) * F_s.pow(2.2) * sqrt(F_b) * F_L.pow(
0.2
) // brightness
val Jz = 100.0 * Qz / Qzw // lightness val Jz = 100.0 * Qz / Qzw // lightness
val ez = 1.015 + cos(89.038 + hz).toRadians() // ~ eccentricity factor val ez = 1.015 + cos(89.038 + hz).toRadians() // ~ eccentricity factor
val Mz = val Mz =

View File

@ -63,5 +63,10 @@ fun animateZcamLchAsState(
} }
) )
} }
return animateValueAsState(targetValue, converter, animationSpec, finishedListener = finishedListener) return animateValueAsState(
targetValue,
converter,
animationSpec,
finishedListener = finishedListener
)
} }

View File

@ -60,5 +60,6 @@ class Matrix3(
z[0] * vec[0] + z[1] * vec[1] + z[2] * vec[2], z[0] * vec[0] + z[1] * vec[1] + z[2] * vec[2],
) )
override fun toString(): String = "{" + arrayOf(x, y, z).joinToString { "[" + it.joinToString() + "]" } + "}" override fun toString(): String =
"{" + arrayOf(x, y, z).joinToString { "[" + it.joinToString() + "]" } + "}"
} }