From caf55451d3baf39c4aa59369a573eaa6bdc8eff6 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Thu, 18 Jan 2024 18:45:34 +0100 Subject: [PATCH] Add UpdateFeedDialog in FeedTab --- .../readrops/app/compose/base/TabViewModel.kt | 6 +- .../readrops/app/compose/feeds/FeedState.kt | 41 +++- .../com/readrops/app/compose/feeds/FeedTab.kt | 11 +- .../app/compose/feeds/FeedViewModel.kt | 180 ++++++++++++++---- .../compose/feeds/dialogs/AddFeedDialog.kt | 6 +- .../compose/feeds/dialogs/FeedBottomSheet.kt | 6 +- .../compose/feeds/dialogs/UpdateFeedDialog.kt | 161 ++++++++++++++++ .../repositories/GetFoldersWithFeeds.kt | 3 +- .../app/compose/timelime/TimelineViewModel.kt | 3 +- .../readrops/db/dao/newdao/NewFolderDao.kt | 5 +- .../com/readrops/db/pojo/FeedWithFolder.kt | 1 + 11 files changed, 363 insertions(+), 60 deletions(-) create mode 100644 appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/UpdateFeedDialog.kt diff --git a/appcompose/src/main/java/com/readrops/app/compose/base/TabViewModel.kt b/appcompose/src/main/java/com/readrops/app/compose/base/TabViewModel.kt index 22a24f99..8d8a2fd9 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/base/TabViewModel.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/base/TabViewModel.kt @@ -5,7 +5,7 @@ import androidx.lifecycle.viewModelScope import com.readrops.app.compose.repositories.BaseRepository import com.readrops.db.Database import com.readrops.db.entities.account.Account -import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent @@ -26,7 +26,7 @@ abstract class TabViewModel( protected var currentAccount: Account? = null - protected val accountEvent = Channel() + protected val accountEvent = MutableSharedFlow() init { viewModelScope.launch { @@ -38,7 +38,7 @@ abstract class TabViewModel( currentAccount = account repository = get(parameters = { parametersOf(account) }) - accountEvent.send(account) + accountEvent.emit(account) } } } diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedState.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedState.kt index 64236cfe..713480ac 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedState.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedState.kt @@ -3,6 +3,7 @@ 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 +import com.readrops.db.entities.account.AccountType data class FeedState( val foldersAndFeeds: FolderAndFeedsState = FolderAndFeedsState.InitialState, @@ -14,7 +15,7 @@ sealed interface DialogState { object AddFolder : DialogState class DeleteFeed(val feed: Feed) : DialogState class DeleteFolder(val folder: Folder) : DialogState - class UpdateFeed(val feed: Feed) : DialogState + class UpdateFeed(val feed: Feed, val folder: Folder?) : DialogState class UpdateFolder(val folder: Folder) : DialogState class FeedSheet(val feed: Feed, val folder: Folder?) : DialogState } @@ -30,9 +31,8 @@ data class AddFeedDialogState( val selectedAccount: Account = Account(accountName = ""), val accounts: List = listOf(), val error: AddFeedError? = null, - val closeDialog: Boolean = false, ) { - fun isError() = error != null + val isError: Boolean get() = error != null val errorText: String get() = when (error) { @@ -51,4 +51,39 @@ data class AddFeedDialogState( object NoRSSFeed : AddFeedError() object NoConnection : AddFeedError() } +} + +data class UpdateFeedDialogState( + val feedName: String = "", + val feedNameError: Error? = null, + val feedUrl: String = "", + val feedUrlError: Error? = null, + val accountType: AccountType? = null, + val selectedFolder: Folder? = null, + val folders: List = listOf(), + val isAccountDropDownExpanded: Boolean = false, +) { + + sealed class Error { + object EmptyField : Error() + object BadUrl : Error() + object NoRSSUrl : Error() + } + + val isFeedNameError + get() = feedNameError != null + + val isFeedUrlError + get() = feedUrlError != null + + fun errorText(error: Error?): String = when (error) { + Error.BadUrl -> "Input is not a valid URL" + Error.EmptyField -> "Field can't be empty" + Error.NoRSSUrl -> "The provided URL is not a valid RSS feed" + else -> "" + } + + val isFeedUrlReadOnly: Boolean + get() = accountType != null && !accountType.accountConfig!!.isFeedUrlEditable + } \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt index d87be57a..02fc6fda 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt @@ -31,6 +31,7 @@ import com.readrops.app.compose.R import com.readrops.app.compose.feeds.dialogs.AddFeedDialog import com.readrops.app.compose.feeds.dialogs.DeleteFeedDialog import com.readrops.app.compose.feeds.dialogs.FeedModalBottomSheet +import com.readrops.app.compose.feeds.dialogs.UpdateFeedDialog import com.readrops.app.compose.util.components.Placeholder import com.readrops.db.entities.Feed import org.koin.androidx.compose.getViewModel @@ -84,13 +85,19 @@ object FeedTab : Tab { uriHandler.openUri(dialog.feed.siteUrl!!) viewModel.closeDialog() }, - onModify = { }, + onUpdate = { viewModel.openDialog(DialogState.UpdateFeed(dialog.feed, dialog.folder)) }, onUpdateColor = {}, onDelete = { viewModel.openDialog(DialogState.DeleteFeed(dialog.feed)) }, ) } - is DialogState.UpdateFeed -> {} + is DialogState.UpdateFeed -> { + UpdateFeedDialog( + viewModel = viewModel, + onDismissRequest = { viewModel.closeDialog() } + ) + } + DialogState.AddFolder -> {} is DialogState.DeleteFolder -> {} is DialogState.UpdateFolder -> {} diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt index 71f2f735..e7e64132 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt @@ -8,12 +8,12 @@ import com.readrops.app.compose.base.TabViewModel import com.readrops.app.compose.repositories.GetFoldersWithFeeds import com.readrops.db.Database import com.readrops.db.entities.Feed +import com.readrops.db.entities.Folder import com.readrops.db.entities.account.Account import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.consumeAsFlow import kotlinx.coroutines.flow.flatMapConcat import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -32,9 +32,12 @@ class FeedViewModel( private val _addFeedDialogState = MutableStateFlow(AddFeedDialogState()) val addFeedDialogState = _addFeedDialogState.asStateFlow() + private val _updateFeedDialogState = MutableStateFlow(UpdateFeedDialogState()) + val updateFeedDialogState = _updateFeedDialogState.asStateFlow() + init { viewModelScope.launch(context = Dispatchers.IO) { - accountEvent.consumeAsFlow() + accountEvent .flatMapConcat { account -> getFoldersWithFeeds.get(account.id) } @@ -62,11 +65,39 @@ class FeedViewModel( } } } + + viewModelScope.launch(context = Dispatchers.IO) { + accountEvent + .flatMapConcat { account -> + database.newFolderDao() + .selectFolders(account.id) + } + .collect { folders -> + _updateFeedDialogState.update { + it.copy( + folders = folders, + accountType = currentAccount!!.accountType + ) + } + } + } } fun closeDialog() = _feedState.update { it.copy(dialog = null) } - fun openDialog(state: DialogState) = _feedState.update { it.copy(dialog = state) } + fun openDialog(state: DialogState) { + if (state is DialogState.UpdateFeed) { + _updateFeedDialogState.update { + it.copy( + feedName = state.feed.name!!, + feedUrl = state.feed.url!!, + selectedFolder = state.folder + ) + } + } + + _feedState.update { it.copy(dialog = state) } + } fun deleteFeed(feed: Feed) { viewModelScope.launch(Dispatchers.IO) { @@ -74,6 +105,8 @@ class FeedViewModel( } } + // Add feed + fun setAddFeedDialogURL(url: String) { _addFeedDialogState.update { it.copy( @@ -94,47 +127,41 @@ class FeedViewModel( fun addFeedDialogValidate() { val url = _addFeedDialogState.value.url - if (url.isEmpty()) { - _addFeedDialogState.update { - it.copy( - error = AddFeedDialogState.AddFeedError.EmptyUrl - ) - } - - return - } else if (!Patterns.WEB_URL.matcher(url).matches()) { - _addFeedDialogState.update { - it.copy( - error = AddFeedDialogState.AddFeedError.BadUrl - ) - } - - return - } - - viewModelScope.launch(Dispatchers.IO) { - if (localRSSDataSource.isUrlRSSResource(url)) { - // TODO add support for all account types - repository?.insertNewFeeds(listOf(url)) - + when { + url.isEmpty() -> { _addFeedDialogState.update { - it.copy(closeDialog = true) + it.copy(error = AddFeedDialogState.AddFeedError.EmptyUrl) } - } else { - val rssUrls = HtmlParser.getFeedLink(url, get()) - if (rssUrls.isEmpty()) { - _addFeedDialogState.update { - it.copy( - error = AddFeedDialogState.AddFeedError.NoRSSFeed - ) - } - } else { + return + } + + !Patterns.WEB_URL.matcher(url).matches() -> { + _addFeedDialogState.update { + it.copy(error = AddFeedDialogState.AddFeedError.BadUrl) + } + + return + } + + else -> viewModelScope.launch(Dispatchers.IO) { + if (localRSSDataSource.isUrlRSSResource(url)) { // TODO add support for all account types - repository?.insertNewFeeds(rssUrls.map { it.url }) + repository?.insertNewFeeds(listOf(url)) - _addFeedDialogState.update { - it.copy(closeDialog = true) + closeDialog() + } else { + val rssUrls = HtmlParser.getFeedLink(url, get()) + + if (rssUrls.isEmpty()) { + _addFeedDialogState.update { + it.copy(error = AddFeedDialogState.AddFeedError.NoRSSFeed) + } + } else { + // TODO add support for all account types + repository?.insertNewFeeds(rssUrls.map { it.url }) + + closeDialog() } } } @@ -146,9 +173,82 @@ class FeedViewModel( it.copy( url = "", error = null, - closeDialog = false ) } } + + // add feed + + // update feed + + fun setAccountDropDownState(isExpanded: Boolean) { + _updateFeedDialogState.update { + it.copy(isAccountDropDownExpanded = isExpanded) + } + } + + fun setSelectedFolder(folder: Folder) { + _updateFeedDialogState.update { + it.copy(selectedFolder = folder) + } + } + + fun setUpdateFeedDialogStateFeedName(feedName: String) { + _updateFeedDialogState.update { + it.copy( + feedName = feedName, + feedNameError = null, + ) + } + } + + fun setUpdateFeedDialogFeedUrl(feedUrl: String) { + _updateFeedDialogState.update { + it.copy( + feedUrl = feedUrl, + feedUrlError = null, + ) + } + } + + fun updateFeedDialogValidate() { + val feedName = _updateFeedDialogState.value.feedName + val feedUrl = _updateFeedDialogState.value.feedUrl + + when { + feedName.isEmpty() -> { + _updateFeedDialogState.update { + it.copy(feedNameError = UpdateFeedDialogState.Error.EmptyField) + } + return + } + + feedUrl.isEmpty() -> { + _updateFeedDialogState.update { + it.copy(feedUrlError = UpdateFeedDialogState.Error.EmptyField) + } + return + } + + !Patterns.WEB_URL.matcher(feedUrl).matches() -> { + _updateFeedDialogState.update { + it.copy(feedUrlError = UpdateFeedDialogState.Error.BadUrl) + } + return + } + + else -> { + viewModelScope.launch(Dispatchers.IO) { + // TODO add logig to update feed + //repository?.updateFeed() + closeDialog() + } + } + } + + } + + + // update feed } diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/AddFeedDialog.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/AddFeedDialog.kt index ef67a381..e9c0d564 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/AddFeedDialog.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/AddFeedDialog.kt @@ -48,10 +48,6 @@ fun AddFeedDialog( var isExpanded by remember { mutableStateOf(false) } - if (state.closeDialog) { - onDismiss() - } - Dialog( onDismissRequest = onDismiss ) { @@ -100,7 +96,7 @@ fun AddFeedDialog( } } }, - isError = state.isError(), + isError = state.isError, supportingText = { Text(state.errorText) } ) diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/FeedBottomSheet.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/FeedBottomSheet.kt index bb6b34d4..e99b5bbe 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/FeedBottomSheet.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/FeedBottomSheet.kt @@ -39,7 +39,7 @@ fun FeedModalBottomSheet( folder: Folder?, onDismissRequest: () -> Unit, onOpen: () -> Unit, - onModify: () -> Unit, + onUpdate: () -> Unit, onUpdateColor: () -> Unit, onDelete: () -> Unit, ) { @@ -98,9 +98,9 @@ fun FeedModalBottomSheet( ) BottomSheetOption( - text = "Modify", + text = "Update", icon = Icons.Default.Create, - onClick = onModify + onClick = onUpdate ) BottomSheetOption( diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/UpdateFeedDialog.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/UpdateFeedDialog.kt new file mode 100644 index 00000000..390925cb --- /dev/null +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/UpdateFeedDialog.kt @@ -0,0 +1,161 @@ +package com.readrops.app.compose.feeds.dialogs + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.ExposedDropdownMenuDefaults +import androidx.compose.material3.Icon +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.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.readrops.app.compose.R +import com.readrops.app.compose.feeds.FeedViewModel +import com.readrops.app.compose.util.theme.LargeSpacer +import com.readrops.app.compose.util.theme.MediumSpacer +import com.readrops.app.compose.util.theme.spacing + + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun UpdateFeedDialog( + viewModel: FeedViewModel, + onDismissRequest: () -> Unit +) { + val state by viewModel.updateFeedDialogState.collectAsStateWithLifecycle() + + Dialog( + onDismissRequest = onDismissRequest + ) { + Card( + shape = RoundedCornerShape(16.dp) + ) { + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.background) + .padding(MaterialTheme.spacing.largeSpacing) + ) { + Icon( + painter = painterResource(id = R.drawable.ic_rss_feed_grey), + contentDescription = null, + modifier = Modifier.size(MaterialTheme.spacing.largeSpacing) + ) + + MediumSpacer() + + Text( + text = "Update feed", + style = MaterialTheme.typography.headlineSmall + ) + + MediumSpacer() + + OutlinedTextField( + value = state.feedName, + onValueChange = { viewModel.setUpdateFeedDialogStateFeedName(it) }, + label = { Text(text = "Feed name") }, + singleLine = true, + isError = state.isFeedNameError, + supportingText = { + if (state.isFeedNameError) { + Text( + text = state.errorText(state.feedNameError) + ) + } + } + ) + + MediumSpacer() + + OutlinedTextField( + value = state.feedUrl, + onValueChange = { viewModel.setUpdateFeedDialogFeedUrl(it) }, + label = { Text(text = "Feed URL") }, + singleLine = true, + readOnly = state.isFeedUrlReadOnly, + isError = state.isFeedUrlError, + supportingText = { + if (state.isFeedUrlError) { + Text( + text = state.errorText(state.feedUrlError) + ) + } + } + ) + + MediumSpacer() + + ExposedDropdownMenuBox( + expanded = state.isAccountDropDownExpanded, + onExpandedChange = { viewModel.setAccountDropDownState(state.isAccountDropDownExpanded.not()) } + ) { + ExposedDropdownMenu( + expanded = state.isAccountDropDownExpanded, + onDismissRequest = { viewModel.setAccountDropDownState(false) } + ) { + for (folder in state.folders) { + DropdownMenuItem( + text = { Text(text = folder.name!!) }, + onClick = { + viewModel.setSelectedFolder(folder) + viewModel.setAccountDropDownState(false) + }, + leadingIcon = { + Icon( + painterResource(id = R.drawable.ic_folder_grey), + contentDescription = null, + ) + } + ) + } + } + + OutlinedTextField( + value = state.selectedFolder?.name.orEmpty(), + readOnly = true, + onValueChange = {}, + trailingIcon = { + ExposedDropdownMenuDefaults.TrailingIcon(expanded = state.isAccountDropDownExpanded) + }, + leadingIcon = { + if (state.selectedFolder != null) { + Icon( + painterResource(id = R.drawable.ic_folder_grey), + contentDescription = null, + ) + } + }, + modifier = Modifier.menuAnchor() + ) + } + + LargeSpacer() + + TextButton( + onClick = { viewModel.updateFeedDialogValidate() }, + ) { + Text(text = "Validate") + } + } + } + } +} diff --git a/appcompose/src/main/java/com/readrops/app/compose/repositories/GetFoldersWithFeeds.kt b/appcompose/src/main/java/com/readrops/app/compose/repositories/GetFoldersWithFeeds.kt index 72154f4f..5c69775d 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/repositories/GetFoldersWithFeeds.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/repositories/GetFoldersWithFeeds.kt @@ -24,7 +24,8 @@ class GetFoldersWithFeeds( id = it.feedId, name = it.feedName, iconUrl = it.feedIcon, - siteUrl = it.feedUrl, + url = it.feedUrl, + siteUrl = it.feedSiteUrl, unreadCount = it.unreadCount ) } diff --git a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt index c0b05173..4d4b06c6 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt @@ -25,7 +25,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.consumeAsFlow import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -44,7 +43,7 @@ class TimelineViewModel( init { viewModelScope.launch(dispatcher) { combine( - accountEvent.consumeAsFlow(), + accountEvent, filters ) { account, filters -> filters.accountId = account.id diff --git a/db/src/main/java/com/readrops/db/dao/newdao/NewFolderDao.kt b/db/src/main/java/com/readrops/db/dao/newdao/NewFolderDao.kt index fe81283b..ae5eaec7 100644 --- a/db/src/main/java/com/readrops/db/dao/newdao/NewFolderDao.kt +++ b/db/src/main/java/com/readrops/db/dao/newdao/NewFolderDao.kt @@ -10,9 +10,12 @@ import kotlinx.coroutines.flow.Flow abstract class NewFolderDao : NewBaseDao { @Query("Select Folder.id As folderId, Folder.name as folderName, Feed.id As feedId, Feed.name AS feedName, " + - "Feed.icon_url As feedIcon, Feed.siteUrl as feedUrl, count(*) As unreadCount From Folder Left Join Feed " + + "Feed.icon_url As feedIcon, Feed.url as feedUrl, Feed.siteUrl as feedSiteUrl, count(*) As unreadCount From Folder Left Join Feed " + "On Folder.id = Feed.folder_id Left Join Item On Item.feed_id = Feed.id " + "Where Feed.folder_id is NULL OR Feed.folder_id is NOT NULL And Item.read = 0 " + "And Feed.account_id = :accountId Group By Feed.id, Folder.id Order By Folder.id") abstract fun selectFoldersAndFeeds(accountId: Int): Flow> + + @Query("Select * From Folder Where account_id = :accountId") + abstract fun selectFolders(accountId: Int): Flow> } \ No newline at end of file diff --git a/db/src/main/java/com/readrops/db/pojo/FeedWithFolder.kt b/db/src/main/java/com/readrops/db/pojo/FeedWithFolder.kt index ea84c72d..d71c5318 100644 --- a/db/src/main/java/com/readrops/db/pojo/FeedWithFolder.kt +++ b/db/src/main/java/com/readrops/db/pojo/FeedWithFolder.kt @@ -19,6 +19,7 @@ data class FolderWithFeed( val feedName: String? = null, val feedIcon: String? = null, val feedUrl: String? = null, + val feedSiteUrl: String? = null, val unreadCount: Int = 0 )