WIP: Support multi-account feature (#243)
This commit is contained in:
parent
96a76b5a97
commit
605ffe1c75
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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())
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
)
|
||||
}
|
@ -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)
|
||||
},
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -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(
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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(
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -79,7 +79,9 @@ fun SubscribeDialog(
|
||||
title = {
|
||||
Text(
|
||||
modifier = Modifier.roundClick {
|
||||
subscribeViewModel.showRenameDialog()
|
||||
if (!subscribeUiState.isSearchPage) {
|
||||
subscribeViewModel.showRenameDialog()
|
||||
}
|
||||
},
|
||||
text = if (subscribeUiState.isSearchPage) {
|
||||
subscribeUiState.title
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
@ -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,
|
||||
)
|
@ -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>
|
||||
|
@ -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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user