mirror of https://github.com/readrops/Readrops.git
Improve state management in FeedTab
This commit is contained in:
parent
69788de077
commit
215399d3ac
|
@ -0,0 +1,51 @@
|
||||||
|
package com.readrops.app.compose.feeds
|
||||||
|
|
||||||
|
import com.readrops.db.entities.Feed
|
||||||
|
import com.readrops.db.entities.Folder
|
||||||
|
import com.readrops.db.entities.account.Account
|
||||||
|
|
||||||
|
data class FeedState(
|
||||||
|
val foldersAndFeeds: FolderAndFeedsState = FolderAndFeedsState.InitialState,
|
||||||
|
val dialog: DialogState? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
sealed interface DialogState {
|
||||||
|
object AddFeed : DialogState
|
||||||
|
class DeleteFeed(val feed: Feed) : DialogState
|
||||||
|
class UpdateFeed(val feed: Feed) : DialogState
|
||||||
|
class FeedSheet(val feed: Feed, val folder: Folder?) : DialogState
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class FolderAndFeedsState {
|
||||||
|
object InitialState : FolderAndFeedsState()
|
||||||
|
data class ErrorState(val exception: Exception) : FolderAndFeedsState()
|
||||||
|
data class LoadedState(val values: Map<Folder?, List<Feed>>) : FolderAndFeedsState()
|
||||||
|
}
|
||||||
|
|
||||||
|
data class AddFeedDialogState(
|
||||||
|
val url: String = "",
|
||||||
|
val selectedAccount: Account = Account(accountName = ""),
|
||||||
|
val accounts: List<Account> = listOf(),
|
||||||
|
val error: AddFeedError? = null,
|
||||||
|
val closeDialog: Boolean = false,
|
||||||
|
) {
|
||||||
|
fun isError() = error != null
|
||||||
|
|
||||||
|
val errorText: String
|
||||||
|
get() = when (error) {
|
||||||
|
is AddFeedError.EmptyUrl -> "Field can't be empty"
|
||||||
|
AddFeedError.BadUrl -> "Input is not a valid URL"
|
||||||
|
AddFeedError.NoConnection -> ""
|
||||||
|
AddFeedError.NoRSSFeed -> "No RSS feed found"
|
||||||
|
AddFeedError.UnreachableUrl -> ""
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class AddFeedError {
|
||||||
|
object EmptyUrl : AddFeedError()
|
||||||
|
object BadUrl : AddFeedError()
|
||||||
|
object UnreachableUrl : AddFeedError()
|
||||||
|
object NoRSSFeed : AddFeedError()
|
||||||
|
object NoConnection : AddFeedError()
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,9 +17,6 @@ import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
|
@ -33,7 +30,6 @@ import cafe.adriel.voyager.navigator.tab.TabOptions
|
||||||
import com.readrops.app.compose.R
|
import com.readrops.app.compose.R
|
||||||
import com.readrops.app.compose.util.components.Placeholder
|
import com.readrops.app.compose.util.components.Placeholder
|
||||||
import com.readrops.db.entities.Feed
|
import com.readrops.db.entities.Feed
|
||||||
import com.readrops.db.entities.Folder
|
|
||||||
import org.koin.androidx.compose.getViewModel
|
import org.koin.androidx.compose.getViewModel
|
||||||
|
|
||||||
object FeedTab : Tab {
|
object FeedTab : Tab {
|
||||||
|
@ -53,32 +49,31 @@ object FeedTab : Tab {
|
||||||
val viewModel = getViewModel<FeedViewModel>()
|
val viewModel = getViewModel<FeedViewModel>()
|
||||||
|
|
||||||
val state by viewModel.feedsState.collectAsStateWithLifecycle()
|
val state by viewModel.feedsState.collectAsStateWithLifecycle()
|
||||||
var showDialog by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
var selectedFeed by remember { mutableStateOf<Feed?>(null) }
|
when (val dialog = state.dialog) {
|
||||||
var selectedFolder by remember { mutableStateOf<Folder?>(null) }
|
is DialogState.AddFeed -> {
|
||||||
var showBottomSheet by remember { mutableStateOf(false) }
|
AddFeedDialog(
|
||||||
|
viewModel = viewModel,
|
||||||
if (showBottomSheet) {
|
onDismiss = {
|
||||||
|
viewModel.closeDialog()
|
||||||
|
viewModel.resetAddFeedDialogState()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is DialogState.DeleteFeed -> {}
|
||||||
|
is DialogState.FeedSheet -> {
|
||||||
FeedModalBottomSheet(
|
FeedModalBottomSheet(
|
||||||
feed = selectedFeed!!,
|
feed = dialog.feed,
|
||||||
folder = selectedFolder,
|
folder = dialog.folder,
|
||||||
onDismissRequest = { showBottomSheet = false },
|
onDismissRequest = { viewModel.closeDialog() },
|
||||||
onOpen = { uriHandler.openUri(selectedFeed!!.siteUrl!!) },
|
onOpen = { uriHandler.openUri(dialog.feed.siteUrl!!) },
|
||||||
onModify = { },
|
onModify = { },
|
||||||
onUpdateColor = {},
|
onUpdateColor = {},
|
||||||
onDelete = {},
|
onDelete = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
is DialogState.UpdateFeed -> {}
|
||||||
if (showDialog) {
|
null -> {}
|
||||||
AddFeedDialog(
|
|
||||||
viewModel = viewModel,
|
|
||||||
onDismiss = {
|
|
||||||
showDialog = false
|
|
||||||
viewModel.resetAddFeedDialogState()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
@ -105,9 +100,9 @@ object FeedTab : Tab {
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
) {
|
) {
|
||||||
when (state) {
|
when (state.foldersAndFeeds) {
|
||||||
is FeedsState.LoadedState -> {
|
is FolderAndFeedsState.LoadedState -> {
|
||||||
val foldersAndFeeds = (state as FeedsState.LoadedState).foldersAndFeeds
|
val foldersAndFeeds = (state.foldersAndFeeds as FolderAndFeedsState.LoadedState).values
|
||||||
|
|
||||||
if (foldersAndFeeds.isNotEmpty()) {
|
if (foldersAndFeeds.isNotEmpty()) {
|
||||||
LazyColumn {
|
LazyColumn {
|
||||||
|
@ -115,12 +110,6 @@ object FeedTab : Tab {
|
||||||
items = foldersAndFeeds.toList()
|
items = foldersAndFeeds.toList()
|
||||||
) { folderWithFeeds ->
|
) { folderWithFeeds ->
|
||||||
|
|
||||||
fun onFeedClick(feed: Feed) {
|
|
||||||
selectedFeed = feed
|
|
||||||
selectedFolder = folderWithFeeds.first
|
|
||||||
showBottomSheet = true
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onFeedLongClick(feed: Feed) {
|
fun onFeedLongClick(feed: Feed) {
|
||||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
uriHandler.openUri(feed.siteUrl!!)
|
uriHandler.openUri(feed.siteUrl!!)
|
||||||
|
@ -130,7 +119,7 @@ object FeedTab : Tab {
|
||||||
FolderExpandableItem(
|
FolderExpandableItem(
|
||||||
folder = folderWithFeeds.first!!,
|
folder = folderWithFeeds.first!!,
|
||||||
feeds = folderWithFeeds.second,
|
feeds = folderWithFeeds.second,
|
||||||
onFeedClick = { feed -> onFeedClick(feed) },
|
onFeedClick = { feed -> viewModel.openFeedSheet(feed, folderWithFeeds.first) },
|
||||||
onFeedLongClick = { feed -> onFeedLongClick(feed) }
|
onFeedLongClick = { feed -> onFeedLongClick(feed) }
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
@ -139,7 +128,7 @@ object FeedTab : Tab {
|
||||||
for (feed in feeds) {
|
for (feed in feeds) {
|
||||||
FeedItem(
|
FeedItem(
|
||||||
feed = feed,
|
feed = feed,
|
||||||
onClick = { onFeedClick(feed) },
|
onClick = { viewModel.openFeedSheet(feed, null) },
|
||||||
onLongClick = { onFeedLongClick(feed) },
|
onLongClick = { onFeedLongClick(feed) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -154,7 +143,7 @@ object FeedTab : Tab {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is FeedsState.ErrorState -> {
|
is FolderAndFeedsState.ErrorState -> {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -167,7 +156,7 @@ object FeedTab : Tab {
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.BottomEnd)
|
.align(Alignment.BottomEnd)
|
||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
onClick = { showDialog = true }
|
onClick = { viewModel.openAddFeedDialog() }
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Add,
|
imageVector = Icons.Default.Add,
|
||||||
|
|
|
@ -27,8 +27,8 @@ class FeedViewModel(
|
||||||
private val localRSSDataSource: LocalRSSDataSource,
|
private val localRSSDataSource: LocalRSSDataSource,
|
||||||
) : TabViewModel(database), KoinComponent {
|
) : TabViewModel(database), KoinComponent {
|
||||||
|
|
||||||
private val _feedsState = MutableStateFlow<FeedsState>(FeedsState.InitialState)
|
private val _feedState = MutableStateFlow(FeedState())
|
||||||
val feedsState = _feedsState.asStateFlow()
|
val feedsState = _feedState.asStateFlow()
|
||||||
|
|
||||||
private val _addFeedDialogState = MutableStateFlow(AddFeedDialogState())
|
private val _addFeedDialogState = MutableStateFlow(AddFeedDialogState())
|
||||||
val addFeedDialogState = _addFeedDialogState.asStateFlow()
|
val addFeedDialogState = _addFeedDialogState.asStateFlow()
|
||||||
|
@ -39,8 +39,16 @@ class FeedViewModel(
|
||||||
.flatMapConcat { account ->
|
.flatMapConcat { account ->
|
||||||
getFoldersWithFeeds.get(account.id)
|
getFoldersWithFeeds.get(account.id)
|
||||||
}
|
}
|
||||||
.catch { _feedsState.value = FeedsState.ErrorState(Exception(it)) }
|
.catch { throwable ->
|
||||||
.collect { _feedsState.value = FeedsState.LoadedState(it) }
|
_feedState.update {
|
||||||
|
it.copy(foldersAndFeeds = FolderAndFeedsState.ErrorState(Exception(throwable)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.collect { foldersAndFeeds ->
|
||||||
|
_feedState.update {
|
||||||
|
it.copy(foldersAndFeeds = FolderAndFeedsState.LoadedState(foldersAndFeeds))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModelScope.launch(context = Dispatchers.IO) {
|
viewModelScope.launch(context = Dispatchers.IO) {
|
||||||
|
@ -57,6 +65,13 @@ class FeedViewModel(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun closeDialog() = _feedState.update { it.copy(dialog = null) }
|
||||||
|
|
||||||
|
fun openAddFeedDialog() = _feedState.update { it.copy(dialog = DialogState.AddFeed) }
|
||||||
|
|
||||||
|
fun openFeedSheet(feed: Feed, folder: Folder?) =
|
||||||
|
_feedState.update { it.copy(dialog = DialogState.FeedSheet(feed, folder)) }
|
||||||
|
|
||||||
fun setAddFeedDialogURL(url: String) {
|
fun setAddFeedDialogURL(url: String) {
|
||||||
_addFeedDialogState.update {
|
_addFeedDialogState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
|
@ -135,37 +150,3 @@ class FeedViewModel(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class FeedsState {
|
|
||||||
object InitialState : FeedsState()
|
|
||||||
data class ErrorState(val exception: Exception) : FeedsState()
|
|
||||||
data class LoadedState(val foldersAndFeeds: Map<Folder?, List<Feed>>) : FeedsState()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
data class AddFeedDialogState(
|
|
||||||
val url: String = "",
|
|
||||||
val selectedAccount: Account = Account(accountName = ""),
|
|
||||||
val accounts: List<Account> = listOf(),
|
|
||||||
val error: AddFeedError? = null,
|
|
||||||
val closeDialog: Boolean = false,
|
|
||||||
) {
|
|
||||||
fun isError() = error != null
|
|
||||||
|
|
||||||
val errorText: String
|
|
||||||
get() = when (error) {
|
|
||||||
is AddFeedError.EmptyUrl -> "Field can't be empty"
|
|
||||||
AddFeedError.BadUrl -> "Input is not a valid URL"
|
|
||||||
AddFeedError.NoConnection -> ""
|
|
||||||
AddFeedError.NoRSSFeed -> "No RSS feed found"
|
|
||||||
AddFeedError.UnreachableUrl -> ""
|
|
||||||
else -> ""
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed class AddFeedError {
|
|
||||||
object EmptyUrl : AddFeedError()
|
|
||||||
object BadUrl : AddFeedError()
|
|
||||||
object UnreachableUrl : AddFeedError()
|
|
||||||
object NoRSSFeed : AddFeedError()
|
|
||||||
object NoConnection : AddFeedError()
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue