Rename UiState

This commit is contained in:
Ash 2022-05-21 05:36:32 +08:00
parent dcbb41f3ab
commit efdff0e49c
29 changed files with 648 additions and 978 deletions

View File

@ -15,11 +15,10 @@ import com.google.accompanist.systemuicontroller.rememberSystemUiController
import me.ash.reader.data.model.Filter import me.ash.reader.data.model.Filter
import me.ash.reader.data.preference.LocalDarkTheme import me.ash.reader.data.preference.LocalDarkTheme
import me.ash.reader.ui.ext.* import me.ash.reader.ui.ext.*
import me.ash.reader.ui.page.home.HomeViewAction
import me.ash.reader.ui.page.home.HomeViewModel import me.ash.reader.ui.page.home.HomeViewModel
import me.ash.reader.ui.page.home.feeds.FeedsPage 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.flow.FlowPage
import me.ash.reader.ui.page.home.read.ReadPage import me.ash.reader.ui.page.home.reading.ReadingPage
import me.ash.reader.ui.page.settings.SettingsPage import me.ash.reader.ui.page.settings.SettingsPage
import me.ash.reader.ui.page.settings.color.ColorAndStylePage import me.ash.reader.ui.page.settings.color.ColorAndStylePage
import me.ash.reader.ui.page.settings.color.DarkThemePage import me.ash.reader.ui.page.settings.color.DarkThemePage
@ -37,7 +36,7 @@ fun HomeEntry(
homeViewModel: HomeViewModel = hiltViewModel(), homeViewModel: HomeViewModel = hiltViewModel(),
) { ) {
val context = LocalContext.current val context = LocalContext.current
val filterState = homeViewModel.filterState.collectAsStateValue() val filterUiState = homeViewModel.filterUiState.collectAsStateValue()
val navController = rememberAnimatedNavController() val navController = rememberAnimatedNavController()
val intent by rememberSaveable { mutableStateOf(context.findActivity()?.intent) } val intent by rememberSaveable { mutableStateOf(context.findActivity()?.intent) }
@ -57,16 +56,14 @@ fun HomeEntry(
// Other initial pages // Other initial pages
} }
homeViewModel.dispatch( homeViewModel.changeFilter(
HomeViewAction.ChangeFilter( filterUiState.copy(
filterState.copy( filter = when (context.initialFilter) {
filter = when (context.initialFilter) { 0 -> Filter.Starred
0 -> Filter.Starred 1 -> Filter.Unread
1 -> Filter.Unread 2 -> Filter.All
2 -> Filter.All else -> Filter.All
else -> Filter.All }
}
)
) )
) )
} }
@ -114,7 +111,7 @@ fun HomeEntry(
) )
} }
animatedComposable(route = "${RouteName.READING}/{articleId}") { animatedComposable(route = "${RouteName.READING}/{articleId}") {
ReadPage(navController = navController) ReadingPage(navController = navController)
} }
// Settings // Settings

View File

@ -7,8 +7,8 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import me.ash.reader.data.entity.Feed import me.ash.reader.data.entity.Feed
import me.ash.reader.data.model.Filter
import me.ash.reader.data.entity.Group import me.ash.reader.data.entity.Group
import me.ash.reader.data.model.Filter
import me.ash.reader.data.module.ApplicationScope import me.ash.reader.data.module.ApplicationScope
import me.ash.reader.data.repository.RssRepository import me.ash.reader.data.repository.RssRepository
import me.ash.reader.data.repository.StringsRepository import me.ash.reader.data.repository.StringsRepository
@ -24,30 +24,20 @@ class HomeViewModel @Inject constructor(
private val applicationScope: CoroutineScope, private val applicationScope: CoroutineScope,
private val workManager: WorkManager, private val workManager: WorkManager,
) : ViewModel() { ) : ViewModel() {
private val _homeUiState = MutableStateFlow(HomeUiState())
val homeUiState: StateFlow<HomeUiState> = _homeUiState.asStateFlow()
private val _viewState = MutableStateFlow(HomeViewState()) private val _filterUiState = MutableStateFlow(FilterState())
val viewState: StateFlow<HomeViewState> = _viewState.asStateFlow() val filterUiState = _filterUiState.asStateFlow()
private val _filterState = MutableStateFlow(FilterState())
val filterState = _filterState.asStateFlow()
val syncWorkLiveData = workManager.getWorkInfoByIdLiveData(SyncWorker.UUID) val syncWorkLiveData = workManager.getWorkInfoByIdLiveData(SyncWorker.UUID)
fun dispatch(action: HomeViewAction) { fun sync() {
when (action) {
is HomeViewAction.Sync -> sync()
is HomeViewAction.ChangeFilter -> changeFilter(action.filterState)
is HomeViewAction.FetchArticles -> fetchArticles()
is HomeViewAction.InputSearchContent -> inputSearchContent(action.content)
}
}
private fun sync() {
rssRepository.get().doSync() rssRepository.get().doSync()
} }
private fun changeFilter(filterState: FilterState) { fun changeFilter(filterState: FilterState) {
_filterState.update { _filterUiState.update {
it.copy( it.copy(
group = filterState.group, group = filterState.group,
feed = filterState.feed, feed = filterState.feed,
@ -57,24 +47,24 @@ class HomeViewModel @Inject constructor(
fetchArticles() fetchArticles()
} }
private fun fetchArticles() { fun fetchArticles() {
_viewState.update { _homeUiState.update {
it.copy( it.copy(
pagingData = Pager(PagingConfig(pageSize = 50)) { pagingData = Pager(PagingConfig(pageSize = 50)) {
if (_viewState.value.searchContent.isNotBlank()) { if (_homeUiState.value.searchContent.isNotBlank()) {
rssRepository.get().searchArticles( rssRepository.get().searchArticles(
content = _viewState.value.searchContent.trim(), content = _homeUiState.value.searchContent.trim(),
groupId = _filterState.value.group?.id, groupId = _filterUiState.value.group?.id,
feedId = _filterState.value.feed?.id, feedId = _filterUiState.value.feed?.id,
isStarred = _filterState.value.filter.isStarred(), isStarred = _filterUiState.value.filter.isStarred(),
isUnread = _filterState.value.filter.isUnread(), isUnread = _filterUiState.value.filter.isUnread(),
) )
} else { } else {
rssRepository.get().pullArticles( rssRepository.get().pullArticles(
groupId = _filterState.value.group?.id, groupId = _filterUiState.value.group?.id,
feedId = _filterState.value.feed?.id, feedId = _filterUiState.value.feed?.id,
isStarred = _filterState.value.filter.isStarred(), isStarred = _filterUiState.value.filter.isStarred(),
isUnread = _filterState.value.filter.isUnread(), isUnread = _filterUiState.value.filter.isUnread(),
) )
} }
}.flow.map { }.flow.map {
@ -94,8 +84,8 @@ class HomeViewModel @Inject constructor(
} }
} }
private fun inputSearchContent(content: String) { fun inputSearchContent(content: String) {
_viewState.update { _homeUiState.update {
it.copy( it.copy(
searchContent = content, searchContent = content,
) )
@ -110,21 +100,7 @@ data class FilterState(
val filter: Filter = Filter.All, val filter: Filter = Filter.All,
) )
data class HomeViewState( data class HomeUiState(
val pagingData: Flow<PagingData<FlowItemView>> = emptyFlow(), val pagingData: Flow<PagingData<FlowItemView>> = emptyFlow(),
val searchContent: String = "", val searchContent: String = "",
) )
sealed class HomeViewAction {
object Sync : HomeViewAction()
data class ChangeFilter(
val filterState: FilterState
) : HomeViewAction()
object FetchArticles : HomeViewAction()
data class InputSearchContent(
val content: String,
) : HomeViewAction()
}

View File

@ -21,7 +21,6 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import me.ash.reader.data.entity.Feed import me.ash.reader.data.entity.Feed
import me.ash.reader.ui.component.FeedIcon import me.ash.reader.ui.component.FeedIcon
import me.ash.reader.ui.page.home.feeds.drawer.feed.FeedOptionViewAction
import me.ash.reader.ui.page.home.feeds.drawer.feed.FeedOptionViewModel import me.ash.reader.ui.page.home.feeds.drawer.feed.FeedOptionViewModel
import kotlin.math.ln import kotlin.math.ln
@ -55,7 +54,7 @@ fun FeedItem(
}, },
onLongClick = { onLongClick = {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
feedOptionViewModel.dispatch(FeedOptionViewAction.Show(scope, feed.id)) feedOptionViewModel.showDrawer(scope, feed.id)
} }
) )
.padding(vertical = 14.dp), .padding(vertical = 14.dp),

View File

@ -39,12 +39,10 @@ import me.ash.reader.ui.ext.findActivity
import me.ash.reader.ui.ext.getCurrentVersion import me.ash.reader.ui.ext.getCurrentVersion
import me.ash.reader.ui.page.common.RouteName import me.ash.reader.ui.page.common.RouteName
import me.ash.reader.ui.page.home.FilterState import me.ash.reader.ui.page.home.FilterState
import me.ash.reader.ui.page.home.HomeViewAction
import me.ash.reader.ui.page.home.HomeViewModel import me.ash.reader.ui.page.home.HomeViewModel
import me.ash.reader.ui.page.home.feeds.drawer.feed.FeedOptionDrawer import me.ash.reader.ui.page.home.feeds.drawer.feed.FeedOptionDrawer
import me.ash.reader.ui.page.home.feeds.drawer.group.GroupOptionDrawer import me.ash.reader.ui.page.home.feeds.drawer.group.GroupOptionDrawer
import me.ash.reader.ui.page.home.feeds.subscribe.SubscribeDialog import me.ash.reader.ui.page.home.feeds.subscribe.SubscribeDialog
import me.ash.reader.ui.page.home.feeds.subscribe.SubscribeViewAction
import me.ash.reader.ui.page.home.feeds.subscribe.SubscribeViewModel import me.ash.reader.ui.page.home.feeds.subscribe.SubscribeViewModel
@OptIn( @OptIn(
@ -67,8 +65,8 @@ fun FeedsPage(
val filterBarPadding = LocalFeedsFilterBarPadding.current val filterBarPadding = LocalFeedsFilterBarPadding.current
val filterBarTonalElevation = LocalFeedsFilterBarTonalElevation.current val filterBarTonalElevation = LocalFeedsFilterBarTonalElevation.current
val feedsViewState = feedsViewModel.viewState.collectAsStateValue() val feedsUiState = feedsViewModel.feedsUiState.collectAsStateValue()
val filterState = homeViewModel.filterState.collectAsStateValue() val filterUiState = homeViewModel.filterUiState.collectAsStateValue()
val newVersion = LocalNewVersionNumber.current val newVersion = LocalNewVersionNumber.current
val skipVersion = LocalSkipVersionNumber.current val skipVersion = LocalSkipVersionNumber.current
@ -92,22 +90,22 @@ fun FeedsPage(
val launcher = rememberLauncherForActivityResult( val launcher = rememberLauncherForActivityResult(
ActivityResultContracts.CreateDocument() ActivityResultContracts.CreateDocument()
) { result -> ) { result ->
feedsViewModel.dispatch(FeedsViewAction.ExportAsString { string -> feedsViewModel.exportAsOpml { string ->
result?.let { uri -> result?.let { uri ->
context.contentResolver.openOutputStream(uri)?.let { outputStream -> context.contentResolver.openOutputStream(uri)?.let { outputStream ->
outputStream.write(string.toByteArray()) outputStream.write(string.toByteArray())
} }
} }
}) }
} }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
feedsViewModel.dispatch(FeedsViewAction.FetchAccount) feedsViewModel.fetchAccount()
} }
LaunchedEffect(filterState) { LaunchedEffect(filterUiState) {
snapshotFlow { filterState }.collect { snapshotFlow { filterUiState }.collect {
feedsViewModel.dispatch(FeedsViewAction.FetchData(it)) feedsViewModel.fetchData(it)
} }
} }
@ -138,14 +136,14 @@ fun FeedsPage(
contentDescription = stringResource(R.string.refresh), contentDescription = stringResource(R.string.refresh),
tint = MaterialTheme.colorScheme.onSurface, tint = MaterialTheme.colorScheme.onSurface,
) { ) {
if (!isSyncing) homeViewModel.dispatch(HomeViewAction.Sync) if (!isSyncing) homeViewModel.sync()
} }
FeedbackIconButton( FeedbackIconButton(
imageVector = Icons.Rounded.Add, imageVector = Icons.Rounded.Add,
contentDescription = stringResource(R.string.subscribe), contentDescription = stringResource(R.string.subscribe),
tint = MaterialTheme.colorScheme.onSurface, tint = MaterialTheme.colorScheme.onSurface,
) { ) {
subscribeViewModel.dispatch(SubscribeViewAction.Show) subscribeViewModel.showDrawer()
} }
}, },
content = { content = {
@ -159,15 +157,15 @@ fun FeedsPage(
} }
) )
}, },
text = feedsViewState.account?.name ?: stringResource(R.string.read_you), text = feedsUiState.account?.name ?: stringResource(R.string.read_you),
desc = if (isSyncing) stringResource(R.string.syncing) else "", desc = if (isSyncing) stringResource(R.string.syncing) else "",
) )
} }
item { item {
Banner( Banner(
title = filterState.filter.getName(), title = filterUiState.filter.getName(),
desc = feedsViewState.importantCount.ifEmpty { stringResource(R.string.loading) }, desc = feedsUiState.importantCount.ifEmpty { stringResource(R.string.loading) },
icon = filterState.filter.iconOutline, icon = filterUiState.filter.iconOutline,
action = { action = {
Icon( Icon(
imageVector = Icons.Outlined.KeyboardArrowRight, imageVector = Icons.Outlined.KeyboardArrowRight,
@ -178,7 +176,7 @@ fun FeedsPage(
filterChange( filterChange(
navController = navController, navController = navController,
homeViewModel = homeViewModel, homeViewModel = homeViewModel,
filterState = filterState.copy( filterState = filterUiState.copy(
group = null, group = null,
feed = null, feed = null,
) )
@ -193,7 +191,7 @@ fun FeedsPage(
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
} }
itemsIndexed(feedsViewState.groupWithFeedList) { index, groupWithFeed -> itemsIndexed(feedsUiState.groupWithFeedList) { index, groupWithFeed ->
// Crossfade(targetState = groupWithFeed) { groupWithFeed -> // Crossfade(targetState = groupWithFeed) { groupWithFeed ->
Column { Column {
GroupItem( GroupItem(
@ -205,7 +203,7 @@ fun FeedsPage(
filterChange( filterChange(
navController = navController, navController = navController,
homeViewModel = homeViewModel, homeViewModel = homeViewModel,
filterState = filterState.copy( filterState = filterUiState.copy(
group = groupWithFeed.group, group = groupWithFeed.group,
feed = null, feed = null,
) )
@ -215,14 +213,14 @@ fun FeedsPage(
filterChange( filterChange(
navController = navController, navController = navController,
homeViewModel = homeViewModel, homeViewModel = homeViewModel,
filterState = filterState.copy( filterState = filterUiState.copy(
group = null, group = null,
feed = feed, feed = feed,
) )
) )
} }
) )
if (index != feedsViewState.groupWithFeedList.lastIndex) { if (index != feedsUiState.groupWithFeedList.lastIndex) {
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
} }
} }
@ -236,7 +234,7 @@ fun FeedsPage(
}, },
bottomBar = { bottomBar = {
FilterBar( FilterBar(
filter = filterState.filter, filter = filterUiState.filter,
filterBarStyle = filterBarStyle.value, filterBarStyle = filterBarStyle.value,
filterBarFilled = filterBarFilled.value, filterBarFilled = filterBarFilled.value,
filterBarPadding = filterBarPadding.dp, filterBarPadding = filterBarPadding.dp,
@ -245,7 +243,7 @@ fun FeedsPage(
filterChange( filterChange(
navController = navController, navController = navController,
homeViewModel = homeViewModel, homeViewModel = homeViewModel,
filterState = filterState.copy(filter = it), filterState = filterUiState.copy(filter = it),
isNavigate = false, isNavigate = false,
) )
} }
@ -263,7 +261,7 @@ private fun filterChange(
filterState: FilterState, filterState: FilterState,
isNavigate: Boolean = true, isNavigate: Boolean = true,
) { ) {
homeViewModel.dispatch(HomeViewAction.ChangeFilter(filterState)) homeViewModel.changeFilter(filterState)
if (isNavigate) { if (isNavigate) {
navController.navigate(RouteName.FLOW) { navController.navigate(RouteName.FLOW) {
launchSingleTop = true launchSingleTop = true

View File

@ -31,21 +31,12 @@ class FeedsViewModel @Inject constructor(
@DispatcherIO @DispatcherIO
private val dispatcherIO: CoroutineDispatcher, private val dispatcherIO: CoroutineDispatcher,
) : ViewModel() { ) : ViewModel() {
private val _viewState = MutableStateFlow(FeedsViewState()) private val _feedsUiState = MutableStateFlow(FeedsUiState())
val viewState: StateFlow<FeedsViewState> = _viewState.asStateFlow() val feedsUiState: StateFlow<FeedsUiState> = _feedsUiState.asStateFlow()
fun dispatch(action: FeedsViewAction) { fun fetchAccount() {
when (action) {
is FeedsViewAction.FetchAccount -> fetchAccount()
is FeedsViewAction.FetchData -> fetchData(action.filterState)
is FeedsViewAction.ExportAsString -> exportAsOpml(action.callback)
is FeedsViewAction.ScrollToItem -> scrollToItem(action.index)
}
}
private fun fetchAccount() {
viewModelScope.launch(dispatcherIO) { viewModelScope.launch(dispatcherIO) {
_viewState.update { _feedsUiState.update {
it.copy( it.copy(
account = accountRepository.getCurrentAccount() account = accountRepository.getCurrentAccount()
) )
@ -53,7 +44,7 @@ class FeedsViewModel @Inject constructor(
} }
} }
private fun exportAsOpml(callback: (String) -> Unit = {}) { fun exportAsOpml(callback: (String) -> Unit = {}) {
viewModelScope.launch(dispatcherDefault) { viewModelScope.launch(dispatcherDefault) {
try { try {
callback(opmlRepository.saveToString()) callback(opmlRepository.saveToString())
@ -63,7 +54,7 @@ class FeedsViewModel @Inject constructor(
} }
} }
private fun fetchData(filterState: FilterState) { fun fetchData(filterState: FilterState) {
viewModelScope.launch(dispatcherIO) { viewModelScope.launch(dispatcherIO) {
pullFeeds( pullFeeds(
isStarred = filterState.filter.isStarred(), isStarred = filterState.filter.isStarred(),
@ -109,13 +100,25 @@ class FeedsViewModel @Inject constructor(
} }
groupWithFeedList groupWithFeedList
}.onEach { groupWithFeedList -> }.onEach { groupWithFeedList ->
_viewState.update { _feedsUiState.update {
it.copy( it.copy(
importantCount = groupWithFeedList.sumOf { it.group.important ?: 0 }.run { importantCount = groupWithFeedList.sumOf { it.group.important ?: 0 }.run {
when { when {
isStarred -> stringsRepository.getQuantityString(R.plurals.starred_desc, this, this) isStarred -> stringsRepository.getQuantityString(
isUnread -> stringsRepository.getQuantityString(R.plurals.unread_desc, this, this) R.plurals.starred_desc,
else -> stringsRepository.getQuantityString(R.plurals.all_desc, this, this) this,
this
)
isUnread -> stringsRepository.getQuantityString(
R.plurals.unread_desc,
this,
this
)
else -> stringsRepository.getQuantityString(
R.plurals.all_desc,
this,
this
)
} }
}, },
groupWithFeedList = groupWithFeedList, groupWithFeedList = groupWithFeedList,
@ -126,15 +129,9 @@ class FeedsViewModel @Inject constructor(
Log.e("RLog", "catch in articleRepository.pullFeeds(): ${it.message}") Log.e("RLog", "catch in articleRepository.pullFeeds(): ${it.message}")
}.flowOn(dispatcherDefault).collect() }.flowOn(dispatcherDefault).collect()
} }
private fun scrollToItem(index: Int) {
viewModelScope.launch {
_viewState.value.listState.scrollToItem(index)
}
}
} }
data class FeedsViewState( data class FeedsUiState(
val account: Account? = null, val account: Account? = null,
val importantCount: String = "", val importantCount: String = "",
val groupWithFeedList: List<GroupWithFeed> = emptyList(), val groupWithFeedList: List<GroupWithFeed> = emptyList(),
@ -142,19 +139,3 @@ data class FeedsViewState(
val listState: LazyListState = LazyListState(), val listState: LazyListState = LazyListState(),
val groupsVisible: Boolean = true, val groupsVisible: Boolean = true,
) )
sealed class FeedsViewAction {
data class FetchData(
val filterState: FilterState,
) : FeedsViewAction()
object FetchAccount : FeedsViewAction()
data class ExportAsString(
val callback: (String) -> Unit = {}
) : FeedsViewAction()
data class ScrollToItem(
val index: Int
) : FeedsViewAction()
}

View File

@ -28,7 +28,6 @@ import me.ash.reader.R
import me.ash.reader.data.entity.Feed import me.ash.reader.data.entity.Feed
import me.ash.reader.data.entity.Group import me.ash.reader.data.entity.Group
import me.ash.reader.ui.ext.alphaLN import me.ash.reader.ui.ext.alphaLN
import me.ash.reader.ui.page.home.feeds.drawer.group.GroupOptionViewAction
import me.ash.reader.ui.page.home.feeds.drawer.group.GroupOptionViewModel import me.ash.reader.ui.page.home.feeds.drawer.group.GroupOptionViewModel
@OptIn(androidx.compose.foundation.ExperimentalFoundationApi::class) @OptIn(androidx.compose.foundation.ExperimentalFoundationApi::class)
@ -61,7 +60,7 @@ fun GroupItem(
}, },
onLongClick = { onLongClick = {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
groupOptionViewModel.dispatch(GroupOptionViewAction.Show(scope, group.id)) groupOptionViewModel.showDrawer(scope, group.id)
} }
) )
.padding(top = 22.dp) .padding(top = 22.dp)

View File

@ -7,7 +7,6 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
@ -18,19 +17,18 @@ import me.ash.reader.ui.ext.showToast
@Composable @Composable
fun ClearFeedDialog( fun ClearFeedDialog(
modifier: Modifier = Modifier,
feedName: String, feedName: String,
viewModel: FeedOptionViewModel = hiltViewModel(), feedOptionViewModel: FeedOptionViewModel = hiltViewModel(),
) { ) {
val context = LocalContext.current val context = LocalContext.current
val viewState = viewModel.viewState.collectAsStateValue() val feedOptionUiState = feedOptionViewModel.feedOptionUiState.collectAsStateValue()
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val toastString = stringResource(R.string.clear_articles_in_feed_toast, feedName) val toastString = stringResource(R.string.clear_articles_in_feed_toast, feedName)
Dialog( Dialog(
visible = viewState.clearDialogVisible, visible = feedOptionUiState.clearDialogVisible,
onDismissRequest = { onDismissRequest = {
viewModel.dispatch(FeedOptionViewAction.HideClearDialog) feedOptionViewModel.hideClearDialog()
}, },
icon = { icon = {
Icon( Icon(
@ -47,11 +45,11 @@ fun ClearFeedDialog(
confirmButton = { confirmButton = {
TextButton( TextButton(
onClick = { onClick = {
viewModel.dispatch(FeedOptionViewAction.Clear { feedOptionViewModel.clearFeed {
viewModel.dispatch(FeedOptionViewAction.HideClearDialog) feedOptionViewModel.hideClearDialog()
viewModel.dispatch(FeedOptionViewAction.Hide(scope)) feedOptionViewModel.hideDrawer(scope)
context.showToast(toastString) context.showToast(toastString)
}) }
} }
) { ) {
Text( Text(
@ -62,7 +60,7 @@ fun ClearFeedDialog(
dismissButton = { dismissButton = {
TextButton( TextButton(
onClick = { onClick = {
viewModel.dispatch(FeedOptionViewAction.HideClearDialog) feedOptionViewModel.hideClearDialog()
} }
) { ) {
Text( Text(

View File

@ -7,7 +7,6 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
@ -18,19 +17,18 @@ import me.ash.reader.ui.ext.showToast
@Composable @Composable
fun DeleteFeedDialog( fun DeleteFeedDialog(
modifier: Modifier = Modifier,
feedName: String, feedName: String,
viewModel: FeedOptionViewModel = hiltViewModel(), feedOptionViewModel: FeedOptionViewModel = hiltViewModel(),
) { ) {
val context = LocalContext.current val context = LocalContext.current
val viewState = viewModel.viewState.collectAsStateValue() val feedOptionUiState = feedOptionViewModel.feedOptionUiState.collectAsStateValue()
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val toastString = stringResource(R.string.delete_toast, feedName) val toastString = stringResource(R.string.delete_toast, feedName)
Dialog( Dialog(
visible = viewState.deleteDialogVisible, visible = feedOptionUiState.deleteDialogVisible,
onDismissRequest = { onDismissRequest = {
viewModel.dispatch(FeedOptionViewAction.HideDeleteDialog) feedOptionViewModel.hideDeleteDialog()
}, },
icon = { icon = {
Icon( Icon(
@ -47,11 +45,11 @@ fun DeleteFeedDialog(
confirmButton = { confirmButton = {
TextButton( TextButton(
onClick = { onClick = {
viewModel.dispatch(FeedOptionViewAction.Delete { feedOptionViewModel.delete {
viewModel.dispatch(FeedOptionViewAction.HideDeleteDialog) feedOptionViewModel.hideDeleteDialog()
viewModel.dispatch(FeedOptionViewAction.Hide(scope)) feedOptionViewModel.hideDrawer(scope)
context.showToast(toastString) context.showToast(toastString)
}) }
} }
) { ) {
Text( Text(
@ -62,7 +60,7 @@ fun DeleteFeedDialog(
dismissButton = { dismissButton = {
TextButton( TextButton(
onClick = { onClick = {
viewModel.dispatch(FeedOptionViewAction.HideDeleteDialog) feedOptionViewModel.hideDeleteDialog()
} }
) { ) {
Text( Text(

View File

@ -19,35 +19,34 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.ash.reader.R import me.ash.reader.R
import me.ash.reader.ui.component.FeedIcon
import me.ash.reader.ui.component.base.BottomDrawer import me.ash.reader.ui.component.base.BottomDrawer
import me.ash.reader.ui.component.base.TextFieldDialog import me.ash.reader.ui.component.base.TextFieldDialog
import me.ash.reader.ui.ext.collectAsStateValue import me.ash.reader.ui.ext.collectAsStateValue
import me.ash.reader.ui.ext.roundClick import me.ash.reader.ui.ext.roundClick
import me.ash.reader.ui.ext.showToast import me.ash.reader.ui.ext.showToast
import me.ash.reader.ui.component.FeedIcon
import me.ash.reader.ui.page.home.feeds.subscribe.ResultView import me.ash.reader.ui.page.home.feeds.subscribe.ResultView
@OptIn(ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
fun FeedOptionDrawer( fun FeedOptionDrawer(
modifier: Modifier = Modifier,
feedOptionViewModel: FeedOptionViewModel = hiltViewModel(), feedOptionViewModel: FeedOptionViewModel = hiltViewModel(),
content: @Composable () -> Unit = {}, content: @Composable () -> Unit = {},
) { ) {
val context = LocalContext.current val context = LocalContext.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val viewState = feedOptionViewModel.viewState.collectAsStateValue() val feedOptionUiState = feedOptionViewModel.feedOptionUiState.collectAsStateValue()
val feed = viewState.feed val feed = feedOptionUiState.feed
val toastString = stringResource(R.string.rename_toast, viewState.newName) val toastString = stringResource(R.string.rename_toast, feedOptionUiState.newName)
BackHandler(viewState.drawerState.isVisible) { BackHandler(feedOptionUiState.drawerState.isVisible) {
scope.launch { scope.launch {
viewState.drawerState.hide() feedOptionUiState.drawerState.hide()
} }
} }
BottomDrawer( BottomDrawer(
drawerState = viewState.drawerState, drawerState = feedOptionUiState.drawerState,
sheetContent = { sheetContent = {
Column(modifier = Modifier.navigationBarsPadding()) { Column(modifier = Modifier.navigationBarsPadding()) {
Column( Column(
@ -65,7 +64,7 @@ fun FeedOptionDrawer(
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
Text( Text(
modifier = Modifier.roundClick { modifier = Modifier.roundClick {
feedOptionViewModel.dispatch(FeedOptionViewAction.ShowRenameDialog) feedOptionViewModel.showRenameDialog()
}, },
text = feed?.name ?: stringResource(R.string.unknown), text = feed?.name ?: stringResource(R.string.unknown),
style = MaterialTheme.typography.headlineSmall, style = MaterialTheme.typography.headlineSmall,
@ -77,32 +76,32 @@ fun FeedOptionDrawer(
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
ResultView( ResultView(
link = feed?.url ?: stringResource(R.string.unknown), link = feed?.url ?: stringResource(R.string.unknown),
groups = viewState.groups, groups = feedOptionUiState.groups,
selectedAllowNotificationPreset = viewState.feed?.isNotification ?: false, selectedAllowNotificationPreset = feedOptionUiState.feed?.isNotification ?: false,
selectedParseFullContentPreset = viewState.feed?.isFullContent ?: false, selectedParseFullContentPreset = feedOptionUiState.feed?.isFullContent ?: false,
isMoveToGroup = true, isMoveToGroup = true,
showUnsubscribe = true, showUnsubscribe = true,
selectedGroupId = viewState.feed?.groupId ?: "", selectedGroupId = feedOptionUiState.feed?.groupId ?: "",
allowNotificationPresetOnClick = { allowNotificationPresetOnClick = {
feedOptionViewModel.dispatch(FeedOptionViewAction.ChangeAllowNotificationPreset) feedOptionViewModel.changeAllowNotificationPreset()
}, },
parseFullContentPresetOnClick = { parseFullContentPresetOnClick = {
feedOptionViewModel.dispatch(FeedOptionViewAction.ChangeParseFullContentPreset) feedOptionViewModel.changeParseFullContentPreset()
}, },
clearArticlesOnClick = { clearArticlesOnClick = {
feedOptionViewModel.dispatch(FeedOptionViewAction.ShowClearDialog) feedOptionViewModel.showClearDialog()
}, },
unsubscribeOnClick = { unsubscribeOnClick = {
feedOptionViewModel.dispatch(FeedOptionViewAction.ShowDeleteDialog) feedOptionViewModel.showDeleteDialog()
}, },
onGroupClick = { onGroupClick = {
feedOptionViewModel.dispatch(FeedOptionViewAction.SelectedGroup(it)) feedOptionViewModel.selectedGroup(it)
}, },
onAddNewGroup = { onAddNewGroup = {
feedOptionViewModel.dispatch(FeedOptionViewAction.ShowNewGroupDialog) feedOptionViewModel.showNewGroupDialog()
}, },
onFeedUrlClick = { onFeedUrlClick = {
feedOptionViewModel.dispatch(FeedOptionViewAction.ShowChangeUrlDialog) feedOptionViewModel.showFeedUrlDialog()
} }
) )
} }
@ -116,56 +115,56 @@ fun FeedOptionDrawer(
ClearFeedDialog(feedName = feed?.name ?: "") ClearFeedDialog(feedName = feed?.name ?: "")
TextFieldDialog( TextFieldDialog(
visible = viewState.newGroupDialogVisible, visible = feedOptionUiState.newGroupDialogVisible,
title = stringResource(R.string.create_new_group), title = stringResource(R.string.create_new_group),
icon = Icons.Outlined.CreateNewFolder, icon = Icons.Outlined.CreateNewFolder,
value = viewState.newGroupContent, value = feedOptionUiState.newGroupContent,
placeholder = stringResource(R.string.name), placeholder = stringResource(R.string.name),
onValueChange = { onValueChange = {
feedOptionViewModel.dispatch(FeedOptionViewAction.InputNewGroup(it)) feedOptionViewModel.inputNewGroup(it)
}, },
onDismissRequest = { onDismissRequest = {
feedOptionViewModel.dispatch(FeedOptionViewAction.HideNewGroupDialog) feedOptionViewModel.hideNewGroupDialog()
}, },
onConfirm = { onConfirm = {
feedOptionViewModel.dispatch(FeedOptionViewAction.AddNewGroup) feedOptionViewModel.addNewGroup()
} }
) )
TextFieldDialog( TextFieldDialog(
visible = viewState.renameDialogVisible, visible = feedOptionUiState.renameDialogVisible,
title = stringResource(R.string.rename), title = stringResource(R.string.rename),
icon = Icons.Outlined.Edit, icon = Icons.Outlined.Edit,
value = viewState.newName, value = feedOptionUiState.newName,
placeholder = stringResource(R.string.name), placeholder = stringResource(R.string.name),
onValueChange = { onValueChange = {
feedOptionViewModel.dispatch(FeedOptionViewAction.InputNewName(it)) feedOptionViewModel.inputNewName(it)
}, },
onDismissRequest = { onDismissRequest = {
feedOptionViewModel.dispatch(FeedOptionViewAction.HideRenameDialog) feedOptionViewModel.hideRenameDialog()
}, },
onConfirm = { onConfirm = {
feedOptionViewModel.dispatch(FeedOptionViewAction.Rename) feedOptionViewModel.renameFeed()
feedOptionViewModel.dispatch(FeedOptionViewAction.Hide(scope)) feedOptionViewModel.hideDrawer(scope)
context.showToast(toastString) context.showToast(toastString)
} }
) )
TextFieldDialog( TextFieldDialog(
visible = viewState.changeUrlDialogVisible, visible = feedOptionUiState.changeUrlDialogVisible,
title = stringResource(R.string.change_url), title = stringResource(R.string.change_url),
icon = Icons.Outlined.Edit, icon = Icons.Outlined.Edit,
value = viewState.newUrl, value = feedOptionUiState.newUrl,
placeholder = stringResource(R.string.feed_url_placeholder), placeholder = stringResource(R.string.feed_url_placeholder),
onValueChange = { onValueChange = {
feedOptionViewModel.dispatch(FeedOptionViewAction.InputNewUrl(it)) feedOptionViewModel.inputNewUrl(it)
}, },
onDismissRequest = { onDismissRequest = {
feedOptionViewModel.dispatch(FeedOptionViewAction.HideChangeUrlDialog) feedOptionViewModel.hideFeedUrlDialog()
}, },
onConfirm = { onConfirm = {
feedOptionViewModel.dispatch(FeedOptionViewAction.ChangeUrl) feedOptionViewModel.changeFeedUrl()
feedOptionViewModel.dispatch(FeedOptionViewAction.Hide(scope)) feedOptionViewModel.hideDrawer(scope)
} }
) )
} }

View File

@ -21,9 +21,7 @@ import me.ash.reader.data.module.DispatcherMain
import me.ash.reader.data.repository.RssRepository import me.ash.reader.data.repository.RssRepository
import javax.inject.Inject import javax.inject.Inject
@OptIn( @OptIn(ExperimentalMaterialApi::class)
ExperimentalMaterialApi::class
)
@HiltViewModel @HiltViewModel
class FeedOptionViewModel @Inject constructor( class FeedOptionViewModel @Inject constructor(
private val rssRepository: RssRepository, private val rssRepository: RssRepository,
@ -32,13 +30,13 @@ class FeedOptionViewModel @Inject constructor(
@DispatcherIO @DispatcherIO
private val dispatcherIO: CoroutineDispatcher, private val dispatcherIO: CoroutineDispatcher,
) : ViewModel() { ) : ViewModel() {
private val _viewState = MutableStateFlow(FeedOptionViewState()) private val _feedOptionUiState = MutableStateFlow(FeedOptionUiState())
val viewState: StateFlow<FeedOptionViewState> = _viewState.asStateFlow() val feedOptionUiState: StateFlow<FeedOptionUiState> = _feedOptionUiState.asStateFlow()
init { init {
viewModelScope.launch(dispatcherIO) { viewModelScope.launch(dispatcherIO) {
rssRepository.get().pullGroups().collect { groups -> rssRepository.get().pullGroups().collect { groups ->
_viewState.update { _feedOptionUiState.update {
it.copy( it.copy(
groups = groups groups = groups
) )
@ -47,37 +45,9 @@ class FeedOptionViewModel @Inject constructor(
} }
} }
fun dispatch(action: FeedOptionViewAction) {
when (action) {
is FeedOptionViewAction.Show -> show(action.scope, action.feedId)
is FeedOptionViewAction.Hide -> hide(action.scope)
is FeedOptionViewAction.SelectedGroup -> selectedGroup(action.groupId)
is FeedOptionViewAction.InputNewGroup -> inputNewGroup(action.content)
is FeedOptionViewAction.ChangeAllowNotificationPreset -> changeAllowNotificationPreset()
is FeedOptionViewAction.ChangeParseFullContentPreset -> changeParseFullContentPreset()
is FeedOptionViewAction.ShowDeleteDialog -> showDeleteDialog()
is FeedOptionViewAction.HideDeleteDialog -> hideDeleteDialog()
is FeedOptionViewAction.Delete -> delete(action.callback)
is FeedOptionViewAction.ShowClearDialog -> showClearDialog()
is FeedOptionViewAction.HideClearDialog -> hideClearDialog()
is FeedOptionViewAction.Clear -> clear(action.callback)
is FeedOptionViewAction.AddNewGroup -> addNewGroup()
is FeedOptionViewAction.ShowNewGroupDialog -> changeNewGroupDialogVisible(true)
is FeedOptionViewAction.HideNewGroupDialog -> changeNewGroupDialogVisible(false)
is FeedOptionViewAction.InputNewName -> inputNewName(action.content)
is FeedOptionViewAction.Rename -> rename()
is FeedOptionViewAction.ShowRenameDialog -> changeRenameDialogVisible(true)
is FeedOptionViewAction.HideRenameDialog -> changeRenameDialogVisible(false)
is FeedOptionViewAction.InputNewUrl -> inputNewUrl(action.content)
is FeedOptionViewAction.ChangeUrl -> changeFeedUrl()
is FeedOptionViewAction.HideChangeUrlDialog -> changeFeedUrlDialogVisible(false)
is FeedOptionViewAction.ShowChangeUrlDialog -> changeFeedUrlDialogVisible(true)
}
}
private suspend fun fetchFeed(feedId: String) { private suspend fun fetchFeed(feedId: String) {
val feed = rssRepository.get().findFeedById(feedId) val feed = rssRepository.get().findFeedById(feedId)
_viewState.update { _feedOptionUiState.update {
it.copy( it.copy(
feed = feed, feed = feed,
selectedGroupId = feed?.groupId ?: "", selectedGroupId = feed?.groupId ?: "",
@ -85,48 +55,57 @@ class FeedOptionViewModel @Inject constructor(
} }
} }
private fun show(scope: CoroutineScope, feedId: String) { fun showDrawer(scope: CoroutineScope, feedId: String) {
scope.launch { scope.launch {
fetchFeed(feedId) fetchFeed(feedId)
_viewState.value.drawerState.show() _feedOptionUiState.value.drawerState.show()
} }
} }
private fun hide(scope: CoroutineScope) { fun hideDrawer(scope: CoroutineScope) {
scope.launch { scope.launch {
_viewState.value.drawerState.hide() _feedOptionUiState.value.drawerState.hide()
} }
} }
private fun changeNewGroupDialogVisible(visible: Boolean) { fun showNewGroupDialog() {
_viewState.update { _feedOptionUiState.update {
it.copy( it.copy(
newGroupDialogVisible = visible, newGroupDialogVisible = true,
newGroupContent = "", newGroupContent = "",
) )
} }
} }
private fun inputNewGroup(content: String) { fun hideNewGroupDialog() {
_viewState.update { _feedOptionUiState.update {
it.copy(
newGroupDialogVisible = false,
newGroupContent = "",
)
}
}
fun inputNewGroup(content: String) {
_feedOptionUiState.update {
it.copy( it.copy(
newGroupContent = content newGroupContent = content
) )
} }
} }
private fun addNewGroup() { fun addNewGroup() {
if (_viewState.value.newGroupContent.isNotBlank()) { if (_feedOptionUiState.value.newGroupContent.isNotBlank()) {
viewModelScope.launch { viewModelScope.launch {
selectedGroup(rssRepository.get().addGroup(_viewState.value.newGroupContent)) selectedGroup(rssRepository.get().addGroup(_feedOptionUiState.value.newGroupContent))
changeNewGroupDialogVisible(false) hideNewGroupDialog()
} }
} }
} }
private fun selectedGroup(groupId: String) { fun selectedGroup(groupId: String) {
viewModelScope.launch(dispatcherIO) { viewModelScope.launch(dispatcherIO) {
_viewState.value.feed?.let { _feedOptionUiState.value.feed?.let {
rssRepository.get().updateFeed( rssRepository.get().updateFeed(
it.copy( it.copy(
groupId = groupId groupId = groupId
@ -137,9 +116,9 @@ class FeedOptionViewModel @Inject constructor(
} }
} }
private fun changeParseFullContentPreset() { fun changeParseFullContentPreset() {
viewModelScope.launch(dispatcherIO) { viewModelScope.launch(dispatcherIO) {
_viewState.value.feed?.let { _feedOptionUiState.value.feed?.let {
rssRepository.get().updateFeed( rssRepository.get().updateFeed(
it.copy( it.copy(
isFullContent = !it.isFullContent isFullContent = !it.isFullContent
@ -150,9 +129,9 @@ class FeedOptionViewModel @Inject constructor(
} }
} }
private fun changeAllowNotificationPreset() { fun changeAllowNotificationPreset() {
viewModelScope.launch(dispatcherIO) { viewModelScope.launch(dispatcherIO) {
_viewState.value.feed?.let { _feedOptionUiState.value.feed?.let {
rssRepository.get().updateFeed( rssRepository.get().updateFeed(
it.copy( it.copy(
isNotification = !it.isNotification isNotification = !it.isNotification
@ -163,8 +142,8 @@ class FeedOptionViewModel @Inject constructor(
} }
} }
private fun delete(callback: () -> Unit = {}) { fun delete(callback: () -> Unit = {}) {
_viewState.value.feed?.let { _feedOptionUiState.value.feed?.let {
viewModelScope.launch(dispatcherIO) { viewModelScope.launch(dispatcherIO) {
rssRepository.get().deleteFeed(it) rssRepository.get().deleteFeed(it)
withContext(dispatcherMain) { withContext(dispatcherMain) {
@ -174,40 +153,40 @@ class FeedOptionViewModel @Inject constructor(
} }
} }
private fun hideDeleteDialog() { fun hideDeleteDialog() {
_viewState.update { _feedOptionUiState.update {
it.copy( it.copy(
deleteDialogVisible = false, deleteDialogVisible = false,
) )
} }
} }
private fun showDeleteDialog() { fun showDeleteDialog() {
_viewState.update { _feedOptionUiState.update {
it.copy( it.copy(
deleteDialogVisible = true, deleteDialogVisible = true,
) )
} }
} }
private fun showClearDialog() { fun showClearDialog() {
_viewState.update { _feedOptionUiState.update {
it.copy( it.copy(
clearDialogVisible = true, clearDialogVisible = true,
) )
} }
} }
private fun hideClearDialog() { fun hideClearDialog() {
_viewState.update { _feedOptionUiState.update {
it.copy( it.copy(
clearDialogVisible = false, clearDialogVisible = false,
) )
} }
} }
private fun clear(callback: () -> Unit = {}) { fun clearFeed(callback: () -> Unit = {}) {
_viewState.value.feed?.let { _feedOptionUiState.value.feed?.let {
viewModelScope.launch(dispatcherIO) { viewModelScope.launch(dispatcherIO) {
rssRepository.get().deleteArticles(feed = it) rssRepository.get().deleteArticles(feed = it)
withContext(dispatcherMain) { withContext(dispatcherMain) {
@ -217,15 +196,15 @@ class FeedOptionViewModel @Inject constructor(
} }
} }
private fun rename() { fun renameFeed() {
_viewState.value.feed?.let { _feedOptionUiState.value.feed?.let {
viewModelScope.launch { viewModelScope.launch {
rssRepository.get().updateFeed( rssRepository.get().updateFeed(
it.copy( it.copy(
name = _viewState.value.newName name = _feedOptionUiState.value.newName
) )
) )
_viewState.update { _feedOptionUiState.update {
it.copy( it.copy(
renameDialogVisible = false, renameDialogVisible = false,
) )
@ -234,49 +213,67 @@ class FeedOptionViewModel @Inject constructor(
} }
} }
private fun changeRenameDialogVisible(visible: Boolean) { fun showRenameDialog() {
_viewState.update { _feedOptionUiState.update {
it.copy( it.copy(
renameDialogVisible = visible, renameDialogVisible = true,
newName = if (visible) _viewState.value.feed?.name ?: "" else "", newName = _feedOptionUiState.value.feed?.name ?: "",
) )
} }
} }
private fun inputNewName(content: String) { fun hideRenameDialog() {
_viewState.update { _feedOptionUiState.update {
it.copy(
renameDialogVisible = false,
newName = "",
)
}
}
fun inputNewName(content: String) {
_feedOptionUiState.update {
it.copy( it.copy(
newName = content newName = content
) )
} }
} }
private fun changeFeedUrlDialogVisible(visible: Boolean) { fun showFeedUrlDialog() {
_viewState.update { _feedOptionUiState.update {
it.copy( it.copy(
changeUrlDialogVisible = visible, changeUrlDialogVisible = true,
newUrl = if (visible) _viewState.value.feed?.url ?: "" else "", newUrl = _feedOptionUiState.value.feed?.url ?: "",
) )
} }
} }
private fun inputNewUrl(content: String) { fun hideFeedUrlDialog() {
_viewState.update { _feedOptionUiState.update {
it.copy(
changeUrlDialogVisible = false,
newUrl = "",
)
}
}
fun inputNewUrl(content: String) {
_feedOptionUiState.update {
it.copy( it.copy(
newUrl = content newUrl = content
) )
} }
} }
private fun changeFeedUrl() { fun changeFeedUrl() {
_viewState.value.feed?.let { _feedOptionUiState.value.feed?.let {
viewModelScope.launch { viewModelScope.launch {
rssRepository.get().updateFeed( rssRepository.get().updateFeed(
it.copy( it.copy(
url = _viewState.value.newUrl url = _feedOptionUiState.value.newUrl
) )
) )
_viewState.update { _feedOptionUiState.update {
it.copy( it.copy(
changeUrlDialogVisible = false, changeUrlDialogVisible = false,
) )
@ -287,7 +284,7 @@ class FeedOptionViewModel @Inject constructor(
} }
@OptIn(ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)
data class FeedOptionViewState( data class FeedOptionUiState(
var drawerState: ModalBottomSheetState = ModalBottomSheetState(ModalBottomSheetValue.Hidden), var drawerState: ModalBottomSheetState = ModalBottomSheetState(ModalBottomSheetValue.Hidden),
val feed: Feed? = null, val feed: Feed? = null,
val selectedGroupId: String = "", val selectedGroupId: String = "",
@ -301,57 +298,3 @@ data class FeedOptionViewState(
val newUrl: String = "", val newUrl: String = "",
val changeUrlDialogVisible: Boolean = false, val changeUrlDialogVisible: Boolean = false,
) )
sealed class FeedOptionViewAction {
data class Show(
val scope: CoroutineScope,
val feedId: String
) : FeedOptionViewAction()
data class Hide(
val scope: CoroutineScope,
) : FeedOptionViewAction()
object ChangeAllowNotificationPreset : FeedOptionViewAction()
object ChangeParseFullContentPreset : FeedOptionViewAction()
data class SelectedGroup(
val groupId: String
) : FeedOptionViewAction()
data class InputNewGroup(
val content: String
) : FeedOptionViewAction()
data class Delete(
val callback: () -> Unit = {}
) : FeedOptionViewAction()
object ShowDeleteDialog : FeedOptionViewAction()
object HideDeleteDialog : FeedOptionViewAction()
data class Clear(
val callback: () -> Unit = {}
) : FeedOptionViewAction()
object ShowClearDialog : FeedOptionViewAction()
object HideClearDialog : FeedOptionViewAction()
object ShowNewGroupDialog : FeedOptionViewAction()
object HideNewGroupDialog : FeedOptionViewAction()
object AddNewGroup : FeedOptionViewAction()
object ShowRenameDialog : FeedOptionViewAction()
object HideRenameDialog : FeedOptionViewAction()
object Rename : FeedOptionViewAction()
data class InputNewName(
val content: String
) : FeedOptionViewAction()
object ShowChangeUrlDialog : FeedOptionViewAction()
object HideChangeUrlDialog : FeedOptionViewAction()
object ChangeUrl : FeedOptionViewAction()
data class InputNewUrl(
val content: String
) : FeedOptionViewAction()
}

View File

@ -7,7 +7,6 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
@ -18,20 +17,19 @@ import me.ash.reader.ui.ext.showToast
@Composable @Composable
fun AllAllowNotificationDialog( fun AllAllowNotificationDialog(
modifier: Modifier = Modifier,
groupName: String, groupName: String,
viewModel: GroupOptionViewModel = hiltViewModel(), groupOptionViewModel: GroupOptionViewModel = hiltViewModel(),
) { ) {
val context = LocalContext.current val context = LocalContext.current
val viewState = viewModel.viewState.collectAsStateValue() val groupOptionUiState = groupOptionViewModel.groupOptionUiState.collectAsStateValue()
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val allowToastString = stringResource(R.string.all_allow_notification_toast, groupName) val allowToastString = stringResource(R.string.all_allow_notification_toast, groupName)
val denyToastString = stringResource(R.string.all_deny_notification_toast, groupName) val denyToastString = stringResource(R.string.all_deny_notification_toast, groupName)
Dialog( Dialog(
visible = viewState.allAllowNotificationDialogVisible, visible = groupOptionUiState.allAllowNotificationDialogVisible,
onDismissRequest = { onDismissRequest = {
viewModel.dispatch(GroupOptionViewAction.HideAllAllowNotificationDialog) groupOptionViewModel.hideAllAllowNotificationDialog()
}, },
icon = { icon = {
Icon( Icon(
@ -48,11 +46,11 @@ fun AllAllowNotificationDialog(
confirmButton = { confirmButton = {
TextButton( TextButton(
onClick = { onClick = {
viewModel.dispatch(GroupOptionViewAction.AllAllowNotification(true) { groupOptionViewModel.allAllowNotification(true) {
viewModel.dispatch(GroupOptionViewAction.HideAllAllowNotificationDialog) groupOptionViewModel.hideAllAllowNotificationDialog()
viewModel.dispatch(GroupOptionViewAction.Hide(scope)) groupOptionViewModel.hideDrawer(scope)
context.showToast(allowToastString) context.showToast(allowToastString)
}) }
} }
) { ) {
Text( Text(
@ -63,11 +61,11 @@ fun AllAllowNotificationDialog(
dismissButton = { dismissButton = {
TextButton( TextButton(
onClick = { onClick = {
viewModel.dispatch(GroupOptionViewAction.AllAllowNotification(false) { groupOptionViewModel.allAllowNotification(false) {
viewModel.dispatch(GroupOptionViewAction.HideAllAllowNotificationDialog) groupOptionViewModel.hideAllAllowNotificationDialog()
viewModel.dispatch(GroupOptionViewAction.Hide(scope)) groupOptionViewModel.hideDrawer(scope)
context.showToast(denyToastString) context.showToast(denyToastString)
}) }
} }
) { ) {
Text( Text(

View File

@ -7,7 +7,6 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
@ -18,20 +17,21 @@ import me.ash.reader.ui.ext.showToast
@Composable @Composable
fun AllMoveToGroupDialog( fun AllMoveToGroupDialog(
modifier: Modifier = Modifier,
groupName: String, groupName: String,
viewModel: GroupOptionViewModel = hiltViewModel(), groupOptionViewModel: GroupOptionViewModel = hiltViewModel(),
) { ) {
val context = LocalContext.current val context = LocalContext.current
val viewState = viewModel.viewState.collectAsStateValue() val groupOptionUiState = groupOptionViewModel.groupOptionUiState.collectAsStateValue()
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val toastString = val toastString = stringResource(
stringResource(R.string.all_move_to_group_toast, viewState.targetGroup?.name ?: "") R.string.all_move_to_group_toast,
groupOptionUiState.targetGroup?.name ?: ""
)
Dialog( Dialog(
visible = viewState.allMoveToGroupDialogVisible, visible = groupOptionUiState.allMoveToGroupDialogVisible,
onDismissRequest = { onDismissRequest = {
viewModel.dispatch(GroupOptionViewAction.HideAllMoveToGroupDialog) groupOptionViewModel.hideAllMoveToGroupDialog()
}, },
icon = { icon = {
Icon( Icon(
@ -47,18 +47,18 @@ fun AllMoveToGroupDialog(
text = stringResource( text = stringResource(
R.string.all_move_to_group_tips, R.string.all_move_to_group_tips,
groupName, groupName,
viewState.targetGroup?.name ?: "", groupOptionUiState.targetGroup?.name ?: "",
) )
) )
}, },
confirmButton = { confirmButton = {
TextButton( TextButton(
onClick = { onClick = {
viewModel.dispatch(GroupOptionViewAction.AllMoveToGroup { groupOptionViewModel.allMoveToGroup {
viewModel.dispatch(GroupOptionViewAction.HideAllMoveToGroupDialog) groupOptionViewModel.hideAllMoveToGroupDialog()
viewModel.dispatch(GroupOptionViewAction.Hide(scope)) groupOptionViewModel.hideDrawer(scope)
context.showToast(toastString) context.showToast(toastString)
}) }
} }
) { ) {
Text( Text(
@ -69,7 +69,7 @@ fun AllMoveToGroupDialog(
dismissButton = { dismissButton = {
TextButton( TextButton(
onClick = { onClick = {
viewModel.dispatch(GroupOptionViewAction.HideAllMoveToGroupDialog) groupOptionViewModel.hideAllMoveToGroupDialog()
} }
) { ) {
Text( Text(

View File

@ -7,7 +7,6 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
@ -18,20 +17,19 @@ import me.ash.reader.ui.ext.showToast
@Composable @Composable
fun AllParseFullContentDialog( fun AllParseFullContentDialog(
modifier: Modifier = Modifier,
groupName: String, groupName: String,
viewModel: GroupOptionViewModel = hiltViewModel(), groupOptionViewModel: GroupOptionViewModel = hiltViewModel(),
) { ) {
val context = LocalContext.current val context = LocalContext.current
val viewState = viewModel.viewState.collectAsStateValue() val groupOptionUiState = groupOptionViewModel.groupOptionUiState.collectAsStateValue()
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val allowToastString = stringResource(R.string.all_parse_full_content_toast, groupName) val allowToastString = stringResource(R.string.all_parse_full_content_toast, groupName)
val denyToastString = stringResource(R.string.all_deny_parse_full_content_toast, groupName) val denyToastString = stringResource(R.string.all_deny_parse_full_content_toast, groupName)
Dialog( Dialog(
visible = viewState.allParseFullContentDialogVisible, visible = groupOptionUiState.allParseFullContentDialogVisible,
onDismissRequest = { onDismissRequest = {
viewModel.dispatch(GroupOptionViewAction.HideAllParseFullContentDialog) groupOptionViewModel.hideAllParseFullContentDialog()
}, },
icon = { icon = {
Icon( Icon(
@ -48,11 +46,11 @@ fun AllParseFullContentDialog(
confirmButton = { confirmButton = {
TextButton( TextButton(
onClick = { onClick = {
viewModel.dispatch(GroupOptionViewAction.AllParseFullContent(true) { groupOptionViewModel.allParseFullContent(true) {
viewModel.dispatch(GroupOptionViewAction.HideAllParseFullContentDialog) groupOptionViewModel.hideAllParseFullContentDialog()
viewModel.dispatch(GroupOptionViewAction.Hide(scope)) groupOptionViewModel.hideDrawer(scope)
context.showToast(allowToastString) context.showToast(allowToastString)
}) }
} }
) { ) {
Text( Text(
@ -63,11 +61,11 @@ fun AllParseFullContentDialog(
dismissButton = { dismissButton = {
TextButton( TextButton(
onClick = { onClick = {
viewModel.dispatch(GroupOptionViewAction.AllParseFullContent(false) { groupOptionViewModel.allParseFullContent(false) {
viewModel.dispatch(GroupOptionViewAction.HideAllParseFullContentDialog) groupOptionViewModel.hideAllParseFullContentDialog()
viewModel.dispatch(GroupOptionViewAction.Hide(scope)) groupOptionViewModel.hideDrawer(scope)
context.showToast(denyToastString) context.showToast(denyToastString)
}) }
} }
) { ) {
Text( Text(

View File

@ -7,7 +7,6 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
@ -18,19 +17,18 @@ import me.ash.reader.ui.ext.showToast
@Composable @Composable
fun ClearGroupDialog( fun ClearGroupDialog(
modifier: Modifier = Modifier,
groupName: String, groupName: String,
viewModel: GroupOptionViewModel = hiltViewModel(), groupOptionViewModel: GroupOptionViewModel = hiltViewModel(),
) { ) {
val context = LocalContext.current val context = LocalContext.current
val viewState = viewModel.viewState.collectAsStateValue() val groupOptionUiState = groupOptionViewModel.groupOptionUiState.collectAsStateValue()
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val toastString = stringResource(R.string.clear_articles_in_group_toast, groupName) val toastString = stringResource(R.string.clear_articles_in_group_toast, groupName)
Dialog( Dialog(
visible = viewState.clearDialogVisible, visible = groupOptionUiState.clearDialogVisible,
onDismissRequest = { onDismissRequest = {
viewModel.dispatch(GroupOptionViewAction.HideClearDialog) groupOptionViewModel.hideClearDialog()
}, },
icon = { icon = {
Icon( Icon(
@ -47,11 +45,11 @@ fun ClearGroupDialog(
confirmButton = { confirmButton = {
TextButton( TextButton(
onClick = { onClick = {
viewModel.dispatch(GroupOptionViewAction.Clear { groupOptionViewModel.clear {
viewModel.dispatch(GroupOptionViewAction.HideClearDialog) groupOptionViewModel.hideClearDialog()
viewModel.dispatch(GroupOptionViewAction.Hide(scope)) groupOptionViewModel.hideDrawer(scope)
context.showToast(toastString) context.showToast(toastString)
}) }
} }
) { ) {
Text( Text(
@ -62,7 +60,7 @@ fun ClearGroupDialog(
dismissButton = { dismissButton = {
TextButton( TextButton(
onClick = { onClick = {
viewModel.dispatch(GroupOptionViewAction.HideClearDialog) groupOptionViewModel.hideClearDialog()
} }
) { ) {
Text( Text(

View File

@ -7,7 +7,6 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
@ -18,19 +17,18 @@ import me.ash.reader.ui.ext.showToast
@Composable @Composable
fun DeleteGroupDialog( fun DeleteGroupDialog(
modifier: Modifier = Modifier,
groupName: String, groupName: String,
viewModel: GroupOptionViewModel = hiltViewModel(), groupOptionViewModel: GroupOptionViewModel = hiltViewModel(),
) { ) {
val context = LocalContext.current val context = LocalContext.current
val viewState = viewModel.viewState.collectAsStateValue() val groupOptionUiState = groupOptionViewModel.groupOptionUiState.collectAsStateValue()
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val toastString = stringResource(R.string.delete_toast, groupName) val toastString = stringResource(R.string.delete_toast, groupName)
Dialog( Dialog(
visible = viewState.deleteDialogVisible, visible = groupOptionUiState.deleteDialogVisible,
onDismissRequest = { onDismissRequest = {
viewModel.dispatch(GroupOptionViewAction.HideDeleteDialog) groupOptionViewModel.hideDeleteDialog()
}, },
icon = { icon = {
Icon( Icon(
@ -47,11 +45,11 @@ fun DeleteGroupDialog(
confirmButton = { confirmButton = {
TextButton( TextButton(
onClick = { onClick = {
viewModel.dispatch(GroupOptionViewAction.Delete { groupOptionViewModel.delete {
viewModel.dispatch(GroupOptionViewAction.HideDeleteDialog) groupOptionViewModel.hideDeleteDialog()
viewModel.dispatch(GroupOptionViewAction.Hide(scope)) groupOptionViewModel.hideDrawer(scope)
context.showToast(toastString) context.showToast(toastString)
}) }
} }
) { ) {
Text( Text(
@ -62,7 +60,7 @@ fun DeleteGroupDialog(
dismissButton = { dismissButton = {
TextButton( TextButton(
onClick = { onClick = {
viewModel.dispatch(GroupOptionViewAction.HideDeleteDialog) groupOptionViewModel.hideDeleteDialog()
} }
) { ) {
Text( Text(

View File

@ -42,24 +42,23 @@ import me.ash.reader.ui.ext.*
@OptIn(ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
fun GroupOptionDrawer( fun GroupOptionDrawer(
modifier: Modifier = Modifier,
groupOptionViewModel: GroupOptionViewModel = hiltViewModel(), groupOptionViewModel: GroupOptionViewModel = hiltViewModel(),
content: @Composable () -> Unit = {}, content: @Composable () -> Unit = {},
) { ) {
val context = LocalContext.current val context = LocalContext.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val viewState = groupOptionViewModel.viewState.collectAsStateValue() val groupOptionUiState = groupOptionViewModel.groupOptionUiState.collectAsStateValue()
val group = viewState.group val group = groupOptionUiState.group
val toastString = stringResource(R.string.rename_toast, viewState.newName) val toastString = stringResource(R.string.rename_toast, groupOptionUiState.newName)
BackHandler(viewState.drawerState.isVisible) { BackHandler(groupOptionUiState.drawerState.isVisible) {
scope.launch { scope.launch {
viewState.drawerState.hide() groupOptionUiState.drawerState.hide()
} }
} }
BottomDrawer( BottomDrawer(
drawerState = viewState.drawerState, drawerState = groupOptionUiState.drawerState,
sheetContent = { sheetContent = {
Column(modifier = Modifier.navigationBarsPadding()) { Column(modifier = Modifier.navigationBarsPadding()) {
Column( Column(
@ -75,7 +74,7 @@ fun GroupOptionDrawer(
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
Text( Text(
modifier = Modifier.roundClick { modifier = Modifier.roundClick {
groupOptionViewModel.dispatch(GroupOptionViewAction.ShowRenameDialog) groupOptionViewModel.showRenameDialog()
}, },
text = group?.name ?: stringResource(R.string.unknown), text = group?.name ?: stringResource(R.string.unknown),
style = MaterialTheme.typography.headlineSmall, style = MaterialTheme.typography.headlineSmall,
@ -106,15 +105,15 @@ fun GroupOptionDrawer(
Spacer(modifier = Modifier.height(10.dp)) Spacer(modifier = Modifier.height(10.dp))
Preset(groupOptionViewModel, group, context) Preset(groupOptionViewModel, group, context)
if (viewState.groups.size != 1) { if (groupOptionUiState.groups.size != 1) {
Spacer(modifier = Modifier.height(26.dp)) Spacer(modifier = Modifier.height(26.dp))
Subtitle(text = stringResource(R.string.move_to_group)) Subtitle(text = stringResource(R.string.move_to_group))
Spacer(modifier = Modifier.height(10.dp)) Spacer(modifier = Modifier.height(10.dp))
if (viewState.groups.size > 6) { if (groupOptionUiState.groups.size > 6) {
LazyRowGroups(viewState, group, groupOptionViewModel) LazyRowGroups(groupOptionUiState, group, groupOptionViewModel)
} else { } else {
FlowRowGroups(viewState, group, groupOptionViewModel) FlowRowGroups(groupOptionUiState, group, groupOptionViewModel)
} }
} }
@ -132,20 +131,20 @@ fun GroupOptionDrawer(
AllParseFullContentDialog(groupName = group?.name ?: "") AllParseFullContentDialog(groupName = group?.name ?: "")
AllMoveToGroupDialog(groupName = group?.name ?: "") AllMoveToGroupDialog(groupName = group?.name ?: "")
TextFieldDialog( TextFieldDialog(
visible = viewState.renameDialogVisible, visible = groupOptionUiState.renameDialogVisible,
title = stringResource(R.string.rename), title = stringResource(R.string.rename),
icon = Icons.Outlined.Edit, icon = Icons.Outlined.Edit,
value = viewState.newName, value = groupOptionUiState.newName,
placeholder = stringResource(R.string.name), placeholder = stringResource(R.string.name),
onValueChange = { onValueChange = {
groupOptionViewModel.dispatch(GroupOptionViewAction.InputNewName(it)) groupOptionViewModel.inputNewName(it)
}, },
onDismissRequest = { onDismissRequest = {
groupOptionViewModel.dispatch(GroupOptionViewAction.HideRenameDialog) groupOptionViewModel.hideRenameDialog()
}, },
onConfirm = { onConfirm = {
groupOptionViewModel.dispatch(GroupOptionViewAction.Rename) groupOptionViewModel.rename()
groupOptionViewModel.dispatch(GroupOptionViewAction.Hide(scope)) groupOptionViewModel.hideDrawer(scope)
context.showToast(toastString) context.showToast(toastString)
} }
) )
@ -177,7 +176,7 @@ private fun Preset(
) )
}, },
) { ) {
groupOptionViewModel.dispatch(GroupOptionViewAction.ShowAllAllowNotificationDialog) groupOptionViewModel.showAllAllowNotificationDialog()
} }
SelectionChip( SelectionChip(
modifier = Modifier.animateContentSize(), modifier = Modifier.animateContentSize(),
@ -193,14 +192,14 @@ private fun Preset(
) )
}, },
) { ) {
groupOptionViewModel.dispatch(GroupOptionViewAction.ShowAllParseFullContentDialog) groupOptionViewModel.showAllParseFullContentDialog()
} }
SelectionChip( SelectionChip(
modifier = Modifier.animateContentSize(), modifier = Modifier.animateContentSize(),
content = stringResource(R.string.clear_articles), content = stringResource(R.string.clear_articles),
selected = false, selected = false,
) { ) {
groupOptionViewModel.dispatch(GroupOptionViewAction.ShowClearDialog) groupOptionViewModel.showClearDialog()
} }
if (group?.id != context.currentAccountId.getDefaultGroupId()) { if (group?.id != context.currentAccountId.getDefaultGroupId()) {
SelectionChip( SelectionChip(
@ -208,7 +207,7 @@ private fun Preset(
content = stringResource(R.string.delete_group), content = stringResource(R.string.delete_group),
selected = false, selected = false,
) { ) {
groupOptionViewModel.dispatch(GroupOptionViewAction.ShowDeleteDialog) groupOptionViewModel.showDeleteDialog()
} }
} }
} }
@ -216,7 +215,7 @@ private fun Preset(
@Composable @Composable
private fun FlowRowGroups( private fun FlowRowGroups(
viewState: GroupOptionViewState, groupOptionUiState: GroupOptionUiState,
group: Group?, group: Group?,
groupOptionViewModel: GroupOptionViewModel groupOptionViewModel: GroupOptionViewModel
) { ) {
@ -226,16 +225,14 @@ private fun FlowRowGroups(
crossAxisSpacing = 10.dp, crossAxisSpacing = 10.dp,
mainAxisSpacing = 10.dp, mainAxisSpacing = 10.dp,
) { ) {
viewState.groups.forEach { groupOptionUiState.groups.forEach {
if (it.id != group?.id) { if (it.id != group?.id) {
SelectionChip( SelectionChip(
modifier = Modifier.animateContentSize(), modifier = Modifier.animateContentSize(),
content = it.name, content = it.name,
selected = false, selected = false,
) { ) {
groupOptionViewModel.dispatch( groupOptionViewModel.showAllMoveToGroupDialog(it)
GroupOptionViewAction.ShowAllMoveToGroupDialog(it)
)
} }
} }
} }
@ -244,21 +241,19 @@ private fun FlowRowGroups(
@Composable @Composable
private fun LazyRowGroups( private fun LazyRowGroups(
viewState: GroupOptionViewState, groupOptionUiState: GroupOptionUiState,
group: Group?, group: Group?,
groupOptionViewModel: GroupOptionViewModel groupOptionViewModel: GroupOptionViewModel
) { ) {
LazyRow { LazyRow {
items(viewState.groups) { items(groupOptionUiState.groups) {
if (it.id != group?.id) { if (it.id != group?.id) {
SelectionChip( SelectionChip(
modifier = Modifier.animateContentSize(), modifier = Modifier.animateContentSize(),
content = it.name, content = it.name,
selected = false, selected = false,
) { ) {
groupOptionViewModel.dispatch( groupOptionViewModel.showAllMoveToGroupDialog(it)
GroupOptionViewAction.ShowAllMoveToGroupDialog(it)
)
} }
} }
Spacer(modifier = Modifier.width(10.dp)) Spacer(modifier = Modifier.width(10.dp))

View File

@ -29,13 +29,13 @@ class GroupOptionViewModel @Inject constructor(
@DispatcherIO @DispatcherIO
private val dispatcherIO: CoroutineDispatcher, private val dispatcherIO: CoroutineDispatcher,
) : ViewModel() { ) : ViewModel() {
private val _viewState = MutableStateFlow(GroupOptionViewState()) private val _groupOptionUiState = MutableStateFlow(GroupOptionUiState())
val viewState: StateFlow<GroupOptionViewState> = _viewState.asStateFlow() val groupOptionUiState: StateFlow<GroupOptionUiState> = _groupOptionUiState.asStateFlow()
init { init {
viewModelScope.launch(dispatcherIO) { viewModelScope.launch(dispatcherIO) {
rssRepository.get().pullGroups().collect { groups -> rssRepository.get().pullGroups().collect { groups ->
_viewState.update { _groupOptionUiState.update {
it.copy( it.copy(
groups = groups groups = groups
) )
@ -44,70 +44,25 @@ class GroupOptionViewModel @Inject constructor(
} }
} }
fun dispatch(action: GroupOptionViewAction) { fun showDrawer(scope: CoroutineScope, groupId: String) {
when (action) {
is GroupOptionViewAction.Show -> show(action.scope, action.groupId)
is GroupOptionViewAction.Hide -> hide(action.scope)
is GroupOptionViewAction.ShowDeleteDialog -> changeDeleteDialogVisible(true)
is GroupOptionViewAction.HideDeleteDialog -> changeDeleteDialogVisible(false)
is GroupOptionViewAction.Delete -> delete(action.callback)
is GroupOptionViewAction.ShowClearDialog -> showClearDialog()
is GroupOptionViewAction.HideClearDialog -> hideClearDialog()
is GroupOptionViewAction.Clear -> clear(action.callback)
is GroupOptionViewAction.ShowAllAllowNotificationDialog ->
changeAllAllowNotificationDialogVisible(true)
is GroupOptionViewAction.HideAllAllowNotificationDialog ->
changeAllAllowNotificationDialogVisible(false)
is GroupOptionViewAction.AllAllowNotification ->
allAllowNotification(action.isNotification, action.callback)
is GroupOptionViewAction.ShowAllParseFullContentDialog ->
changeAllParseFullContentDialogVisible(true)
is GroupOptionViewAction.HideAllParseFullContentDialog ->
changeAllParseFullContentDialogVisible(false)
is GroupOptionViewAction.AllParseFullContent ->
allParseFullContent(action.isFullContent, action.callback)
is GroupOptionViewAction.ShowAllMoveToGroupDialog ->
changeAllMoveToGroupDialogVisible(action.targetGroup, true)
is GroupOptionViewAction.HideAllMoveToGroupDialog ->
changeAllMoveToGroupDialogVisible(visible = false)
is GroupOptionViewAction.AllMoveToGroup ->
allMoveToGroup(action.callback)
is GroupOptionViewAction.InputNewName -> inputNewName(action.content)
is GroupOptionViewAction.Rename -> rename()
is GroupOptionViewAction.ShowRenameDialog -> changeRenameDialogVisible(true)
is GroupOptionViewAction.HideRenameDialog -> changeRenameDialogVisible(false)
}
}
private suspend fun fetchGroup(groupId: String) {
val group = rssRepository.get().findGroupById(groupId)
_viewState.update {
it.copy(
group = group,
)
}
}
private fun show(scope: CoroutineScope, groupId: String) {
scope.launch { scope.launch {
fetchGroup(groupId) _groupOptionUiState.update {
_viewState.value.drawerState.show() it.copy(
group = rssRepository.get().findGroupById(groupId),
)
}
_groupOptionUiState.value.drawerState.show()
} }
} }
private fun hide(scope: CoroutineScope) { fun hideDrawer(scope: CoroutineScope) {
scope.launch { scope.launch {
_viewState.value.drawerState.hide() _groupOptionUiState.value.drawerState.hide()
} }
} }
private fun allAllowNotification(isNotification: Boolean, callback: () -> Unit = {}) { fun allAllowNotification(isNotification: Boolean, callback: () -> Unit = {}) {
_viewState.value.group?.let { _groupOptionUiState.value.group?.let {
viewModelScope.launch(dispatcherIO) { viewModelScope.launch(dispatcherIO) {
rssRepository.get().groupAllowNotification(it, isNotification) rssRepository.get().groupAllowNotification(it, isNotification)
withContext(dispatcherMain) { withContext(dispatcherMain) {
@ -117,16 +72,24 @@ class GroupOptionViewModel @Inject constructor(
} }
} }
private fun changeAllAllowNotificationDialogVisible(visible: Boolean) { fun showAllAllowNotificationDialog() {
_viewState.update { _groupOptionUiState.update {
it.copy( it.copy(
allAllowNotificationDialogVisible = visible, allAllowNotificationDialogVisible = true,
) )
} }
} }
private fun allParseFullContent(isFullContent: Boolean, callback: () -> Unit = {}) { fun hideAllAllowNotificationDialog() {
_viewState.value.group?.let { _groupOptionUiState.update {
it.copy(
allAllowNotificationDialogVisible = false,
)
}
}
fun allParseFullContent(isFullContent: Boolean, callback: () -> Unit = {}) {
_groupOptionUiState.value.group?.let {
viewModelScope.launch(dispatcherIO) { viewModelScope.launch(dispatcherIO) {
rssRepository.get().groupParseFullContent(it, isFullContent) rssRepository.get().groupParseFullContent(it, isFullContent)
withContext(dispatcherMain) { withContext(dispatcherMain) {
@ -136,16 +99,24 @@ class GroupOptionViewModel @Inject constructor(
} }
} }
private fun changeAllParseFullContentDialogVisible(visible: Boolean) { fun showAllParseFullContentDialog() {
_viewState.update { _groupOptionUiState.update {
it.copy( it.copy(
allParseFullContentDialogVisible = visible, allParseFullContentDialogVisible = true,
) )
} }
} }
private fun delete(callback: () -> Unit = {}) { fun hideAllParseFullContentDialog() {
_viewState.value.group?.let { _groupOptionUiState.update {
it.copy(
allParseFullContentDialogVisible = false,
)
}
}
fun delete(callback: () -> Unit = {}) {
_groupOptionUiState.value.group?.let {
viewModelScope.launch(dispatcherIO) { viewModelScope.launch(dispatcherIO) {
rssRepository.get().deleteGroup(it) rssRepository.get().deleteGroup(it)
withContext(dispatcherMain) { withContext(dispatcherMain) {
@ -155,32 +126,40 @@ class GroupOptionViewModel @Inject constructor(
} }
} }
private fun changeDeleteDialogVisible(visible: Boolean) { fun showDeleteDialog() {
_viewState.update { _groupOptionUiState.update {
it.copy( it.copy(
deleteDialogVisible = visible, deleteDialogVisible = true,
) )
} }
} }
private fun showClearDialog() { fun hideDeleteDialog() {
_viewState.update { _groupOptionUiState.update {
it.copy(
deleteDialogVisible = false,
)
}
}
fun showClearDialog() {
_groupOptionUiState.update {
it.copy( it.copy(
clearDialogVisible = true, clearDialogVisible = true,
) )
} }
} }
private fun hideClearDialog() { fun hideClearDialog() {
_viewState.update { _groupOptionUiState.update {
it.copy( it.copy(
clearDialogVisible = false, clearDialogVisible = false,
) )
} }
} }
private fun clear(callback: () -> Unit = {}) { fun clear(callback: () -> Unit = {}) {
_viewState.value.group?.let { _groupOptionUiState.value.group?.let {
viewModelScope.launch(dispatcherIO) { viewModelScope.launch(dispatcherIO) {
rssRepository.get().deleteArticles(group = it) rssRepository.get().deleteArticles(group = it)
withContext(dispatcherMain) { withContext(dispatcherMain) {
@ -190,9 +169,9 @@ class GroupOptionViewModel @Inject constructor(
} }
} }
private fun allMoveToGroup(callback: () -> Unit) { fun allMoveToGroup(callback: () -> Unit) {
_viewState.value.group?.let { group -> _groupOptionUiState.value.group?.let { group ->
_viewState.value.targetGroup?.let { targetGroup -> _groupOptionUiState.value.targetGroup?.let { targetGroup ->
viewModelScope.launch(dispatcherIO) { viewModelScope.launch(dispatcherIO) {
rssRepository.get().groupMoveToTargetGroup(group, targetGroup) rssRepository.get().groupMoveToTargetGroup(group, targetGroup)
withContext(dispatcherMain) { withContext(dispatcherMain) {
@ -203,24 +182,33 @@ class GroupOptionViewModel @Inject constructor(
} }
} }
private fun changeAllMoveToGroupDialogVisible(targetGroup: Group? = null, visible: Boolean) { fun showAllMoveToGroupDialog(targetGroup: Group) {
_viewState.update { _groupOptionUiState.update {
it.copy( it.copy(
targetGroup = if (visible) targetGroup else null, targetGroup = targetGroup,
allMoveToGroupDialogVisible = visible, allMoveToGroupDialogVisible = true,
) )
} }
} }
private fun rename() { fun hideAllMoveToGroupDialog() {
_viewState.value.group?.let { _groupOptionUiState.update {
it.copy(
targetGroup = null,
allMoveToGroupDialogVisible = false,
)
}
}
fun rename() {
_groupOptionUiState.value.group?.let {
viewModelScope.launch { viewModelScope.launch {
rssRepository.get().updateGroup( rssRepository.get().updateGroup(
it.copy( it.copy(
name = _viewState.value.newName name = _groupOptionUiState.value.newName
) )
) )
_viewState.update { _groupOptionUiState.update {
it.copy( it.copy(
renameDialogVisible = false, renameDialogVisible = false,
) )
@ -229,17 +217,26 @@ class GroupOptionViewModel @Inject constructor(
} }
} }
private fun changeRenameDialogVisible(visible: Boolean) { fun showRenameDialog() {
_viewState.update { _groupOptionUiState.update {
it.copy( it.copy(
renameDialogVisible = visible, renameDialogVisible = true,
newName = if (visible) _viewState.value.group?.name ?: "" else "", newName = _groupOptionUiState.value.group?.name ?: "",
) )
} }
} }
private fun inputNewName(content: String) { fun hideRenameDialog() {
_viewState.update { _groupOptionUiState.update {
it.copy(
renameDialogVisible = false,
newName = "",
)
}
}
fun inputNewName(content: String) {
_groupOptionUiState.update {
it.copy( it.copy(
newName = content newName = content
) )
@ -248,7 +245,7 @@ class GroupOptionViewModel @Inject constructor(
} }
@OptIn(ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)
data class GroupOptionViewState( data class GroupOptionUiState(
var drawerState: ModalBottomSheetState = ModalBottomSheetState(ModalBottomSheetValue.Hidden), var drawerState: ModalBottomSheetState = ModalBottomSheetState(ModalBottomSheetValue.Hidden),
val group: Group? = null, val group: Group? = null,
val targetGroup: Group? = null, val targetGroup: Group? = null,
@ -261,61 +258,3 @@ data class GroupOptionViewState(
val newName: String = "", val newName: String = "",
val renameDialogVisible: Boolean = false, val renameDialogVisible: Boolean = false,
) )
sealed class GroupOptionViewAction {
data class Show(
val scope: CoroutineScope,
val groupId: String
) : GroupOptionViewAction()
data class Hide(
val scope: CoroutineScope,
) : GroupOptionViewAction()
data class Delete(
val callback: () -> Unit = {}
) : GroupOptionViewAction()
object ShowDeleteDialog : GroupOptionViewAction()
object HideDeleteDialog : GroupOptionViewAction()
data class Clear(
val callback: () -> Unit = {}
) : GroupOptionViewAction()
object ShowClearDialog : GroupOptionViewAction()
object HideClearDialog : GroupOptionViewAction()
data class AllParseFullContent(
val isFullContent: Boolean,
val callback: () -> Unit = {}
) : GroupOptionViewAction()
object ShowAllParseFullContentDialog : GroupOptionViewAction()
object HideAllParseFullContentDialog : GroupOptionViewAction()
data class AllAllowNotification(
val isNotification: Boolean,
val callback: () -> Unit = {}
) : GroupOptionViewAction()
object ShowAllAllowNotificationDialog : GroupOptionViewAction()
object HideAllAllowNotificationDialog : GroupOptionViewAction()
data class AllMoveToGroup(
val callback: () -> Unit = {}
) : GroupOptionViewAction()
data class ShowAllMoveToGroupDialog(
val targetGroup: Group
) : GroupOptionViewAction()
object HideAllMoveToGroupDialog : GroupOptionViewAction()
object ShowRenameDialog : GroupOptionViewAction()
object HideRenameDialog : GroupOptionViewAction()
object Rename : GroupOptionViewAction()
data class InputNewName(
val content: String
) : GroupOptionViewAction()
}

View File

@ -41,32 +41,32 @@ fun SubscribeDialog(
) { ) {
val context = LocalContext.current val context = LocalContext.current
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
val viewState = subscribeViewModel.viewState.collectAsStateValue() val subscribeUiState = subscribeViewModel.subscribeUiState.collectAsStateValue()
val groupsState = viewState.groups.collectAsState(initial = emptyList()) val groupsState = subscribeUiState.groups.collectAsState(initial = emptyList())
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) {
it?.let { uri -> it?.let { uri ->
context.contentResolver.openInputStream(uri)?.let { inputStream -> context.contentResolver.openInputStream(uri)?.let { inputStream ->
subscribeViewModel.dispatch(SubscribeViewAction.ImportFromInputStream(inputStream)) subscribeViewModel.importFromInputStream(inputStream)
} }
} }
} }
LaunchedEffect(viewState.visible) { LaunchedEffect(subscribeUiState.visible) {
if (viewState.visible) { if (subscribeUiState.visible) {
subscribeViewModel.dispatch(SubscribeViewAction.Init) subscribeViewModel.init()
} else { } else {
subscribeViewModel.dispatch(SubscribeViewAction.Reset) subscribeViewModel.reset()
subscribeViewModel.dispatch(SubscribeViewAction.SwitchPage(true)) subscribeViewModel.switchPage(true)
} }
} }
Dialog( Dialog(
modifier = Modifier.padding(horizontal = 44.dp), modifier = Modifier.padding(horizontal = 44.dp),
visible = viewState.visible, visible = subscribeUiState.visible,
properties = DialogProperties(usePlatformDefaultWidth = false), properties = DialogProperties(usePlatformDefaultWidth = false),
onDismissRequest = { onDismissRequest = {
focusManager.clearFocus() focusManager.clearFocus()
subscribeViewModel.dispatch(SubscribeViewAction.Hide) subscribeViewModel.hideDrawer()
}, },
icon = { icon = {
Icon( Icon(
@ -76,10 +76,10 @@ fun SubscribeDialog(
}, },
title = { title = {
Text( Text(
text = if (viewState.isSearchPage) { text = if (subscribeUiState.isSearchPage) {
viewState.title subscribeUiState.title
} else { } else {
viewState.feed?.name ?: stringResource(R.string.unknown) subscribeUiState.feed?.name ?: stringResource(R.string.unknown)
}, },
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
@ -87,7 +87,7 @@ fun SubscribeDialog(
}, },
text = { text = {
AnimatedContent( AnimatedContent(
targetState = viewState.isSearchPage, targetState = subscribeUiState.isSearchPage,
transitionSpec = { transitionSpec = {
slideInHorizontally { width -> width } + fadeIn() with slideInHorizontally { width -> width } + fadeIn() with
slideOutHorizontally { width -> -width } + fadeOut() slideOutHorizontally { width -> -width } + fadeOut()
@ -95,55 +95,55 @@ fun SubscribeDialog(
) { targetExpanded -> ) { targetExpanded ->
if (targetExpanded) { if (targetExpanded) {
ClipboardTextField( ClipboardTextField(
readOnly = viewState.lockLinkInput, readOnly = subscribeUiState.lockLinkInput,
value = viewState.linkContent, value = subscribeUiState.linkContent,
onValueChange = { onValueChange = {
subscribeViewModel.dispatch(SubscribeViewAction.InputLink(it)) subscribeViewModel.inputLink(it)
}, },
placeholder = stringResource(R.string.feed_or_site_url), placeholder = stringResource(R.string.feed_or_site_url),
errorText = viewState.errorMessage, errorText = subscribeUiState.errorMessage,
imeAction = ImeAction.Search, imeAction = ImeAction.Search,
focusManager = focusManager, focusManager = focusManager,
onConfirm = { onConfirm = {
subscribeViewModel.dispatch(SubscribeViewAction.Search) subscribeViewModel.search()
}, },
) )
} else { } else {
ResultView( ResultView(
link = viewState.linkContent, link = subscribeUiState.linkContent,
groups = groupsState.value, groups = groupsState.value,
selectedAllowNotificationPreset = viewState.allowNotificationPreset, selectedAllowNotificationPreset = subscribeUiState.allowNotificationPreset,
selectedParseFullContentPreset = viewState.parseFullContentPreset, selectedParseFullContentPreset = subscribeUiState.parseFullContentPreset,
selectedGroupId = viewState.selectedGroupId, selectedGroupId = subscribeUiState.selectedGroupId,
allowNotificationPresetOnClick = { allowNotificationPresetOnClick = {
subscribeViewModel.dispatch(SubscribeViewAction.ChangeAllowNotificationPreset) subscribeViewModel.changeAllowNotificationPreset()
}, },
parseFullContentPresetOnClick = { parseFullContentPresetOnClick = {
subscribeViewModel.dispatch(SubscribeViewAction.ChangeParseFullContentPreset) subscribeViewModel.changeParseFullContentPreset()
}, },
onGroupClick = { onGroupClick = {
subscribeViewModel.dispatch(SubscribeViewAction.SelectedGroup(it)) subscribeViewModel.selectedGroup(it)
}, },
onAddNewGroup = { onAddNewGroup = {
subscribeViewModel.dispatch(SubscribeViewAction.ShowNewGroupDialog) subscribeViewModel.showNewGroupDialog()
}, },
) )
} }
} }
}, },
confirmButton = { confirmButton = {
if (viewState.isSearchPage) { if (subscribeUiState.isSearchPage) {
TextButton( TextButton(
enabled = viewState.linkContent.isNotBlank() enabled = subscribeUiState.linkContent.isNotBlank()
&& viewState.title != stringResource(R.string.searching), && subscribeUiState.title != stringResource(R.string.searching),
onClick = { onClick = {
focusManager.clearFocus() focusManager.clearFocus()
subscribeViewModel.dispatch(SubscribeViewAction.Search) subscribeViewModel.search()
} }
) { ) {
Text( Text(
text = stringResource(R.string.search), text = stringResource(R.string.search),
color = if (viewState.linkContent.isNotBlank()) { color = if (subscribeUiState.linkContent.isNotBlank()) {
Color.Unspecified Color.Unspecified
} else { } else {
MaterialTheme.colorScheme.outline.copy(alpha = 0.7f) MaterialTheme.colorScheme.outline.copy(alpha = 0.7f)
@ -154,7 +154,7 @@ fun SubscribeDialog(
TextButton( TextButton(
onClick = { onClick = {
focusManager.clearFocus() focusManager.clearFocus()
subscribeViewModel.dispatch(SubscribeViewAction.Subscribe) subscribeViewModel.subscribe()
} }
) { ) {
Text(stringResource(R.string.subscribe)) Text(stringResource(R.string.subscribe))
@ -162,12 +162,12 @@ fun SubscribeDialog(
} }
}, },
dismissButton = { dismissButton = {
if (viewState.isSearchPage) { if (subscribeUiState.isSearchPage) {
TextButton( TextButton(
onClick = { onClick = {
focusManager.clearFocus() focusManager.clearFocus()
launcher.launch("*/*") launcher.launch("*/*")
subscribeViewModel.dispatch(SubscribeViewAction.Hide) subscribeViewModel.hideDrawer()
} }
) { ) {
Text(text = stringResource(R.string.import_from_opml)) Text(text = stringResource(R.string.import_from_opml))
@ -176,7 +176,7 @@ fun SubscribeDialog(
TextButton( TextButton(
onClick = { onClick = {
focusManager.clearFocus() focusManager.clearFocus()
subscribeViewModel.dispatch(SubscribeViewAction.Hide) subscribeViewModel.hideDrawer()
} }
) { ) {
Text(text = stringResource(R.string.cancel)) Text(text = stringResource(R.string.cancel))
@ -186,19 +186,19 @@ fun SubscribeDialog(
) )
TextFieldDialog( TextFieldDialog(
visible = viewState.newGroupDialogVisible, visible = subscribeUiState.newGroupDialogVisible,
title = stringResource(R.string.create_new_group), title = stringResource(R.string.create_new_group),
icon = Icons.Outlined.CreateNewFolder, icon = Icons.Outlined.CreateNewFolder,
value = viewState.newGroupContent, value = subscribeUiState.newGroupContent,
placeholder = stringResource(R.string.name), placeholder = stringResource(R.string.name),
onValueChange = { onValueChange = {
subscribeViewModel.dispatch(SubscribeViewAction.InputNewGroup(it)) subscribeViewModel.inputNewGroup(it)
}, },
onDismissRequest = { onDismissRequest = {
subscribeViewModel.dispatch(SubscribeViewAction.HideNewGroupDialog) subscribeViewModel.hideNewGroupDialog()
}, },
onConfirm = { onConfirm = {
subscribeViewModel.dispatch(SubscribeViewAction.AddNewGroup) subscribeViewModel.addNewGroup()
} }
) )
} }

View File

@ -3,7 +3,6 @@ package me.ash.reader.ui.page.home.feeds.subscribe
import android.util.Log import android.util.Log
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.google.accompanist.pager.ExperimentalPagerApi
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -32,35 +31,12 @@ class SubscribeViewModel @Inject constructor(
@DispatcherIO @DispatcherIO
private val dispatcherIO: CoroutineDispatcher, private val dispatcherIO: CoroutineDispatcher,
) : ViewModel() { ) : ViewModel() {
private val _viewState = MutableStateFlow(SubscribeViewState()) private val _subscribeUiState = MutableStateFlow(SubscribeUiState())
val viewState: StateFlow<SubscribeViewState> = _viewState.asStateFlow() val subscribeUiState: StateFlow<SubscribeUiState> = _subscribeUiState.asStateFlow()
private var searchJob: Job? = null private var searchJob: Job? = null
fun dispatch(action: SubscribeViewAction) { fun init() {
when (action) { _subscribeUiState.update {
is SubscribeViewAction.Init -> init()
is SubscribeViewAction.Reset -> reset()
is SubscribeViewAction.Show -> changeVisible(true)
is SubscribeViewAction.Hide -> changeVisible(false)
is SubscribeViewAction.ShowNewGroupDialog -> changeNewGroupDialogVisible(true)
is SubscribeViewAction.HideNewGroupDialog -> changeNewGroupDialogVisible(false)
is SubscribeViewAction.SwitchPage -> switchPage(action.isSearchPage)
is SubscribeViewAction.ImportFromInputStream -> importFromInputStream(action.inputStream)
is SubscribeViewAction.InputLink -> inputLink(action.content)
is SubscribeViewAction.Search -> search()
is SubscribeViewAction.ChangeAllowNotificationPreset ->
changeAllowNotificationPreset()
is SubscribeViewAction.ChangeParseFullContentPreset ->
changeParseFullContentPreset()
is SubscribeViewAction.SelectedGroup -> selectedGroup(action.groupId)
is SubscribeViewAction.InputNewGroup -> inputNewGroup(action.content)
is SubscribeViewAction.AddNewGroup -> addNewGroup()
is SubscribeViewAction.Subscribe -> subscribe()
}
}
private fun init() {
_viewState.update {
it.copy( it.copy(
title = stringsRepository.getString(R.string.subscribe), title = stringsRepository.getString(R.string.subscribe),
groups = rssRepository.get().pullGroups(), groups = rssRepository.get().pullGroups(),
@ -68,17 +44,17 @@ class SubscribeViewModel @Inject constructor(
} }
} }
private fun reset() { fun reset() {
searchJob?.cancel() searchJob?.cancel()
searchJob = null searchJob = null
_viewState.update { _subscribeUiState.update {
SubscribeViewState().copy( SubscribeUiState().copy(
title = stringsRepository.getString(R.string.subscribe), title = stringsRepository.getString(R.string.subscribe),
) )
} }
} }
private fun importFromInputStream(inputStream: InputStream) { fun importFromInputStream(inputStream: InputStream) {
viewModelScope.launch(dispatcherIO) { viewModelScope.launch(dispatcherIO) {
try { try {
opmlRepository.saveToDatabase(inputStream) opmlRepository.saveToDatabase(inputStream)
@ -89,38 +65,38 @@ class SubscribeViewModel @Inject constructor(
} }
} }
private fun subscribe() { fun subscribe() {
val feed = _viewState.value.feed ?: return val feed = _subscribeUiState.value.feed ?: return
val articles = _viewState.value.articles val articles = _subscribeUiState.value.articles
viewModelScope.launch(dispatcherIO) { viewModelScope.launch(dispatcherIO) {
val groupId = async { val groupId = async {
_viewState.value.selectedGroupId _subscribeUiState.value.selectedGroupId
} }
rssRepository.get().subscribe( rssRepository.get().subscribe(
feed.copy( feed.copy(
groupId = groupId.await(), groupId = groupId.await(),
isNotification = _viewState.value.allowNotificationPreset, isNotification = _subscribeUiState.value.allowNotificationPreset,
isFullContent = _viewState.value.parseFullContentPreset, isFullContent = _subscribeUiState.value.parseFullContentPreset,
), articles ), articles
) )
changeVisible(false) hideDrawer()
} }
} }
private fun selectedGroup(groupId: String) { fun selectedGroup(groupId: String) {
_viewState.update { _subscribeUiState.update {
it.copy( it.copy(
selectedGroupId = groupId, selectedGroupId = groupId,
) )
} }
} }
private fun addNewGroup() { fun addNewGroup() {
if (_viewState.value.newGroupContent.isNotBlank()) { if (_subscribeUiState.value.newGroupContent.isNotBlank()) {
viewModelScope.launch { viewModelScope.launch {
selectedGroup(rssRepository.get().addGroup(_viewState.value.newGroupContent)) selectedGroup(rssRepository.get().addGroup(_subscribeUiState.value.newGroupContent))
changeNewGroupDialogVisible(false) hideNewGroupDialog()
_viewState.update { _subscribeUiState.update {
it.copy( it.copy(
newGroupContent = "", newGroupContent = "",
) )
@ -129,48 +105,48 @@ class SubscribeViewModel @Inject constructor(
} }
} }
private fun changeParseFullContentPreset() { fun changeParseFullContentPreset() {
_viewState.update { _subscribeUiState.update {
it.copy( it.copy(
parseFullContentPreset = !_viewState.value.parseFullContentPreset parseFullContentPreset = !_subscribeUiState.value.parseFullContentPreset
) )
} }
} }
private fun changeAllowNotificationPreset() { fun changeAllowNotificationPreset() {
_viewState.update { _subscribeUiState.update {
it.copy( it.copy(
allowNotificationPreset = !_viewState.value.allowNotificationPreset allowNotificationPreset = !_subscribeUiState.value.allowNotificationPreset
) )
} }
} }
private fun search() { fun search() {
searchJob?.cancel() searchJob?.cancel()
viewModelScope.launch(dispatcherIO) { viewModelScope.launch(dispatcherIO) {
try { try {
_viewState.update { _subscribeUiState.update {
it.copy( it.copy(
errorMessage = "", errorMessage = "",
) )
} }
_viewState.value.linkContent.formatUrl().let { str -> _subscribeUiState.value.linkContent.formatUrl().let { str ->
if (str != _viewState.value.linkContent) { if (str != _subscribeUiState.value.linkContent) {
_viewState.update { _subscribeUiState.update {
it.copy( it.copy(
linkContent = str linkContent = str
) )
} }
} }
} }
_viewState.update { _subscribeUiState.update {
it.copy( it.copy(
title = stringsRepository.getString(R.string.searching), title = stringsRepository.getString(R.string.searching),
lockLinkInput = true, lockLinkInput = true,
) )
} }
if (rssRepository.get().isFeedExist(_viewState.value.linkContent)) { if (rssRepository.get().isFeedExist(_subscribeUiState.value.linkContent)) {
_viewState.update { _subscribeUiState.update {
it.copy( it.copy(
title = stringsRepository.getString(R.string.subscribe), title = stringsRepository.getString(R.string.subscribe),
errorMessage = stringsRepository.getString(R.string.already_subscribed), errorMessage = stringsRepository.getString(R.string.already_subscribed),
@ -179,8 +155,8 @@ class SubscribeViewModel @Inject constructor(
} }
return@launch return@launch
} }
val feedWithArticle = rssHelper.searchFeed(_viewState.value.linkContent) val feedWithArticle = rssHelper.searchFeed(_subscribeUiState.value.linkContent)
_viewState.update { _subscribeUiState.update {
it.copy( it.copy(
feed = feedWithArticle.feed, feed = feedWithArticle.feed,
articles = feedWithArticle.articles, articles = feedWithArticle.articles,
@ -189,7 +165,7 @@ class SubscribeViewModel @Inject constructor(
switchPage(false) switchPage(false)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
_viewState.update { _subscribeUiState.update {
it.copy( it.copy(
title = stringsRepository.getString(R.string.subscribe), title = stringsRepository.getString(R.string.subscribe),
errorMessage = e.message ?: stringsRepository.getString(R.string.unknown), errorMessage = e.message ?: stringsRepository.getString(R.string.unknown),
@ -202,8 +178,8 @@ class SubscribeViewModel @Inject constructor(
} }
} }
private fun inputLink(content: String) { fun inputLink(content: String) {
_viewState.update { _subscribeUiState.update {
it.copy( it.copy(
linkContent = content, linkContent = content,
errorMessage = "", errorMessage = "",
@ -211,32 +187,48 @@ class SubscribeViewModel @Inject constructor(
} }
} }
private fun inputNewGroup(content: String) { fun inputNewGroup(content: String) {
_viewState.update { _subscribeUiState.update {
it.copy( it.copy(
newGroupContent = content newGroupContent = content
) )
} }
} }
private fun changeVisible(visible: Boolean) { fun showDrawer() {
_viewState.update { _subscribeUiState.update {
it.copy( it.copy(
visible = visible visible = true
) )
} }
} }
private fun changeNewGroupDialogVisible(visible: Boolean) { fun hideDrawer() {
_viewState.update { _subscribeUiState.update {
it.copy( it.copy(
newGroupDialogVisible = visible, visible = false
) )
} }
} }
private fun switchPage(isSearchPage: Boolean) { fun showNewGroupDialog() {
_viewState.update { _subscribeUiState.update {
it.copy(
newGroupDialogVisible = true,
)
}
}
fun hideNewGroupDialog() {
_subscribeUiState.update {
it.copy(
newGroupDialogVisible = false,
)
}
}
fun switchPage(isSearchPage: Boolean) {
_subscribeUiState.update {
it.copy( it.copy(
isSearchPage = isSearchPage isSearchPage = isSearchPage
) )
@ -244,7 +236,7 @@ class SubscribeViewModel @Inject constructor(
} }
} }
data class SubscribeViewState( data class SubscribeUiState(
val visible: Boolean = false, val visible: Boolean = false,
val title: String = "", val title: String = "",
val errorMessage: String = "", val errorMessage: String = "",
@ -260,42 +252,3 @@ data class SubscribeViewState(
val groups: Flow<List<Group>> = emptyFlow(), val groups: Flow<List<Group>> = emptyFlow(),
val isSearchPage: Boolean = true, val isSearchPage: Boolean = true,
) )
sealed class SubscribeViewAction {
object Init : SubscribeViewAction()
object Reset : SubscribeViewAction()
object Show : SubscribeViewAction()
object Hide : SubscribeViewAction()
object ShowNewGroupDialog : SubscribeViewAction()
object HideNewGroupDialog : SubscribeViewAction()
object AddNewGroup : SubscribeViewAction()
data class SwitchPage(
val isSearchPage: Boolean
) : SubscribeViewAction()
data class ImportFromInputStream(
val inputStream: InputStream
) : SubscribeViewAction()
data class InputLink(
val content: String
) : SubscribeViewAction()
object Search : SubscribeViewAction()
object ChangeAllowNotificationPreset : SubscribeViewAction()
object ChangeParseFullContentPreset : SubscribeViewAction()
data class SelectedGroup(
val groupId: String
) : SubscribeViewAction()
data class InputNewGroup(
val content: String
) : SubscribeViewAction()
object Subscribe : SubscribeViewAction()
}

View File

@ -34,7 +34,6 @@ import me.ash.reader.ui.component.base.SwipeRefresh
import me.ash.reader.ui.ext.collectAsStateValue import me.ash.reader.ui.ext.collectAsStateValue
import me.ash.reader.ui.page.common.RouteName import me.ash.reader.ui.page.common.RouteName
import me.ash.reader.ui.page.home.FilterState import me.ash.reader.ui.page.home.FilterState
import me.ash.reader.ui.page.home.HomeViewAction
import me.ash.reader.ui.page.home.HomeViewModel import me.ash.reader.ui.page.home.HomeViewModel
@OptIn( @OptIn(
@ -47,8 +46,6 @@ fun FlowPage(
flowViewModel: FlowViewModel = hiltViewModel(), flowViewModel: FlowViewModel = hiltViewModel(),
homeViewModel: HomeViewModel, homeViewModel: HomeViewModel,
) { ) {
val homeViewView = homeViewModel.viewState.collectAsStateValue()
val pagingItems = homeViewView.pagingData.collectAsLazyPagingItems()
val keyboardController = LocalSoftwareKeyboardController.current val keyboardController = LocalSoftwareKeyboardController.current
val topBarTonalElevation = LocalFlowTopBarTonalElevation.current val topBarTonalElevation = LocalFlowTopBarTonalElevation.current
val articleListTonalElevation = LocalFlowArticleListTonalElevation.current val articleListTonalElevation = LocalFlowArticleListTonalElevation.current
@ -59,16 +56,18 @@ fun FlowPage(
val filterBarPadding = LocalFlowFilterBarPadding.current val filterBarPadding = LocalFlowFilterBarPadding.current
val filterBarTonalElevation = LocalFlowFilterBarTonalElevation.current val filterBarTonalElevation = LocalFlowFilterBarTonalElevation.current
val homeUiState = homeViewModel.homeUiState.collectAsStateValue()
val flowUiState = flowViewModel.flowUiState.collectAsStateValue()
val filterUiState = homeViewModel.filterUiState.collectAsStateValue()
val pagingItems = homeUiState.pagingData.collectAsLazyPagingItems()
val listState =
if (pagingItems.itemCount > 0) flowUiState.listState else rememberLazyListState()
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
var markAsRead by remember { mutableStateOf(false) } var markAsRead by remember { mutableStateOf(false) }
var onSearch by remember { mutableStateOf(false) } var onSearch by remember { mutableStateOf(false) }
val viewState = flowViewModel.viewState.collectAsStateValue()
val filterState = homeViewModel.filterState.collectAsStateValue()
val homeViewState = homeViewModel.viewState.collectAsStateValue()
val listState = if (pagingItems.itemCount > 0) viewState.listState else rememberLazyListState()
val owner = LocalLifecycleOwner.current val owner = LocalLifecycleOwner.current
var isSyncing by remember { mutableStateOf(false) } var isSyncing by remember { mutableStateOf(false) }
homeViewModel.syncWorkLiveData.observe(owner) { homeViewModel.syncWorkLiveData.observe(owner) {
@ -82,15 +81,15 @@ fun FlowPage(
focusRequester.requestFocus() focusRequester.requestFocus()
} else { } else {
keyboardController?.hide() keyboardController?.hide()
if (homeViewState.searchContent.isNotBlank()) { if (homeUiState.searchContent.isNotBlank()) {
homeViewModel.dispatch(HomeViewAction.InputSearchContent("")) homeViewModel.inputSearchContent("")
} }
} }
} }
} }
LaunchedEffect(viewState.listState) { LaunchedEffect(flowUiState.listState) {
snapshotFlow { viewState.listState.firstVisibleItemIndex }.collect { snapshotFlow { flowUiState.listState.firstVisibleItemIndex }.collect {
if (it > 0) { if (it > 0) {
keyboardController?.hide() keyboardController?.hide()
} }
@ -122,7 +121,7 @@ fun FlowPage(
}, },
actions = { actions = {
AnimatedVisibility( AnimatedVisibility(
visible = !filterState.filter.isStarred(), visible = !filterUiState.filter.isStarred(),
enter = fadeIn() + expandVertically(), enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(), exit = fadeOut() + shrinkVertically(),
) { ) {
@ -136,7 +135,7 @@ fun FlowPage(
}, },
) { ) {
scope.launch { scope.launch {
viewState.listState.scrollToItem(0) flowUiState.listState.scrollToItem(0)
markAsRead = !markAsRead markAsRead = !markAsRead
onSearch = false onSearch = false
} }
@ -152,7 +151,7 @@ fun FlowPage(
}, },
) { ) {
scope.launch { scope.launch {
viewState.listState.scrollToItem(0) flowUiState.listState.scrollToItem(0)
onSearch = !onSearch onSearch = !onSearch
} }
} }
@ -161,7 +160,7 @@ fun FlowPage(
SwipeRefresh( SwipeRefresh(
onRefresh = { onRefresh = {
if (!isSyncing) { if (!isSyncing) {
flowViewModel.dispatch(FlowViewAction.Sync) flowViewModel.sync()
} }
} }
) { ) {
@ -170,7 +169,7 @@ fun FlowPage(
state = listState, state = listState,
) { ) {
item { item {
DisplayTextHeader(filterState, isSyncing, articleListFeedIcon.value) DisplayTextHeader(filterUiState, isSyncing, articleListFeedIcon.value)
AnimatedVisibility( AnimatedVisibility(
visible = markAsRead, visible = markAsRead,
enter = fadeIn() + expandVertically(), enter = fadeIn() + expandVertically(),
@ -186,13 +185,11 @@ fun FlowPage(
}, },
) { ) {
markAsRead = false markAsRead = false
flowViewModel.dispatch( flowViewModel.markAsRead(
FlowViewAction.MarkAsRead( groupId = filterUiState.group?.id,
groupId = filterState.group?.id, feedId = filterUiState.feed?.id,
feedId = filterState.feed?.id, articleId = null,
articleId = null, markAsReadBefore = it,
markAsReadBefore = it,
)
) )
} }
AnimatedVisibility( AnimatedVisibility(
@ -201,30 +198,30 @@ fun FlowPage(
exit = fadeOut() + shrinkVertically(), exit = fadeOut() + shrinkVertically(),
) { ) {
SearchBar( SearchBar(
value = homeViewState.searchContent, value = homeUiState.searchContent,
placeholder = when { placeholder = when {
filterState.group != null -> stringResource( filterUiState.group != null -> stringResource(
R.string.search_for_in, R.string.search_for_in,
filterState.filter.getName(), filterUiState.filter.getName(),
filterState.group.name filterUiState.group.name
) )
filterState.feed != null -> stringResource( filterUiState.feed != null -> stringResource(
R.string.search_for_in, R.string.search_for_in,
filterState.filter.getName(), filterUiState.filter.getName(),
filterState.feed.name filterUiState.feed.name
) )
else -> stringResource( else -> stringResource(
R.string.search_for, R.string.search_for,
filterState.filter.getName() filterUiState.filter.getName()
) )
}, },
focusRequester = focusRequester, focusRequester = focusRequester,
onValueChange = { onValueChange = {
homeViewModel.dispatch(HomeViewAction.InputSearchContent(it)) homeViewModel.inputSearchContent(it)
}, },
onClose = { onClose = {
onSearch = false onSearch = false
homeViewModel.dispatch(HomeViewAction.InputSearchContent("")) homeViewModel.inputSearchContent("")
} }
) )
Spacer(modifier = Modifier.height((56 + 24 + 10).dp)) Spacer(modifier = Modifier.height((56 + 24 + 10).dp))
@ -253,15 +250,15 @@ fun FlowPage(
}, },
bottomBar = { bottomBar = {
FilterBar( FilterBar(
filter = filterState.filter, filter = filterUiState.filter,
filterBarStyle = filterBarStyle.value, filterBarStyle = filterBarStyle.value,
filterBarFilled = filterBarFilled.value, filterBarFilled = filterBarFilled.value,
filterBarPadding = filterBarPadding.dp, filterBarPadding = filterBarPadding.dp,
filterBarTonalElevation = filterBarTonalElevation.value.dp, filterBarTonalElevation = filterBarTonalElevation.value.dp,
) { ) {
flowViewModel.dispatch(FlowViewAction.ScrollToItem(0)) flowViewModel.scrollToItem(0)
homeViewModel.dispatch(HomeViewAction.ChangeFilter(filterState.copy(filter = it))) homeViewModel.changeFilter(filterUiState.copy(filter = it))
homeViewModel.dispatch(HomeViewAction.FetchArticles) homeViewModel.fetchArticles()
} }
} }
) )

View File

@ -7,7 +7,6 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.ash.reader.data.entity.ArticleWithFeed import me.ash.reader.data.entity.ArticleWithFeed
import me.ash.reader.data.repository.RssRepository import me.ash.reader.data.repository.RssRepository
@ -19,40 +18,20 @@ import javax.inject.Inject
class FlowViewModel @Inject constructor( class FlowViewModel @Inject constructor(
private val rssRepository: RssRepository, private val rssRepository: RssRepository,
) : ViewModel() { ) : ViewModel() {
private val _viewState = MutableStateFlow(ArticleViewState()) private val _flowUiState = MutableStateFlow(FlowUiState())
val viewState: StateFlow<ArticleViewState> = _viewState.asStateFlow() val flowUiState: StateFlow<FlowUiState> = _flowUiState.asStateFlow()
fun dispatch(action: FlowViewAction) { fun sync() {
when (action) {
is FlowViewAction.Sync -> sync()
is FlowViewAction.ChangeIsBack -> changeIsBack(action.isBack)
is FlowViewAction.ScrollToItem -> scrollToItem(action.index)
is FlowViewAction.MarkAsRead -> markAsRead(
action.groupId,
action.feedId,
action.articleId,
action.markAsReadBefore,
)
}
}
private fun sync() {
rssRepository.get().doSync() rssRepository.get().doSync()
} }
private fun scrollToItem(index: Int) { fun scrollToItem(index: Int) {
viewModelScope.launch { viewModelScope.launch {
_viewState.value.listState.scrollToItem(index) _flowUiState.value.listState.scrollToItem(index)
} }
} }
private fun changeIsBack(isBack: Boolean) { fun markAsRead(
_viewState.update {
it.copy(isBack = isBack)
}
}
private fun markAsRead(
groupId: String?, groupId: String?,
feedId: String?, feedId: String?,
articleId: String?, articleId: String?,
@ -84,32 +63,13 @@ class FlowViewModel @Inject constructor(
} }
} }
data class ArticleViewState( data class FlowUiState(
val filterImportant: Int = 0, val filterImportant: Int = 0,
val listState: LazyListState = LazyListState(), val listState: LazyListState = LazyListState(),
val isBack: Boolean = false, val isBack: Boolean = false,
val syncWorkInfo: String = "", val syncWorkInfo: String = "",
) )
sealed class FlowViewAction {
object Sync : FlowViewAction()
data class ChangeIsBack(
val isBack: Boolean
) : FlowViewAction()
data class ScrollToItem(
val index: Int
) : FlowViewAction()
data class MarkAsRead(
val groupId: String?,
val feedId: String?,
val articleId: String?,
val markAsReadBefore: MarkAsReadBefore
) : FlowViewAction()
}
enum class MarkAsReadBefore { enum class MarkAsReadBefore {
SevenDays, SevenDays,
ThreeDays, ThreeDays,

View File

@ -1,4 +1,4 @@
package me.ash.reader.ui.page.home.read package me.ash.reader.ui.page.home.reading
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri

View File

@ -1,4 +1,4 @@
package me.ash.reader.ui.page.home.read package me.ash.reader.ui.page.home.reading
import android.view.HapticFeedbackConstants import android.view.HapticFeedbackConstants
import androidx.compose.foundation.background import androidx.compose.foundation.background

View File

@ -1,4 +1,4 @@
package me.ash.reader.ui.page.home.read package me.ash.reader.ui.page.home.reading
import android.content.Intent import android.content.Intent
import android.util.Log import android.util.Log
@ -32,26 +32,26 @@ import me.ash.reader.ui.ext.collectAsStateValue
import me.ash.reader.ui.ext.drawVerticalScrollbar import me.ash.reader.ui.ext.drawVerticalScrollbar
@Composable @Composable
fun ReadPage( fun ReadingPage(
navController: NavHostController, navController: NavHostController,
readViewModel: ReadViewModel = hiltViewModel(), readingViewModel: ReadingViewModel = hiltViewModel(),
) { ) {
val viewState = readViewModel.viewState.collectAsStateValue() val readingUiState = readingViewModel.readingUiState.collectAsStateValue()
val isScrollDown = viewState.listState.isScrollDown() val isScrollDown = readingUiState.listState.isScrollDown()
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
navController.currentBackStackEntryFlow.collect { navController.currentBackStackEntryFlow.collect {
it.arguments?.getString("articleId")?.let { it.arguments?.getString("articleId")?.let {
readViewModel.dispatch(ReadViewAction.InitData(it)) readingViewModel.initData(it)
} }
} }
} }
LaunchedEffect(viewState.articleWithFeed?.article?.id) { LaunchedEffect(readingUiState.articleWithFeed?.article?.id) {
Log.i("RLog", "ReadPage: ${viewState.articleWithFeed}") Log.i("RLog", "ReadPage: ${readingUiState.articleWithFeed}")
viewState.articleWithFeed?.let { readingUiState.articleWithFeed?.let {
if (it.article.isUnread) { if (it.article.isUnread) {
readViewModel.dispatch(ReadViewAction.MarkUnread(false)) readingViewModel.markUnread(false)
} }
} }
} }
@ -66,19 +66,19 @@ fun ReadPage(
contentAlignment = Alignment.TopCenter contentAlignment = Alignment.TopCenter
) { ) {
TopBar( TopBar(
isShow = viewState.articleWithFeed == null || !isScrollDown, isShow = readingUiState.articleWithFeed == null || !isScrollDown,
title = viewState.articleWithFeed?.article?.title, title = readingUiState.articleWithFeed?.article?.title,
link = viewState.articleWithFeed?.article?.link, link = readingUiState.articleWithFeed?.article?.link,
onClose = { onClose = {
navController.popBackStack() navController.popBackStack()
}, },
) )
} }
Content( Content(
content = viewState.content ?: "", content = readingUiState.content ?: "",
articleWithFeed = viewState.articleWithFeed, articleWithFeed = readingUiState.articleWithFeed,
isLoading = viewState.isLoading, isLoading = readingUiState.isLoading,
listState = viewState.listState, listState = readingUiState.listState,
) )
Box( Box(
modifier = Modifier modifier = Modifier
@ -87,17 +87,17 @@ fun ReadPage(
contentAlignment = Alignment.BottomCenter contentAlignment = Alignment.BottomCenter
) { ) {
BottomBar( BottomBar(
isShow = viewState.articleWithFeed != null && !isScrollDown, isShow = readingUiState.articleWithFeed != null && !isScrollDown,
articleWithFeed = viewState.articleWithFeed, articleWithFeed = readingUiState.articleWithFeed,
unreadOnClick = { unreadOnClick = {
readViewModel.dispatch(ReadViewAction.MarkUnread(it)) readingViewModel.markUnread(it)
}, },
starredOnClick = { starredOnClick = {
readViewModel.dispatch(ReadViewAction.MarkStarred(it)) readingViewModel.markStarred(it)
}, },
fullContentOnClick = { afterIsFullContent -> fullContentOnClick = { afterIsFullContent ->
if (afterIsFullContent) readViewModel.dispatch(ReadViewAction.RenderFullContent) if (afterIsFullContent) readingViewModel.renderFullContent()
else readViewModel.dispatch(ReadViewAction.RenderDescriptionContent) else readingViewModel.renderDescriptionContent()
}, },
) )
} }

View File

@ -1,4 +1,4 @@
package me.ash.reader.ui.page.home.read package me.ash.reader.ui.page.home.reading
import android.util.Log import android.util.Log
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
@ -17,41 +17,29 @@ import me.ash.reader.data.repository.RssRepository
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class ReadViewModel @Inject constructor( class ReadingViewModel @Inject constructor(
val rssRepository: RssRepository, val rssRepository: RssRepository,
private val rssHelper: RssHelper, private val rssHelper: RssHelper,
) : ViewModel() { ) : ViewModel() {
private val _viewState = MutableStateFlow(ReadViewState()) private val _readingUiState = MutableStateFlow(ReadingUiState())
val viewState: StateFlow<ReadViewState> = _viewState.asStateFlow() val readingUiState: StateFlow<ReadingUiState> = _readingUiState.asStateFlow()
fun dispatch(action: ReadViewAction) { fun initData(articleId: String) {
when (action) { showLoading()
is ReadViewAction.InitData -> bindArticleWithFeed(action.articleId)
is ReadViewAction.RenderDescriptionContent -> renderDescriptionContent()
is ReadViewAction.RenderFullContent -> renderFullContent()
is ReadViewAction.MarkUnread -> markUnread(action.isUnread)
is ReadViewAction.MarkStarred -> markStarred(action.isStarred)
is ReadViewAction.ClearArticle -> clearArticle()
is ReadViewAction.ChangeLoading -> changeLoading(action.isLoading)
}
}
private fun bindArticleWithFeed(articleId: String) {
changeLoading(true)
viewModelScope.launch { viewModelScope.launch {
_viewState.update { _readingUiState.update {
it.copy(articleWithFeed = rssRepository.get().findArticleById(articleId)) it.copy(articleWithFeed = rssRepository.get().findArticleById(articleId))
} }
_viewState.value.articleWithFeed?.let { _readingUiState.value.articleWithFeed?.let {
if (it.feed.isFullContent) internalRenderFullContent() if (it.feed.isFullContent) internalRenderFullContent()
else renderDescriptionContent() else renderDescriptionContent()
} }
changeLoading(false) hideLoading()
} }
} }
private fun renderDescriptionContent() { fun renderDescriptionContent() {
_viewState.update { _readingUiState.update {
it.copy( it.copy(
content = it.articleWithFeed?.article?.fullContent content = it.articleWithFeed?.article?.fullContent
?: it.articleWithFeed?.article?.rawDescription ?: "", ?: it.articleWithFeed?.article?.rawDescription ?: "",
@ -59,38 +47,38 @@ class ReadViewModel @Inject constructor(
} }
} }
private fun renderFullContent() { fun renderFullContent() {
viewModelScope.launch { viewModelScope.launch {
internalRenderFullContent() internalRenderFullContent()
} }
} }
private suspend fun internalRenderFullContent() { suspend fun internalRenderFullContent() {
changeLoading(true) showLoading()
try { try {
_viewState.update { _readingUiState.update {
it.copy( it.copy(
content = rssHelper.parseFullContent( content = rssHelper.parseFullContent(
_viewState.value.articleWithFeed?.article?.link ?: "", _readingUiState.value.articleWithFeed?.article?.link ?: "",
_viewState.value.articleWithFeed?.article?.title ?: "" _readingUiState.value.articleWithFeed?.article?.title ?: ""
) )
) )
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.i("RLog", "renderFullContent: ${e.message}") Log.i("RLog", "renderFullContent: ${e.message}")
_viewState.update { _readingUiState.update {
it.copy( it.copy(
content = e.message content = e.message
) )
} }
} }
changeLoading(false) hideLoading()
} }
private fun markUnread(isUnread: Boolean) { fun markUnread(isUnread: Boolean) {
val articleWithFeed = _viewState.value.articleWithFeed ?: return val articleWithFeed = _readingUiState.value.articleWithFeed ?: return
viewModelScope.launch { viewModelScope.launch {
_viewState.update { _readingUiState.update {
it.copy( it.copy(
articleWithFeed = articleWithFeed.copy( articleWithFeed = articleWithFeed.copy(
article = articleWithFeed.article.copy( article = articleWithFeed.article.copy(
@ -102,17 +90,17 @@ class ReadViewModel @Inject constructor(
rssRepository.get().markAsRead( rssRepository.get().markAsRead(
groupId = null, groupId = null,
feedId = null, feedId = null,
articleId = _viewState.value.articleWithFeed!!.article.id, articleId = _readingUiState.value.articleWithFeed!!.article.id,
before = null, before = null,
isUnread = isUnread, isUnread = isUnread,
) )
} }
} }
private fun markStarred(isStarred: Boolean) { fun markStarred(isStarred: Boolean) {
val articleWithFeed = _viewState.value.articleWithFeed ?: return val articleWithFeed = _readingUiState.value.articleWithFeed ?: return
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
_viewState.update { _readingUiState.update {
it.copy( it.copy(
articleWithFeed = articleWithFeed.copy( articleWithFeed = articleWithFeed.copy(
article = articleWithFeed.article.copy( article = articleWithFeed.article.copy(
@ -129,47 +117,22 @@ class ReadViewModel @Inject constructor(
} }
} }
private fun clearArticle() { private fun showLoading() {
_viewState.update { _readingUiState.update {
it.copy(articleWithFeed = null) it.copy(isLoading = true)
} }
} }
private fun changeLoading(isLoading: Boolean) { private fun hideLoading() {
_viewState.update { _readingUiState.update {
it.copy(isLoading = isLoading) it.copy(isLoading = false)
} }
} }
} }
data class ReadViewState( data class ReadingUiState(
val articleWithFeed: ArticleWithFeed? = null, val articleWithFeed: ArticleWithFeed? = null,
val content: String? = null, val content: String? = null,
val isLoading: Boolean = true, val isLoading: Boolean = true,
// val scrollState: ScrollState = ScrollState(0),
val listState: LazyListState = LazyListState(), val listState: LazyListState = LazyListState(),
) )
sealed class ReadViewAction {
data class InitData(
val articleId: String,
) : ReadViewAction()
object RenderDescriptionContent : ReadViewAction()
object RenderFullContent : ReadViewAction()
data class MarkUnread(
val isUnread: Boolean,
) : ReadViewAction()
data class MarkStarred(
val isStarred: Boolean,
) : ReadViewAction()
object ClearArticle : ReadViewAction()
data class ChangeLoading(
val isLoading: Boolean
) : ReadViewAction()
}

View File

@ -28,7 +28,6 @@ import me.ash.reader.ui.component.base.FeedbackIconButton
import me.ash.reader.ui.ext.getCurrentVersion import me.ash.reader.ui.ext.getCurrentVersion
import me.ash.reader.ui.page.common.RouteName import me.ash.reader.ui.page.common.RouteName
import me.ash.reader.ui.page.settings.tips.UpdateDialog import me.ash.reader.ui.page.settings.tips.UpdateDialog
import me.ash.reader.ui.page.settings.tips.UpdateViewAction
import me.ash.reader.ui.page.settings.tips.UpdateViewModel import me.ash.reader.ui.page.settings.tips.UpdateViewModel
import me.ash.reader.ui.theme.palette.onLight import me.ash.reader.ui.theme.palette.onLight
@ -76,7 +75,7 @@ fun SettingsPage(
) )
}, },
) { ) {
updateViewModel.dispatch(UpdateViewAction.Show) updateViewModel.showDialog()
} }
} }
Banner( Banner(

View File

@ -103,23 +103,21 @@ fun TipsAndSupportPage(
onTap = { onTap = {
if (System.currentTimeMillis() - clickTime > 2000) { if (System.currentTimeMillis() - clickTime > 2000) {
clickTime = System.currentTimeMillis() clickTime = System.currentTimeMillis()
updateViewModel.dispatch( updateViewModel.checkUpdate(
UpdateViewAction.CheckUpdate( {
{ context.showToast(context.getString(R.string.checking_updates))
context.showToast(context.getString(R.string.checking_updates)) context.dataStore.put(
context.dataStore.put( DataStoreKeys.SkipVersionNumber,
DataStoreKeys.SkipVersionNumber, ""
"" )
},
{
if (!it) {
context.showToast(
context.getString(R.string.is_latest_version)
) )
},
{
if (!it) {
context.showToast(
context.getString(R.string.is_latest_version)
)
}
} }
) }
) )
} else { } else {
clickTime = System.currentTimeMillis() clickTime = System.currentTimeMillis()

View File

@ -42,8 +42,8 @@ fun UpdateDialog(
updateViewModel: UpdateViewModel = hiltViewModel(), updateViewModel: UpdateViewModel = hiltViewModel(),
) { ) {
val context = LocalContext.current val context = LocalContext.current
val viewState = updateViewModel.viewState.collectAsStateValue() val updateUiState = updateViewModel.updateUiState.collectAsStateValue()
val downloadState = viewState.downloadFlow.collectAsState(initial = Download.NotYet).value val downloadState = updateUiState.downloadFlow.collectAsState(initial = Download.NotYet).value
val scope = rememberCoroutineScope { Dispatchers.IO } val scope = rememberCoroutineScope { Dispatchers.IO }
val newVersionNumber = LocalNewVersionNumber.current val newVersionNumber = LocalNewVersionNumber.current
val newVersionPublishDate = LocalNewVersionPublishDate.current val newVersionPublishDate = LocalNewVersionPublishDate.current
@ -73,8 +73,8 @@ fun UpdateDialog(
Dialog( Dialog(
modifier = Modifier.heightIn(max = 400.dp), modifier = Modifier.heightIn(max = 400.dp),
visible = viewState.updateDialogVisible, visible = updateUiState.updateDialogVisible,
onDismissRequest = { updateViewModel.dispatch(UpdateViewAction.Hide) }, onDismissRequest = { updateViewModel.hideDialog() },
icon = { icon = {
Icon( Icon(
imageVector = Icons.Rounded.Update, imageVector = Icons.Rounded.Update,
@ -147,7 +147,7 @@ fun UpdateDialog(
TextButton( TextButton(
onClick = { onClick = {
SkipVersionNumberPreference.put(context, scope, newVersionNumber.toString()) SkipVersionNumberPreference.put(context, scope, newVersionNumber.toString())
updateViewModel.dispatch(UpdateViewAction.Hide) updateViewModel.hideDialog()
} }
) { ) {
Text(text = stringResource(R.string.skip_this_version)) Text(text = stringResource(R.string.skip_this_version))

View File

@ -14,22 +14,10 @@ import javax.inject.Inject
class UpdateViewModel @Inject constructor( class UpdateViewModel @Inject constructor(
private val appRepository: AppRepository, private val appRepository: AppRepository,
) : ViewModel() { ) : ViewModel() {
private val _viewState = MutableStateFlow(UpdateViewState()) private val _updateUiState = MutableStateFlow(UpdateUiState())
val viewState: StateFlow<UpdateViewState> = _viewState.asStateFlow() val updateUiState: StateFlow<UpdateUiState> = _updateUiState.asStateFlow()
fun dispatch(action: UpdateViewAction) { fun checkUpdate(
when (action) {
is UpdateViewAction.Show -> changeUpdateDialogVisible(true)
is UpdateViewAction.Hide -> changeUpdateDialogVisible(false)
is UpdateViewAction.CheckUpdate -> checkUpdate(
action.preProcessor,
action.postProcessor
)
is UpdateViewAction.DownloadUpdate -> downloadUpdate(action.url)
}
}
private fun checkUpdate(
preProcessor: suspend () -> Unit = {}, preProcessor: suspend () -> Unit = {},
postProcessor: suspend (Boolean) -> Unit = {} postProcessor: suspend (Boolean) -> Unit = {}
) { ) {
@ -38,7 +26,11 @@ class UpdateViewModel @Inject constructor(
preProcessor() preProcessor()
appRepository.checkUpdate().let { appRepository.checkUpdate().let {
it?.let { it?.let {
changeUpdateDialogVisible(it) if (it) {
showDialog()
} else {
hideDialog()
}
postProcessor(it) postProcessor(it)
} }
} }
@ -46,22 +38,30 @@ class UpdateViewModel @Inject constructor(
} }
} }
private fun changeUpdateDialogVisible(visible: Boolean) { fun showDialog() {
_viewState.update { _updateUiState.update {
it.copy( it.copy(
updateDialogVisible = visible updateDialogVisible = true
) )
} }
} }
private fun downloadUpdate(url: String) { fun hideDialog() {
_updateUiState.update {
it.copy(
updateDialogVisible = false
)
}
}
fun downloadUpdate(url: String) {
viewModelScope.launch { viewModelScope.launch {
_viewState.update { _updateUiState.update {
it.copy( it.copy(
downloadFlow = flow { emit(Download.Progress(0)) } downloadFlow = flow { emit(Download.Progress(0)) }
) )
} }
_viewState.update { _updateUiState.update {
it.copy( it.copy(
downloadFlow = appRepository.downloadFile(url) downloadFlow = appRepository.downloadFile(url)
) )
@ -70,21 +70,7 @@ class UpdateViewModel @Inject constructor(
} }
} }
data class UpdateViewState( data class UpdateUiState(
val updateDialogVisible: Boolean = false, val updateDialogVisible: Boolean = false,
val downloadFlow: Flow<Download> = emptyFlow(), val downloadFlow: Flow<Download> = emptyFlow(),
) )
sealed class UpdateViewAction {
object Show : UpdateViewAction()
object Hide : UpdateViewAction()
data class CheckUpdate(
val preProcessor: suspend () -> Unit = {},
val postProcessor: suspend (Boolean) -> Unit = {}
) : UpdateViewAction()
data class DownloadUpdate(
val url: String,
) : UpdateViewAction()
}