Support Fever API (#291)
This commit is contained in:
parent
ba745e8ab1
commit
3642e22fbb
371
app/schemas/me.ash.reader.data.source.RYDatabase/4.json
Normal file
371
app/schemas/me.ash.reader.data.source.RYDatabase/4.json
Normal file
@ -0,0 +1,371 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 4,
|
||||||
|
"identityHash": "ff6225eee095fd62d3d3bff48aa0be8e",
|
||||||
|
"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 '', `securityKey` TEXT DEFAULT 'CvJ1PKM8EW8=')",
|
||||||
|
"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": "''"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "securityKey",
|
||||||
|
"columnName": "securityKey",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false,
|
||||||
|
"defaultValue": "'CvJ1PKM8EW8='"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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, 'ff6225eee095fd62d3d3bff48aa0be8e')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
@ -13,7 +13,7 @@ import kotlinx.coroutines.withContext
|
|||||||
import me.ash.reader.data.module.ApplicationScope
|
import me.ash.reader.data.module.ApplicationScope
|
||||||
import me.ash.reader.data.module.IODispatcher
|
import me.ash.reader.data.module.IODispatcher
|
||||||
import me.ash.reader.data.repository.*
|
import me.ash.reader.data.repository.*
|
||||||
import me.ash.reader.data.source.OpmlLocalDataSource
|
import me.ash.reader.data.source.OPMLDataSource
|
||||||
import me.ash.reader.data.source.RYDatabase
|
import me.ash.reader.data.source.RYDatabase
|
||||||
import me.ash.reader.data.source.RYNetworkDataSource
|
import me.ash.reader.data.source.RYNetworkDataSource
|
||||||
import me.ash.reader.ui.ext.del
|
import me.ash.reader.ui.ext.del
|
||||||
@ -51,7 +51,7 @@ class RYApp : Application(), Configuration.Provider {
|
|||||||
lateinit var ryNetworkDataSource: RYNetworkDataSource
|
lateinit var ryNetworkDataSource: RYNetworkDataSource
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var opmlLocalDataSource: OpmlLocalDataSource
|
lateinit var OPMLDataSource: OPMLDataSource
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var rssHelper: RssHelper
|
lateinit var rssHelper: RssHelper
|
||||||
|
@ -276,6 +276,19 @@ interface ArticleDao {
|
|||||||
isUnread: Boolean,
|
isUnread: Boolean,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
UPDATE article SET isStarred = :isStarred
|
||||||
|
WHERE id = :articleId
|
||||||
|
AND accountId = :accountId
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
suspend fun markAsStarredByArticleId(
|
||||||
|
accountId: Int,
|
||||||
|
articleId: String,
|
||||||
|
isStarred: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
DELETE FROM article
|
DELETE FROM article
|
||||||
@ -540,8 +553,14 @@ interface ArticleDao {
|
|||||||
)
|
)
|
||||||
suspend fun queryById(id: String): ArticleWithFeed?
|
suspend fun queryById(id: String): ArticleWithFeed?
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
suspend fun insert(vararg article: Article)
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
|
suspend fun insertOnConflictIgnore(vararg article: Article)
|
||||||
|
|
||||||
@Insert
|
@Insert
|
||||||
suspend fun insertList(articles: List<Article>): List<Long>
|
suspend fun insertList(articles: List<Article>)
|
||||||
|
|
||||||
@Update
|
@Update
|
||||||
suspend fun update(vararg article: Article)
|
suspend fun update(vararg article: Article)
|
||||||
|
@ -87,8 +87,8 @@ interface FeedDao {
|
|||||||
)
|
)
|
||||||
suspend fun queryByLink(accountId: Int, url: String): List<Feed>
|
suspend fun queryByLink(accountId: Int, url: String): List<Feed>
|
||||||
|
|
||||||
@Insert
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
suspend fun insert(feed: Feed): Long
|
suspend fun insert(vararg feed: Feed)
|
||||||
|
|
||||||
@Insert
|
@Insert
|
||||||
suspend fun insertList(feeds: List<Feed>): List<Long>
|
suspend fun insertList(feeds: List<Feed>): List<Long>
|
||||||
|
@ -58,8 +58,8 @@ interface GroupDao {
|
|||||||
)
|
)
|
||||||
suspend fun queryAll(accountId: Int): List<Group>
|
suspend fun queryAll(accountId: Int): List<Group>
|
||||||
|
|
||||||
@Insert
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
suspend fun insert(group: Group): Long
|
suspend fun insert(vararg group: Group)
|
||||||
|
|
||||||
@Update
|
@Update
|
||||||
suspend fun update(vararg group: Group)
|
suspend fun update(vararg group: Group)
|
||||||
|
@ -3,6 +3,7 @@ package me.ash.reader.data.model.account
|
|||||||
import androidx.room.ColumnInfo
|
import androidx.room.ColumnInfo
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
|
import me.ash.reader.data.model.account.security.DESUtils
|
||||||
import me.ash.reader.data.model.preference.*
|
import me.ash.reader.data.model.preference.*
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@ -32,4 +33,6 @@ data class Account(
|
|||||||
var keepArchived: KeepArchivedPreference = KeepArchivedPreference.default,
|
var keepArchived: KeepArchivedPreference = KeepArchivedPreference.default,
|
||||||
@ColumnInfo(defaultValue = "")
|
@ColumnInfo(defaultValue = "")
|
||||||
var syncBlockList: SyncBlockList = SyncBlockListPreference.default,
|
var syncBlockList: SyncBlockList = SyncBlockListPreference.default,
|
||||||
|
@ColumnInfo(defaultValue = DESUtils.empty)
|
||||||
|
var securityKey: String? = DESUtils.empty,
|
||||||
)
|
)
|
||||||
|
@ -0,0 +1,34 @@
|
|||||||
|
package me.ash.reader.data.model.account.security
|
||||||
|
|
||||||
|
import android.util.Base64
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
import javax.crypto.SecretKeyFactory
|
||||||
|
import javax.crypto.spec.DESKeySpec
|
||||||
|
|
||||||
|
object DESUtils {
|
||||||
|
|
||||||
|
const val empty = "CvJ1PKM8EW8="
|
||||||
|
private const val secret = "mJn':4Nbk};AMVFGEWiY!(8&gp1xOv@/"
|
||||||
|
|
||||||
|
fun encrypt(cleartext: String): String {
|
||||||
|
val key = SecretKeyFactory
|
||||||
|
.getInstance("DES")
|
||||||
|
.generateSecret(DESKeySpec(secret.toByteArray()))
|
||||||
|
|
||||||
|
return Cipher.getInstance("DES").run {
|
||||||
|
init(Cipher.ENCRYPT_MODE, key)
|
||||||
|
Base64.encodeToString(doFinal(cleartext.toByteArray()), Base64.DEFAULT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decrypt(ciphertext: String): String {
|
||||||
|
val key = SecretKeyFactory
|
||||||
|
.getInstance("DES")
|
||||||
|
.generateSecret(DESKeySpec(secret.toByteArray()))
|
||||||
|
|
||||||
|
return Cipher.getInstance("DES").run {
|
||||||
|
init(Cipher.DECRYPT_MODE, key)
|
||||||
|
String(doFinal(Base64.decode(ciphertext, Base64.DEFAULT)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
package me.ash.reader.data.model.account.security
|
||||||
|
|
||||||
|
class FeverSecurityKey private constructor() : SecurityKey() {
|
||||||
|
|
||||||
|
var serverUrl: String? = null
|
||||||
|
var username: String? = null
|
||||||
|
var password: String? = null
|
||||||
|
|
||||||
|
constructor(serverUrl: String?, username: String?, password: String?) : this() {
|
||||||
|
this.serverUrl = serverUrl
|
||||||
|
this.username = username
|
||||||
|
this.password = password
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(value: String? = DESUtils.empty) : this() {
|
||||||
|
decode(value, FeverSecurityKey::class.java).let {
|
||||||
|
serverUrl = it.serverUrl
|
||||||
|
username = it.username
|
||||||
|
password = it.password
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
package me.ash.reader.data.model.account.security
|
||||||
|
|
||||||
|
class GoogleReaderSecurityKey private constructor() : SecurityKey() {
|
||||||
|
|
||||||
|
var serverUrl: String? = null
|
||||||
|
var username: String? = null
|
||||||
|
var password: String? = null
|
||||||
|
|
||||||
|
constructor(serverUrl: String?, username: String?, password: String?) : this() {
|
||||||
|
this.serverUrl = serverUrl
|
||||||
|
this.username = username
|
||||||
|
this.password = password
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(value: String? = DESUtils.empty) : this() {
|
||||||
|
decode(value, GoogleReaderSecurityKey::class.java).let {
|
||||||
|
serverUrl = it.serverUrl
|
||||||
|
username = it.username
|
||||||
|
password = it.password
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
package me.ash.reader.data.model.account.security
|
||||||
|
|
||||||
|
class LocalSecurityKey private constructor() : SecurityKey() {
|
||||||
|
|
||||||
|
constructor(value: String? = DESUtils.empty) : this() {
|
||||||
|
decode(value, LocalSecurityKey::class.java).let {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
package me.ash.reader.data.model.account.security
|
||||||
|
|
||||||
|
import com.google.gson.Gson
|
||||||
|
|
||||||
|
abstract class SecurityKey {
|
||||||
|
|
||||||
|
fun <T> decode(value: String?, classOfT: Class<T>): T =
|
||||||
|
Gson().fromJson(DESUtils.decrypt(value?.ifEmpty { DESUtils.empty } ?: DESUtils.empty), classOfT)
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return DESUtils.encrypt(Gson().toJson(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
return this.toString() == other.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return javaClass.hashCode()
|
||||||
|
}
|
||||||
|
}
|
@ -4,8 +4,6 @@ import dagger.Module
|
|||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import me.ash.reader.data.source.FeverApiDataSource
|
|
||||||
import me.ash.reader.data.source.GoogleReaderApiDataSource
|
|
||||||
import me.ash.reader.data.source.RYNetworkDataSource
|
import me.ash.reader.data.source.RYNetworkDataSource
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@ -13,8 +11,6 @@ import javax.inject.Singleton
|
|||||||
* Provides network requests for Retrofit.
|
* Provides network requests for Retrofit.
|
||||||
*
|
*
|
||||||
* - [RYNetworkDataSource]: For network requests within the application
|
* - [RYNetworkDataSource]: For network requests within the application
|
||||||
* - [FeverApiDataSource]: For network requests to the Fever API
|
|
||||||
* - [GoogleReaderApiDataSource]: For network requests to the Google Reader API
|
|
||||||
*/
|
*/
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
@ -24,14 +20,4 @@ object RetrofitModule {
|
|||||||
@Singleton
|
@Singleton
|
||||||
fun provideAppNetworkDataSource(): RYNetworkDataSource =
|
fun provideAppNetworkDataSource(): RYNetworkDataSource =
|
||||||
RYNetworkDataSource.getInstance()
|
RYNetworkDataSource.getInstance()
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun provideFeverApiDataSource(): FeverApiDataSource =
|
|
||||||
FeverApiDataSource.getInstance()
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun provideGoogleReaderApiDataSource(): GoogleReaderApiDataSource =
|
|
||||||
GoogleReaderApiDataSource.getInstance()
|
|
||||||
}
|
}
|
||||||
|
20
app/src/main/java/me/ash/reader/data/provider/BaseAPI.kt
Normal file
20
app/src/main/java/me/ash/reader/data/provider/BaseAPI.kt
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
package me.ash.reader.data.provider
|
||||||
|
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.GsonBuilder
|
||||||
|
import me.ash.reader.data.module.UserAgentInterceptor
|
||||||
|
import me.ash.reader.data.module.cachingHttpClient
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
|
||||||
|
abstract class BaseAPI {
|
||||||
|
|
||||||
|
protected val client: OkHttpClient = cachingHttpClient()
|
||||||
|
.newBuilder()
|
||||||
|
.addNetworkInterceptor(UserAgentInterceptor)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
protected val gson: Gson = GsonBuilder().create()
|
||||||
|
|
||||||
|
protected inline fun <reified T> toDTO(jsonStr: String): T =
|
||||||
|
gson.fromJson(jsonStr, T::class.java)!!
|
||||||
|
}
|
117
app/src/main/java/me/ash/reader/data/provider/fever/FeverAPI.kt
Normal file
117
app/src/main/java/me/ash/reader/data/provider/fever/FeverAPI.kt
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
package me.ash.reader.data.provider.fever
|
||||||
|
|
||||||
|
import me.ash.reader.data.provider.BaseAPI
|
||||||
|
import me.ash.reader.ui.ext.encodeBase64
|
||||||
|
import me.ash.reader.ui.ext.md5
|
||||||
|
import okhttp3.FormBody
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.executeAsync
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
|
class FeverAPI private constructor(
|
||||||
|
private val serverUrl: String,
|
||||||
|
private val apiKey: String,
|
||||||
|
private val httpUsername: String? = null,
|
||||||
|
private val httpPassword: String? = null,
|
||||||
|
) : BaseAPI() {
|
||||||
|
|
||||||
|
private suspend inline fun <reified T> postRequest(query: String?): T {
|
||||||
|
val response = client.newCall(
|
||||||
|
Request.Builder()
|
||||||
|
.apply {
|
||||||
|
if (httpUsername != null) {
|
||||||
|
addHeader("Authorization", "Basic ${"$httpUsername:$httpPassword".encodeBase64()}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.url("$serverUrl?api=&${query ?: ""}")
|
||||||
|
.post(FormBody.Builder().add("api_key", apiKey).build())
|
||||||
|
.build())
|
||||||
|
.executeAsync()
|
||||||
|
|
||||||
|
when (response.code) {
|
||||||
|
401 -> throw Exception("Unauthorized")
|
||||||
|
!in 200..299 -> throw Exception("Forbidden")
|
||||||
|
}
|
||||||
|
|
||||||
|
return toDTO(response.body.string())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkAuth(authMap: Map<String, Any>): Int = checkAuth(authMap["auth"] as Int?)
|
||||||
|
|
||||||
|
private fun checkAuth(auth: Int?): Int = auth?.takeIf { it > 0 } ?: throw Exception("Unauthorized")
|
||||||
|
|
||||||
|
@Throws
|
||||||
|
suspend fun validCredentials(): Int = checkAuth(postRequest<FeverDTO.Common>(null).auth)
|
||||||
|
|
||||||
|
suspend fun getApiVersion(): Long =
|
||||||
|
postRequest<Map<String, Any>>(null)["api_version"] as Long?
|
||||||
|
?: throw Exception("Unable to get version")
|
||||||
|
|
||||||
|
suspend fun getGroups(): FeverDTO.Groups =
|
||||||
|
postRequest<FeverDTO.Groups>("groups").apply { checkAuth(auth) }
|
||||||
|
|
||||||
|
suspend fun getFeeds(): FeverDTO.Feeds =
|
||||||
|
postRequest<FeverDTO.Feeds>("feeds").apply { checkAuth(auth) }
|
||||||
|
|
||||||
|
suspend fun getFavicons(): FeverDTO.Favicons =
|
||||||
|
postRequest<FeverDTO.Favicons>("favicons").apply { checkAuth(auth) }
|
||||||
|
|
||||||
|
suspend fun getItems(): FeverDTO.Items =
|
||||||
|
postRequest<FeverDTO.Items>("items").apply { checkAuth(auth) }
|
||||||
|
|
||||||
|
suspend fun getItemsSince(id: String): FeverDTO.Items =
|
||||||
|
postRequest<FeverDTO.Items>("items&since_id=$id").apply { checkAuth(auth) }
|
||||||
|
|
||||||
|
suspend fun getItemsMax(id: String): FeverDTO.Items =
|
||||||
|
postRequest<FeverDTO.Items>("items&max_id=$id").apply { checkAuth(auth) }
|
||||||
|
|
||||||
|
suspend fun getItemsWith(ids: List<String>): FeverDTO.Items =
|
||||||
|
if (ids.size > 50) throw Exception("Too many ids")
|
||||||
|
else postRequest<FeverDTO.Items>("items&with_ids=${ids.joinToString(",")}").apply { checkAuth(auth) }
|
||||||
|
|
||||||
|
suspend fun getLinks(): FeverDTO.Links =
|
||||||
|
postRequest<FeverDTO.Links>("links").apply { checkAuth(auth) }
|
||||||
|
|
||||||
|
suspend fun getLinksWith(offset: Long, days: Long, page: Long): FeverDTO.Links =
|
||||||
|
postRequest<FeverDTO.Links>("links&offset=$offset&range=$days&page=$page").apply { checkAuth(auth) }
|
||||||
|
|
||||||
|
suspend fun getUnreadItems(): FeverDTO.ItemsByUnread =
|
||||||
|
postRequest<FeverDTO.ItemsByUnread>("unread_item_ids").apply { checkAuth(auth) }
|
||||||
|
|
||||||
|
suspend fun getSavedItems(): FeverDTO.ItemsByStarred =
|
||||||
|
postRequest<FeverDTO.ItemsByStarred>("saved_item_ids").apply { checkAuth(auth) }
|
||||||
|
|
||||||
|
suspend fun markItem(status: FeverDTO.StatusEnum, id: String): FeverDTO.Common =
|
||||||
|
postRequest<FeverDTO.Common>("&mark=item&as=${status.value}&id=$id").apply { checkAuth(auth) }
|
||||||
|
|
||||||
|
private suspend fun markFeedOrGroup(
|
||||||
|
act: String,
|
||||||
|
status: FeverDTO.StatusEnum,
|
||||||
|
id: Long,
|
||||||
|
before: Long,
|
||||||
|
): FeverDTO.Common = postRequest<FeverDTO.Common>("&mark=$act&as=${status.value}&id=$id&before=$before")
|
||||||
|
.apply { checkAuth(auth) }
|
||||||
|
|
||||||
|
suspend fun markGroup(status: FeverDTO.StatusEnum, id: Long, before: Long) =
|
||||||
|
markFeedOrGroup("group", status, id, before)
|
||||||
|
|
||||||
|
suspend fun markFeed(status: FeverDTO.StatusEnum, id: Long, before: Long) =
|
||||||
|
markFeedOrGroup("feed", status, id, before)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private val instances: ConcurrentHashMap<String, FeverAPI> = ConcurrentHashMap()
|
||||||
|
|
||||||
|
fun getInstance(
|
||||||
|
serverUrl: String,
|
||||||
|
username: String,
|
||||||
|
password: String,
|
||||||
|
httpUsername: String? = null,
|
||||||
|
httpPassword: String? = null,
|
||||||
|
): FeverAPI = "$username:$password".md5().run {
|
||||||
|
instances.getOrPut("$serverUrl$this$httpUsername$httpPassword") {
|
||||||
|
FeverAPI(serverUrl, this, httpUsername, httpPassword)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,12 @@
|
|||||||
package me.ash.reader.data.source
|
package me.ash.reader.data.provider.fever
|
||||||
|
|
||||||
object FeverApiDto {
|
object FeverDTO {
|
||||||
|
|
||||||
|
data class Common(
|
||||||
|
val api_version: Int?,
|
||||||
|
val auth: Int?,
|
||||||
|
val last_refreshed_on_time: Long?,
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @link fever.php/?api=&feeds=
|
* @link fever.php/?api=&feeds=
|
||||||
@ -28,22 +34,22 @@ object FeverApiDto {
|
|||||||
* ]
|
* ]
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
data class Feed(
|
data class Feeds(
|
||||||
val api_version: Int,
|
val api_version: Int?,
|
||||||
val auth: Int,
|
val auth: Int?,
|
||||||
val last_refreshed_on_time: Long,
|
val last_refreshed_on_time: Long?,
|
||||||
val feeds: List<FeedItem>,
|
val feeds: List<FeedItem>?,
|
||||||
val feeds_groups: List<FeedsGroupsItem>,
|
val feeds_groups: List<FeedsGroupsItem>?,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class FeedItem(
|
data class FeedItem(
|
||||||
val id: Int,
|
val id: Int?,
|
||||||
val favicon_id: Int,
|
val favicon_id: Int?,
|
||||||
val title: String,
|
val title: String?,
|
||||||
val url: String,
|
val url: String?,
|
||||||
val site_url: String,
|
val site_url: String?,
|
||||||
val is_spark: Int,
|
val is_spark: Int?,
|
||||||
val last_refreshed_on_time: Long,
|
val last_refreshed_on_time: Long?,
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -68,21 +74,39 @@ object FeverApiDto {
|
|||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
data class Groups(
|
data class Groups(
|
||||||
val api_version: Int,
|
val api_version: Int?,
|
||||||
val auth: Int,
|
val auth: Int?,
|
||||||
val last_refreshed_on_time: Long,
|
val last_refreshed_on_time: Long?,
|
||||||
val groups: List<GroupItem>,
|
val groups: List<GroupItem>?,
|
||||||
val feeds_groups: List<FeedsGroupsItem>,
|
val feeds_groups: List<FeedsGroupsItem>?,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class GroupItem(
|
data class GroupItem(
|
||||||
val id: Int,
|
val id: Int?,
|
||||||
val title: String,
|
val title: String?,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class FeedsGroupsItem(
|
data class FeedsGroupsItem(
|
||||||
val group_id: Int,
|
val group_id: Int?,
|
||||||
val feed_ids: String,
|
val feed_ids: String?,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @link fever.php/?api=&favicons=
|
||||||
|
* @sample
|
||||||
|
* {
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
data class Favicons(
|
||||||
|
val api_version: Int?,
|
||||||
|
val auth: Int?,
|
||||||
|
val last_refreshed_on_time: Long?,
|
||||||
|
val favicons: Map<String, Favicon>,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Favicon(
|
||||||
|
val mime_type: String,
|
||||||
|
val data: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -110,23 +134,49 @@ object FeverApiDto {
|
|||||||
* {
|
* {
|
||||||
*/
|
*/
|
||||||
data class Items(
|
data class Items(
|
||||||
val api_version: Int,
|
val api_version: Int?,
|
||||||
val auth: Int,
|
val auth: Int?,
|
||||||
val last_refreshed_on_time: Long,
|
val last_refreshed_on_time: Long?,
|
||||||
val total_items: Int,
|
val total_items: Int?,
|
||||||
val items: List<Item>,
|
val items: List<Item>?,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class Item(
|
data class Item(
|
||||||
val id: String,
|
val id: String?,
|
||||||
val feed_id: Int,
|
val feed_id: Int?,
|
||||||
val title: String,
|
val title: String?,
|
||||||
val author: String,
|
val author: String?,
|
||||||
val html: String,
|
val html: String?,
|
||||||
val url: String,
|
val url: String?,
|
||||||
val is_saved: Int,
|
val is_saved: Int?,
|
||||||
val is_read: Int,
|
val is_read: Int?,
|
||||||
val created_on_time: Long,
|
val created_on_time: Long?,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @link fever.php/?api=&links=
|
||||||
|
* @sample
|
||||||
|
* {
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
data class Links(
|
||||||
|
val api_version: Int?,
|
||||||
|
val auth: Int?,
|
||||||
|
val last_refreshed_on_time: Long?,
|
||||||
|
val links: List<Link>?,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Link(
|
||||||
|
val id: String?,
|
||||||
|
val feed_id: String?,
|
||||||
|
val item_id: String?,
|
||||||
|
val temperature: Float?,
|
||||||
|
val is_item: Boolean?,
|
||||||
|
val is_local: Boolean?,
|
||||||
|
val is_saved: Boolean?,
|
||||||
|
val title: String?,
|
||||||
|
val url: String?,
|
||||||
|
val item_ids: List<String>?,
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -140,10 +190,10 @@ object FeverApiDto {
|
|||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
data class ItemsByUnread(
|
data class ItemsByUnread(
|
||||||
val api_version: Int,
|
val api_version: Int?,
|
||||||
val auth: Int,
|
val auth: Int?,
|
||||||
val last_refreshed_on_time: Long,
|
val last_refreshed_on_time: Long?,
|
||||||
val unread_item_ids: String,
|
val unread_item_ids: String?,
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -157,9 +207,16 @@ object FeverApiDto {
|
|||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
data class ItemsByStarred(
|
data class ItemsByStarred(
|
||||||
val api_version: Int,
|
val api_version: Int?,
|
||||||
val auth: Int,
|
val auth: Int?,
|
||||||
val last_refreshed_on_time: Long,
|
val last_refreshed_on_time: Long?,
|
||||||
val saved_item_ids: String,
|
val saved_item_ids: String?,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
enum class StatusEnum(val value: String) {
|
||||||
|
Read("read"),
|
||||||
|
Unread("unread"),
|
||||||
|
Saved("saved"),
|
||||||
|
Unsaved("unsaved"),
|
||||||
|
}
|
||||||
}
|
}
|
@ -7,9 +7,12 @@ import androidx.work.CoroutineWorker
|
|||||||
import androidx.work.ListenableWorker
|
import androidx.work.ListenableWorker
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.flowOn
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.flow.mapLatest
|
import kotlinx.coroutines.flow.mapLatest
|
||||||
|
import kotlinx.coroutines.supervisorScope
|
||||||
import me.ash.reader.data.dao.AccountDao
|
import me.ash.reader.data.dao.AccountDao
|
||||||
import me.ash.reader.data.dao.ArticleDao
|
import me.ash.reader.data.dao.ArticleDao
|
||||||
import me.ash.reader.data.dao.FeedDao
|
import me.ash.reader.data.dao.FeedDao
|
||||||
@ -17,11 +20,13 @@ import me.ash.reader.data.dao.GroupDao
|
|||||||
import me.ash.reader.data.model.article.Article
|
import me.ash.reader.data.model.article.Article
|
||||||
import me.ash.reader.data.model.article.ArticleWithFeed
|
import me.ash.reader.data.model.article.ArticleWithFeed
|
||||||
import me.ash.reader.data.model.feed.Feed
|
import me.ash.reader.data.model.feed.Feed
|
||||||
|
import me.ash.reader.data.model.feed.FeedWithArticle
|
||||||
import me.ash.reader.data.model.group.Group
|
import me.ash.reader.data.model.group.Group
|
||||||
import me.ash.reader.data.model.group.GroupWithFeed
|
import me.ash.reader.data.model.group.GroupWithFeed
|
||||||
import me.ash.reader.data.model.preference.KeepArchivedPreference
|
import me.ash.reader.data.model.preference.KeepArchivedPreference
|
||||||
import me.ash.reader.data.model.preference.SyncIntervalPreference
|
import me.ash.reader.data.model.preference.SyncIntervalPreference
|
||||||
import me.ash.reader.ui.ext.currentAccountId
|
import me.ash.reader.ui.ext.currentAccountId
|
||||||
|
import me.ash.reader.ui.ext.spacerDollar
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
abstract class AbstractRssRepository constructor(
|
abstract class AbstractRssRepository constructor(
|
||||||
@ -31,31 +36,135 @@ abstract class AbstractRssRepository constructor(
|
|||||||
private val groupDao: GroupDao,
|
private val groupDao: GroupDao,
|
||||||
private val feedDao: FeedDao,
|
private val feedDao: FeedDao,
|
||||||
private val workManager: WorkManager,
|
private val workManager: WorkManager,
|
||||||
|
private val rssHelper: RssHelper,
|
||||||
|
private val notificationHelper: NotificationHelper,
|
||||||
private val dispatcherIO: CoroutineDispatcher,
|
private val dispatcherIO: CoroutineDispatcher,
|
||||||
private val dispatcherDefault: CoroutineDispatcher,
|
private val dispatcherDefault: CoroutineDispatcher,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
abstract suspend fun updateArticleInfo(article: Article)
|
open val subscribe = true
|
||||||
|
open val move = true
|
||||||
|
|
||||||
abstract suspend fun subscribe(feed: Feed, articles: List<Article>)
|
open suspend fun validCredentials(): Boolean = true
|
||||||
|
|
||||||
abstract suspend fun addGroup(name: String): String
|
open suspend fun subscribe(feed: Feed, articles: List<Article>) {
|
||||||
|
feedDao.insert(feed)
|
||||||
|
articleDao.insertList(articles.map {
|
||||||
|
it.copy(feedId = feed.id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
abstract suspend fun sync(coroutineWorker: CoroutineWorker): ListenableWorker.Result
|
open suspend fun addGroup(name: String): String {
|
||||||
|
context.currentAccountId.let { accountId ->
|
||||||
|
return accountId.spacerDollar(UUID.randomUUID().toString()).also {
|
||||||
|
groupDao.insert(
|
||||||
|
Group(
|
||||||
|
id = it,
|
||||||
|
name = name,
|
||||||
|
accountId = accountId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
abstract suspend fun markAsRead(
|
open suspend fun sync(coroutineWorker: CoroutineWorker): ListenableWorker.Result =
|
||||||
|
supervisorScope {
|
||||||
|
coroutineWorker.setProgress(SyncWorker.setIsSyncing(true))
|
||||||
|
val preTime = System.currentTimeMillis()
|
||||||
|
val accountId = context.currentAccountId
|
||||||
|
feedDao.queryAll(accountId)
|
||||||
|
.chunked(16)
|
||||||
|
.forEach {
|
||||||
|
it.map { feed -> async { syncFeed(feed) } }
|
||||||
|
.awaitAll()
|
||||||
|
.forEach {
|
||||||
|
if (it.feed.isNotification) {
|
||||||
|
notificationHelper.notify(it.apply {
|
||||||
|
articles = articleDao.insertListIfNotExist(it.articles)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
articleDao.insertListIfNotExist(it.articles)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.i("RlOG", "onCompletion: ${System.currentTimeMillis() - preTime}")
|
||||||
|
accountDao.queryById(accountId)?.let { account ->
|
||||||
|
accountDao.update(account.apply { updateAt = Date() })
|
||||||
|
}
|
||||||
|
coroutineWorker.setProgress(SyncWorker.setIsSyncing(false))
|
||||||
|
ListenableWorker.Result.success()
|
||||||
|
}
|
||||||
|
|
||||||
|
open suspend fun markAsRead(
|
||||||
groupId: String?,
|
groupId: String?,
|
||||||
feedId: String?,
|
feedId: String?,
|
||||||
articleId: String?,
|
articleId: String?,
|
||||||
before: Date?,
|
before: Date?,
|
||||||
isUnread: Boolean,
|
isUnread: Boolean,
|
||||||
)
|
) {
|
||||||
|
val accountId = context.currentAccountId
|
||||||
|
when {
|
||||||
|
groupId != null -> {
|
||||||
|
articleDao.markAllAsReadByGroupId(
|
||||||
|
accountId = accountId,
|
||||||
|
groupId = groupId,
|
||||||
|
isUnread = isUnread,
|
||||||
|
before = before ?: Date(Long.MAX_VALUE)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun keepArchivedArticles() {
|
feedId != null -> {
|
||||||
|
articleDao.markAllAsReadByFeedId(
|
||||||
|
accountId = accountId,
|
||||||
|
feedId = feedId,
|
||||||
|
isUnread = isUnread,
|
||||||
|
before = before ?: Date(Long.MAX_VALUE)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
articleId != null -> {
|
||||||
|
articleDao.markAsReadByArticleId(accountId, articleId, isUnread)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
articleDao.markAllAsRead(accountId, isUnread, before ?: Date(Long.MAX_VALUE))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open suspend fun markAsStarred(articleId: String, isStarred: Boolean) {
|
||||||
|
val accountId = context.currentAccountId
|
||||||
|
articleDao.markAsStarredByArticleId(accountId, articleId, isStarred)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun syncFeed(feed: Feed): FeedWithArticle {
|
||||||
|
val latest = articleDao.queryLatestByFeedId(context.currentAccountId, feed.id)
|
||||||
|
val articles = rssHelper.queryRssXml(feed, latest?.link)
|
||||||
|
// try {
|
||||||
|
// if (feed.icon == null && !articles.isNullOrEmpty()) {
|
||||||
|
// rssHelper.queryRssIcon(feedDao, feed, articles.first().link)
|
||||||
|
// }
|
||||||
|
// } catch (e: Exception) {
|
||||||
|
// Log.e("RLog", "queryRssIcon[${feed.name}]: ${e.message}")
|
||||||
|
// return FeedWithArticle(
|
||||||
|
// feed = feed.apply { isNotification = false },
|
||||||
|
// articles = listOf()
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
return FeedWithArticle(
|
||||||
|
feed = feed.apply { isNotification = feed.isNotification && articles.isNotEmpty() },
|
||||||
|
articles = articles
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun clearKeepArchivedArticles() {
|
||||||
accountDao.queryById(context.currentAccountId)!!
|
accountDao.queryById(context.currentAccountId)!!
|
||||||
.takeIf { it.keepArchived != KeepArchivedPreference.Always }
|
.takeIf { it.keepArchived != KeepArchivedPreference.Always }
|
||||||
?.let {
|
?.let {
|
||||||
articleDao.deleteAllArchivedBeforeThan(it.id!!, Date(System.currentTimeMillis() - it.keepArchived.value))
|
articleDao.deleteAllArchivedBeforeThan(it.id!!,
|
||||||
|
Date(System.currentTimeMillis() - it.keepArchived.value))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,7 +56,7 @@ class AccountRepository @Inject constructor(
|
|||||||
suspend fun addDefaultAccount(): Account =
|
suspend fun addDefaultAccount(): Account =
|
||||||
addAccount(Account(
|
addAccount(Account(
|
||||||
type = AccountType.Local,
|
type = AccountType.Local,
|
||||||
name = context.getString(R.string.read_you)
|
name = context.getString(R.string.read_you),
|
||||||
))
|
))
|
||||||
|
|
||||||
suspend fun update(accountId: Int, block: Account.() -> Unit) {
|
suspend fun update(accountId: Int, block: Account.() -> Unit) {
|
||||||
|
@ -0,0 +1,231 @@
|
|||||||
|
package me.ash.reader.data.repository
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.text.Html
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.work.CoroutineWorker
|
||||||
|
import androidx.work.ListenableWorker
|
||||||
|
import androidx.work.WorkManager
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.supervisorScope
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
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.security.FeverSecurityKey
|
||||||
|
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.module.DefaultDispatcher
|
||||||
|
import me.ash.reader.data.module.IODispatcher
|
||||||
|
import me.ash.reader.data.module.MainDispatcher
|
||||||
|
import me.ash.reader.data.provider.fever.FeverAPI
|
||||||
|
import me.ash.reader.data.provider.fever.FeverDTO
|
||||||
|
import me.ash.reader.ui.ext.currentAccountId
|
||||||
|
import me.ash.reader.ui.ext.dollarLast
|
||||||
|
import me.ash.reader.ui.ext.showToast
|
||||||
|
import me.ash.reader.ui.ext.spacerDollar
|
||||||
|
import net.dankito.readability4j.extended.Readability4JExtended
|
||||||
|
import java.util.*
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class FeverRssRepository @Inject constructor(
|
||||||
|
@ApplicationContext
|
||||||
|
private val context: Context,
|
||||||
|
private val articleDao: ArticleDao,
|
||||||
|
private val feedDao: FeedDao,
|
||||||
|
private val rssHelper: RssHelper,
|
||||||
|
private val notificationHelper: NotificationHelper,
|
||||||
|
private val accountDao: AccountDao,
|
||||||
|
private val groupDao: GroupDao,
|
||||||
|
@IODispatcher
|
||||||
|
private val ioDispatcher: CoroutineDispatcher,
|
||||||
|
@MainDispatcher
|
||||||
|
private val mainDispatcher: CoroutineDispatcher,
|
||||||
|
@DefaultDispatcher
|
||||||
|
private val defaultDispatcher: CoroutineDispatcher,
|
||||||
|
workManager: WorkManager,
|
||||||
|
) : AbstractRssRepository(
|
||||||
|
context, accountDao, articleDao, groupDao,
|
||||||
|
feedDao, workManager, rssHelper, notificationHelper, ioDispatcher, defaultDispatcher
|
||||||
|
) {
|
||||||
|
|
||||||
|
override val subscribe = false
|
||||||
|
override val move: Boolean = false
|
||||||
|
|
||||||
|
private suspend fun getFeverAPI() =
|
||||||
|
FeverSecurityKey(accountDao.queryById(context.currentAccountId)!!.securityKey).run {
|
||||||
|
FeverAPI.getInstance(
|
||||||
|
serverUrl = serverUrl!!,
|
||||||
|
username = username!!,
|
||||||
|
password = password!!,
|
||||||
|
httpUsername = null,
|
||||||
|
httpPassword = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun validCredentials(): Boolean = getFeverAPI().validCredentials() > 0
|
||||||
|
|
||||||
|
override suspend fun subscribe(feed: Feed, articles: List<Article>) {
|
||||||
|
throw Exception("Unsupported")
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun addGroup(name: String): String {
|
||||||
|
throw Exception("Unsupported")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync handling for the Fever API.
|
||||||
|
*
|
||||||
|
* 1. Fetch the Fever groups
|
||||||
|
* 2. Fetch the Fever feeds
|
||||||
|
* 3. Fetch the Fever articles
|
||||||
|
* 4. Fetch the Fever favicons
|
||||||
|
*/
|
||||||
|
override suspend fun sync(coroutineWorker: CoroutineWorker): ListenableWorker.Result =
|
||||||
|
supervisorScope {
|
||||||
|
coroutineWorker.setProgress(SyncWorker.setIsSyncing(true))
|
||||||
|
|
||||||
|
try {
|
||||||
|
val preTime = System.currentTimeMillis()
|
||||||
|
val accountId = context.currentAccountId
|
||||||
|
val feverAPI = getFeverAPI()
|
||||||
|
|
||||||
|
// 1. Fetch the Fever groups
|
||||||
|
groupDao.insert(
|
||||||
|
*feverAPI.getGroups().groups?.map {
|
||||||
|
Group(
|
||||||
|
id = accountId.spacerDollar(it.id!!),
|
||||||
|
name = it.title ?: context.getString(R.string.empty),
|
||||||
|
accountId = accountId,
|
||||||
|
)
|
||||||
|
}?.toTypedArray() ?: emptyArray()
|
||||||
|
)
|
||||||
|
|
||||||
|
// 2. Fetch the Fever feeds
|
||||||
|
val feedsBody = feverAPI.getFeeds()
|
||||||
|
val feedsGroupsMap = mutableMapOf<String, String>()
|
||||||
|
feedsBody.feeds_groups?.forEach { feedsGroups ->
|
||||||
|
feedsGroups.group_id?.toString()?.let { groupId ->
|
||||||
|
feedsGroups.feed_ids?.split(",")?.forEach { feedId ->
|
||||||
|
feedsGroupsMap[feedId] = groupId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
feedDao.insert(
|
||||||
|
*feedsBody.feeds?.map {
|
||||||
|
Feed(
|
||||||
|
id = accountId.spacerDollar(it.id!!),
|
||||||
|
name = it.title ?: context.getString(R.string.empty),
|
||||||
|
url = it.url!!,
|
||||||
|
groupId = accountId.spacerDollar(feedsGroupsMap[it.id.toString()]!!),
|
||||||
|
accountId = accountId,
|
||||||
|
)
|
||||||
|
}?.toTypedArray() ?: emptyArray()
|
||||||
|
)
|
||||||
|
|
||||||
|
// 3. Fetch the Fever articles (up to unlimited counts)
|
||||||
|
var sinceId = ""
|
||||||
|
var itemsBody = feverAPI.getItemsSince(sinceId)
|
||||||
|
while (itemsBody.items?.isEmpty() == false) {
|
||||||
|
articleDao.insert(
|
||||||
|
*itemsBody.items?.map {
|
||||||
|
Article(
|
||||||
|
id = accountId.spacerDollar(it.id!!),
|
||||||
|
date = it.created_on_time?.run { Date(this * 1000) } ?: Date(),
|
||||||
|
title = Html.fromHtml(it.title ?: context.getString(R.string.empty)).toString(),
|
||||||
|
author = it.author,
|
||||||
|
rawDescription = it.html ?: "",
|
||||||
|
shortDescription = (Readability4JExtended("", it.html ?: "")
|
||||||
|
.parse().textContent ?: "")
|
||||||
|
.take(110)
|
||||||
|
.trim(),
|
||||||
|
fullContent = it.html,
|
||||||
|
img = rssHelper.findImg(it.html ?: ""),
|
||||||
|
link = it.url ?: "",
|
||||||
|
feedId = accountId.spacerDollar(it.feed_id!!),
|
||||||
|
accountId = accountId,
|
||||||
|
isUnread = (it.is_read ?: 0) <= 0,
|
||||||
|
isStarred = (it.is_saved ?: 0) > 0,
|
||||||
|
updateAt = Date(),
|
||||||
|
).also {
|
||||||
|
sinceId = it.id.dollarLast()
|
||||||
|
}
|
||||||
|
}?.toTypedArray() ?: emptyArray()
|
||||||
|
)
|
||||||
|
itemsBody = feverAPI.getItemsSince(sinceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: 4. Fetch the Fever favicons
|
||||||
|
|
||||||
|
Log.i("RLog", "onCompletion: ${System.currentTimeMillis() - preTime}")
|
||||||
|
accountDao.queryById(accountId)?.let { account ->
|
||||||
|
accountDao.update(account.apply { updateAt = Date() })
|
||||||
|
}
|
||||||
|
ListenableWorker.Result.success(SyncWorker.setIsSyncing(false))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("RLog", "On sync exception: ${e.message}", e)
|
||||||
|
withContext(mainDispatcher) {
|
||||||
|
context.showToast(e.message)
|
||||||
|
}
|
||||||
|
ListenableWorker.Result.failure(SyncWorker.setIsSyncing(false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun markAsRead(
|
||||||
|
groupId: String?,
|
||||||
|
feedId: String?,
|
||||||
|
articleId: String?,
|
||||||
|
before: Date?,
|
||||||
|
isUnread: Boolean,
|
||||||
|
) {
|
||||||
|
super.markAsRead(groupId, feedId, articleId, before, isUnread)
|
||||||
|
val feverAPI = getFeverAPI()
|
||||||
|
when {
|
||||||
|
groupId != null -> {
|
||||||
|
feverAPI.markGroup(
|
||||||
|
status = if (isUnread) FeverDTO.StatusEnum.Unread else FeverDTO.StatusEnum.Read,
|
||||||
|
id = groupId.dollarLast().toLong(),
|
||||||
|
before = before?.time ?: Date(Long.MAX_VALUE).time
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
feedId != null -> {
|
||||||
|
feverAPI.markFeed(
|
||||||
|
status = if (isUnread) FeverDTO.StatusEnum.Unread else FeverDTO.StatusEnum.Read,
|
||||||
|
id = feedId.dollarLast().toLong(),
|
||||||
|
before = before?.time ?: Date(Long.MAX_VALUE).time
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
articleId != null -> {
|
||||||
|
feverAPI.markItem(
|
||||||
|
status = if (isUnread) FeverDTO.StatusEnum.Unread else FeverDTO.StatusEnum.Read,
|
||||||
|
id = articleId.dollarLast(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
feedDao.queryAll(context.currentAccountId).forEach {
|
||||||
|
feverAPI.markFeed(
|
||||||
|
status = if (isUnread) FeverDTO.StatusEnum.Unread else FeverDTO.StatusEnum.Read,
|
||||||
|
id = it.id.dollarLast().toLong(),
|
||||||
|
before = before?.time ?: Date(Long.MAX_VALUE).time
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun markAsStarred(articleId: String, isStarred: Boolean) {
|
||||||
|
super.markAsStarred(articleId, isStarred)
|
||||||
|
val feverAPI = getFeverAPI()
|
||||||
|
feverAPI.markItem(
|
||||||
|
status = if (isStarred) FeverDTO.StatusEnum.Saved else FeverDTO.StatusEnum.Unsaved,
|
||||||
|
id = articleId.dollarLast()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -1,29 +1,15 @@
|
|||||||
package me.ash.reader.data.repository
|
package me.ash.reader.data.repository
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
|
||||||
import androidx.work.CoroutineWorker
|
|
||||||
import androidx.work.ListenableWorker
|
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.awaitAll
|
|
||||||
import kotlinx.coroutines.supervisorScope
|
|
||||||
import me.ash.reader.data.dao.AccountDao
|
import me.ash.reader.data.dao.AccountDao
|
||||||
import me.ash.reader.data.dao.ArticleDao
|
import me.ash.reader.data.dao.ArticleDao
|
||||||
import me.ash.reader.data.dao.FeedDao
|
import me.ash.reader.data.dao.FeedDao
|
||||||
import me.ash.reader.data.dao.GroupDao
|
import me.ash.reader.data.dao.GroupDao
|
||||||
import me.ash.reader.data.model.article.Article
|
|
||||||
import me.ash.reader.data.model.feed.Feed
|
|
||||||
import me.ash.reader.data.model.feed.FeedWithArticle
|
|
||||||
import me.ash.reader.data.model.group.Group
|
|
||||||
import me.ash.reader.data.module.DefaultDispatcher
|
import me.ash.reader.data.module.DefaultDispatcher
|
||||||
import me.ash.reader.data.module.IODispatcher
|
import me.ash.reader.data.module.IODispatcher
|
||||||
import me.ash.reader.data.repository.SyncWorker.Companion.setIsSyncing
|
|
||||||
import me.ash.reader.ui.ext.currentAccountId
|
|
||||||
import me.ash.reader.ui.ext.spacerDollar
|
|
||||||
import java.util.*
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class LocalRssRepository @Inject constructor(
|
class LocalRssRepository @Inject constructor(
|
||||||
@ -42,117 +28,5 @@ class LocalRssRepository @Inject constructor(
|
|||||||
workManager: WorkManager,
|
workManager: WorkManager,
|
||||||
) : AbstractRssRepository(
|
) : AbstractRssRepository(
|
||||||
context, accountDao, articleDao, groupDao,
|
context, accountDao, articleDao, groupDao,
|
||||||
feedDao, workManager, ioDispatcher, defaultDispatcher
|
feedDao, workManager, rssHelper, notificationHelper, ioDispatcher, defaultDispatcher
|
||||||
) {
|
)
|
||||||
|
|
||||||
override suspend fun updateArticleInfo(article: Article) {
|
|
||||||
articleDao.update(article)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun subscribe(feed: Feed, articles: List<Article>) {
|
|
||||||
feedDao.insert(feed)
|
|
||||||
articleDao.insertList(articles.map {
|
|
||||||
it.copy(feedId = feed.id)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun addGroup(name: String): String {
|
|
||||||
context.currentAccountId.let { accountId ->
|
|
||||||
return accountId.spacerDollar(UUID.randomUUID().toString()).also {
|
|
||||||
groupDao.insert(
|
|
||||||
Group(
|
|
||||||
id = it,
|
|
||||||
name = name,
|
|
||||||
accountId = accountId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun sync(coroutineWorker: CoroutineWorker): ListenableWorker.Result =
|
|
||||||
supervisorScope {
|
|
||||||
coroutineWorker.setProgress(setIsSyncing(true))
|
|
||||||
val preTime = System.currentTimeMillis()
|
|
||||||
val accountId = context.currentAccountId
|
|
||||||
feedDao.queryAll(accountId)
|
|
||||||
.chunked(16)
|
|
||||||
.forEach {
|
|
||||||
it.map { feed -> async { syncFeed(feed) } }
|
|
||||||
.awaitAll()
|
|
||||||
.forEach {
|
|
||||||
if (it.feed.isNotification) {
|
|
||||||
notificationHelper.notify(it.apply {
|
|
||||||
articles = articleDao.insertListIfNotExist(it.articles)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
articleDao.insertListIfNotExist(it.articles)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.i("RlOG", "onCompletion: ${System.currentTimeMillis() - preTime}")
|
|
||||||
accountDao.queryById(accountId)?.let { account ->
|
|
||||||
accountDao.update(account.apply { updateAt = Date() })
|
|
||||||
}
|
|
||||||
coroutineWorker.setProgress(setIsSyncing(false))
|
|
||||||
ListenableWorker.Result.success()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun markAsRead(
|
|
||||||
groupId: String?,
|
|
||||||
feedId: String?,
|
|
||||||
articleId: String?,
|
|
||||||
before: Date?,
|
|
||||||
isUnread: Boolean,
|
|
||||||
) {
|
|
||||||
val accountId = context.currentAccountId
|
|
||||||
when {
|
|
||||||
groupId != null -> {
|
|
||||||
articleDao.markAllAsReadByGroupId(
|
|
||||||
accountId = accountId,
|
|
||||||
groupId = groupId,
|
|
||||||
isUnread = isUnread,
|
|
||||||
before = before ?: Date(Long.MAX_VALUE)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
feedId != null -> {
|
|
||||||
articleDao.markAllAsReadByFeedId(
|
|
||||||
accountId = accountId,
|
|
||||||
feedId = feedId,
|
|
||||||
isUnread = isUnread,
|
|
||||||
before = before ?: Date(Long.MAX_VALUE)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
articleId != null -> {
|
|
||||||
articleDao.markAsReadByArticleId(accountId, articleId, isUnread)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
articleDao.markAllAsRead(accountId, isUnread, before ?: Date(Long.MAX_VALUE))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun syncFeed(feed: Feed): FeedWithArticle {
|
|
||||||
val latest = articleDao.queryLatestByFeedId(context.currentAccountId, feed.id)
|
|
||||||
val articles = rssHelper.queryRssXml(feed, latest?.link)
|
|
||||||
// try {
|
|
||||||
// if (feed.icon == null && !articles.isNullOrEmpty()) {
|
|
||||||
// rssHelper.queryRssIcon(feedDao, feed, articles.first().link)
|
|
||||||
// }
|
|
||||||
// } catch (e: Exception) {
|
|
||||||
// Log.e("RLog", "queryRssIcon[${feed.name}]: ${e.message}")
|
|
||||||
// return FeedWithArticle(
|
|
||||||
// feed = feed.apply { isNotification = false },
|
|
||||||
// articles = listOf()
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
return FeedWithArticle(
|
|
||||||
feed = feed.apply { isNotification = feed.isNotification && articles.isNotEmpty() },
|
|
||||||
articles = articles
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -11,7 +11,7 @@ import me.ash.reader.data.dao.AccountDao
|
|||||||
import me.ash.reader.data.dao.FeedDao
|
import me.ash.reader.data.dao.FeedDao
|
||||||
import me.ash.reader.data.dao.GroupDao
|
import me.ash.reader.data.dao.GroupDao
|
||||||
import me.ash.reader.data.model.feed.Feed
|
import me.ash.reader.data.model.feed.Feed
|
||||||
import me.ash.reader.data.source.OpmlLocalDataSource
|
import me.ash.reader.data.source.OPMLDataSource
|
||||||
import me.ash.reader.ui.ext.currentAccountId
|
import me.ash.reader.ui.ext.currentAccountId
|
||||||
import me.ash.reader.ui.ext.getDefaultGroupId
|
import me.ash.reader.ui.ext.getDefaultGroupId
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
@ -28,7 +28,7 @@ class OpmlRepository @Inject constructor(
|
|||||||
private val feedDao: FeedDao,
|
private val feedDao: FeedDao,
|
||||||
private val accountDao: AccountDao,
|
private val accountDao: AccountDao,
|
||||||
private val rssRepository: RssRepository,
|
private val rssRepository: RssRepository,
|
||||||
private val opmlLocalDataSource: OpmlLocalDataSource,
|
private val OPMLDataSource: OPMLDataSource,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -40,7 +40,7 @@ class OpmlRepository @Inject constructor(
|
|||||||
suspend fun saveToDatabase(inputStream: InputStream) {
|
suspend fun saveToDatabase(inputStream: InputStream) {
|
||||||
val defaultGroup = groupDao.queryById(getDefaultGroupId(context.currentAccountId))!!
|
val defaultGroup = groupDao.queryById(getDefaultGroupId(context.currentAccountId))!!
|
||||||
val groupWithFeedList =
|
val groupWithFeedList =
|
||||||
opmlLocalDataSource.parseFileInputStream(inputStream, defaultGroup)
|
OPMLDataSource.parseFileInputStream(inputStream, defaultGroup)
|
||||||
groupWithFeedList.forEach { groupWithFeed ->
|
groupWithFeedList.forEach { groupWithFeed ->
|
||||||
if (groupWithFeed.group != defaultGroup) {
|
if (groupWithFeed.group != defaultGroup) {
|
||||||
groupDao.insert(groupWithFeed.group)
|
groupDao.insert(groupWithFeed.group)
|
||||||
|
@ -25,7 +25,7 @@ import javax.inject.Inject
|
|||||||
class RYRepository @Inject constructor(
|
class RYRepository @Inject constructor(
|
||||||
@ApplicationContext
|
@ApplicationContext
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val RYNetworkDataSource: RYNetworkDataSource,
|
private val networkDataSource: RYNetworkDataSource,
|
||||||
@IODispatcher
|
@IODispatcher
|
||||||
private val ioDispatcher: CoroutineDispatcher,
|
private val ioDispatcher: CoroutineDispatcher,
|
||||||
@MainDispatcher
|
@MainDispatcher
|
||||||
@ -34,7 +34,7 @@ class RYRepository @Inject constructor(
|
|||||||
|
|
||||||
suspend fun checkUpdate(showToast: Boolean = true): Boolean? = withContext(ioDispatcher) {
|
suspend fun checkUpdate(showToast: Boolean = true): Boolean? = withContext(ioDispatcher) {
|
||||||
try {
|
try {
|
||||||
val response = RYNetworkDataSource.getReleaseLatest(context.getString(R.string.update_link))
|
val response = networkDataSource.getReleaseLatest(context.getString(R.string.update_link))
|
||||||
when {
|
when {
|
||||||
response.code() == 403 -> {
|
response.code() == 403 -> {
|
||||||
withContext(mainDispatcher) {
|
withContext(mainDispatcher) {
|
||||||
@ -86,7 +86,7 @@ class RYRepository @Inject constructor(
|
|||||||
withContext(ioDispatcher) {
|
withContext(ioDispatcher) {
|
||||||
Log.i("RLog", "downloadFile start: $url")
|
Log.i("RLog", "downloadFile start: $url")
|
||||||
try {
|
try {
|
||||||
return@withContext RYNetworkDataSource.downloadFile(url)
|
return@withContext networkDataSource.downloadFile(url)
|
||||||
.downloadToFileWithProgress(context.getLatestApk())
|
.downloadToFileWithProgress(context.getLatestApk())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
|
@ -130,7 +130,7 @@ class RssHelper @Inject constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun findImg(rawDescription: String): String? {
|
fun findImg(rawDescription: String): String? {
|
||||||
// From: https://gitlab.com/spacecowboy/Feeder
|
// From: https://gitlab.com/spacecowboy/Feeder
|
||||||
// Using negative lookahead to skip data: urls, being inline base64
|
// Using negative lookahead to skip data: urls, being inline base64
|
||||||
// And capturing original quote to use as ending quote
|
// And capturing original quote to use as ending quote
|
||||||
|
@ -10,7 +10,7 @@ class RssRepository @Inject constructor(
|
|||||||
@ApplicationContext
|
@ApplicationContext
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val localRssRepository: LocalRssRepository,
|
private val localRssRepository: LocalRssRepository,
|
||||||
// private val feverRssRepository: FeverRssRepository,
|
private val feverRssRepository: FeverRssRepository,
|
||||||
// private val googleReaderRssRepository: GoogleReaderRssRepository,
|
// private val googleReaderRssRepository: GoogleReaderRssRepository,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@ -18,9 +18,10 @@ class RssRepository @Inject constructor(
|
|||||||
|
|
||||||
fun get(accountTypeId: Int) = when (accountTypeId) {
|
fun get(accountTypeId: Int) = when (accountTypeId) {
|
||||||
AccountType.Local.id -> localRssRepository
|
AccountType.Local.id -> localRssRepository
|
||||||
// Account.Type.LOCAL -> feverRssRepository
|
AccountType.Fever.id -> feverRssRepository
|
||||||
// Account.Type.FEVER -> feverRssRepository
|
AccountType.GoogleReader.id -> localRssRepository
|
||||||
// Account.Type.GOOGLE_READER -> googleReaderRssRepository
|
AccountType.Inoreader.id -> localRssRepository
|
||||||
|
AccountType.Feedly.id -> localRssRepository
|
||||||
else -> localRssRepository
|
else -> localRssRepository
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ class SyncWorker @AssistedInject constructor(
|
|||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
Log.i("RLog", "doWork: ")
|
Log.i("RLog", "doWork: ")
|
||||||
rssRepository.get().sync(this@SyncWorker).also {
|
rssRepository.get().sync(this@SyncWorker).also {
|
||||||
rssRepository.get().keepArchivedArticles()
|
rssRepository.get().clearKeepArchivedArticles()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,60 +0,0 @@
|
|||||||
package me.ash.reader.data.source
|
|
||||||
|
|
||||||
import okhttp3.RequestBody
|
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
|
||||||
import retrofit2.Call
|
|
||||||
import retrofit2.Retrofit
|
|
||||||
import retrofit2.converter.gson.GsonConverterFactory
|
|
||||||
import retrofit2.http.Multipart
|
|
||||||
import retrofit2.http.POST
|
|
||||||
import retrofit2.http.Part
|
|
||||||
import retrofit2.http.Query
|
|
||||||
|
|
||||||
interface FeverApiDataSource {
|
|
||||||
|
|
||||||
@Multipart
|
|
||||||
@POST("fever.php/?api=&feeds=")
|
|
||||||
fun feeds(@Part("api_key") apiKey: RequestBody? = "1352b707f828a6f502db3768fa8d7151".toRequestBody()): Call<FeverApiDto.Feed>
|
|
||||||
|
|
||||||
@Multipart
|
|
||||||
@POST("fever.php/?api=&groups=")
|
|
||||||
fun groups(@Part("api_key") apiKey: RequestBody? = "1352b707f828a6f502db3768fa8d7151".toRequestBody()): Call<FeverApiDto.Groups>
|
|
||||||
|
|
||||||
@Multipart
|
|
||||||
@POST("fever.php/?api=&items=")
|
|
||||||
fun itemsBySince(
|
|
||||||
@Query("since_id") since: Long,
|
|
||||||
@Part("api_key") apiKey: RequestBody? = "1352b707f828a6f502db3768fa8d7151".toRequestBody(),
|
|
||||||
): Call<FeverApiDto.Items>
|
|
||||||
|
|
||||||
@Multipart
|
|
||||||
@POST("fever.php/?api=&unread_item_ids=")
|
|
||||||
fun itemsByUnread(@Part("api_key") apiKey: RequestBody? = "1352b707f828a6f502db3768fa8d7151".toRequestBody()): Call<FeverApiDto.ItemsByUnread>
|
|
||||||
|
|
||||||
@Multipart
|
|
||||||
@POST("fever.php/?api=&saved_item_ids=")
|
|
||||||
fun itemsByStarred(@Part("api_key") apiKey: RequestBody? = "1352b707f828a6f502db3768fa8d7151".toRequestBody()): Call<FeverApiDto.ItemsByStarred>
|
|
||||||
|
|
||||||
@Multipart
|
|
||||||
@POST("fever.php/?api=&items=")
|
|
||||||
fun itemsByIds(
|
|
||||||
@Query("with_ids") ids: String,
|
|
||||||
@Part("api_key") apiKey: RequestBody? = "1352b707f828a6f502db3768fa8d7151".toRequestBody(),
|
|
||||||
): Call<FeverApiDto.Items>
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private var instance: FeverApiDataSource? = null
|
|
||||||
|
|
||||||
fun getInstance(): FeverApiDataSource {
|
|
||||||
return instance ?: synchronized(this) {
|
|
||||||
instance ?: Retrofit.Builder()
|
|
||||||
.baseUrl("http://10.0.2.2/api/")
|
|
||||||
.addConverterFactory(GsonConverterFactory.create())
|
|
||||||
.build().create(FeverApiDataSource::class.java).also {
|
|
||||||
instance = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,45 +0,0 @@
|
|||||||
package me.ash.reader.data.source
|
|
||||||
|
|
||||||
import retrofit2.Call
|
|
||||||
import retrofit2.Retrofit
|
|
||||||
import retrofit2.converter.gson.GsonConverterFactory
|
|
||||||
import retrofit2.http.Headers
|
|
||||||
import retrofit2.http.POST
|
|
||||||
|
|
||||||
interface GoogleReaderApiDataSource {
|
|
||||||
|
|
||||||
@POST("accounts/ClientLogin")
|
|
||||||
fun login(Email: String, Passwd: String): Call<String>
|
|
||||||
|
|
||||||
@Headers("Authorization:GoogleLogin auth=${"Ashinch/678592edaf9145e5b27068d9dc3afc41494ba54e"}")
|
|
||||||
@POST("reader/api/0/subscription/list?output=json")
|
|
||||||
fun subscriptionList(): Call<GoogleReaderApiDto.SubscriptionList>
|
|
||||||
|
|
||||||
@Headers("Authorization:GoogleLogin auth=${"Ashinch/678592edaf9145e5b27068d9dc3afc41494ba54e"}")
|
|
||||||
@POST("reader/api/0/unread-count?output=json")
|
|
||||||
fun unreadCount(): Call<GoogleReaderApiDto.UnreadCount>
|
|
||||||
|
|
||||||
@Headers("Authorization:GoogleLogin auth=${"Ashinch/678592edaf9145e5b27068d9dc3afc41494ba54e"}")
|
|
||||||
@POST("reader/api/0/tag/list?output=json")
|
|
||||||
fun tagList(): Call<GoogleReaderApiDto.TagList>
|
|
||||||
|
|
||||||
@Headers("Authorization:GoogleLogin auth=${"Ashinch/678592edaf9145e5b27068d9dc3afc41494ba54e"}")
|
|
||||||
@POST("reader/api/0/stream/contents/reading-list")
|
|
||||||
fun readingList(): Call<GoogleReaderApiDto.ReadingList>
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private var instance: GoogleReaderApiDataSource? = null
|
|
||||||
|
|
||||||
fun getInstance(): GoogleReaderApiDataSource {
|
|
||||||
return instance ?: synchronized(this) {
|
|
||||||
instance ?: Retrofit.Builder()
|
|
||||||
.baseUrl("http://10.0.2.2/api/greader.php/")
|
|
||||||
.addConverterFactory(GsonConverterFactory.create())
|
|
||||||
.build().create(GoogleReaderApiDataSource::class.java).also {
|
|
||||||
instance = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -15,7 +15,7 @@ import java.io.InputStream
|
|||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class OpmlLocalDataSource @Inject constructor(
|
class OPMLDataSource @Inject constructor(
|
||||||
@ApplicationContext
|
@ApplicationContext
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
@IODispatcher
|
@IODispatcher
|
@ -9,6 +9,7 @@ import me.ash.reader.data.dao.ArticleDao
|
|||||||
import me.ash.reader.data.dao.FeedDao
|
import me.ash.reader.data.dao.FeedDao
|
||||||
import me.ash.reader.data.dao.GroupDao
|
import me.ash.reader.data.dao.GroupDao
|
||||||
import me.ash.reader.data.model.account.*
|
import me.ash.reader.data.model.account.*
|
||||||
|
import me.ash.reader.data.model.account.security.DESUtils
|
||||||
import me.ash.reader.data.model.article.Article
|
import me.ash.reader.data.model.article.Article
|
||||||
import me.ash.reader.data.model.feed.Feed
|
import me.ash.reader.data.model.feed.Feed
|
||||||
import me.ash.reader.data.model.group.Group
|
import me.ash.reader.data.model.group.Group
|
||||||
@ -18,7 +19,7 @@ import java.util.*
|
|||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
entities = [Account::class, Feed::class, Article::class, Group::class],
|
entities = [Account::class, Feed::class, Article::class, Group::class],
|
||||||
version = 3
|
version = 4
|
||||||
)
|
)
|
||||||
@TypeConverters(
|
@TypeConverters(
|
||||||
RYDatabase.DateConverters::class,
|
RYDatabase.DateConverters::class,
|
||||||
@ -71,6 +72,7 @@ abstract class RYDatabase : RoomDatabase() {
|
|||||||
val allMigrations = arrayOf(
|
val allMigrations = arrayOf(
|
||||||
MIGRATION_1_2,
|
MIGRATION_1_2,
|
||||||
MIGRATION_2_3,
|
MIGRATION_2_3,
|
||||||
|
MIGRATION_3_4,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Suppress("ClassName")
|
@Suppress("ClassName")
|
||||||
@ -126,3 +128,15 @@ object MIGRATION_2_3 : Migration(2, 3) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("ClassName")
|
||||||
|
object MIGRATION_3_4 : Migration(3, 4) {
|
||||||
|
|
||||||
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
|
database.execSQL(
|
||||||
|
"""
|
||||||
|
ALTER TABLE account ADD COLUMN securityKey TEXT DEFAULT '${DESUtils.empty}'
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -26,6 +26,7 @@ fun ClipboardTextField(
|
|||||||
singleLine: Boolean = true,
|
singleLine: Boolean = true,
|
||||||
onValueChange: (String) -> Unit = {},
|
onValueChange: (String) -> Unit = {},
|
||||||
placeholder: String = "",
|
placeholder: String = "",
|
||||||
|
isPassword: Boolean = false,
|
||||||
errorText: String = "",
|
errorText: String = "",
|
||||||
imeAction: ImeAction = ImeAction.Done,
|
imeAction: ImeAction = ImeAction.Done,
|
||||||
focusManager: FocusManager? = null,
|
focusManager: FocusManager? = null,
|
||||||
@ -39,6 +40,7 @@ fun ClipboardTextField(
|
|||||||
singleLine = singleLine,
|
singleLine = singleLine,
|
||||||
onValueChange = onValueChange,
|
onValueChange = onValueChange,
|
||||||
placeholder = placeholder,
|
placeholder = placeholder,
|
||||||
|
isPassword = isPassword,
|
||||||
errorMessage = errorText,
|
errorMessage = errorText,
|
||||||
keyboardActions = KeyboardActions(
|
keyboardActions = KeyboardActions(
|
||||||
onDone = if (imeAction == ImeAction.Done)
|
onDone = if (imeAction == ImeAction.Done)
|
||||||
|
@ -5,16 +5,18 @@ import androidx.compose.foundation.text.KeyboardOptions
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.rounded.Close
|
import androidx.compose.material.icons.rounded.Close
|
||||||
import androidx.compose.material.icons.rounded.ContentPaste
|
import androidx.compose.material.icons.rounded.ContentPaste
|
||||||
|
import androidx.compose.material.icons.rounded.Visibility
|
||||||
|
import androidx.compose.material.icons.rounded.VisibilityOff
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalClipboardManager
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import me.ash.reader.R
|
import me.ash.reader.R
|
||||||
|
|
||||||
@ -25,6 +27,8 @@ fun RYOutlineTextField(
|
|||||||
label: String = "",
|
label: String = "",
|
||||||
singleLine: Boolean = true,
|
singleLine: Boolean = true,
|
||||||
onValueChange: (String) -> Unit,
|
onValueChange: (String) -> Unit,
|
||||||
|
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||||
|
isPassword: Boolean = false,
|
||||||
placeholder: String = "",
|
placeholder: String = "",
|
||||||
errorMessage: String = "",
|
errorMessage: String = "",
|
||||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||||
@ -32,6 +36,7 @@ fun RYOutlineTextField(
|
|||||||
) {
|
) {
|
||||||
val clipboardManager = LocalClipboardManager.current
|
val clipboardManager = LocalClipboardManager.current
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
var showPassword by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
delay(100) // ???
|
delay(100) // ???
|
||||||
@ -52,6 +57,7 @@ fun RYOutlineTextField(
|
|||||||
onValueChange = {
|
onValueChange = {
|
||||||
if (!readOnly) onValueChange(it)
|
if (!readOnly) onValueChange(it)
|
||||||
},
|
},
|
||||||
|
visualTransformation = if (isPassword && !showPassword) PasswordVisualTransformation() else visualTransformation,
|
||||||
placeholder = {
|
placeholder = {
|
||||||
Text(
|
Text(
|
||||||
text = placeholder,
|
text = placeholder,
|
||||||
@ -64,11 +70,18 @@ fun RYOutlineTextField(
|
|||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
if (value.isNotEmpty()) {
|
if (value.isNotEmpty()) {
|
||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
if (!readOnly) onValueChange("")
|
if (isPassword) {
|
||||||
|
showPassword = !showPassword
|
||||||
|
} else if (!readOnly) {
|
||||||
|
onValueChange("")
|
||||||
|
}
|
||||||
}) {
|
}) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Rounded.Close,
|
imageVector = if (isPassword) {
|
||||||
contentDescription = stringResource(R.string.clear),
|
if (showPassword) Icons.Rounded.Visibility
|
||||||
|
else Icons.Rounded.VisibilityOff
|
||||||
|
} else Icons.Rounded.Close,
|
||||||
|
contentDescription = if (isPassword) stringResource(R.string.password) else stringResource(R.string.clear),
|
||||||
tint = MaterialTheme.colorScheme.outline.copy(alpha = 0.5f),
|
tint = MaterialTheme.colorScheme.outline.copy(alpha = 0.5f),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -5,16 +5,18 @@ import androidx.compose.foundation.text.KeyboardOptions
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.rounded.Close
|
import androidx.compose.material.icons.rounded.Close
|
||||||
import androidx.compose.material.icons.rounded.ContentPaste
|
import androidx.compose.material.icons.rounded.ContentPaste
|
||||||
|
import androidx.compose.material.icons.rounded.Visibility
|
||||||
|
import androidx.compose.material.icons.rounded.VisibilityOff
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalClipboardManager
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import me.ash.reader.R
|
import me.ash.reader.R
|
||||||
|
|
||||||
@ -25,6 +27,8 @@ fun RYTextField(
|
|||||||
label: String = "",
|
label: String = "",
|
||||||
singleLine: Boolean = true,
|
singleLine: Boolean = true,
|
||||||
onValueChange: (String) -> Unit,
|
onValueChange: (String) -> Unit,
|
||||||
|
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||||
|
isPassword: Boolean = false,
|
||||||
placeholder: String = "",
|
placeholder: String = "",
|
||||||
errorMessage: String = "",
|
errorMessage: String = "",
|
||||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||||
@ -32,6 +36,7 @@ fun RYTextField(
|
|||||||
) {
|
) {
|
||||||
val clipboardManager = LocalClipboardManager.current
|
val clipboardManager = LocalClipboardManager.current
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
var showPassword by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
delay(100) // ???
|
delay(100) // ???
|
||||||
@ -52,6 +57,7 @@ fun RYTextField(
|
|||||||
onValueChange = {
|
onValueChange = {
|
||||||
if (!readOnly) onValueChange(it)
|
if (!readOnly) onValueChange(it)
|
||||||
},
|
},
|
||||||
|
visualTransformation = if (isPassword && !showPassword) PasswordVisualTransformation() else visualTransformation,
|
||||||
placeholder = {
|
placeholder = {
|
||||||
Text(
|
Text(
|
||||||
text = placeholder,
|
text = placeholder,
|
||||||
@ -64,11 +70,18 @@ fun RYTextField(
|
|||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
if (value.isNotEmpty()) {
|
if (value.isNotEmpty()) {
|
||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
if (!readOnly) onValueChange("")
|
if (isPassword) {
|
||||||
|
showPassword = !showPassword
|
||||||
|
} else if (!readOnly) {
|
||||||
|
onValueChange("")
|
||||||
|
}
|
||||||
}) {
|
}) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Rounded.Close,
|
imageVector = if (isPassword) {
|
||||||
contentDescription = stringResource(R.string.clear),
|
if (showPassword) Icons.Rounded.Visibility
|
||||||
|
else Icons.Rounded.VisibilityOff
|
||||||
|
} else Icons.Rounded.Close,
|
||||||
|
contentDescription = if (isPassword) stringResource(R.string.password) else stringResource(R.string.clear),
|
||||||
tint = MaterialTheme.colorScheme.outline.copy(alpha = 0.5f),
|
tint = MaterialTheme.colorScheme.outline.copy(alpha = 0.5f),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,7 @@ fun TextFieldDialog(
|
|||||||
icon: ImageVector? = null,
|
icon: ImageVector? = null,
|
||||||
value: String = "",
|
value: String = "",
|
||||||
placeholder: String = "",
|
placeholder: String = "",
|
||||||
|
isPassword: Boolean = false,
|
||||||
errorText: String = "",
|
errorText: String = "",
|
||||||
dismissText: String = stringResource(R.string.cancel),
|
dismissText: String = stringResource(R.string.cancel),
|
||||||
confirmText: String = stringResource(R.string.confirm),
|
confirmText: String = stringResource(R.string.confirm),
|
||||||
@ -59,6 +60,7 @@ fun TextFieldDialog(
|
|||||||
singleLine = singleLine,
|
singleLine = singleLine,
|
||||||
onValueChange = onValueChange,
|
onValueChange = onValueChange,
|
||||||
placeholder = placeholder,
|
placeholder = placeholder,
|
||||||
|
isPassword = isPassword,
|
||||||
errorText = errorText,
|
errorText = errorText,
|
||||||
imeAction = imeAction,
|
imeAction = imeAction,
|
||||||
focusManager = focusManager,
|
focusManager = focusManager,
|
||||||
|
@ -2,6 +2,8 @@ package me.ash.reader.ui.ext
|
|||||||
|
|
||||||
fun Int.spacerDollar(str: Any): String = "$this$$str"
|
fun Int.spacerDollar(str: Any): String = "$this$$str"
|
||||||
|
|
||||||
|
fun String.dollarLast(): String = split("$").last()
|
||||||
|
|
||||||
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
|
fun Int.toBoolean(): Boolean = this != 0
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
package me.ash.reader.ui.ext
|
package me.ash.reader.ui.ext
|
||||||
|
|
||||||
|
import android.util.Base64
|
||||||
|
import java.math.BigInteger
|
||||||
|
import java.security.MessageDigest
|
||||||
|
|
||||||
fun String.formatUrl(): String {
|
fun String.formatUrl(): String {
|
||||||
if (this.startsWith("//")) {
|
if (this.startsWith("//")) {
|
||||||
return "https:$this"
|
return "https:$this"
|
||||||
@ -16,3 +20,15 @@ fun String.isUrl(): Boolean {
|
|||||||
val regex = Regex("(https?|ftp|file)://[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]")
|
val regex = Regex("(https?|ftp|file)://[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]")
|
||||||
return regex.matches(this)
|
return regex.matches(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun String.mask(): String = run {
|
||||||
|
"\u2022".repeat(length)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun String.encodeBase64(): String = Base64.encodeToString(toByteArray(), Base64.DEFAULT)
|
||||||
|
|
||||||
|
fun String.decodeBase64(): String = String(Base64.decode(this, Base64.DEFAULT))
|
||||||
|
|
||||||
|
fun String.md5(): String =
|
||||||
|
BigInteger(1, MessageDigest.getInstance("MD5").digest(toByteArray()))
|
||||||
|
.toString(16).padStart(32, '0')
|
||||||
|
@ -38,6 +38,7 @@ fun FeedOptionView(
|
|||||||
selectedAllowNotificationPreset: Boolean = false,
|
selectedAllowNotificationPreset: Boolean = false,
|
||||||
selectedParseFullContentPreset: Boolean = false,
|
selectedParseFullContentPreset: Boolean = false,
|
||||||
isMoveToGroup: Boolean = false,
|
isMoveToGroup: Boolean = false,
|
||||||
|
showGroup:Boolean = true,
|
||||||
showUnsubscribe: Boolean = false,
|
showUnsubscribe: Boolean = false,
|
||||||
selectedGroupId: String = "",
|
selectedGroupId: String = "",
|
||||||
allowNotificationPresetOnClick: () -> Unit = {},
|
allowNotificationPresetOnClick: () -> Unit = {},
|
||||||
@ -72,15 +73,18 @@ fun FeedOptionView(
|
|||||||
clearArticlesOnClick = clearArticlesOnClick,
|
clearArticlesOnClick = clearArticlesOnClick,
|
||||||
unsubscribeOnClick = unsubscribeOnClick,
|
unsubscribeOnClick = unsubscribeOnClick,
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(26.dp))
|
|
||||||
|
|
||||||
AddToGroup(
|
if (showGroup) {
|
||||||
isMoveToGroup = isMoveToGroup,
|
Spacer(modifier = Modifier.height(26.dp))
|
||||||
groups = groups,
|
|
||||||
selectedGroupId = selectedGroupId,
|
AddToGroup(
|
||||||
onGroupClick = onGroupClick,
|
isMoveToGroup = isMoveToGroup,
|
||||||
onAddNewGroup = onAddNewGroup,
|
groups = groups,
|
||||||
)
|
selectedGroupId = selectedGroupId,
|
||||||
|
onGroupClick = onGroupClick,
|
||||||
|
onAddNewGroup = onAddNewGroup,
|
||||||
|
)
|
||||||
|
}
|
||||||
Spacer(modifier = Modifier.height(6.dp))
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,9 +22,9 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
|
import androidx.work.WorkInfo
|
||||||
import me.ash.reader.R
|
import me.ash.reader.R
|
||||||
import me.ash.reader.data.model.preference.*
|
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.FilterBar
|
||||||
import me.ash.reader.ui.component.base.*
|
import me.ash.reader.ui.component.base.*
|
||||||
import me.ash.reader.ui.ext.*
|
import me.ash.reader.ui.ext.*
|
||||||
@ -79,7 +79,7 @@ fun FeedsPage(
|
|||||||
val owner = LocalLifecycleOwner.current
|
val owner = LocalLifecycleOwner.current
|
||||||
var isSyncing by remember { mutableStateOf(false) }
|
var isSyncing by remember { mutableStateOf(false) }
|
||||||
homeViewModel.syncWorkLiveData.observe(owner) {
|
homeViewModel.syncWorkLiveData.observe(owner) {
|
||||||
it?.let { isSyncing = it.any { it.progress.getIsSyncing() } }
|
it?.let { isSyncing = it.any { it.state == WorkInfo.State.RUNNING } }
|
||||||
}
|
}
|
||||||
|
|
||||||
val infiniteTransition = rememberInfiniteTransition()
|
val infiniteTransition = rememberInfiniteTransition()
|
||||||
@ -148,12 +148,14 @@ fun FeedsPage(
|
|||||||
) {
|
) {
|
||||||
if (!isSyncing) homeViewModel.sync()
|
if (!isSyncing) homeViewModel.sync()
|
||||||
}
|
}
|
||||||
FeedbackIconButton(
|
if (subscribeViewModel.rssRepository.get().subscribe) {
|
||||||
imageVector = Icons.Rounded.Add,
|
FeedbackIconButton(
|
||||||
contentDescription = stringResource(R.string.subscribe),
|
imageVector = Icons.Rounded.Add,
|
||||||
tint = MaterialTheme.colorScheme.onSurface,
|
contentDescription = stringResource(R.string.subscribe),
|
||||||
) {
|
tint = MaterialTheme.colorScheme.onSurface,
|
||||||
subscribeViewModel.showDrawer()
|
) {
|
||||||
|
subscribeViewModel.showDrawer()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
content = {
|
content = {
|
||||||
|
@ -86,6 +86,7 @@ fun FeedOptionDrawer(
|
|||||||
?: false,
|
?: false,
|
||||||
selectedParseFullContentPreset = feedOptionUiState.feed?.isFullContent ?: false,
|
selectedParseFullContentPreset = feedOptionUiState.feed?.isFullContent ?: false,
|
||||||
isMoveToGroup = true,
|
isMoveToGroup = true,
|
||||||
|
showGroup = feedOptionViewModel.rssRepository.get().move,
|
||||||
showUnsubscribe = true,
|
showUnsubscribe = true,
|
||||||
selectedGroupId = feedOptionUiState.feed?.groupId ?: "",
|
selectedGroupId = feedOptionUiState.feed?.groupId ?: "",
|
||||||
allowNotificationPresetOnClick = {
|
allowNotificationPresetOnClick = {
|
||||||
|
@ -24,7 +24,7 @@ import javax.inject.Inject
|
|||||||
@OptIn(ExperimentalMaterialApi::class)
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class FeedOptionViewModel @Inject constructor(
|
class FeedOptionViewModel @Inject constructor(
|
||||||
private val rssRepository: RssRepository,
|
val rssRepository: RssRepository,
|
||||||
@MainDispatcher
|
@MainDispatcher
|
||||||
private val mainDispatcher: CoroutineDispatcher,
|
private val mainDispatcher: CoroutineDispatcher,
|
||||||
@IODispatcher
|
@IODispatcher
|
||||||
|
@ -22,7 +22,7 @@ import javax.inject.Inject
|
|||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class SubscribeViewModel @Inject constructor(
|
class SubscribeViewModel @Inject constructor(
|
||||||
private val opmlRepository: OpmlRepository,
|
private val opmlRepository: OpmlRepository,
|
||||||
private val rssRepository: RssRepository,
|
val rssRepository: RssRepository,
|
||||||
private val rssHelper: RssHelper,
|
private val rssHelper: RssHelper,
|
||||||
private val stringsRepository: StringsRepository,
|
private val stringsRepository: StringsRepository,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
@ -20,6 +20,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import androidx.paging.compose.collectAsLazyPagingItems
|
import androidx.paging.compose.collectAsLazyPagingItems
|
||||||
|
import androidx.work.WorkInfo
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import me.ash.reader.R
|
import me.ash.reader.R
|
||||||
@ -69,7 +70,7 @@ fun FlowPage(
|
|||||||
val owner = LocalLifecycleOwner.current
|
val owner = LocalLifecycleOwner.current
|
||||||
var isSyncing by remember { mutableStateOf(false) }
|
var isSyncing by remember { mutableStateOf(false) }
|
||||||
homeViewModel.syncWorkLiveData.observe(owner) {
|
homeViewModel.syncWorkLiveData.observe(owner) {
|
||||||
it?.let { isSyncing = it.any { it.progress.getIsSyncing() } }
|
it?.let { isSyncing = it.any { it.state == WorkInfo.State.RUNNING } }
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(onSearch) {
|
LaunchedEffect(onSearch) {
|
||||||
|
@ -116,8 +116,9 @@ class ReadingViewModel @Inject constructor(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
rssRepository.get().updateArticleInfo(
|
rssRepository.get().markAsStarred(
|
||||||
articleWithFeed.article.copy(isStarred = isStarred)
|
articleId = articleWithFeed.article.id,
|
||||||
|
isStarred = isStarred,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,7 @@ import me.ash.reader.ui.ext.collectAsStateValue
|
|||||||
import me.ash.reader.ui.ext.showToast
|
import me.ash.reader.ui.ext.showToast
|
||||||
import me.ash.reader.ui.ext.showToastLong
|
import me.ash.reader.ui.ext.showToastLong
|
||||||
import me.ash.reader.ui.page.settings.SettingItem
|
import me.ash.reader.ui.page.settings.SettingItem
|
||||||
|
import me.ash.reader.ui.page.settings.accounts.connection.AccountConnection
|
||||||
import me.ash.reader.ui.theme.palette.onLight
|
import me.ash.reader.ui.theme.palette.onLight
|
||||||
|
|
||||||
@OptIn(ExperimentalAnimationApi::class)
|
@OptIn(ExperimentalAnimationApi::class)
|
||||||
@ -36,21 +37,17 @@ fun AccountDetailsPage(
|
|||||||
navController: NavHostController = rememberAnimatedNavController(),
|
navController: NavHostController = rememberAnimatedNavController(),
|
||||||
viewModel: AccountViewModel = hiltViewModel(),
|
viewModel: AccountViewModel = hiltViewModel(),
|
||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
val uiState = viewModel.accountUiState.collectAsStateValue()
|
val uiState = viewModel.accountUiState.collectAsStateValue()
|
||||||
val context = LocalContext.current
|
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)
|
val selectedAccount = uiState.selectedAccount.collectAsStateValue(initial = null)
|
||||||
|
|
||||||
var nameValue by remember { mutableStateOf(selectedAccount?.name) }
|
var nameValue by remember { mutableStateOf(selectedAccount?.name) }
|
||||||
var nameDialogVisible by remember { mutableStateOf(false) }
|
var nameDialogVisible by remember { mutableStateOf(false) }
|
||||||
var blockListValue by remember { mutableStateOf(SyncBlockListPreference.toString(syncBlockList)) }
|
var blockListValue by remember {
|
||||||
|
mutableStateOf(SyncBlockListPreference.toString(selectedAccount?.syncBlockList
|
||||||
|
?: SyncBlockListPreference.default))
|
||||||
|
}
|
||||||
var blockListDialogVisible by remember { mutableStateOf(false) }
|
var blockListDialogVisible by remember { mutableStateOf(false) }
|
||||||
var syncIntervalDialogVisible by remember { mutableStateOf(false) }
|
var syncIntervalDialogVisible by remember { mutableStateOf(false) }
|
||||||
var keepArchivedDialogVisible by remember { mutableStateOf(false) }
|
var keepArchivedDialogVisible by remember { mutableStateOf(false) }
|
||||||
@ -107,6 +104,11 @@ fun AccountDetailsPage(
|
|||||||
) {}
|
) {}
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
}
|
}
|
||||||
|
if (selectedAccount != null) {
|
||||||
|
item {
|
||||||
|
AccountConnection(account = selectedAccount)
|
||||||
|
}
|
||||||
|
}
|
||||||
item {
|
item {
|
||||||
Subtitle(
|
Subtitle(
|
||||||
modifier = Modifier.padding(horizontal = 24.dp),
|
modifier = Modifier.padding(horizontal = 24.dp),
|
||||||
@ -114,20 +116,20 @@ fun AccountDetailsPage(
|
|||||||
)
|
)
|
||||||
SettingItem(
|
SettingItem(
|
||||||
title = stringResource(R.string.sync_interval),
|
title = stringResource(R.string.sync_interval),
|
||||||
desc = syncInterval.toDesc(context),
|
desc = selectedAccount?.syncInterval?.toDesc(context),
|
||||||
onClick = { syncIntervalDialogVisible = true },
|
onClick = { syncIntervalDialogVisible = true },
|
||||||
) {}
|
) {}
|
||||||
SettingItem(
|
SettingItem(
|
||||||
title = stringResource(R.string.sync_once_on_start),
|
title = stringResource(R.string.sync_once_on_start),
|
||||||
onClick = {
|
onClick = {
|
||||||
selectedAccount?.id?.let {
|
selectedAccount?.id?.let {
|
||||||
(!syncOnStart).put(it, viewModel)
|
(!selectedAccount.syncOnStart).put(it, viewModel)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
RYSwitch(activated = syncOnStart.value) {
|
RYSwitch(activated = selectedAccount?.syncOnStart?.value == true) {
|
||||||
selectedAccount?.id?.let {
|
selectedAccount?.id?.let {
|
||||||
(!syncOnStart).put(it, viewModel)
|
(!selectedAccount.syncOnStart).put(it, viewModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -135,13 +137,13 @@ fun AccountDetailsPage(
|
|||||||
title = stringResource(R.string.only_on_wifi),
|
title = stringResource(R.string.only_on_wifi),
|
||||||
onClick = {
|
onClick = {
|
||||||
selectedAccount?.id?.let {
|
selectedAccount?.id?.let {
|
||||||
(!syncOnlyOnWiFi).put(it, viewModel)
|
(!selectedAccount.syncOnlyOnWiFi).put(it, viewModel)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
RYSwitch(activated = syncOnlyOnWiFi.value) {
|
RYSwitch(activated = selectedAccount?.syncOnlyOnWiFi?.value == true) {
|
||||||
selectedAccount?.id?.let {
|
selectedAccount?.id?.let {
|
||||||
(!syncOnlyOnWiFi).put(it, viewModel)
|
(!selectedAccount.syncOnlyOnWiFi).put(it, viewModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -149,19 +151,19 @@ fun AccountDetailsPage(
|
|||||||
title = stringResource(R.string.only_when_charging),
|
title = stringResource(R.string.only_when_charging),
|
||||||
onClick = {
|
onClick = {
|
||||||
selectedAccount?.id?.let {
|
selectedAccount?.id?.let {
|
||||||
(!syncOnlyWhenCharging).put(it, viewModel)
|
(!selectedAccount.syncOnlyWhenCharging).put(it, viewModel)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
RYSwitch(activated = syncOnlyWhenCharging.value) {
|
RYSwitch(activated = selectedAccount?.syncOnlyWhenCharging?.value == true) {
|
||||||
selectedAccount?.id?.let {
|
selectedAccount?.id?.let {
|
||||||
(!syncOnlyWhenCharging).put(it, viewModel)
|
(!selectedAccount.syncOnlyWhenCharging).put(it, viewModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SettingItem(
|
SettingItem(
|
||||||
title = stringResource(R.string.keep_archived_articles),
|
title = stringResource(R.string.keep_archived_articles),
|
||||||
desc = keepArchived.toDesc(context),
|
desc = selectedAccount?.keepArchived?.toDesc(context),
|
||||||
onClick = { keepArchivedDialogVisible = true },
|
onClick = { keepArchivedDialogVisible = true },
|
||||||
) {}
|
) {}
|
||||||
// SettingItem(
|
// SettingItem(
|
||||||
@ -229,7 +231,7 @@ fun AccountDetailsPage(
|
|||||||
options = SyncIntervalPreference.values.map {
|
options = SyncIntervalPreference.values.map {
|
||||||
RadioDialogOption(
|
RadioDialogOption(
|
||||||
text = it.toDesc(context),
|
text = it.toDesc(context),
|
||||||
selected = it == syncInterval,
|
selected = it == selectedAccount?.syncInterval,
|
||||||
) {
|
) {
|
||||||
selectedAccount?.id?.let { accountId ->
|
selectedAccount?.id?.let { accountId ->
|
||||||
it.put(accountId, viewModel)
|
it.put(accountId, viewModel)
|
||||||
@ -246,7 +248,7 @@ fun AccountDetailsPage(
|
|||||||
options = KeepArchivedPreference.values.map {
|
options = KeepArchivedPreference.values.map {
|
||||||
RadioDialogOption(
|
RadioDialogOption(
|
||||||
text = it.toDesc(context),
|
text = it.toDesc(context),
|
||||||
selected = it == keepArchived,
|
selected = it == selectedAccount?.keepArchived,
|
||||||
) {
|
) {
|
||||||
selectedAccount?.id?.let { accountId ->
|
selectedAccount?.id?.let { accountId ->
|
||||||
it.put(accountId, viewModel)
|
it.put(accountId, viewModel)
|
||||||
@ -271,7 +273,7 @@ fun AccountDetailsPage(
|
|||||||
},
|
},
|
||||||
onConfirm = {
|
onConfirm = {
|
||||||
selectedAccount?.id?.let {
|
selectedAccount?.id?.let {
|
||||||
SyncBlockListPreference.put(it, viewModel, syncBlockList)
|
SyncBlockListPreference.put(it, viewModel, selectedAccount.syncBlockList)
|
||||||
blockListDialogVisible = false
|
blockListDialogVisible = false
|
||||||
context.showToast(selectedAccount.syncBlockList.toString())
|
context.showToast(selectedAccount.syncBlockList.toString())
|
||||||
}
|
}
|
||||||
|
@ -90,11 +90,22 @@ class AccountViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addAccount(account: Account, callback: (Account) -> Unit = {}) {
|
fun addAccount(account: Account, callback: (Account?) -> Unit = {}) {
|
||||||
viewModelScope.launch(ioDispatcher) {
|
viewModelScope.launch(ioDispatcher) {
|
||||||
val addAccount = accountRepository.addAccount(account)
|
val addAccount = accountRepository.addAccount(account)
|
||||||
withContext(mainDispatcher) {
|
try {
|
||||||
callback(addAccount)
|
if (rssRepository.get(addAccount.type.id).validCredentials()) {
|
||||||
|
withContext(mainDispatcher) {
|
||||||
|
callback(addAccount)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw Exception("Unauthorized")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
accountRepository.delete(account.id!!)
|
||||||
|
withContext(mainDispatcher) {
|
||||||
|
callback(null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,7 @@ import me.ash.reader.ui.component.base.FeedbackIconButton
|
|||||||
import me.ash.reader.ui.component.base.RYScaffold
|
import me.ash.reader.ui.component.base.RYScaffold
|
||||||
import me.ash.reader.ui.component.base.Subtitle
|
import me.ash.reader.ui.component.base.Subtitle
|
||||||
import me.ash.reader.ui.page.settings.SettingItem
|
import me.ash.reader.ui.page.settings.SettingItem
|
||||||
|
import me.ash.reader.ui.page.settings.accounts.addition.AddFeverAccountDialog
|
||||||
import me.ash.reader.ui.page.settings.accounts.addition.AddLocalAccountDialog
|
import me.ash.reader.ui.page.settings.accounts.addition.AddLocalAccountDialog
|
||||||
import me.ash.reader.ui.page.settings.accounts.addition.AdditionViewModel
|
import me.ash.reader.ui.page.settings.accounts.addition.AdditionViewModel
|
||||||
import me.ash.reader.ui.theme.palette.onLight
|
import me.ash.reader.ui.theme.palette.onLight
|
||||||
@ -113,20 +114,11 @@ fun AddAccountsPage(
|
|||||||
},
|
},
|
||||||
) {}
|
) {}
|
||||||
SettingItem(
|
SettingItem(
|
||||||
enable = false,
|
|
||||||
title = stringResource(R.string.fever),
|
title = stringResource(R.string.fever),
|
||||||
desc = stringResource(R.string.fever_desc),
|
desc = stringResource(R.string.fever_desc),
|
||||||
iconPainter = painterResource(id = R.drawable.ic_fever),
|
iconPainter = painterResource(id = R.drawable.ic_fever),
|
||||||
onClick = {
|
onClick = {
|
||||||
// viewModel.addAccount(Account(
|
additionViewModel.showAddFeverAccountDialog()
|
||||||
// type = AccountType.Fever,
|
|
||||||
// name = "name",
|
|
||||||
// )) {
|
|
||||||
// navController.popBackStack()
|
|
||||||
// navController.navigate("${RouteName.ACCOUNT_DETAILS}/${it.id}") {
|
|
||||||
// launchSingleTop = true
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
},
|
},
|
||||||
) {}
|
) {}
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
@ -140,6 +132,7 @@ fun AddAccountsPage(
|
|||||||
)
|
)
|
||||||
|
|
||||||
AddLocalAccountDialog(navController)
|
AddLocalAccountDialog(navController)
|
||||||
|
AddFeverAccountDialog(navController)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
|
@ -0,0 +1,145 @@
|
|||||||
|
package me.ash.reader.ui.page.settings.accounts.addition
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.rounded.RssFeed
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
|
import me.ash.reader.R
|
||||||
|
import me.ash.reader.data.model.account.Account
|
||||||
|
import me.ash.reader.data.model.account.AccountType
|
||||||
|
import me.ash.reader.data.model.account.security.FeverSecurityKey
|
||||||
|
import me.ash.reader.ui.component.base.RYDialog
|
||||||
|
import me.ash.reader.ui.component.base.RYOutlineTextField
|
||||||
|
import me.ash.reader.ui.ext.collectAsStateValue
|
||||||
|
import me.ash.reader.ui.ext.showToast
|
||||||
|
import me.ash.reader.ui.page.common.RouteName
|
||||||
|
import me.ash.reader.ui.page.settings.accounts.AccountViewModel
|
||||||
|
|
||||||
|
@OptIn(androidx.compose.ui.ExperimentalComposeUiApi::class)
|
||||||
|
@Composable
|
||||||
|
fun AddFeverAccountDialog(
|
||||||
|
navController: NavHostController,
|
||||||
|
viewModel: AdditionViewModel = hiltViewModel(),
|
||||||
|
accountViewModel: AccountViewModel = hiltViewModel(),
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val focusManager = LocalFocusManager.current
|
||||||
|
val uiState = viewModel.additionUiState.collectAsStateValue()
|
||||||
|
|
||||||
|
var serverUrl by remember { mutableStateOf("") }
|
||||||
|
var username by remember { mutableStateOf("") }
|
||||||
|
var password by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
RYDialog(
|
||||||
|
modifier = Modifier.padding(horizontal = 44.dp),
|
||||||
|
visible = uiState.addFeverAccountDialogVisible,
|
||||||
|
properties = DialogProperties(usePlatformDefaultWidth = false),
|
||||||
|
onDismissRequest = {
|
||||||
|
focusManager.clearFocus()
|
||||||
|
// subscribeViewModel.hideDrawer()
|
||||||
|
},
|
||||||
|
icon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Rounded.RssFeed,
|
||||||
|
contentDescription = stringResource(R.string.fever),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.fever),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.verticalScroll(rememberScrollState())
|
||||||
|
) {
|
||||||
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
RYOutlineTextField(
|
||||||
|
value = serverUrl,
|
||||||
|
onValueChange = { serverUrl = it },
|
||||||
|
label = stringResource(R.string.server_url),
|
||||||
|
placeholder = "https://demo.freshrss.org/api/fever.php",
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri),
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
RYOutlineTextField(
|
||||||
|
value = username,
|
||||||
|
onValueChange = { username = it },
|
||||||
|
label = stringResource(R.string.username),
|
||||||
|
placeholder = "demo",
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email),
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
RYOutlineTextField(
|
||||||
|
value = password,
|
||||||
|
onValueChange = { password = it },
|
||||||
|
isPassword = true,
|
||||||
|
label = stringResource(R.string.password),
|
||||||
|
placeholder = "demodemo",
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
enabled = serverUrl.isNotBlank() && username.isNotEmpty() && password.isNotEmpty(),
|
||||||
|
onClick = {
|
||||||
|
focusManager.clearFocus()
|
||||||
|
accountViewModel.addAccount(Account(
|
||||||
|
type = AccountType.Fever,
|
||||||
|
name = context.getString(R.string.fever),
|
||||||
|
securityKey = FeverSecurityKey(
|
||||||
|
serverUrl = serverUrl,
|
||||||
|
username = username,
|
||||||
|
password = password,
|
||||||
|
).toString(),
|
||||||
|
)) {
|
||||||
|
if (it == null) {
|
||||||
|
context.showToast("Not valid credentials")
|
||||||
|
} else {
|
||||||
|
viewModel.hideAddFeverAccountDialog()
|
||||||
|
navController.popBackStack()
|
||||||
|
navController.navigate("${RouteName.ACCOUNT_DETAILS}/${it.id}") {
|
||||||
|
launchSingleTop = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.add))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
focusManager.clearFocus()
|
||||||
|
viewModel.hideAddFeverAccountDialog()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.cancel))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
@ -1,6 +1,8 @@
|
|||||||
package me.ash.reader.ui.page.settings.accounts.addition
|
package me.ash.reader.ui.page.settings.accounts.addition
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
@ -27,6 +29,7 @@ import me.ash.reader.data.model.account.AccountType
|
|||||||
import me.ash.reader.ui.component.base.RYDialog
|
import me.ash.reader.ui.component.base.RYDialog
|
||||||
import me.ash.reader.ui.component.base.RYOutlineTextField
|
import me.ash.reader.ui.component.base.RYOutlineTextField
|
||||||
import me.ash.reader.ui.ext.collectAsStateValue
|
import me.ash.reader.ui.ext.collectAsStateValue
|
||||||
|
import me.ash.reader.ui.ext.showToast
|
||||||
import me.ash.reader.ui.page.common.RouteName
|
import me.ash.reader.ui.page.common.RouteName
|
||||||
import me.ash.reader.ui.page.settings.accounts.AccountViewModel
|
import me.ash.reader.ui.page.settings.accounts.AccountViewModel
|
||||||
|
|
||||||
@ -68,12 +71,14 @@ fun AddLocalAccountDialog(
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier.verticalScroll(rememberScrollState())
|
modifier = Modifier.verticalScroll(rememberScrollState())
|
||||||
) {
|
) {
|
||||||
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
RYOutlineTextField(
|
RYOutlineTextField(
|
||||||
value = name,
|
value = name,
|
||||||
onValueChange = { name = it },
|
onValueChange = { name = it },
|
||||||
label = stringResource(R.string.name),
|
label = stringResource(R.string.name),
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email)
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email),
|
||||||
)
|
)
|
||||||
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
@ -81,15 +86,18 @@ fun AddLocalAccountDialog(
|
|||||||
enabled = name.isNotBlank(),
|
enabled = name.isNotBlank(),
|
||||||
onClick = {
|
onClick = {
|
||||||
focusManager.clearFocus()
|
focusManager.clearFocus()
|
||||||
viewModel.hideAddLocalAccountDialog()
|
|
||||||
|
|
||||||
accountViewModel.addAccount(Account(
|
accountViewModel.addAccount(Account(
|
||||||
type = AccountType.Local,
|
type = AccountType.Local,
|
||||||
name = name,
|
name = name,
|
||||||
)) {
|
)) {
|
||||||
navController.popBackStack()
|
if (it == null) {
|
||||||
navController.navigate("${RouteName.ACCOUNT_DETAILS}/${it.id}") {
|
context.showToast("Not valid credentials")
|
||||||
launchSingleTop = true
|
} else {
|
||||||
|
viewModel.hideAddLocalAccountDialog()
|
||||||
|
navController.popBackStack()
|
||||||
|
navController.navigate("${RouteName.ACCOUNT_DETAILS}/${it.id}") {
|
||||||
|
launchSingleTop = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,36 @@
|
|||||||
|
package me.ash.reader.ui.page.settings.accounts.connection
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyItemScope
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import me.ash.reader.R
|
||||||
|
import me.ash.reader.data.model.account.Account
|
||||||
|
import me.ash.reader.data.model.account.AccountType
|
||||||
|
import me.ash.reader.ui.component.base.Subtitle
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LazyItemScope.AccountConnection(
|
||||||
|
account: Account,
|
||||||
|
) {
|
||||||
|
if (account.type.id != AccountType.Local.id) {
|
||||||
|
Subtitle(
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp),
|
||||||
|
text = stringResource(R.string.connection),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
when (account.type.id) {
|
||||||
|
AccountType.Fever.id -> FeverConnection(account)
|
||||||
|
AccountType.GoogleReader.id -> {}
|
||||||
|
AccountType.FreshRSS.id -> {}
|
||||||
|
AccountType.Feedly.id -> {}
|
||||||
|
AccountType.Inoreader.id -> {}
|
||||||
|
}
|
||||||
|
if (account.type.id != AccountType.Local.id) {
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,132 @@
|
|||||||
|
package me.ash.reader.ui.page.settings.accounts.connection
|
||||||
|
|
||||||
|
import androidx.compose.foundation.lazy.LazyItemScope
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import me.ash.reader.R
|
||||||
|
import me.ash.reader.data.model.account.Account
|
||||||
|
import me.ash.reader.data.model.account.security.FeverSecurityKey
|
||||||
|
import me.ash.reader.ui.component.base.TextFieldDialog
|
||||||
|
import me.ash.reader.ui.ext.mask
|
||||||
|
import me.ash.reader.ui.page.settings.SettingItem
|
||||||
|
import me.ash.reader.ui.page.settings.accounts.AccountViewModel
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LazyItemScope.FeverConnection(
|
||||||
|
account: Account,
|
||||||
|
viewModel: AccountViewModel = hiltViewModel(),
|
||||||
|
) {
|
||||||
|
val securityKey by remember {
|
||||||
|
derivedStateOf { FeverSecurityKey(account.securityKey) }
|
||||||
|
}
|
||||||
|
|
||||||
|
var passwordMask by remember { mutableStateOf(securityKey.password?.mask()) }
|
||||||
|
|
||||||
|
var serverUrlValue by remember { mutableStateOf(securityKey.serverUrl) }
|
||||||
|
var usernameValue by remember { mutableStateOf(securityKey.username) }
|
||||||
|
var passwordValue by remember { mutableStateOf(securityKey.password) }
|
||||||
|
|
||||||
|
var serverUrlDialogVisible by remember { mutableStateOf(false) }
|
||||||
|
var usernameDialogVisible by remember { mutableStateOf(false) }
|
||||||
|
var passwordDialogVisible by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
LaunchedEffect(securityKey.password) {
|
||||||
|
passwordMask = securityKey.password?.mask()
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingItem(
|
||||||
|
title = stringResource(R.string.server_url),
|
||||||
|
desc = securityKey.serverUrl ?: "",
|
||||||
|
onClick = {
|
||||||
|
serverUrlDialogVisible = true
|
||||||
|
},
|
||||||
|
) {}
|
||||||
|
SettingItem(
|
||||||
|
title = stringResource(R.string.username),
|
||||||
|
desc = securityKey.username ?: "",
|
||||||
|
onClick = {
|
||||||
|
usernameDialogVisible = true
|
||||||
|
},
|
||||||
|
) {}
|
||||||
|
SettingItem(
|
||||||
|
title = stringResource(R.string.password),
|
||||||
|
desc = passwordMask,
|
||||||
|
onClick = {
|
||||||
|
passwordDialogVisible = true
|
||||||
|
},
|
||||||
|
) {}
|
||||||
|
|
||||||
|
TextFieldDialog(
|
||||||
|
visible = serverUrlDialogVisible,
|
||||||
|
title = stringResource(R.string.server_url),
|
||||||
|
value = serverUrlValue ?: "",
|
||||||
|
placeholder = "https://demo.freshrss.org/api/fever.php",
|
||||||
|
onValueChange = {
|
||||||
|
serverUrlValue = it
|
||||||
|
},
|
||||||
|
onDismissRequest = {
|
||||||
|
serverUrlDialogVisible = false
|
||||||
|
},
|
||||||
|
onConfirm = {
|
||||||
|
if (securityKey.serverUrl?.isNotBlank() == true) {
|
||||||
|
securityKey.serverUrl = serverUrlValue
|
||||||
|
save(account, viewModel, securityKey)
|
||||||
|
serverUrlDialogVisible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
TextFieldDialog(
|
||||||
|
visible = usernameDialogVisible,
|
||||||
|
title = stringResource(R.string.username),
|
||||||
|
value = usernameValue ?: "",
|
||||||
|
placeholder = "demo",
|
||||||
|
onValueChange = {
|
||||||
|
usernameValue = it
|
||||||
|
},
|
||||||
|
onDismissRequest = {
|
||||||
|
usernameDialogVisible = false
|
||||||
|
},
|
||||||
|
onConfirm = {
|
||||||
|
if (securityKey.username?.isNotEmpty() == true) {
|
||||||
|
securityKey.username = usernameValue
|
||||||
|
save(account, viewModel, securityKey)
|
||||||
|
usernameDialogVisible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
TextFieldDialog(
|
||||||
|
visible = passwordDialogVisible,
|
||||||
|
title = stringResource(R.string.password),
|
||||||
|
value = passwordValue ?: "",
|
||||||
|
placeholder = "demodemo",
|
||||||
|
isPassword = true,
|
||||||
|
onValueChange = {
|
||||||
|
passwordValue = it
|
||||||
|
},
|
||||||
|
onDismissRequest = {
|
||||||
|
passwordDialogVisible = false
|
||||||
|
},
|
||||||
|
onConfirm = {
|
||||||
|
if (securityKey.password?.isNotEmpty() == true) {
|
||||||
|
securityKey.password = passwordValue
|
||||||
|
save(account, viewModel, securityKey)
|
||||||
|
passwordDialogVisible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun save(
|
||||||
|
account: Account,
|
||||||
|
viewModel: AccountViewModel,
|
||||||
|
securityKey: FeverSecurityKey,
|
||||||
|
) {
|
||||||
|
account.id?.let {
|
||||||
|
viewModel.update(it) {
|
||||||
|
this.securityKey = securityKey.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -23,6 +23,7 @@
|
|||||||
<string name="deny">拒绝</string>
|
<string name="deny">拒绝</string>
|
||||||
<string name="defaults">默认</string>
|
<string name="defaults">默认</string>
|
||||||
<string name="unknown">未知</string>
|
<string name="unknown">未知</string>
|
||||||
|
<string name="empty">空</string>
|
||||||
<string name="back">返回</string>
|
<string name="back">返回</string>
|
||||||
<string name="go_to">转到</string>
|
<string name="go_to">转到</string>
|
||||||
<string name="settings">设置</string>
|
<string name="settings">设置</string>
|
||||||
@ -346,4 +347,8 @@
|
|||||||
<string name="switch_account">切换账户</string>
|
<string name="switch_account">切换账户</string>
|
||||||
<string name="add">添加</string>
|
<string name="add">添加</string>
|
||||||
<string name="accounts_tips">在订阅源页面中点击帐户名称来切换它们。</string>
|
<string name="accounts_tips">在订阅源页面中点击帐户名称来切换它们。</string>
|
||||||
|
<string name="server_url">服务器地址</string>
|
||||||
|
<string name="username">用户名</string>
|
||||||
|
<string name="password">密码</string>
|
||||||
|
<string name="connection">连接信息</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -27,6 +27,7 @@
|
|||||||
<string name="deny">Deny</string>
|
<string name="deny">Deny</string>
|
||||||
<string name="defaults">Default</string>
|
<string name="defaults">Default</string>
|
||||||
<string name="unknown">Unknown</string>
|
<string name="unknown">Unknown</string>
|
||||||
|
<string name="empty">Empty</string>
|
||||||
<string name="back">Back</string>
|
<string name="back">Back</string>
|
||||||
<string name="go_to">Go to</string>
|
<string name="go_to">Go to</string>
|
||||||
<string name="settings">Settings</string>
|
<string name="settings">Settings</string>
|
||||||
@ -37,7 +38,7 @@
|
|||||||
<string name="already_subscribed">Already subscribed</string>
|
<string name="already_subscribed">Already subscribed</string>
|
||||||
<string name="clear">Clear</string>
|
<string name="clear">Clear</string>
|
||||||
<string name="paste">Paste</string>
|
<string name="paste">Paste</string>
|
||||||
<string name="feed_or_site_url">Feed or URL</string>
|
<string name="feed_or_site_url">Feed or Site URL</string>
|
||||||
<string name="import_from_opml">Import from OPML</string>
|
<string name="import_from_opml">Import from OPML</string>
|
||||||
<string name="preset">Preset</string>
|
<string name="preset">Preset</string>
|
||||||
<string name="selected">Selected</string>
|
<string name="selected">Selected</string>
|
||||||
@ -382,7 +383,11 @@
|
|||||||
<string name="clear_all_articles_toast">All articles from this account have been cleared</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="delete_account_toast">This account has been deleted</string>
|
||||||
<string name="synchronous_tips">Restart is required for changes to take effect.</string>
|
<string name="synchronous_tips">Restart is required for changes to take effect.</string>
|
||||||
<string name="switch_account">Switch Account</string>
|
<string name="switch_account">Switch</string>
|
||||||
<string name="add">Add</string>
|
<string name="add">Add</string>
|
||||||
<string name="accounts_tips">Click the account name on the feed page to switch them.</string>
|
<string name="accounts_tips">Click the account name on the feed page to switch them.</string>
|
||||||
|
<string name="server_url">Server URL</string>
|
||||||
|
<string name="username">Username</string>
|
||||||
|
<string name="password">Password</string>
|
||||||
|
<string name="connection">Connection</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user