Add rename account option in AccountTab only for local accounts

This commit is contained in:
Shinokuni 2024-07-10 12:47:16 +02:00
parent 3109fed97e
commit feeed2771f
19 changed files with 288 additions and 128 deletions

View File

@ -9,6 +9,8 @@ import com.readrops.api.opml.OPMLParser
import com.readrops.app.base.TabScreenModel
import com.readrops.app.repositories.ErrorResult
import com.readrops.app.repositories.GetFoldersWithFeeds
import com.readrops.app.util.components.TextFieldError
import com.readrops.app.util.components.dialog.TextFieldDialogState
import com.readrops.db.Database
import com.readrops.db.entities.Feed
import com.readrops.db.entities.Folder
@ -56,7 +58,13 @@ class AccountScreenModel(
}
}
fun openDialog(dialog: DialogState) = _accountState.update { it.copy(dialog = dialog) }
fun openDialog(dialog: DialogState) {
if (dialog is DialogState.RenameAccount) {
_accountState.update { it.copy(renameAccountState = TextFieldDialogState(value = dialog.name)) }
}
_accountState.update { it.copy(dialog = dialog) }
}
fun closeDialog(dialog: DialogState? = null) {
if (dialog is DialogState.ErrorList) {
@ -82,7 +90,7 @@ class AccountScreenModel(
}
fun exportOPMLFile(uri: Uri, context: Context) {
screenModelScope.launch {
screenModelScope.launch(dispatcher) {
val stream = context.contentResolver.openOutputStream(uri)
if (stream == null) {
_accountState.update { it.copy(error = NoSuchFileException(uri.toFile())) }
@ -179,6 +187,27 @@ class AccountScreenModel(
database.accountDao().insert(account)
}
}
fun setAccountRenameStateName(name: String) = _accountState.update {
it.copy(
renameAccountState = it.renameAccountState.copy(
value = name,
textFieldError = null
)
)
}
fun renameAccount() = with(_accountState) {
if (value.renameAccountState.value.isEmpty()) {
update { it.copy(renameAccountState = it.renameAccountState.copy(textFieldError = TextFieldError.EmptyField)) }
return@with
}
screenModelScope.launch(dispatcher) {
database.accountDao().renameAccount(value.account.id, value.renameAccountState.value)
closeDialog()
}
}
}
@Stable
@ -189,17 +218,20 @@ data class AccountState(
val error: Exception? = null,
val opmlExportSuccess: Boolean = false,
val opmlExportUri: Uri? = null,
val accounts: List<Account> = emptyList()
val accounts: List<Account> = emptyList(),
val renameAccountState: TextFieldDialogState = TextFieldDialogState()
)
sealed interface DialogState {
object DeleteAccount : DialogState
object NewAccount : DialogState
data object DeleteAccount : DialogState
data object NewAccount : DialogState
data class OPMLImport(val currentFeed: String, val feedCount: Int, val feedMax: Int) :
DialogState
data class ErrorList(val errorResult: ErrorResult) : DialogState
data class Error(val exception: Exception) : DialogState
object OPMLChoice : DialogState
data object OPMLChoice : DialogState
data class RenameAccount(val name: String) : DialogState
}

View File

@ -4,6 +4,7 @@ import android.content.Intent
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
@ -51,10 +52,12 @@ import com.readrops.app.account.selection.AccountSelectionScreen
import com.readrops.app.account.selection.adaptiveIconPainterResource
import com.readrops.app.notifications.NotificationsScreen
import com.readrops.app.timelime.ErrorListDialog
import com.readrops.app.util.components.ErrorDialog
import com.readrops.app.util.components.SelectableIconText
import com.readrops.app.util.components.SelectableImageText
import com.readrops.app.util.components.TwoChoicesDialog
import com.readrops.app.util.components.ThreeDotsMenu
import com.readrops.app.util.components.dialog.TwoChoicesDialog
import com.readrops.app.util.components.dialog.ErrorDialog
import com.readrops.app.util.components.dialog.TextFieldDialog
import com.readrops.app.util.theme.LargeSpacer
import com.readrops.app.util.theme.MediumSpacer
import com.readrops.app.util.theme.VeryShortSpacer
@ -88,16 +91,6 @@ object AccountTab : Tab {
screenModel.resetCloseHome()
}
val opmlImportLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
uri?.let { screenModel.parseOPMLFile(uri, context) }
}
val opmlExportLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("text/xml")) { uri ->
uri?.let { screenModel.exportOPMLFile(uri, context) }
}
LaunchedEffect(state.error) {
if (state.error != null) {
val action = snackbarHostState.showSnackbar(
@ -158,87 +151,10 @@ object AccountTab : Tab {
}
}
when (val dialog = state.dialog) {
is DialogState.DeleteAccount -> {
TwoChoicesDialog(
title = stringResource(R.string.delete_account),
text = stringResource(R.string.delete_account_question),
icon = rememberVectorPainter(image = Icons.Default.Delete),
confirmText = stringResource(R.string.delete),
dismissText = stringResource(R.string.cancel),
onDismiss = { screenModel.closeDialog() },
onConfirm = {
screenModel.closeDialog()
screenModel.deleteAccount()
}
)
}
is DialogState.NewAccount -> {
AccountSelectionDialog(
onDismiss = { screenModel.closeDialog() },
onValidate = { accountType ->
screenModel.closeDialog()
if (accountType == AccountType.LOCAL) {
screenModel.createLocalAccount()
} else {
val account = Account(
accountType = accountType,
accountName = context.resources.getString(accountType.typeName)
)
navigator.push(
AccountCredentialsScreen(
account,
AccountCredentialsScreenMode.NEW_CREDENTIALS
)
)
}
}
)
}
is DialogState.OPMLImport -> {
OPMLImportProgressDialog(
currentFeed = dialog.currentFeed,
feedCount = dialog.feedCount,
feedMax = dialog.feedMax
)
}
is DialogState.ErrorList -> {
ErrorListDialog(
errorResult = dialog.errorResult,
onDismiss = { screenModel.closeDialog(dialog) }
)
}
is DialogState.Error -> {
ErrorDialog(
exception = dialog.exception,
onDismiss = { screenModel.closeDialog(dialog) }
)
}
is DialogState.OPMLChoice -> {
OPMLChoiceDialog(
onChoice = {
if (it == OPML.IMPORT) {
opmlImportLauncher.launch(ApiUtils.OPML_MIMETYPES.toTypedArray())
} else {
opmlExportLauncher.launch("subscriptions.opml")
}
screenModel.closeDialog()
},
onDismiss = { screenModel.closeDialog() }
)
}
else -> {}
}
AccountDialogs(
state = state,
screenModel = screenModel
)
Scaffold(
topBar = {
@ -264,34 +180,47 @@ object AccountTab : Tab {
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = MaterialTheme.spacing.mediumSpacing)
) {
Image(
painter = adaptiveIconPainterResource(id = state.account.accountType!!.iconRes),
contentDescription = null,
modifier = Modifier.size(48.dp)
)
MediumSpacer()
Column {
Text(
text = state.account.accountName!!,
style = MaterialTheme.typography.titleLarge
Row(
verticalAlignment = Alignment.CenterVertically,
) {
Image(
painter = adaptiveIconPainterResource(id = state.account.accountType!!.iconRes),
contentDescription = null,
modifier = Modifier.size(48.dp)
)
if (state.account.displayedName != null) {
VeryShortSpacer()
MediumSpacer()
Column {
Text(
text = state.account.displayedName!!,
style = MaterialTheme.typography.bodyMedium
text = state.account.accountName!!,
style = MaterialTheme.typography.titleLarge
)
if (state.account.displayedName != null) {
VeryShortSpacer()
Text(
text = state.account.displayedName!!,
style = MaterialTheme.typography.bodyMedium
)
}
}
}
if (state.account.isLocal) {
ThreeDotsMenu(
items = mapOf(1 to "Rename"),
onItemClick = {
screenModel.openDialog(DialogState.RenameAccount(state.account.accountName!!))
},
)
}
}
LargeSpacer()
@ -380,4 +309,113 @@ object AccountTab : Tab {
}
}
}
@Composable
private fun AccountDialogs(state: AccountState, screenModel: AccountScreenModel) {
val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow
val opmlImportLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
uri?.let { screenModel.parseOPMLFile(uri, context) }
}
val opmlExportLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("text/xml")) { uri ->
uri?.let { screenModel.exportOPMLFile(uri, context) }
}
when (val dialog = state.dialog) {
is DialogState.DeleteAccount -> {
TwoChoicesDialog(
title = stringResource(R.string.delete_account),
text = stringResource(R.string.delete_account_question),
icon = rememberVectorPainter(image = Icons.Default.Delete),
confirmText = stringResource(R.string.delete),
dismissText = stringResource(R.string.cancel),
onDismiss = { screenModel.closeDialog() },
onConfirm = {
screenModel.closeDialog()
screenModel.deleteAccount()
}
)
}
is DialogState.NewAccount -> {
AccountSelectionDialog(
onDismiss = { screenModel.closeDialog() },
onValidate = { accountType ->
screenModel.closeDialog()
if (accountType == AccountType.LOCAL) {
screenModel.createLocalAccount()
} else {
val account = Account(
accountType = accountType,
accountName = context.resources.getString(accountType.typeName)
)
navigator.push(
AccountCredentialsScreen(
account,
AccountCredentialsScreenMode.NEW_CREDENTIALS
)
)
}
}
)
}
is DialogState.OPMLImport -> {
OPMLImportProgressDialog(
currentFeed = dialog.currentFeed,
feedCount = dialog.feedCount,
feedMax = dialog.feedMax
)
}
is DialogState.ErrorList -> {
ErrorListDialog(
errorResult = dialog.errorResult,
onDismiss = { screenModel.closeDialog(dialog) }
)
}
is DialogState.Error -> {
ErrorDialog(
exception = dialog.exception,
onDismiss = { screenModel.closeDialog(dialog) }
)
}
is DialogState.OPMLChoice -> {
OPMLChoiceDialog(
onChoice = {
if (it == OPML.IMPORT) {
opmlImportLauncher.launch(ApiUtils.OPML_MIMETYPES.toTypedArray())
} else {
opmlExportLauncher.launch("subscriptions.opml")
}
screenModel.closeDialog()
},
onDismiss = { screenModel.closeDialog() }
)
}
is DialogState.RenameAccount -> {
TextFieldDialog(
title = stringResource(id = R.string.rename_account),
icon = painterResource(id = R.drawable.ic_person),
label = stringResource(id = R.string.name),
state = state.renameAccountState,
onValueChange = { screenModel.setAccountRenameStateName(it) },
onValidate = { screenModel.renameAccount() },
onDismiss = { screenModel.closeDialog() }
)
}
else -> {}
}
}
}

View File

@ -11,7 +11,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import com.readrops.app.R
import com.readrops.app.util.components.BaseDialog
import com.readrops.app.util.components.dialog.BaseDialog
import com.readrops.app.util.theme.spacing
enum class OPML {

View File

@ -4,7 +4,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import com.readrops.app.R
import com.readrops.app.util.components.BaseDialog
import com.readrops.app.util.components.dialog.BaseDialog
import com.readrops.app.util.components.RefreshIndicator
@Composable

View File

@ -6,7 +6,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.readrops.app.R
import com.readrops.app.util.components.BaseDialog
import com.readrops.app.util.components.dialog.BaseDialog
import com.readrops.app.util.components.SelectableImageText
import com.readrops.app.util.theme.spacing
import com.readrops.db.entities.account.AccountType

View File

@ -50,7 +50,7 @@ import com.readrops.app.util.ErrorMessage
import com.readrops.app.util.components.CenteredProgressIndicator
import com.readrops.app.util.components.ErrorMessage
import com.readrops.app.util.components.Placeholder
import com.readrops.app.util.components.TwoChoicesDialog
import com.readrops.app.util.components.dialog.TwoChoicesDialog
import com.readrops.app.util.theme.spacing
import com.readrops.db.entities.Feed

View File

@ -30,7 +30,7 @@ import com.readrops.app.R
import com.readrops.app.account.selection.adaptiveIconPainterResource
import com.readrops.app.feeds.FeedScreenModel
import com.readrops.app.util.ErrorMessage
import com.readrops.app.util.components.BaseDialog
import com.readrops.app.util.components.dialog.BaseDialog
import com.readrops.app.util.theme.LargeSpacer
import com.readrops.app.util.theme.MediumSpacer
import com.readrops.app.util.theme.ShortSpacer

View File

@ -17,7 +17,7 @@ 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.BaseDialog
import com.readrops.app.util.components.dialog.BaseDialog
import com.readrops.app.util.theme.LargeSpacer
@Composable

View File

@ -19,7 +19,7 @@ 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.BaseDialog
import com.readrops.app.util.components.dialog.BaseDialog
import com.readrops.app.util.theme.LargeSpacer
import com.readrops.app.util.theme.MediumSpacer

View File

@ -10,7 +10,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.readrops.app.R
import com.readrops.app.util.components.BaseDialog
import com.readrops.app.util.components.dialog.BaseDialog
import com.readrops.app.util.components.SelectableImageText
import com.readrops.app.util.theme.spacing

View File

@ -15,7 +15,7 @@ import androidx.compose.ui.unit.dp
import com.readrops.app.R
import com.readrops.app.repositories.ErrorResult
import com.readrops.app.util.ErrorMessage
import com.readrops.app.util.components.BaseDialog
import com.readrops.app.util.components.dialog.BaseDialog
import com.readrops.app.util.theme.MediumSpacer
import com.readrops.app.util.theme.ShortSpacer

View File

@ -58,7 +58,7 @@ import com.readrops.app.util.ErrorMessage
import com.readrops.app.util.components.CenteredProgressIndicator
import com.readrops.app.util.components.Placeholder
import com.readrops.app.util.components.RefreshScreen
import com.readrops.app.util.components.TwoChoicesDialog
import com.readrops.app.util.components.dialog.TwoChoicesDialog
import com.readrops.app.util.theme.spacing
import com.readrops.db.filters.ListSortType
import com.readrops.db.filters.MainFilter

View File

@ -1,4 +1,4 @@
package com.readrops.app.util.components
package com.readrops.app.util.components.dialog
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement

View File

@ -1,4 +1,4 @@
package com.readrops.app.util.components
package com.readrops.app.util.components.dialog
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable

View File

@ -0,0 +1,83 @@
package com.readrops.app.util.components.dialog
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.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import com.readrops.app.R
import com.readrops.app.util.ErrorMessage
import com.readrops.app.util.components.TextFieldError
import com.readrops.app.util.theme.LargeSpacer
data class TextFieldDialogState(
val value: String = "",
val textFieldError: TextFieldError? = null,
val exception: Exception? = null
) {
val isTextFieldError
get() = textFieldError != null
}
@Composable
fun TextFieldDialog(
title: String,
icon: Painter,
label: String,
state: TextFieldDialogState,
onValueChange: (String) -> Unit,
onValidate: () -> Unit,
onDismiss: () -> Unit,
modifier: Modifier = Modifier
) {
BaseDialog(
title = title,
icon = icon,
onDismiss = onDismiss,
modifier = modifier
) {
OutlinedTextField(
value = state.value,
label = { Text(text = label) },
onValueChange = onValueChange,
singleLine = true,
trailingIcon = {
if (state.value.isNotEmpty()) {
IconButton(
onClick = { onValueChange("") }
) {
Icon(
imageVector = Icons.Default.Clear,
contentDescription = null
)
}
}
},
isError = state.isTextFieldError,
supportingText = { Text(text = state.textFieldError?.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))
}
}
}

View File

@ -1,4 +1,4 @@
package com.readrops.app.util.components
package com.readrops.app.util.components.dialog
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Icon

View File

@ -172,4 +172,6 @@
<string name="make_donation">Faire une donation</string>
<string name="other_accounts">Autres comptes</string>
<string name="update">Mettre à jour</string>
<string name="rename_account">Renommer le compte</string>
<string name="name">Nom</string>
</resources>

View File

@ -181,4 +181,6 @@
<string name="app_issues_url" translatable="false">https://github.com/readrops/Readrops/issues</string>
<string name="other_accounts">Other accounts</string>
<string name="update">Update</string>
<string name="rename_account">Rename account</string>
<string name="name">Name</string>
</resources>

View File

@ -42,4 +42,7 @@ interface AccountDao : BaseDao<Account> {
@Query("""Update Account set current_account = Case When id = :accountId Then 1
When id Is Not :accountId Then 0 End""")
suspend fun updateCurrentAccount(accountId: Int)
@Query("Update Account set account_name = :name Where id = :accountId")
suspend fun renameAccount(accountId: Int, name: String)
}