Skip to content

Commit 605ffe1

Browse files
authored
WIP: Support multi-account feature (#243)
1 parent 96a76b5 commit 605ffe1

21 files changed

Lines changed: 592 additions & 78 deletions

app/src/main/java/me/ash/reader/CrashHandler.kt

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import android.os.Looper
55
import android.util.Log
66
import me.ash.reader.ui.ext.showToastLong
77
import java.lang.Thread.UncaughtExceptionHandler
8-
import kotlin.system.exitProcess
98

109
/**
1110
* The uncaught exception handler for the application.
@@ -20,12 +19,27 @@ class CrashHandler(private val context: Context) : UncaughtExceptionHandler {
2019
* Catch all uncaught exception and log it.
2120
*/
2221
override fun uncaughtException(p0: Thread, p1: Throwable) {
23-
Log.e("RLog", "uncaughtException: ${p1.message}")
22+
val causeMessage = getCauseMessage(p1)
23+
Log.e("RLog", "uncaughtException: $causeMessage")
2424
Looper.myLooper() ?: Looper.prepare()
25-
context.showToastLong(p1.message)
25+
context.showToastLong(causeMessage)
2626
Looper.loop()
2727
p1.printStackTrace()
28-
android.os.Process.killProcess(android.os.Process.myPid());
29-
exitProcess(1)
28+
// android.os.Process.killProcess(android.os.Process.myPid());
29+
// exitProcess(1)
30+
}
31+
32+
private fun getCauseMessage(e: Throwable?): String? {
33+
val cause = getCauseRecursively(e)
34+
return if (cause != null) cause.message else e?.javaClass?.name
35+
}
36+
37+
private fun getCauseRecursively(e: Throwable?): Throwable? {
38+
var cause: Throwable?
39+
cause = e
40+
while (cause?.cause != null && cause !is RuntimeException) {
41+
cause = cause.cause
42+
}
43+
return cause
3044
}
3145
}

app/src/main/java/me/ash/reader/RYApp.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ import me.ash.reader.data.repository.*
1616
import me.ash.reader.data.source.OpmlLocalDataSource
1717
import me.ash.reader.data.source.RYDatabase
1818
import me.ash.reader.data.source.RYNetworkDataSource
19-
import me.ash.reader.ui.ext.*
19+
import me.ash.reader.ui.ext.del
20+
import me.ash.reader.ui.ext.getLatestApk
21+
import me.ash.reader.ui.ext.isFdroid
2022
import okhttp3.OkHttpClient
2123
import javax.inject.Inject
2224

@@ -119,9 +121,7 @@ class RYApp : Application(), Configuration.Provider {
119121
private suspend fun accountInit() {
120122
withContext(ioDispatcher) {
121123
if (accountRepository.isNoAccount()) {
122-
val account = accountRepository.addDefaultAccount()
123-
applicationContext.dataStore.put(DataStoreKeys.CurrentAccountId, account.id!!)
124-
applicationContext.dataStore.put(DataStoreKeys.CurrentAccountType, account.type.id)
124+
accountRepository.addDefaultAccount()
125125
}
126126
}
127127
}

app/src/main/java/me/ash/reader/data/repository/AbstractRssRepository.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ abstract class AbstractRssRepository constructor(
5959
}
6060
}
6161

62+
fun cancelSync() {
63+
workManager.cancelAllWork()
64+
}
65+
6266
suspend fun doSync(isOnStart: Boolean = false) {
6367
workManager.cancelAllWork()
6468
accountDao.queryById(context.currentAccountId)?.let {
Lines changed: 43 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package me.ash.reader.data.repository
22

33
import android.content.Context
4+
import android.os.Looper
45
import dagger.hilt.android.qualifiers.ApplicationContext
56
import kotlinx.coroutines.flow.Flow
67
import me.ash.reader.R
@@ -11,10 +12,7 @@ import me.ash.reader.data.dao.GroupDao
1112
import me.ash.reader.data.model.account.Account
1213
import me.ash.reader.data.model.account.AccountType
1314
import me.ash.reader.data.model.group.Group
14-
import me.ash.reader.ui.ext.currentAccountId
15-
import me.ash.reader.ui.ext.getDefaultGroupId
16-
import me.ash.reader.ui.ext.showToast
17-
import me.ash.reader.ui.ext.showToastLong
15+
import me.ash.reader.ui.ext.*
1816
import javax.inject.Inject
1917

2018
class AccountRepository @Inject constructor(
@@ -24,7 +22,9 @@ class AccountRepository @Inject constructor(
2422
private val groupDao: GroupDao,
2523
private val feedDao: FeedDao,
2624
private val articleDao: ArticleDao,
25+
private val rssRepository: RssRepository,
2726
) {
27+
2828
fun getAccounts(): Flow<List<Account>> = accountDao.queryAllAsFlow()
2929

3030
fun getAccountById(accountId: Int): Flow<Account?> = accountDao.queryAccount(accountId)
@@ -33,26 +33,31 @@ class AccountRepository @Inject constructor(
3333

3434
suspend fun isNoAccount(): Boolean = accountDao.queryAll().isEmpty()
3535

36-
suspend fun addDefaultAccount(): Account {
37-
val readYouString = context.getString(R.string.read_you)
38-
val defaultString = context.getString(R.string.defaults)
39-
return Account(
40-
name = readYouString,
41-
type = AccountType.Local,
42-
).apply {
36+
suspend fun addAccount(account: Account): Account =
37+
account.apply {
4338
id = accountDao.insert(this).toInt()
4439
}.also {
45-
if (groupDao.queryAll(it.id!!).isEmpty()) {
46-
groupDao.insert(
47-
Group(
48-
id = it.id!!.getDefaultGroupId(),
49-
name = defaultString,
50-
accountId = it.id!!,
40+
// handle default group
41+
when (it.type) {
42+
AccountType.Local -> {
43+
groupDao.insert(
44+
Group(
45+
id = it.id!!.getDefaultGroupId(),
46+
name = context.getString(R.string.defaults),
47+
accountId = it.id!!,
48+
)
5149
)
52-
)
50+
}
5351
}
52+
context.dataStore.put(DataStoreKeys.CurrentAccountId, it.id!!)
53+
context.dataStore.put(DataStoreKeys.CurrentAccountType, it.type.id)
5454
}
55-
}
55+
56+
suspend fun addDefaultAccount(): Account =
57+
addAccount(Account(
58+
type = AccountType.Local,
59+
name = context.getString(R.string.read_you)
60+
))
5661

5762
suspend fun update(accountId: Int, block: Account.() -> Unit) {
5863
accountDao.queryById(accountId)?.let {
@@ -62,15 +67,33 @@ class AccountRepository @Inject constructor(
6267

6368
suspend fun delete(accountId: Int) {
6469
if (accountDao.queryAll().size == 1) {
70+
Looper.myLooper() ?: Looper.prepare()
6571
context.showToast(context.getString(R.string.must_have_an_account))
72+
Looper.loop()
6673
return
6774
}
6875
accountDao.queryById(accountId)?.let {
6976
articleDao.deleteByAccountId(accountId)
7077
feedDao.deleteByAccountId(accountId)
7178
groupDao.deleteByAccountId(accountId)
7279
accountDao.delete(it)
73-
context.showToastLong(context.getString(R.string.delete_account_toast))
80+
accountDao.queryAll().getOrNull(0)?.let {
81+
context.dataStore.put(DataStoreKeys.CurrentAccountId, it.id!!)
82+
context.dataStore.put(DataStoreKeys.CurrentAccountType, it.type.id)
83+
}
7484
}
7585
}
86+
87+
suspend fun switch(account: Account) {
88+
rssRepository.get().cancelSync()
89+
context.dataStore.put(DataStoreKeys.CurrentAccountId, account.id!!)
90+
context.dataStore.put(DataStoreKeys.CurrentAccountType, account.type.id)
91+
92+
// Restart
93+
// context.packageManager.getLaunchIntentForPackage(context.packageName)?.let {
94+
// it.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
95+
// context.startActivity(it)
96+
// android.os.Process.killProcess(android.os.Process.myPid())
97+
// }
98+
}
7699
}

app/src/main/java/me/ash/reader/data/repository/OpmlRepository.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ class OpmlRepository @Inject constructor(
3838
*/
3939
@Throws(Exception::class)
4040
suspend fun saveToDatabase(inputStream: InputStream) {
41-
val defaultGroup = groupDao.queryById(getDefaultGroupId())!!
41+
val defaultGroup = groupDao.queryById(getDefaultGroupId(context.currentAccountId))!!
4242
val groupWithFeedList =
4343
opmlLocalDataSource.parseFileInputStream(inputStream, defaultGroup)
4444
groupWithFeedList.forEach { groupWithFeed ->
@@ -60,18 +60,18 @@ class OpmlRepository @Inject constructor(
6060
* Exports OPML file.
6161
*/
6262
@Throws(Exception::class)
63-
suspend fun saveToString(): String {
64-
val defaultGroup = groupDao.queryById(getDefaultGroupId())!!
63+
suspend fun saveToString(accountId: Int): String {
64+
val defaultGroup = groupDao.queryById(getDefaultGroupId(accountId))!!
6565
return OpmlWriter().write(
6666
Opml(
6767
"2.0",
6868
Head(
69-
accountDao.queryById(context.currentAccountId)?.name,
69+
accountDao.queryById(accountId)?.name,
7070
Date().toString(), null, null, null,
7171
null, null, null, null,
7272
null, null, null, null,
7373
),
74-
Body(groupDao.queryAllGroupWithFeed(context.currentAccountId).map {
74+
Body(groupDao.queryAllGroupWithFeed(accountId).map {
7575
Outline(
7676
mapOf(
7777
"text" to it.group.name,
@@ -97,5 +97,5 @@ class OpmlRepository @Inject constructor(
9797
)!!
9898
}
9999

100-
private fun getDefaultGroupId(): String = context.currentAccountId.getDefaultGroupId()
100+
private fun getDefaultGroupId(accountId: Int): String = accountId.getDefaultGroupId()
101101
}

app/src/main/java/me/ash/reader/data/repository/RssRepository.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ class RssRepository @Inject constructor(
1616

1717
fun get() = get(context.currentAccountType)
1818

19-
fun get(accountId: Int) = when (accountId) {
19+
fun get(accountTypeId: Int) = when (accountTypeId) {
2020
AccountType.Local.id -> localRssRepository
2121
// Account.Type.LOCAL -> feverRssRepository
2222
// Account.Type.FEVER -> feverRssRepository
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package me.ash.reader.ui.component.base
2+
3+
import androidx.compose.foundation.text.KeyboardActions
4+
import androidx.compose.foundation.text.KeyboardOptions
5+
import androidx.compose.material.icons.Icons
6+
import androidx.compose.material.icons.rounded.Close
7+
import androidx.compose.material.icons.rounded.ContentPaste
8+
import androidx.compose.material3.*
9+
import androidx.compose.runtime.Composable
10+
import androidx.compose.runtime.LaunchedEffect
11+
import androidx.compose.runtime.remember
12+
import androidx.compose.ui.Modifier
13+
import androidx.compose.ui.focus.FocusRequester
14+
import androidx.compose.ui.focus.focusRequester
15+
import androidx.compose.ui.graphics.Color
16+
import androidx.compose.ui.platform.LocalClipboardManager
17+
import androidx.compose.ui.res.stringResource
18+
import kotlinx.coroutines.delay
19+
import me.ash.reader.R
20+
21+
@Composable
22+
fun RYOutlineTextField(
23+
readOnly: Boolean = false,
24+
value: String,
25+
label: String = "",
26+
singleLine: Boolean = true,
27+
onValueChange: (String) -> Unit,
28+
placeholder: String = "",
29+
errorMessage: String = "",
30+
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
31+
keyboardActions: KeyboardActions = KeyboardActions(),
32+
) {
33+
val clipboardManager = LocalClipboardManager.current
34+
val focusRequester = remember { FocusRequester() }
35+
36+
LaunchedEffect(Unit) {
37+
delay(100) // ???
38+
focusRequester.requestFocus()
39+
}
40+
41+
OutlinedTextField(
42+
modifier = Modifier.focusRequester(focusRequester),
43+
colors = TextFieldDefaults.textFieldColors(
44+
containerColor = Color.Transparent,
45+
),
46+
maxLines = if (singleLine) 1 else Int.MAX_VALUE,
47+
enabled = !readOnly,
48+
value = value,
49+
label = if (label.isEmpty()) null else {
50+
{ Text(label) }
51+
},
52+
onValueChange = {
53+
if (!readOnly) onValueChange(it)
54+
},
55+
placeholder = {
56+
Text(
57+
text = placeholder,
58+
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
59+
style = MaterialTheme.typography.bodyMedium
60+
)
61+
},
62+
isError = errorMessage.isNotEmpty(),
63+
singleLine = singleLine,
64+
trailingIcon = {
65+
if (value.isNotEmpty()) {
66+
IconButton(onClick = {
67+
if (!readOnly) onValueChange("")
68+
}) {
69+
Icon(
70+
imageVector = Icons.Rounded.Close,
71+
contentDescription = stringResource(R.string.clear),
72+
tint = MaterialTheme.colorScheme.outline.copy(alpha = 0.5f),
73+
)
74+
}
75+
} else {
76+
IconButton(onClick = {
77+
onValueChange(clipboardManager.getText()?.text ?: "")
78+
}) {
79+
Icon(
80+
imageVector = Icons.Rounded.ContentPaste,
81+
contentDescription = stringResource(R.string.paste),
82+
tint = MaterialTheme.colorScheme.primary
83+
)
84+
}
85+
}
86+
},
87+
keyboardOptions = keyboardOptions,
88+
keyboardActions = keyboardActions,
89+
)
90+
}

app/src/main/java/me/ash/reader/ui/component/base/RYTextField.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,11 @@ import me.ash.reader.R
2222
fun RYTextField(
2323
readOnly: Boolean,
2424
value: String,
25+
label: String = "",
2526
singleLine: Boolean = true,
2627
onValueChange: (String) -> Unit,
27-
placeholder: String,
28-
errorMessage: String,
28+
placeholder: String = "",
29+
errorMessage: String = "",
2930
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
3031
keyboardActions: KeyboardActions = KeyboardActions(),
3132
) {
@@ -45,6 +46,9 @@ fun RYTextField(
4546
maxLines = if (singleLine) 1 else Int.MAX_VALUE,
4647
enabled = !readOnly,
4748
value = value,
49+
label = if (label.isEmpty()) null else {
50+
{ Text(label) }
51+
},
4852
onValueChange = {
4953
if (!readOnly) onValueChange(it)
5054
},

0 commit comments

Comments
 (0)