From 605ffe1c75eabca591ef25f43d04c71b1253939b Mon Sep 17 00:00:00 2001 From: Ash Date: Sat, 10 Dec 2022 23:34:53 +0800 Subject: [PATCH] WIP: Support multi-account feature (#243) --- .../main/java/me/ash/reader/CrashHandler.kt | 24 +++- app/src/main/java/me/ash/reader/RYApp.kt | 8 +- .../data/repository/AbstractRssRepository.kt | 4 + .../data/repository/AccountRepository.kt | 63 ++++++--- .../reader/data/repository/OpmlRepository.kt | 12 +- .../reader/data/repository/RssRepository.kt | 2 +- .../ui/component/base/RYOutlineTextField.kt | 90 ++++++++++++ .../reader/ui/component/base/RYTextField.kt | 8 +- .../reader/ui/page/home/feeds/FeedsPage.kt | 51 +++++-- .../page/home/feeds/accounts/AccountsTab.kt | 130 ++++++++++++++++++ .../home/feeds/drawer/feed/ClearFeedDialog.kt | 6 +- .../feeds/drawer/group/ClearGroupDialog.kt | 6 +- .../home/feeds/subscribe/SubscribeDialog.kt | 4 +- .../settings/accounts/AccountDetailsPage.kt | 16 ++- .../settings/accounts/AccountViewModel.kt | 26 +++- .../ui/page/settings/accounts/AccountsPage.kt | 11 +- .../page/settings/accounts/AddAccountsPage.kt | 26 +++- .../addition/AddLocalAccountDialog.kt | 111 +++++++++++++++ .../accounts/addition/AdditionViewModel.kt | 62 +++++++++ app/src/main/res/values-zh-rCN/strings.xml | 5 +- app/src/main/res/values/strings.xml | 5 +- 21 files changed, 592 insertions(+), 78 deletions(-) create mode 100644 app/src/main/java/me/ash/reader/ui/component/base/RYOutlineTextField.kt create mode 100644 app/src/main/java/me/ash/reader/ui/page/home/feeds/accounts/AccountsTab.kt create mode 100644 app/src/main/java/me/ash/reader/ui/page/settings/accounts/addition/AddLocalAccountDialog.kt create mode 100644 app/src/main/java/me/ash/reader/ui/page/settings/accounts/addition/AdditionViewModel.kt diff --git a/app/src/main/java/me/ash/reader/CrashHandler.kt b/app/src/main/java/me/ash/reader/CrashHandler.kt index 59cd2014..c3f26ca8 100644 --- a/app/src/main/java/me/ash/reader/CrashHandler.kt +++ b/app/src/main/java/me/ash/reader/CrashHandler.kt @@ -5,7 +5,6 @@ import android.os.Looper import android.util.Log import me.ash.reader.ui.ext.showToastLong import java.lang.Thread.UncaughtExceptionHandler -import kotlin.system.exitProcess /** * The uncaught exception handler for the application. @@ -20,12 +19,27 @@ class CrashHandler(private val context: Context) : UncaughtExceptionHandler { * Catch all uncaught exception and log it. */ override fun uncaughtException(p0: Thread, p1: Throwable) { - Log.e("RLog", "uncaughtException: ${p1.message}") + val causeMessage = getCauseMessage(p1) + Log.e("RLog", "uncaughtException: $causeMessage") Looper.myLooper() ?: Looper.prepare() - context.showToastLong(p1.message) + context.showToastLong(causeMessage) Looper.loop() p1.printStackTrace() - android.os.Process.killProcess(android.os.Process.myPid()); - exitProcess(1) + // android.os.Process.killProcess(android.os.Process.myPid()); + // exitProcess(1) + } + + private fun getCauseMessage(e: Throwable?): String? { + val cause = getCauseRecursively(e) + return if (cause != null) cause.message else e?.javaClass?.name + } + + private fun getCauseRecursively(e: Throwable?): Throwable? { + var cause: Throwable? + cause = e + while (cause?.cause != null && cause !is RuntimeException) { + cause = cause.cause + } + return cause } } diff --git a/app/src/main/java/me/ash/reader/RYApp.kt b/app/src/main/java/me/ash/reader/RYApp.kt index 4c2acb2a..5fee0baf 100644 --- a/app/src/main/java/me/ash/reader/RYApp.kt +++ b/app/src/main/java/me/ash/reader/RYApp.kt @@ -16,7 +16,9 @@ import me.ash.reader.data.repository.* import me.ash.reader.data.source.OpmlLocalDataSource import me.ash.reader.data.source.RYDatabase import me.ash.reader.data.source.RYNetworkDataSource -import me.ash.reader.ui.ext.* +import me.ash.reader.ui.ext.del +import me.ash.reader.ui.ext.getLatestApk +import me.ash.reader.ui.ext.isFdroid import okhttp3.OkHttpClient import javax.inject.Inject @@ -119,9 +121,7 @@ class RYApp : Application(), Configuration.Provider { private suspend fun accountInit() { withContext(ioDispatcher) { if (accountRepository.isNoAccount()) { - val account = accountRepository.addDefaultAccount() - applicationContext.dataStore.put(DataStoreKeys.CurrentAccountId, account.id!!) - applicationContext.dataStore.put(DataStoreKeys.CurrentAccountType, account.type.id) + accountRepository.addDefaultAccount() } } } diff --git a/app/src/main/java/me/ash/reader/data/repository/AbstractRssRepository.kt b/app/src/main/java/me/ash/reader/data/repository/AbstractRssRepository.kt index 106b36a1..29db84b7 100644 --- a/app/src/main/java/me/ash/reader/data/repository/AbstractRssRepository.kt +++ b/app/src/main/java/me/ash/reader/data/repository/AbstractRssRepository.kt @@ -59,6 +59,10 @@ abstract class AbstractRssRepository constructor( } } + fun cancelSync() { + workManager.cancelAllWork() + } + suspend fun doSync(isOnStart: Boolean = false) { workManager.cancelAllWork() accountDao.queryById(context.currentAccountId)?.let { diff --git a/app/src/main/java/me/ash/reader/data/repository/AccountRepository.kt b/app/src/main/java/me/ash/reader/data/repository/AccountRepository.kt index a080e871..7ec03e5f 100644 --- a/app/src/main/java/me/ash/reader/data/repository/AccountRepository.kt +++ b/app/src/main/java/me/ash/reader/data/repository/AccountRepository.kt @@ -1,6 +1,7 @@ package me.ash.reader.data.repository import android.content.Context +import android.os.Looper import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.Flow import me.ash.reader.R @@ -11,10 +12,7 @@ import me.ash.reader.data.dao.GroupDao import me.ash.reader.data.model.account.Account import me.ash.reader.data.model.account.AccountType import me.ash.reader.data.model.group.Group -import me.ash.reader.ui.ext.currentAccountId -import me.ash.reader.ui.ext.getDefaultGroupId -import me.ash.reader.ui.ext.showToast -import me.ash.reader.ui.ext.showToastLong +import me.ash.reader.ui.ext.* import javax.inject.Inject class AccountRepository @Inject constructor( @@ -24,7 +22,9 @@ class AccountRepository @Inject constructor( private val groupDao: GroupDao, private val feedDao: FeedDao, private val articleDao: ArticleDao, + private val rssRepository: RssRepository, ) { + fun getAccounts(): Flow> = accountDao.queryAllAsFlow() fun getAccountById(accountId: Int): Flow = accountDao.queryAccount(accountId) @@ -33,26 +33,31 @@ class AccountRepository @Inject constructor( suspend fun isNoAccount(): Boolean = accountDao.queryAll().isEmpty() - suspend fun addDefaultAccount(): Account { - val readYouString = context.getString(R.string.read_you) - val defaultString = context.getString(R.string.defaults) - return Account( - name = readYouString, - type = AccountType.Local, - ).apply { + suspend fun addAccount(account: Account): Account = + account.apply { id = accountDao.insert(this).toInt() }.also { - if (groupDao.queryAll(it.id!!).isEmpty()) { - groupDao.insert( - Group( - id = it.id!!.getDefaultGroupId(), - name = defaultString, - accountId = it.id!!, + // handle default group + when (it.type) { + AccountType.Local -> { + groupDao.insert( + Group( + id = it.id!!.getDefaultGroupId(), + name = context.getString(R.string.defaults), + accountId = it.id!!, + ) ) - ) + } } + context.dataStore.put(DataStoreKeys.CurrentAccountId, it.id!!) + context.dataStore.put(DataStoreKeys.CurrentAccountType, it.type.id) } - } + + suspend fun addDefaultAccount(): Account = + addAccount(Account( + type = AccountType.Local, + name = context.getString(R.string.read_you) + )) suspend fun update(accountId: Int, block: Account.() -> Unit) { accountDao.queryById(accountId)?.let { @@ -62,7 +67,9 @@ class AccountRepository @Inject constructor( suspend fun delete(accountId: Int) { if (accountDao.queryAll().size == 1) { + Looper.myLooper() ?: Looper.prepare() context.showToast(context.getString(R.string.must_have_an_account)) + Looper.loop() return } accountDao.queryById(accountId)?.let { @@ -70,7 +77,23 @@ class AccountRepository @Inject constructor( feedDao.deleteByAccountId(accountId) groupDao.deleteByAccountId(accountId) accountDao.delete(it) - context.showToastLong(context.getString(R.string.delete_account_toast)) + accountDao.queryAll().getOrNull(0)?.let { + context.dataStore.put(DataStoreKeys.CurrentAccountId, it.id!!) + context.dataStore.put(DataStoreKeys.CurrentAccountType, it.type.id) + } } } + + suspend fun switch(account: Account) { + rssRepository.get().cancelSync() + context.dataStore.put(DataStoreKeys.CurrentAccountId, account.id!!) + context.dataStore.put(DataStoreKeys.CurrentAccountType, account.type.id) + + // Restart + // context.packageManager.getLaunchIntentForPackage(context.packageName)?.let { + // it.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + // context.startActivity(it) + // android.os.Process.killProcess(android.os.Process.myPid()) + // } + } } diff --git a/app/src/main/java/me/ash/reader/data/repository/OpmlRepository.kt b/app/src/main/java/me/ash/reader/data/repository/OpmlRepository.kt index f397f1d0..aae44cd3 100644 --- a/app/src/main/java/me/ash/reader/data/repository/OpmlRepository.kt +++ b/app/src/main/java/me/ash/reader/data/repository/OpmlRepository.kt @@ -38,7 +38,7 @@ class OpmlRepository @Inject constructor( */ @Throws(Exception::class) suspend fun saveToDatabase(inputStream: InputStream) { - val defaultGroup = groupDao.queryById(getDefaultGroupId())!! + val defaultGroup = groupDao.queryById(getDefaultGroupId(context.currentAccountId))!! val groupWithFeedList = opmlLocalDataSource.parseFileInputStream(inputStream, defaultGroup) groupWithFeedList.forEach { groupWithFeed -> @@ -60,18 +60,18 @@ class OpmlRepository @Inject constructor( * Exports OPML file. */ @Throws(Exception::class) - suspend fun saveToString(): String { - val defaultGroup = groupDao.queryById(getDefaultGroupId())!! + suspend fun saveToString(accountId: Int): String { + val defaultGroup = groupDao.queryById(getDefaultGroupId(accountId))!! return OpmlWriter().write( Opml( "2.0", Head( - accountDao.queryById(context.currentAccountId)?.name, + accountDao.queryById(accountId)?.name, Date().toString(), null, null, null, null, null, null, null, null, null, null, null, ), - Body(groupDao.queryAllGroupWithFeed(context.currentAccountId).map { + Body(groupDao.queryAllGroupWithFeed(accountId).map { Outline( mapOf( "text" to it.group.name, @@ -97,5 +97,5 @@ class OpmlRepository @Inject constructor( )!! } - private fun getDefaultGroupId(): String = context.currentAccountId.getDefaultGroupId() + private fun getDefaultGroupId(accountId: Int): String = accountId.getDefaultGroupId() } diff --git a/app/src/main/java/me/ash/reader/data/repository/RssRepository.kt b/app/src/main/java/me/ash/reader/data/repository/RssRepository.kt index 1e6e13a6..743c2254 100644 --- a/app/src/main/java/me/ash/reader/data/repository/RssRepository.kt +++ b/app/src/main/java/me/ash/reader/data/repository/RssRepository.kt @@ -16,7 +16,7 @@ class RssRepository @Inject constructor( fun get() = get(context.currentAccountType) - fun get(accountId: Int) = when (accountId) { + fun get(accountTypeId: Int) = when (accountTypeId) { AccountType.Local.id -> localRssRepository // Account.Type.LOCAL -> feverRssRepository // Account.Type.FEVER -> feverRssRepository diff --git a/app/src/main/java/me/ash/reader/ui/component/base/RYOutlineTextField.kt b/app/src/main/java/me/ash/reader/ui/component/base/RYOutlineTextField.kt new file mode 100644 index 00000000..c5eba2d9 --- /dev/null +++ b/app/src/main/java/me/ash/reader/ui/component/base/RYOutlineTextField.kt @@ -0,0 +1,90 @@ +package me.ash.reader.ui.component.base + +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Close +import androidx.compose.material.icons.rounded.ContentPaste +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.res.stringResource +import kotlinx.coroutines.delay +import me.ash.reader.R + +@Composable +fun RYOutlineTextField( + readOnly: Boolean = false, + value: String, + label: String = "", + singleLine: Boolean = true, + onValueChange: (String) -> Unit, + placeholder: String = "", + errorMessage: String = "", + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + keyboardActions: KeyboardActions = KeyboardActions(), +) { + val clipboardManager = LocalClipboardManager.current + val focusRequester = remember { FocusRequester() } + + LaunchedEffect(Unit) { + delay(100) // ??? + focusRequester.requestFocus() + } + + OutlinedTextField( + modifier = Modifier.focusRequester(focusRequester), + colors = TextFieldDefaults.textFieldColors( + containerColor = Color.Transparent, + ), + maxLines = if (singleLine) 1 else Int.MAX_VALUE, + enabled = !readOnly, + value = value, + label = if (label.isEmpty()) null else { + { Text(label) } + }, + onValueChange = { + if (!readOnly) onValueChange(it) + }, + placeholder = { + Text( + text = placeholder, + color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f), + style = MaterialTheme.typography.bodyMedium + ) + }, + isError = errorMessage.isNotEmpty(), + singleLine = singleLine, + trailingIcon = { + if (value.isNotEmpty()) { + IconButton(onClick = { + if (!readOnly) onValueChange("") + }) { + Icon( + imageVector = Icons.Rounded.Close, + contentDescription = stringResource(R.string.clear), + tint = MaterialTheme.colorScheme.outline.copy(alpha = 0.5f), + ) + } + } else { + IconButton(onClick = { + onValueChange(clipboardManager.getText()?.text ?: "") + }) { + Icon( + imageVector = Icons.Rounded.ContentPaste, + contentDescription = stringResource(R.string.paste), + tint = MaterialTheme.colorScheme.primary + ) + } + } + }, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + ) +} diff --git a/app/src/main/java/me/ash/reader/ui/component/base/RYTextField.kt b/app/src/main/java/me/ash/reader/ui/component/base/RYTextField.kt index 770bf81c..15f2b17f 100644 --- a/app/src/main/java/me/ash/reader/ui/component/base/RYTextField.kt +++ b/app/src/main/java/me/ash/reader/ui/component/base/RYTextField.kt @@ -22,10 +22,11 @@ import me.ash.reader.R fun RYTextField( readOnly: Boolean, value: String, + label: String = "", singleLine: Boolean = true, onValueChange: (String) -> Unit, - placeholder: String, - errorMessage: String, + placeholder: String = "", + errorMessage: String = "", keyboardOptions: KeyboardOptions = KeyboardOptions.Default, keyboardActions: KeyboardActions = KeyboardActions(), ) { @@ -45,6 +46,9 @@ fun RYTextField( maxLines = if (singleLine) 1 else Int.MAX_VALUE, enabled = !readOnly, value = value, + label = if (label.isEmpty()) null else { + { Text(label) } + }, onValueChange = { if (!readOnly) onValueChange(it) }, diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsPage.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsPage.kt index 46940dd6..427eef00 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsPage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsPage.kt @@ -2,7 +2,7 @@ package me.ash.reader.ui.page.home.feeds import androidx.activity.compose.BackHandler import androidx.compose.animation.core.* -import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed @@ -16,7 +16,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.draw.rotate -import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.stringResource @@ -32,10 +31,12 @@ import me.ash.reader.ui.ext.* import me.ash.reader.ui.page.common.RouteName import me.ash.reader.ui.page.home.FilterState import me.ash.reader.ui.page.home.HomeViewModel +import me.ash.reader.ui.page.home.feeds.accounts.AccountsTab import me.ash.reader.ui.page.home.feeds.drawer.feed.FeedOptionDrawer import me.ash.reader.ui.page.home.feeds.drawer.group.GroupOptionDrawer import me.ash.reader.ui.page.home.feeds.subscribe.SubscribeDialog import me.ash.reader.ui.page.home.feeds.subscribe.SubscribeViewModel +import me.ash.reader.ui.page.settings.accounts.AccountViewModel import kotlin.math.ln @OptIn( @@ -44,10 +45,15 @@ import kotlin.math.ln @Composable fun FeedsPage( navController: NavHostController, + accountViewModel: AccountViewModel = hiltViewModel(), feedsViewModel: FeedsViewModel = hiltViewModel(), subscribeViewModel: SubscribeViewModel = hiltViewModel(), homeViewModel: HomeViewModel, ) { + var accountTabVisible by remember { mutableStateOf(false) } + + + val scope = rememberCoroutineScope() val context = LocalContext.current val topBarTonalElevation = LocalFeedsTopBarTonalElevation.current val groupListTonalElevation = LocalFeedsGroupListTonalElevation.current @@ -57,6 +63,8 @@ fun FeedsPage( val filterBarPadding = LocalFeedsFilterBarPadding.current val filterBarTonalElevation = LocalFeedsFilterBarTonalElevation.current + val accounts = accountViewModel.accounts.collectAsStateValue(initial = emptyList()) + val feedsUiState = feedsViewModel.feedsUiState.collectAsStateValue() val filterUiState = homeViewModel.filterUiState.collectAsStateValue() val importantSum = @@ -152,13 +160,20 @@ fun FeedsPage( LazyColumn { item { DisplayText( - modifier = Modifier.pointerInput(Unit) { - detectTapGestures( - onLongPress = { - - } - ) - }, + modifier = Modifier + .clickable { + accountTabVisible = true + }, + // .pointerInput(Unit) { + // detectTapGestures( + // onPress = { + // accountTabRemember = true + // }, + // onLongPress = { + // accountTabRemember = true + // } + // ) + // }, text = feedsUiState.account?.name ?: "", desc = if (isSyncing) stringResource(R.string.syncing) else "", ) @@ -268,6 +283,24 @@ fun FeedsPage( SubscribeDialog() GroupOptionDrawer() FeedOptionDrawer() + + AccountsTab( + visible = accountTabVisible, + accounts = accounts, + onAccountSwitch = { + accountViewModel.switchAccount(it) { + accountTabVisible = false + navController.navigate(RouteName.SETTINGS) + navController.navigate(RouteName.FEEDS) { + launchSingleTop = true + restoreState = true + } + } + }, + onDismissRequest = { + accountTabVisible = false + }, + ) } private fun filterChange( diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/accounts/AccountsTab.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/accounts/AccountsTab.kt new file mode 100644 index 00000000..6836e22c --- /dev/null +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/accounts/AccountsTab.kt @@ -0,0 +1,130 @@ +package me.ash.reader.ui.page.home.feeds.accounts + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.People +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.google.accompanist.flowlayout.FlowCrossAxisAlignment +import com.google.accompanist.flowlayout.FlowRow +import com.google.accompanist.flowlayout.MainAxisAlignment +import me.ash.reader.R +import me.ash.reader.data.model.account.Account +import me.ash.reader.ui.component.base.RYDialog +import me.ash.reader.ui.ext.currentAccountId +import me.ash.reader.ui.theme.palette.alwaysLight + +@Composable +fun AccountsTab( + modifier: Modifier = Modifier, + visible: Boolean = false, + accounts: List, + onAccountSwitch: (Account) -> Unit = {}, + onDismissRequest: () -> Unit = {}, +) { + val context = LocalContext.current + + RYDialog( + modifier = modifier, + visible = visible, + onDismissRequest = onDismissRequest, + icon = { + Icon( + imageVector = Icons.Outlined.People, + contentDescription = stringResource(R.string.switch_account), + ) + }, + title = { + Text(text = stringResource(R.string.switch_account)) + }, + text = { + FlowRow( + mainAxisAlignment = MainAxisAlignment.Start, + crossAxisAlignment = FlowCrossAxisAlignment.Start, + crossAxisSpacing = 10.dp, + mainAxisSpacing = 10.dp, + ) { + accounts.forEach { account -> + Column( + modifier = Modifier + .clip(MaterialTheme.shapes.medium) + .clickable { + onAccountSwitch(account) + } + .padding(8.dp), + ) { + Box( + modifier = Modifier + .size(52.dp) + .clip(CircleShape) + .background( + if (account.id == context.currentAccountId) { + MaterialTheme.colorScheme.tertiaryContainer alwaysLight true + } else { + MaterialTheme.colorScheme.primaryContainer alwaysLight true + } + ), + contentAlignment = Alignment.Center, + ) { + Icon(account = account) + } + Text( + modifier = Modifier + .padding(top = 6.dp) + .width(52.dp), + textAlign = TextAlign.Center, + text = account.name, + style = MaterialTheme.typography.displaySmall.copy(fontSize = 11.sp), + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + } + }, + confirmButton = {}, + dismissButton = {}, + ) +} + +@Composable +fun Icon( + account: Account, +) { + val icon = account.type.toIcon().takeIf { it is ImageVector }?.let { it as ImageVector } + val iconPainter = account.type.toIcon().takeIf { it is Painter }?.let { it as Painter } + if (icon != null) { + Icon( + modifier = Modifier.size(24.dp), + imageVector = icon, + contentDescription = account.name, + tint = MaterialTheme.colorScheme.onSurface alwaysLight true + ) + } else { + iconPainter?.let { + Icon( + modifier = Modifier.size(24.dp), + painter = it, + contentDescription = account.name, + tint = MaterialTheme.colorScheme.onSurface alwaysLight true + ) + } + } +} diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/feed/ClearFeedDialog.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/feed/ClearFeedDialog.kt index 50142370..0144eb70 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/feed/ClearFeedDialog.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/feed/ClearFeedDialog.kt @@ -1,7 +1,7 @@ package me.ash.reader.ui.page.home.feeds.drawer.feed import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.DeleteForever +import androidx.compose.material.icons.outlined.DeleteSweep import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -32,7 +32,7 @@ fun ClearFeedDialog( }, icon = { Icon( - imageVector = Icons.Outlined.DeleteForever, + imageVector = Icons.Outlined.DeleteSweep, contentDescription = stringResource(R.string.clear_articles), ) }, @@ -69,4 +69,4 @@ fun ClearFeedDialog( } }, ) -} \ No newline at end of file +} diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/group/ClearGroupDialog.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/group/ClearGroupDialog.kt index fadd2a25..d21c7497 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/group/ClearGroupDialog.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/group/ClearGroupDialog.kt @@ -1,7 +1,7 @@ package me.ash.reader.ui.page.home.feeds.drawer.group import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.DeleteForever +import androidx.compose.material.icons.outlined.DeleteSweep import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -32,7 +32,7 @@ fun ClearGroupDialog( }, icon = { Icon( - imageVector = Icons.Outlined.DeleteForever, + imageVector = Icons.Outlined.DeleteSweep, contentDescription = stringResource(R.string.clear_articles), ) }, @@ -69,4 +69,4 @@ fun ClearGroupDialog( } }, ) -} \ No newline at end of file +} diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/subscribe/SubscribeDialog.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/subscribe/SubscribeDialog.kt index ab1309db..619c5e42 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/subscribe/SubscribeDialog.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/subscribe/SubscribeDialog.kt @@ -79,7 +79,9 @@ fun SubscribeDialog( title = { Text( modifier = Modifier.roundClick { - subscribeViewModel.showRenameDialog() + if (!subscribeUiState.isSearchPage) { + subscribeViewModel.showRenameDialog() + } }, text = if (subscribeUiState.isSearchPage) { subscribeUiState.title diff --git a/app/src/main/java/me/ash/reader/ui/page/settings/accounts/AccountDetailsPage.kt b/app/src/main/java/me/ash/reader/ui/page/settings/accounts/AccountDetailsPage.kt index 371fd49e..feb5265e 100644 --- a/app/src/main/java/me/ash/reader/ui/page/settings/accounts/AccountDetailsPage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/settings/accounts/AccountDetailsPage.kt @@ -6,7 +6,7 @@ import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.DeleteForever +import androidx.compose.material.icons.outlined.DeleteSweep import androidx.compose.material.icons.outlined.PersonOff import androidx.compose.material.icons.rounded.ArrowBack import androidx.compose.material3.Icon @@ -66,7 +66,7 @@ fun AccountDetailsPage( val launcher = rememberLauncherForActivityResult( ActivityResultContracts.CreateDocument() ) { result -> - viewModel.exportAsOPML { string -> + viewModel.exportAsOPML(selectedAccount!!.id!!) { string -> result?.let { uri -> context.contentResolver.openOutputStream(uri)?.use { outputStream -> outputStream.write(string.toByteArray()) @@ -100,7 +100,10 @@ fun AccountDetailsPage( SettingItem( title = stringResource(R.string.name), desc = selectedAccount?.name ?: "", - onClick = { nameDialogVisible = true }, + onClick = { + nameValue = selectedAccount?.name + nameDialogVisible = true + }, ) {} Spacer(modifier = Modifier.height(24.dp)) } @@ -282,7 +285,7 @@ fun AccountDetailsPage( }, icon = { Icon( - imageVector = Icons.Outlined.DeleteForever, + imageVector = Icons.Outlined.DeleteSweep, contentDescription = stringResource(R.string.clear_all_articles), ) }, @@ -295,7 +298,7 @@ fun AccountDetailsPage( confirmButton = { TextButton( onClick = { - selectedAccount?.id?.let { + selectedAccount?.let { viewModel.clear(it) { viewModel.hideClearDialog() context.showToastLong(context.getString(R.string.clear_all_articles_toast)) @@ -343,7 +346,8 @@ fun AccountDetailsPage( onClick = { selectedAccount?.id?.let { viewModel.delete(it) { - viewModel.hideDeleteDialog() + navController.popBackStack() + context.showToastLong(context.getString(R.string.delete_account_toast)) } } } diff --git a/app/src/main/java/me/ash/reader/ui/page/settings/accounts/AccountViewModel.kt b/app/src/main/java/me/ash/reader/ui/page/settings/accounts/AccountViewModel.kt index 78f5b52c..279529b3 100644 --- a/app/src/main/java/me/ash/reader/ui/page/settings/accounts/AccountViewModel.kt +++ b/app/src/main/java/me/ash/reader/ui/page/settings/accounts/AccountViewModel.kt @@ -46,10 +46,10 @@ class AccountViewModel @Inject constructor( } } - fun exportAsOPML(callback: (String) -> Unit = {}) { + fun exportAsOPML(accountId: Int, callback: (String) -> Unit = {}) { viewModelScope.launch(defaultDispatcher) { try { - callback(opmlRepository.saveToString()) + callback(opmlRepository.saveToString(accountId)) } catch (e: Exception) { Log.e("FeedsViewModel", "exportAsOpml: ", e) } @@ -81,9 +81,27 @@ class AccountViewModel @Inject constructor( } } - fun clear(accountId: Int, callback: () -> Unit = {}) { + fun clear(account: Account, callback: () -> Unit = {}) { viewModelScope.launch(ioDispatcher) { - rssRepository.get(accountId).deleteAccountArticles(accountId) + rssRepository.get(account.type.id).deleteAccountArticles(account.id!!) + withContext(mainDispatcher) { + callback() + } + } + } + + fun addAccount(account: Account, callback: (Account) -> Unit = {}) { + viewModelScope.launch(ioDispatcher) { + val addAccount = accountRepository.addAccount(account) + withContext(mainDispatcher) { + callback(addAccount) + } + } + } + + fun switchAccount(targetAccount: Account, callback: () -> Unit = {}) { + viewModelScope.launch(ioDispatcher) { + accountRepository.switch(targetAccount) withContext(mainDispatcher) { callback() } diff --git a/app/src/main/java/me/ash/reader/ui/page/settings/accounts/AccountsPage.kt b/app/src/main/java/me/ash/reader/ui/page/settings/accounts/AccountsPage.kt index 51eb3377..ba0c8b22 100644 --- a/app/src/main/java/me/ash/reader/ui/page/settings/accounts/AccountsPage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/settings/accounts/AccountsPage.kt @@ -19,10 +19,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavHostController import com.google.accompanist.navigation.animation.rememberAnimatedNavController import me.ash.reader.R -import me.ash.reader.ui.component.base.DisplayText -import me.ash.reader.ui.component.base.FeedbackIconButton -import me.ash.reader.ui.component.base.RYScaffold -import me.ash.reader.ui.component.base.Subtitle +import me.ash.reader.ui.component.base.* import me.ash.reader.ui.ext.collectAsStateValue import me.ash.reader.ui.page.common.RouteName import me.ash.reader.ui.page.settings.SettingItem @@ -72,9 +69,12 @@ fun AccountsPage( } }, ) {} - Spacer(modifier = Modifier.height(24.dp)) } } + item { + Tips(text = stringResource(R.string.accounts_tips)) + Spacer(modifier = Modifier.height(24.dp)) + } item { Subtitle( modifier = Modifier.padding(horizontal = 24.dp), @@ -90,7 +90,6 @@ fun AccountsPage( } }, ) {} - Spacer(modifier = Modifier.height(24.dp)) } item { Spacer(modifier = Modifier.height(24.dp)) diff --git a/app/src/main/java/me/ash/reader/ui/page/settings/accounts/AddAccountsPage.kt b/app/src/main/java/me/ash/reader/ui/page/settings/accounts/AddAccountsPage.kt index 03c368ab..bf1a6e8d 100644 --- a/app/src/main/java/me/ash/reader/ui/page/settings/accounts/AddAccountsPage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/settings/accounts/AddAccountsPage.kt @@ -14,20 +14,28 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavHostController import com.google.accompanist.navigation.animation.rememberAnimatedNavController import me.ash.reader.R +import me.ash.reader.data.model.account.Account +import me.ash.reader.data.model.account.AccountType import me.ash.reader.ui.component.base.DisplayText import me.ash.reader.ui.component.base.FeedbackIconButton import me.ash.reader.ui.component.base.RYScaffold import me.ash.reader.ui.component.base.Subtitle +import me.ash.reader.ui.page.common.RouteName import me.ash.reader.ui.page.settings.SettingItem +import me.ash.reader.ui.page.settings.accounts.addition.AddLocalAccountDialog +import me.ash.reader.ui.page.settings.accounts.addition.AdditionViewModel import me.ash.reader.ui.theme.palette.onLight @OptIn(ExperimentalAnimationApi::class) @Composable fun AddAccountsPage( navController: NavHostController = rememberAnimatedNavController(), + viewModel: AccountViewModel = hiltViewModel(), + additionViewModel: AdditionViewModel = hiltViewModel(), ) { val context = LocalContext.current @@ -54,14 +62,11 @@ fun AddAccountsPage( text = stringResource(R.string.local), ) SettingItem( - enable = false, title = stringResource(R.string.local), desc = stringResource(R.string.local_desc), icon = Icons.Rounded.RssFeed, onClick = { - // navController.navigate(RouteName.ACCOUNT_DETAILS) { - // launchSingleTop = true - // } + additionViewModel.showAddLocalAccountDialog() }, ) {} Spacer(modifier = Modifier.height(24.dp)) @@ -111,12 +116,19 @@ fun AddAccountsPage( }, ) {} SettingItem( - enable = false, title = stringResource(R.string.fever), desc = stringResource(R.string.fever_desc), iconPainter = painterResource(id = R.drawable.ic_fever), onClick = { - + viewModel.addAccount(Account( + type = AccountType.Fever, + name = "name", + )) { + navController.popBackStack() + navController.navigate("${RouteName.ACCOUNT_DETAILS}/${it.id}") { + launchSingleTop = true + } + } }, ) {} Spacer(modifier = Modifier.height(24.dp)) @@ -128,6 +140,8 @@ fun AddAccountsPage( } } ) + + AddLocalAccountDialog(navController) } @Preview diff --git a/app/src/main/java/me/ash/reader/ui/page/settings/accounts/addition/AddLocalAccountDialog.kt b/app/src/main/java/me/ash/reader/ui/page/settings/accounts/addition/AddLocalAccountDialog.kt new file mode 100644 index 00000000..9fe8b8ac --- /dev/null +++ b/app/src/main/java/me/ash/reader/ui/page/settings/accounts/addition/AddLocalAccountDialog.kt @@ -0,0 +1,111 @@ +package me.ash.reader.ui.page.settings.accounts.addition + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.RssFeed +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.DialogProperties +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavHostController +import me.ash.reader.R +import me.ash.reader.data.model.account.Account +import me.ash.reader.data.model.account.AccountType +import me.ash.reader.ui.component.base.RYDialog +import me.ash.reader.ui.component.base.RYOutlineTextField +import me.ash.reader.ui.ext.collectAsStateValue +import me.ash.reader.ui.page.common.RouteName +import me.ash.reader.ui.page.settings.accounts.AccountViewModel + +@OptIn(androidx.compose.ui.ExperimentalComposeUiApi::class) +@Composable +fun AddLocalAccountDialog( + navController: NavHostController, + viewModel: AdditionViewModel = hiltViewModel(), + accountViewModel: AccountViewModel = hiltViewModel(), +) { + val context = LocalContext.current + val focusManager = LocalFocusManager.current + val uiState = viewModel.additionUiState.collectAsStateValue() + + var name by remember { mutableStateOf("") } + + RYDialog( + modifier = Modifier.padding(horizontal = 44.dp), + visible = uiState.addLocalAccountDialogVisible, + properties = DialogProperties(usePlatformDefaultWidth = false), + onDismissRequest = { + focusManager.clearFocus() + // subscribeViewModel.hideDrawer() + }, + icon = { + Icon( + imageVector = Icons.Rounded.RssFeed, + contentDescription = stringResource(R.string.local), + ) + }, + title = { + Text( + text = stringResource(R.string.local), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + }, + text = { + Column( + modifier = Modifier.verticalScroll(rememberScrollState()) + ) { + RYOutlineTextField( + value = name, + onValueChange = { name = it }, + label = stringResource(R.string.name), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email) + ) + } + }, + confirmButton = { + TextButton( + enabled = name.isNotBlank(), + onClick = { + focusManager.clearFocus() + viewModel.hideAddLocalAccountDialog() + + accountViewModel.addAccount(Account( + type = AccountType.Local, + name = name, + )) { + navController.popBackStack() + navController.navigate("${RouteName.ACCOUNT_DETAILS}/${it.id}") { + launchSingleTop = true + } + } + } + ) { + Text(stringResource(R.string.add)) + } + }, + dismissButton = { + TextButton( + onClick = { + focusManager.clearFocus() + viewModel.hideAddLocalAccountDialog() + } + ) { + Text(stringResource(R.string.cancel)) + } + }, + ) +} diff --git a/app/src/main/java/me/ash/reader/ui/page/settings/accounts/addition/AdditionViewModel.kt b/app/src/main/java/me/ash/reader/ui/page/settings/accounts/addition/AdditionViewModel.kt new file mode 100644 index 00000000..f291d5bc --- /dev/null +++ b/app/src/main/java/me/ash/reader/ui/page/settings/accounts/addition/AdditionViewModel.kt @@ -0,0 +1,62 @@ +package me.ash.reader.ui.page.settings.accounts.addition + +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import me.ash.reader.data.repository.OpmlRepository +import me.ash.reader.data.repository.RssHelper +import me.ash.reader.data.repository.RssRepository +import me.ash.reader.data.repository.StringsRepository +import javax.inject.Inject + +@HiltViewModel +class AdditionViewModel @Inject constructor( + private val opmlRepository: OpmlRepository, + private val rssRepository: RssRepository, + private val rssHelper: RssHelper, + private val stringsRepository: StringsRepository, +) : ViewModel() { + + private val _additionUiState = MutableStateFlow(AdditionUiState()) + val additionUiState: StateFlow = _additionUiState.asStateFlow() + + fun showAddLocalAccountDialog() { + _additionUiState.update { + it.copy( + addLocalAccountDialogVisible = true, + ) + } + } + + fun hideAddLocalAccountDialog() { + _additionUiState.update { + it.copy( + addLocalAccountDialogVisible = false, + ) + } + } + + fun showAddFeverAccountDialog() { + _additionUiState.update { + it.copy( + addFeverAccountDialogVisible = true, + ) + } + } + + fun hideAddFeverAccountDialog() { + _additionUiState.update { + it.copy( + addFeverAccountDialogVisible = false, + ) + } + } +} + +data class AdditionUiState( + val addLocalAccountDialogVisible: Boolean = false, + val addFeverAccountDialogVisible: Boolean = false, +) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 201a7e06..99d5f693 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -343,4 +343,7 @@ 已清空该账户的所有文章 该账户已被删除 需要重新启动才能生效。 - \ 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 32433c24..f5e81f8a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -382,4 +382,7 @@ All articles from this account have been cleared This account has been deleted Restart is required for changes to take effect. - \ No newline at end of file + Switch Account + Add + Click the account name on the feed page to switch them. +