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.
|
||||
*/
|
||||
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}")
|
||||
context.showToastLong(p1.message)
|
||||
p1.printStackTrace()
|
||||
android.os.Process.killProcess(android.os.Process.myPid());
|
||||
exitProcess(1)
|
||||
}
|
||||
|
|
|
@ -10,6 +10,8 @@ import androidx.profileinstaller.ProfileInstallerInitializer
|
|||
import coil.ImageLoader
|
||||
import coil.compose.LocalImageLoader
|
||||
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.SettingsProvider
|
||||
import me.ash.reader.ui.ext.languages
|
||||
|
@ -25,6 +27,9 @@ class MainActivity : ComponentActivity() {
|
|||
@Inject
|
||||
lateinit var imageLoader: ImageLoader
|
||||
|
||||
@Inject
|
||||
lateinit var accountDao: AccountDao
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
|
@ -40,8 +45,10 @@ class MainActivity : ComponentActivity() {
|
|||
CompositionLocalProvider(
|
||||
LocalImageLoader provides imageLoader,
|
||||
) {
|
||||
SettingsProvider {
|
||||
HomeEntry()
|
||||
AccountSettingsProvider(accountDao) {
|
||||
SettingsProvider {
|
||||
HomeEntry()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -103,7 +103,7 @@ class RYApp : Application(), Configuration.Provider {
|
|||
applicationScope.launch {
|
||||
accountInit()
|
||||
workerInit()
|
||||
if (notFdroid) checkUpdate()
|
||||
checkUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -126,11 +126,12 @@ class RYApp : Application(), Configuration.Provider {
|
|||
}
|
||||
}
|
||||
|
||||
private fun workerInit() {
|
||||
rssRepository.get().doSync()
|
||||
private suspend fun workerInit() {
|
||||
rssRepository.get().doSync(isOnStart = true)
|
||||
}
|
||||
|
||||
private suspend fun checkUpdate() {
|
||||
if (isFdroid) return
|
||||
withContext(ioDispatcher) {
|
||||
applicationContext.getLatestApk().let {
|
||||
if (it.exists()) it.del()
|
||||
|
|
|
@ -1,11 +1,20 @@
|
|||
package me.ash.reader.data.dao
|
||||
|
||||
import androidx.room.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import me.ash.reader.data.model.account.Account
|
||||
|
||||
@Dao
|
||||
interface AccountDao {
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT * FROM account
|
||||
WHERE id = :id
|
||||
"""
|
||||
)
|
||||
fun queryAccount(id: Int): Flow<Account?>
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT * FROM account
|
||||
|
@ -13,6 +22,13 @@ interface AccountDao {
|
|||
)
|
||||
suspend fun queryAll(): List<Account>
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT * FROM account
|
||||
"""
|
||||
)
|
||||
fun queryAllAsFlow(): Flow<List<Account>>
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT * FROM account
|
||||
|
|
|
@ -203,6 +203,20 @@ interface ArticleDao {
|
|||
text: String,
|
||||
): 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(
|
||||
"""
|
||||
UPDATE article SET isUnread = :isUnread
|
||||
|
@ -285,6 +299,14 @@ interface ArticleDao {
|
|||
)
|
||||
suspend fun deleteByGroupId(accountId: Int, groupId: String)
|
||||
|
||||
@Query(
|
||||
"""
|
||||
DELETE FROM article
|
||||
WHERE accountId = :accountId
|
||||
"""
|
||||
)
|
||||
suspend fun deleteByAccountId(accountId: Int)
|
||||
|
||||
@Transaction
|
||||
@Query(
|
||||
"""
|
||||
|
@ -376,7 +398,7 @@ interface ArticleDao {
|
|||
"""
|
||||
SELECT a.id, a.date, a.title, a.author, a.rawDescription,
|
||||
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 ON b.id = a.feedId
|
||||
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,
|
||||
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 ON b.id = a.feedId
|
||||
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,
|
||||
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 ON b.id = a.feedId
|
||||
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,
|
||||
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
|
||||
ON a.feedId = b.id
|
||||
WHERE a.feedId = :feedId
|
||||
|
|
|
@ -54,6 +54,14 @@ interface FeedDao {
|
|||
)
|
||||
suspend fun deleteByGroupId(accountId: Int, groupId: String)
|
||||
|
||||
@Query(
|
||||
"""
|
||||
DELETE FROM feed
|
||||
WHERE accountId = :accountId
|
||||
"""
|
||||
)
|
||||
suspend fun deleteByAccountId(accountId: Int)
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT * FROM feed
|
||||
|
|
|
@ -42,6 +42,14 @@ interface GroupDao {
|
|||
)
|
||||
fun queryAllGroup(accountId: Int): Flow<MutableList<Group>>
|
||||
|
||||
@Query(
|
||||
"""
|
||||
DELETE FROM `group`
|
||||
WHERE accountId = :accountId
|
||||
"""
|
||||
)
|
||||
suspend fun deleteByAccountId(accountId: Int)
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT * FROM `group`
|
||||
|
|
|
@ -3,6 +3,7 @@ package me.ash.reader.data.model.account
|
|||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import me.ash.reader.data.model.preference.*
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
|
@ -19,4 +20,16 @@ data class Account(
|
|||
var type: AccountType,
|
||||
@ColumnInfo
|
||||
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
|
||||
|
||||
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.TypeConverter
|
||||
import me.ash.reader.R
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
init {
|
||||
if (id < 1 || id > 3) {
|
||||
if (id < 1 || id > 6) {
|
||||
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.
|
||||
*/
|
||||
|
@ -25,6 +56,9 @@ class AccountType(val id: Int) {
|
|||
val Local = AccountType(1)
|
||||
val Fever = AccountType(2)
|
||||
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,
|
||||
@ColumnInfo(index = true)
|
||||
var accountId: Int,
|
||||
@ColumnInfo(defaultValue = "true")
|
||||
@ColumnInfo
|
||||
var isUnread: Boolean = true,
|
||||
@ColumnInfo(defaultValue = "false")
|
||||
@ColumnInfo
|
||||
var isStarred: Boolean = false,
|
||||
@ColumnInfo(defaultValue = "false")
|
||||
@ColumnInfo
|
||||
var isReadLater: Boolean = false,
|
||||
@ColumnInfo
|
||||
var updateAt: Date? = null,
|
||||
) {
|
||||
|
||||
@Ignore
|
||||
|
|
|
@ -29,9 +29,9 @@ data class Feed(
|
|||
var groupId: String,
|
||||
@ColumnInfo(index = true)
|
||||
var accountId: Int,
|
||||
@ColumnInfo(defaultValue = "false")
|
||||
@ColumnInfo
|
||||
var isNotification: Boolean = false,
|
||||
@ColumnInfo(defaultValue = "false")
|
||||
@ColumnInfo
|
||||
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 androidx.paging.PagingSource
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.ExistingPeriodicWorkPolicy
|
||||
import androidx.work.ListenableWorker
|
||||
import androidx.work.WorkManager
|
||||
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.group.Group
|
||||
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 java.util.*
|
||||
|
||||
|
@ -50,12 +51,41 @@ abstract class AbstractRssRepository constructor(
|
|||
isUnread: Boolean,
|
||||
)
|
||||
|
||||
fun doSync() {
|
||||
workManager.enqueueUniquePeriodicWork(
|
||||
SyncWorker.WORK_NAME,
|
||||
ExistingPeriodicWorkPolicy.REPLACE,
|
||||
SyncWorker.repeatingRequest
|
||||
)
|
||||
suspend fun keepArchivedArticles() {
|
||||
accountDao.queryById(context.currentAccountId)!!
|
||||
.takeIf { it.keepArchived != KeepArchivedPreference.Always }
|
||||
?.let {
|
||||
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>> =
|
||||
|
@ -155,6 +185,10 @@ abstract class AbstractRssRepository constructor(
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun deleteAccountArticles(accountId: Int) {
|
||||
articleDao.deleteByAccountId(accountId)
|
||||
}
|
||||
|
||||
suspend fun groupParseFullContent(group: Group, isFullContent: Boolean) {
|
||||
feedDao.updateIsFullContentByGroupId(context.currentAccountId, group.id, isFullContent)
|
||||
}
|
||||
|
|
|
@ -2,14 +2,19 @@ package me.ash.reader.data.repository
|
|||
|
||||
import android.content.Context
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import me.ash.reader.R
|
||||
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.model.account.Account
|
||||
import me.ash.reader.data.model.account.AccountType
|
||||
import me.ash.reader.data.model.group.Group
|
||||
import me.ash.reader.ui.ext.currentAccountId
|
||||
import me.ash.reader.ui.ext.getDefaultGroupId
|
||||
import me.ash.reader.ui.ext.showToast
|
||||
import me.ash.reader.ui.ext.showToastLong
|
||||
import javax.inject.Inject
|
||||
|
||||
class AccountRepository @Inject constructor(
|
||||
|
@ -17,9 +22,14 @@ class AccountRepository @Inject constructor(
|
|||
private val context: Context,
|
||||
private val accountDao: AccountDao,
|
||||
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()
|
||||
|
||||
|
@ -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 =
|
||||
supervisorScope {
|
||||
coroutineWorker.setProgress(setIsSyncing(true))
|
||||
val preTime = System.currentTimeMillis()
|
||||
val accountId = context.currentAccountId
|
||||
feedDao.queryAll(accountId)
|
||||
.also { coroutineWorker.setProgress(setIsSyncing(true)) }
|
||||
.chunked(16)
|
||||
.forEach {
|
||||
it.map { feed -> async { syncFeed(feed) } }
|
||||
|
|
|
@ -126,6 +126,7 @@ class RssHelper @Inject constructor(
|
|||
fullContent = content,
|
||||
img = findImg((content ?: desc) ?: ""),
|
||||
link = syndEntry.link ?: "",
|
||||
updateAt = Date(),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -14,7 +14,9 @@ class RssRepository @Inject constructor(
|
|||
// private val googleReaderRssRepository: GoogleReaderRssRepository,
|
||||
) {
|
||||
|
||||
fun get() = when (context.currentAccountType) {
|
||||
fun get() = get(context.currentAccountType)
|
||||
|
||||
fun get(accountId: Int) = when (accountId) {
|
||||
AccountType.Local.id -> localRssRepository
|
||||
// Account.Type.LOCAL -> feverRssRepository
|
||||
// Account.Type.FEVER -> feverRssRepository
|
||||
|
|
|
@ -8,6 +8,9 @@ import dagger.assisted.Assisted
|
|||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
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.concurrent.TimeUnit
|
||||
|
||||
|
@ -15,31 +18,55 @@ import java.util.concurrent.TimeUnit
|
|||
class SyncWorker @AssistedInject constructor(
|
||||
@Assisted context: Context,
|
||||
@Assisted workerParams: WorkerParameters,
|
||||
private val accountRepository: AccountRepository,
|
||||
private val rssRepository: RssRepository,
|
||||
) : CoroutineWorker(context, workerParams) {
|
||||
|
||||
override suspend fun doWork(): Result =
|
||||
withContext(Dispatchers.Default) {
|
||||
Log.i("RLog", "doWork: ")
|
||||
rssRepository.get().sync(this@SyncWorker)
|
||||
rssRepository.get().sync(this@SyncWorker).also {
|
||||
rssRepository.get().keepArchivedArticles()
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
val repeatingRequest = PeriodicWorkRequestBuilder<SyncWorker>(
|
||||
15, TimeUnit.MINUTES
|
||||
).setConstraints(
|
||||
Constraints.Builder()
|
||||
fun enqueueOneTimeWork(
|
||||
workManager: WorkManager,
|
||||
) {
|
||||
workManager.enqueue(OneTimeWorkRequestBuilder<SyncWorker>()
|
||||
.addTag(WORK_NAME)
|
||||
.build()
|
||||
).addTag(WORK_NAME).build().also {
|
||||
uuid = it.id
|
||||
)
|
||||
}
|
||||
|
||||
fun setIsSyncing(boolean: Boolean) = workDataOf("isSyncing" to boolean)
|
||||
fun Data.getIsSyncing(): Boolean = getBoolean("isSyncing", false)
|
||||
fun enqueuePeriodicWork(
|
||||
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.FeedDao
|
||||
import me.ash.reader.data.dao.GroupDao
|
||||
import me.ash.reader.data.model.account.Account
|
||||
import me.ash.reader.data.model.account.AccountTypeConverters
|
||||
import me.ash.reader.data.model.account.*
|
||||
import me.ash.reader.data.model.article.Article
|
||||
import me.ash.reader.data.model.feed.Feed
|
||||
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.*
|
||||
|
||||
@Database(
|
||||
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 fun accountDao(): AccountDao
|
||||
|
@ -60,6 +70,7 @@ abstract class RYDatabase : RoomDatabase() {
|
|||
|
||||
val allMigrations = arrayOf(
|
||||
MIGRATION_1_2,
|
||||
MIGRATION_2_3,
|
||||
)
|
||||
|
||||
@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,
|
||||
readOnly: Boolean = false,
|
||||
value: String = "",
|
||||
singleLine: Boolean = true,
|
||||
onValueChange: (String) -> Unit = {},
|
||||
placeholder: String = "",
|
||||
errorText: String = "",
|
||||
|
@ -35,6 +36,7 @@ fun ClipboardTextField(
|
|||
RYTextField(
|
||||
readOnly = readOnly,
|
||||
value = value,
|
||||
singleLine = singleLine,
|
||||
onValueChange = onValueChange,
|
||||
placeholder = placeholder,
|
||||
errorMessage = errorText,
|
||||
|
|
|
@ -22,6 +22,7 @@ import me.ash.reader.R
|
|||
fun RYTextField(
|
||||
readOnly: Boolean,
|
||||
value: String,
|
||||
singleLine: Boolean = true,
|
||||
onValueChange: (String) -> Unit,
|
||||
placeholder: String,
|
||||
errorMessage: String,
|
||||
|
@ -41,7 +42,7 @@ fun RYTextField(
|
|||
colors = TextFieldDefaults.textFieldColors(
|
||||
containerColor = Color.Transparent,
|
||||
),
|
||||
maxLines = 1,
|
||||
maxLines = if (singleLine) 1 else Int.MAX_VALUE,
|
||||
enabled = !readOnly,
|
||||
value = value,
|
||||
onValueChange = {
|
||||
|
@ -55,7 +56,7 @@ fun RYTextField(
|
|||
)
|
||||
},
|
||||
isError = errorMessage.isNotEmpty(),
|
||||
singleLine = true,
|
||||
singleLine = singleLine,
|
||||
trailingIcon = {
|
||||
if (value.isNotEmpty()) {
|
||||
IconButton(onClick = {
|
||||
|
@ -82,4 +83,4 @@ fun RYTextField(
|
|||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = keyboardActions,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ fun TextFieldDialog(
|
|||
properties: DialogProperties = DialogProperties(),
|
||||
visible: Boolean = false,
|
||||
readOnly: Boolean = false,
|
||||
singleLine: Boolean = true,
|
||||
title: String = "",
|
||||
icon: ImageVector? = null,
|
||||
value: String = "",
|
||||
|
@ -31,7 +32,7 @@ fun TextFieldDialog(
|
|||
onValueChange: (String) -> Unit = {},
|
||||
onDismissRequest: () -> Unit = {},
|
||||
onConfirm: (String) -> Unit = {},
|
||||
imeAction: ImeAction = ImeAction.Done,
|
||||
imeAction: ImeAction = if (singleLine) ImeAction.Done else ImeAction.Default,
|
||||
) {
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
|
@ -55,6 +56,7 @@ fun TextFieldDialog(
|
|||
modifier = modifier,
|
||||
readOnly = readOnly,
|
||||
value = value,
|
||||
singleLine = singleLine,
|
||||
onValueChange = onValueChange,
|
||||
placeholder = placeholder,
|
||||
errorText = errorText,
|
||||
|
@ -87,4 +89,4 @@ fun TextFieldDialog(
|
|||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
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.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.FileProvider
|
||||
|
@ -48,9 +49,11 @@ fun Context.installLatestApk() {
|
|||
private var toast: Toast? = null
|
||||
|
||||
fun Context.showToast(message: String?, duration: Int = Toast.LENGTH_SHORT) {
|
||||
Looper.myLooper() ?: Looper.prepare()
|
||||
toast?.cancel()
|
||||
toast = Toast.makeText(this, message, duration)
|
||||
toast?.show()
|
||||
Looper.loop()
|
||||
}
|
||||
|
||||
fun Context.showToastLong(message: String?) {
|
||||
|
@ -70,4 +73,4 @@ fun Context.share(content: String) {
|
|||
fun Context.openURL(url: String?) {
|
||||
url?.takeIf { it.trim().isNotEmpty() }
|
||||
?.let { startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(it))) }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,4 +2,6 @@ package me.ash.reader.ui.ext
|
|||
|
||||
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.reading.ReadingPage
|
||||
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.DarkThemePage
|
||||
import me.ash.reader.ui.page.settings.color.feeds.FeedsPageStylePage
|
||||
|
@ -140,6 +143,19 @@ fun HomeEntry(
|
|||
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
|
||||
animatedComposable(route = RouteName.COLOR_AND_STYLE) {
|
||||
ColorAndStylePage(navController)
|
||||
|
|
|
@ -13,6 +13,11 @@ object RouteName {
|
|||
// Settings
|
||||
const val SETTINGS = "settings"
|
||||
|
||||
// Accounts
|
||||
const val ACCOUNTS = "accounts"
|
||||
const val ACCOUNT_DETAILS = "account_details"
|
||||
const val ADD_ACCOUNTS = "add_accounts"
|
||||
|
||||
// Color & Style
|
||||
const val COLOR_AND_STYLE = "color_and_style"
|
||||
const val DARK_THEME = "dark_theme"
|
||||
|
|
|
@ -1,20 +1,24 @@
|
|||
package me.ash.reader.ui.page.home
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.cachedIn
|
||||
import androidx.work.WorkManager
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import me.ash.reader.data.model.article.ArticleFlowItem
|
||||
import me.ash.reader.data.model.article.mapPagingFlowItem
|
||||
import me.ash.reader.data.model.feed.Feed
|
||||
import me.ash.reader.data.model.general.Filter
|
||||
import me.ash.reader.data.model.group.Group
|
||||
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.StringsRepository
|
||||
import me.ash.reader.data.repository.SyncWorker
|
||||
|
@ -27,6 +31,8 @@ class HomeViewModel @Inject constructor(
|
|||
@ApplicationScope
|
||||
private val applicationScope: CoroutineScope,
|
||||
private val workManager: WorkManager,
|
||||
@IODispatcher
|
||||
private val ioDispatcher: CoroutineDispatcher,
|
||||
) : ViewModel() {
|
||||
|
||||
private val _homeUiState = MutableStateFlow(HomeUiState())
|
||||
|
@ -35,10 +41,12 @@ class HomeViewModel @Inject constructor(
|
|||
private val _filterUiState = MutableStateFlow(FilterState())
|
||||
val filterUiState = _filterUiState.asStateFlow()
|
||||
|
||||
val syncWorkLiveData = workManager.getWorkInfoByIdLiveData(SyncWorker.uuid)
|
||||
val syncWorkLiveData = workManager.getWorkInfosByTagLiveData(SyncWorker.WORK_NAME)
|
||||
|
||||
fun sync() {
|
||||
rssRepository.get().doSync()
|
||||
viewModelScope.launch(ioDispatcher) {
|
||||
rssRepository.get().doSync()
|
||||
}
|
||||
}
|
||||
|
||||
fun changeFilter(filterState: FilterState) {
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
package me.ash.reader.ui.page.home.feeds
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
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.ui.component.FilterBar
|
||||
import me.ash.reader.ui.component.base.*
|
||||
import me.ash.reader.ui.ext.alphaLN
|
||||
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.ext.*
|
||||
import me.ash.reader.ui.page.common.RouteName
|
||||
import me.ash.reader.ui.page.home.FilterState
|
||||
import me.ash.reader.ui.page.home.HomeViewModel
|
||||
|
@ -76,7 +71,7 @@ fun FeedsPage(
|
|||
val owner = LocalLifecycleOwner.current
|
||||
var isSyncing by remember { mutableStateOf(false) }
|
||||
homeViewModel.syncWorkLiveData.observe(owner) {
|
||||
it?.let { isSyncing = it.progress.getIsSyncing() }
|
||||
it?.let { isSyncing = it.any { it.progress.getIsSyncing() } }
|
||||
}
|
||||
|
||||
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 groupAlpha by remember { derivedStateOf { groupListTonalElevation.value.dp.alphaLN(weight = 1.2f) } }
|
||||
val groupIndicatorAlpha by remember {
|
||||
|
@ -172,11 +155,11 @@ fun FeedsPage(
|
|||
modifier = Modifier.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
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 "",
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package me.ash.reader.ui.page.home.feeds
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.lifecycle.ViewModel
|
||||
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.IODispatcher
|
||||
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.StringsRepository
|
||||
import me.ash.reader.ui.page.home.FilterState
|
||||
|
@ -23,7 +21,6 @@ import javax.inject.Inject
|
|||
class FeedsViewModel @Inject constructor(
|
||||
private val accountRepository: AccountRepository,
|
||||
private val rssRepository: RssRepository,
|
||||
private val opmlRepository: OpmlRepository,
|
||||
private val stringsRepository: StringsRepository,
|
||||
@DefaultDispatcher
|
||||
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) {
|
||||
val isStarred = filterState.filter.isStarred()
|
||||
val isUnread = filterState.filter.isUnread()
|
||||
|
|
|
@ -69,7 +69,7 @@ fun FlowPage(
|
|||
val owner = LocalLifecycleOwner.current
|
||||
var isSyncing by remember { mutableStateOf(false) }
|
||||
homeViewModel.syncWorkLiveData.observe(owner) {
|
||||
it?.let { isSyncing = it.progress.getIsSyncing() }
|
||||
it?.let { isSyncing = it.any { it.progress.getIsSyncing() } }
|
||||
}
|
||||
|
||||
LaunchedEffect(onSearch) {
|
||||
|
|
|
@ -4,24 +4,30 @@ import androidx.compose.foundation.lazy.LazyListState
|
|||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import me.ash.reader.data.model.general.MarkAsReadConditions
|
||||
import me.ash.reader.data.module.IODispatcher
|
||||
import me.ash.reader.data.repository.RssRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class FlowViewModel @Inject constructor(
|
||||
private val rssRepository: RssRepository,
|
||||
@IODispatcher
|
||||
private val ioDispatcher: CoroutineDispatcher,
|
||||
) : ViewModel() {
|
||||
|
||||
private val _flowUiState = MutableStateFlow(FlowUiState())
|
||||
val flowUiState: StateFlow<FlowUiState> = _flowUiState.asStateFlow()
|
||||
|
||||
fun sync() {
|
||||
rssRepository.get().doSync()
|
||||
viewModelScope.launch(ioDispatcher) {
|
||||
rssRepository.get().doSync()
|
||||
}
|
||||
}
|
||||
|
||||
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.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
|
|
@ -16,6 +16,7 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
@ -29,6 +30,7 @@ fun SettingItem(
|
|||
title: String,
|
||||
desc: String? = null,
|
||||
icon: ImageVector? = null,
|
||||
iconPainter: Painter? = null,
|
||||
separatedActions: Boolean = false,
|
||||
onClick: () -> Unit,
|
||||
action: (@Composable () -> Unit)? = null,
|
||||
|
@ -47,13 +49,24 @@ fun SettingItem(
|
|||
.padding(24.dp, 16.dp, 16.dp, 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
icon?.let {
|
||||
if (icon != null) {
|
||||
Icon(
|
||||
imageVector = it,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.padding(end = 24.dp),
|
||||
imageVector = icon,
|
||||
contentDescription = title,
|
||||
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)) {
|
||||
Text(
|
||||
|
|
|
@ -92,8 +92,11 @@ fun SettingsPage(
|
|||
title = stringResource(R.string.accounts),
|
||||
desc = stringResource(R.string.accounts_desc),
|
||||
icon = Icons.Outlined.AccountCircle,
|
||||
enable = false,
|
||||
) {}
|
||||
) {
|
||||
navController.navigate(RouteName.ACCOUNTS) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
}
|
||||
item {
|
||||
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="videos_desc">圆角、水平边距</string>
|
||||
<string name="maximize">最大化</string>
|
||||
</resources>
|
||||
<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>
|
||||
|
|
|
@ -330,4 +330,55 @@
|
|||
<string name="images_desc">Rounded corners, horizontal padding</string>
|
||||
<string name="videos_desc">Rounded corners, horizontal padding</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>
|
||||
|
|
Loading…
Reference in New Issue
Block a user