Replace FolderDialog by TextFieldDialog in FeedTab

This commit is contained in:
Shinokuni 2024-07-10 16:43:26 +02:00
parent feeed2771f
commit 8e8d1ce9c8
3 changed files with 153 additions and 208 deletions

View File

@ -9,11 +9,13 @@ import com.readrops.app.R
import com.readrops.app.base.TabScreenModel import com.readrops.app.base.TabScreenModel
import com.readrops.app.repositories.GetFoldersWithFeeds import com.readrops.app.repositories.GetFoldersWithFeeds
import com.readrops.app.util.components.TextFieldError import com.readrops.app.util.components.TextFieldError
import com.readrops.app.util.components.dialog.TextFieldDialogState
import com.readrops.db.Database import com.readrops.db.Database
import com.readrops.db.entities.Feed import com.readrops.db.entities.Feed
import com.readrops.db.entities.Folder import com.readrops.db.entities.Folder
import com.readrops.db.entities.account.Account import com.readrops.db.entities.account.Account
import com.readrops.db.filters.MainFilter import com.readrops.db.filters.MainFilter
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -31,7 +33,8 @@ class FeedScreenModel(
database: Database, database: Database,
private val getFoldersWithFeeds: GetFoldersWithFeeds, private val getFoldersWithFeeds: GetFoldersWithFeeds,
private val localRSSDataSource: LocalRSSDataSource, private val localRSSDataSource: LocalRSSDataSource,
private val context: Context private val context: Context,
private val dispatcher: CoroutineDispatcher = Dispatchers.IO
) : TabScreenModel(database), KoinComponent { ) : TabScreenModel(database), KoinComponent {
private val _feedState = MutableStateFlow(FeedState()) private val _feedState = MutableStateFlow(FeedState())
@ -43,11 +46,11 @@ class FeedScreenModel(
private val _updateFeedDialogState = MutableStateFlow(UpdateFeedDialogState()) private val _updateFeedDialogState = MutableStateFlow(UpdateFeedDialogState())
val updateFeedDialogState = _updateFeedDialogState.asStateFlow() val updateFeedDialogState = _updateFeedDialogState.asStateFlow()
private val _folderState = MutableStateFlow(FolderState()) private val _folderState = MutableStateFlow(TextFieldDialogState())
val folderState = _folderState.asStateFlow() val folderState = _folderState.asStateFlow()
init { init {
screenModelScope.launch(context = Dispatchers.IO) { screenModelScope.launch(dispatcher) {
accountEvent accountEvent
.flatMapLatest { account -> .flatMapLatest { account ->
_feedState.update { it.copy(displayFolderCreationButton = account.config.canCreateFolder) } _feedState.update { it.copy(displayFolderCreationButton = account.config.canCreateFolder) }
@ -57,7 +60,11 @@ class FeedScreenModel(
) )
} }
getFoldersWithFeeds.get(account.id, MainFilter.ALL, account.config.useSeparateState) getFoldersWithFeeds.get(
account.id,
MainFilter.ALL,
account.config.useSeparateState
)
} }
.catch { throwable -> .catch { throwable ->
_feedState.update { _feedState.update {
@ -72,7 +79,7 @@ class FeedScreenModel(
} }
screenModelScope.launch(context = Dispatchers.IO) { screenModelScope.launch(dispatcher) {
database.accountDao() database.accountDao()
.selectAllAccounts() .selectAllAccounts()
.collect { accounts -> .collect { accounts ->
@ -87,17 +94,16 @@ class FeedScreenModel(
} }
} }
screenModelScope.launch(context = Dispatchers.IO) { screenModelScope.launch(dispatcher) {
accountEvent.flatMapLatest { account -> accountEvent.flatMapLatest { account ->
_updateFeedDialogState.update { _updateFeedDialogState.update {
it.copy( it.copy(
isFeedUrlReadOnly = account.config.isFeedUrlReadOnly, isFeedUrlReadOnly = account.config.isFeedUrlReadOnly,
) )
}
database.folderDao()
.selectFolders(account.id)
} }
database.folderDao().selectFolders(account.id)
}
.collect { folders -> .collect { folders ->
_updateFeedDialogState.update { _updateFeedDialogState.update {
it.copy( it.copy(
@ -131,18 +137,21 @@ class FeedScreenModel(
) )
} }
} }
is DialogState.AddFolder, is DialogState.UpdateFolder -> { is DialogState.AddFolder, is DialogState.UpdateFolder -> {
_folderState.update { _folderState.update {
it.copy( it.copy(
folder = Folder(), value = "",
nameError = null, textFieldError = null,
exception = null exception = null
) )
} }
} }
is DialogState.UpdateFeed -> { is DialogState.UpdateFeed -> {
_updateFeedDialogState.update { it.copy(exception = null) } _updateFeedDialogState.update { it.copy(exception = null) }
} }
else -> {} else -> {}
} }
@ -165,7 +174,7 @@ class FeedScreenModel(
if (state is DialogState.UpdateFolder) { if (state is DialogState.UpdateFolder) {
_folderState.update { _folderState.update {
it.copy( it.copy(
folder = state.folder value = state.folder.name.orEmpty()
) )
} }
} }
@ -174,7 +183,7 @@ class FeedScreenModel(
} }
fun deleteFeed(feed: Feed) { fun deleteFeed(feed: Feed) {
screenModelScope.launch(Dispatchers.IO) { screenModelScope.launch(dispatcher) {
try { try {
repository?.deleteFeed(feed) repository?.deleteFeed(feed)
} catch (e: Exception) { } catch (e: Exception) {
@ -184,7 +193,7 @@ class FeedScreenModel(
} }
fun deleteFolder(folder: Folder) { fun deleteFolder(folder: Folder) {
screenModelScope.launch(Dispatchers.IO) { screenModelScope.launch(dispatcher) {
try { try {
repository?.deleteFolder(folder) repository?.deleteFolder(folder)
} catch (e: Exception) { } catch (e: Exception) {
@ -228,7 +237,7 @@ class FeedScreenModel(
return return
} }
else -> screenModelScope.launch(Dispatchers.IO) { else -> screenModelScope.launch(dispatcher) {
try { try {
if (localRSSDataSource.isUrlRSSResource(url)) { if (localRSSDataSource.isUrlRSSResource(url)) {
insertFeeds(listOf(Feed(url = url))) insertFeeds(listOf(Feed(url = url)))
@ -329,7 +338,7 @@ class FeedScreenModel(
else -> { else -> {
_updateFeedDialogState.update { it.copy(exception = null) } _updateFeedDialogState.update { it.copy(exception = null) }
screenModelScope.launch(Dispatchers.IO) { screenModelScope.launch(dispatcher) {
with(_updateFeedDialogState.value) { with(_updateFeedDialogState.value) {
try { try {
repository?.updateFeed( repository?.updateFeed(
@ -362,28 +371,26 @@ class FeedScreenModel(
fun setFolderName(name: String) = _folderState.update { fun setFolderName(name: String) = _folderState.update {
it.copy( it.copy(
folder = it.folder.copy(name = name), value = name,
nameError = null, textFieldError = null,
) )
} }
fun folderValidate(updateFolder: Boolean = false) { fun folderValidate(updateFolder: Boolean = false) {
val name = _folderState.value.name.orEmpty() val name = _folderState.value.value
if (name.isEmpty()) { if (name.isEmpty()) {
_folderState.update { it.copy(nameError = TextFieldError.EmptyField) } _folderState.update { it.copy(textFieldError = TextFieldError.EmptyField) }
return return
} }
screenModelScope.launch(Dispatchers.IO) { screenModelScope.launch(dispatcher) {
try { try {
if (updateFolder) { if (updateFolder) {
repository?.updateFolder(_folderState.value.folder) val folder = (_feedState.value.dialog as DialogState.UpdateFolder).folder
repository?.updateFolder(folder.copy(name = name))
} else { } else {
repository?.addFolder(_folderState.value.folder.apply { repository?.addFolder(Folder(name = name, accountId = currentAccount!!.id))
accountId = currentAccount!!.id
})
} }
} catch (e: Exception) { } catch (e: Exception) {
_folderState.update { it.copy(exception = e) } _folderState.update { it.copy(exception = e) }

View File

@ -44,12 +44,12 @@ import cafe.adriel.voyager.navigator.tab.TabOptions
import com.readrops.app.R import com.readrops.app.R
import com.readrops.app.feeds.dialogs.AddFeedDialog import com.readrops.app.feeds.dialogs.AddFeedDialog
import com.readrops.app.feeds.dialogs.FeedModalBottomSheet import com.readrops.app.feeds.dialogs.FeedModalBottomSheet
import com.readrops.app.feeds.dialogs.FolderDialog
import com.readrops.app.feeds.dialogs.UpdateFeedDialog import com.readrops.app.feeds.dialogs.UpdateFeedDialog
import com.readrops.app.util.ErrorMessage import com.readrops.app.util.ErrorMessage
import com.readrops.app.util.components.CenteredProgressIndicator import com.readrops.app.util.components.CenteredProgressIndicator
import com.readrops.app.util.components.ErrorMessage import com.readrops.app.util.components.ErrorMessage
import com.readrops.app.util.components.Placeholder import com.readrops.app.util.components.Placeholder
import com.readrops.app.util.components.dialog.TextFieldDialog
import com.readrops.app.util.components.dialog.TwoChoicesDialog import com.readrops.app.util.components.dialog.TwoChoicesDialog
import com.readrops.app.util.theme.spacing import com.readrops.app.util.theme.spacing
import com.readrops.db.entities.Feed import com.readrops.db.entities.Feed
@ -70,109 +70,24 @@ object FeedTab : Tab {
val uriHandler = LocalUriHandler.current val uriHandler = LocalUriHandler.current
val context = LocalContext.current val context = LocalContext.current
val viewModel = getScreenModel<FeedScreenModel>() val screenModel = getScreenModel<FeedScreenModel>()
val state by viewModel.feedsState.collectAsStateWithLifecycle() val state by screenModel.feedsState.collectAsStateWithLifecycle()
val snackbarHostState = remember { SnackbarHostState() } val snackbarHostState = remember { SnackbarHostState() }
val topAppBarScrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) val topAppBarScrollBehavior =
TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
LaunchedEffect(state.exception) { LaunchedEffect(state.exception) {
if (state.exception != null) { if (state.exception != null) {
snackbarHostState.showSnackbar(ErrorMessage.get(state.exception!!, context)) snackbarHostState.showSnackbar(ErrorMessage.get(state.exception!!, context))
viewModel.resetException() screenModel.resetException()
} }
} }
when (val dialog = state.dialog) { FeedDialogs(
is DialogState.AddFeed -> { state = state,
AddFeedDialog( screenModel = screenModel
viewModel = viewModel, )
onDismiss = {
viewModel.closeDialog(DialogState.AddFeed)
},
)
}
is DialogState.DeleteFeed -> {
TwoChoicesDialog(
title = stringResource(R.string.delete_feed),
text = "Do you want to delete feed ${dialog.feed.name}?",
icon = rememberVectorPainter(image = Icons.Default.Delete),
confirmText = stringResource(R.string.delete),
dismissText = stringResource(R.string.cancel),
onDismiss = { viewModel.closeDialog() },
onConfirm = {
viewModel.deleteFeed(dialog.feed)
viewModel.closeDialog()
}
)
}
is DialogState.FeedSheet -> {
FeedModalBottomSheet(
feed = dialog.feed,
onDismissRequest = { viewModel.closeDialog() },
onOpen = {
uriHandler.openUri(dialog.feed.siteUrl!!)
viewModel.closeDialog()
},
onUpdate = {
viewModel.openDialog(DialogState.UpdateFeed(dialog.feed, dialog.folder))
},
onUpdateColor = {},
onDelete = { viewModel.openDialog(DialogState.DeleteFeed(dialog.feed)) }
)
}
is DialogState.UpdateFeed -> {
UpdateFeedDialog(
viewModel = viewModel,
onDismissRequest = { viewModel.closeDialog(dialog) }
)
}
DialogState.AddFolder -> {
FolderDialog(
viewModel = viewModel,
onDismiss = {
viewModel.closeDialog(DialogState.AddFolder)
},
onValidate = {
viewModel.folderValidate()
}
)
}
is DialogState.DeleteFolder -> {
TwoChoicesDialog(
title = stringResource(R.string.delete_folder),
text = "Do you want to delete folder ${dialog.folder.name}?",
icon = rememberVectorPainter(image = Icons.Default.Delete),
confirmText = stringResource(R.string.delete),
dismissText = stringResource(R.string.cancel),
onDismiss = { viewModel.closeDialog() },
onConfirm = {
viewModel.deleteFolder(dialog.folder)
viewModel.closeDialog()
}
)
}
is DialogState.UpdateFolder -> {
FolderDialog(
updateFolder = true,
viewModel = viewModel,
onDismiss = {
viewModel.closeDialog(DialogState.UpdateFolder(dialog.folder))
},
onValidate = {
viewModel.folderValidate(updateFolder = true)
}
)
}
null -> {}
}
Scaffold( Scaffold(
topBar = { topBar = {
@ -180,7 +95,7 @@ object FeedTab : Tab {
title = { Text(text = stringResource(R.string.feeds)) }, title = { Text(text = stringResource(R.string.feeds)) },
actions = { actions = {
IconButton( IconButton(
onClick = { viewModel.setFolderExpandState(state.areFoldersExpanded.not()) } onClick = { screenModel.setFolderExpandState(state.areFoldersExpanded.not()) }
) { ) {
Icon( Icon(
painter = painterResource( painter = painterResource(
@ -205,7 +120,7 @@ object FeedTab : Tab {
start = MaterialTheme.spacing.veryShortSpacing, start = MaterialTheme.spacing.veryShortSpacing,
bottom = MaterialTheme.spacing.shortSpacing bottom = MaterialTheme.spacing.shortSpacing
), ),
onClick = { viewModel.openDialog(DialogState.AddFolder) } onClick = { screenModel.openDialog(DialogState.AddFolder) }
) { ) {
Icon( Icon(
painter = painterResource(id = R.drawable.ic_new_folder), painter = painterResource(id = R.drawable.ic_new_folder),
@ -216,7 +131,7 @@ object FeedTab : Tab {
} }
FloatingActionButton( FloatingActionButton(
onClick = { viewModel.openDialog(DialogState.AddFeed) } onClick = { screenModel.openDialog(DialogState.AddFeed) }
) { ) {
Icon( Icon(
imageVector = Icons.Default.Add, imageVector = Icons.Default.Add,
@ -256,18 +171,18 @@ object FeedTab : Tab {
feeds = folderWithFeeds.second, feeds = folderWithFeeds.second,
isExpanded = state.areFoldersExpanded, isExpanded = state.areFoldersExpanded,
onFeedClick = { feed -> onFeedClick = { feed ->
viewModel.openDialog( screenModel.openDialog(
DialogState.FeedSheet(feed, folder) DialogState.FeedSheet(feed, folder)
) )
}, },
onFeedLongClick = { feed -> onFeedLongClick(feed) }, onFeedLongClick = { feed -> onFeedLongClick(feed) },
onUpdateFolder = { onUpdateFolder = {
viewModel.openDialog( screenModel.openDialog(
DialogState.UpdateFolder(folder) DialogState.UpdateFolder(folder)
) )
}, },
onDeleteFolder = { onDeleteFolder = {
viewModel.openDialog( screenModel.openDialog(
DialogState.DeleteFolder(folder) DialogState.DeleteFolder(folder)
) )
} }
@ -279,7 +194,7 @@ object FeedTab : Tab {
FeedItem( FeedItem(
feed = feed, feed = feed,
onClick = { onClick = {
viewModel.openDialog( screenModel.openDialog(
DialogState.FeedSheet(feed, null) DialogState.FeedSheet(feed, null)
) )
}, },
@ -310,4 +225,102 @@ object FeedTab : Tab {
} }
} }
} }
@Composable
private fun FeedDialogs(state: FeedState, screenModel: FeedScreenModel) {
val uriHandler = LocalUriHandler.current
val folderState by screenModel.folderState.collectAsStateWithLifecycle()
when (val dialog = state.dialog) {
is DialogState.AddFeed -> {
AddFeedDialog(
viewModel = screenModel,
onDismiss = {
screenModel.closeDialog(DialogState.AddFeed)
},
)
}
is DialogState.DeleteFeed -> {
TwoChoicesDialog(
title = stringResource(R.string.delete_feed),
text = "Do you want to delete feed ${dialog.feed.name}?",
icon = rememberVectorPainter(image = Icons.Default.Delete),
confirmText = stringResource(R.string.delete),
dismissText = stringResource(R.string.cancel),
onDismiss = { screenModel.closeDialog() },
onConfirm = {
screenModel.deleteFeed(dialog.feed)
screenModel.closeDialog()
}
)
}
is DialogState.FeedSheet -> {
FeedModalBottomSheet(
feed = dialog.feed,
onDismissRequest = { screenModel.closeDialog() },
onOpen = {
uriHandler.openUri(dialog.feed.siteUrl!!)
screenModel.closeDialog()
},
onUpdate = {
screenModel.openDialog(DialogState.UpdateFeed(dialog.feed, dialog.folder))
},
onUpdateColor = {},
onDelete = { screenModel.openDialog(DialogState.DeleteFeed(dialog.feed)) }
)
}
is DialogState.UpdateFeed -> {
UpdateFeedDialog(
viewModel = screenModel,
onDismissRequest = { screenModel.closeDialog(dialog) }
)
}
DialogState.AddFolder -> {
TextFieldDialog(
title = stringResource(id = R.string.add_folder),
icon = painterResource(id = R.drawable.ic_new_folder),
label = stringResource(id = R.string.name),
state = folderState,
onValueChange = { screenModel.setFolderName(it) },
onValidate = { screenModel.folderValidate() },
onDismiss = { screenModel.closeDialog(DialogState.AddFolder) }
)
}
is DialogState.DeleteFolder -> {
TwoChoicesDialog(
title = stringResource(R.string.delete_folder),
text = "Do you want to delete folder ${dialog.folder.name}?",
icon = rememberVectorPainter(image = Icons.Default.Delete),
confirmText = stringResource(R.string.delete),
dismissText = stringResource(R.string.cancel),
onDismiss = { screenModel.closeDialog() },
onConfirm = {
screenModel.deleteFolder(dialog.folder)
screenModel.closeDialog()
}
)
}
is DialogState.UpdateFolder -> {
TextFieldDialog(
title = stringResource(id = R.string.edit_folder),
icon = painterResource(id = R.drawable.ic_folder_grey),
label = stringResource(id = R.string.name),
state = folderState,
onValueChange = { screenModel.setFolderName(it) },
onValidate = { screenModel.folderValidate(updateFolder = true) },
onDismiss = { screenModel.closeDialog(DialogState.UpdateFolder(dialog.folder)) }
)
}
null -> {}
}
}
} }

View File

@ -1,75 +0,0 @@
package com.readrops.app.feeds.dialogs
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.readrops.app.R
import com.readrops.app.feeds.FeedScreenModel
import com.readrops.app.util.ErrorMessage
import com.readrops.app.util.components.dialog.BaseDialog
import com.readrops.app.util.theme.LargeSpacer
@Composable
fun FolderDialog(
updateFolder: Boolean = false,
viewModel: FeedScreenModel,
onDismiss: () -> Unit,
onValidate: () -> Unit
) {
val state by viewModel.folderState.collectAsStateWithLifecycle()
BaseDialog(
title = stringResource(id = if (updateFolder) R.string.edit_folder else R.string.add_folder),
icon = painterResource(id = if (updateFolder) R.drawable.ic_folder_grey else R.drawable.ic_new_folder),
onDismiss = onDismiss
) {
OutlinedTextField(
value = state.name.orEmpty(),
label = {
Text(text = "URL")
},
onValueChange = { viewModel.setFolderName(it) },
singleLine = true,
trailingIcon = {
if (!state.name.isNullOrEmpty()) {
IconButton(
onClick = { viewModel.setFolderName("") }
) {
Icon(
imageVector = Icons.Default.Clear,
contentDescription = null
)
}
}
},
isError = state.isTextFieldError,
supportingText = { Text(text = state.nameError?.errorText().orEmpty()) }
)
if (state.exception != null) {
Text(
text = ErrorMessage.get(state.exception!!, LocalContext.current),
color = MaterialTheme.colorScheme.error
)
}
LargeSpacer()
TextButton(
onClick = { onValidate() },
) {
Text(text = stringResource(R.string.validate))
}
}
}