Merge branch 'feature/separate' into main

This commit is contained in:
Ash 2022-04-28 23:18:45 +08:00
commit ee5e6e3687
27 changed files with 318 additions and 450 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,27 +1,30 @@
package me.ash.reader.ui.page.home
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()
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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