From 45c2de44595ab48e6d65d4d73b287ec03dffe5f5 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Tue, 9 Jul 2024 15:44:53 +0200 Subject: [PATCH] Improve AccountTab UI and add account change --- .../app/account/AccountScreenModel.kt | 32 ++++- .../com/readrops/app/account/AccountTab.kt | 135 ++++++++++++------ .../AccountCredentialsScreenModel.kt | 1 + .../com/readrops/app/base/TabScreenModel.kt | 32 +++-- .../com/readrops/app/feeds/FeedScreenModel.kt | 20 +-- .../app/timelime/TimelineScreenModel.kt | 4 +- .../readrops/app/util/components/IconText.kt | 3 +- app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + .../java/com/readrops/db/dao/AccountDao.kt | 4 + 10 files changed, 159 insertions(+), 74 deletions(-) diff --git a/app/src/main/java/com/readrops/app/account/AccountScreenModel.kt b/app/src/main/java/com/readrops/app/account/AccountScreenModel.kt index dcab1ecc..05d49237 100644 --- a/app/src/main/java/com/readrops/app/account/AccountScreenModel.kt +++ b/app/src/main/java/com/readrops/app/account/AccountScreenModel.kt @@ -15,15 +15,18 @@ import com.readrops.db.entities.Folder import com.readrops.db.entities.account.Account import com.readrops.db.entities.account.AccountType import com.readrops.db.filters.MainFilter +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch class AccountScreenModel( - private val database: Database + private val database: Database, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO ) : TabScreenModel(database) { private val _closeHome = MutableStateFlow(false) @@ -33,7 +36,7 @@ class AccountScreenModel( val accountState = _accountState.asStateFlow() init { - screenModelScope.launch(Dispatchers.IO) { + screenModelScope.launch(dispatcher) { accountEvent.collect { account -> _accountState.update { it.copy( @@ -42,6 +45,14 @@ class AccountScreenModel( } } } + + screenModelScope.launch(dispatcher) { + database.accountDao().selectAllAccounts() + .map { it.filter { account -> !account.isCurrentAccount } } + .collect { accounts -> + _accountState.update { it.copy(accounts = accounts) } + } + } } fun openDialog(dialog: DialogState) = _accountState.update { it.copy(dialog = dialog) } @@ -57,11 +68,15 @@ class AccountScreenModel( } fun deleteAccount() { - screenModelScope.launch(Dispatchers.IO) { + screenModelScope.launch(dispatcher) { database.accountDao() .delete(currentAccount!!) - _closeHome.update { true } + if (_accountState.value.accounts.isNotEmpty()) { + database.accountDao().updateCurrentAccount(_accountState.value.accounts.first().id) + } else { + _closeHome.update { true } + } } } @@ -92,7 +107,7 @@ class AccountScreenModel( } fun parseOPMLFile(uri: Uri, context: Context) { - screenModelScope.launch(Dispatchers.IO) { + screenModelScope.launch(dispatcher) { val foldersAndFeeds: Map> try { @@ -144,6 +159,12 @@ class AccountScreenModel( _accountState.update { it.copy(opmlExportUri = null, opmlExportSuccess = false) } fun resetCloseHome() = _closeHome.update { false } + + fun updateCurrentAccount(account: Account) { + screenModelScope.launch(dispatcher) { + database.accountDao().updateCurrentAccount(account.id) + } + } } @Stable @@ -154,6 +175,7 @@ data class AccountState( val error: Exception? = null, val opmlExportSuccess: Boolean = false, val opmlExportUri: Uri? = null, + val accounts: List = emptyList() ) sealed interface DialogState { diff --git a/app/src/main/java/com/readrops/app/account/AccountTab.kt b/app/src/main/java/com/readrops/app/account/AccountTab.kt index 9a056e9d..2c51c94f 100644 --- a/app/src/main/java/com/readrops/app/account/AccountTab.kt +++ b/app/src/main/java/com/readrops/app/account/AccountTab.kt @@ -4,7 +4,6 @@ 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 @@ -13,11 +12,10 @@ import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AccountCircle import androidx.compose.material.icons.filled.Delete -import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarDuration @@ -36,6 +34,7 @@ import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import cafe.adriel.voyager.koin.getScreenModel @@ -54,9 +53,11 @@ 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.theme.LargeSpacer import com.readrops.app.util.theme.MediumSpacer +import com.readrops.app.util.theme.VeryShortSpacer import com.readrops.app.util.theme.spacing import com.readrops.db.entities.account.Account @@ -231,17 +232,7 @@ object AccountTab : Tab { Scaffold( topBar = { TopAppBar( - title = { Text(text = stringResource(R.string.account)) }, - actions = { - IconButton( - onClick = { } - ) { - Icon( - imageVector = Icons.Default.MoreVert, - contentDescription = null - ) - } - } + title = { Text(text = stringResource(R.string.account)) } ) }, floatingActionButton = { @@ -262,69 +253,119 @@ object AccountTab : Tab { ) { Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center, - modifier = Modifier.fillMaxWidth() + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = MaterialTheme.spacing.mediumSpacing) ) { Image( - painter = adaptiveIconPainterResource(id = R.drawable.ic_freshrss), + painter = adaptiveIconPainterResource(id = state.account.accountType!!.iconRes), contentDescription = null, modifier = Modifier.size(48.dp) ) MediumSpacer() - Text( - text = state.account.accountName!!, - style = MaterialTheme.typography.titleLarge - ) + Column { + Text( + text = state.account.accountName!!, + style = MaterialTheme.typography.titleLarge + ) + + if (state.account.displayedName != null) { + VeryShortSpacer() + + Text( + text = state.account.displayedName!!, + style = MaterialTheme.typography.bodyMedium + ) + } + } + } LargeSpacer() - SelectableIconText( - icon = painterResource(id = R.drawable.ic_add_account), - text = stringResource(R.string.credentials), - style = MaterialTheme.typography.titleMedium, - spacing = MaterialTheme.spacing.mediumSpacing, - padding = MaterialTheme.spacing.mediumSpacing, - onClick = { - navigator.push( - AccountCredentialsScreen( - state.account, - AccountCredentialsScreenMode.EDIT_CREDENTIALS + if (!state.account.isLocal) { + SelectableIconText( + icon = painterResource(id = R.drawable.ic_person), + text = stringResource(R.string.credentials), + style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Normal), + spacing = MaterialTheme.spacing.largeSpacing, + padding = MaterialTheme.spacing.mediumSpacing, + tint = MaterialTheme.colorScheme.primary, + iconSize = 24.dp, + onClick = { + navigator.push( + AccountCredentialsScreen( + state.account, + AccountCredentialsScreenMode.EDIT_CREDENTIALS + ) ) - ) - } - ) + } + ) + } SelectableIconText( icon = painterResource(id = R.drawable.ic_notifications), text = stringResource(R.string.notifications), - style = MaterialTheme.typography.titleMedium, - spacing = MaterialTheme.spacing.mediumSpacing, + style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Normal), + spacing = MaterialTheme.spacing.largeSpacing, padding = MaterialTheme.spacing.mediumSpacing, + tint = MaterialTheme.colorScheme.primary, + iconSize = 24.dp, onClick = { navigator.push(NotificationsScreen(state.account)) } ) - SelectableIconText( - icon = painterResource(id = R.drawable.ic_import_export), - text = stringResource(R.string.opml_import_export), - style = MaterialTheme.typography.titleMedium, - spacing = MaterialTheme.spacing.mediumSpacing, - padding = MaterialTheme.spacing.mediumSpacing, - onClick = { screenModel.openDialog(DialogState.OPMLChoice) } - ) + if (state.account.isLocal) { + SelectableIconText( + icon = painterResource(id = R.drawable.ic_import_export), + text = stringResource(R.string.opml_import_export), + style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Normal), + spacing = MaterialTheme.spacing.largeSpacing, + padding = MaterialTheme.spacing.mediumSpacing, + tint = MaterialTheme.colorScheme.primary, + iconSize = 24.dp, + onClick = { screenModel.openDialog(DialogState.OPMLChoice) } + ) + } SelectableIconText( icon = rememberVectorPainter(image = Icons.Default.AccountCircle), text = stringResource(R.string.delete_account), - style = MaterialTheme.typography.titleMedium, - spacing = MaterialTheme.spacing.mediumSpacing, + style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Normal), + spacing = MaterialTheme.spacing.largeSpacing, padding = MaterialTheme.spacing.mediumSpacing, color = MaterialTheme.colorScheme.error, tint = MaterialTheme.colorScheme.error, + iconSize = 24.dp, onClick = { screenModel.openDialog(DialogState.DeleteAccount) } ) + + if (state.accounts.isNotEmpty()) { + HorizontalDivider( + modifier = Modifier.padding(MaterialTheme.spacing.mediumSpacing) + ) + + Text( + text = stringResource(id = R.string.other_accounts), + style = MaterialTheme.typography.labelSmall, + modifier = Modifier.padding(horizontal = MaterialTheme.spacing.mediumSpacing) + ) + + VeryShortSpacer() + + for (account in state.accounts) { + SelectableImageText( + image = adaptiveIconPainterResource(id = account.accountType!!.iconRes), + text = account.accountName!!, + style = MaterialTheme.typography.titleMedium, + padding = MaterialTheme.spacing.mediumSpacing, + spacing = MaterialTheme.spacing.mediumSpacing, + imageSize = 24.dp, + onClick = { screenModel.updateCurrentAccount(account) } + ) + } + } } } } diff --git a/app/src/main/java/com/readrops/app/account/credentials/AccountCredentialsScreenModel.kt b/app/src/main/java/com/readrops/app/account/credentials/AccountCredentialsScreenModel.kt index 60ab3758..fe538be2 100644 --- a/app/src/main/java/com/readrops/app/account/credentials/AccountCredentialsScreenModel.kt +++ b/app/src/main/java/com/readrops/app/account/credentials/AccountCredentialsScreenModel.kt @@ -83,6 +83,7 @@ class AccountCredentialsScreenModel( if (mode == AccountCredentialsScreenMode.NEW_CREDENTIALS) { newAccount.id = database.accountDao().insert(newAccount).toInt() + database.accountDao().updateCurrentAccount(newAccount.id) get().edit() .putString(newAccount.loginKey, newAccount.login) diff --git a/app/src/main/java/com/readrops/app/base/TabScreenModel.kt b/app/src/main/java/com/readrops/app/base/TabScreenModel.kt index d0cac95c..a517a93a 100644 --- a/app/src/main/java/com/readrops/app/base/TabScreenModel.kt +++ b/app/src/main/java/com/readrops/app/base/TabScreenModel.kt @@ -8,8 +8,12 @@ import com.readrops.api.utils.AuthInterceptor import com.readrops.app.repositories.BaseRepository import com.readrops.db.Database import com.readrops.db.entities.account.Account +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent import org.koin.core.component.get @@ -20,6 +24,7 @@ import org.koin.core.parameter.parametersOf */ abstract class TabScreenModel( private val database: Database, + dispatcher: CoroutineDispatcher = Dispatchers.IO ) : ScreenModel, KoinComponent { /** @@ -29,31 +34,38 @@ abstract class TabScreenModel( protected var currentAccount: Account? = null - protected val accountEvent = MutableSharedFlow() + private val _accountEvent = MutableSharedFlow() + protected val accountEvent = + _accountEvent.shareIn(scope = screenModelScope, started = SharingStarted.Eagerly) init { - screenModelScope.launch { + screenModelScope.launch(dispatcher) { database.accountDao() .selectCurrentAccount() .distinctUntilChanged() .collect { account -> if (account != null) { - if (account.login == null || account.password == null) { - val encryptedPreferences = get() + if (!account.isLocal) { + if (account.login == null || account.password == null) { + val encryptedPreferences = get() - account.login = encryptedPreferences.getString(account.loginKey, null) - account.password = encryptedPreferences.getString(account.passwordKey, null) + account.login = + encryptedPreferences.getString(account.loginKey, null) + account.password = + encryptedPreferences.getString(account.passwordKey, null) + } + + // very important to avoid credentials conflicts between accounts + get().credentials = Credentials.toCredentials(account) } currentAccount = account repository = get(parameters = { parametersOf(account) }) - // very important to avoid credentials conflicts between accounts - get().credentials = Credentials.toCredentials(account) - accountEvent.emit(account) + + _accountEvent.emit(account) } } } } - } \ No newline at end of file diff --git a/app/src/main/java/com/readrops/app/feeds/FeedScreenModel.kt b/app/src/main/java/com/readrops/app/feeds/FeedScreenModel.kt index 6c309d2f..1a98638d 100644 --- a/app/src/main/java/com/readrops/app/feeds/FeedScreenModel.kt +++ b/app/src/main/java/com/readrops/app/feeds/FeedScreenModel.kt @@ -19,7 +19,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.flatMapConcat +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent @@ -49,7 +49,7 @@ class FeedScreenModel( init { screenModelScope.launch(context = Dispatchers.IO) { accountEvent - .flatMapConcat { account -> + .flatMapLatest { account -> _feedState.update { it.copy(displayFolderCreationButton = account.config.canCreateFolder) } _updateFeedDialogState.update { it.copy( @@ -69,24 +69,26 @@ class FeedScreenModel( it.copy(foldersAndFeeds = FolderAndFeedsState.LoadedState(foldersAndFeeds)) } } + } screenModelScope.launch(context = Dispatchers.IO) { database.accountDao() .selectAllAccounts() .collect { accounts -> - _addFeedDialogState.update { dialogState -> - dialogState.copy( - accounts = accounts, - selectedAccount = accounts.find { it.isCurrentAccount }!! - ) + if (accounts.isNotEmpty()) { + _addFeedDialogState.update { dialogState -> + dialogState.copy( + accounts = accounts, + selectedAccount = accounts.find { it.isCurrentAccount }!! + ) + } } } } screenModelScope.launch(context = Dispatchers.IO) { - accountEvent - .flatMapConcat { account -> + accountEvent.flatMapLatest { account -> _updateFeedDialogState.update { it.copy( isFeedUrlReadOnly = account.config.isFeedUrlReadOnly, diff --git a/app/src/main/java/com/readrops/app/timelime/TimelineScreenModel.kt b/app/src/main/java/com/readrops/app/timelime/TimelineScreenModel.kt index eb1a9263..6218f34f 100644 --- a/app/src/main/java/com/readrops/app/timelime/TimelineScreenModel.kt +++ b/app/src/main/java/com/readrops/app/timelime/TimelineScreenModel.kt @@ -31,7 +31,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.flow.flatMapConcat +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.transformLatest import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -97,7 +97,7 @@ class TimelineScreenModel( } screenModelScope.launch(dispatcher) { - accountEvent.flatMapConcat { + accountEvent.flatMapLatest { getFoldersWithFeeds.getNewItemsUnreadCount(it.id, it.config.useSeparateState) }.collectLatest { count -> _timelineState.update { diff --git a/app/src/main/java/com/readrops/app/util/components/IconText.kt b/app/src/main/java/com/readrops/app/util/components/IconText.kt index 8ee71321..c4d15dbe 100644 --- a/app/src/main/java/com/readrops/app/util/components/IconText.kt +++ b/app/src/main/java/com/readrops/app/util/components/IconText.kt @@ -115,6 +115,7 @@ fun SelectableIconText( modifier: Modifier = Modifier, color: Color = LocalContentColor.current, tint: Color = LocalContentColor.current, + iconSize: Dp = style.toDp(), spacing: Dp = MaterialTheme.spacing.veryShortSpacing, padding: Dp = MaterialTheme.spacing.shortSpacing ) { @@ -134,7 +135,7 @@ fun SelectableIconText( painter = icon, tint = tint, contentDescription = null, - modifier = Modifier.size(style.toDp()), + modifier = Modifier.size(iconSize), ) } } diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 7a409c2f..3dd11884 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -170,4 +170,5 @@ Tout désactiver Bibliothèques Open source Faire une donation + Autres comptes \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c5502d2b..12a71a3c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -179,4 +179,5 @@ Make a donation https://github.com/readrops/Readrops/blob/develop/CHANGELOG.md https://github.com/readrops/Readrops/issues + Other accounts \ No newline at end of file diff --git a/db/src/main/java/com/readrops/db/dao/AccountDao.kt b/db/src/main/java/com/readrops/db/dao/AccountDao.kt index 4d9ea14c..cee804cd 100644 --- a/db/src/main/java/com/readrops/db/dao/AccountDao.kt +++ b/db/src/main/java/com/readrops/db/dao/AccountDao.kt @@ -28,4 +28,8 @@ interface AccountDao : BaseDao { @Query("Select notifications_enabled From Account Where id = :accountId") fun selectAccountNotificationsState(accountId: Int): Flow + + @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) } \ No newline at end of file