Add update and delete folder dialogs

This commit is contained in:
Shinokuni 2024-02-11 17:51:58 +01:00
parent a3f78094f1
commit 6f01333065
9 changed files with 158 additions and 55 deletions

View File

@ -56,9 +56,11 @@ data class UpdateFeedDialogState(
get() = accountType != null && !accountType.accountConfig!!.isFeedUrlEditable get() = accountType != null && !accountType.accountConfig!!.isFeedUrlEditable
} }
data class AddFolderState( data class AddUpdateFolderState(
val name: String = "", val folder: Folder = Folder(),
val nameError: TextFieldError? = null, val nameError: TextFieldError? = null,
) { ) {
val name = folder.name
val isError = nameError != null val isError = nameError != null
} }

View File

@ -9,6 +9,7 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.FloatingActionButton
@ -21,6 +22,7 @@ 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.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
@ -31,7 +33,7 @@ import cafe.adriel.voyager.navigator.tab.Tab
import cafe.adriel.voyager.navigator.tab.TabOptions import cafe.adriel.voyager.navigator.tab.TabOptions
import com.readrops.app.compose.R import com.readrops.app.compose.R
import com.readrops.app.compose.feeds.dialogs.AddFeedDialog import com.readrops.app.compose.feeds.dialogs.AddFeedDialog
import com.readrops.app.compose.feeds.dialogs.AddFolderDialog import com.readrops.app.compose.feeds.dialogs.AddUpdateFolderDialog
import com.readrops.app.compose.feeds.dialogs.DeleteFeedDialog import com.readrops.app.compose.feeds.dialogs.DeleteFeedDialog
import com.readrops.app.compose.feeds.dialogs.FeedModalBottomSheet import com.readrops.app.compose.feeds.dialogs.FeedModalBottomSheet
import com.readrops.app.compose.feeds.dialogs.UpdateFeedDialog import com.readrops.app.compose.feeds.dialogs.UpdateFeedDialog
@ -110,27 +112,54 @@ object FeedTab : Tab {
} }
DialogState.AddFolder -> { DialogState.AddFolder -> {
AddFolderDialog( AddUpdateFolderDialog(
viewModel = viewModel, viewModel = viewModel,
onDismiss = { onDismiss = {
viewModel.closeDialog() viewModel.closeDialog()
viewModel.resetAddFolderState() viewModel.resetAddFolderState()
},
onValidate = {
viewModel.addFolderValidate()
} }
)
}
is DialogState.DeleteFolder -> {
TwoChoicesDialog(
title = "Delete folder",
text = "Do you want to delete folder ${dialog.folder.name}?",
icon = rememberVectorPainter(image = Icons.Default.Delete),
confirmText = "Delete",
dismissText = "Cancel",
onDismiss = { viewModel.closeDialog() },
onConfirm = {
viewModel.deleteFolder(dialog.folder)
viewModel.closeDialog()
}
) )
} }
is DialogState.DeleteFolder -> {} is DialogState.DeleteFolder -> {}
is DialogState.UpdateFolder -> {} AddUpdateFolderDialog(
updateFolder = true,
viewModel = viewModel,
onDismiss = {
viewModel.closeDialog()
viewModel.resetAddFolderState()
},
onValidate = {
viewModel.updateFolderValidate()
}
)
}
null -> {} null -> {}
} }
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( TopAppBar(
title = { title = { Text(text = "Feeds") },
Text(text = "Feeds")
},
actions = { actions = {
IconButton( IconButton(
onClick = {} onClick = {}
@ -187,25 +216,33 @@ object FeedTab : Tab {
items( items(
items = foldersAndFeeds.toList() items = foldersAndFeeds.toList()
) { folderWithFeeds -> ) { folderWithFeeds ->
fun onFeedLongClick(feed: Feed) { fun onFeedLongClick(feed: Feed) {
haptic.performHapticFeedback(HapticFeedbackType.LongPress) haptic.performHapticFeedback(HapticFeedbackType.LongPress)
uriHandler.openUri(feed.siteUrl!!) uriHandler.openUri(feed.siteUrl!!)
} }
if (folderWithFeeds.first != null) { if (folderWithFeeds.first != null) {
val folder = folderWithFeeds.first!!
FolderExpandableItem( FolderExpandableItem(
folder = folderWithFeeds.first!!, folder = folder,
feeds = folderWithFeeds.second, feeds = folderWithFeeds.second,
onFeedClick = { feed -> onFeedClick = { feed ->
viewModel.openDialog( viewModel.openDialog(
DialogState.FeedSheet( DialogState.FeedSheet(feed, folder)
feed,
folderWithFeeds.first
)
) )
}, },
onFeedLongClick = { feed -> onFeedLongClick(feed) } onFeedLongClick = { feed -> onFeedLongClick(feed) },
onUpdateFolder = {
viewModel.openDialog(
DialogState.UpdateFolder(folder)
)
},
onDeleteFolder = {
viewModel.openDialog(
DialogState.DeleteFolder(folder)
)
}
) )
} else { } else {
val feeds = folderWithFeeds.second val feeds = folderWithFeeds.second

View File

@ -36,7 +36,7 @@ class FeedViewModel(
private val _updateFeedDialogState = MutableStateFlow(UpdateFeedDialogState()) private val _updateFeedDialogState = MutableStateFlow(UpdateFeedDialogState())
val updateFeedDialogState = _updateFeedDialogState.asStateFlow() val updateFeedDialogState = _updateFeedDialogState.asStateFlow()
private val _addFolderState = MutableStateFlow(AddFolderState()) private val _addFolderState = MutableStateFlow(AddUpdateFolderState())
val addFolderState = _addFolderState.asStateFlow() val addFolderState = _addFolderState.asStateFlow()
init { init {
@ -100,6 +100,14 @@ class FeedViewModel(
} }
} }
if (state is DialogState.UpdateFolder) {
_addFolderState.update {
it.copy(
folder = state.folder
)
}
}
_feedState.update { it.copy(dialog = state) } _feedState.update { it.copy(dialog = state) }
} }
@ -109,6 +117,12 @@ class FeedViewModel(
} }
} }
fun deleteFolder(folder: Folder) {
viewModelScope.launch(Dispatchers.IO) {
repository?.deleteFolder(folder)
}
}
// Add feed // Add feed
fun setAddFeedDialogURL(url: String) { fun setAddFeedDialogURL(url: String) {
@ -257,13 +271,43 @@ class FeedViewModel(
fun setFolderName(name: String) = _addFolderState.update { fun setFolderName(name: String) = _addFolderState.update {
it.copy( it.copy(
name = name, folder = it.folder.copy(name = name),
nameError = null, nameError = null,
) )
} }
fun addFolderValidate() { fun addFolderValidate() {
val name = _addFolderState.value.name val name = _addFolderState.value.name.orEmpty()
if (name.isEmpty()) {
_addFolderState.update { it.copy(nameError = TextFieldError.EmptyField) }
return
}
viewModelScope.launch(Dispatchers.IO) {
repository?.addFolder(_addFolderState.value.folder.apply { accountId = currentAccount!!.id })
closeDialog()
resetAddFolderState()
}
}
fun resetAddFolderState() {
_addFolderState.update {
it.copy(
folder = Folder(),
nameError = null,
)
}
}
// add folder
// update folder
fun updateFolderValidate() {
val name = _addFolderState.value.name.orEmpty()
if (name.isEmpty()) { if (name.isEmpty()) {
_addFolderState.update { _addFolderState.update {
@ -274,22 +318,13 @@ class FeedViewModel(
} }
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
repository?.addFolder(Folder(name = name, accountId = currentAccount?.id!!)) repository?.updateFolder(_addFolderState.value.folder)
closeDialog() closeDialog()
resetAddFolderState() resetAddFolderState()
} }
} }
fun resetAddFolderState() { // update folder
_addFolderState.update {
it.copy(
name = "",
nameError = null,
)
}
}
// add folder
} }

View File

@ -2,17 +2,20 @@ package com.readrops.app.compose.feeds
import androidx.compose.animation.animateContentSize import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.LinearOutSlowInEasing import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDropDown import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -22,7 +25,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue 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.draw.rotate
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import com.readrops.app.compose.R import com.readrops.app.compose.R
@ -37,12 +39,11 @@ fun FolderExpandableItem(
feeds: List<Feed>, feeds: List<Feed>,
onFeedClick: (Feed) -> Unit, onFeedClick: (Feed) -> Unit,
onFeedLongClick: (Feed) -> Unit, onFeedLongClick: (Feed) -> Unit,
onUpdateFolder: () -> Unit,
onDeleteFolder: () -> Unit
) { ) {
var isExpanded by remember { mutableStateOf(false) } var isExpanded by remember { mutableStateOf(false) }
val rotationState by animateFloatAsState( var isDropDownMenuExpanded by remember { mutableStateOf(false) }
targetValue = if (isExpanded) 180f else 0f,
label = "folder item arrow rotation"
)
Column( Column(
modifier = Modifier modifier = Modifier
@ -85,13 +86,36 @@ fun FolderExpandableItem(
) )
} }
Row { Box {
Icon( IconButton(
imageVector = Icons.Default.ArrowDropDown, onClick = { isDropDownMenuExpanded = isDropDownMenuExpanded.not() }
contentDescription = null, ) {
modifier = Modifier Icon(
.rotate(rotationState) imageVector = Icons.Default.MoreVert,
) contentDescription = null,
)
}
DropdownMenu(
expanded = isDropDownMenuExpanded,
onDismissRequest = { isDropDownMenuExpanded = false }
) {
DropdownMenuItem(
text = { Text(text = "Update") },
onClick = {
isDropDownMenuExpanded = false
onUpdateFolder()
}
)
DropdownMenuItem(
text = { Text(text = "Delete") },
onClick = {
isDropDownMenuExpanded = false
onDeleteFolder()
}
)
}
} }
} }
} }

View File

@ -15,27 +15,29 @@ import com.readrops.app.compose.feeds.FeedViewModel
import com.readrops.app.compose.util.components.BaseDialog import com.readrops.app.compose.util.components.BaseDialog
@Composable @Composable
fun AddFolderDialog( fun AddUpdateFolderDialog(
updateFolder: Boolean = false,
viewModel: FeedViewModel, viewModel: FeedViewModel,
onDismiss: () -> Unit onDismiss: () -> Unit,
onValidate: () -> Unit
) { ) {
val state by viewModel.addFolderState.collectAsStateWithLifecycle() val state by viewModel.addFolderState.collectAsStateWithLifecycle()
BaseDialog( BaseDialog(
title = "Add Folder", title = if (updateFolder) "Update Folder" else "Add Folder",
icon = painterResource(id = R.drawable.ic_new_folder), icon = painterResource(id = if (updateFolder) R.drawable.ic_folder_grey else R.drawable.ic_new_folder),
onDismiss = { onDismiss() }, onDismiss = onDismiss,
onValidate = { viewModel.addFolderValidate() } onValidate = onValidate
) { ) {
OutlinedTextField( OutlinedTextField(
value = state.name, value = state.name.orEmpty(),
label = { label = {
Text(text = "URL") Text(text = "URL")
}, },
onValueChange = { viewModel.setFolderName(it) }, onValueChange = { viewModel.setFolderName(it) },
singleLine = true, singleLine = true,
trailingIcon = { trailingIcon = {
if (state.name.isNotEmpty()) { if (!state.name.isNullOrEmpty()) {
IconButton( IconButton(
onClick = { viewModel.setFolderName("") } onClick = { viewModel.setFolderName("") }
) { ) {

View File

@ -53,6 +53,8 @@ abstract class BaseRepository(
open suspend fun addFolder(folder: Folder) = database.newFolderDao().insert(folder) open suspend fun addFolder(folder: Folder) = database.newFolderDao().insert(folder)
open suspend fun updateFolder(folder: Folder) = database.newFolderDao().update(folder)
open suspend fun deleteFolder(folder: Folder) = database.newFolderDao().delete(folder) open suspend fun deleteFolder(folder: Folder) = database.newFolderDao().delete(folder)
open suspend fun setItemReadState(item: Item) { open suspend fun setItemReadState(item: Item) {

View File

@ -18,7 +18,7 @@ class GetFoldersWithFeeds(
.selectFeedsWithoutFolder(accountId) .selectFeedsWithoutFolder(accountId)
) { folders, feedsWithoutFolder -> ) { folders, feedsWithoutFolder ->
val foldersWithFeeds = folders.groupBy( val foldersWithFeeds = folders.groupBy(
keySelector = { Folder(id = it.folderId, name = it.folderName) }, keySelector = { Folder(id = it.folderId, name = it.folderName, accountId = it.accountId) },
valueTransform = { valueTransform = {
Feed( Feed(
id = it.feedId, id = it.feedId,

View File

@ -10,8 +10,8 @@ import kotlinx.coroutines.flow.Flow
abstract class NewFolderDao : NewBaseDao<Folder> { abstract class NewFolderDao : NewBaseDao<Folder> {
@Query("Select Folder.id As folderId, Folder.name as folderName, Feed.id As feedId, Feed.name AS feedName, " + @Query("Select Folder.id As folderId, Folder.name as folderName, Feed.id As feedId, Feed.name AS feedName, " +
"Feed.icon_url As feedIcon, Feed.url as feedUrl, Feed.siteUrl as feedSiteUrl, count(*) As unreadCount From Folder Left Join Feed " + "Feed.icon_url As feedIcon, Feed.url as feedUrl, Feed.siteUrl as feedSiteUrl, count(*) As unreadCount, Folder.account_id as accountId " +
"On Folder.id = Feed.folder_id Left Join Item On Item.feed_id = Feed.id " + "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 " + "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") "And Feed.account_id = :accountId Group By Feed.id, Folder.id Order By Folder.id")
abstract fun selectFoldersAndFeeds(accountId: Int): Flow<List<FolderWithFeed>> abstract fun selectFoldersAndFeeds(accountId: Int): Flow<List<FolderWithFeed>>

View File

@ -20,7 +20,8 @@ data class FolderWithFeed(
val feedIcon: String? = null, val feedIcon: String? = null,
val feedUrl: String? = null, val feedUrl: String? = null,
val feedSiteUrl: String? = null, val feedSiteUrl: String? = null,
val unreadCount: Int = 0 val unreadCount: Int = 0,
val accountId: Int = 0
) )
data class FeedWithCount( data class FeedWithCount(