Improve AccountTab UI and add account change
This commit is contained in:
parent
016d309d05
commit
45c2de4459
@ -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 {
|
||||
|
@ -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) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
@ -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>
|
@ -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)
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user