mirror of https://github.com/Ashinch/ReadYou.git
Merge branch 'feature/separate' into main
This commit is contained in:
commit
ee5e6e3687
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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<PagingData<FlowItemView>> = 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()
|
||||
}
|
|
@ -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<WorkInfo>,
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<FeedsViewState> = _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<GroupWithFeed> = emptyList(),
|
||||
val feedsVisible: List<Boolean> = emptyList(),
|
||||
val listState: LazyListState = LazyListState(),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<WorkInfo>,
|
||||
filterState: FilterState,
|
||||
onFilterChange: (filterState: FilterState) -> Unit = {},
|
||||
onScrollToPage: (targetPage: Int) -> Unit = {},
|
||||
onItemClick: (item: ArticleWithFeed) -> Unit = {},
|
||||
homeViewModel: HomeViewModel,
|
||||
pagingItems: LazyPagingItems<FlowItemView>,
|
||||
) {
|
||||
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 "",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<ArticleViewState> = _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<PagingData<FlowItemView>> = 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 {
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -63,5 +63,10 @@ fun animateZcamLchAsState(
|
|||
}
|
||||
)
|
||||
}
|
||||
return animateValueAsState(targetValue, converter, animationSpec, finishedListener = finishedListener)
|
||||
return animateValueAsState(
|
||||
targetValue,
|
||||
converter,
|
||||
animationSpec,
|
||||
finishedListener = finishedListener
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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() + "]" } + "}"
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue