diff --git a/app/src/main/java/me/ash/reader/data/entity/Filter.kt b/app/src/main/java/me/ash/reader/data/entity/Filter.kt index cb197f2..ec010e8 100644 --- a/app/src/main/java/me/ash/reader/data/entity/Filter.kt +++ b/app/src/main/java/me/ash/reader/data/entity/Filter.kt @@ -1,18 +1,14 @@ package me.ash.reader.data.entity 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.rounded.Star import androidx.compose.material.icons.rounded.StarOutline import androidx.compose.material.icons.rounded.Subject import androidx.compose.ui.graphics.vector.ImageVector class Filter( var index: Int, - var important: Int, var icon: ImageVector, - var filledIcon: ImageVector, ) { fun isStarred(): Boolean = this == Starred fun isUnread(): Boolean = this == Unread @@ -21,21 +17,15 @@ class Filter( companion object { val Starred = Filter( index = 0, - important = 666, icon = Icons.Rounded.StarOutline, - filledIcon = Icons.Rounded.Star, ) val Unread = Filter( index = 1, - important = 666, icon = Icons.Outlined.FiberManualRecord, - filledIcon = Icons.Filled.FiberManualRecord, ) val All = Filter( index = 2, - important = 666, icon = Icons.Rounded.Subject, - filledIcon = Icons.Rounded.Subject, ) } } \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/data/repository/StringsRepository.kt b/app/src/main/java/me/ash/reader/data/repository/StringsRepository.kt index 0019e54..6819345 100644 --- a/app/src/main/java/me/ash/reader/data/repository/StringsRepository.kt +++ b/app/src/main/java/me/ash/reader/data/repository/StringsRepository.kt @@ -10,6 +10,6 @@ class StringsRepository @Inject constructor( @ApplicationContext 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) } diff --git a/app/src/main/java/me/ash/reader/ui/component/Banner.kt b/app/src/main/java/me/ash/reader/ui/component/Banner.kt index aa3f0f8..aafbc18 100644 --- a/app/src/main/java/me/ash/reader/ui/component/Banner.kt +++ b/app/src/main/java/me/ash/reader/ui/component/Banner.kt @@ -10,6 +10,8 @@ package me.ash.reader.ui.component import android.view.SoundEffectConstants 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.clickable import androidx.compose.foundation.layout.* @@ -84,6 +86,7 @@ fun Banner( ) desc?.let { Text( + modifier = Modifier.animateContentSize(tween()), text = it, style = MaterialTheme.typography.bodyMedium, color = (MaterialTheme.colorScheme.onSurface alwaysLight true).copy(alpha = 0.7f), diff --git a/app/src/main/java/me/ash/reader/ui/component/DisplayText.kt b/app/src/main/java/me/ash/reader/ui/component/DisplayText.kt index 1c64a02..2c0bc8f 100644 --- a/app/src/main/java/me/ash/reader/ui/component/DisplayText.kt +++ b/app/src/main/java/me/ash/reader/ui/component/DisplayText.kt @@ -1,6 +1,7 @@ package me.ash.reader.ui.component import androidx.compose.animation.* +import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -30,7 +31,9 @@ fun DisplayText( ) ) { Text( - modifier = Modifier.height(44.dp), + modifier = Modifier + .height(44.dp) + .animateContentSize(tween()), text = text, style = MaterialTheme.typography.displaySmall.copy( baselineShift = BaselineShift.Superscript diff --git a/app/src/main/java/me/ash/reader/ui/component/WebView.kt b/app/src/main/java/me/ash/reader/ui/component/WebView.kt index 921cf60..f19e9b7 100644 --- a/app/src/main/java/me/ash/reader/ui/component/WebView.kt +++ b/app/src/main/java/me/ash/reader/ui/component/WebView.kt @@ -70,7 +70,12 @@ fun WebView( ): Boolean { if (null == request?.url) return false 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 } diff --git a/app/src/main/java/me/ash/reader/ui/ext/FilterExt.kt b/app/src/main/java/me/ash/reader/ui/ext/FilterExt.kt index 82db922..8a2327a 100644 --- a/app/src/main/java/me/ash/reader/ui/ext/FilterExt.kt +++ b/app/src/main/java/me/ash/reader/ui/ext/FilterExt.kt @@ -11,10 +11,3 @@ fun Filter.getName(): String = when (this) { Filter.Starred -> stringResource(R.string.starred) 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) -} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/page/common/HomeEntry.kt b/app/src/main/java/me/ash/reader/ui/page/common/HomeEntry.kt index f7bd98b..378d1ea 100644 --- a/app/src/main/java/me/ash/reader/ui/page/common/HomeEntry.kt +++ b/app/src/main/java/me/ash/reader/ui/page/common/HomeEntry.kt @@ -3,16 +3,24 @@ package me.ash.reader.ui.page.common import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.background 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.graphics.Color 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.rememberAnimatedNavController import com.google.accompanist.systemuicontroller.rememberSystemUiController 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.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.SettingsPage 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) @Composable -fun HomeEntry() { +fun HomeEntry( + homeViewModel: HomeViewModel = hiltViewModel(), +) { + val viewState = homeViewModel.viewState.collectAsStateValue() + val pagingItems = viewState.pagingData.collectAsLazyPagingItems() + AppTheme { val context = LocalContext.current val useDarkTheme = LocalUseDarkTheme.current 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 { setStatusBarColor(Color.Transparent, !useDarkTheme) setSystemBarsColor(Color.Transparent, !useDarkTheme) @@ -37,13 +66,23 @@ fun HomeEntry() { AnimatedNavHost( modifier = Modifier.background(MaterialTheme.colorScheme.surface), navController = navController, - startDestination = if (context.isFirstLaunch) RouteName.STARTUP else RouteName.HOME, + startDestination = if (context.isFirstLaunch) RouteName.STARTUP else RouteName.FEEDS, ) { animatedComposable(route = RouteName.STARTUP) { StartupPage(navController) } - animatedComposable(route = RouteName.HOME) { - HomePage(navController) + animatedComposable(route = RouteName.FEEDS) { + 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) { SettingsPage(navController) diff --git a/app/src/main/java/me/ash/reader/ui/page/common/RouteName.kt b/app/src/main/java/me/ash/reader/ui/page/common/RouteName.kt index ecf7ec3..36a8c05 100644 --- a/app/src/main/java/me/ash/reader/ui/page/common/RouteName.kt +++ b/app/src/main/java/me/ash/reader/ui/page/common/RouteName.kt @@ -2,10 +2,9 @@ package me.ash.reader.ui.page.common object RouteName { const val STARTUP = "startup" - const val HOME = "home" - const val FEED = "feed" - const val ARTICLE = "article" - const val READ = "read" + const val FEEDS = "feeds" + const val FLOW = "flow" + const val READING = "reading" const val SETTINGS = "settings" const val COLOR_AND_STYLE = "color_and_style" const val TIPS_AND_SUPPORT = "tips_and_support" diff --git a/app/src/main/java/me/ash/reader/ui/page/home/HomePage.kt b/app/src/main/java/me/ash/reader/ui/page/home/HomePage.kt deleted file mode 100644 index a22439f..0000000 --- a/app/src/main/java/me/ash/reader/ui/page/home/HomePage.kt +++ /dev/null @@ -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() -} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/page/home/HomeViewModel.kt b/app/src/main/java/me/ash/reader/ui/page/home/HomeViewModel.kt index 128dea5..585607b 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/HomeViewModel.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/HomeViewModel.kt @@ -1,27 +1,30 @@ package me.ash.reader.ui.page.home import androidx.lifecycle.ViewModel +import androidx.paging.* import androidx.work.WorkManager import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.PagerState import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update +import kotlinx.coroutines.flow.* import me.ash.reader.data.entity.Feed import me.ash.reader.data.entity.Filter 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.StringsRepository 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 @OptIn(ExperimentalPagerApi::class) @HiltViewModel class HomeViewModel @Inject constructor( private val rssRepository: RssRepository, + private val stringsRepository: StringsRepository, + @ApplicationScope + private val applicationScope: CoroutineScope, workManager: WorkManager, ) : ViewModel() { @@ -37,11 +40,8 @@ class HomeViewModel @Inject constructor( when (action) { is HomeViewAction.Sync -> sync() is HomeViewAction.ChangeFilter -> changeFilter(action.filterState) - is HomeViewAction.ScrollToPage -> scrollToPage( - action.scope, - action.targetPage, - action.callback - ) + is HomeViewAction.FetchArticles -> fetchArticles() + is HomeViewAction.InputSearchContent -> inputSearchContent(action.content) } } @@ -57,10 +57,53 @@ class HomeViewModel @Inject constructor( filter = filterState.filter, ) } + fetchArticles() } - private fun scrollToPage(scope: CoroutineScope, targetPage: Int, callback: () -> Unit = {}) { - _viewState.value.pagerState.animateScrollToPage(scope, targetPage, callback) + private fun fetchArticles() { + _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) data class HomeViewState( val pagerState: PagerState = PagerState(0), + val pagingData: Flow> = emptyFlow(), + val searchContent: String = "", ) sealed class HomeViewAction { @@ -82,9 +127,9 @@ sealed class HomeViewAction { val filterState: FilterState ) : HomeViewAction() - data class ScrollToPage( - val scope: CoroutineScope, - val targetPage: Int, - val callback: () -> Unit = {}, + object FetchArticles : HomeViewAction() + + data class InputSearchContent( + val content: String, ) : HomeViewAction() } \ 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 74904d4..0473c38 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,6 +1,7 @@ package me.ash.reader.ui.page.home.feeds import android.annotation.SuppressLint +import androidx.activity.compose.BackHandler import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts 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.unit.dp import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.LiveData import androidx.navigation.NavHostController -import androidx.work.WorkInfo import kotlinx.coroutines.flow.map import me.ash.reader.R -import me.ash.reader.data.entity.Version import me.ash.reader.data.entity.toVersion import me.ash.reader.data.repository.SyncWorker.Companion.getIsSyncing 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.home.FilterBar 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.SubscribeViewAction 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 fun FeedsPage( - modifier: Modifier = Modifier, navController: NavHostController, feedsViewModel: FeedsViewModel = hiltViewModel(), - syncWorkLiveData: LiveData, - filterState: FilterState, subscribeViewModel: SubscribeViewModel = hiltViewModel(), - onSyncClick: () -> Unit = {}, - onFilterChange: (filterState: FilterState) -> Unit = {}, - onScrollToPage: (targetPage: Int) -> Unit = {}, + homeViewModel: HomeViewModel, ) { val context = LocalContext.current - val viewState = feedsViewModel.viewState.collectAsStateValue() + val feedsViewState = feedsViewModel.viewState.collectAsStateValue() + val filterState = homeViewModel.filterState.collectAsStateValue() val skipVersion = context.dataStore.data .map { it[DataStoreKeys.SkipVersionNumber.key] ?: "" } @@ -78,7 +76,7 @@ fun FeedsPage( val owner = LocalLifecycleOwner.current var isSyncing by remember { mutableStateOf(false) } - syncWorkLiveData.observe(owner) { + homeViewModel.syncWorkLiveData.observe(owner) { it?.let { isSyncing = it.progress.getIsSyncing() } } @@ -108,13 +106,13 @@ fun FeedsPage( } LaunchedEffect(filterState) { - feedsViewModel.dispatch(FeedsViewAction.FetchData(filterState)) + snapshotFlow { filterState }.collect { + feedsViewModel.dispatch(FeedsViewAction.FetchData(it)) + } } - LaunchedEffect(isSyncing) { - if (!isSyncing) { - feedsViewModel.dispatch(FeedsViewAction.FetchData(filterState)) - } + BackHandler(true) { + context.findActivity()?.moveTaskToBack(false) } Scaffold( @@ -133,7 +131,9 @@ fun FeedsPage( tint = MaterialTheme.colorScheme.onSurface, showBadge = latestVersion.whetherNeedUpdate(currentVersion, skipVersion), ) { - navController.navigate(RouteName.SETTINGS) + navController.navigate(RouteName.SETTINGS) { + popUpTo(RouteName.FEEDS) + } } }, actions = { @@ -143,9 +143,7 @@ fun FeedsPage( contentDescription = stringResource(R.string.refresh), tint = MaterialTheme.colorScheme.onSurface, ) { - if (!isSyncing) { - onSyncClick() - } + if (!isSyncing) homeViewModel.dispatch(HomeViewAction.Sync) } FeedbackIconButton( imageVector = Icons.Rounded.Add, @@ -158,7 +156,6 @@ fun FeedsPage( ) }, content = { - SubscribeDialog() LazyColumn { item { 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 "", ) } item { Banner( title = filterState.filter.getName(), - desc = filterState.filter.getDesc(), + desc = feedsViewState.importantCount, icon = filterState.filter.icon, action = { Icon( @@ -185,13 +182,14 @@ fun FeedsPage( ) }, ) { - onFilterChange( - filterState.copy( + filterChange( + navController = navController, + homeViewModel = homeViewModel, + filterState = filterState.copy( group = null, - feed = null + feed = null, ) ) - onScrollToPage(1) } } item { @@ -202,32 +200,34 @@ fun FeedsPage( ) Spacer(modifier = Modifier.height(8.dp)) } - itemsIndexed(viewState.groupWithFeedList) { index, groupWithFeed -> + itemsIndexed(feedsViewState.groupWithFeedList) { index, groupWithFeed -> // Crossfade(targetState = groupWithFeed) { groupWithFeed -> Column { GroupItem( group = groupWithFeed.group, feeds = groupWithFeed.feeds, groupOnClick = { - onFilterChange( - filterState.copy( + filterChange( + navController = navController, + homeViewModel = homeViewModel, + filterState = filterState.copy( group = groupWithFeed.group, - feed = null + feed = null, ) ) - onScrollToPage(1) }, feedOnClick = { feed -> - onFilterChange( - filterState.copy( + filterChange( + navController = navController, + homeViewModel = homeViewModel, + filterState = filterState.copy( group = null, - feed = feed + feed = feed, ) ) - onScrollToPage(1) } ) - if (index != viewState.groupWithFeedList.lastIndex) { + if (index != feedsViewState.groupWithFeedList.lastIndex) { Spacer(modifier = Modifier.height(8.dp)) } } @@ -246,14 +246,32 @@ fun FeedsPage( .fillMaxWidth(), filter = filterState.filter, filterOnClick = { - onFilterChange( - filterState.copy( - filter = it - ) + filterChange( + navController = navController, + 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) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsViewModel.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsViewModel.kt index 0b81c58..541b890 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsViewModel.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsViewModel.kt @@ -8,12 +8,13 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch +import me.ash.reader.R 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.repository.AccountRepository import me.ash.reader.data.repository.OpmlRepository import me.ash.reader.data.repository.RssRepository +import me.ash.reader.data.repository.StringsRepository import me.ash.reader.ui.page.home.FilterState import javax.inject.Inject @@ -22,6 +23,7 @@ class FeedsViewModel @Inject constructor( private val accountRepository: AccountRepository, private val rssRepository: RssRepository, private val opmlRepository: OpmlRepository, + private val stringsRepository: StringsRepository, ) : ViewModel() { private val _viewState = MutableStateFlow(FeedsViewState()) val viewState: StateFlow = _viewState.asStateFlow() @@ -105,19 +107,19 @@ class FeedsViewModel @Inject constructor( }.onEach { groupWithFeedList -> _viewState.update { it.copy( - filter = when { - isStarred -> Filter.Starred - isUnread -> Filter.Unread - else -> Filter.All - }.apply { - important = groupWithFeedList.sumOf { it.group.important ?: 0 } + importantCount = groupWithFeedList.sumOf { it.group.important ?: 0 }.run { + when { + isStarred -> stringsRepository.getString(R.string.unread_desc, this) + isUnread -> stringsRepository.getString(R.string.starred_desc, this) + else -> stringsRepository.getString(R.string.all_desc, this) + } }, groupWithFeedList = groupWithFeedList, feedsVisible = List(groupWithFeedList.size, init = { true }) ) } - }.catch { - Log.e("RLog", "catch in articleRepository.pullFeeds(): $this") + }.catch() { + Log.e("RLog", "catch in articleRepository.pullFeeds(): ${it.message}") }.flowOn(Dispatchers.Default).collect() } @@ -130,7 +132,7 @@ class FeedsViewModel @Inject constructor( data class FeedsViewState( val account: Account? = null, - val filter: Filter = Filter.All, + val importantCount: String = "", val groupWithFeedList: List = emptyList(), val feedsVisible: List = emptyList(), val listState: LazyListState = LazyListState(), diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/option/feed/FeedOptionDrawer.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/option/feed/FeedOptionDrawer.kt index 61ba69b..3b13b6f 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/option/feed/FeedOptionDrawer.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/option/feed/FeedOptionDrawer.kt @@ -1,6 +1,5 @@ package me.ash.reader.ui.page.home.feeds.option.feed -import android.widget.Toast import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.* import androidx.compose.material.ExperimentalMaterialApi 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 51d7b65..52074c8 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 @@ -1,5 +1,6 @@ package me.ash.reader.ui.page.home.flow +import android.util.Log import androidx.activity.compose.BackHandler import androidx.compose.animation.* 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.unit.dp import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.LiveData import androidx.navigation.NavHostController import androidx.paging.LoadState -import androidx.paging.compose.collectAsLazyPagingItems -import androidx.work.WorkInfo +import androidx.paging.compose.LazyPagingItems import kotlinx.coroutines.delay import kotlinx.coroutines.launch 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.ui.component.DisplayText import me.ash.reader.ui.component.FeedbackIconButton import me.ash.reader.ui.component.SwipeRefresh import me.ash.reader.ui.ext.collectAsStateValue 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.FilterState +import me.ash.reader.ui.page.home.HomeViewAction +import me.ash.reader.ui.page.home.HomeViewModel @OptIn( ExperimentalMaterial3Api::class, - ExperimentalFoundationApi::class, com.google.accompanist.pager.ExperimentalPagerApi::class, + ExperimentalFoundationApi::class, + com.google.accompanist.pager.ExperimentalPagerApi::class, androidx.compose.ui.ExperimentalComposeUiApi::class, ) @Composable fun FlowPage( - modifier: Modifier = Modifier, navController: NavHostController, flowViewModel: FlowViewModel = hiltViewModel(), - syncWorkLiveData: LiveData, - filterState: FilterState, - onFilterChange: (filterState: FilterState) -> Unit = {}, - onScrollToPage: (targetPage: Int) -> Unit = {}, - onItemClick: (item: ArticleWithFeed) -> Unit = {}, + homeViewModel: HomeViewModel, + pagingItems: LazyPagingItems, ) { val keyboardController = LocalSoftwareKeyboardController.current - val focusRequester = remember { FocusRequester() } + val scope = rememberCoroutineScope() + val focusRequester = remember { FocusRequester() } var markAsRead by remember { mutableStateOf(false) } var onSearch by remember { mutableStateOf(false) } + 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 owner = LocalLifecycleOwner.current var isSyncing by remember { mutableStateOf(false) } - syncWorkLiveData.observe(owner) { + homeViewModel.syncWorkLiveData.observe(owner) { it?.let { isSyncing = it.progress.getIsSyncing() } } - LaunchedEffect(filterState) { - snapshotFlow { filterState }.collect { - flowViewModel.dispatch( - FlowViewAction.FetchData(it) - ) - } - } - LaunchedEffect(onSearch) { snapshotFlow { onSearch }.collect { if (it) { @@ -87,8 +80,8 @@ fun FlowPage( focusRequester.requestFocus() } else { keyboardController?.hide() - if (viewState.searchContent.isNotBlank()) { - flowViewModel.dispatch(FlowViewAction.InputSearchContent("")) + if (homeViewState.searchContent.isNotBlank()) { + homeViewModel.dispatch(HomeViewAction.InputSearchContent("")) } } } @@ -96,6 +89,7 @@ fun FlowPage( LaunchedEffect(viewState.listState) { snapshotFlow { viewState.listState.firstVisibleItemIndex }.collect { + Log.i("RLog", "FlowPage: ${it}") if (it > 0) { keyboardController?.hide() } @@ -121,7 +115,7 @@ fun FlowPage( tint = MaterialTheme.colorScheme.onSurface ) { onSearch = false - onScrollToPage(0) + navController.popBackStack() } }, actions = { @@ -215,7 +209,7 @@ fun FlowPage( exit = fadeOut() + shrinkVertically(), ) { SearchBar( - value = viewState.searchContent, + value = homeViewState.searchContent, placeholder = when { filterState.group != null -> stringResource( R.string.search_for_in, @@ -234,11 +228,11 @@ fun FlowPage( }, focusRequester = focusRequester, onValueChange = { - flowViewModel.dispatch(FlowViewAction.InputSearchContent(it)) + homeViewModel.dispatch(HomeViewAction.InputSearchContent(it)) }, onClose = { onSearch = false - flowViewModel.dispatch(FlowViewAction.InputSearchContent("")) + homeViewModel.dispatch(HomeViewAction.InputSearchContent("")) } ) Spacer(modifier = Modifier.height((56 + 24 + 10).dp)) @@ -248,7 +242,9 @@ fun FlowPage( pagingItems = pagingItems, ) { onSearch = false - onItemClick(it) + navController.navigate("${RouteName.READING}/${it.article.id}") { + popUpTo(RouteName.FLOW) + } } item { Spacer(modifier = Modifier.height(64.dp)) @@ -266,7 +262,9 @@ fun FlowPage( .fillMaxWidth(), filter = filterState.filter, filterOnClick = { - onFilterChange(filterState.copy(filter = it)) + flowViewModel.dispatch(FlowViewAction.ScrollToItem(0)) + homeViewModel.dispatch(HomeViewAction.ChangeFilter(filterState.copy(filter = it))) + homeViewModel.dispatch(HomeViewAction.FetchArticles) }, ) } @@ -287,4 +285,4 @@ private fun DisplayTextHeader( }, desc = if (isSyncing) stringResource(R.string.syncing) else "", ) -} \ No newline at end of file +} diff --git a/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowViewModel.kt b/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowViewModel.kt index 2b0d893..c115dac 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowViewModel.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowViewModel.kt @@ -3,21 +3,20 @@ package me.ash.reader.ui.page.home.flow import androidx.compose.foundation.lazy.LazyListState import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import androidx.paging.* 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 me.ash.reader.data.entity.ArticleWithFeed 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 javax.inject.Inject @HiltViewModel class FlowViewModel @Inject constructor( private val rssRepository: RssRepository, - private val stringsRepository: StringsRepository, ) : ViewModel() { private val _viewState = MutableStateFlow(ArticleViewState()) val viewState: StateFlow = _viewState.asStateFlow() @@ -25,7 +24,6 @@ class FlowViewModel @Inject constructor( fun dispatch(action: FlowViewAction) { when (action) { is FlowViewAction.Sync -> sync() - is FlowViewAction.FetchData -> fetchData(action.filterState) is FlowViewAction.ChangeIsBack -> changeIsBack(action.isBack) is FlowViewAction.ScrollToItem -> scrollToItem(action.index) is FlowViewAction.MarkAsRead -> markAsRead( @@ -34,7 +32,6 @@ class FlowViewModel @Inject constructor( action.articleId, action.markAsReadBefore, ) - is FlowViewAction.InputSearchContent -> inputSearchContent(action.content) } } @@ -42,77 +39,6 @@ class FlowViewModel @Inject constructor( 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) { viewModelScope.launch { _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( - val filterState: FilterState? = null, val filterImportant: Int = 0, val listState: LazyListState = LazyListState(), val isBack: Boolean = false, - val pagingData: Flow> = emptyFlow(), val syncWorkInfo: String = "", - val searchContent: String = "", ) sealed class FlowViewAction { object Sync : FlowViewAction() - data class FetchData( - val filterState: FilterState, - ) : FlowViewAction() - data class ChangeIsBack( val isBack: Boolean ) : FlowViewAction() @@ -197,10 +107,6 @@ sealed class FlowViewAction { val articleId: String?, val markAsReadBefore: MarkAsReadBefore ) : FlowViewAction() - - data class InputSearchContent( - val content: String, - ) : FlowViewAction() } enum class MarkAsReadBefore { diff --git a/app/src/main/java/me/ash/reader/ui/page/home/read/Header.kt b/app/src/main/java/me/ash/reader/ui/page/home/read/Header.kt index 90e6dd0..7838f8b 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/read/Header.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/read/Header.kt @@ -23,7 +23,7 @@ fun Header( modifier = Modifier .fillMaxWidth() .roundClick { - articleWithFeed.article.link .let { + articleWithFeed.article.link.let { if (it.isNotEmpty()) { context.startActivity( Intent(Intent.ACTION_VIEW, Uri.parse(articleWithFeed.article.link)) diff --git a/app/src/main/java/me/ash/reader/ui/page/home/read/ReadPage.kt b/app/src/main/java/me/ash/reader/ui/page/home/read/ReadPage.kt index 48cab21..6e91c70 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/read/ReadPage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/read/ReadPage.kt @@ -31,13 +31,19 @@ import me.ash.reader.ui.ext.collectAsStateValue @Composable fun ReadPage( navController: NavHostController, - modifier: Modifier = Modifier, readViewModel: ReadViewModel = hiltViewModel(), - onScrollToPage: (targetPage: Int, callback: () -> Unit) -> Unit = { _, _ -> }, ) { val viewState = readViewModel.viewState.collectAsStateValue() 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) { LaunchedEffect(Unit) { Log.i("RLog", "scroll: start") @@ -84,10 +90,9 @@ fun ReadPage( TopBar( isShow = viewState.articleWithFeed == null || !isScrollDown, isShowActions = viewState.articleWithFeed != null, - onScrollToPage = onScrollToPage, - onClearArticle = { - readViewModel.dispatch(ReadViewAction.ClearArticle) - } + onClose = { + navController.popBackStack() + }, ) } Content( @@ -127,8 +132,7 @@ fun ReadPage( private fun TopBar( isShow: Boolean, isShowActions: Boolean = false, - onScrollToPage: (targetPage: Int, callback: () -> Unit) -> Unit = { _, _ -> }, - onClearArticle: () -> Unit = {}, + onClose: () -> Unit = {}, ) { AnimatedVisibility( visible = isShow, @@ -147,9 +151,7 @@ private fun TopBar( contentDescription = stringResource(R.string.close), tint = MaterialTheme.colorScheme.onSurface ) { - onScrollToPage(1) { - onClearArticle() - } + onClose() } }, actions = { diff --git a/app/src/main/java/me/ash/reader/ui/page/settings/SettingItem.kt b/app/src/main/java/me/ash/reader/ui/page/settings/SettingItem.kt index e402670..00d2644 100644 --- a/app/src/main/java/me/ash/reader/ui/page/settings/SettingItem.kt +++ b/app/src/main/java/me/ash/reader/ui/page/settings/SettingItem.kt @@ -32,7 +32,9 @@ fun SettingItem( action: (@Composable () -> Unit)? = null ) { 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 ) { Row( diff --git a/app/src/main/java/me/ash/reader/ui/page/settings/SettingsPage.kt b/app/src/main/java/me/ash/reader/ui/page/settings/SettingsPage.kt index dda4f4f..1118a63 100644 --- a/app/src/main/java/me/ash/reader/ui/page/settings/SettingsPage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/settings/SettingsPage.kt @@ -65,7 +65,7 @@ fun SettingsPage( contentDescription = stringResource(R.string.back), tint = MaterialTheme.colorScheme.onSurface ) { - navController.navigate(RouteName.HOME) + navController.popBackStack() } }, actions = {} @@ -119,7 +119,9 @@ fun SettingsPage( desc = stringResource(R.string.color_and_style_desc), icon = Icons.Outlined.Palette, ) { - navController.navigate(RouteName.COLOR_AND_STYLE) + navController.navigate(RouteName.COLOR_AND_STYLE) { + popUpTo(RouteName.SETTINGS) + } } } item { @@ -144,7 +146,9 @@ fun SettingsPage( desc = stringResource(R.string.tips_and_support_desc), icon = Icons.Outlined.TipsAndUpdates, ) { - navController.navigate(RouteName.TIPS_AND_SUPPORT) + navController.navigate(RouteName.TIPS_AND_SUPPORT) { + popUpTo(RouteName.SETTINGS) + } } } } diff --git a/app/src/main/java/me/ash/reader/ui/page/startup/StartupPage.kt b/app/src/main/java/me/ash/reader/ui/page/startup/StartupPage.kt index cde4063..da7c51d 100644 --- a/app/src/main/java/me/ash/reader/ui/page/startup/StartupPage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/startup/StartupPage.kt @@ -102,7 +102,7 @@ fun StartupPage( floatingActionButton = { ExtendedFloatingActionButton( onClick = { - navController.navigate(route = RouteName.HOME) + navController.navigate(RouteName.FEEDS) scope.launch { context.dataStore.put(DataStoreKeys.IsFirstLaunch, false) } diff --git a/app/src/main/java/me/ash/reader/ui/theme/palette/colorspace/jzazbz/Jzazbz.kt b/app/src/main/java/me/ash/reader/ui/theme/palette/colorspace/jzazbz/Jzazbz.kt index b018380..e3d1220 100644 --- a/app/src/main/java/me/ash/reader/ui/theme/palette/colorspace/jzazbz/Jzazbz.kt +++ b/app/src/main/java/me/ash/reader/ui/theme/palette/colorspace/jzazbz/Jzazbz.kt @@ -18,14 +18,14 @@ data class Jzazbz( ) { fun toXyz(): CieXyz { val (x_, y_, z) = lmsToXyz * ( - IzazbzToLms * doubleArrayOf( - (Jz + d_0) / (1.0 + d - d * (Jz + d_0)), - az, - bz, - ) - ).map { - 10000.0 * ((c_1 - it.pow(1.0 / p)) / (c_3 * it.pow(1.0 / p) - c_2)).pow(1.0 / n) - }.toDoubleArray() + IzazbzToLms * doubleArrayOf( + (Jz + d_0) / (1.0 + d - d * (Jz + d_0)), + az, + bz, + ) + ).map { + 10000.0 * ((c_1 - it.pow(1.0 / p)) / (c_3 * it.pow(1.0 / p) - c_2)).pow(1.0 / n) + }.toDoubleArray() val x = (x_ + (b - 1.0) * z) / b val y = (y_ + (g - 1.0) * x) / g return CieXyz( @@ -61,14 +61,16 @@ data class Jzazbz( fun CieXyz.toJzazbz(): Jzazbz { val (Iz, az, bz) = lmsToIzazbz * ( - xyzToLms * doubleArrayOf( - b * x - (b - 1.0) * z, - g * y - (g - 1.0) * x, - z, - ) - ).map { - ((c_1 + c_2 * (it / 10000.0).pow(n)) / (1.0 + c_3 * (it / 10000.0).pow(n))).pow(p) - }.toDoubleArray() + xyzToLms * doubleArrayOf( + b * x - (b - 1.0) * z, + g * y - (g - 1.0) * x, + z, + ) + ).map { + ((c_1 + c_2 * (it / 10000.0).pow(n)) / (1.0 + c_3 * (it / 10000.0).pow(n))).pow( + p + ) + }.toDoubleArray() return Jzazbz( Jz = (1.0 + d) * Iz / (1.0 + d * Iz) - d_0, az = az, diff --git a/app/src/main/java/me/ash/reader/ui/theme/palette/colorspace/rgb/Rgb.kt b/app/src/main/java/me/ash/reader/ui/theme/palette/colorspace/rgb/Rgb.kt index 9f43047..d816f90 100644 --- a/app/src/main/java/me/ash/reader/ui/theme/palette/colorspace/rgb/Rgb.kt +++ b/app/src/main/java/me/ash/reader/ui/theme/palette/colorspace/rgb/Rgb.kt @@ -22,13 +22,14 @@ data class Rgb( 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 = ( - colorSpace.rgbToXyzMatrix * rgb.map { - colorSpace.transferFunction.EOTF(it) - }.toDoubleArray() - ).asXyz() * luminance + colorSpace.rgbToXyzMatrix * rgb.map { + colorSpace.transferFunction.EOTF(it) + }.toDoubleArray() + ).asXyz() * luminance override fun toString(): String = "Rgb(r=$r, g=$g, b=$b, colorSpace=${colorSpace.name})" @@ -40,6 +41,7 @@ data class Rgb( .map { colorSpace.transferFunction.OETF(it) } .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) } } diff --git a/app/src/main/java/me/ash/reader/ui/theme/palette/colorspace/rgb/transferfunction/PQTransferFunction.kt b/app/src/main/java/me/ash/reader/ui/theme/palette/colorspace/rgb/transferfunction/PQTransferFunction.kt index 5a832f0..408ec20 100644 --- a/app/src/main/java/me/ash/reader/ui/theme/palette/colorspace/rgb/transferfunction/PQTransferFunction.kt +++ b/app/src/main/java/me/ash/reader/ui/theme/palette/colorspace/rgb/transferfunction/PQTransferFunction.kt @@ -22,9 +22,11 @@ class PQTransferFunction : TransferFunction { } 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( - m_2 - ) + override fun OETF(x: Double): Double = + ((c_1 + c_2 * x / 10000.0) / (1 + c_3 * x / 10000.0)).pow( + m_2 + ) } diff --git a/app/src/main/java/me/ash/reader/ui/theme/palette/colorspace/zcam/Izazbz.kt b/app/src/main/java/me/ash/reader/ui/theme/palette/colorspace/zcam/Izazbz.kt index 2beb18c..00a8609 100644 --- a/app/src/main/java/me/ash/reader/ui/theme/palette/colorspace/zcam/Izazbz.kt +++ b/app/src/main/java/me/ash/reader/ui/theme/palette/colorspace/zcam/Izazbz.kt @@ -54,16 +54,16 @@ data class Izazbz( fun CieXyz.toIzazbz(): Izazbz { val (I, az, bz) = lmsToIzazbz * ( - xyzToLms * doubleArrayOf( - b * x - (b - 1.0) * z, - g * y - (g - 1.0) * x, - z, - ) - ).map { - ((c_1 + c_2 * (it / 10000.0).pow(eta)) / (1.0 + c_3 * (it / 10000.0).pow(eta))).pow( - rho - ) - }.toDoubleArray() + xyzToLms * doubleArrayOf( + b * x - (b - 1.0) * z, + g * y - (g - 1.0) * x, + z, + ) + ).map { + ((c_1 + c_2 * (it / 10000.0).pow(eta)) / (1.0 + c_3 * (it / 10000.0).pow(eta))).pow( + rho + ) + }.toDoubleArray() return Izazbz( Iz = I - epsilon, az = az, diff --git a/app/src/main/java/me/ash/reader/ui/theme/palette/colorspace/zcam/Zcam.kt b/app/src/main/java/me/ash/reader/ui/theme/palette/colorspace/zcam/Zcam.kt index 9d5e5b7..77e875c 100644 --- a/app/src/main/java/me/ash/reader/ui/theme/palette/colorspace/zcam/Zcam.kt +++ b/app/src/main/java/me/ash/reader/ui/theme/palette/colorspace/zcam/Zcam.kt @@ -37,12 +37,12 @@ data class Zcam( } with(cond) { val Iz = ( - when { - !Qz.isNaN() -> Qz - !Jz.isNaN() -> Jz * Qzw / 100.0 - else -> Double.NaN - } / (2700.0 * F_s.pow(2.2) * sqrt(F_b) * F_L.pow(0.2)) - ).pow(F_b.pow(0.12) / (1.6 * F_s)) + when { + !Qz.isNaN() -> Qz + !Jz.isNaN() -> Jz * Qzw / 100.0 + else -> Double.NaN + } / (2700.0 * F_s.pow(2.2) * sqrt(F_b) * F_L.pow(0.2)) + ).pow(F_b.pow(0.12) / (1.6 * F_s)) val Jz = Jz.takeUnless { it.isNaN() } ?: when { !Qz.isNaN() -> 100.0 * Qz / Qzw else -> Double.NaN @@ -98,7 +98,8 @@ data class Zcam( if (!current.toIzazbz().toXyz().toRgb(cond.luminance, colorSpace).isInGamut()) { high = mid } 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()) { low = mid } else { @@ -124,19 +125,22 @@ data class Zcam( 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 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 { with(cond) { val hz = atan2(bz, az).toDegrees().mod(360.0) // hue angle 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 ez = 1.015 + cos(89.038 + hz).toRadians() // ~ eccentricity factor val Mz = 100.0 * (square(az) + square(bz)).pow(0.37) * ez.pow(0.068) * F_L.pow(0.2) / - (F_b.pow(0.1) * Izw.pow(0.78)) // colorfulness + (F_b.pow(0.1) * Izw.pow(0.78)) // colorfulness val Cz = 100.0 * Mz / Qzw // chroma val Sz = 100.0 * F_L.pow(0.6) * sqrt(Mz / Qz) // saturation diff --git a/app/src/main/java/me/ash/reader/ui/theme/palette/core/ColorUtils.kt b/app/src/main/java/me/ash/reader/ui/theme/palette/core/ColorUtils.kt index 37f433f..04134bd 100644 --- a/app/src/main/java/me/ash/reader/ui/theme/palette/core/ColorUtils.kt +++ b/app/src/main/java/me/ash/reader/ui/theme/palette/core/ColorUtils.kt @@ -63,5 +63,10 @@ fun animateZcamLchAsState( } ) } - return animateValueAsState(targetValue, converter, animationSpec, finishedListener = finishedListener) + return animateValueAsState( + targetValue, + converter, + animationSpec, + finishedListener = finishedListener + ) } diff --git a/app/src/main/java/me/ash/reader/ui/theme/palette/util/MathUtils.kt b/app/src/main/java/me/ash/reader/ui/theme/palette/util/MathUtils.kt index cdcd13d..0975b3d 100644 --- a/app/src/main/java/me/ash/reader/ui/theme/palette/util/MathUtils.kt +++ b/app/src/main/java/me/ash/reader/ui/theme/palette/util/MathUtils.kt @@ -38,8 +38,8 @@ class Matrix3( private fun determinant(): Double = x[0] * (y[1] * z[2] - y[2] * z[1]) - - x[1] * (y[0] * z[2] - y[2] * z[0]) + - x[2] * (y[0] * z[1] - y[1] * z[0]) + x[1] * (y[0] * z[2] - y[2] * z[0]) + + x[2] * (y[0] * z[1] - y[1] * z[0]) private fun transpose() = Matrix3( doubleArrayOf(x[0], y[0], z[0]), @@ -60,5 +60,6 @@ class Matrix3( 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() + "]" } + "}" }