Improve AccountTab UI and add account change

This commit is contained in:
Shinokuni 2024-07-09 15:44:53 +02:00
parent 016d309d05
commit 45c2de4459
10 changed files with 159 additions and 74 deletions

View File

@ -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<Folder?, List<Feed>>
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<Account> = emptyList()
)
sealed interface DialogState {

View File

@ -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) }
)
}
}
}
}
}

View File

@ -83,6 +83,7 @@ class AccountCredentialsScreenModel(
if (mode == AccountCredentialsScreenMode.NEW_CREDENTIALS) {
newAccount.id = database.accountDao().insert(newAccount).toInt()
database.accountDao().updateCurrentAccount(newAccount.id)
get<SharedPreferences>().edit()
.putString(newAccount.loginKey, newAccount.login)

View File

@ -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<Account>()
private val _accountEvent = MutableSharedFlow<Account>()
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<SharedPreferences>()
if (!account.isLocal) {
if (account.login == null || account.password == null) {
val encryptedPreferences = get<SharedPreferences>()
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<AuthInterceptor>().credentials = Credentials.toCredentials(account)
}
currentAccount = account
repository = get(parameters = { parametersOf(account) })
// very important to avoid credentials conflicts between accounts
get<AuthInterceptor>().credentials = Credentials.toCredentials(account)
accountEvent.emit(account)
_accountEvent.emit(account)
}
}
}
}
}

View File

@ -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,

View File

@ -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 {

View File

@ -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),
)
}
}

View File

@ -170,4 +170,5 @@
<string name="disable_all">Tout désactiver</string>
<string name="open_source_libraries">Bibliothèques Open source</string>
<string name="make_donation">Faire une donation</string>
<string name="other_accounts">Autres comptes</string>
</resources>

View File

@ -179,4 +179,5 @@
<string name="make_donation">Make a donation</string>
<string name="app_changelog_url" translatable="false">https://github.com/readrops/Readrops/blob/develop/CHANGELOG.md</string>
<string name="app_issues_url" translatable="false">https://github.com/readrops/Readrops/issues</string>
<string name="other_accounts">Other accounts</string>
</resources>

View File

@ -28,4 +28,8 @@ interface AccountDao : BaseDao<Account> {
@Query("Select notifications_enabled From Account Where id = :accountId")
fun selectAccountNotificationsState(accountId: Int): Flow<Boolean>
@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)
}