WIP: Support multi-account feature (#243)

This commit is contained in:
Ash 2022-12-10 23:34:53 +08:00 committed by GitHub
parent 96a76b5a97
commit 605ffe1c75
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 592 additions and 78 deletions

View File

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

View File

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

View File

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

View File

@ -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<List<Account>> = accountDao.queryAllAsFlow()
fun getAccountById(accountId: Int): Flow<Account?> = 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())
// }
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -79,7 +79,9 @@ fun SubscribeDialog(
title = {
Text(
modifier = Modifier.roundClick {
subscribeViewModel.showRenameDialog()
if (!subscribeUiState.isSearchPage) {
subscribeViewModel.showRenameDialog()
}
},
text = if (subscribeUiState.isSearchPage) {
subscribeUiState.title

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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> = _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,
)

View File

@ -343,4 +343,7 @@
<string name="clear_all_articles_toast">已清空该账户的所有文章</string>
<string name="delete_account_toast">该账户已被删除</string>
<string name="synchronous_tips">需要重新启动才能生效。</string>
</resources>
<string name="switch_account">切换账户</string>
<string name="add">添加</string>
<string name="accounts_tips">在订阅源页面中点击帐户名称来切换它们。</string>
</resources>

View File

@ -382,4 +382,7 @@
<string name="clear_all_articles_toast">All articles from this account have been cleared</string>
<string name="delete_account_toast">This account has been deleted</string>
<string name="synchronous_tips">Restart is required for changes to take effect.</string>
</resources>
<string name="switch_account">Switch Account</string>
<string name="add">Add</string>
<string name="accounts_tips">Click the account name on the feed page to switch them.</string>
</resources>