WIP: Add the Accounts settings feature (#185)
* Add the Accounts settings page (WIP) * Fix sync function and Add change account name feature * Adaptation Account List * Support account settings * Fix auto delete archived articles
This commit is contained in:
parent
386b716e4d
commit
75ac40ed96
364
app/schemas/me.ash.reader.data.source.RYDatabase/3.json
Normal file
364
app/schemas/me.ash.reader.data.source.RYDatabase/3.json
Normal file
|
@ -0,0 +1,364 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 3,
|
||||||
|
"identityHash": "b13c17e4d1ff644caeecce9fc365db2e",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "account",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT NOT NULL, `type` INTEGER NOT NULL, `updateAt` INTEGER, `syncInterval` INTEGER NOT NULL DEFAULT 30, `syncOnStart` INTEGER NOT NULL DEFAULT 0, `syncOnlyOnWiFi` INTEGER NOT NULL DEFAULT 0, `syncOnlyWhenCharging` INTEGER NOT NULL DEFAULT 0, `keepArchived` INTEGER NOT NULL DEFAULT 2592000000, `syncBlockList` TEXT NOT NULL DEFAULT '')",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "type",
|
||||||
|
"columnName": "type",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "updateAt",
|
||||||
|
"columnName": "updateAt",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "syncInterval",
|
||||||
|
"columnName": "syncInterval",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "30"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "syncOnStart",
|
||||||
|
"columnName": "syncOnStart",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "syncOnlyOnWiFi",
|
||||||
|
"columnName": "syncOnlyOnWiFi",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "syncOnlyWhenCharging",
|
||||||
|
"columnName": "syncOnlyWhenCharging",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "keepArchived",
|
||||||
|
"columnName": "keepArchived",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "2592000000"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "syncBlockList",
|
||||||
|
"columnName": "syncBlockList",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "''"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `icon` TEXT, `url` TEXT NOT NULL, `groupId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `isNotification` INTEGER NOT NULL, `isFullContent` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`groupId`) REFERENCES `group`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "icon",
|
||||||
|
"columnName": "icon",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "groupId",
|
||||||
|
"columnName": "groupId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accountId",
|
||||||
|
"columnName": "accountId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isNotification",
|
||||||
|
"columnName": "isNotification",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isFullContent",
|
||||||
|
"columnName": "isFullContent",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_groupId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"groupId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_groupId` ON `${TABLE_NAME}` (`groupId`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_feed_accountId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"accountId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_accountId` ON `${TABLE_NAME}` (`accountId`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "group",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"groupId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "article",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `date` INTEGER NOT NULL, `title` TEXT NOT NULL, `author` TEXT, `rawDescription` TEXT NOT NULL, `shortDescription` TEXT NOT NULL, `fullContent` TEXT, `img` TEXT, `link` TEXT NOT NULL, `feedId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `isUnread` INTEGER NOT NULL, `isStarred` INTEGER NOT NULL, `isReadLater` INTEGER NOT NULL, `updateAt` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`feedId`) REFERENCES `feed`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "date",
|
||||||
|
"columnName": "date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "author",
|
||||||
|
"columnName": "author",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "rawDescription",
|
||||||
|
"columnName": "rawDescription",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "shortDescription",
|
||||||
|
"columnName": "shortDescription",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "fullContent",
|
||||||
|
"columnName": "fullContent",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "img",
|
||||||
|
"columnName": "img",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "link",
|
||||||
|
"columnName": "link",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "feedId",
|
||||||
|
"columnName": "feedId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accountId",
|
||||||
|
"columnName": "accountId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isUnread",
|
||||||
|
"columnName": "isUnread",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isStarred",
|
||||||
|
"columnName": "isStarred",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isReadLater",
|
||||||
|
"columnName": "isReadLater",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "updateAt",
|
||||||
|
"columnName": "updateAt",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_article_feedId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"feedId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_article_feedId` ON `${TABLE_NAME}` (`feedId`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_article_accountId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"accountId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_article_accountId` ON `${TABLE_NAME}` (`accountId`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "feed",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"feedId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "group",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `accountId` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accountId",
|
||||||
|
"columnName": "accountId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_group_accountId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"accountId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_group_accountId` ON `${TABLE_NAME}` (`accountId`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b13c17e4d1ff644caeecce9fc365db2e')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,11 +20,9 @@ class CrashHandler(private val context: Context) : UncaughtExceptionHandler {
|
||||||
* Catch all uncaught exception and log it.
|
* Catch all uncaught exception and log it.
|
||||||
*/
|
*/
|
||||||
override fun uncaughtException(p0: Thread, p1: Throwable) {
|
override fun uncaughtException(p0: Thread, p1: Throwable) {
|
||||||
Looper.myLooper() ?: Looper.prepare()
|
|
||||||
context.showToastLong(p1.message)
|
|
||||||
Looper.loop()
|
|
||||||
p1.printStackTrace()
|
|
||||||
Log.e("RLog", "uncaughtException: ${p1.message}")
|
Log.e("RLog", "uncaughtException: ${p1.message}")
|
||||||
|
context.showToastLong(p1.message)
|
||||||
|
p1.printStackTrace()
|
||||||
android.os.Process.killProcess(android.os.Process.myPid());
|
android.os.Process.killProcess(android.os.Process.myPid());
|
||||||
exitProcess(1)
|
exitProcess(1)
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,8 @@ import androidx.profileinstaller.ProfileInstallerInitializer
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import coil.compose.LocalImageLoader
|
import coil.compose.LocalImageLoader
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import me.ash.reader.data.dao.AccountDao
|
||||||
|
import me.ash.reader.data.model.preference.AccountSettingsProvider
|
||||||
import me.ash.reader.data.model.preference.LanguagesPreference
|
import me.ash.reader.data.model.preference.LanguagesPreference
|
||||||
import me.ash.reader.data.model.preference.SettingsProvider
|
import me.ash.reader.data.model.preference.SettingsProvider
|
||||||
import me.ash.reader.ui.ext.languages
|
import me.ash.reader.ui.ext.languages
|
||||||
|
@ -25,6 +27,9 @@ class MainActivity : ComponentActivity() {
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var imageLoader: ImageLoader
|
lateinit var imageLoader: ImageLoader
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var accountDao: AccountDao
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
|
@ -40,8 +45,10 @@ class MainActivity : ComponentActivity() {
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalImageLoader provides imageLoader,
|
LocalImageLoader provides imageLoader,
|
||||||
) {
|
) {
|
||||||
SettingsProvider {
|
AccountSettingsProvider(accountDao) {
|
||||||
HomeEntry()
|
SettingsProvider {
|
||||||
|
HomeEntry()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -103,7 +103,7 @@ class RYApp : Application(), Configuration.Provider {
|
||||||
applicationScope.launch {
|
applicationScope.launch {
|
||||||
accountInit()
|
accountInit()
|
||||||
workerInit()
|
workerInit()
|
||||||
if (notFdroid) checkUpdate()
|
checkUpdate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -126,11 +126,12 @@ class RYApp : Application(), Configuration.Provider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun workerInit() {
|
private suspend fun workerInit() {
|
||||||
rssRepository.get().doSync()
|
rssRepository.get().doSync(isOnStart = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun checkUpdate() {
|
private suspend fun checkUpdate() {
|
||||||
|
if (isFdroid) return
|
||||||
withContext(ioDispatcher) {
|
withContext(ioDispatcher) {
|
||||||
applicationContext.getLatestApk().let {
|
applicationContext.getLatestApk().let {
|
||||||
if (it.exists()) it.del()
|
if (it.exists()) it.del()
|
||||||
|
|
|
@ -1,11 +1,20 @@
|
||||||
package me.ash.reader.data.dao
|
package me.ash.reader.data.dao
|
||||||
|
|
||||||
import androidx.room.*
|
import androidx.room.*
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import me.ash.reader.data.model.account.Account
|
import me.ash.reader.data.model.account.Account
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface AccountDao {
|
interface AccountDao {
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT * FROM account
|
||||||
|
WHERE id = :id
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
fun queryAccount(id: Int): Flow<Account?>
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
SELECT * FROM account
|
SELECT * FROM account
|
||||||
|
@ -13,6 +22,13 @@ interface AccountDao {
|
||||||
)
|
)
|
||||||
suspend fun queryAll(): List<Account>
|
suspend fun queryAll(): List<Account>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT * FROM account
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
fun queryAllAsFlow(): Flow<List<Account>>
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
SELECT * FROM account
|
SELECT * FROM account
|
||||||
|
|
|
@ -203,6 +203,20 @@ interface ArticleDao {
|
||||||
text: String,
|
text: String,
|
||||||
): PagingSource<Int, ArticleWithFeed>
|
): PagingSource<Int, ArticleWithFeed>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
DELETE FROM article
|
||||||
|
WHERE accountId = :accountId
|
||||||
|
AND updateAt < :before
|
||||||
|
AND isUnread = 0
|
||||||
|
AND isStarred = 0
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
suspend fun deleteAllArchivedBeforeThan(
|
||||||
|
accountId: Int,
|
||||||
|
before: Date,
|
||||||
|
)
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
UPDATE article SET isUnread = :isUnread
|
UPDATE article SET isUnread = :isUnread
|
||||||
|
@ -285,6 +299,14 @@ interface ArticleDao {
|
||||||
)
|
)
|
||||||
suspend fun deleteByGroupId(accountId: Int, groupId: String)
|
suspend fun deleteByGroupId(accountId: Int, groupId: String)
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
DELETE FROM article
|
||||||
|
WHERE accountId = :accountId
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
suspend fun deleteByAccountId(accountId: Int)
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
|
@ -376,7 +398,7 @@ interface ArticleDao {
|
||||||
"""
|
"""
|
||||||
SELECT a.id, a.date, a.title, a.author, a.rawDescription,
|
SELECT a.id, a.date, a.title, a.author, a.rawDescription,
|
||||||
a.shortDescription, a.fullContent, a.img, a.link, a.feedId,
|
a.shortDescription, a.fullContent, a.img, a.link, a.feedId,
|
||||||
a.accountId, a.isUnread, a.isStarred, a.isReadLater
|
a.accountId, a.isUnread, a.isStarred, a.isReadLater, a.updateAt
|
||||||
FROM article AS a
|
FROM article AS a
|
||||||
LEFT JOIN feed AS b ON b.id = a.feedId
|
LEFT JOIN feed AS b ON b.id = a.feedId
|
||||||
LEFT JOIN `group` AS c ON c.id = b.groupId
|
LEFT JOIN `group` AS c ON c.id = b.groupId
|
||||||
|
@ -396,7 +418,7 @@ interface ArticleDao {
|
||||||
"""
|
"""
|
||||||
SELECT a.id, a.date, a.title, a.author, a.rawDescription,
|
SELECT a.id, a.date, a.title, a.author, a.rawDescription,
|
||||||
a.shortDescription, a.fullContent, a.img, a.link, a.feedId,
|
a.shortDescription, a.fullContent, a.img, a.link, a.feedId,
|
||||||
a.accountId, a.isUnread, a.isStarred, a.isReadLater
|
a.accountId, a.isUnread, a.isStarred, a.isReadLater, a.updateAt
|
||||||
FROM article AS a
|
FROM article AS a
|
||||||
LEFT JOIN feed AS b ON b.id = a.feedId
|
LEFT JOIN feed AS b ON b.id = a.feedId
|
||||||
LEFT JOIN `group` AS c ON c.id = b.groupId
|
LEFT JOIN `group` AS c ON c.id = b.groupId
|
||||||
|
@ -418,7 +440,7 @@ interface ArticleDao {
|
||||||
"""
|
"""
|
||||||
SELECT a.id, a.date, a.title, a.author, a.rawDescription,
|
SELECT a.id, a.date, a.title, a.author, a.rawDescription,
|
||||||
a.shortDescription, a.fullContent, a.img, a.link, a.feedId,
|
a.shortDescription, a.fullContent, a.img, a.link, a.feedId,
|
||||||
a.accountId, a.isUnread, a.isStarred, a.isReadLater
|
a.accountId, a.isUnread, a.isStarred, a.isReadLater, a.updateAt
|
||||||
FROM article AS a
|
FROM article AS a
|
||||||
LEFT JOIN feed AS b ON b.id = a.feedId
|
LEFT JOIN feed AS b ON b.id = a.feedId
|
||||||
LEFT JOIN `group` AS c ON c.id = b.groupId
|
LEFT JOIN `group` AS c ON c.id = b.groupId
|
||||||
|
@ -485,7 +507,7 @@ interface ArticleDao {
|
||||||
"""
|
"""
|
||||||
SELECT a.id, a.date, a.title, a.author, a.rawDescription,
|
SELECT a.id, a.date, a.title, a.author, a.rawDescription,
|
||||||
a.shortDescription, a.fullContent, a.img, a.link, a.feedId,
|
a.shortDescription, a.fullContent, a.img, a.link, a.feedId,
|
||||||
a.accountId, a.isUnread, a.isStarred, a.isReadLater
|
a.accountId, a.isUnread, a.isStarred, a.isReadLater, a.updateAt
|
||||||
FROM article AS a LEFT JOIN feed AS b
|
FROM article AS a LEFT JOIN feed AS b
|
||||||
ON a.feedId = b.id
|
ON a.feedId = b.id
|
||||||
WHERE a.feedId = :feedId
|
WHERE a.feedId = :feedId
|
||||||
|
|
|
@ -54,6 +54,14 @@ interface FeedDao {
|
||||||
)
|
)
|
||||||
suspend fun deleteByGroupId(accountId: Int, groupId: String)
|
suspend fun deleteByGroupId(accountId: Int, groupId: String)
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
DELETE FROM feed
|
||||||
|
WHERE accountId = :accountId
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
suspend fun deleteByAccountId(accountId: Int)
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
SELECT * FROM feed
|
SELECT * FROM feed
|
||||||
|
|
|
@ -42,6 +42,14 @@ interface GroupDao {
|
||||||
)
|
)
|
||||||
fun queryAllGroup(accountId: Int): Flow<MutableList<Group>>
|
fun queryAllGroup(accountId: Int): Flow<MutableList<Group>>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
DELETE FROM `group`
|
||||||
|
WHERE accountId = :accountId
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
suspend fun deleteByAccountId(accountId: Int)
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
SELECT * FROM `group`
|
SELECT * FROM `group`
|
||||||
|
|
|
@ -3,6 +3,7 @@ package me.ash.reader.data.model.account
|
||||||
import androidx.room.ColumnInfo
|
import androidx.room.ColumnInfo
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
|
import me.ash.reader.data.model.preference.*
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -19,4 +20,16 @@ data class Account(
|
||||||
var type: AccountType,
|
var type: AccountType,
|
||||||
@ColumnInfo
|
@ColumnInfo
|
||||||
var updateAt: Date? = null,
|
var updateAt: Date? = null,
|
||||||
|
@ColumnInfo(defaultValue = "30")
|
||||||
|
var syncInterval: SyncIntervalPreference = SyncIntervalPreference.default,
|
||||||
|
@ColumnInfo(defaultValue = "0")
|
||||||
|
var syncOnStart: SyncOnStartPreference = SyncOnStartPreference.default,
|
||||||
|
@ColumnInfo(defaultValue = "0")
|
||||||
|
var syncOnlyOnWiFi: SyncOnlyOnWiFiPreference = SyncOnlyOnWiFiPreference.default,
|
||||||
|
@ColumnInfo(defaultValue = "0")
|
||||||
|
var syncOnlyWhenCharging: SyncOnlyWhenChargingPreference = SyncOnlyWhenChargingPreference.default,
|
||||||
|
@ColumnInfo(defaultValue = "2592000000")
|
||||||
|
var keepArchived: KeepArchivedPreference = KeepArchivedPreference.default,
|
||||||
|
@ColumnInfo(defaultValue = "")
|
||||||
|
var syncBlockList: SyncBlockList = SyncBlockListPreference.default,
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,7 +1,14 @@
|
||||||
package me.ash.reader.data.model.account
|
package me.ash.reader.data.model.account
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.rounded.RssFeed
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
import androidx.room.TypeConverter
|
import androidx.room.TypeConverter
|
||||||
|
import me.ash.reader.R
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Each account will specify its local or third-party API type.
|
* Each account will specify its local or third-party API type.
|
||||||
|
@ -12,11 +19,35 @@ class AccountType(val id: Int) {
|
||||||
* Make sure the constructed object is valid.
|
* Make sure the constructed object is valid.
|
||||||
*/
|
*/
|
||||||
init {
|
init {
|
||||||
if (id < 1 || id > 3) {
|
if (id < 1 || id > 6) {
|
||||||
throw IllegalArgumentException("Account type id is not valid.")
|
throw IllegalArgumentException("Account type id is not valid.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun toDesc(context: Context): String =
|
||||||
|
when (this.id) {
|
||||||
|
1 -> context.getString(R.string.local)
|
||||||
|
2 -> context.getString(R.string.fever)
|
||||||
|
3 -> context.getString(R.string.google_reader)
|
||||||
|
4 -> context.getString(R.string.fresh_rss)
|
||||||
|
5 -> context.getString(R.string.feedlly)
|
||||||
|
6 -> context.getString(R.string.inoreader)
|
||||||
|
else -> context.getString(R.string.unknown)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
@Composable
|
||||||
|
fun toIcon(): Any =
|
||||||
|
when (this.id) {
|
||||||
|
1 -> Icons.Rounded.RssFeed
|
||||||
|
2 -> painterResource(id = R.drawable.ic_fever)
|
||||||
|
3 -> Icons.Rounded.RssFeed
|
||||||
|
4 -> painterResource(id = R.drawable.ic_freshrss)
|
||||||
|
5 -> painterResource(id = R.drawable.ic_feedly)
|
||||||
|
6 -> painterResource(id = R.drawable.ic_inoreader)
|
||||||
|
else -> Icons.Rounded.RssFeed
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Type of account currently supported.
|
* Type of account currently supported.
|
||||||
*/
|
*/
|
||||||
|
@ -25,6 +56,9 @@ class AccountType(val id: Int) {
|
||||||
val Local = AccountType(1)
|
val Local = AccountType(1)
|
||||||
val Fever = AccountType(2)
|
val Fever = AccountType(2)
|
||||||
val GoogleReader = AccountType(3)
|
val GoogleReader = AccountType(3)
|
||||||
|
val FreshRSS = AccountType(4)
|
||||||
|
val Feedlly = AccountType(5)
|
||||||
|
val Inoreader = AccountType(6)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
package me.ash.reader.data.model.account
|
||||||
|
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
import androidx.room.TypeConverter
|
||||||
|
import me.ash.reader.data.model.preference.KeepArchivedPreference
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide [TypeConverter] of [KeepArchivedPreference] for [RoomDatabase].
|
||||||
|
*/
|
||||||
|
class KeepArchivedConverters {
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun toKeepArchived(keepArchived: Long): KeepArchivedPreference {
|
||||||
|
return KeepArchivedPreference.values.find { it.value == keepArchived } ?: KeepArchivedPreference.default
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun fromKeepArchived(keepArchived: KeepArchivedPreference): Long {
|
||||||
|
return keepArchived.value
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
package me.ash.reader.data.model.account
|
||||||
|
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
import androidx.room.TypeConverter
|
||||||
|
import me.ash.reader.data.model.preference.SyncBlockList
|
||||||
|
import me.ash.reader.data.model.preference.SyncBlockListPreference
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide [TypeConverter] of [SyncBlockListPreference] for [RoomDatabase].
|
||||||
|
*/
|
||||||
|
class SyncBlockListConverters {
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun toBlockList(syncBlockList: String): SyncBlockList =
|
||||||
|
SyncBlockListPreference.of(syncBlockList)
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun fromBlockList(syncBlockList: SyncBlockList?): String =
|
||||||
|
SyncBlockListPreference.toString(syncBlockList ?: emptyList())
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
package me.ash.reader.data.model.account
|
||||||
|
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
import androidx.room.TypeConverter
|
||||||
|
import me.ash.reader.data.model.preference.SyncIntervalPreference
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide [TypeConverter] of [SyncIntervalPreference] for [RoomDatabase].
|
||||||
|
*/
|
||||||
|
class SyncIntervalConverters {
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun toSyncInterval(syncInterval: Long): SyncIntervalPreference {
|
||||||
|
return SyncIntervalPreference.values.find { it.value == syncInterval } ?: SyncIntervalPreference.default
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun fromSyncInterval(syncInterval: SyncIntervalPreference): Long {
|
||||||
|
return syncInterval.value
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
package me.ash.reader.data.model.account
|
||||||
|
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
import androidx.room.TypeConverter
|
||||||
|
import me.ash.reader.data.model.preference.SyncOnStartPreference
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide [TypeConverter] of [SyncOnStartPreference] for [RoomDatabase].
|
||||||
|
*/
|
||||||
|
class SyncOnStartConverters {
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun toSyncOnStart(syncOnStart: Boolean): SyncOnStartPreference {
|
||||||
|
return SyncOnStartPreference.values.find { it.value == syncOnStart } ?: SyncOnStartPreference.default
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun fromSyncOnStart(syncOnStart: SyncOnStartPreference): Boolean {
|
||||||
|
return syncOnStart.value
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
package me.ash.reader.data.model.account
|
||||||
|
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
import androidx.room.TypeConverter
|
||||||
|
import me.ash.reader.data.model.preference.SyncOnlyOnWiFiPreference
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide [TypeConverter] of [SyncOnlyOnWiFiPreference] for [RoomDatabase].
|
||||||
|
*/
|
||||||
|
class SyncOnlyOnWiFiConverters {
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun toSyncOnlyOnWiFi(syncOnlyOnWiFi: Boolean): SyncOnlyOnWiFiPreference {
|
||||||
|
return SyncOnlyOnWiFiPreference.values.find { it.value == syncOnlyOnWiFi } ?: SyncOnlyOnWiFiPreference.default
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun fromSyncOnlyOnWiFi(syncOnlyOnWiFi: SyncOnlyOnWiFiPreference): Boolean {
|
||||||
|
return syncOnlyOnWiFi.value
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
package me.ash.reader.data.model.account
|
||||||
|
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
import androidx.room.TypeConverter
|
||||||
|
import me.ash.reader.data.model.preference.SyncOnlyWhenChargingPreference
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide [TypeConverter] of [SyncOnlyWhenChargingPreference] for [RoomDatabase].
|
||||||
|
*/
|
||||||
|
class SyncOnlyWhenChargingConverters {
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun toSyncOnlyWhenCharging(syncOnlyWhenCharging: Boolean): SyncOnlyWhenChargingPreference {
|
||||||
|
return SyncOnlyWhenChargingPreference.values.find { it.value == syncOnlyWhenCharging }
|
||||||
|
?: SyncOnlyWhenChargingPreference.default
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun fromSyncOnlyWhenCharging(syncOnlyWhenCharging: SyncOnlyWhenChargingPreference): Boolean {
|
||||||
|
return syncOnlyWhenCharging.value
|
||||||
|
}
|
||||||
|
}
|
|
@ -40,12 +40,14 @@ data class Article(
|
||||||
var feedId: String,
|
var feedId: String,
|
||||||
@ColumnInfo(index = true)
|
@ColumnInfo(index = true)
|
||||||
var accountId: Int,
|
var accountId: Int,
|
||||||
@ColumnInfo(defaultValue = "true")
|
@ColumnInfo
|
||||||
var isUnread: Boolean = true,
|
var isUnread: Boolean = true,
|
||||||
@ColumnInfo(defaultValue = "false")
|
@ColumnInfo
|
||||||
var isStarred: Boolean = false,
|
var isStarred: Boolean = false,
|
||||||
@ColumnInfo(defaultValue = "false")
|
@ColumnInfo
|
||||||
var isReadLater: Boolean = false,
|
var isReadLater: Boolean = false,
|
||||||
|
@ColumnInfo
|
||||||
|
var updateAt: Date? = null,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@Ignore
|
@Ignore
|
||||||
|
|
|
@ -29,9 +29,9 @@ data class Feed(
|
||||||
var groupId: String,
|
var groupId: String,
|
||||||
@ColumnInfo(index = true)
|
@ColumnInfo(index = true)
|
||||||
var accountId: Int,
|
var accountId: Int,
|
||||||
@ColumnInfo(defaultValue = "false")
|
@ColumnInfo
|
||||||
var isNotification: Boolean = false,
|
var isNotification: Boolean = false,
|
||||||
@ColumnInfo(defaultValue = "false")
|
@ColumnInfo
|
||||||
var isFullContent: Boolean = false,
|
var isFullContent: Boolean = false,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
package me.ash.reader.data.model.preference
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.compositionLocalOf
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import me.ash.reader.data.dao.AccountDao
|
||||||
|
import me.ash.reader.ui.ext.collectAsStateValue
|
||||||
|
import me.ash.reader.ui.ext.currentAccountId
|
||||||
|
|
||||||
|
// Accounts
|
||||||
|
val LocalSyncInterval = compositionLocalOf<SyncIntervalPreference> { SyncIntervalPreference.default }
|
||||||
|
val LocalSyncOnStart = compositionLocalOf<SyncOnStartPreference> { SyncOnStartPreference.default }
|
||||||
|
val LocalSyncOnlyOnWiFi = compositionLocalOf<SyncOnlyOnWiFiPreference> { SyncOnlyOnWiFiPreference.default }
|
||||||
|
val LocalSyncOnlyWhenCharging =
|
||||||
|
compositionLocalOf<SyncOnlyWhenChargingPreference> { SyncOnlyWhenChargingPreference.default }
|
||||||
|
val LocalKeepArchived = compositionLocalOf<KeepArchivedPreference> { KeepArchivedPreference.default }
|
||||||
|
val LocalSyncBlockList = compositionLocalOf { SyncBlockListPreference.default }
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AccountSettingsProvider(
|
||||||
|
accountDao: AccountDao,
|
||||||
|
content: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val accountSettings = accountDao.queryAccount(context.currentAccountId).collectAsStateValue(initial = null)
|
||||||
|
|
||||||
|
CompositionLocalProvider(
|
||||||
|
// Accounts
|
||||||
|
LocalSyncInterval provides (accountSettings?.syncInterval ?: SyncIntervalPreference.default),
|
||||||
|
LocalSyncOnStart provides (accountSettings?.syncOnStart ?: SyncOnStartPreference.default),
|
||||||
|
LocalSyncOnlyOnWiFi provides (accountSettings?.syncOnlyOnWiFi ?: SyncOnlyOnWiFiPreference.default),
|
||||||
|
LocalSyncOnlyWhenCharging provides (accountSettings?.syncOnlyWhenCharging ?: SyncOnlyWhenChargingPreference.default),
|
||||||
|
LocalKeepArchived provides (accountSettings?.keepArchived ?: KeepArchivedPreference.default),
|
||||||
|
LocalSyncBlockList provides (accountSettings?.syncBlockList ?: SyncBlockListPreference.default),
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
package me.ash.reader.data.model.preference
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import me.ash.reader.R
|
||||||
|
import me.ash.reader.ui.page.settings.accounts.AccountViewModel
|
||||||
|
|
||||||
|
sealed class KeepArchivedPreference(
|
||||||
|
val value: Long,
|
||||||
|
) {
|
||||||
|
|
||||||
|
object Always : KeepArchivedPreference(0L)
|
||||||
|
object For1Day : KeepArchivedPreference(86400000L)
|
||||||
|
object For2Days : KeepArchivedPreference(172800000L)
|
||||||
|
object For3Days : KeepArchivedPreference(259200000L)
|
||||||
|
object For1Week : KeepArchivedPreference(604800000L)
|
||||||
|
object For2Weeks : KeepArchivedPreference(1209600000L)
|
||||||
|
object For1Month : KeepArchivedPreference(2592000000L)
|
||||||
|
|
||||||
|
fun put(accountId: Int, viewModel: AccountViewModel) {
|
||||||
|
viewModel.update(accountId) { keepArchived = this@KeepArchivedPreference }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toDesc(context: Context): String =
|
||||||
|
when (this) {
|
||||||
|
Always -> context.getString(R.string.always)
|
||||||
|
For1Day -> context.getString(R.string.for_1_day)
|
||||||
|
For2Days -> context.getString(R.string.for_2_days)
|
||||||
|
For3Days -> context.getString(R.string.for_3_days)
|
||||||
|
For1Week -> context.getString(R.string.for_1_week)
|
||||||
|
For2Weeks -> context.getString(R.string.for_2_weeks)
|
||||||
|
For1Month -> context.getString(R.string.for_1_month)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
val default = For1Month
|
||||||
|
val values = listOf(
|
||||||
|
Always,
|
||||||
|
For1Day,
|
||||||
|
For2Days,
|
||||||
|
For3Days,
|
||||||
|
For1Week,
|
||||||
|
For2Weeks,
|
||||||
|
For1Month,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
package me.ash.reader.data.model.preference
|
||||||
|
|
||||||
|
import me.ash.reader.ui.page.settings.accounts.AccountViewModel
|
||||||
|
|
||||||
|
typealias SyncBlockList = List<String>
|
||||||
|
|
||||||
|
object SyncBlockListPreference {
|
||||||
|
|
||||||
|
val default: SyncBlockList = emptyList()
|
||||||
|
|
||||||
|
fun put(accountId: Int, viewModel: AccountViewModel, syncBlockList: SyncBlockList) {
|
||||||
|
viewModel.update(accountId) { this.syncBlockList = syncBlockList }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun of(syncBlockList: String): SyncBlockList {
|
||||||
|
return syncBlockList.split("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toString(syncBlockList: SyncBlockList): String = syncBlockList
|
||||||
|
.filter { it.isNotBlank() }
|
||||||
|
.map { it.trim() }
|
||||||
|
.joinToString { "$it\n" }
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
package me.ash.reader.data.model.preference
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.work.PeriodicWorkRequest
|
||||||
|
import androidx.work.PeriodicWorkRequestBuilder
|
||||||
|
import me.ash.reader.R
|
||||||
|
import me.ash.reader.data.repository.SyncWorker
|
||||||
|
import me.ash.reader.ui.page.settings.accounts.AccountViewModel
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
sealed class SyncIntervalPreference(
|
||||||
|
val value: Long,
|
||||||
|
) {
|
||||||
|
|
||||||
|
object Manually : SyncIntervalPreference(0L)
|
||||||
|
object Every15Minutes : SyncIntervalPreference(15L)
|
||||||
|
object Every30Minutes : SyncIntervalPreference(30L)
|
||||||
|
object Every1Hour : SyncIntervalPreference(60L)
|
||||||
|
object Every2Hours : SyncIntervalPreference(120L)
|
||||||
|
object Every3Hours : SyncIntervalPreference(180L)
|
||||||
|
object Every6Hours : SyncIntervalPreference(360L)
|
||||||
|
object Every12Hours : SyncIntervalPreference(720L)
|
||||||
|
object Every1Day : SyncIntervalPreference(1440L)
|
||||||
|
|
||||||
|
fun put(accountId: Int, viewModel: AccountViewModel) {
|
||||||
|
viewModel.update(accountId) { syncInterval = this@SyncIntervalPreference }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toDesc(context: Context): String =
|
||||||
|
when (this) {
|
||||||
|
Manually -> context.getString(R.string.manually)
|
||||||
|
Every15Minutes -> context.getString(R.string.every_15_minutes)
|
||||||
|
Every30Minutes -> context.getString(R.string.every_30_minutes)
|
||||||
|
Every1Hour -> context.getString(R.string.every_1_hour)
|
||||||
|
Every2Hours -> context.getString(R.string.every_2_hours)
|
||||||
|
Every3Hours -> context.getString(R.string.every_3_hours)
|
||||||
|
Every6Hours -> context.getString(R.string.every_6_hours)
|
||||||
|
Every12Hours -> context.getString(R.string.every_12_hours)
|
||||||
|
Every1Day -> context.getString(R.string.every_1_day)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toPeriodicWorkRequestBuilder(): PeriodicWorkRequest.Builder =
|
||||||
|
PeriodicWorkRequestBuilder<SyncWorker>(value, TimeUnit.MINUTES)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
val default = Every30Minutes
|
||||||
|
val values = listOf(
|
||||||
|
Manually,
|
||||||
|
Every15Minutes,
|
||||||
|
Every30Minutes,
|
||||||
|
Every1Hour,
|
||||||
|
Every2Hours,
|
||||||
|
Every3Hours,
|
||||||
|
Every6Hours,
|
||||||
|
Every12Hours,
|
||||||
|
Every1Day,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
package me.ash.reader.data.model.preference
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import me.ash.reader.R
|
||||||
|
import me.ash.reader.ui.page.settings.accounts.AccountViewModel
|
||||||
|
|
||||||
|
sealed class SyncOnStartPreference(
|
||||||
|
val value: Boolean,
|
||||||
|
) {
|
||||||
|
|
||||||
|
object On : SyncOnStartPreference(true)
|
||||||
|
object Off : SyncOnStartPreference(false)
|
||||||
|
|
||||||
|
fun put(accountId: Int, viewModel: AccountViewModel) {
|
||||||
|
viewModel.update(accountId) { syncOnStart = this@SyncOnStartPreference }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toDesc(context: Context): String =
|
||||||
|
when (this) {
|
||||||
|
On -> context.getString(R.string.on)
|
||||||
|
Off -> context.getString(R.string.off)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
val default = Off
|
||||||
|
val values = listOf(On, Off)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun SyncOnStartPreference.not(): SyncOnStartPreference =
|
||||||
|
when (value) {
|
||||||
|
true -> SyncOnStartPreference.Off
|
||||||
|
false -> SyncOnStartPreference.On
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
package me.ash.reader.data.model.preference
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import me.ash.reader.R
|
||||||
|
import me.ash.reader.ui.page.settings.accounts.AccountViewModel
|
||||||
|
|
||||||
|
sealed class SyncOnlyOnWiFiPreference(
|
||||||
|
val value: Boolean,
|
||||||
|
) {
|
||||||
|
|
||||||
|
object On : SyncOnlyOnWiFiPreference(true)
|
||||||
|
object Off : SyncOnlyOnWiFiPreference(false)
|
||||||
|
|
||||||
|
fun put(accountId: Int, viewModel: AccountViewModel) {
|
||||||
|
viewModel.update(accountId) { syncOnlyOnWiFi = this@SyncOnlyOnWiFiPreference }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toDesc(context: Context): String =
|
||||||
|
when (this) {
|
||||||
|
On -> context.getString(R.string.on)
|
||||||
|
Off -> context.getString(R.string.off)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
val default = Off
|
||||||
|
val values = listOf(On, Off)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun SyncOnlyOnWiFiPreference.not(): SyncOnlyOnWiFiPreference =
|
||||||
|
when (value) {
|
||||||
|
true -> SyncOnlyOnWiFiPreference.Off
|
||||||
|
false -> SyncOnlyOnWiFiPreference.On
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
package me.ash.reader.data.model.preference
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import me.ash.reader.R
|
||||||
|
import me.ash.reader.ui.page.settings.accounts.AccountViewModel
|
||||||
|
|
||||||
|
sealed class SyncOnlyWhenChargingPreference(
|
||||||
|
val value: Boolean,
|
||||||
|
) {
|
||||||
|
|
||||||
|
object On : SyncOnlyWhenChargingPreference(true)
|
||||||
|
object Off : SyncOnlyWhenChargingPreference(false)
|
||||||
|
|
||||||
|
fun put(accountId: Int, viewModel: AccountViewModel) {
|
||||||
|
viewModel.update(accountId) { syncOnlyWhenCharging = this@SyncOnlyWhenChargingPreference }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toDesc(context: Context): String =
|
||||||
|
when (this) {
|
||||||
|
On -> context.getString(R.string.on)
|
||||||
|
Off -> context.getString(R.string.off)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
val default = Off
|
||||||
|
val values = listOf(On, Off)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun SyncOnlyWhenChargingPreference.not(): SyncOnlyWhenChargingPreference =
|
||||||
|
when (value) {
|
||||||
|
true -> SyncOnlyWhenChargingPreference.Off
|
||||||
|
false -> SyncOnlyWhenChargingPreference.On
|
||||||
|
}
|
|
@ -4,7 +4,6 @@ import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.paging.PagingSource
|
import androidx.paging.PagingSource
|
||||||
import androidx.work.CoroutineWorker
|
import androidx.work.CoroutineWorker
|
||||||
import androidx.work.ExistingPeriodicWorkPolicy
|
|
||||||
import androidx.work.ListenableWorker
|
import androidx.work.ListenableWorker
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
@ -20,6 +19,8 @@ import me.ash.reader.data.model.article.ArticleWithFeed
|
||||||
import me.ash.reader.data.model.feed.Feed
|
import me.ash.reader.data.model.feed.Feed
|
||||||
import me.ash.reader.data.model.group.Group
|
import me.ash.reader.data.model.group.Group
|
||||||
import me.ash.reader.data.model.group.GroupWithFeed
|
import me.ash.reader.data.model.group.GroupWithFeed
|
||||||
|
import me.ash.reader.data.model.preference.KeepArchivedPreference
|
||||||
|
import me.ash.reader.data.model.preference.SyncIntervalPreference
|
||||||
import me.ash.reader.ui.ext.currentAccountId
|
import me.ash.reader.ui.ext.currentAccountId
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
@ -50,12 +51,41 @@ abstract class AbstractRssRepository constructor(
|
||||||
isUnread: Boolean,
|
isUnread: Boolean,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun doSync() {
|
suspend fun keepArchivedArticles() {
|
||||||
workManager.enqueueUniquePeriodicWork(
|
accountDao.queryById(context.currentAccountId)!!
|
||||||
SyncWorker.WORK_NAME,
|
.takeIf { it.keepArchived != KeepArchivedPreference.Always }
|
||||||
ExistingPeriodicWorkPolicy.REPLACE,
|
?.let {
|
||||||
SyncWorker.repeatingRequest
|
articleDao.deleteAllArchivedBeforeThan(it.id!!, Date(System.currentTimeMillis() - it.keepArchived.value))
|
||||||
)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun doSync(isOnStart: Boolean = false) {
|
||||||
|
workManager.cancelAllWork()
|
||||||
|
accountDao.queryById(context.currentAccountId)?.let {
|
||||||
|
if (isOnStart) {
|
||||||
|
if (it.syncOnStart.value) {
|
||||||
|
SyncWorker.enqueueOneTimeWork(workManager)
|
||||||
|
}
|
||||||
|
if (it.syncInterval != SyncIntervalPreference.Manually) {
|
||||||
|
SyncWorker.enqueuePeriodicWork(
|
||||||
|
workManager = workManager,
|
||||||
|
syncInterval = it.syncInterval,
|
||||||
|
syncOnlyWhenCharging = it.syncOnlyWhenCharging,
|
||||||
|
syncOnlyOnWiFi = it.syncOnlyOnWiFi,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
SyncWorker.enqueueOneTimeWork(workManager)
|
||||||
|
SyncWorker.enqueuePeriodicWork(
|
||||||
|
workManager = workManager,
|
||||||
|
syncInterval = it.syncInterval,
|
||||||
|
syncOnlyWhenCharging = it.syncOnlyWhenCharging,
|
||||||
|
syncOnlyOnWiFi = it.syncOnlyOnWiFi,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pullGroups(): Flow<MutableList<Group>> =
|
fun pullGroups(): Flow<MutableList<Group>> =
|
||||||
|
@ -155,6 +185,10 @@ abstract class AbstractRssRepository constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun deleteAccountArticles(accountId: Int) {
|
||||||
|
articleDao.deleteByAccountId(accountId)
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun groupParseFullContent(group: Group, isFullContent: Boolean) {
|
suspend fun groupParseFullContent(group: Group, isFullContent: Boolean) {
|
||||||
feedDao.updateIsFullContentByGroupId(context.currentAccountId, group.id, isFullContent)
|
feedDao.updateIsFullContentByGroupId(context.currentAccountId, group.id, isFullContent)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,14 +2,19 @@ package me.ash.reader.data.repository
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import me.ash.reader.R
|
import me.ash.reader.R
|
||||||
import me.ash.reader.data.dao.AccountDao
|
import me.ash.reader.data.dao.AccountDao
|
||||||
|
import me.ash.reader.data.dao.ArticleDao
|
||||||
|
import me.ash.reader.data.dao.FeedDao
|
||||||
import me.ash.reader.data.dao.GroupDao
|
import me.ash.reader.data.dao.GroupDao
|
||||||
import me.ash.reader.data.model.account.Account
|
import me.ash.reader.data.model.account.Account
|
||||||
import me.ash.reader.data.model.account.AccountType
|
import me.ash.reader.data.model.account.AccountType
|
||||||
import me.ash.reader.data.model.group.Group
|
import me.ash.reader.data.model.group.Group
|
||||||
import me.ash.reader.ui.ext.currentAccountId
|
import me.ash.reader.ui.ext.currentAccountId
|
||||||
import me.ash.reader.ui.ext.getDefaultGroupId
|
import me.ash.reader.ui.ext.getDefaultGroupId
|
||||||
|
import me.ash.reader.ui.ext.showToast
|
||||||
|
import me.ash.reader.ui.ext.showToastLong
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class AccountRepository @Inject constructor(
|
class AccountRepository @Inject constructor(
|
||||||
|
@ -17,9 +22,14 @@ class AccountRepository @Inject constructor(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val accountDao: AccountDao,
|
private val accountDao: AccountDao,
|
||||||
private val groupDao: GroupDao,
|
private val groupDao: GroupDao,
|
||||||
|
private val feedDao: FeedDao,
|
||||||
|
private val articleDao: ArticleDao,
|
||||||
) {
|
) {
|
||||||
|
fun getAccounts(): Flow<List<Account>> = accountDao.queryAllAsFlow()
|
||||||
|
|
||||||
suspend fun getCurrentAccount(): Account? = accountDao.queryById(context.currentAccountId)
|
fun getAccountById(accountId: Int): Flow<Account?> = accountDao.queryAccount(accountId)
|
||||||
|
|
||||||
|
suspend fun getCurrentAccount(): Account = accountDao.queryById(context.currentAccountId)!!
|
||||||
|
|
||||||
suspend fun isNoAccount(): Boolean = accountDao.queryAll().isEmpty()
|
suspend fun isNoAccount(): Boolean = accountDao.queryAll().isEmpty()
|
||||||
|
|
||||||
|
@ -43,4 +53,24 @@ class AccountRepository @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun update(accountId: Int, block: Account.() -> Unit) {
|
||||||
|
accountDao.queryById(accountId)?.let {
|
||||||
|
accountDao.update(it.apply(block))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun delete(accountId: Int) {
|
||||||
|
if (accountDao.queryAll().size == 1) {
|
||||||
|
context.showToast(context.getString(R.string.must_have_an_account))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
accountDao.queryById(accountId)?.let {
|
||||||
|
articleDao.deleteByAccountId(accountId)
|
||||||
|
feedDao.deleteByAccountId(accountId)
|
||||||
|
groupDao.deleteByAccountId(accountId)
|
||||||
|
accountDao.delete(it)
|
||||||
|
context.showToastLong(context.getString(R.string.delete_account_toast))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,10 +72,10 @@ class LocalRssRepository @Inject constructor(
|
||||||
|
|
||||||
override suspend fun sync(coroutineWorker: CoroutineWorker): ListenableWorker.Result =
|
override suspend fun sync(coroutineWorker: CoroutineWorker): ListenableWorker.Result =
|
||||||
supervisorScope {
|
supervisorScope {
|
||||||
|
coroutineWorker.setProgress(setIsSyncing(true))
|
||||||
val preTime = System.currentTimeMillis()
|
val preTime = System.currentTimeMillis()
|
||||||
val accountId = context.currentAccountId
|
val accountId = context.currentAccountId
|
||||||
feedDao.queryAll(accountId)
|
feedDao.queryAll(accountId)
|
||||||
.also { coroutineWorker.setProgress(setIsSyncing(true)) }
|
|
||||||
.chunked(16)
|
.chunked(16)
|
||||||
.forEach {
|
.forEach {
|
||||||
it.map { feed -> async { syncFeed(feed) } }
|
it.map { feed -> async { syncFeed(feed) } }
|
||||||
|
|
|
@ -126,6 +126,7 @@ class RssHelper @Inject constructor(
|
||||||
fullContent = content,
|
fullContent = content,
|
||||||
img = findImg((content ?: desc) ?: ""),
|
img = findImg((content ?: desc) ?: ""),
|
||||||
link = syndEntry.link ?: "",
|
link = syndEntry.link ?: "",
|
||||||
|
updateAt = Date(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,9 @@ class RssRepository @Inject constructor(
|
||||||
// private val googleReaderRssRepository: GoogleReaderRssRepository,
|
// private val googleReaderRssRepository: GoogleReaderRssRepository,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun get() = when (context.currentAccountType) {
|
fun get() = get(context.currentAccountType)
|
||||||
|
|
||||||
|
fun get(accountId: Int) = when (accountId) {
|
||||||
AccountType.Local.id -> localRssRepository
|
AccountType.Local.id -> localRssRepository
|
||||||
// Account.Type.LOCAL -> feverRssRepository
|
// Account.Type.LOCAL -> feverRssRepository
|
||||||
// Account.Type.FEVER -> feverRssRepository
|
// Account.Type.FEVER -> feverRssRepository
|
||||||
|
|
|
@ -8,6 +8,9 @@ import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import me.ash.reader.data.model.preference.SyncIntervalPreference
|
||||||
|
import me.ash.reader.data.model.preference.SyncOnlyOnWiFiPreference
|
||||||
|
import me.ash.reader.data.model.preference.SyncOnlyWhenChargingPreference
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
@ -15,31 +18,55 @@ import java.util.concurrent.TimeUnit
|
||||||
class SyncWorker @AssistedInject constructor(
|
class SyncWorker @AssistedInject constructor(
|
||||||
@Assisted context: Context,
|
@Assisted context: Context,
|
||||||
@Assisted workerParams: WorkerParameters,
|
@Assisted workerParams: WorkerParameters,
|
||||||
|
private val accountRepository: AccountRepository,
|
||||||
private val rssRepository: RssRepository,
|
private val rssRepository: RssRepository,
|
||||||
) : CoroutineWorker(context, workerParams) {
|
) : CoroutineWorker(context, workerParams) {
|
||||||
|
|
||||||
override suspend fun doWork(): Result =
|
override suspend fun doWork(): Result =
|
||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
Log.i("RLog", "doWork: ")
|
Log.i("RLog", "doWork: ")
|
||||||
rssRepository.get().sync(this@SyncWorker)
|
rssRepository.get().sync(this@SyncWorker).also {
|
||||||
|
rssRepository.get().keepArchivedArticles()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
const val WORK_NAME = "article.sync"
|
private const val IS_SYNCING = "isSyncing"
|
||||||
|
const val WORK_NAME = "ReadYou"
|
||||||
|
lateinit var uuid: UUID
|
||||||
|
|
||||||
val uuid: UUID
|
fun enqueueOneTimeWork(
|
||||||
|
workManager: WorkManager,
|
||||||
val repeatingRequest = PeriodicWorkRequestBuilder<SyncWorker>(
|
) {
|
||||||
15, TimeUnit.MINUTES
|
workManager.enqueue(OneTimeWorkRequestBuilder<SyncWorker>()
|
||||||
).setConstraints(
|
.addTag(WORK_NAME)
|
||||||
Constraints.Builder()
|
|
||||||
.build()
|
.build()
|
||||||
).addTag(WORK_NAME).build().also {
|
)
|
||||||
uuid = it.id
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setIsSyncing(boolean: Boolean) = workDataOf("isSyncing" to boolean)
|
fun enqueuePeriodicWork(
|
||||||
fun Data.getIsSyncing(): Boolean = getBoolean("isSyncing", false)
|
workManager: WorkManager,
|
||||||
|
syncInterval: SyncIntervalPreference,
|
||||||
|
syncOnlyWhenCharging: SyncOnlyWhenChargingPreference,
|
||||||
|
syncOnlyOnWiFi: SyncOnlyOnWiFiPreference,
|
||||||
|
) {
|
||||||
|
workManager.enqueueUniquePeriodicWork(
|
||||||
|
WORK_NAME,
|
||||||
|
ExistingPeriodicWorkPolicy.REPLACE,
|
||||||
|
PeriodicWorkRequestBuilder<SyncWorker>(syncInterval.value, TimeUnit.MINUTES)
|
||||||
|
.setConstraints(Constraints.Builder()
|
||||||
|
.setRequiresCharging(syncOnlyWhenCharging.value)
|
||||||
|
.setRequiredNetworkType(if (syncOnlyOnWiFi.value) NetworkType.UNMETERED else NetworkType.CONNECTED)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.addTag(WORK_NAME)
|
||||||
|
.setInitialDelay(syncInterval.value, TimeUnit.MINUTES)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setIsSyncing(boolean: Boolean) = workDataOf(IS_SYNCING to boolean)
|
||||||
|
fun Data.getIsSyncing(): Boolean = getBoolean(IS_SYNCING, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,18 +8,28 @@ import me.ash.reader.data.dao.AccountDao
|
||||||
import me.ash.reader.data.dao.ArticleDao
|
import me.ash.reader.data.dao.ArticleDao
|
||||||
import me.ash.reader.data.dao.FeedDao
|
import me.ash.reader.data.dao.FeedDao
|
||||||
import me.ash.reader.data.dao.GroupDao
|
import me.ash.reader.data.dao.GroupDao
|
||||||
import me.ash.reader.data.model.account.Account
|
import me.ash.reader.data.model.account.*
|
||||||
import me.ash.reader.data.model.account.AccountTypeConverters
|
|
||||||
import me.ash.reader.data.model.article.Article
|
import me.ash.reader.data.model.article.Article
|
||||||
import me.ash.reader.data.model.feed.Feed
|
import me.ash.reader.data.model.feed.Feed
|
||||||
import me.ash.reader.data.model.group.Group
|
import me.ash.reader.data.model.group.Group
|
||||||
|
import me.ash.reader.data.model.preference.*
|
||||||
|
import me.ash.reader.ui.ext.toInt
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
entities = [Account::class, Feed::class, Article::class, Group::class],
|
entities = [Account::class, Feed::class, Article::class, Group::class],
|
||||||
version = 2
|
version = 3
|
||||||
|
)
|
||||||
|
@TypeConverters(
|
||||||
|
RYDatabase.DateConverters::class,
|
||||||
|
AccountTypeConverters::class,
|
||||||
|
SyncIntervalConverters::class,
|
||||||
|
SyncOnStartConverters::class,
|
||||||
|
SyncOnlyOnWiFiConverters::class,
|
||||||
|
SyncOnlyWhenChargingConverters::class,
|
||||||
|
KeepArchivedConverters::class,
|
||||||
|
SyncBlockListConverters::class,
|
||||||
)
|
)
|
||||||
@TypeConverters(RYDatabase.DateConverters::class, AccountTypeConverters::class)
|
|
||||||
abstract class RYDatabase : RoomDatabase() {
|
abstract class RYDatabase : RoomDatabase() {
|
||||||
|
|
||||||
abstract fun accountDao(): AccountDao
|
abstract fun accountDao(): AccountDao
|
||||||
|
@ -60,6 +70,7 @@ abstract class RYDatabase : RoomDatabase() {
|
||||||
|
|
||||||
val allMigrations = arrayOf(
|
val allMigrations = arrayOf(
|
||||||
MIGRATION_1_2,
|
MIGRATION_1_2,
|
||||||
|
MIGRATION_2_3,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Suppress("ClassName")
|
@Suppress("ClassName")
|
||||||
|
@ -73,3 +84,45 @@ object MIGRATION_1_2 : Migration(1, 2) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("ClassName")
|
||||||
|
object MIGRATION_2_3 : Migration(2, 3) {
|
||||||
|
|
||||||
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
|
database.execSQL(
|
||||||
|
"""
|
||||||
|
ALTER TABLE article ADD COLUMN updateAt INTEGER DEFAULT ${System.currentTimeMillis()}
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
database.execSQL(
|
||||||
|
"""
|
||||||
|
ALTER TABLE account ADD COLUMN syncInterval INTEGER NOT NULL DEFAULT ${SyncIntervalPreference.default.value}
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
database.execSQL(
|
||||||
|
"""
|
||||||
|
ALTER TABLE account ADD COLUMN syncOnStart INTEGER NOT NULL DEFAULT ${SyncOnStartPreference.default.value.toInt()}
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
database.execSQL(
|
||||||
|
"""
|
||||||
|
ALTER TABLE account ADD COLUMN syncOnlyOnWiFi INTEGER NOT NULL DEFAULT ${SyncOnlyOnWiFiPreference.default.value.toInt()}
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
database.execSQL(
|
||||||
|
"""
|
||||||
|
ALTER TABLE account ADD COLUMN syncOnlyWhenCharging INTEGER NOT NULL DEFAULT ${SyncOnlyWhenChargingPreference.default.value.toInt()}
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
database.execSQL(
|
||||||
|
"""
|
||||||
|
ALTER TABLE account ADD COLUMN keepArchived INTEGER NOT NULL DEFAULT ${KeepArchivedPreference.default.value}
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
database.execSQL(
|
||||||
|
"""
|
||||||
|
ALTER TABLE account ADD COLUMN syncBlockList TEXT NOT NULL DEFAULT ''
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ fun ClipboardTextField(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
readOnly: Boolean = false,
|
readOnly: Boolean = false,
|
||||||
value: String = "",
|
value: String = "",
|
||||||
|
singleLine: Boolean = true,
|
||||||
onValueChange: (String) -> Unit = {},
|
onValueChange: (String) -> Unit = {},
|
||||||
placeholder: String = "",
|
placeholder: String = "",
|
||||||
errorText: String = "",
|
errorText: String = "",
|
||||||
|
@ -35,6 +36,7 @@ fun ClipboardTextField(
|
||||||
RYTextField(
|
RYTextField(
|
||||||
readOnly = readOnly,
|
readOnly = readOnly,
|
||||||
value = value,
|
value = value,
|
||||||
|
singleLine = singleLine,
|
||||||
onValueChange = onValueChange,
|
onValueChange = onValueChange,
|
||||||
placeholder = placeholder,
|
placeholder = placeholder,
|
||||||
errorMessage = errorText,
|
errorMessage = errorText,
|
||||||
|
|
|
@ -22,6 +22,7 @@ import me.ash.reader.R
|
||||||
fun RYTextField(
|
fun RYTextField(
|
||||||
readOnly: Boolean,
|
readOnly: Boolean,
|
||||||
value: String,
|
value: String,
|
||||||
|
singleLine: Boolean = true,
|
||||||
onValueChange: (String) -> Unit,
|
onValueChange: (String) -> Unit,
|
||||||
placeholder: String,
|
placeholder: String,
|
||||||
errorMessage: String,
|
errorMessage: String,
|
||||||
|
@ -41,7 +42,7 @@ fun RYTextField(
|
||||||
colors = TextFieldDefaults.textFieldColors(
|
colors = TextFieldDefaults.textFieldColors(
|
||||||
containerColor = Color.Transparent,
|
containerColor = Color.Transparent,
|
||||||
),
|
),
|
||||||
maxLines = 1,
|
maxLines = if (singleLine) 1 else Int.MAX_VALUE,
|
||||||
enabled = !readOnly,
|
enabled = !readOnly,
|
||||||
value = value,
|
value = value,
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
|
@ -55,7 +56,7 @@ fun RYTextField(
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
isError = errorMessage.isNotEmpty(),
|
isError = errorMessage.isNotEmpty(),
|
||||||
singleLine = true,
|
singleLine = singleLine,
|
||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
if (value.isNotEmpty()) {
|
if (value.isNotEmpty()) {
|
||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
|
|
|
@ -21,6 +21,7 @@ fun TextFieldDialog(
|
||||||
properties: DialogProperties = DialogProperties(),
|
properties: DialogProperties = DialogProperties(),
|
||||||
visible: Boolean = false,
|
visible: Boolean = false,
|
||||||
readOnly: Boolean = false,
|
readOnly: Boolean = false,
|
||||||
|
singleLine: Boolean = true,
|
||||||
title: String = "",
|
title: String = "",
|
||||||
icon: ImageVector? = null,
|
icon: ImageVector? = null,
|
||||||
value: String = "",
|
value: String = "",
|
||||||
|
@ -31,7 +32,7 @@ fun TextFieldDialog(
|
||||||
onValueChange: (String) -> Unit = {},
|
onValueChange: (String) -> Unit = {},
|
||||||
onDismissRequest: () -> Unit = {},
|
onDismissRequest: () -> Unit = {},
|
||||||
onConfirm: (String) -> Unit = {},
|
onConfirm: (String) -> Unit = {},
|
||||||
imeAction: ImeAction = ImeAction.Done,
|
imeAction: ImeAction = if (singleLine) ImeAction.Done else ImeAction.Default,
|
||||||
) {
|
) {
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
|
|
||||||
|
@ -55,6 +56,7 @@ fun TextFieldDialog(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
readOnly = readOnly,
|
readOnly = readOnly,
|
||||||
value = value,
|
value = value,
|
||||||
|
singleLine = singleLine,
|
||||||
onValueChange = onValueChange,
|
onValueChange = onValueChange,
|
||||||
placeholder = placeholder,
|
placeholder = placeholder,
|
||||||
errorText = errorText,
|
errorText = errorText,
|
||||||
|
|
3
app/src/main/java/me/ash/reader/ui/ext/BooleanExt.kt
Normal file
3
app/src/main/java/me/ash/reader/ui/ext/BooleanExt.kt
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
package me.ash.reader.ui.ext
|
||||||
|
|
||||||
|
fun Boolean.toInt(): Int = if (this) 1 else 0
|
|
@ -5,6 +5,7 @@ import android.content.Context
|
||||||
import android.content.ContextWrapper
|
import android.content.ContextWrapper
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Looper
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
|
@ -48,9 +49,11 @@ fun Context.installLatestApk() {
|
||||||
private var toast: Toast? = null
|
private var toast: Toast? = null
|
||||||
|
|
||||||
fun Context.showToast(message: String?, duration: Int = Toast.LENGTH_SHORT) {
|
fun Context.showToast(message: String?, duration: Int = Toast.LENGTH_SHORT) {
|
||||||
|
Looper.myLooper() ?: Looper.prepare()
|
||||||
toast?.cancel()
|
toast?.cancel()
|
||||||
toast = Toast.makeText(this, message, duration)
|
toast = Toast.makeText(this, message, duration)
|
||||||
toast?.show()
|
toast?.show()
|
||||||
|
Looper.loop()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Context.showToastLong(message: String?) {
|
fun Context.showToastLong(message: String?) {
|
||||||
|
|
|
@ -3,3 +3,5 @@ package me.ash.reader.ui.ext
|
||||||
fun Int.spacerDollar(str: Any): String = "$this$$str"
|
fun Int.spacerDollar(str: Any): String = "$this$$str"
|
||||||
|
|
||||||
fun Int.getDefaultGroupId() = this.spacerDollar("read_you_app_default_group")
|
fun Int.getDefaultGroupId() = this.spacerDollar("read_you_app_default_group")
|
||||||
|
|
||||||
|
fun Int.toBoolean(): Boolean = this != 0
|
||||||
|
|
|
@ -24,6 +24,9 @@ import me.ash.reader.ui.page.home.feeds.FeedsPage
|
||||||
import me.ash.reader.ui.page.home.flow.FlowPage
|
import me.ash.reader.ui.page.home.flow.FlowPage
|
||||||
import me.ash.reader.ui.page.home.reading.ReadingPage
|
import me.ash.reader.ui.page.home.reading.ReadingPage
|
||||||
import me.ash.reader.ui.page.settings.SettingsPage
|
import me.ash.reader.ui.page.settings.SettingsPage
|
||||||
|
import me.ash.reader.ui.page.settings.accounts.AccountDetailsPage
|
||||||
|
import me.ash.reader.ui.page.settings.accounts.AccountsPage
|
||||||
|
import me.ash.reader.ui.page.settings.accounts.AddAccountsPage
|
||||||
import me.ash.reader.ui.page.settings.color.ColorAndStylePage
|
import me.ash.reader.ui.page.settings.color.ColorAndStylePage
|
||||||
import me.ash.reader.ui.page.settings.color.DarkThemePage
|
import me.ash.reader.ui.page.settings.color.DarkThemePage
|
||||||
import me.ash.reader.ui.page.settings.color.feeds.FeedsPageStylePage
|
import me.ash.reader.ui.page.settings.color.feeds.FeedsPageStylePage
|
||||||
|
@ -140,6 +143,19 @@ fun HomeEntry(
|
||||||
SettingsPage(navController)
|
SettingsPage(navController)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Accounts
|
||||||
|
animatedComposable(route = RouteName.ACCOUNTS) {
|
||||||
|
AccountsPage(navController)
|
||||||
|
}
|
||||||
|
|
||||||
|
animatedComposable(route = "${RouteName.ACCOUNT_DETAILS}/{accountId}") {
|
||||||
|
AccountDetailsPage(navController)
|
||||||
|
}
|
||||||
|
|
||||||
|
animatedComposable(route = RouteName.ADD_ACCOUNTS) {
|
||||||
|
AddAccountsPage(navController)
|
||||||
|
}
|
||||||
|
|
||||||
// Color & Style
|
// Color & Style
|
||||||
animatedComposable(route = RouteName.COLOR_AND_STYLE) {
|
animatedComposable(route = RouteName.COLOR_AND_STYLE) {
|
||||||
ColorAndStylePage(navController)
|
ColorAndStylePage(navController)
|
||||||
|
|
|
@ -13,6 +13,11 @@ object RouteName {
|
||||||
// Settings
|
// Settings
|
||||||
const val SETTINGS = "settings"
|
const val SETTINGS = "settings"
|
||||||
|
|
||||||
|
// Accounts
|
||||||
|
const val ACCOUNTS = "accounts"
|
||||||
|
const val ACCOUNT_DETAILS = "account_details"
|
||||||
|
const val ADD_ACCOUNTS = "add_accounts"
|
||||||
|
|
||||||
// Color & Style
|
// Color & Style
|
||||||
const val COLOR_AND_STYLE = "color_and_style"
|
const val COLOR_AND_STYLE = "color_and_style"
|
||||||
const val DARK_THEME = "dark_theme"
|
const val DARK_THEME = "dark_theme"
|
||||||
|
|
|
@ -1,20 +1,24 @@
|
||||||
package me.ash.reader.ui.page.home
|
package me.ash.reader.ui.page.home
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.paging.Pager
|
import androidx.paging.Pager
|
||||||
import androidx.paging.PagingConfig
|
import androidx.paging.PagingConfig
|
||||||
import androidx.paging.PagingData
|
import androidx.paging.PagingData
|
||||||
import androidx.paging.cachedIn
|
import androidx.paging.cachedIn
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import me.ash.reader.data.model.article.ArticleFlowItem
|
import me.ash.reader.data.model.article.ArticleFlowItem
|
||||||
import me.ash.reader.data.model.article.mapPagingFlowItem
|
import me.ash.reader.data.model.article.mapPagingFlowItem
|
||||||
import me.ash.reader.data.model.feed.Feed
|
import me.ash.reader.data.model.feed.Feed
|
||||||
import me.ash.reader.data.model.general.Filter
|
import me.ash.reader.data.model.general.Filter
|
||||||
import me.ash.reader.data.model.group.Group
|
import me.ash.reader.data.model.group.Group
|
||||||
import me.ash.reader.data.module.ApplicationScope
|
import me.ash.reader.data.module.ApplicationScope
|
||||||
|
import me.ash.reader.data.module.IODispatcher
|
||||||
import me.ash.reader.data.repository.RssRepository
|
import me.ash.reader.data.repository.RssRepository
|
||||||
import me.ash.reader.data.repository.StringsRepository
|
import me.ash.reader.data.repository.StringsRepository
|
||||||
import me.ash.reader.data.repository.SyncWorker
|
import me.ash.reader.data.repository.SyncWorker
|
||||||
|
@ -27,6 +31,8 @@ class HomeViewModel @Inject constructor(
|
||||||
@ApplicationScope
|
@ApplicationScope
|
||||||
private val applicationScope: CoroutineScope,
|
private val applicationScope: CoroutineScope,
|
||||||
private val workManager: WorkManager,
|
private val workManager: WorkManager,
|
||||||
|
@IODispatcher
|
||||||
|
private val ioDispatcher: CoroutineDispatcher,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _homeUiState = MutableStateFlow(HomeUiState())
|
private val _homeUiState = MutableStateFlow(HomeUiState())
|
||||||
|
@ -35,10 +41,12 @@ class HomeViewModel @Inject constructor(
|
||||||
private val _filterUiState = MutableStateFlow(FilterState())
|
private val _filterUiState = MutableStateFlow(FilterState())
|
||||||
val filterUiState = _filterUiState.asStateFlow()
|
val filterUiState = _filterUiState.asStateFlow()
|
||||||
|
|
||||||
val syncWorkLiveData = workManager.getWorkInfoByIdLiveData(SyncWorker.uuid)
|
val syncWorkLiveData = workManager.getWorkInfosByTagLiveData(SyncWorker.WORK_NAME)
|
||||||
|
|
||||||
fun sync() {
|
fun sync() {
|
||||||
rssRepository.get().doSync()
|
viewModelScope.launch(ioDispatcher) {
|
||||||
|
rssRepository.get().doSync()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun changeFilter(filterState: FilterState) {
|
fun changeFilter(filterState: FilterState) {
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
package me.ash.reader.ui.page.home.feeds
|
package me.ash.reader.ui.page.home.feeds
|
||||||
|
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.compose.animation.core.*
|
import androidx.compose.animation.core.*
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
|
@ -30,10 +28,7 @@ import me.ash.reader.data.model.preference.*
|
||||||
import me.ash.reader.data.repository.SyncWorker.Companion.getIsSyncing
|
import me.ash.reader.data.repository.SyncWorker.Companion.getIsSyncing
|
||||||
import me.ash.reader.ui.component.FilterBar
|
import me.ash.reader.ui.component.FilterBar
|
||||||
import me.ash.reader.ui.component.base.*
|
import me.ash.reader.ui.component.base.*
|
||||||
import me.ash.reader.ui.ext.alphaLN
|
import me.ash.reader.ui.ext.*
|
||||||
import me.ash.reader.ui.ext.collectAsStateValue
|
|
||||||
import me.ash.reader.ui.ext.findActivity
|
|
||||||
import me.ash.reader.ui.ext.getCurrentVersion
|
|
||||||
import me.ash.reader.ui.page.common.RouteName
|
import me.ash.reader.ui.page.common.RouteName
|
||||||
import me.ash.reader.ui.page.home.FilterState
|
import me.ash.reader.ui.page.home.FilterState
|
||||||
import me.ash.reader.ui.page.home.HomeViewModel
|
import me.ash.reader.ui.page.home.HomeViewModel
|
||||||
|
@ -76,7 +71,7 @@ fun FeedsPage(
|
||||||
val owner = LocalLifecycleOwner.current
|
val owner = LocalLifecycleOwner.current
|
||||||
var isSyncing by remember { mutableStateOf(false) }
|
var isSyncing by remember { mutableStateOf(false) }
|
||||||
homeViewModel.syncWorkLiveData.observe(owner) {
|
homeViewModel.syncWorkLiveData.observe(owner) {
|
||||||
it?.let { isSyncing = it.progress.getIsSyncing() }
|
it?.let { isSyncing = it.any { it.progress.getIsSyncing() } }
|
||||||
}
|
}
|
||||||
|
|
||||||
val infiniteTransition = rememberInfiniteTransition()
|
val infiniteTransition = rememberInfiniteTransition()
|
||||||
|
@ -88,18 +83,6 @@ fun FeedsPage(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
val launcher = rememberLauncherForActivityResult(
|
|
||||||
ActivityResultContracts.CreateDocument()
|
|
||||||
) { result ->
|
|
||||||
feedsViewModel.exportAsOpml { string ->
|
|
||||||
result?.let { uri ->
|
|
||||||
context.contentResolver.openOutputStream(uri)?.use { outputStream ->
|
|
||||||
outputStream.write(string.toByteArray())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val feedBadgeAlpha by remember { derivedStateOf { (ln(groupListTonalElevation.value + 1.4f) + 2f) / 100f } }
|
val feedBadgeAlpha by remember { derivedStateOf { (ln(groupListTonalElevation.value + 1.4f) + 2f) / 100f } }
|
||||||
val groupAlpha by remember { derivedStateOf { groupListTonalElevation.value.dp.alphaLN(weight = 1.2f) } }
|
val groupAlpha by remember { derivedStateOf { groupListTonalElevation.value.dp.alphaLN(weight = 1.2f) } }
|
||||||
val groupIndicatorAlpha by remember {
|
val groupIndicatorAlpha by remember {
|
||||||
|
@ -172,11 +155,11 @@ fun FeedsPage(
|
||||||
modifier = Modifier.pointerInput(Unit) {
|
modifier = Modifier.pointerInput(Unit) {
|
||||||
detectTapGestures(
|
detectTapGestures(
|
||||||
onLongPress = {
|
onLongPress = {
|
||||||
launcher.launch("ReadYou.opml")
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
text = feedsUiState.account?.name ?: stringResource(R.string.read_you),
|
text = feedsUiState.account?.name ?: "",
|
||||||
desc = if (isSyncing) stringResource(R.string.syncing) else "",
|
desc = if (isSyncing) stringResource(R.string.syncing) else "",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package me.ash.reader.ui.page.home.feeds
|
package me.ash.reader.ui.page.home.feeds
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
@ -13,7 +12,6 @@ import me.ash.reader.data.model.account.Account
|
||||||
import me.ash.reader.data.module.DefaultDispatcher
|
import me.ash.reader.data.module.DefaultDispatcher
|
||||||
import me.ash.reader.data.module.IODispatcher
|
import me.ash.reader.data.module.IODispatcher
|
||||||
import me.ash.reader.data.repository.AccountRepository
|
import me.ash.reader.data.repository.AccountRepository
|
||||||
import me.ash.reader.data.repository.OpmlRepository
|
|
||||||
import me.ash.reader.data.repository.RssRepository
|
import me.ash.reader.data.repository.RssRepository
|
||||||
import me.ash.reader.data.repository.StringsRepository
|
import me.ash.reader.data.repository.StringsRepository
|
||||||
import me.ash.reader.ui.page.home.FilterState
|
import me.ash.reader.ui.page.home.FilterState
|
||||||
|
@ -23,7 +21,6 @@ import javax.inject.Inject
|
||||||
class FeedsViewModel @Inject constructor(
|
class FeedsViewModel @Inject constructor(
|
||||||
private val accountRepository: AccountRepository,
|
private val accountRepository: AccountRepository,
|
||||||
private val rssRepository: RssRepository,
|
private val rssRepository: RssRepository,
|
||||||
private val opmlRepository: OpmlRepository,
|
|
||||||
private val stringsRepository: StringsRepository,
|
private val stringsRepository: StringsRepository,
|
||||||
@DefaultDispatcher
|
@DefaultDispatcher
|
||||||
private val defaultDispatcher: CoroutineDispatcher,
|
private val defaultDispatcher: CoroutineDispatcher,
|
||||||
|
@ -40,16 +37,6 @@ class FeedsViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun exportAsOpml(callback: (String) -> Unit = {}) {
|
|
||||||
viewModelScope.launch(defaultDispatcher) {
|
|
||||||
try {
|
|
||||||
callback(opmlRepository.saveToString())
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("FeedsViewModel", "exportAsOpml: ", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun pullFeeds(filterState: FilterState) {
|
fun pullFeeds(filterState: FilterState) {
|
||||||
val isStarred = filterState.filter.isStarred()
|
val isStarred = filterState.filter.isStarred()
|
||||||
val isUnread = filterState.filter.isUnread()
|
val isUnread = filterState.filter.isUnread()
|
||||||
|
|
|
@ -69,7 +69,7 @@ fun FlowPage(
|
||||||
val owner = LocalLifecycleOwner.current
|
val owner = LocalLifecycleOwner.current
|
||||||
var isSyncing by remember { mutableStateOf(false) }
|
var isSyncing by remember { mutableStateOf(false) }
|
||||||
homeViewModel.syncWorkLiveData.observe(owner) {
|
homeViewModel.syncWorkLiveData.observe(owner) {
|
||||||
it?.let { isSyncing = it.progress.getIsSyncing() }
|
it?.let { isSyncing = it.any { it.progress.getIsSyncing() } }
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(onSearch) {
|
LaunchedEffect(onSearch) {
|
||||||
|
|
|
@ -4,24 +4,30 @@ import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import me.ash.reader.data.model.general.MarkAsReadConditions
|
import me.ash.reader.data.model.general.MarkAsReadConditions
|
||||||
|
import me.ash.reader.data.module.IODispatcher
|
||||||
import me.ash.reader.data.repository.RssRepository
|
import me.ash.reader.data.repository.RssRepository
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class FlowViewModel @Inject constructor(
|
class FlowViewModel @Inject constructor(
|
||||||
private val rssRepository: RssRepository,
|
private val rssRepository: RssRepository,
|
||||||
|
@IODispatcher
|
||||||
|
private val ioDispatcher: CoroutineDispatcher,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _flowUiState = MutableStateFlow(FlowUiState())
|
private val _flowUiState = MutableStateFlow(FlowUiState())
|
||||||
val flowUiState: StateFlow<FlowUiState> = _flowUiState.asStateFlow()
|
val flowUiState: StateFlow<FlowUiState> = _flowUiState.asStateFlow()
|
||||||
|
|
||||||
fun sync() {
|
fun sync() {
|
||||||
rssRepository.get().doSync()
|
viewModelScope.launch(ioDispatcher) {
|
||||||
|
rssRepository.get().doSync()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun scrollToItem(index: Int) {
|
fun scrollToItem(index: Int) {
|
||||||
|
|
|
@ -7,6 +7,7 @@ import androidx.compose.animation.core.spring
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
|
@ -16,6 +16,7 @@ import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
@ -29,6 +30,7 @@ fun SettingItem(
|
||||||
title: String,
|
title: String,
|
||||||
desc: String? = null,
|
desc: String? = null,
|
||||||
icon: ImageVector? = null,
|
icon: ImageVector? = null,
|
||||||
|
iconPainter: Painter? = null,
|
||||||
separatedActions: Boolean = false,
|
separatedActions: Boolean = false,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
action: (@Composable () -> Unit)? = null,
|
action: (@Composable () -> Unit)? = null,
|
||||||
|
@ -47,13 +49,24 @@ fun SettingItem(
|
||||||
.padding(24.dp, 16.dp, 16.dp, 16.dp),
|
.padding(24.dp, 16.dp, 16.dp, 16.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
icon?.let {
|
if (icon != null) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = it,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.padding(end = 24.dp),
|
modifier = Modifier.padding(end = 24.dp),
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = title,
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
iconPainter?.let {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(end = 24.dp)
|
||||||
|
.size(24.dp),
|
||||||
|
painter = it,
|
||||||
|
contentDescription = title,
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
Text(
|
Text(
|
||||||
|
|
|
@ -92,8 +92,11 @@ fun SettingsPage(
|
||||||
title = stringResource(R.string.accounts),
|
title = stringResource(R.string.accounts),
|
||||||
desc = stringResource(R.string.accounts_desc),
|
desc = stringResource(R.string.accounts_desc),
|
||||||
icon = Icons.Outlined.AccountCircle,
|
icon = Icons.Outlined.AccountCircle,
|
||||||
enable = false,
|
) {
|
||||||
) {}
|
navController.navigate(RouteName.ACCOUNTS) {
|
||||||
|
launchSingleTop = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
SelectableSettingGroupItem(
|
SelectableSettingGroupItem(
|
||||||
|
|
|
@ -0,0 +1,368 @@
|
||||||
|
package me.ash.reader.ui.page.settings.accounts
|
||||||
|
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
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.PersonOff
|
||||||
|
import androidx.compose.material.icons.rounded.ArrowBack
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
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.res.stringResource
|
||||||
|
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.preference.*
|
||||||
|
import me.ash.reader.ui.component.base.*
|
||||||
|
import me.ash.reader.ui.ext.collectAsStateValue
|
||||||
|
import me.ash.reader.ui.ext.showToast
|
||||||
|
import me.ash.reader.ui.ext.showToastLong
|
||||||
|
import me.ash.reader.ui.page.settings.SettingItem
|
||||||
|
import me.ash.reader.ui.theme.palette.onLight
|
||||||
|
|
||||||
|
@OptIn(ExperimentalAnimationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun AccountDetailsPage(
|
||||||
|
navController: NavHostController = rememberAnimatedNavController(),
|
||||||
|
viewModel: AccountViewModel = hiltViewModel(),
|
||||||
|
) {
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val uiState = viewModel.accountUiState.collectAsStateValue()
|
||||||
|
val context = LocalContext.current
|
||||||
|
val syncInterval = LocalSyncInterval.current
|
||||||
|
val syncOnStart = LocalSyncOnStart.current
|
||||||
|
val syncOnlyOnWiFi = LocalSyncOnlyOnWiFi.current
|
||||||
|
val syncOnlyWhenCharging = LocalSyncOnlyWhenCharging.current
|
||||||
|
val keepArchived = LocalKeepArchived.current
|
||||||
|
val syncBlockList = LocalSyncBlockList.current
|
||||||
|
|
||||||
|
val selectedAccount = uiState.selectedAccount.collectAsStateValue(initial = null)
|
||||||
|
|
||||||
|
var nameValue by remember { mutableStateOf(selectedAccount?.name) }
|
||||||
|
var nameDialogVisible by remember { mutableStateOf(false) }
|
||||||
|
var blockListValue by remember { mutableStateOf(SyncBlockListPreference.toString(syncBlockList)) }
|
||||||
|
var blockListDialogVisible by remember { mutableStateOf(false) }
|
||||||
|
var syncIntervalDialogVisible by remember { mutableStateOf(false) }
|
||||||
|
var keepArchivedDialogVisible by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
navController.currentBackStackEntryFlow.collect {
|
||||||
|
it.arguments?.getString("accountId")?.let {
|
||||||
|
viewModel.initData(it.toInt())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val launcher = rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.CreateDocument()
|
||||||
|
) { result ->
|
||||||
|
viewModel.exportAsOPML { string ->
|
||||||
|
result?.let { uri ->
|
||||||
|
context.contentResolver.openOutputStream(uri)?.use { outputStream ->
|
||||||
|
outputStream.write(string.toByteArray())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RYScaffold(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surface onLight MaterialTheme.colorScheme.inverseOnSurface,
|
||||||
|
navigationIcon = {
|
||||||
|
FeedbackIconButton(
|
||||||
|
imageVector = Icons.Rounded.ArrowBack,
|
||||||
|
contentDescription = stringResource(R.string.back),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurface
|
||||||
|
) {
|
||||||
|
navController.popBackStack()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
content = {
|
||||||
|
LazyColumn {
|
||||||
|
item {
|
||||||
|
DisplayText(text = selectedAccount?.type?.toDesc(context) ?: "", desc = "")
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
Subtitle(
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp),
|
||||||
|
text = stringResource(R.string.display),
|
||||||
|
)
|
||||||
|
SettingItem(
|
||||||
|
title = stringResource(R.string.name),
|
||||||
|
desc = selectedAccount?.name ?: "",
|
||||||
|
onClick = { nameDialogVisible = true },
|
||||||
|
) {}
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
Subtitle(
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp),
|
||||||
|
text = stringResource(R.string.synchronous),
|
||||||
|
)
|
||||||
|
SettingItem(
|
||||||
|
title = stringResource(R.string.sync_interval),
|
||||||
|
desc = syncInterval.toDesc(context),
|
||||||
|
onClick = { syncIntervalDialogVisible = true },
|
||||||
|
) {}
|
||||||
|
SettingItem(
|
||||||
|
title = stringResource(R.string.sync_once_on_start),
|
||||||
|
onClick = {
|
||||||
|
selectedAccount?.id?.let {
|
||||||
|
(!syncOnStart).put(it, viewModel)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
RYSwitch(activated = syncOnStart.value) {
|
||||||
|
selectedAccount?.id?.let {
|
||||||
|
(!syncOnStart).put(it, viewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SettingItem(
|
||||||
|
title = stringResource(R.string.only_on_wifi),
|
||||||
|
onClick = {
|
||||||
|
selectedAccount?.id?.let {
|
||||||
|
(!syncOnlyOnWiFi).put(it, viewModel)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
RYSwitch(activated = syncOnlyOnWiFi.value) {
|
||||||
|
selectedAccount?.id?.let {
|
||||||
|
(!syncOnlyOnWiFi).put(it, viewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SettingItem(
|
||||||
|
title = stringResource(R.string.only_when_charging),
|
||||||
|
onClick = {
|
||||||
|
selectedAccount?.id?.let {
|
||||||
|
(!syncOnlyWhenCharging).put(it, viewModel)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
RYSwitch(activated = syncOnlyWhenCharging.value) {
|
||||||
|
selectedAccount?.id?.let {
|
||||||
|
(!syncOnlyWhenCharging).put(it, viewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SettingItem(
|
||||||
|
title = stringResource(R.string.keep_archived_articles),
|
||||||
|
desc = keepArchived.toDesc(context),
|
||||||
|
onClick = { keepArchivedDialogVisible = true },
|
||||||
|
) {}
|
||||||
|
// SettingItem(
|
||||||
|
// title = stringResource(R.string.block_list),
|
||||||
|
// onClick = { blockListDialogVisible = true },
|
||||||
|
// ) {}
|
||||||
|
Tips(text = stringResource(R.string.synchronous_tips))
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
Subtitle(
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp),
|
||||||
|
text = stringResource(R.string.advanced),
|
||||||
|
)
|
||||||
|
SettingItem(
|
||||||
|
title = stringResource(R.string.export_as_opml),
|
||||||
|
onClick = {
|
||||||
|
launcher.launch("ReadYou.opml")
|
||||||
|
},
|
||||||
|
) {}
|
||||||
|
SettingItem(
|
||||||
|
title = stringResource(R.string.clear_all_articles),
|
||||||
|
onClick = { viewModel.showClearDialog() },
|
||||||
|
) {}
|
||||||
|
SettingItem(
|
||||||
|
title = stringResource(R.string.delete_account),
|
||||||
|
onClick = { viewModel.showDeleteDialog() },
|
||||||
|
) {}
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
Spacer(modifier = Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
TextFieldDialog(
|
||||||
|
visible = nameDialogVisible,
|
||||||
|
title = stringResource(R.string.name),
|
||||||
|
value = nameValue ?: "",
|
||||||
|
placeholder = stringResource(R.string.value),
|
||||||
|
onValueChange = {
|
||||||
|
nameValue = it
|
||||||
|
},
|
||||||
|
onDismissRequest = {
|
||||||
|
nameDialogVisible = false
|
||||||
|
},
|
||||||
|
onConfirm = {
|
||||||
|
if (nameValue?.isNotBlank() == true) {
|
||||||
|
selectedAccount?.id?.let {
|
||||||
|
viewModel.update(it) {
|
||||||
|
name = nameValue ?: ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nameDialogVisible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
RadioDialog(
|
||||||
|
visible = syncIntervalDialogVisible,
|
||||||
|
title = stringResource(R.string.sync_interval),
|
||||||
|
options = SyncIntervalPreference.values.map {
|
||||||
|
RadioDialogOption(
|
||||||
|
text = it.toDesc(context),
|
||||||
|
selected = it == syncInterval,
|
||||||
|
) {
|
||||||
|
selectedAccount?.id?.let { accountId ->
|
||||||
|
it.put(accountId, viewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
syncIntervalDialogVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
RadioDialog(
|
||||||
|
visible = keepArchivedDialogVisible,
|
||||||
|
title = stringResource(R.string.keep_archived_articles),
|
||||||
|
options = KeepArchivedPreference.values.map {
|
||||||
|
RadioDialogOption(
|
||||||
|
text = it.toDesc(context),
|
||||||
|
selected = it == keepArchived,
|
||||||
|
) {
|
||||||
|
selectedAccount?.id?.let { accountId ->
|
||||||
|
it.put(accountId, viewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
keepArchivedDialogVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
TextFieldDialog(
|
||||||
|
visible = blockListDialogVisible,
|
||||||
|
title = stringResource(R.string.block_list),
|
||||||
|
value = blockListValue,
|
||||||
|
singleLine = false,
|
||||||
|
placeholder = stringResource(R.string.value),
|
||||||
|
onValueChange = {
|
||||||
|
blockListValue = it
|
||||||
|
},
|
||||||
|
onDismissRequest = {
|
||||||
|
blockListDialogVisible = false
|
||||||
|
},
|
||||||
|
onConfirm = {
|
||||||
|
selectedAccount?.id?.let {
|
||||||
|
SyncBlockListPreference.put(it, viewModel, syncBlockList)
|
||||||
|
blockListDialogVisible = false
|
||||||
|
context.showToast(selectedAccount.syncBlockList.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
RYDialog(
|
||||||
|
visible = uiState.clearDialogVisible,
|
||||||
|
onDismissRequest = {
|
||||||
|
viewModel.hideClearDialog()
|
||||||
|
},
|
||||||
|
icon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.DeleteForever,
|
||||||
|
contentDescription = stringResource(R.string.clear_all_articles),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
Text(text = stringResource(R.string.clear_all_articles))
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Text(text = stringResource(R.string.clear_all_articles_tips))
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
selectedAccount?.id?.let {
|
||||||
|
viewModel.clear(it) {
|
||||||
|
viewModel.hideClearDialog()
|
||||||
|
context.showToastLong(context.getString(R.string.clear_all_articles_toast))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.clear),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
viewModel.hideClearDialog()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.cancel),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
RYDialog(
|
||||||
|
visible = uiState.deleteDialogVisible,
|
||||||
|
onDismissRequest = {
|
||||||
|
viewModel.hideDeleteDialog()
|
||||||
|
},
|
||||||
|
icon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.PersonOff,
|
||||||
|
contentDescription = stringResource(R.string.delete_account),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
Text(text = stringResource(R.string.delete_account))
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Text(text = stringResource(R.string.delete_account_tips))
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
selectedAccount?.id?.let {
|
||||||
|
viewModel.delete(it) {
|
||||||
|
viewModel.hideDeleteDialog()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.delete),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
viewModel.hideDeleteDialog()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.cancel),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,98 @@
|
||||||
|
package me.ash.reader.ui.page.settings.accounts
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.flow.*
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import me.ash.reader.data.model.account.Account
|
||||||
|
import me.ash.reader.data.module.DefaultDispatcher
|
||||||
|
import me.ash.reader.data.module.IODispatcher
|
||||||
|
import me.ash.reader.data.module.MainDispatcher
|
||||||
|
import me.ash.reader.data.repository.AccountRepository
|
||||||
|
import me.ash.reader.data.repository.OpmlRepository
|
||||||
|
import me.ash.reader.data.repository.RssRepository
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class AccountViewModel @Inject constructor(
|
||||||
|
private val accountRepository: AccountRepository,
|
||||||
|
private val rssRepository: RssRepository,
|
||||||
|
private val opmlRepository: OpmlRepository,
|
||||||
|
@IODispatcher
|
||||||
|
private val ioDispatcher: CoroutineDispatcher,
|
||||||
|
@DefaultDispatcher
|
||||||
|
private val defaultDispatcher: CoroutineDispatcher,
|
||||||
|
@MainDispatcher
|
||||||
|
private val mainDispatcher: CoroutineDispatcher,
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
private val _accountUiState = MutableStateFlow(AccountUiState())
|
||||||
|
val accountUiState: StateFlow<AccountUiState> = _accountUiState.asStateFlow()
|
||||||
|
val accounts = accountRepository.getAccounts()
|
||||||
|
|
||||||
|
fun initData(accountId: Int) {
|
||||||
|
viewModelScope.launch(ioDispatcher) {
|
||||||
|
_accountUiState.update { it.copy(selectedAccount = accountRepository.getAccountById(accountId)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun update(accountId: Int, block: Account.() -> Unit) {
|
||||||
|
viewModelScope.launch(ioDispatcher) {
|
||||||
|
accountRepository.update(accountId, block)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun exportAsOPML(callback: (String) -> Unit = {}) {
|
||||||
|
viewModelScope.launch(defaultDispatcher) {
|
||||||
|
try {
|
||||||
|
callback(opmlRepository.saveToString())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("FeedsViewModel", "exportAsOpml: ", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hideDeleteDialog() {
|
||||||
|
_accountUiState.update { it.copy(deleteDialogVisible = false) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showDeleteDialog() {
|
||||||
|
_accountUiState.update { it.copy(deleteDialogVisible = true) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showClearDialog() {
|
||||||
|
_accountUiState.update { it.copy(clearDialogVisible = true) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hideClearDialog() {
|
||||||
|
_accountUiState.update { it.copy(clearDialogVisible = false) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun delete(accountId: Int, callback: () -> Unit = {}) {
|
||||||
|
viewModelScope.launch(ioDispatcher) {
|
||||||
|
accountRepository.delete(accountId)
|
||||||
|
withContext(mainDispatcher) {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear(accountId: Int, callback: () -> Unit = {}) {
|
||||||
|
viewModelScope.launch(ioDispatcher) {
|
||||||
|
rssRepository.get(accountId).deleteAccountArticles(accountId)
|
||||||
|
withContext(mainDispatcher) {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class AccountUiState(
|
||||||
|
val selectedAccount: Flow<Account?> = emptyFlow(),
|
||||||
|
val deleteDialogVisible: Boolean = false,
|
||||||
|
val clearDialogVisible: Boolean = false,
|
||||||
|
)
|
|
@ -0,0 +1,108 @@
|
||||||
|
package me.ash.reader.ui.page.settings.accounts
|
||||||
|
|
||||||
|
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.PersonAdd
|
||||||
|
import androidx.compose.material.icons.rounded.ArrowBack
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
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.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.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.ext.collectAsStateValue
|
||||||
|
import me.ash.reader.ui.page.common.RouteName
|
||||||
|
import me.ash.reader.ui.page.settings.SettingItem
|
||||||
|
import me.ash.reader.ui.theme.palette.onLight
|
||||||
|
|
||||||
|
@OptIn(ExperimentalAnimationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun AccountsPage(
|
||||||
|
navController: NavHostController = rememberAnimatedNavController(),
|
||||||
|
viewModel: AccountViewModel = hiltViewModel(),
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val uiState = viewModel.accountUiState.collectAsStateValue()
|
||||||
|
val accounts = viewModel.accounts.collectAsStateValue(initial = emptyList())
|
||||||
|
|
||||||
|
RYScaffold(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surface onLight MaterialTheme.colorScheme.inverseOnSurface,
|
||||||
|
navigationIcon = {
|
||||||
|
FeedbackIconButton(
|
||||||
|
imageVector = Icons.Rounded.ArrowBack,
|
||||||
|
contentDescription = stringResource(R.string.back),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurface
|
||||||
|
) {
|
||||||
|
navController.popBackStack()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
content = {
|
||||||
|
LazyColumn {
|
||||||
|
item {
|
||||||
|
DisplayText(text = stringResource(R.string.accounts), desc = "")
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Subtitle(
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp),
|
||||||
|
text = stringResource(R.string.list),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
accounts.forEach {
|
||||||
|
item {
|
||||||
|
SettingItem(
|
||||||
|
title = it.name,
|
||||||
|
desc = it.type.toDesc(context),
|
||||||
|
icon = it.type.toIcon().takeIf { it is ImageVector }?.let { it as ImageVector },
|
||||||
|
iconPainter = it.type.toIcon().takeIf { it is Painter }?.let { it as Painter },
|
||||||
|
onClick = {
|
||||||
|
navController.navigate("${RouteName.ACCOUNT_DETAILS}/${it.id}") {
|
||||||
|
launchSingleTop = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {}
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
Subtitle(
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp),
|
||||||
|
text = stringResource(R.string.more),
|
||||||
|
)
|
||||||
|
SettingItem(
|
||||||
|
title = stringResource(R.string.add_accounts),
|
||||||
|
desc = stringResource(R.string.add_accounts_desc),
|
||||||
|
icon = Icons.Outlined.PersonAdd,
|
||||||
|
onClick = {
|
||||||
|
navController.navigate(RouteName.ADD_ACCOUNTS) {
|
||||||
|
launchSingleTop = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {}
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
Spacer(modifier = Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun AccountsPreview() {
|
||||||
|
AccountsPage()
|
||||||
|
}
|
|
@ -0,0 +1,137 @@
|
||||||
|
package me.ash.reader.ui.page.settings.accounts
|
||||||
|
|
||||||
|
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.rounded.ArrowBack
|
||||||
|
import androidx.compose.material.icons.rounded.RssFeed
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
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.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.page.settings.SettingItem
|
||||||
|
import me.ash.reader.ui.theme.palette.onLight
|
||||||
|
|
||||||
|
@OptIn(ExperimentalAnimationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun AddAccountsPage(
|
||||||
|
navController: NavHostController = rememberAnimatedNavController(),
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
RYScaffold(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surface onLight MaterialTheme.colorScheme.inverseOnSurface,
|
||||||
|
navigationIcon = {
|
||||||
|
FeedbackIconButton(
|
||||||
|
imageVector = Icons.Rounded.ArrowBack,
|
||||||
|
contentDescription = stringResource(R.string.back),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurface
|
||||||
|
) {
|
||||||
|
navController.popBackStack()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
content = {
|
||||||
|
LazyColumn {
|
||||||
|
item {
|
||||||
|
DisplayText(text = stringResource(R.string.add_accounts), desc = "")
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
Subtitle(
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp),
|
||||||
|
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
|
||||||
|
// }
|
||||||
|
},
|
||||||
|
) {}
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
Subtitle(
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp),
|
||||||
|
text = stringResource(R.string.services),
|
||||||
|
)
|
||||||
|
SettingItem(
|
||||||
|
enable = false,
|
||||||
|
title = stringResource(R.string.feedlly),
|
||||||
|
desc = stringResource(R.string.feedlly_desc),
|
||||||
|
iconPainter = painterResource(id = R.drawable.ic_feedly),
|
||||||
|
onClick = {},
|
||||||
|
) {}
|
||||||
|
SettingItem(
|
||||||
|
enable = false,
|
||||||
|
title = stringResource(R.string.inoreader),
|
||||||
|
desc = stringResource(R.string.inoreader_desc),
|
||||||
|
iconPainter = painterResource(id = R.drawable.ic_inoreader),
|
||||||
|
onClick = {},
|
||||||
|
) {}
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
Subtitle(
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp),
|
||||||
|
text = stringResource(R.string.self_hosted),
|
||||||
|
)
|
||||||
|
SettingItem(
|
||||||
|
enable = false,
|
||||||
|
title = stringResource(R.string.fresh_rss),
|
||||||
|
desc = stringResource(R.string.fresh_rss_desc),
|
||||||
|
iconPainter = painterResource(id = R.drawable.ic_freshrss),
|
||||||
|
onClick = {
|
||||||
|
|
||||||
|
},
|
||||||
|
) {}
|
||||||
|
SettingItem(
|
||||||
|
enable = false,
|
||||||
|
title = stringResource(R.string.google_reader),
|
||||||
|
desc = stringResource(R.string.google_reader_desc),
|
||||||
|
icon = Icons.Rounded.RssFeed,
|
||||||
|
onClick = {
|
||||||
|
|
||||||
|
},
|
||||||
|
) {}
|
||||||
|
SettingItem(
|
||||||
|
enable = false,
|
||||||
|
title = stringResource(R.string.fever),
|
||||||
|
desc = stringResource(R.string.fever_desc),
|
||||||
|
iconPainter = painterResource(id = R.drawable.ic_fever),
|
||||||
|
onClick = {
|
||||||
|
|
||||||
|
},
|
||||||
|
) {}
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
Spacer(modifier = Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun AddAccountsPreview() {
|
||||||
|
AddAccountsPage()
|
||||||
|
}
|
9
app/src/main/res/drawable/ic_feedly.xml
Normal file
9
app/src/main/res/drawable/ic_feedly.xml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="64dp"
|
||||||
|
android:height="64dp"
|
||||||
|
android:viewportWidth="50"
|
||||||
|
android:viewportHeight="50">
|
||||||
|
<path
|
||||||
|
android:pathData="M20.027,45L29.969,45C31.563,45 33.086,44.367 34.215,43.242L46.215,31.242C48.555,28.898 48.555,25.102 46.215,22.758L29.242,5.785C26.898,3.445 23.102,3.445 20.758,5.785L3.785,22.758C1.445,25.102 1.445,28.898 3.785,31.242L15.785,43.242C16.91,44.367 18.438,45 20.027,45ZM17.465,33.605L16.516,32.656C16.121,32.266 16.121,31.633 16.516,31.242L23.586,24.172C23.977,23.781 24.609,23.781 25,24.172L27.121,26.293C27.512,26.684 27.512,27.316 27.121,27.707L21.219,33.605C21.031,33.793 20.777,33.898 20.512,33.898L18.172,33.898C17.906,33.898 17.652,33.793 17.465,33.605ZM27.828,38.313L26.879,39.266C26.691,39.453 26.438,39.559 26.172,39.559L23.828,39.559C23.563,39.559 23.309,39.453 23.121,39.266L22.172,38.313C21.781,37.922 21.781,37.289 22.172,36.898L24.293,34.777C24.684,34.387 25.316,34.387 25.707,34.777L27.828,36.898C28.219,37.289 28.219,37.922 27.828,38.313ZM10.859,25.586L23.586,12.859C23.977,12.469 24.609,12.469 25,12.859L27.121,14.98C27.512,15.371 27.512,16.004 27.121,16.395L15.566,27.949C15.379,28.137 15.121,28.242 14.859,28.242L12.516,28.242C12.25,28.242 11.996,28.137 11.809,27.949L10.859,27C10.469,26.609 10.469,25.977 10.859,25.586Z"
|
||||||
|
android:fillColor="#000000"/>
|
||||||
|
</vector>
|
10
app/src/main/res/drawable/ic_fever.xml
Normal file
10
app/src/main/res/drawable/ic_fever.xml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="88dp"
|
||||||
|
android:height="88dp"
|
||||||
|
android:viewportWidth="88"
|
||||||
|
android:viewportHeight="88">
|
||||||
|
<path
|
||||||
|
android:pathData="M28.8,17.8c-17.5,17.5 -19.1,20.4 -19.2,33.2 -0.1,7.7 0.2,9.2 3.2,15.2 3.8,7.7 9.2,13 17.1,16.7 8.1,3.7 20.1,3.7 28.2,-0 7.9,-3.7 13.3,-9 17.1,-16.7 3,-6 3.3,-7.5 3.2,-15.2 -0.1,-12.8 -1.7,-15.7 -19.2,-33.2 -8,-8.2 -14.9,-14.8 -15.2,-14.8 -0.3,-0 -7.2,6.6 -15.2,14.8zM53.8,37.3c4.7,3.1 7.2,8 7.2,14.2 0,7.9 -5.1,14.4 -13,16.5 -7.5,2 -17,-2.8 -20,-10 -1.8,-4.3 -0.8,-13 1.8,-16.7 5.2,-7.4 16.2,-9.2 24,-4z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:strokeColor="#00000000"/>
|
||||||
|
</vector>
|
16
app/src/main/res/drawable/ic_freshrss.xml
Normal file
16
app/src/main/res/drawable/ic_freshrss.xml
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<vector android:height="64dp" android:viewportHeight="256"
|
||||||
|
android:viewportWidth="256" android:width="64dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="#0062BE" android:pathData="M128,128m-33,0a33,33 0,1 1,66 0a33,33 0,1 1,-66 0"/>
|
||||||
|
<path android:fillColor="#00000000"
|
||||||
|
android:pathData="M12,128A116,116 0,1 1,128 244"
|
||||||
|
android:strokeAlpha="0.3" android:strokeColor="#0062BE" android:strokeWidth="24"/>
|
||||||
|
<path android:fillColor="#00000000"
|
||||||
|
android:pathData="M54,128A74,74 0,1 1,128 202"
|
||||||
|
android:strokeAlpha="0.3" android:strokeColor="#0062BE" android:strokeWidth="24"/>
|
||||||
|
<path android:fillColor="#00000000"
|
||||||
|
android:pathData="M128,12A116,116 0,0 1,244 128"
|
||||||
|
android:strokeColor="#0062BE" android:strokeWidth="24"/>
|
||||||
|
<path android:fillColor="#00000000"
|
||||||
|
android:pathData="M128,54A74,74 0,0 1,202 128"
|
||||||
|
android:strokeColor="#0062BE" android:strokeWidth="24"/>
|
||||||
|
</vector>
|
12
app/src/main/res/drawable/ic_inoreader.xml
Normal file
12
app/src/main/res/drawable/ic_inoreader.xml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="72dp"
|
||||||
|
android:height="72dp"
|
||||||
|
android:viewportWidth="72"
|
||||||
|
android:viewportHeight="72">
|
||||||
|
<path
|
||||||
|
android:pathData="M36,0C55.882,0 72,16.118 72,36C72,55.882 55.882,72 36,72C16.118,72 0,55.882 0,36C0,16.118 16.118,0 36,0ZM28.01,30.98C20.825,30.98 15,36.804 15,43.989C15,51.175 20.825,57 28.01,57C35.193,57 41.019,51.175 41.019,43.989C41.019,36.804 35.193,30.98 28.01,30.98ZM31.679,36.545C33.736,36.545 35.404,38.215 35.404,40.27C35.404,42.331 33.736,43.999 31.679,43.999C29.619,43.999 27.95,42.331 27.95,40.27C27.95,38.215 29.619,36.545 31.679,36.545ZM28.01,21.787L28.01,26.474C37.666,26.474 45.526,34.332 45.526,43.989L45.526,43.989L50.214,43.989C50.214,31.747 40.254,21.787 28.01,21.787L28.01,21.787ZM28.01,12L28.01,16.687C35.302,16.687 42.158,19.528 47.316,24.685C52.474,29.842 55.311,36.699 55.311,43.989L55.311,43.989L60,43.989C60,35.445 56.672,27.412 50.629,21.368C44.586,15.327 36.555,12 28.01,12L28.01,12Z"
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillType="nonZero"
|
||||||
|
android:strokeColor="#00000000"/>
|
||||||
|
</vector>
|
|
@ -301,4 +301,46 @@
|
||||||
<string name="images_desc">圆角、水平边距</string>
|
<string name="images_desc">圆角、水平边距</string>
|
||||||
<string name="videos_desc">圆角、水平边距</string>
|
<string name="videos_desc">圆角、水平边距</string>
|
||||||
<string name="maximize">最大化</string>
|
<string name="maximize">最大化</string>
|
||||||
|
<string name="every_15_minutes">每 15 分钟</string>
|
||||||
|
<string name="every_30_minutes">每 30 分钟</string>
|
||||||
|
<string name="every_1_hour">每 1 小时</string>
|
||||||
|
<string name="every_2_hours">每 2 小时</string>
|
||||||
|
<string name="every_3_hours">每 3 小时</string>
|
||||||
|
<string name="every_6_hours">每 6 小时</string>
|
||||||
|
<string name="every_12_hours">每 12 小时</string>
|
||||||
|
<string name="every_1_day">每 1 天</string>
|
||||||
|
<string name="manually">手动</string>
|
||||||
|
<string name="always">总是</string>
|
||||||
|
<string name="for_1_day">1 天</string>
|
||||||
|
<string name="for_2_days">2 天</string>
|
||||||
|
<string name="for_3_days">3 天</string>
|
||||||
|
<string name="for_1_week">1 周</string>
|
||||||
|
<string name="for_2_weeks">2 周</string>
|
||||||
|
<string name="for_1_month">1 个月</string>
|
||||||
|
<string name="local">本地</string>
|
||||||
|
<string name="local_desc">在这台设备上</string>
|
||||||
|
<string name="services">服务</string>
|
||||||
|
<string name="self_hosted">自托管</string>
|
||||||
|
<string name="fever_desc">已过时,不推荐。</string>
|
||||||
|
<string name="more">更多</string>
|
||||||
|
<string name="add_accounts">添加账户</string>
|
||||||
|
<string name="list">列表</string>
|
||||||
|
<string name="add_accounts_desc">本地、第三方服务、自托管</string>
|
||||||
|
<string name="display">显示</string>
|
||||||
|
<string name="synchronous">同步时</string>
|
||||||
|
<string name="sync_interval">同步频率</string>
|
||||||
|
<string name="sync_once_on_start">启动时同步一次</string>
|
||||||
|
<string name="only_on_wifi">仅限连接 Wi-Fi 时</string>
|
||||||
|
<string name="only_when_charging">仅限连接充电器时</string>
|
||||||
|
<string name="keep_archived_articles">保留已归档的文章</string>
|
||||||
|
<string name="block_list">屏蔽列表</string>
|
||||||
|
<string name="export_as_opml">导出为 OPML</string>
|
||||||
|
<string name="clear_all_articles">清空所有文章</string>
|
||||||
|
<string name="delete_account">删除账户</string>
|
||||||
|
<string name="clear_all_articles_tips">确定要清空该账户中所有的文章吗?</string>
|
||||||
|
<string name="delete_account_tips">确定要删除该账户吗?</string>
|
||||||
|
<string name="must_have_an_account">必须保留一个账户</string>
|
||||||
|
<string name="clear_all_articles_toast">已清空该账户的所有文章</string>
|
||||||
|
<string name="delete_account_toast">该账户已被删除</string>
|
||||||
|
<string name="synchronous_tips">需要重新启动才能生效。</string>
|
||||||
</resources>
|
</resources>
|
|
@ -330,4 +330,55 @@
|
||||||
<string name="images_desc">Rounded corners, horizontal padding</string>
|
<string name="images_desc">Rounded corners, horizontal padding</string>
|
||||||
<string name="videos_desc">Rounded corners, horizontal padding</string>
|
<string name="videos_desc">Rounded corners, horizontal padding</string>
|
||||||
<string name="maximize">Maximize</string>
|
<string name="maximize">Maximize</string>
|
||||||
|
<string name="every_15_minutes">Every 15 minutes</string>
|
||||||
|
<string name="every_30_minutes">Every 30 minutes</string>
|
||||||
|
<string name="every_1_hour">Every hour</string>
|
||||||
|
<string name="every_2_hours">Every 2 hours</string>
|
||||||
|
<string name="every_3_hours">Every 3 hours</string>
|
||||||
|
<string name="every_6_hours">Every 6 hours</string>
|
||||||
|
<string name="every_12_hours">Every 12 hours</string>
|
||||||
|
<string name="every_1_day">Every day</string>
|
||||||
|
<string name="manually">Manually</string>
|
||||||
|
<string name="always">Always</string>
|
||||||
|
<string name="for_1_day">1 day</string>
|
||||||
|
<string name="for_2_days">2 days</string>
|
||||||
|
<string name="for_3_days">3 days</string>
|
||||||
|
<string name="for_1_week">1 week</string>
|
||||||
|
<string name="for_2_weeks">2 weeks</string>
|
||||||
|
<string name="for_1_month">1 month</string>
|
||||||
|
<string name="local">Local</string>
|
||||||
|
<string name="fever" translatable="false">Fever</string>
|
||||||
|
<string name="google_reader" translatable="false">Google Reader</string>
|
||||||
|
<string name="fresh_rss" translatable="false">FreshRSS</string>
|
||||||
|
<string name="feedlly" translatable="false">Feedlly</string>
|
||||||
|
<string name="inoreader" translatable="false">Inoreader</string>
|
||||||
|
<string name="local_desc">On this device</string>
|
||||||
|
<string name="services">Services</string>
|
||||||
|
<string name="feedlly_desc" translatable="false">feedlly.com</string>
|
||||||
|
<string name="inoreader_desc" translatable="false">inoreader.com</string>
|
||||||
|
<string name="self_hosted">Self-Hosted</string>
|
||||||
|
<string name="fresh_rss_desc" translatable="false">freshrss.org</string>
|
||||||
|
<string name="google_reader_desc" translatable="false">Google Reader API</string>
|
||||||
|
<string name="fever_desc">Deprecated. Not recommended.</string>
|
||||||
|
<string name="more">More</string>
|
||||||
|
<string name="add_accounts">Add accounts</string>
|
||||||
|
<string name="list">List</string>
|
||||||
|
<string name="add_accounts_desc">Local, Services, Self-Hosted</string>
|
||||||
|
<string name="display">Display</string>
|
||||||
|
<string name="synchronous">Syncing</string>
|
||||||
|
<string name="sync_interval">Sync</string>
|
||||||
|
<string name="sync_once_on_start">Sync once on start</string>
|
||||||
|
<string name="only_on_wifi">Only on Wi-Fi</string>
|
||||||
|
<string name="only_when_charging">Only when charging</string>
|
||||||
|
<string name="keep_archived_articles">Keep archived articles</string>
|
||||||
|
<string name="block_list">Block list</string>
|
||||||
|
<string name="export_as_opml">Export as OPML</string>
|
||||||
|
<string name="clear_all_articles">Clear all articles</string>
|
||||||
|
<string name="delete_account">Delete account</string>
|
||||||
|
<string name="clear_all_articles_tips">Are you sure you want to clear all articles from this account?</string>
|
||||||
|
<string name="delete_account_tips">Are you sure you want to delete this account?</string>
|
||||||
|
<string name="must_have_an_account">Must have an account</string>
|
||||||
|
<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 require for changes to take effect.</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user