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.preference.LocalDarkTheme
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.feeds.FeedsPage
import me.ash.reader.ui.page.home.flow.FlowPage
import me.ash.reader.ui.page.home.read.ReadPage
import me.ash.reader.ui.page.home.reading.ReadingPage
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.DarkThemePage
@ -37,7 +36,7 @@ fun HomeEntry(
homeViewModel: HomeViewModel = hiltViewModel(),
) {
val context = LocalContext.current
val filterState = homeViewModel.filterState.collectAsStateValue()
val filterUiState = homeViewModel.filterUiState.collectAsStateValue()
val navController = rememberAnimatedNavController()
val intent by rememberSaveable { mutableStateOf(context.findActivity()?.intent) }
@ -57,9 +56,8 @@ fun HomeEntry(
// Other initial pages
}
homeViewModel.dispatch(
HomeViewAction.ChangeFilter(
filterState.copy(
homeViewModel.changeFilter(
filterUiState.copy(
filter = when (context.initialFilter) {
0 -> Filter.Starred
1 -> Filter.Unread
@ -68,7 +66,6 @@ fun HomeEntry(
}
)
)
)
}
LaunchedEffect(openArticleId) {
@ -114,7 +111,7 @@ fun HomeEntry(
)
}
animatedComposable(route = "${RouteName.READING}/{articleId}") {
ReadPage(navController = navController)
ReadingPage(navController = navController)
}
// Settings

View File

@ -7,8 +7,8 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.*
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.model.Filter
import me.ash.reader.data.module.ApplicationScope
import me.ash.reader.data.repository.RssRepository
import me.ash.reader.data.repository.StringsRepository
@ -24,30 +24,20 @@ class HomeViewModel @Inject constructor(
private val applicationScope: CoroutineScope,
private val workManager: WorkManager,
) : ViewModel() {
private val _homeUiState = MutableStateFlow(HomeUiState())
val homeUiState: StateFlow<HomeUiState> = _homeUiState.asStateFlow()
private val _viewState = MutableStateFlow(HomeViewState())
val viewState: StateFlow<HomeViewState> = _viewState.asStateFlow()
private val _filterState = MutableStateFlow(FilterState())
val filterState = _filterState.asStateFlow()
private val _filterUiState = MutableStateFlow(FilterState())
val filterUiState = _filterUiState.asStateFlow()
val syncWorkLiveData = workManager.getWorkInfoByIdLiveData(SyncWorker.UUID)
fun dispatch(action: HomeViewAction) {
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() {
fun sync() {
rssRepository.get().doSync()
}
private fun changeFilter(filterState: FilterState) {
_filterState.update {
fun changeFilter(filterState: FilterState) {
_filterUiState.update {
it.copy(
group = filterState.group,
feed = filterState.feed,
@ -57,24 +47,24 @@ class HomeViewModel @Inject constructor(
fetchArticles()
}
private fun fetchArticles() {
_viewState.update {
fun fetchArticles() {
_homeUiState.update {
it.copy(
pagingData = Pager(PagingConfig(pageSize = 50)) {
if (_viewState.value.searchContent.isNotBlank()) {
if (_homeUiState.value.searchContent.isNotBlank()) {
rssRepository.get().searchArticles(
content = _viewState.value.searchContent.trim(),
groupId = _filterState.value.group?.id,
feedId = _filterState.value.feed?.id,
isStarred = _filterState.value.filter.isStarred(),
isUnread = _filterState.value.filter.isUnread(),
content = _homeUiState.value.searchContent.trim(),
groupId = _filterUiState.value.group?.id,
feedId = _filterUiState.value.feed?.id,
isStarred = _filterUiState.value.filter.isStarred(),
isUnread = _filterUiState.value.filter.isUnread(),
)
} else {
rssRepository.get().pullArticles(
groupId = _filterState.value.group?.id,
feedId = _filterState.value.feed?.id,
isStarred = _filterState.value.filter.isStarred(),
isUnread = _filterState.value.filter.isUnread(),
groupId = _filterUiState.value.group?.id,
feedId = _filterUiState.value.feed?.id,
isStarred = _filterUiState.value.filter.isStarred(),
isUnread = _filterUiState.value.filter.isUnread(),
)
}
}.flow.map {
@ -94,8 +84,8 @@ class HomeViewModel @Inject constructor(
}
}
private fun inputSearchContent(content: String) {
_viewState.update {
fun inputSearchContent(content: String) {
_homeUiState.update {
it.copy(
searchContent = content,
)
@ -110,21 +100,7 @@ data class FilterState(
val filter: Filter = Filter.All,
)
data class HomeViewState(
data class HomeUiState(
val pagingData: Flow<PagingData<FlowItemView>> = emptyFlow(),
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 me.ash.reader.data.entity.Feed
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 kotlin.math.ln
@ -55,7 +54,7 @@ fun FeedItem(
},
onLongClick = {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
feedOptionViewModel.dispatch(FeedOptionViewAction.Show(scope, feed.id))
feedOptionViewModel.showDrawer(scope, feed.id)
}
)
.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.page.common.RouteName
import me.ash.reader.ui.page.home.FilterState
import me.ash.reader.ui.page.home.HomeViewAction
import me.ash.reader.ui.page.home.HomeViewModel
import me.ash.reader.ui.page.home.feeds.drawer.feed.FeedOptionDrawer
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.SubscribeViewAction
import me.ash.reader.ui.page.home.feeds.subscribe.SubscribeViewModel
@OptIn(
@ -67,8 +65,8 @@ fun FeedsPage(
val filterBarPadding = LocalFeedsFilterBarPadding.current
val filterBarTonalElevation = LocalFeedsFilterBarTonalElevation.current
val feedsViewState = feedsViewModel.viewState.collectAsStateValue()
val filterState = homeViewModel.filterState.collectAsStateValue()
val feedsUiState = feedsViewModel.feedsUiState.collectAsStateValue()
val filterUiState = homeViewModel.filterUiState.collectAsStateValue()
val newVersion = LocalNewVersionNumber.current
val skipVersion = LocalSkipVersionNumber.current
@ -92,22 +90,22 @@ fun FeedsPage(
val launcher = rememberLauncherForActivityResult(
ActivityResultContracts.CreateDocument()
) { result ->
feedsViewModel.dispatch(FeedsViewAction.ExportAsString { string ->
feedsViewModel.exportAsOpml { string ->
result?.let { uri ->
context.contentResolver.openOutputStream(uri)?.let { outputStream ->
outputStream.write(string.toByteArray())
}
}
})
}
}
LaunchedEffect(Unit) {
feedsViewModel.dispatch(FeedsViewAction.FetchAccount)
feedsViewModel.fetchAccount()
}
LaunchedEffect(filterState) {
snapshotFlow { filterState }.collect {
feedsViewModel.dispatch(FeedsViewAction.FetchData(it))
LaunchedEffect(filterUiState) {
snapshotFlow { filterUiState }.collect {
feedsViewModel.fetchData(it)
}
}
@ -138,14 +136,14 @@ fun FeedsPage(
contentDescription = stringResource(R.string.refresh),
tint = MaterialTheme.colorScheme.onSurface,
) {
if (!isSyncing) homeViewModel.dispatch(HomeViewAction.Sync)
if (!isSyncing) homeViewModel.sync()
}
FeedbackIconButton(
imageVector = Icons.Rounded.Add,
contentDescription = stringResource(R.string.subscribe),
tint = MaterialTheme.colorScheme.onSurface,
) {
subscribeViewModel.dispatch(SubscribeViewAction.Show)
subscribeViewModel.showDrawer()
}
},
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 "",
)
}
item {
Banner(
title = filterState.filter.getName(),
desc = feedsViewState.importantCount.ifEmpty { stringResource(R.string.loading) },
icon = filterState.filter.iconOutline,
title = filterUiState.filter.getName(),
desc = feedsUiState.importantCount.ifEmpty { stringResource(R.string.loading) },
icon = filterUiState.filter.iconOutline,
action = {
Icon(
imageVector = Icons.Outlined.KeyboardArrowRight,
@ -178,7 +176,7 @@ fun FeedsPage(
filterChange(
navController = navController,
homeViewModel = homeViewModel,
filterState = filterState.copy(
filterState = filterUiState.copy(
group = null,
feed = null,
)
@ -193,7 +191,7 @@ fun FeedsPage(
)
Spacer(modifier = Modifier.height(8.dp))
}
itemsIndexed(feedsViewState.groupWithFeedList) { index, groupWithFeed ->
itemsIndexed(feedsUiState.groupWithFeedList) { index, groupWithFeed ->
// Crossfade(targetState = groupWithFeed) { groupWithFeed ->
Column {
GroupItem(
@ -205,7 +203,7 @@ fun FeedsPage(
filterChange(
navController = navController,
homeViewModel = homeViewModel,
filterState = filterState.copy(
filterState = filterUiState.copy(
group = groupWithFeed.group,
feed = null,
)
@ -215,14 +213,14 @@ fun FeedsPage(
filterChange(
navController = navController,
homeViewModel = homeViewModel,
filterState = filterState.copy(
filterState = filterUiState.copy(
group = null,
feed = feed,
)
)
}
)
if (index != feedsViewState.groupWithFeedList.lastIndex) {
if (index != feedsUiState.groupWithFeedList.lastIndex) {
Spacer(modifier = Modifier.height(8.dp))
}
}
@ -236,7 +234,7 @@ fun FeedsPage(
},
bottomBar = {
FilterBar(
filter = filterState.filter,
filter = filterUiState.filter,
filterBarStyle = filterBarStyle.value,
filterBarFilled = filterBarFilled.value,
filterBarPadding = filterBarPadding.dp,
@ -245,7 +243,7 @@ fun FeedsPage(
filterChange(
navController = navController,
homeViewModel = homeViewModel,
filterState = filterState.copy(filter = it),
filterState = filterUiState.copy(filter = it),
isNavigate = false,
)
}
@ -263,7 +261,7 @@ private fun filterChange(
filterState: FilterState,
isNavigate: Boolean = true,
) {
homeViewModel.dispatch(HomeViewAction.ChangeFilter(filterState))
homeViewModel.changeFilter(filterState)
if (isNavigate) {
navController.navigate(RouteName.FLOW) {
launchSingleTop = true

View File

@ -31,21 +31,12 @@ class FeedsViewModel @Inject constructor(
@DispatcherIO
private val dispatcherIO: CoroutineDispatcher,
) : ViewModel() {
private val _viewState = MutableStateFlow(FeedsViewState())
val viewState: StateFlow<FeedsViewState> = _viewState.asStateFlow()
private val _feedsUiState = MutableStateFlow(FeedsUiState())
val feedsUiState: StateFlow<FeedsUiState> = _feedsUiState.asStateFlow()
fun dispatch(action: FeedsViewAction) {
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() {
fun fetchAccount() {
viewModelScope.launch(dispatcherIO) {
_viewState.update {
_feedsUiState.update {
it.copy(
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) {
try {
callback(opmlRepository.saveToString())
@ -63,7 +54,7 @@ class FeedsViewModel @Inject constructor(
}
}
private fun fetchData(filterState: FilterState) {
fun fetchData(filterState: FilterState) {
viewModelScope.launch(dispatcherIO) {
pullFeeds(
isStarred = filterState.filter.isStarred(),
@ -109,13 +100,25 @@ class FeedsViewModel @Inject constructor(
}
groupWithFeedList
}.onEach { groupWithFeedList ->
_viewState.update {
_feedsUiState.update {
it.copy(
importantCount = groupWithFeedList.sumOf { it.group.important ?: 0 }.run {
when {
isStarred -> stringsRepository.getQuantityString(R.plurals.starred_desc, this, this)
isUnread -> stringsRepository.getQuantityString(R.plurals.unread_desc, this, this)
else -> stringsRepository.getQuantityString(R.plurals.all_desc, this, this)
isStarred -> stringsRepository.getQuantityString(
R.plurals.starred_desc,
this,
this
)
isUnread -> stringsRepository.getQuantityString(
R.plurals.unread_desc,
this,
this
)
else -> stringsRepository.getQuantityString(
R.plurals.all_desc,
this,
this
)
}
},
groupWithFeedList = groupWithFeedList,
@ -126,15 +129,9 @@ class FeedsViewModel @Inject constructor(
Log.e("RLog", "catch in articleRepository.pullFeeds(): ${it.message}")
}.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 importantCount: String = "",
val groupWithFeedList: List<GroupWithFeed> = emptyList(),
@ -142,19 +139,3 @@ data class FeedsViewState(
val listState: LazyListState = LazyListState(),
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.Group
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
@OptIn(androidx.compose.foundation.ExperimentalFoundationApi::class)
@ -61,7 +60,7 @@ fun GroupItem(
},
onLongClick = {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
groupOptionViewModel.dispatch(GroupOptionViewAction.Show(scope, group.id))
groupOptionViewModel.showDrawer(scope, group.id)
}
)
.padding(top = 22.dp)

View File

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

View File

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

View File

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

View File

@ -21,9 +21,7 @@ import me.ash.reader.data.module.DispatcherMain
import me.ash.reader.data.repository.RssRepository
import javax.inject.Inject
@OptIn(
ExperimentalMaterialApi::class
)
@OptIn(ExperimentalMaterialApi::class)
@HiltViewModel
class FeedOptionViewModel @Inject constructor(
private val rssRepository: RssRepository,
@ -32,13 +30,13 @@ class FeedOptionViewModel @Inject constructor(
@DispatcherIO
private val dispatcherIO: CoroutineDispatcher,
) : ViewModel() {
private val _viewState = MutableStateFlow(FeedOptionViewState())
val viewState: StateFlow<FeedOptionViewState> = _viewState.asStateFlow()
private val _feedOptionUiState = MutableStateFlow(FeedOptionUiState())
val feedOptionUiState: StateFlow<FeedOptionUiState> = _feedOptionUiState.asStateFlow()
init {
viewModelScope.launch(dispatcherIO) {
rssRepository.get().pullGroups().collect { groups ->
_viewState.update {
_feedOptionUiState.update {
it.copy(
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) {
val feed = rssRepository.get().findFeedById(feedId)
_viewState.update {
_feedOptionUiState.update {
it.copy(
feed = feed,
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 {
fetchFeed(feedId)
_viewState.value.drawerState.show()
_feedOptionUiState.value.drawerState.show()
}
}
private fun hide(scope: CoroutineScope) {
fun hideDrawer(scope: CoroutineScope) {
scope.launch {
_viewState.value.drawerState.hide()
_feedOptionUiState.value.drawerState.hide()
}
}
private fun changeNewGroupDialogVisible(visible: Boolean) {
_viewState.update {
fun showNewGroupDialog() {
_feedOptionUiState.update {
it.copy(
newGroupDialogVisible = visible,
newGroupDialogVisible = true,
newGroupContent = "",
)
}
}
private fun inputNewGroup(content: String) {
_viewState.update {
fun hideNewGroupDialog() {
_feedOptionUiState.update {
it.copy(
newGroupDialogVisible = false,
newGroupContent = "",
)
}
}
fun inputNewGroup(content: String) {
_feedOptionUiState.update {
it.copy(
newGroupContent = content
)
}
}
private fun addNewGroup() {
if (_viewState.value.newGroupContent.isNotBlank()) {
fun addNewGroup() {
if (_feedOptionUiState.value.newGroupContent.isNotBlank()) {
viewModelScope.launch {
selectedGroup(rssRepository.get().addGroup(_viewState.value.newGroupContent))
changeNewGroupDialogVisible(false)
selectedGroup(rssRepository.get().addGroup(_feedOptionUiState.value.newGroupContent))
hideNewGroupDialog()
}
}
}
private fun selectedGroup(groupId: String) {
fun selectedGroup(groupId: String) {
viewModelScope.launch(dispatcherIO) {
_viewState.value.feed?.let {
_feedOptionUiState.value.feed?.let {
rssRepository.get().updateFeed(
it.copy(
groupId = groupId
@ -137,9 +116,9 @@ class FeedOptionViewModel @Inject constructor(
}
}
private fun changeParseFullContentPreset() {
fun changeParseFullContentPreset() {
viewModelScope.launch(dispatcherIO) {
_viewState.value.feed?.let {
_feedOptionUiState.value.feed?.let {
rssRepository.get().updateFeed(
it.copy(
isFullContent = !it.isFullContent
@ -150,9 +129,9 @@ class FeedOptionViewModel @Inject constructor(
}
}
private fun changeAllowNotificationPreset() {
fun changeAllowNotificationPreset() {
viewModelScope.launch(dispatcherIO) {
_viewState.value.feed?.let {
_feedOptionUiState.value.feed?.let {
rssRepository.get().updateFeed(
it.copy(
isNotification = !it.isNotification
@ -163,8 +142,8 @@ class FeedOptionViewModel @Inject constructor(
}
}
private fun delete(callback: () -> Unit = {}) {
_viewState.value.feed?.let {
fun delete(callback: () -> Unit = {}) {
_feedOptionUiState.value.feed?.let {
viewModelScope.launch(dispatcherIO) {
rssRepository.get().deleteFeed(it)
withContext(dispatcherMain) {
@ -174,40 +153,40 @@ class FeedOptionViewModel @Inject constructor(
}
}
private fun hideDeleteDialog() {
_viewState.update {
fun hideDeleteDialog() {
_feedOptionUiState.update {
it.copy(
deleteDialogVisible = false,
)
}
}
private fun showDeleteDialog() {
_viewState.update {
fun showDeleteDialog() {
_feedOptionUiState.update {
it.copy(
deleteDialogVisible = true,
)
}
}
private fun showClearDialog() {
_viewState.update {
fun showClearDialog() {
_feedOptionUiState.update {
it.copy(
clearDialogVisible = true,
)
}
}
private fun hideClearDialog() {
_viewState.update {
fun hideClearDialog() {
_feedOptionUiState.update {
it.copy(
clearDialogVisible = false,
)
}
}
private fun clear(callback: () -> Unit = {}) {
_viewState.value.feed?.let {
fun clearFeed(callback: () -> Unit = {}) {
_feedOptionUiState.value.feed?.let {
viewModelScope.launch(dispatcherIO) {
rssRepository.get().deleteArticles(feed = it)
withContext(dispatcherMain) {
@ -217,15 +196,15 @@ class FeedOptionViewModel @Inject constructor(
}
}
private fun rename() {
_viewState.value.feed?.let {
fun renameFeed() {
_feedOptionUiState.value.feed?.let {
viewModelScope.launch {
rssRepository.get().updateFeed(
it.copy(
name = _viewState.value.newName
name = _feedOptionUiState.value.newName
)
)
_viewState.update {
_feedOptionUiState.update {
it.copy(
renameDialogVisible = false,
)
@ -234,49 +213,67 @@ class FeedOptionViewModel @Inject constructor(
}
}
private fun changeRenameDialogVisible(visible: Boolean) {
_viewState.update {
fun showRenameDialog() {
_feedOptionUiState.update {
it.copy(
renameDialogVisible = visible,
newName = if (visible) _viewState.value.feed?.name ?: "" else "",
renameDialogVisible = true,
newName = _feedOptionUiState.value.feed?.name ?: "",
)
}
}
private fun inputNewName(content: String) {
_viewState.update {
fun hideRenameDialog() {
_feedOptionUiState.update {
it.copy(
renameDialogVisible = false,
newName = "",
)
}
}
fun inputNewName(content: String) {
_feedOptionUiState.update {
it.copy(
newName = content
)
}
}
private fun changeFeedUrlDialogVisible(visible: Boolean) {
_viewState.update {
fun showFeedUrlDialog() {
_feedOptionUiState.update {
it.copy(
changeUrlDialogVisible = visible,
newUrl = if (visible) _viewState.value.feed?.url ?: "" else "",
changeUrlDialogVisible = true,
newUrl = _feedOptionUiState.value.feed?.url ?: "",
)
}
}
private fun inputNewUrl(content: String) {
_viewState.update {
fun hideFeedUrlDialog() {
_feedOptionUiState.update {
it.copy(
changeUrlDialogVisible = false,
newUrl = "",
)
}
}
fun inputNewUrl(content: String) {
_feedOptionUiState.update {
it.copy(
newUrl = content
)
}
}
private fun changeFeedUrl() {
_viewState.value.feed?.let {
fun changeFeedUrl() {
_feedOptionUiState.value.feed?.let {
viewModelScope.launch {
rssRepository.get().updateFeed(
it.copy(
url = _viewState.value.newUrl
url = _feedOptionUiState.value.newUrl
)
)
_viewState.update {
_feedOptionUiState.update {
it.copy(
changeUrlDialogVisible = false,
)
@ -287,7 +284,7 @@ class FeedOptionViewModel @Inject constructor(
}
@OptIn(ExperimentalMaterialApi::class)
data class FeedOptionViewState(
data class FeedOptionUiState(
var drawerState: ModalBottomSheetState = ModalBottomSheetState(ModalBottomSheetValue.Hidden),
val feed: Feed? = null,
val selectedGroupId: String = "",
@ -301,57 +298,3 @@ data class FeedOptionViewState(
val newUrl: String = "",
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.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
@ -18,20 +17,19 @@ import me.ash.reader.ui.ext.showToast
@Composable
fun AllAllowNotificationDialog(
modifier: Modifier = Modifier,
groupName: String,
viewModel: GroupOptionViewModel = hiltViewModel(),
groupOptionViewModel: GroupOptionViewModel = hiltViewModel(),
) {
val context = LocalContext.current
val viewState = viewModel.viewState.collectAsStateValue()
val groupOptionUiState = groupOptionViewModel.groupOptionUiState.collectAsStateValue()
val scope = rememberCoroutineScope()
val allowToastString = stringResource(R.string.all_allow_notification_toast, groupName)
val denyToastString = stringResource(R.string.all_deny_notification_toast, groupName)
Dialog(
visible = viewState.allAllowNotificationDialogVisible,
visible = groupOptionUiState.allAllowNotificationDialogVisible,
onDismissRequest = {
viewModel.dispatch(GroupOptionViewAction.HideAllAllowNotificationDialog)
groupOptionViewModel.hideAllAllowNotificationDialog()
},
icon = {
Icon(
@ -48,11 +46,11 @@ fun AllAllowNotificationDialog(
confirmButton = {
TextButton(
onClick = {
viewModel.dispatch(GroupOptionViewAction.AllAllowNotification(true) {
viewModel.dispatch(GroupOptionViewAction.HideAllAllowNotificationDialog)
viewModel.dispatch(GroupOptionViewAction.Hide(scope))
groupOptionViewModel.allAllowNotification(true) {
groupOptionViewModel.hideAllAllowNotificationDialog()
groupOptionViewModel.hideDrawer(scope)
context.showToast(allowToastString)
})
}
}
) {
Text(
@ -63,11 +61,11 @@ fun AllAllowNotificationDialog(
dismissButton = {
TextButton(
onClick = {
viewModel.dispatch(GroupOptionViewAction.AllAllowNotification(false) {
viewModel.dispatch(GroupOptionViewAction.HideAllAllowNotificationDialog)
viewModel.dispatch(GroupOptionViewAction.Hide(scope))
groupOptionViewModel.allAllowNotification(false) {
groupOptionViewModel.hideAllAllowNotificationDialog()
groupOptionViewModel.hideDrawer(scope)
context.showToast(denyToastString)
})
}
}
) {
Text(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -29,13 +29,13 @@ class GroupOptionViewModel @Inject constructor(
@DispatcherIO
private val dispatcherIO: CoroutineDispatcher,
) : ViewModel() {
private val _viewState = MutableStateFlow(GroupOptionViewState())
val viewState: StateFlow<GroupOptionViewState> = _viewState.asStateFlow()
private val _groupOptionUiState = MutableStateFlow(GroupOptionUiState())
val groupOptionUiState: StateFlow<GroupOptionUiState> = _groupOptionUiState.asStateFlow()
init {
viewModelScope.launch(dispatcherIO) {
rssRepository.get().pullGroups().collect { groups ->
_viewState.update {
_groupOptionUiState.update {
it.copy(
groups = groups
)
@ -44,70 +44,25 @@ class GroupOptionViewModel @Inject constructor(
}
}
fun dispatch(action: GroupOptionViewAction) {
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 {
fun showDrawer(scope: CoroutineScope, groupId: String) {
scope.launch {
_groupOptionUiState.update {
it.copy(
group = group,
group = rssRepository.get().findGroupById(groupId),
)
}
_groupOptionUiState.value.drawerState.show()
}
}
private fun show(scope: CoroutineScope, groupId: String) {
fun hideDrawer(scope: CoroutineScope) {
scope.launch {
fetchGroup(groupId)
_viewState.value.drawerState.show()
_groupOptionUiState.value.drawerState.hide()
}
}
private fun hide(scope: CoroutineScope) {
scope.launch {
_viewState.value.drawerState.hide()
}
}
private fun allAllowNotification(isNotification: Boolean, callback: () -> Unit = {}) {
_viewState.value.group?.let {
fun allAllowNotification(isNotification: Boolean, callback: () -> Unit = {}) {
_groupOptionUiState.value.group?.let {
viewModelScope.launch(dispatcherIO) {
rssRepository.get().groupAllowNotification(it, isNotification)
withContext(dispatcherMain) {
@ -117,16 +72,24 @@ class GroupOptionViewModel @Inject constructor(
}
}
private fun changeAllAllowNotificationDialogVisible(visible: Boolean) {
_viewState.update {
fun showAllAllowNotificationDialog() {
_groupOptionUiState.update {
it.copy(
allAllowNotificationDialogVisible = visible,
allAllowNotificationDialogVisible = true,
)
}
}
private fun allParseFullContent(isFullContent: Boolean, callback: () -> Unit = {}) {
_viewState.value.group?.let {
fun hideAllAllowNotificationDialog() {
_groupOptionUiState.update {
it.copy(
allAllowNotificationDialogVisible = false,
)
}
}
fun allParseFullContent(isFullContent: Boolean, callback: () -> Unit = {}) {
_groupOptionUiState.value.group?.let {
viewModelScope.launch(dispatcherIO) {
rssRepository.get().groupParseFullContent(it, isFullContent)
withContext(dispatcherMain) {
@ -136,16 +99,24 @@ class GroupOptionViewModel @Inject constructor(
}
}
private fun changeAllParseFullContentDialogVisible(visible: Boolean) {
_viewState.update {
fun showAllParseFullContentDialog() {
_groupOptionUiState.update {
it.copy(
allParseFullContentDialogVisible = visible,
allParseFullContentDialogVisible = true,
)
}
}
private fun delete(callback: () -> Unit = {}) {
_viewState.value.group?.let {
fun hideAllParseFullContentDialog() {
_groupOptionUiState.update {
it.copy(
allParseFullContentDialogVisible = false,
)
}
}
fun delete(callback: () -> Unit = {}) {
_groupOptionUiState.value.group?.let {
viewModelScope.launch(dispatcherIO) {
rssRepository.get().deleteGroup(it)
withContext(dispatcherMain) {
@ -155,32 +126,40 @@ class GroupOptionViewModel @Inject constructor(
}
}
private fun changeDeleteDialogVisible(visible: Boolean) {
_viewState.update {
fun showDeleteDialog() {
_groupOptionUiState.update {
it.copy(
deleteDialogVisible = visible,
deleteDialogVisible = true,
)
}
}
private fun showClearDialog() {
_viewState.update {
fun hideDeleteDialog() {
_groupOptionUiState.update {
it.copy(
deleteDialogVisible = false,
)
}
}
fun showClearDialog() {
_groupOptionUiState.update {
it.copy(
clearDialogVisible = true,
)
}
}
private fun hideClearDialog() {
_viewState.update {
fun hideClearDialog() {
_groupOptionUiState.update {
it.copy(
clearDialogVisible = false,
)
}
}
private fun clear(callback: () -> Unit = {}) {
_viewState.value.group?.let {
fun clear(callback: () -> Unit = {}) {
_groupOptionUiState.value.group?.let {
viewModelScope.launch(dispatcherIO) {
rssRepository.get().deleteArticles(group = it)
withContext(dispatcherMain) {
@ -190,9 +169,9 @@ class GroupOptionViewModel @Inject constructor(
}
}
private fun allMoveToGroup(callback: () -> Unit) {
_viewState.value.group?.let { group ->
_viewState.value.targetGroup?.let { targetGroup ->
fun allMoveToGroup(callback: () -> Unit) {
_groupOptionUiState.value.group?.let { group ->
_groupOptionUiState.value.targetGroup?.let { targetGroup ->
viewModelScope.launch(dispatcherIO) {
rssRepository.get().groupMoveToTargetGroup(group, targetGroup)
withContext(dispatcherMain) {
@ -203,24 +182,33 @@ class GroupOptionViewModel @Inject constructor(
}
}
private fun changeAllMoveToGroupDialogVisible(targetGroup: Group? = null, visible: Boolean) {
_viewState.update {
fun showAllMoveToGroupDialog(targetGroup: Group) {
_groupOptionUiState.update {
it.copy(
targetGroup = if (visible) targetGroup else null,
allMoveToGroupDialogVisible = visible,
targetGroup = targetGroup,
allMoveToGroupDialogVisible = true,
)
}
}
private fun rename() {
_viewState.value.group?.let {
fun hideAllMoveToGroupDialog() {
_groupOptionUiState.update {
it.copy(
targetGroup = null,
allMoveToGroupDialogVisible = false,
)
}
}
fun rename() {
_groupOptionUiState.value.group?.let {
viewModelScope.launch {
rssRepository.get().updateGroup(
it.copy(
name = _viewState.value.newName
name = _groupOptionUiState.value.newName
)
)
_viewState.update {
_groupOptionUiState.update {
it.copy(
renameDialogVisible = false,
)
@ -229,17 +217,26 @@ class GroupOptionViewModel @Inject constructor(
}
}
private fun changeRenameDialogVisible(visible: Boolean) {
_viewState.update {
fun showRenameDialog() {
_groupOptionUiState.update {
it.copy(
renameDialogVisible = visible,
newName = if (visible) _viewState.value.group?.name ?: "" else "",
renameDialogVisible = true,
newName = _groupOptionUiState.value.group?.name ?: "",
)
}
}
private fun inputNewName(content: String) {
_viewState.update {
fun hideRenameDialog() {
_groupOptionUiState.update {
it.copy(
renameDialogVisible = false,
newName = "",
)
}
}
fun inputNewName(content: String) {
_groupOptionUiState.update {
it.copy(
newName = content
)
@ -248,7 +245,7 @@ class GroupOptionViewModel @Inject constructor(
}
@OptIn(ExperimentalMaterialApi::class)
data class GroupOptionViewState(
data class GroupOptionUiState(
var drawerState: ModalBottomSheetState = ModalBottomSheetState(ModalBottomSheetValue.Hidden),
val group: Group? = null,
val targetGroup: Group? = null,
@ -261,61 +258,3 @@ data class GroupOptionViewState(
val newName: String = "",
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 focusManager = LocalFocusManager.current
val viewState = subscribeViewModel.viewState.collectAsStateValue()
val groupsState = viewState.groups.collectAsState(initial = emptyList())
val subscribeUiState = subscribeViewModel.subscribeUiState.collectAsStateValue()
val groupsState = subscribeUiState.groups.collectAsState(initial = emptyList())
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) {
it?.let { uri ->
context.contentResolver.openInputStream(uri)?.let { inputStream ->
subscribeViewModel.dispatch(SubscribeViewAction.ImportFromInputStream(inputStream))
subscribeViewModel.importFromInputStream(inputStream)
}
}
}
LaunchedEffect(viewState.visible) {
if (viewState.visible) {
subscribeViewModel.dispatch(SubscribeViewAction.Init)
LaunchedEffect(subscribeUiState.visible) {
if (subscribeUiState.visible) {
subscribeViewModel.init()
} else {
subscribeViewModel.dispatch(SubscribeViewAction.Reset)
subscribeViewModel.dispatch(SubscribeViewAction.SwitchPage(true))
subscribeViewModel.reset()
subscribeViewModel.switchPage(true)
}
}
Dialog(
modifier = Modifier.padding(horizontal = 44.dp),
visible = viewState.visible,
visible = subscribeUiState.visible,
properties = DialogProperties(usePlatformDefaultWidth = false),
onDismissRequest = {
focusManager.clearFocus()
subscribeViewModel.dispatch(SubscribeViewAction.Hide)
subscribeViewModel.hideDrawer()
},
icon = {
Icon(
@ -76,10 +76,10 @@ fun SubscribeDialog(
},
title = {
Text(
text = if (viewState.isSearchPage) {
viewState.title
text = if (subscribeUiState.isSearchPage) {
subscribeUiState.title
} else {
viewState.feed?.name ?: stringResource(R.string.unknown)
subscribeUiState.feed?.name ?: stringResource(R.string.unknown)
},
maxLines = 1,
overflow = TextOverflow.Ellipsis,
@ -87,7 +87,7 @@ fun SubscribeDialog(
},
text = {
AnimatedContent(
targetState = viewState.isSearchPage,
targetState = subscribeUiState.isSearchPage,
transitionSpec = {
slideInHorizontally { width -> width } + fadeIn() with
slideOutHorizontally { width -> -width } + fadeOut()
@ -95,55 +95,55 @@ fun SubscribeDialog(
) { targetExpanded ->
if (targetExpanded) {
ClipboardTextField(
readOnly = viewState.lockLinkInput,
value = viewState.linkContent,
readOnly = subscribeUiState.lockLinkInput,
value = subscribeUiState.linkContent,
onValueChange = {
subscribeViewModel.dispatch(SubscribeViewAction.InputLink(it))
subscribeViewModel.inputLink(it)
},
placeholder = stringResource(R.string.feed_or_site_url),
errorText = viewState.errorMessage,
errorText = subscribeUiState.errorMessage,
imeAction = ImeAction.Search,
focusManager = focusManager,
onConfirm = {
subscribeViewModel.dispatch(SubscribeViewAction.Search)
subscribeViewModel.search()
},
)
} else {
ResultView(
link = viewState.linkContent,
link = subscribeUiState.linkContent,
groups = groupsState.value,
selectedAllowNotificationPreset = viewState.allowNotificationPreset,
selectedParseFullContentPreset = viewState.parseFullContentPreset,
selectedGroupId = viewState.selectedGroupId,
selectedAllowNotificationPreset = subscribeUiState.allowNotificationPreset,
selectedParseFullContentPreset = subscribeUiState.parseFullContentPreset,
selectedGroupId = subscribeUiState.selectedGroupId,
allowNotificationPresetOnClick = {
subscribeViewModel.dispatch(SubscribeViewAction.ChangeAllowNotificationPreset)
subscribeViewModel.changeAllowNotificationPreset()
},
parseFullContentPresetOnClick = {
subscribeViewModel.dispatch(SubscribeViewAction.ChangeParseFullContentPreset)
subscribeViewModel.changeParseFullContentPreset()
},
onGroupClick = {
subscribeViewModel.dispatch(SubscribeViewAction.SelectedGroup(it))
subscribeViewModel.selectedGroup(it)
},
onAddNewGroup = {
subscribeViewModel.dispatch(SubscribeViewAction.ShowNewGroupDialog)
subscribeViewModel.showNewGroupDialog()
},
)
}
}
},
confirmButton = {
if (viewState.isSearchPage) {
if (subscribeUiState.isSearchPage) {
TextButton(
enabled = viewState.linkContent.isNotBlank()
&& viewState.title != stringResource(R.string.searching),
enabled = subscribeUiState.linkContent.isNotBlank()
&& subscribeUiState.title != stringResource(R.string.searching),
onClick = {
focusManager.clearFocus()
subscribeViewModel.dispatch(SubscribeViewAction.Search)
subscribeViewModel.search()
}
) {
Text(
text = stringResource(R.string.search),
color = if (viewState.linkContent.isNotBlank()) {
color = if (subscribeUiState.linkContent.isNotBlank()) {
Color.Unspecified
} else {
MaterialTheme.colorScheme.outline.copy(alpha = 0.7f)
@ -154,7 +154,7 @@ fun SubscribeDialog(
TextButton(
onClick = {
focusManager.clearFocus()
subscribeViewModel.dispatch(SubscribeViewAction.Subscribe)
subscribeViewModel.subscribe()
}
) {
Text(stringResource(R.string.subscribe))
@ -162,12 +162,12 @@ fun SubscribeDialog(
}
},
dismissButton = {
if (viewState.isSearchPage) {
if (subscribeUiState.isSearchPage) {
TextButton(
onClick = {
focusManager.clearFocus()
launcher.launch("*/*")
subscribeViewModel.dispatch(SubscribeViewAction.Hide)
subscribeViewModel.hideDrawer()
}
) {
Text(text = stringResource(R.string.import_from_opml))
@ -176,7 +176,7 @@ fun SubscribeDialog(
TextButton(
onClick = {
focusManager.clearFocus()
subscribeViewModel.dispatch(SubscribeViewAction.Hide)
subscribeViewModel.hideDrawer()
}
) {
Text(text = stringResource(R.string.cancel))
@ -186,19 +186,19 @@ fun SubscribeDialog(
)
TextFieldDialog(
visible = viewState.newGroupDialogVisible,
visible = subscribeUiState.newGroupDialogVisible,
title = stringResource(R.string.create_new_group),
icon = Icons.Outlined.CreateNewFolder,
value = viewState.newGroupContent,
value = subscribeUiState.newGroupContent,
placeholder = stringResource(R.string.name),
onValueChange = {
subscribeViewModel.dispatch(SubscribeViewAction.InputNewGroup(it))
subscribeViewModel.inputNewGroup(it)
},
onDismissRequest = {
subscribeViewModel.dispatch(SubscribeViewAction.HideNewGroupDialog)
subscribeViewModel.hideNewGroupDialog()
},
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 androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.accompanist.pager.ExperimentalPagerApi
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Job
@ -32,35 +31,12 @@ class SubscribeViewModel @Inject constructor(
@DispatcherIO
private val dispatcherIO: CoroutineDispatcher,
) : ViewModel() {
private val _viewState = MutableStateFlow(SubscribeViewState())
val viewState: StateFlow<SubscribeViewState> = _viewState.asStateFlow()
private val _subscribeUiState = MutableStateFlow(SubscribeUiState())
val subscribeUiState: StateFlow<SubscribeUiState> = _subscribeUiState.asStateFlow()
private var searchJob: Job? = null
fun dispatch(action: SubscribeViewAction) {
when (action) {
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 {
fun init() {
_subscribeUiState.update {
it.copy(
title = stringsRepository.getString(R.string.subscribe),
groups = rssRepository.get().pullGroups(),
@ -68,17 +44,17 @@ class SubscribeViewModel @Inject constructor(
}
}
private fun reset() {
fun reset() {
searchJob?.cancel()
searchJob = null
_viewState.update {
SubscribeViewState().copy(
_subscribeUiState.update {
SubscribeUiState().copy(
title = stringsRepository.getString(R.string.subscribe),
)
}
}
private fun importFromInputStream(inputStream: InputStream) {
fun importFromInputStream(inputStream: InputStream) {
viewModelScope.launch(dispatcherIO) {
try {
opmlRepository.saveToDatabase(inputStream)
@ -89,38 +65,38 @@ class SubscribeViewModel @Inject constructor(
}
}
private fun subscribe() {
val feed = _viewState.value.feed ?: return
val articles = _viewState.value.articles
fun subscribe() {
val feed = _subscribeUiState.value.feed ?: return
val articles = _subscribeUiState.value.articles
viewModelScope.launch(dispatcherIO) {
val groupId = async {
_viewState.value.selectedGroupId
_subscribeUiState.value.selectedGroupId
}
rssRepository.get().subscribe(
feed.copy(
groupId = groupId.await(),
isNotification = _viewState.value.allowNotificationPreset,
isFullContent = _viewState.value.parseFullContentPreset,
isNotification = _subscribeUiState.value.allowNotificationPreset,
isFullContent = _subscribeUiState.value.parseFullContentPreset,
), articles
)
changeVisible(false)
hideDrawer()
}
}
private fun selectedGroup(groupId: String) {
_viewState.update {
fun selectedGroup(groupId: String) {
_subscribeUiState.update {
it.copy(
selectedGroupId = groupId,
)
}
}
private fun addNewGroup() {
if (_viewState.value.newGroupContent.isNotBlank()) {
fun addNewGroup() {
if (_subscribeUiState.value.newGroupContent.isNotBlank()) {
viewModelScope.launch {
selectedGroup(rssRepository.get().addGroup(_viewState.value.newGroupContent))
changeNewGroupDialogVisible(false)
_viewState.update {
selectedGroup(rssRepository.get().addGroup(_subscribeUiState.value.newGroupContent))
hideNewGroupDialog()
_subscribeUiState.update {
it.copy(
newGroupContent = "",
)
@ -129,48 +105,48 @@ class SubscribeViewModel @Inject constructor(
}
}
private fun changeParseFullContentPreset() {
_viewState.update {
fun changeParseFullContentPreset() {
_subscribeUiState.update {
it.copy(
parseFullContentPreset = !_viewState.value.parseFullContentPreset
parseFullContentPreset = !_subscribeUiState.value.parseFullContentPreset
)
}
}
private fun changeAllowNotificationPreset() {
_viewState.update {
fun changeAllowNotificationPreset() {
_subscribeUiState.update {
it.copy(
allowNotificationPreset = !_viewState.value.allowNotificationPreset
allowNotificationPreset = !_subscribeUiState.value.allowNotificationPreset
)
}
}
private fun search() {
fun search() {
searchJob?.cancel()
viewModelScope.launch(dispatcherIO) {
try {
_viewState.update {
_subscribeUiState.update {
it.copy(
errorMessage = "",
)
}
_viewState.value.linkContent.formatUrl().let { str ->
if (str != _viewState.value.linkContent) {
_viewState.update {
_subscribeUiState.value.linkContent.formatUrl().let { str ->
if (str != _subscribeUiState.value.linkContent) {
_subscribeUiState.update {
it.copy(
linkContent = str
)
}
}
}
_viewState.update {
_subscribeUiState.update {
it.copy(
title = stringsRepository.getString(R.string.searching),
lockLinkInput = true,
)
}
if (rssRepository.get().isFeedExist(_viewState.value.linkContent)) {
_viewState.update {
if (rssRepository.get().isFeedExist(_subscribeUiState.value.linkContent)) {
_subscribeUiState.update {
it.copy(
title = stringsRepository.getString(R.string.subscribe),
errorMessage = stringsRepository.getString(R.string.already_subscribed),
@ -179,8 +155,8 @@ class SubscribeViewModel @Inject constructor(
}
return@launch
}
val feedWithArticle = rssHelper.searchFeed(_viewState.value.linkContent)
_viewState.update {
val feedWithArticle = rssHelper.searchFeed(_subscribeUiState.value.linkContent)
_subscribeUiState.update {
it.copy(
feed = feedWithArticle.feed,
articles = feedWithArticle.articles,
@ -189,7 +165,7 @@ class SubscribeViewModel @Inject constructor(
switchPage(false)
} catch (e: Exception) {
e.printStackTrace()
_viewState.update {
_subscribeUiState.update {
it.copy(
title = stringsRepository.getString(R.string.subscribe),
errorMessage = e.message ?: stringsRepository.getString(R.string.unknown),
@ -202,8 +178,8 @@ class SubscribeViewModel @Inject constructor(
}
}
private fun inputLink(content: String) {
_viewState.update {
fun inputLink(content: String) {
_subscribeUiState.update {
it.copy(
linkContent = content,
errorMessage = "",
@ -211,32 +187,48 @@ class SubscribeViewModel @Inject constructor(
}
}
private fun inputNewGroup(content: String) {
_viewState.update {
fun inputNewGroup(content: String) {
_subscribeUiState.update {
it.copy(
newGroupContent = content
)
}
}
private fun changeVisible(visible: Boolean) {
_viewState.update {
fun showDrawer() {
_subscribeUiState.update {
it.copy(
visible = visible
visible = true
)
}
}
private fun changeNewGroupDialogVisible(visible: Boolean) {
_viewState.update {
fun hideDrawer() {
_subscribeUiState.update {
it.copy(
newGroupDialogVisible = visible,
visible = false
)
}
}
private fun switchPage(isSearchPage: Boolean) {
_viewState.update {
fun showNewGroupDialog() {
_subscribeUiState.update {
it.copy(
newGroupDialogVisible = true,
)
}
}
fun hideNewGroupDialog() {
_subscribeUiState.update {
it.copy(
newGroupDialogVisible = false,
)
}
}
fun switchPage(isSearchPage: Boolean) {
_subscribeUiState.update {
it.copy(
isSearchPage = isSearchPage
)
@ -244,7 +236,7 @@ class SubscribeViewModel @Inject constructor(
}
}
data class SubscribeViewState(
data class SubscribeUiState(
val visible: Boolean = false,
val title: String = "",
val errorMessage: String = "",
@ -260,42 +252,3 @@ data class SubscribeViewState(
val groups: Flow<List<Group>> = emptyFlow(),
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.page.common.RouteName
import me.ash.reader.ui.page.home.FilterState
import me.ash.reader.ui.page.home.HomeViewAction
import me.ash.reader.ui.page.home.HomeViewModel
@OptIn(
@ -47,8 +46,6 @@ fun FlowPage(
flowViewModel: FlowViewModel = hiltViewModel(),
homeViewModel: HomeViewModel,
) {
val homeViewView = homeViewModel.viewState.collectAsStateValue()
val pagingItems = homeViewView.pagingData.collectAsLazyPagingItems()
val keyboardController = LocalSoftwareKeyboardController.current
val topBarTonalElevation = LocalFlowTopBarTonalElevation.current
val articleListTonalElevation = LocalFlowArticleListTonalElevation.current
@ -59,16 +56,18 @@ fun FlowPage(
val filterBarPadding = LocalFlowFilterBarPadding.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 focusRequester = remember { FocusRequester() }
var markAsRead 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
var isSyncing by remember { mutableStateOf(false) }
homeViewModel.syncWorkLiveData.observe(owner) {
@ -82,15 +81,15 @@ fun FlowPage(
focusRequester.requestFocus()
} else {
keyboardController?.hide()
if (homeViewState.searchContent.isNotBlank()) {
homeViewModel.dispatch(HomeViewAction.InputSearchContent(""))
if (homeUiState.searchContent.isNotBlank()) {
homeViewModel.inputSearchContent("")
}
}
}
}
LaunchedEffect(viewState.listState) {
snapshotFlow { viewState.listState.firstVisibleItemIndex }.collect {
LaunchedEffect(flowUiState.listState) {
snapshotFlow { flowUiState.listState.firstVisibleItemIndex }.collect {
if (it > 0) {
keyboardController?.hide()
}
@ -122,7 +121,7 @@ fun FlowPage(
},
actions = {
AnimatedVisibility(
visible = !filterState.filter.isStarred(),
visible = !filterUiState.filter.isStarred(),
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(),
) {
@ -136,7 +135,7 @@ fun FlowPage(
},
) {
scope.launch {
viewState.listState.scrollToItem(0)
flowUiState.listState.scrollToItem(0)
markAsRead = !markAsRead
onSearch = false
}
@ -152,7 +151,7 @@ fun FlowPage(
},
) {
scope.launch {
viewState.listState.scrollToItem(0)
flowUiState.listState.scrollToItem(0)
onSearch = !onSearch
}
}
@ -161,7 +160,7 @@ fun FlowPage(
SwipeRefresh(
onRefresh = {
if (!isSyncing) {
flowViewModel.dispatch(FlowViewAction.Sync)
flowViewModel.sync()
}
}
) {
@ -170,7 +169,7 @@ fun FlowPage(
state = listState,
) {
item {
DisplayTextHeader(filterState, isSyncing, articleListFeedIcon.value)
DisplayTextHeader(filterUiState, isSyncing, articleListFeedIcon.value)
AnimatedVisibility(
visible = markAsRead,
enter = fadeIn() + expandVertically(),
@ -186,14 +185,12 @@ fun FlowPage(
},
) {
markAsRead = false
flowViewModel.dispatch(
FlowViewAction.MarkAsRead(
groupId = filterState.group?.id,
feedId = filterState.feed?.id,
flowViewModel.markAsRead(
groupId = filterUiState.group?.id,
feedId = filterUiState.feed?.id,
articleId = null,
markAsReadBefore = it,
)
)
}
AnimatedVisibility(
visible = onSearch,
@ -201,30 +198,30 @@ fun FlowPage(
exit = fadeOut() + shrinkVertically(),
) {
SearchBar(
value = homeViewState.searchContent,
value = homeUiState.searchContent,
placeholder = when {
filterState.group != null -> stringResource(
filterUiState.group != null -> stringResource(
R.string.search_for_in,
filterState.filter.getName(),
filterState.group.name
filterUiState.filter.getName(),
filterUiState.group.name
)
filterState.feed != null -> stringResource(
filterUiState.feed != null -> stringResource(
R.string.search_for_in,
filterState.filter.getName(),
filterState.feed.name
filterUiState.filter.getName(),
filterUiState.feed.name
)
else -> stringResource(
R.string.search_for,
filterState.filter.getName()
filterUiState.filter.getName()
)
},
focusRequester = focusRequester,
onValueChange = {
homeViewModel.dispatch(HomeViewAction.InputSearchContent(it))
homeViewModel.inputSearchContent(it)
},
onClose = {
onSearch = false
homeViewModel.dispatch(HomeViewAction.InputSearchContent(""))
homeViewModel.inputSearchContent("")
}
)
Spacer(modifier = Modifier.height((56 + 24 + 10).dp))
@ -253,15 +250,15 @@ fun FlowPage(
},
bottomBar = {
FilterBar(
filter = filterState.filter,
filter = filterUiState.filter,
filterBarStyle = filterBarStyle.value,
filterBarFilled = filterBarFilled.value,
filterBarPadding = filterBarPadding.dp,
filterBarTonalElevation = filterBarTonalElevation.value.dp,
) {
flowViewModel.dispatch(FlowViewAction.ScrollToItem(0))
homeViewModel.dispatch(HomeViewAction.ChangeFilter(filterState.copy(filter = it)))
homeViewModel.dispatch(HomeViewAction.FetchArticles)
flowViewModel.scrollToItem(0)
homeViewModel.changeFilter(filterUiState.copy(filter = it))
homeViewModel.fetchArticles()
}
}
)

View File

@ -7,7 +7,6 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import me.ash.reader.data.entity.ArticleWithFeed
import me.ash.reader.data.repository.RssRepository
@ -19,40 +18,20 @@ import javax.inject.Inject
class FlowViewModel @Inject constructor(
private val rssRepository: RssRepository,
) : ViewModel() {
private val _viewState = MutableStateFlow(ArticleViewState())
val viewState: StateFlow<ArticleViewState> = _viewState.asStateFlow()
private val _flowUiState = MutableStateFlow(FlowUiState())
val flowUiState: StateFlow<FlowUiState> = _flowUiState.asStateFlow()
fun dispatch(action: FlowViewAction) {
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() {
fun sync() {
rssRepository.get().doSync()
}
private fun scrollToItem(index: Int) {
fun scrollToItem(index: Int) {
viewModelScope.launch {
_viewState.value.listState.scrollToItem(index)
_flowUiState.value.listState.scrollToItem(index)
}
}
private fun changeIsBack(isBack: Boolean) {
_viewState.update {
it.copy(isBack = isBack)
}
}
private fun markAsRead(
fun markAsRead(
groupId: String?,
feedId: String?,
articleId: String?,
@ -84,32 +63,13 @@ class FlowViewModel @Inject constructor(
}
}
data class ArticleViewState(
data class FlowUiState(
val filterImportant: Int = 0,
val listState: LazyListState = LazyListState(),
val isBack: Boolean = false,
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 {
SevenDays,
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.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 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.util.Log
@ -32,26 +32,26 @@ import me.ash.reader.ui.ext.collectAsStateValue
import me.ash.reader.ui.ext.drawVerticalScrollbar
@Composable
fun ReadPage(
fun ReadingPage(
navController: NavHostController,
readViewModel: ReadViewModel = hiltViewModel(),
readingViewModel: ReadingViewModel = hiltViewModel(),
) {
val viewState = readViewModel.viewState.collectAsStateValue()
val isScrollDown = viewState.listState.isScrollDown()
val readingUiState = readingViewModel.readingUiState.collectAsStateValue()
val isScrollDown = readingUiState.listState.isScrollDown()
LaunchedEffect(Unit) {
navController.currentBackStackEntryFlow.collect {
it.arguments?.getString("articleId")?.let {
readViewModel.dispatch(ReadViewAction.InitData(it))
readingViewModel.initData(it)
}
}
}
LaunchedEffect(viewState.articleWithFeed?.article?.id) {
Log.i("RLog", "ReadPage: ${viewState.articleWithFeed}")
viewState.articleWithFeed?.let {
LaunchedEffect(readingUiState.articleWithFeed?.article?.id) {
Log.i("RLog", "ReadPage: ${readingUiState.articleWithFeed}")
readingUiState.articleWithFeed?.let {
if (it.article.isUnread) {
readViewModel.dispatch(ReadViewAction.MarkUnread(false))
readingViewModel.markUnread(false)
}
}
}
@ -66,19 +66,19 @@ fun ReadPage(
contentAlignment = Alignment.TopCenter
) {
TopBar(
isShow = viewState.articleWithFeed == null || !isScrollDown,
title = viewState.articleWithFeed?.article?.title,
link = viewState.articleWithFeed?.article?.link,
isShow = readingUiState.articleWithFeed == null || !isScrollDown,
title = readingUiState.articleWithFeed?.article?.title,
link = readingUiState.articleWithFeed?.article?.link,
onClose = {
navController.popBackStack()
},
)
}
Content(
content = viewState.content ?: "",
articleWithFeed = viewState.articleWithFeed,
isLoading = viewState.isLoading,
listState = viewState.listState,
content = readingUiState.content ?: "",
articleWithFeed = readingUiState.articleWithFeed,
isLoading = readingUiState.isLoading,
listState = readingUiState.listState,
)
Box(
modifier = Modifier
@ -87,17 +87,17 @@ fun ReadPage(
contentAlignment = Alignment.BottomCenter
) {
BottomBar(
isShow = viewState.articleWithFeed != null && !isScrollDown,
articleWithFeed = viewState.articleWithFeed,
isShow = readingUiState.articleWithFeed != null && !isScrollDown,
articleWithFeed = readingUiState.articleWithFeed,
unreadOnClick = {
readViewModel.dispatch(ReadViewAction.MarkUnread(it))
readingViewModel.markUnread(it)
},
starredOnClick = {
readViewModel.dispatch(ReadViewAction.MarkStarred(it))
readingViewModel.markStarred(it)
},
fullContentOnClick = { afterIsFullContent ->
if (afterIsFullContent) readViewModel.dispatch(ReadViewAction.RenderFullContent)
else readViewModel.dispatch(ReadViewAction.RenderDescriptionContent)
if (afterIsFullContent) readingViewModel.renderFullContent()
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 androidx.compose.foundation.lazy.LazyListState
@ -17,41 +17,29 @@ import me.ash.reader.data.repository.RssRepository
import javax.inject.Inject
@HiltViewModel
class ReadViewModel @Inject constructor(
class ReadingViewModel @Inject constructor(
val rssRepository: RssRepository,
private val rssHelper: RssHelper,
) : ViewModel() {
private val _viewState = MutableStateFlow(ReadViewState())
val viewState: StateFlow<ReadViewState> = _viewState.asStateFlow()
private val _readingUiState = MutableStateFlow(ReadingUiState())
val readingUiState: StateFlow<ReadingUiState> = _readingUiState.asStateFlow()
fun dispatch(action: ReadViewAction) {
when (action) {
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)
fun initData(articleId: String) {
showLoading()
viewModelScope.launch {
_viewState.update {
_readingUiState.update {
it.copy(articleWithFeed = rssRepository.get().findArticleById(articleId))
}
_viewState.value.articleWithFeed?.let {
_readingUiState.value.articleWithFeed?.let {
if (it.feed.isFullContent) internalRenderFullContent()
else renderDescriptionContent()
}
changeLoading(false)
hideLoading()
}
}
private fun renderDescriptionContent() {
_viewState.update {
fun renderDescriptionContent() {
_readingUiState.update {
it.copy(
content = it.articleWithFeed?.article?.fullContent
?: it.articleWithFeed?.article?.rawDescription ?: "",
@ -59,38 +47,38 @@ class ReadViewModel @Inject constructor(
}
}
private fun renderFullContent() {
fun renderFullContent() {
viewModelScope.launch {
internalRenderFullContent()
}
}
private suspend fun internalRenderFullContent() {
changeLoading(true)
suspend fun internalRenderFullContent() {
showLoading()
try {
_viewState.update {
_readingUiState.update {
it.copy(
content = rssHelper.parseFullContent(
_viewState.value.articleWithFeed?.article?.link ?: "",
_viewState.value.articleWithFeed?.article?.title ?: ""
_readingUiState.value.articleWithFeed?.article?.link ?: "",
_readingUiState.value.articleWithFeed?.article?.title ?: ""
)
)
}
} catch (e: Exception) {
Log.i("RLog", "renderFullContent: ${e.message}")
_viewState.update {
_readingUiState.update {
it.copy(
content = e.message
)
}
}
changeLoading(false)
hideLoading()
}
private fun markUnread(isUnread: Boolean) {
val articleWithFeed = _viewState.value.articleWithFeed ?: return
fun markUnread(isUnread: Boolean) {
val articleWithFeed = _readingUiState.value.articleWithFeed ?: return
viewModelScope.launch {
_viewState.update {
_readingUiState.update {
it.copy(
articleWithFeed = articleWithFeed.copy(
article = articleWithFeed.article.copy(
@ -102,17 +90,17 @@ class ReadViewModel @Inject constructor(
rssRepository.get().markAsRead(
groupId = null,
feedId = null,
articleId = _viewState.value.articleWithFeed!!.article.id,
articleId = _readingUiState.value.articleWithFeed!!.article.id,
before = null,
isUnread = isUnread,
)
}
}
private fun markStarred(isStarred: Boolean) {
val articleWithFeed = _viewState.value.articleWithFeed ?: return
fun markStarred(isStarred: Boolean) {
val articleWithFeed = _readingUiState.value.articleWithFeed ?: return
viewModelScope.launch(Dispatchers.IO) {
_viewState.update {
_readingUiState.update {
it.copy(
articleWithFeed = articleWithFeed.copy(
article = articleWithFeed.article.copy(
@ -129,47 +117,22 @@ class ReadViewModel @Inject constructor(
}
}
private fun clearArticle() {
_viewState.update {
it.copy(articleWithFeed = null)
private fun showLoading() {
_readingUiState.update {
it.copy(isLoading = true)
}
}
private fun changeLoading(isLoading: Boolean) {
_viewState.update {
it.copy(isLoading = isLoading)
private fun hideLoading() {
_readingUiState.update {
it.copy(isLoading = false)
}
}
}
data class ReadViewState(
data class ReadingUiState(
val articleWithFeed: ArticleWithFeed? = null,
val content: String? = null,
val isLoading: Boolean = true,
// val scrollState: ScrollState = ScrollState(0),
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.page.common.RouteName
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.theme.palette.onLight
@ -76,7 +75,7 @@ fun SettingsPage(
)
},
) {
updateViewModel.dispatch(UpdateViewAction.Show)
updateViewModel.showDialog()
}
}
Banner(

View File

@ -103,8 +103,7 @@ fun TipsAndSupportPage(
onTap = {
if (System.currentTimeMillis() - clickTime > 2000) {
clickTime = System.currentTimeMillis()
updateViewModel.dispatch(
UpdateViewAction.CheckUpdate(
updateViewModel.checkUpdate(
{
context.showToast(context.getString(R.string.checking_updates))
context.dataStore.put(
@ -120,7 +119,6 @@ fun TipsAndSupportPage(
}
}
)
)
} else {
clickTime = System.currentTimeMillis()
}

View File

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

View File

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