Merge branch 'main' into patch-1
This commit is contained in:
commit
dbf2f7f194
|
@ -116,7 +116,7 @@ dependencies {
|
||||||
implementation "org.conscrypt:conscrypt-android:2.5.2"
|
implementation "org.conscrypt:conscrypt-android:2.5.2"
|
||||||
|
|
||||||
// https://square.github.io/okhttp/changelogs/changelog/
|
// https://square.github.io/okhttp/changelogs/changelog/
|
||||||
implementation "com.squareup.okhttp3:okhttp:5.0.0-alpha.6"
|
implementation "com.squareup.okhttp3:okhttp:$okhttp"
|
||||||
implementation "com.squareup.retrofit2:retrofit:$retrofit2"
|
implementation "com.squareup.retrofit2:retrofit:$retrofit2"
|
||||||
implementation "com.squareup.retrofit2:converter-gson:$retrofit2"
|
implementation "com.squareup.retrofit2:converter-gson:$retrofit2"
|
||||||
|
|
||||||
|
@ -166,9 +166,9 @@ dependencies {
|
||||||
// https://developer.android.com/jetpack/androidx/releases/compose-material
|
// https://developer.android.com/jetpack/androidx/releases/compose-material
|
||||||
implementation "androidx.compose.material:material:$compose"
|
implementation "androidx.compose.material:material:$compose"
|
||||||
implementation "androidx.compose.material:material-icons-extended:$compose"
|
implementation "androidx.compose.material:material-icons-extended:$compose"
|
||||||
|
debugImplementation "androidx.compose.ui:ui-tooling:$compose"
|
||||||
implementation "androidx.compose.ui:ui-tooling-preview:$compose"
|
implementation "androidx.compose.ui:ui-tooling-preview:$compose"
|
||||||
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose"
|
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose"
|
||||||
debugImplementation "androidx.compose.ui:ui-tooling:$compose"
|
|
||||||
|
|
||||||
// hilt
|
// hilt
|
||||||
implementation "androidx.hilt:hilt-work:1.0.0"
|
implementation "androidx.hilt:hilt-work:1.0.0"
|
||||||
|
|
321
app/schemas/me.ash.reader.data.source.RYDatabase/2.json
Normal file
321
app/schemas/me.ash.reader.data.source.RYDatabase/2.json
Normal file
|
@ -0,0 +1,321 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 2,
|
||||||
|
"identityHash": "98462c2e9c32394054102313366e7262",
|
||||||
|
"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)",
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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 DEFAULT false, `isFullContent` INTEGER NOT NULL DEFAULT false, 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,
|
||||||
|
"defaultValue": "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isFullContent",
|
||||||
|
"columnName": "isFullContent",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "false"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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 DEFAULT true, `isStarred` INTEGER NOT NULL DEFAULT false, `isReadLater` INTEGER NOT NULL DEFAULT false, 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,
|
||||||
|
"defaultValue": "true"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isStarred",
|
||||||
|
"columnName": "isStarred",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isReadLater",
|
||||||
|
"columnName": "isReadLater",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "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, '98462c2e9c32394054102313366e7262')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,11 +5,8 @@
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
|
||||||
<!-- Disable automatic updates in F-Droid -->
|
|
||||||
<!-- <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />-->
|
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".App"
|
android:name=".RYApp"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/read_you"
|
android:label="@string/read_you"
|
||||||
|
|
|
@ -12,20 +12,17 @@ import kotlinx.coroutines.launch
|
||||||
import me.ash.reader.data.module.ApplicationScope
|
import me.ash.reader.data.module.ApplicationScope
|
||||||
import me.ash.reader.data.module.DispatcherDefault
|
import me.ash.reader.data.module.DispatcherDefault
|
||||||
import me.ash.reader.data.repository.*
|
import me.ash.reader.data.repository.*
|
||||||
import me.ash.reader.data.source.AppNetworkDataSource
|
|
||||||
import me.ash.reader.data.source.OpmlLocalDataSource
|
import me.ash.reader.data.source.OpmlLocalDataSource
|
||||||
import me.ash.reader.data.source.ReaderDatabase
|
import me.ash.reader.data.source.RYDatabase
|
||||||
|
import me.ash.reader.data.source.RYNetworkDataSource
|
||||||
import me.ash.reader.ui.ext.*
|
import me.ash.reader.ui.ext.*
|
||||||
import okhttp3.Cache
|
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import org.conscrypt.Conscrypt
|
import org.conscrypt.Conscrypt
|
||||||
import java.io.File
|
|
||||||
import java.security.Security
|
import java.security.Security
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltAndroidApp
|
@HiltAndroidApp
|
||||||
class App : Application(), Configuration.Provider {
|
class RYApp : Application(), Configuration.Provider {
|
||||||
init {
|
init {
|
||||||
// From: https://gitlab.com/spacecowboy/Feeder
|
// From: https://gitlab.com/spacecowboy/Feeder
|
||||||
// Install Conscrypt to handle TLSv1.3 pre Android10
|
// Install Conscrypt to handle TLSv1.3 pre Android10
|
||||||
|
@ -33,7 +30,7 @@ class App : Application(), Configuration.Provider {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var readerDatabase: ReaderDatabase
|
lateinit var RYDatabase: RYDatabase
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var workerFactory: HiltWorkerFactory
|
lateinit var workerFactory: HiltWorkerFactory
|
||||||
|
@ -42,7 +39,7 @@ class App : Application(), Configuration.Provider {
|
||||||
lateinit var workManager: WorkManager
|
lateinit var workManager: WorkManager
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var appNetworkDataSource: AppNetworkDataSource
|
lateinit var RYNetworkDataSource: RYNetworkDataSource
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var opmlLocalDataSource: OpmlLocalDataSource
|
lateinit var opmlLocalDataSource: OpmlLocalDataSource
|
||||||
|
@ -51,7 +48,10 @@ class App : Application(), Configuration.Provider {
|
||||||
lateinit var rssHelper: RssHelper
|
lateinit var rssHelper: RssHelper
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var appRepository: AppRepository
|
lateinit var notificationHelper: NotificationHelper
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var ryRepository: RYRepository
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var stringsRepository: StringsRepository
|
lateinit var stringsRepository: StringsRepository
|
||||||
|
@ -62,9 +62,6 @@ class App : Application(), Configuration.Provider {
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var localRssRepository: LocalRssRepository
|
lateinit var localRssRepository: LocalRssRepository
|
||||||
|
|
||||||
// @Inject
|
|
||||||
// lateinit var feverRssRepository: FeverRssRepository
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var opmlRepository: OpmlRepository
|
lateinit var opmlRepository: OpmlRepository
|
||||||
|
|
||||||
|
@ -79,6 +76,9 @@ class App : Application(), Configuration.Provider {
|
||||||
@DispatcherDefault
|
@DispatcherDefault
|
||||||
lateinit var dispatcherDefault: CoroutineDispatcher
|
lateinit var dispatcherDefault: CoroutineDispatcher
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var okHttpClient: OkHttpClient
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var imageLoader: ImageLoader
|
lateinit var imageLoader: ImageLoader
|
||||||
|
|
||||||
|
@ -89,7 +89,7 @@ class App : Application(), Configuration.Provider {
|
||||||
applicationScope.launch(dispatcherDefault) {
|
applicationScope.launch(dispatcherDefault) {
|
||||||
accountInit()
|
accountInit()
|
||||||
workerInit()
|
workerInit()
|
||||||
if (BuildConfig.FLAVOR != "fdroid") {
|
if (notFdroid) {
|
||||||
checkUpdate()
|
checkUpdate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -116,7 +116,7 @@ class App : Application(), Configuration.Provider {
|
||||||
it.del()
|
it.del()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
appRepository.checkUpdate(showToast = false)
|
ryRepository.checkUpdate(showToast = false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getWorkManagerConfiguration(): Configuration =
|
override fun getWorkManagerConfiguration(): Configuration =
|
||||||
|
@ -125,28 +125,3 @@ class App : Application(), Configuration.Provider {
|
||||||
.setMinimumLoggingLevel(android.util.Log.DEBUG)
|
.setMinimumLoggingLevel(android.util.Log.DEBUG)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cachingHttpClient(
|
|
||||||
cacheDirectory: File? = null,
|
|
||||||
cacheSize: Long = 10L * 1024L * 1024L,
|
|
||||||
trustAllCerts: Boolean = true,
|
|
||||||
connectTimeoutSecs: Long = 30L,
|
|
||||||
readTimeoutSecs: Long = 30L
|
|
||||||
): OkHttpClient {
|
|
||||||
val builder: OkHttpClient.Builder = OkHttpClient.Builder()
|
|
||||||
|
|
||||||
if (cacheDirectory != null) {
|
|
||||||
builder.cache(Cache(cacheDirectory, cacheSize))
|
|
||||||
}
|
|
||||||
|
|
||||||
builder
|
|
||||||
.connectTimeout(connectTimeoutSecs, TimeUnit.SECONDS)
|
|
||||||
.readTimeout(readTimeoutSecs, TimeUnit.SECONDS)
|
|
||||||
.followRedirects(true)
|
|
||||||
|
|
||||||
// if (trustAllCerts) {
|
|
||||||
// builder.trustAllCerts()
|
|
||||||
// }
|
|
||||||
|
|
||||||
return builder.build()
|
|
||||||
}
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
package me.ash.reader.data.constant
|
||||||
|
|
||||||
|
object ElevationTokens {
|
||||||
|
const val Level0 = 0
|
||||||
|
const val Level1 = 1
|
||||||
|
const val Level2 = 3
|
||||||
|
const val Level3 = 6
|
||||||
|
const val Level4 = 8
|
||||||
|
const val Level5 = 12
|
||||||
|
}
|
|
@ -18,7 +18,7 @@ interface AccountDao {
|
||||||
WHERE id = :id
|
WHERE id = :id
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
suspend fun queryById(id: Int): Account
|
suspend fun queryById(id: Int): Account?
|
||||||
|
|
||||||
@Insert
|
@Insert
|
||||||
suspend fun insert(account: Account): Long
|
suspend fun insert(account: Account): Long
|
||||||
|
|
|
@ -5,7 +5,7 @@ import androidx.room.*
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import me.ash.reader.data.entity.Article
|
import me.ash.reader.data.entity.Article
|
||||||
import me.ash.reader.data.entity.ArticleWithFeed
|
import me.ash.reader.data.entity.ArticleWithFeed
|
||||||
import me.ash.reader.data.entity.ImportantCount
|
import me.ash.reader.data.model.ImportantCount
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
package me.ash.reader.data.entity
|
package me.ash.reader.data.entity
|
||||||
|
|
||||||
import androidx.room.ColumnInfo
|
import androidx.room.*
|
||||||
import androidx.room.Entity
|
|
||||||
import androidx.room.ForeignKey
|
|
||||||
import androidx.room.PrimaryKey
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@Entity(
|
@Entity(
|
||||||
|
@ -18,13 +15,13 @@ import java.util.*
|
||||||
)
|
)
|
||||||
data class Article(
|
data class Article(
|
||||||
@PrimaryKey
|
@PrimaryKey
|
||||||
val id: String,
|
var id: String,
|
||||||
@ColumnInfo
|
@ColumnInfo
|
||||||
val date: Date,
|
var date: Date,
|
||||||
@ColumnInfo
|
@ColumnInfo
|
||||||
val title: String,
|
var title: String,
|
||||||
@ColumnInfo
|
@ColumnInfo
|
||||||
val author: String? = null,
|
var author: String? = null,
|
||||||
@ColumnInfo
|
@ColumnInfo
|
||||||
var rawDescription: String,
|
var rawDescription: String,
|
||||||
@ColumnInfo
|
@ColumnInfo
|
||||||
|
@ -32,17 +29,20 @@ data class Article(
|
||||||
@ColumnInfo
|
@ColumnInfo
|
||||||
var fullContent: String? = null,
|
var fullContent: String? = null,
|
||||||
@ColumnInfo
|
@ColumnInfo
|
||||||
val img: String? = null,
|
var img: String? = null,
|
||||||
@ColumnInfo
|
@ColumnInfo
|
||||||
val link: String,
|
var link: String,
|
||||||
@ColumnInfo(index = true)
|
@ColumnInfo(index = true)
|
||||||
val feedId: String,
|
var feedId: String,
|
||||||
@ColumnInfo(index = true)
|
@ColumnInfo(index = true)
|
||||||
val accountId: Int,
|
var accountId: Int,
|
||||||
@ColumnInfo(defaultValue = "true")
|
@ColumnInfo(defaultValue = "true")
|
||||||
var isUnread: Boolean = true,
|
var isUnread: Boolean = true,
|
||||||
@ColumnInfo(defaultValue = "false")
|
@ColumnInfo(defaultValue = "false")
|
||||||
var isStarred: Boolean = false,
|
var isStarred: Boolean = false,
|
||||||
@ColumnInfo(defaultValue = "false")
|
@ColumnInfo(defaultValue = "false")
|
||||||
var isReadLater: Boolean = false,
|
var isReadLater: Boolean = false,
|
||||||
)
|
) {
|
||||||
|
@Ignore
|
||||||
|
var dateString: String? = null
|
||||||
|
}
|
|
@ -5,7 +5,7 @@ import androidx.room.Relation
|
||||||
|
|
||||||
data class ArticleWithFeed(
|
data class ArticleWithFeed(
|
||||||
@Embedded
|
@Embedded
|
||||||
val article: Article,
|
var article: Article,
|
||||||
@Relation(parentColumn = "feedId", entityColumn = "id")
|
@Relation(parentColumn = "feedId", entityColumn = "id")
|
||||||
val feed: Feed,
|
var feed: Feed,
|
||||||
)
|
)
|
||||||
|
|
|
@ -14,17 +14,17 @@ import androidx.room.*
|
||||||
)
|
)
|
||||||
data class Feed(
|
data class Feed(
|
||||||
@PrimaryKey
|
@PrimaryKey
|
||||||
val id: String,
|
var id: String,
|
||||||
@ColumnInfo
|
@ColumnInfo
|
||||||
val name: String,
|
var name: String,
|
||||||
@ColumnInfo
|
@ColumnInfo
|
||||||
var icon: String? = null,
|
var icon: String? = null,
|
||||||
@ColumnInfo
|
@ColumnInfo
|
||||||
val url: String,
|
var url: String,
|
||||||
@ColumnInfo(index = true)
|
@ColumnInfo(index = true)
|
||||||
var groupId: String,
|
var groupId: String,
|
||||||
@ColumnInfo(index = true)
|
@ColumnInfo(index = true)
|
||||||
val accountId: Int,
|
var accountId: Int,
|
||||||
@ColumnInfo(defaultValue = "false")
|
@ColumnInfo(defaultValue = "false")
|
||||||
var isNotification: Boolean = false,
|
var isNotification: Boolean = false,
|
||||||
@ColumnInfo(defaultValue = "false")
|
@ColumnInfo(defaultValue = "false")
|
||||||
|
|
|
@ -5,7 +5,7 @@ import androidx.room.Relation
|
||||||
|
|
||||||
data class FeedWithArticle(
|
data class FeedWithArticle(
|
||||||
@Embedded
|
@Embedded
|
||||||
val feed: Feed,
|
var feed: Feed,
|
||||||
@Relation(parentColumn = "id", entityColumn = "feedId")
|
@Relation(parentColumn = "id", entityColumn = "feedId")
|
||||||
val articles: List<Article>
|
var articles: List<Article>
|
||||||
)
|
)
|
||||||
|
|
|
@ -5,7 +5,7 @@ import androidx.room.Relation
|
||||||
|
|
||||||
data class FeedWithGroup(
|
data class FeedWithGroup(
|
||||||
@Embedded
|
@Embedded
|
||||||
val feed: Feed,
|
var feed: Feed,
|
||||||
@Relation(parentColumn = "groupId", entityColumn = "id")
|
@Relation(parentColumn = "groupId", entityColumn = "id")
|
||||||
val group: Group
|
var group: Group
|
||||||
)
|
)
|
||||||
|
|
|
@ -8,11 +8,11 @@ import androidx.room.PrimaryKey
|
||||||
@Entity(tableName = "group")
|
@Entity(tableName = "group")
|
||||||
data class Group(
|
data class Group(
|
||||||
@PrimaryKey
|
@PrimaryKey
|
||||||
val id: String,
|
var id: String,
|
||||||
@ColumnInfo
|
@ColumnInfo
|
||||||
val name: String,
|
var name: String,
|
||||||
@ColumnInfo(index = true)
|
@ColumnInfo(index = true)
|
||||||
val accountId: Int,
|
var accountId: Int,
|
||||||
) {
|
) {
|
||||||
@Ignore
|
@Ignore
|
||||||
var important: Int? = 0
|
var important: Int? = 0
|
||||||
|
|
|
@ -5,7 +5,7 @@ import androidx.room.Relation
|
||||||
|
|
||||||
data class GroupWithFeed(
|
data class GroupWithFeed(
|
||||||
@Embedded
|
@Embedded
|
||||||
val group: Group,
|
var group: Group,
|
||||||
@Relation(parentColumn = "id", entityColumn = "groupId")
|
@Relation(parentColumn = "id", entityColumn = "groupId")
|
||||||
val feeds: MutableList<Feed>
|
var feeds: MutableList<Feed>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
package me.ash.reader.data.entity
|
|
||||||
|
|
||||||
data class LatestRelease(
|
|
||||||
val html_url: String? = null,
|
|
||||||
val tag_name: String? = null,
|
|
||||||
val name: String? = null,
|
|
||||||
val draft: Boolean? = null,
|
|
||||||
val prerelease: Boolean? = null,
|
|
||||||
val created_at: String? = null,
|
|
||||||
val published_at: String? = null,
|
|
||||||
val assets: List<AssetsItem>? = null,
|
|
||||||
val body: String? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class AssetsItem(
|
|
||||||
val name: String? = null,
|
|
||||||
val content_type: String? = null,
|
|
||||||
val size: Int? = null,
|
|
||||||
val download_count: Int? = null,
|
|
||||||
val created_at: String? = null,
|
|
||||||
val updated_at: String? = null,
|
|
||||||
val browser_download_url: String? = null,
|
|
||||||
)
|
|
|
@ -1,4 +1,4 @@
|
||||||
package me.ash.reader.data.entity
|
package me.ash.reader.data.model
|
||||||
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.FiberManualRecord
|
import androidx.compose.material.icons.outlined.FiberManualRecord
|
||||||
|
@ -6,7 +6,10 @@ import androidx.compose.material.icons.rounded.FiberManualRecord
|
||||||
import androidx.compose.material.icons.rounded.Star
|
import androidx.compose.material.icons.rounded.Star
|
||||||
import androidx.compose.material.icons.rounded.StarOutline
|
import androidx.compose.material.icons.rounded.StarOutline
|
||||||
import androidx.compose.material.icons.rounded.Subject
|
import androidx.compose.material.icons.rounded.Subject
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import me.ash.reader.R
|
||||||
|
|
||||||
class Filter(
|
class Filter(
|
||||||
val index: Int,
|
val index: Int,
|
||||||
|
@ -33,5 +36,13 @@ class Filter(
|
||||||
iconOutline = Icons.Rounded.Subject,
|
iconOutline = Icons.Rounded.Subject,
|
||||||
iconFilled = Icons.Rounded.Subject,
|
iconFilled = Icons.Rounded.Subject,
|
||||||
)
|
)
|
||||||
|
val values = listOf(Starred, Unread, All)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Filter.getName(): String = when (this) {
|
||||||
|
Filter.Unread -> stringResource(R.string.unread)
|
||||||
|
Filter.Starred -> stringResource(R.string.starred)
|
||||||
|
else -> stringResource(R.string.all)
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package me.ash.reader.data.entity
|
package me.ash.reader.data.model
|
||||||
|
|
||||||
data class ImportantCount(
|
data class ImportantCount(
|
||||||
val important: Int,
|
val important: Int,
|
|
@ -0,0 +1,2 @@
|
||||||
|
package me.ash.reader.data.model
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package me.ash.reader.data.entity
|
package me.ash.reader.data.model
|
||||||
|
|
||||||
class Version(identifiers: List<String>) {
|
class Version(identifiers: List<String>) {
|
||||||
private var major: Int = 0
|
private var major: Int = 0
|
|
@ -10,7 +10,7 @@ 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.source.ReaderDatabase
|
import me.ash.reader.data.source.RYDatabase
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
|
@ -19,26 +19,26 @@ object DatabaseModule {
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideArticleDao(readerDatabase: ReaderDatabase): ArticleDao =
|
fun provideArticleDao(RYDatabase: RYDatabase): ArticleDao =
|
||||||
readerDatabase.articleDao()
|
RYDatabase.articleDao()
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideFeedDao(readerDatabase: ReaderDatabase): FeedDao =
|
fun provideFeedDao(RYDatabase: RYDatabase): FeedDao =
|
||||||
readerDatabase.feedDao()
|
RYDatabase.feedDao()
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideGroupDao(readerDatabase: ReaderDatabase): GroupDao =
|
fun provideGroupDao(RYDatabase: RYDatabase): GroupDao =
|
||||||
readerDatabase.groupDao()
|
RYDatabase.groupDao()
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideAccountDao(readerDatabase: ReaderDatabase): AccountDao =
|
fun provideAccountDao(RYDatabase: RYDatabase): AccountDao =
|
||||||
readerDatabase.accountDao()
|
RYDatabase.accountDao()
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideReaderDatabase(@ApplicationContext context: Context): ReaderDatabase =
|
fun provideReaderDatabase(@ApplicationContext context: Context): RYDatabase =
|
||||||
ReaderDatabase.getInstance(context)
|
RYDatabase.getInstance(context)
|
||||||
}
|
}
|
|
@ -15,7 +15,7 @@ import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import me.ash.reader.cachingHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
|
@ -25,16 +25,11 @@ object ImageLoaderModule {
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideImageLoader(
|
fun provideImageLoader(
|
||||||
@ApplicationContext context: Context
|
@ApplicationContext context: Context,
|
||||||
|
okHttpClient: OkHttpClient,
|
||||||
): ImageLoader {
|
): ImageLoader {
|
||||||
return ImageLoader.Builder(context)
|
return ImageLoader.Builder(context)
|
||||||
.okHttpClient(
|
.okHttpClient(okHttpClient)
|
||||||
okHttpClient = cachingHttpClient(
|
|
||||||
cacheDirectory = context.cacheDir.resolve("http")
|
|
||||||
).newBuilder()
|
|
||||||
//.addNetworkInterceptor(UserAgentInterceptor)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.dispatcher(Dispatchers.Default) // This slightly improves scrolling performance
|
.dispatcher(Dispatchers.Default) // This slightly improves scrolling performance
|
||||||
.components{
|
.components{
|
||||||
add(SvgDecoder.Factory())
|
add(SvgDecoder.Factory())
|
||||||
|
@ -59,4 +54,4 @@ object ImageLoaderModule {
|
||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,121 @@
|
||||||
|
/*
|
||||||
|
* Feeder: Android RSS reader app
|
||||||
|
* https://gitlab.com/spacecowboy/Feeder
|
||||||
|
*
|
||||||
|
* Copyright (C) 2022 Jonas Kalderstam
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package me.ash.reader.data.module
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import me.ash.reader.BuildConfig
|
||||||
|
import okhttp3.Cache
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Response
|
||||||
|
import java.io.File
|
||||||
|
import java.security.KeyManagementException
|
||||||
|
import java.security.NoSuchAlgorithmException
|
||||||
|
import java.security.cert.X509Certificate
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import javax.inject.Singleton
|
||||||
|
import javax.net.ssl.HostnameVerifier
|
||||||
|
import javax.net.ssl.SSLContext
|
||||||
|
import javax.net.ssl.TrustManager
|
||||||
|
import javax.net.ssl.X509TrustManager
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
object OkHttpClientModule {
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideOkHttpClient(
|
||||||
|
@ApplicationContext context: Context
|
||||||
|
): OkHttpClient = cachingHttpClient(
|
||||||
|
cacheDirectory = context.cacheDir.resolve("http")
|
||||||
|
).newBuilder()
|
||||||
|
.addNetworkInterceptor(UserAgentInterceptor)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cachingHttpClient(
|
||||||
|
cacheDirectory: File? = null,
|
||||||
|
cacheSize: Long = 10L * 1024L * 1024L,
|
||||||
|
trustAllCerts: Boolean = true,
|
||||||
|
connectTimeoutSecs: Long = 30L,
|
||||||
|
readTimeoutSecs: Long = 30L
|
||||||
|
): OkHttpClient {
|
||||||
|
val builder: OkHttpClient.Builder = OkHttpClient.Builder()
|
||||||
|
|
||||||
|
if (cacheDirectory != null) {
|
||||||
|
builder.cache(Cache(cacheDirectory, cacheSize))
|
||||||
|
}
|
||||||
|
|
||||||
|
builder
|
||||||
|
.connectTimeout(connectTimeoutSecs, TimeUnit.SECONDS)
|
||||||
|
.readTimeout(readTimeoutSecs, TimeUnit.SECONDS)
|
||||||
|
.followRedirects(true)
|
||||||
|
|
||||||
|
if (trustAllCerts) {
|
||||||
|
builder.trustAllCerts()
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun OkHttpClient.Builder.trustAllCerts() {
|
||||||
|
try {
|
||||||
|
val trustManager = object : X509TrustManager {
|
||||||
|
override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun checkServerTrusted(chain: Array<out X509Certificate>?, authType: String?) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAcceptedIssuers(): Array<X509Certificate> = emptyArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
val sslContext = SSLContext.getInstance("TLS")
|
||||||
|
sslContext.init(null, arrayOf<TrustManager>(trustManager), null)
|
||||||
|
val sslSocketFactory = sslContext.socketFactory
|
||||||
|
|
||||||
|
sslSocketFactory(sslSocketFactory, trustManager)
|
||||||
|
.hostnameVerifier(HostnameVerifier { _, _ -> true })
|
||||||
|
} catch (e: NoSuchAlgorithmException) {
|
||||||
|
// ignore
|
||||||
|
} catch (e: KeyManagementException) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object UserAgentInterceptor : Interceptor {
|
||||||
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
return chain.proceed(
|
||||||
|
chain.request()
|
||||||
|
.newBuilder()
|
||||||
|
.header("User-Agent", USER_AGENT_STRING)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const val USER_AGENT_STRING = "ReadYou / ${BuildConfig.VERSION_NAME}(${BuildConfig.VERSION_CODE})"
|
|
@ -4,7 +4,7 @@ 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.AppNetworkDataSource
|
import me.ash.reader.data.source.RYNetworkDataSource
|
||||||
import me.ash.reader.data.source.FeverApiDataSource
|
import me.ash.reader.data.source.FeverApiDataSource
|
||||||
import me.ash.reader.data.source.GoogleReaderApiDataSource
|
import me.ash.reader.data.source.GoogleReaderApiDataSource
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
@ -15,8 +15,8 @@ object RetrofitModule {
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideAppNetworkDataSource(): AppNetworkDataSource =
|
fun provideAppNetworkDataSource(): RYNetworkDataSource =
|
||||||
AppNetworkDataSource.getInstance()
|
RYNetworkDataSource.getInstance()
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
|
|
|
@ -3,6 +3,7 @@ package me.ash.reader.data.preference
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.ReadOnlyComposable
|
||||||
import androidx.datastore.preferences.core.Preferences
|
import androidx.datastore.preferences.core.Preferences
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
@ -33,6 +34,7 @@ sealed class DarkThemePreference(val value: Int) : Preference() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@ReadOnlyComposable
|
||||||
fun isDarkTheme(): Boolean = when (this) {
|
fun isDarkTheme(): Boolean = when (this) {
|
||||||
UseDeviceTheme -> isSystemInDarkTheme()
|
UseDeviceTheme -> isSystemInDarkTheme()
|
||||||
ON -> true
|
ON -> true
|
||||||
|
|
|
@ -4,17 +4,18 @@ import android.content.Context
|
||||||
import androidx.datastore.preferences.core.Preferences
|
import androidx.datastore.preferences.core.Preferences
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import me.ash.reader.data.constant.ElevationTokens
|
||||||
import me.ash.reader.ui.ext.DataStoreKeys
|
import me.ash.reader.ui.ext.DataStoreKeys
|
||||||
import me.ash.reader.ui.ext.dataStore
|
import me.ash.reader.ui.ext.dataStore
|
||||||
import me.ash.reader.ui.ext.put
|
import me.ash.reader.ui.ext.put
|
||||||
|
|
||||||
sealed class FeedsFilterBarTonalElevationPreference(val value: Int) : Preference() {
|
sealed class FeedsFilterBarTonalElevationPreference(val value: Int) : Preference() {
|
||||||
object Level0 : FeedsFilterBarTonalElevationPreference(0)
|
object Level0 : FeedsFilterBarTonalElevationPreference(ElevationTokens.Level0)
|
||||||
object Level1 : FeedsFilterBarTonalElevationPreference(1)
|
object Level1 : FeedsFilterBarTonalElevationPreference(ElevationTokens.Level1)
|
||||||
object Level2 : FeedsFilterBarTonalElevationPreference(3)
|
object Level2 : FeedsFilterBarTonalElevationPreference(ElevationTokens.Level2)
|
||||||
object Level3 : FeedsFilterBarTonalElevationPreference(6)
|
object Level3 : FeedsFilterBarTonalElevationPreference(ElevationTokens.Level3)
|
||||||
object Level4 : FeedsFilterBarTonalElevationPreference(8)
|
object Level4 : FeedsFilterBarTonalElevationPreference(ElevationTokens.Level4)
|
||||||
object Level5 : FeedsFilterBarTonalElevationPreference(12)
|
object Level5 : FeedsFilterBarTonalElevationPreference(ElevationTokens.Level5)
|
||||||
|
|
||||||
override fun put(context: Context, scope: CoroutineScope) {
|
override fun put(context: Context, scope: CoroutineScope) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
@ -27,12 +28,12 @@ sealed class FeedsFilterBarTonalElevationPreference(val value: Int) : Preference
|
||||||
|
|
||||||
fun getDesc(context: Context): String =
|
fun getDesc(context: Context): String =
|
||||||
when (this) {
|
when (this) {
|
||||||
Level0 -> "Level 0 (0dp)"
|
Level0 -> "Level 0 (${ElevationTokens.Level0}dp)"
|
||||||
Level1 -> "Level 1 (1dp)"
|
Level1 -> "Level 1 (${ElevationTokens.Level1}dp)"
|
||||||
Level2 -> "Level 2 (3dp)"
|
Level2 -> "Level 2 (${ElevationTokens.Level2}dp)"
|
||||||
Level3 -> "Level 3 (6dp)"
|
Level3 -> "Level 3 (${ElevationTokens.Level3}dp)"
|
||||||
Level4 -> "Level 4 (8dp)"
|
Level4 -> "Level 4 (${ElevationTokens.Level4}dp)"
|
||||||
Level5 -> "Level 5 (12dp)"
|
Level5 -> "Level 5 (${ElevationTokens.Level5}dp)"
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -41,13 +42,14 @@ sealed class FeedsFilterBarTonalElevationPreference(val value: Int) : Preference
|
||||||
|
|
||||||
fun fromPreferences(preferences: Preferences) =
|
fun fromPreferences(preferences: Preferences) =
|
||||||
when (preferences[DataStoreKeys.FeedsFilterBarTonalElevation.key]) {
|
when (preferences[DataStoreKeys.FeedsFilterBarTonalElevation.key]) {
|
||||||
0 -> Level0
|
ElevationTokens.Level0 -> Level0
|
||||||
1 -> Level1
|
ElevationTokens.Level1 -> Level1
|
||||||
3 -> Level2
|
ElevationTokens.Level2 -> Level2
|
||||||
6 -> Level3
|
ElevationTokens.Level3 -> Level3
|
||||||
8 -> Level4
|
ElevationTokens.Level4 -> Level4
|
||||||
12 -> Level5
|
ElevationTokens.Level5 -> Level5
|
||||||
else -> default
|
else -> default
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,17 +4,18 @@ import android.content.Context
|
||||||
import androidx.datastore.preferences.core.Preferences
|
import androidx.datastore.preferences.core.Preferences
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import me.ash.reader.data.constant.ElevationTokens
|
||||||
import me.ash.reader.ui.ext.DataStoreKeys
|
import me.ash.reader.ui.ext.DataStoreKeys
|
||||||
import me.ash.reader.ui.ext.dataStore
|
import me.ash.reader.ui.ext.dataStore
|
||||||
import me.ash.reader.ui.ext.put
|
import me.ash.reader.ui.ext.put
|
||||||
|
|
||||||
sealed class FeedsGroupListTonalElevationPreference(val value: Int) : Preference() {
|
sealed class FeedsGroupListTonalElevationPreference(val value: Int) : Preference() {
|
||||||
object Level0 : FeedsGroupListTonalElevationPreference(0)
|
object Level0 : FeedsGroupListTonalElevationPreference(ElevationTokens.Level0)
|
||||||
object Level1 : FeedsGroupListTonalElevationPreference(1)
|
object Level1 : FeedsGroupListTonalElevationPreference(ElevationTokens.Level1)
|
||||||
object Level2 : FeedsGroupListTonalElevationPreference(3)
|
object Level2 : FeedsGroupListTonalElevationPreference(ElevationTokens.Level2)
|
||||||
object Level3 : FeedsGroupListTonalElevationPreference(6)
|
object Level3 : FeedsGroupListTonalElevationPreference(ElevationTokens.Level3)
|
||||||
object Level4 : FeedsGroupListTonalElevationPreference(8)
|
object Level4 : FeedsGroupListTonalElevationPreference(ElevationTokens.Level4)
|
||||||
object Level5 : FeedsGroupListTonalElevationPreference(12)
|
object Level5 : FeedsGroupListTonalElevationPreference(ElevationTokens.Level5)
|
||||||
|
|
||||||
override fun put(context: Context, scope: CoroutineScope) {
|
override fun put(context: Context, scope: CoroutineScope) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
@ -27,12 +28,12 @@ sealed class FeedsGroupListTonalElevationPreference(val value: Int) : Preference
|
||||||
|
|
||||||
fun getDesc(context: Context): String =
|
fun getDesc(context: Context): String =
|
||||||
when (this) {
|
when (this) {
|
||||||
Level0 -> "Level 0 (0dp)"
|
Level0 -> "Level 0 (${ElevationTokens.Level0}dp)"
|
||||||
Level1 -> "Level 1 (1dp)"
|
Level1 -> "Level 1 (${ElevationTokens.Level1}dp)"
|
||||||
Level2 -> "Level 2 (3dp)"
|
Level2 -> "Level 2 (${ElevationTokens.Level2}dp)"
|
||||||
Level3 -> "Level 3 (6dp)"
|
Level3 -> "Level 3 (${ElevationTokens.Level3}dp)"
|
||||||
Level4 -> "Level 4 (8dp)"
|
Level4 -> "Level 4 (${ElevationTokens.Level4}dp)"
|
||||||
Level5 -> "Level 5 (12dp)"
|
Level5 -> "Level 5 (${ElevationTokens.Level5}dp)"
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -41,12 +42,12 @@ sealed class FeedsGroupListTonalElevationPreference(val value: Int) : Preference
|
||||||
|
|
||||||
fun fromPreferences(preferences: Preferences) =
|
fun fromPreferences(preferences: Preferences) =
|
||||||
when (preferences[DataStoreKeys.FeedsGroupListTonalElevation.key]) {
|
when (preferences[DataStoreKeys.FeedsGroupListTonalElevation.key]) {
|
||||||
0 -> Level0
|
ElevationTokens.Level0 -> Level0
|
||||||
1 -> Level1
|
ElevationTokens.Level1 -> Level1
|
||||||
3 -> Level2
|
ElevationTokens.Level2 -> Level2
|
||||||
6 -> Level3
|
ElevationTokens.Level3 -> Level3
|
||||||
8 -> Level4
|
ElevationTokens.Level4 -> Level4
|
||||||
12 -> Level5
|
ElevationTokens.Level5 -> Level5
|
||||||
else -> default
|
else -> default
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,17 +4,18 @@ import android.content.Context
|
||||||
import androidx.datastore.preferences.core.Preferences
|
import androidx.datastore.preferences.core.Preferences
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import me.ash.reader.data.constant.ElevationTokens
|
||||||
import me.ash.reader.ui.ext.DataStoreKeys
|
import me.ash.reader.ui.ext.DataStoreKeys
|
||||||
import me.ash.reader.ui.ext.dataStore
|
import me.ash.reader.ui.ext.dataStore
|
||||||
import me.ash.reader.ui.ext.put
|
import me.ash.reader.ui.ext.put
|
||||||
|
|
||||||
sealed class FeedsTopBarTonalElevationPreference(val value: Int) : Preference() {
|
sealed class FeedsTopBarTonalElevationPreference(val value: Int) : Preference() {
|
||||||
object Level0 : FeedsTopBarTonalElevationPreference(0)
|
object Level0 : FeedsTopBarTonalElevationPreference(ElevationTokens.Level0)
|
||||||
object Level1 : FeedsTopBarTonalElevationPreference(1)
|
object Level1 : FeedsTopBarTonalElevationPreference(ElevationTokens.Level1)
|
||||||
object Level2 : FeedsTopBarTonalElevationPreference(3)
|
object Level2 : FeedsTopBarTonalElevationPreference(ElevationTokens.Level2)
|
||||||
object Level3 : FeedsTopBarTonalElevationPreference(6)
|
object Level3 : FeedsTopBarTonalElevationPreference(ElevationTokens.Level3)
|
||||||
object Level4 : FeedsTopBarTonalElevationPreference(8)
|
object Level4 : FeedsTopBarTonalElevationPreference(ElevationTokens.Level4)
|
||||||
object Level5 : FeedsTopBarTonalElevationPreference(12)
|
object Level5 : FeedsTopBarTonalElevationPreference(ElevationTokens.Level5)
|
||||||
|
|
||||||
override fun put(context: Context, scope: CoroutineScope) {
|
override fun put(context: Context, scope: CoroutineScope) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
@ -27,12 +28,12 @@ sealed class FeedsTopBarTonalElevationPreference(val value: Int) : Preference()
|
||||||
|
|
||||||
fun getDesc(context: Context): String =
|
fun getDesc(context: Context): String =
|
||||||
when (this) {
|
when (this) {
|
||||||
Level0 -> "Level 0 (0dp)"
|
Level0 -> "Level 0 (${ElevationTokens.Level0}dp)"
|
||||||
Level1 -> "Level 1 (1dp)"
|
Level1 -> "Level 1 (${ElevationTokens.Level1}dp)"
|
||||||
Level2 -> "Level 2 (3dp)"
|
Level2 -> "Level 2 (${ElevationTokens.Level2}dp)"
|
||||||
Level3 -> "Level 3 (6dp)"
|
Level3 -> "Level 3 (${ElevationTokens.Level3}dp)"
|
||||||
Level4 -> "Level 4 (8dp)"
|
Level4 -> "Level 4 (${ElevationTokens.Level4}dp)"
|
||||||
Level5 -> "Level 5 (12dp)"
|
Level5 -> "Level 5 (${ElevationTokens.Level5}dp)"
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -41,12 +42,12 @@ sealed class FeedsTopBarTonalElevationPreference(val value: Int) : Preference()
|
||||||
|
|
||||||
fun fromPreferences(preferences: Preferences) =
|
fun fromPreferences(preferences: Preferences) =
|
||||||
when (preferences[DataStoreKeys.FeedsTopBarTonalElevation.key]) {
|
when (preferences[DataStoreKeys.FeedsTopBarTonalElevation.key]) {
|
||||||
0 -> Level0
|
ElevationTokens.Level0 -> Level0
|
||||||
1 -> Level1
|
ElevationTokens.Level1 -> Level1
|
||||||
3 -> Level2
|
ElevationTokens.Level2 -> Level2
|
||||||
6 -> Level3
|
ElevationTokens.Level3 -> Level3
|
||||||
8 -> Level4
|
ElevationTokens.Level4 -> Level4
|
||||||
12 -> Level5
|
ElevationTokens.Level5 -> Level5
|
||||||
else -> default
|
else -> default
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,17 +4,18 @@ import android.content.Context
|
||||||
import androidx.datastore.preferences.core.Preferences
|
import androidx.datastore.preferences.core.Preferences
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import me.ash.reader.data.constant.ElevationTokens
|
||||||
import me.ash.reader.ui.ext.DataStoreKeys
|
import me.ash.reader.ui.ext.DataStoreKeys
|
||||||
import me.ash.reader.ui.ext.dataStore
|
import me.ash.reader.ui.ext.dataStore
|
||||||
import me.ash.reader.ui.ext.put
|
import me.ash.reader.ui.ext.put
|
||||||
|
|
||||||
sealed class FlowArticleListTonalElevationPreference(val value: Int) : Preference() {
|
sealed class FlowArticleListTonalElevationPreference(val value: Int) : Preference() {
|
||||||
object Level0 : FlowArticleListTonalElevationPreference(0)
|
object Level0 : FlowArticleListTonalElevationPreference(ElevationTokens.Level0)
|
||||||
object Level1 : FlowArticleListTonalElevationPreference(1)
|
object Level1 : FlowArticleListTonalElevationPreference(ElevationTokens.Level1)
|
||||||
object Level2 : FlowArticleListTonalElevationPreference(3)
|
object Level2 : FlowArticleListTonalElevationPreference(ElevationTokens.Level2)
|
||||||
object Level3 : FlowArticleListTonalElevationPreference(6)
|
object Level3 : FlowArticleListTonalElevationPreference(ElevationTokens.Level3)
|
||||||
object Level4 : FlowArticleListTonalElevationPreference(8)
|
object Level4 : FlowArticleListTonalElevationPreference(ElevationTokens.Level4)
|
||||||
object Level5 : FlowArticleListTonalElevationPreference(12)
|
object Level5 : FlowArticleListTonalElevationPreference(ElevationTokens.Level5)
|
||||||
|
|
||||||
override fun put(context: Context, scope: CoroutineScope) {
|
override fun put(context: Context, scope: CoroutineScope) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
@ -27,12 +28,12 @@ sealed class FlowArticleListTonalElevationPreference(val value: Int) : Preferenc
|
||||||
|
|
||||||
fun getDesc(context: Context): String =
|
fun getDesc(context: Context): String =
|
||||||
when (this) {
|
when (this) {
|
||||||
Level0 -> "Level 0 (0dp)"
|
Level0 -> "Level 0 (${ElevationTokens.Level0}dp)"
|
||||||
Level1 -> "Level 1 (1dp)"
|
Level1 -> "Level 1 (${ElevationTokens.Level1}dp)"
|
||||||
Level2 -> "Level 2 (3dp)"
|
Level2 -> "Level 2 (${ElevationTokens.Level2}dp)"
|
||||||
Level3 -> "Level 3 (6dp)"
|
Level3 -> "Level 3 (${ElevationTokens.Level3}dp)"
|
||||||
Level4 -> "Level 4 (8dp)"
|
Level4 -> "Level 4 (${ElevationTokens.Level4}dp)"
|
||||||
Level5 -> "Level 5 (12dp)"
|
Level5 -> "Level 5 (${ElevationTokens.Level5}dp)"
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -41,12 +42,12 @@ sealed class FlowArticleListTonalElevationPreference(val value: Int) : Preferenc
|
||||||
|
|
||||||
fun fromPreferences(preferences: Preferences) =
|
fun fromPreferences(preferences: Preferences) =
|
||||||
when (preferences[DataStoreKeys.FlowArticleListTonalElevation.key]) {
|
when (preferences[DataStoreKeys.FlowArticleListTonalElevation.key]) {
|
||||||
0 -> Level0
|
ElevationTokens.Level0 -> Level0
|
||||||
1 -> Level1
|
ElevationTokens.Level1 -> Level1
|
||||||
3 -> Level2
|
ElevationTokens.Level2 -> Level2
|
||||||
6 -> Level3
|
ElevationTokens.Level3 -> Level3
|
||||||
8 -> Level4
|
ElevationTokens.Level4 -> Level4
|
||||||
12 -> Level5
|
ElevationTokens.Level5 -> Level5
|
||||||
else -> default
|
else -> default
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,17 +4,18 @@ import android.content.Context
|
||||||
import androidx.datastore.preferences.core.Preferences
|
import androidx.datastore.preferences.core.Preferences
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import me.ash.reader.data.constant.ElevationTokens
|
||||||
import me.ash.reader.ui.ext.DataStoreKeys
|
import me.ash.reader.ui.ext.DataStoreKeys
|
||||||
import me.ash.reader.ui.ext.dataStore
|
import me.ash.reader.ui.ext.dataStore
|
||||||
import me.ash.reader.ui.ext.put
|
import me.ash.reader.ui.ext.put
|
||||||
|
|
||||||
sealed class FlowFilterBarTonalElevationPreference(val value: Int) : Preference() {
|
sealed class FlowFilterBarTonalElevationPreference(val value: Int) : Preference() {
|
||||||
object Level0 : FlowFilterBarTonalElevationPreference(0)
|
object Level0 : FlowFilterBarTonalElevationPreference(ElevationTokens.Level0)
|
||||||
object Level1 : FlowFilterBarTonalElevationPreference(1)
|
object Level1 : FlowFilterBarTonalElevationPreference(ElevationTokens.Level1)
|
||||||
object Level2 : FlowFilterBarTonalElevationPreference(3)
|
object Level2 : FlowFilterBarTonalElevationPreference(ElevationTokens.Level2)
|
||||||
object Level3 : FlowFilterBarTonalElevationPreference(6)
|
object Level3 : FlowFilterBarTonalElevationPreference(ElevationTokens.Level3)
|
||||||
object Level4 : FlowFilterBarTonalElevationPreference(8)
|
object Level4 : FlowFilterBarTonalElevationPreference(ElevationTokens.Level4)
|
||||||
object Level5 : FlowFilterBarTonalElevationPreference(12)
|
object Level5 : FlowFilterBarTonalElevationPreference(ElevationTokens.Level5)
|
||||||
|
|
||||||
override fun put(context: Context, scope: CoroutineScope) {
|
override fun put(context: Context, scope: CoroutineScope) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
@ -27,12 +28,12 @@ sealed class FlowFilterBarTonalElevationPreference(val value: Int) : Preference(
|
||||||
|
|
||||||
fun getDesc(context: Context): String =
|
fun getDesc(context: Context): String =
|
||||||
when (this) {
|
when (this) {
|
||||||
Level0 -> "Level 0 (0dp)"
|
Level0 -> "Level 0 (${ElevationTokens.Level0}dp)"
|
||||||
Level1 -> "Level 1 (1dp)"
|
Level1 -> "Level 1 (${ElevationTokens.Level1}dp)"
|
||||||
Level2 -> "Level 2 (3dp)"
|
Level2 -> "Level 2 (${ElevationTokens.Level2}dp)"
|
||||||
Level3 -> "Level 3 (6dp)"
|
Level3 -> "Level 3 (${ElevationTokens.Level3}dp)"
|
||||||
Level4 -> "Level 4 (8dp)"
|
Level4 -> "Level 4 (${ElevationTokens.Level4}dp)"
|
||||||
Level5 -> "Level 5 (12dp)"
|
Level5 -> "Level 5 (${ElevationTokens.Level5}dp)"
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -41,12 +42,12 @@ sealed class FlowFilterBarTonalElevationPreference(val value: Int) : Preference(
|
||||||
|
|
||||||
fun fromPreferences(preferences: Preferences) =
|
fun fromPreferences(preferences: Preferences) =
|
||||||
when (preferences[DataStoreKeys.FlowFilterBarTonalElevation.key]) {
|
when (preferences[DataStoreKeys.FlowFilterBarTonalElevation.key]) {
|
||||||
0 -> Level0
|
ElevationTokens.Level0 -> Level0
|
||||||
1 -> Level1
|
ElevationTokens.Level1 -> Level1
|
||||||
3 -> Level2
|
ElevationTokens.Level2 -> Level2
|
||||||
6 -> Level3
|
ElevationTokens.Level3 -> Level3
|
||||||
8 -> Level4
|
ElevationTokens.Level4 -> Level4
|
||||||
12 -> Level5
|
ElevationTokens.Level5 -> Level5
|
||||||
else -> default
|
else -> default
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,17 +4,18 @@ import android.content.Context
|
||||||
import androidx.datastore.preferences.core.Preferences
|
import androidx.datastore.preferences.core.Preferences
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import me.ash.reader.data.constant.ElevationTokens
|
||||||
import me.ash.reader.ui.ext.DataStoreKeys
|
import me.ash.reader.ui.ext.DataStoreKeys
|
||||||
import me.ash.reader.ui.ext.dataStore
|
import me.ash.reader.ui.ext.dataStore
|
||||||
import me.ash.reader.ui.ext.put
|
import me.ash.reader.ui.ext.put
|
||||||
|
|
||||||
sealed class FlowTopBarTonalElevationPreference(val value: Int) : Preference() {
|
sealed class FlowTopBarTonalElevationPreference(val value: Int) : Preference() {
|
||||||
object Level0 : FlowTopBarTonalElevationPreference(0)
|
object Level0 : FlowTopBarTonalElevationPreference(ElevationTokens.Level0)
|
||||||
object Level1 : FlowTopBarTonalElevationPreference(1)
|
object Level1 : FlowTopBarTonalElevationPreference(ElevationTokens.Level1)
|
||||||
object Level2 : FlowTopBarTonalElevationPreference(3)
|
object Level2 : FlowTopBarTonalElevationPreference(ElevationTokens.Level2)
|
||||||
object Level3 : FlowTopBarTonalElevationPreference(6)
|
object Level3 : FlowTopBarTonalElevationPreference(ElevationTokens.Level3)
|
||||||
object Level4 : FlowTopBarTonalElevationPreference(8)
|
object Level4 : FlowTopBarTonalElevationPreference(ElevationTokens.Level4)
|
||||||
object Level5 : FlowTopBarTonalElevationPreference(12)
|
object Level5 : FlowTopBarTonalElevationPreference(ElevationTokens.Level5)
|
||||||
|
|
||||||
override fun put(context: Context, scope: CoroutineScope) {
|
override fun put(context: Context, scope: CoroutineScope) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
@ -27,12 +28,12 @@ sealed class FlowTopBarTonalElevationPreference(val value: Int) : Preference() {
|
||||||
|
|
||||||
fun getDesc(context: Context): String =
|
fun getDesc(context: Context): String =
|
||||||
when (this) {
|
when (this) {
|
||||||
Level0 -> "Level 0 (0dp)"
|
Level0 -> "Level 0 (${ElevationTokens.Level0}dp)"
|
||||||
Level1 -> "Level 1 (1dp)"
|
Level1 -> "Level 1 (${ElevationTokens.Level1}dp)"
|
||||||
Level2 -> "Level 2 (3dp)"
|
Level2 -> "Level 2 (${ElevationTokens.Level2}dp)"
|
||||||
Level3 -> "Level 3 (6dp)"
|
Level3 -> "Level 3 (${ElevationTokens.Level3}dp)"
|
||||||
Level4 -> "Level 4 (8dp)"
|
Level4 -> "Level 4 (${ElevationTokens.Level4}dp)"
|
||||||
Level5 -> "Level 5 (12dp)"
|
Level5 -> "Level 5 (${ElevationTokens.Level5}dp)"
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -41,12 +42,12 @@ sealed class FlowTopBarTonalElevationPreference(val value: Int) : Preference() {
|
||||||
|
|
||||||
fun fromPreferences(preferences: Preferences) =
|
fun fromPreferences(preferences: Preferences) =
|
||||||
when (preferences[DataStoreKeys.FlowTopBarTonalElevation.key]) {
|
when (preferences[DataStoreKeys.FlowTopBarTonalElevation.key]) {
|
||||||
0 -> Level0
|
ElevationTokens.Level0 -> Level0
|
||||||
1 -> Level1
|
ElevationTokens.Level1 -> Level1
|
||||||
3 -> Level2
|
ElevationTokens.Level2 -> Level2
|
||||||
6 -> Level3
|
ElevationTokens.Level3 -> Level3
|
||||||
8 -> Level4
|
ElevationTokens.Level4 -> Level4
|
||||||
12 -> Level5
|
ElevationTokens.Level5 -> Level5
|
||||||
else -> default
|
else -> default
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
package me.ash.reader.data.preference
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.datastore.preferences.core.Preferences
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import me.ash.reader.R
|
||||||
|
import me.ash.reader.ui.ext.DataStoreKeys
|
||||||
|
import me.ash.reader.ui.ext.dataStore
|
||||||
|
import me.ash.reader.ui.ext.put
|
||||||
|
|
||||||
|
sealed class InitialFilterPreference(val value: Int) : Preference() {
|
||||||
|
object Starred : InitialFilterPreference(0)
|
||||||
|
object Unread : InitialFilterPreference(1)
|
||||||
|
object All : InitialFilterPreference(2)
|
||||||
|
|
||||||
|
override fun put(context: Context, scope: CoroutineScope) {
|
||||||
|
scope.launch {
|
||||||
|
context.dataStore.put(
|
||||||
|
DataStoreKeys.InitialFilter,
|
||||||
|
value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDesc(context: Context): String =
|
||||||
|
when (this) {
|
||||||
|
Starred -> context.getString(R.string.starred)
|
||||||
|
Unread -> context.getString(R.string.unread)
|
||||||
|
All -> context.getString(R.string.all)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val default = All
|
||||||
|
val values = listOf(Starred, Unread, All)
|
||||||
|
|
||||||
|
fun fromPreferences(preferences: Preferences) =
|
||||||
|
when (preferences[DataStoreKeys.InitialFilter.key]) {
|
||||||
|
0 -> Starred
|
||||||
|
1 -> Unread
|
||||||
|
2 -> All
|
||||||
|
else -> default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
package me.ash.reader.data.preference
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.datastore.preferences.core.Preferences
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import me.ash.reader.R
|
||||||
|
import me.ash.reader.ui.ext.DataStoreKeys
|
||||||
|
import me.ash.reader.ui.ext.dataStore
|
||||||
|
import me.ash.reader.ui.ext.put
|
||||||
|
|
||||||
|
sealed class InitialPagePreference(val value: Int) : Preference() {
|
||||||
|
object FeedsPage : InitialPagePreference(0)
|
||||||
|
object FlowPage : InitialPagePreference(1)
|
||||||
|
|
||||||
|
override fun put(context: Context, scope: CoroutineScope) {
|
||||||
|
scope.launch {
|
||||||
|
context.dataStore.put(
|
||||||
|
DataStoreKeys.InitialPage,
|
||||||
|
value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDesc(context: Context): String =
|
||||||
|
when (this) {
|
||||||
|
FeedsPage -> context.getString(R.string.feeds_page)
|
||||||
|
FlowPage -> context.getString(R.string.flow_page)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val default = FeedsPage
|
||||||
|
val values = listOf(FeedsPage, FlowPage)
|
||||||
|
|
||||||
|
fun fromPreferences(preferences: Preferences) =
|
||||||
|
when (preferences[DataStoreKeys.InitialPage.key]) {
|
||||||
|
0 -> FeedsPage
|
||||||
|
1 -> FlowPage
|
||||||
|
else -> default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,6 +17,8 @@ sealed class LanguagesPreference(val value: Int) : Preference() {
|
||||||
object English : LanguagesPreference(1)
|
object English : LanguagesPreference(1)
|
||||||
object ChineseSimplified : LanguagesPreference(2)
|
object ChineseSimplified : LanguagesPreference(2)
|
||||||
object German : LanguagesPreference(3)
|
object German : LanguagesPreference(3)
|
||||||
|
object French : LanguagesPreference(4)
|
||||||
|
object Czech : LanguagesPreference(5)
|
||||||
|
|
||||||
override fun put(context: Context, scope: CoroutineScope) {
|
override fun put(context: Context, scope: CoroutineScope) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
@ -34,6 +36,8 @@ sealed class LanguagesPreference(val value: Int) : Preference() {
|
||||||
English -> context.getString(R.string.english)
|
English -> context.getString(R.string.english)
|
||||||
ChineseSimplified -> context.getString(R.string.chinese_simplified)
|
ChineseSimplified -> context.getString(R.string.chinese_simplified)
|
||||||
German -> context.getString(R.string.german)
|
German -> context.getString(R.string.german)
|
||||||
|
French -> context.getString(R.string.french)
|
||||||
|
Czech -> context.getString(R.string.czech)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getLocale(): Locale =
|
fun getLocale(): Locale =
|
||||||
|
@ -42,6 +46,8 @@ sealed class LanguagesPreference(val value: Int) : Preference() {
|
||||||
English -> Locale("en", "US")
|
English -> Locale("en", "US")
|
||||||
ChineseSimplified -> Locale("zh", "CN")
|
ChineseSimplified -> Locale("zh", "CN")
|
||||||
German -> Locale("de", "DE")
|
German -> Locale("de", "DE")
|
||||||
|
French -> Locale("fr", "FR")
|
||||||
|
Czech -> Locale("cs", "CZ")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setLocale(context: Context) {
|
fun setLocale(context: Context) {
|
||||||
|
@ -68,7 +74,7 @@ sealed class LanguagesPreference(val value: Int) : Preference() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val default = UseDeviceLanguages
|
val default = UseDeviceLanguages
|
||||||
val values = listOf(UseDeviceLanguages, English, ChineseSimplified, German)
|
val values = listOf(UseDeviceLanguages, English, ChineseSimplified, German, French, Czech)
|
||||||
|
|
||||||
fun fromPreferences(preferences: Preferences): LanguagesPreference =
|
fun fromPreferences(preferences: Preferences): LanguagesPreference =
|
||||||
when (preferences[DataStoreKeys.Languages.key]) {
|
when (preferences[DataStoreKeys.Languages.key]) {
|
||||||
|
@ -76,6 +82,8 @@ sealed class LanguagesPreference(val value: Int) : Preference() {
|
||||||
1 -> English
|
1 -> English
|
||||||
2 -> ChineseSimplified
|
2 -> ChineseSimplified
|
||||||
3 -> German
|
3 -> German
|
||||||
|
4 -> French
|
||||||
|
5 -> Czech
|
||||||
else -> default
|
else -> default
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,6 +93,8 @@ sealed class LanguagesPreference(val value: Int) : Preference() {
|
||||||
1 -> English
|
1 -> English
|
||||||
2 -> ChineseSimplified
|
2 -> ChineseSimplified
|
||||||
3 -> German
|
3 -> German
|
||||||
|
4 -> French
|
||||||
|
5 -> Czech
|
||||||
else -> default
|
else -> default
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
package me.ash.reader.data.preference
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.datastore.preferences.core.Preferences
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import me.ash.reader.ui.ext.DataStoreKeys
|
||||||
|
import me.ash.reader.ui.ext.dataStore
|
||||||
|
import me.ash.reader.ui.ext.put
|
||||||
|
|
||||||
|
object NewVersionDownloadUrlPreference {
|
||||||
|
const val default = ""
|
||||||
|
|
||||||
|
fun put(context: Context, scope: CoroutineScope, value: String) {
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
context.dataStore.put(DataStoreKeys.NewVersionDownloadUrl, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fromPreferences(preferences: Preferences) =
|
||||||
|
preferences[DataStoreKeys.NewVersionDownloadUrl.key] ?: default
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
package me.ash.reader.data.preference
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.datastore.preferences.core.Preferences
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import me.ash.reader.ui.ext.DataStoreKeys
|
||||||
|
import me.ash.reader.ui.ext.dataStore
|
||||||
|
import me.ash.reader.ui.ext.put
|
||||||
|
|
||||||
|
object NewVersionLogPreference {
|
||||||
|
const val default = ""
|
||||||
|
|
||||||
|
fun put(context: Context, scope: CoroutineScope, value: String) {
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
context.dataStore.put(DataStoreKeys.NewVersionLog, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fromPreferences(preferences: Preferences) =
|
||||||
|
preferences[DataStoreKeys.NewVersionLog.key] ?: default
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
package me.ash.reader.data.preference
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.datastore.preferences.core.Preferences
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import me.ash.reader.data.model.Version
|
||||||
|
import me.ash.reader.data.model.toVersion
|
||||||
|
import me.ash.reader.ui.ext.DataStoreKeys
|
||||||
|
import me.ash.reader.ui.ext.dataStore
|
||||||
|
import me.ash.reader.ui.ext.put
|
||||||
|
|
||||||
|
object NewVersionNumberPreference {
|
||||||
|
val default = Version()
|
||||||
|
|
||||||
|
fun put(context: Context, scope: CoroutineScope, value: String) {
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
context.dataStore.put(DataStoreKeys.NewVersionNumber, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fromPreferences(preferences: Preferences) =
|
||||||
|
preferences[DataStoreKeys.NewVersionNumber.key].toVersion()
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
package me.ash.reader.data.preference
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.datastore.preferences.core.Preferences
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import me.ash.reader.ui.ext.DataStoreKeys
|
||||||
|
import me.ash.reader.ui.ext.dataStore
|
||||||
|
import me.ash.reader.ui.ext.put
|
||||||
|
|
||||||
|
object NewVersionPublishDatePreference {
|
||||||
|
const val default = ""
|
||||||
|
|
||||||
|
fun put(context: Context, scope: CoroutineScope, value: String) {
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
context.dataStore.put(DataStoreKeys.NewVersionPublishDate, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fromPreferences(preferences: Preferences) =
|
||||||
|
preferences[DataStoreKeys.NewVersionPublishDate.key] ?: default
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
package me.ash.reader.data.preference
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.datastore.preferences.core.Preferences
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import me.ash.reader.ui.ext.DataStoreKeys
|
||||||
|
import me.ash.reader.ui.ext.dataStore
|
||||||
|
import me.ash.reader.ui.ext.put
|
||||||
|
|
||||||
|
object NewVersionSizePreference {
|
||||||
|
const val default = ""
|
||||||
|
|
||||||
|
fun Int.formatSize(): String =
|
||||||
|
(this / 1024f / 1024f)
|
||||||
|
.takeIf { it > 0f }
|
||||||
|
?.run { " ${String.format("%.2f", this)} MB" } ?: ""
|
||||||
|
|
||||||
|
fun put(context: Context, scope: CoroutineScope, value: String) {
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
context.dataStore.put(DataStoreKeys.NewVersionSize, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fromPreferences(preferences: Preferences) =
|
||||||
|
preferences[DataStoreKeys.NewVersionSize.key] ?: default
|
||||||
|
}
|
|
@ -1,8 +1,53 @@
|
||||||
package me.ash.reader.data.preference
|
package me.ash.reader.data.preference
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import androidx.datastore.preferences.core.Preferences
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
||||||
sealed class Preference {
|
sealed class Preference {
|
||||||
abstract fun put(context: Context, scope: CoroutineScope)
|
abstract fun put(context: Context, scope: CoroutineScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Preferences.toSettings(): Settings {
|
||||||
|
return Settings(
|
||||||
|
newVersionNumber = NewVersionNumberPreference.fromPreferences(this),
|
||||||
|
skipVersionNumber = SkipVersionNumberPreference.fromPreferences(this),
|
||||||
|
newVersionPublishDate = NewVersionPublishDatePreference.fromPreferences(this),
|
||||||
|
newVersionLog = NewVersionLogPreference.fromPreferences(this),
|
||||||
|
newVersionSize = NewVersionSizePreference.fromPreferences(this),
|
||||||
|
newVersionDownloadUrl = NewVersionDownloadUrlPreference.fromPreferences(this),
|
||||||
|
|
||||||
|
themeIndex = ThemeIndexPreference.fromPreferences(this),
|
||||||
|
customPrimaryColor = CustomPrimaryColorPreference.fromPreferences(this),
|
||||||
|
darkTheme = DarkThemePreference.fromPreferences(this),
|
||||||
|
amoledDarkTheme = AmoledDarkThemePreference.fromPreferences(this),
|
||||||
|
|
||||||
|
feedsFilterBarStyle = FeedsFilterBarStylePreference.fromPreferences(this),
|
||||||
|
feedsFilterBarFilled = FeedsFilterBarFilledPreference.fromPreferences(this),
|
||||||
|
feedsFilterBarPadding = FeedsFilterBarPaddingPreference.fromPreferences(this),
|
||||||
|
feedsFilterBarTonalElevation = FeedsFilterBarTonalElevationPreference.fromPreferences(this),
|
||||||
|
feedsTopBarTonalElevation = FeedsTopBarTonalElevationPreference.fromPreferences(this),
|
||||||
|
feedsGroupListExpand = FeedsGroupListExpandPreference.fromPreferences(this),
|
||||||
|
feedsGroupListTonalElevation = FeedsGroupListTonalElevationPreference.fromPreferences(this),
|
||||||
|
|
||||||
|
flowFilterBarStyle = FlowFilterBarStylePreference.fromPreferences(this),
|
||||||
|
flowFilterBarFilled = FlowFilterBarFilledPreference.fromPreferences(this),
|
||||||
|
flowFilterBarPadding = FlowFilterBarPaddingPreference.fromPreferences(this),
|
||||||
|
flowFilterBarTonalElevation = FlowFilterBarTonalElevationPreference.fromPreferences(this),
|
||||||
|
flowTopBarTonalElevation = FlowTopBarTonalElevationPreference.fromPreferences(this),
|
||||||
|
flowArticleListFeedIcon = FlowArticleListFeedIconPreference.fromPreferences(this),
|
||||||
|
flowArticleListFeedName = FlowArticleListFeedNamePreference.fromPreferences(this),
|
||||||
|
flowArticleListImage = FlowArticleListImagePreference.fromPreferences(this),
|
||||||
|
flowArticleListDesc = FlowArticleListDescPreference.fromPreferences(this),
|
||||||
|
flowArticleListTime = FlowArticleListTimePreference.fromPreferences(this),
|
||||||
|
flowArticleListDateStickyHeader = FlowArticleListDateStickyHeaderPreference.fromPreferences(
|
||||||
|
this
|
||||||
|
),
|
||||||
|
flowArticleListTonalElevation = FlowArticleListTonalElevationPreference.fromPreferences(this),
|
||||||
|
|
||||||
|
initialPage = InitialPagePreference.fromPreferences(this),
|
||||||
|
initialFilter = InitialFilterPreference.fromPreferences(this),
|
||||||
|
|
||||||
|
languages = LanguagesPreference.fromPreferences(this),
|
||||||
|
)
|
||||||
}
|
}
|
|
@ -6,12 +6,19 @@ import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.compositionLocalOf
|
import androidx.compose.runtime.compositionLocalOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.datastore.preferences.core.Preferences
|
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
import me.ash.reader.data.model.Version
|
||||||
import me.ash.reader.ui.ext.collectAsStateValue
|
import me.ash.reader.ui.ext.collectAsStateValue
|
||||||
import me.ash.reader.ui.ext.dataStore
|
import me.ash.reader.ui.ext.dataStore
|
||||||
|
|
||||||
data class Settings(
|
data class Settings(
|
||||||
|
val newVersionNumber: Version = NewVersionNumberPreference.default,
|
||||||
|
val skipVersionNumber: Version = SkipVersionNumberPreference.default,
|
||||||
|
val newVersionPublishDate: String = NewVersionPublishDatePreference.default,
|
||||||
|
val newVersionLog: String = NewVersionLogPreference.default,
|
||||||
|
val newVersionSize: String = NewVersionSizePreference.default,
|
||||||
|
val newVersionDownloadUrl: String = NewVersionDownloadUrlPreference.default,
|
||||||
|
|
||||||
val themeIndex: Int = ThemeIndexPreference.default,
|
val themeIndex: Int = ThemeIndexPreference.default,
|
||||||
val customPrimaryColor: String = CustomPrimaryColorPreference.default,
|
val customPrimaryColor: String = CustomPrimaryColorPreference.default,
|
||||||
val darkTheme: DarkThemePreference = DarkThemePreference.default,
|
val darkTheme: DarkThemePreference = DarkThemePreference.default,
|
||||||
|
@ -38,43 +45,12 @@ data class Settings(
|
||||||
val flowArticleListDateStickyHeader: FlowArticleListDateStickyHeaderPreference = FlowArticleListDateStickyHeaderPreference.default,
|
val flowArticleListDateStickyHeader: FlowArticleListDateStickyHeaderPreference = FlowArticleListDateStickyHeaderPreference.default,
|
||||||
val flowArticleListTonalElevation: FlowArticleListTonalElevationPreference = FlowArticleListTonalElevationPreference.default,
|
val flowArticleListTonalElevation: FlowArticleListTonalElevationPreference = FlowArticleListTonalElevationPreference.default,
|
||||||
|
|
||||||
|
val initialPage: InitialPagePreference = InitialPagePreference.default,
|
||||||
|
val initialFilter: InitialFilterPreference = InitialFilterPreference.default,
|
||||||
|
|
||||||
val languages: LanguagesPreference = LanguagesPreference.default,
|
val languages: LanguagesPreference = LanguagesPreference.default,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun Preferences.toSettings(): Settings {
|
|
||||||
return Settings(
|
|
||||||
themeIndex = ThemeIndexPreference.fromPreferences(this),
|
|
||||||
customPrimaryColor = CustomPrimaryColorPreference.fromPreferences(this),
|
|
||||||
darkTheme = DarkThemePreference.fromPreferences(this),
|
|
||||||
amoledDarkTheme = AmoledDarkThemePreference.fromPreferences(this),
|
|
||||||
|
|
||||||
feedsFilterBarStyle = FeedsFilterBarStylePreference.fromPreferences(this),
|
|
||||||
feedsFilterBarFilled = FeedsFilterBarFilledPreference.fromPreferences(this),
|
|
||||||
feedsFilterBarPadding = FeedsFilterBarPaddingPreference.fromPreferences(this),
|
|
||||||
feedsFilterBarTonalElevation = FeedsFilterBarTonalElevationPreference.fromPreferences(this),
|
|
||||||
feedsTopBarTonalElevation = FeedsTopBarTonalElevationPreference.fromPreferences(this),
|
|
||||||
feedsGroupListExpand = FeedsGroupListExpandPreference.fromPreferences(this),
|
|
||||||
feedsGroupListTonalElevation = FeedsGroupListTonalElevationPreference.fromPreferences(this),
|
|
||||||
|
|
||||||
flowFilterBarStyle = FlowFilterBarStylePreference.fromPreferences(this),
|
|
||||||
flowFilterBarFilled = FlowFilterBarFilledPreference.fromPreferences(this),
|
|
||||||
flowFilterBarPadding = FlowFilterBarPaddingPreference.fromPreferences(this),
|
|
||||||
flowFilterBarTonalElevation = FlowFilterBarTonalElevationPreference.fromPreferences(this),
|
|
||||||
flowTopBarTonalElevation = FlowTopBarTonalElevationPreference.fromPreferences(this),
|
|
||||||
flowArticleListFeedIcon = FlowArticleListFeedIconPreference.fromPreferences(this),
|
|
||||||
flowArticleListFeedName = FlowArticleListFeedNamePreference.fromPreferences(this),
|
|
||||||
flowArticleListImage = FlowArticleListImagePreference.fromPreferences(this),
|
|
||||||
flowArticleListDesc = FlowArticleListDescPreference.fromPreferences(this),
|
|
||||||
flowArticleListTime = FlowArticleListTimePreference.fromPreferences(this),
|
|
||||||
flowArticleListDateStickyHeader = FlowArticleListDateStickyHeaderPreference.fromPreferences(
|
|
||||||
this
|
|
||||||
),
|
|
||||||
flowArticleListTonalElevation = FlowArticleListTonalElevationPreference.fromPreferences(this),
|
|
||||||
|
|
||||||
languages = LanguagesPreference.fromPreferences(this),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsProvider(
|
fun SettingsProvider(
|
||||||
content: @Composable () -> Unit,
|
content: @Composable () -> Unit,
|
||||||
|
@ -88,6 +64,13 @@ fun SettingsProvider(
|
||||||
}.collectAsStateValue(initial = Settings())
|
}.collectAsStateValue(initial = Settings())
|
||||||
|
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
|
LocalNewVersionNumber provides settings.newVersionNumber,
|
||||||
|
LocalSkipVersionNumber provides settings.skipVersionNumber,
|
||||||
|
LocalNewVersionPublishDate provides settings.newVersionPublishDate,
|
||||||
|
LocalNewVersionLog provides settings.newVersionLog,
|
||||||
|
LocalNewVersionSize provides settings.newVersionSize,
|
||||||
|
LocalNewVersionDownloadUrl provides settings.newVersionDownloadUrl,
|
||||||
|
|
||||||
LocalThemeIndex provides settings.themeIndex,
|
LocalThemeIndex provides settings.themeIndex,
|
||||||
LocalCustomPrimaryColor provides settings.customPrimaryColor,
|
LocalCustomPrimaryColor provides settings.customPrimaryColor,
|
||||||
LocalDarkTheme provides settings.darkTheme,
|
LocalDarkTheme provides settings.darkTheme,
|
||||||
|
@ -114,12 +97,22 @@ fun SettingsProvider(
|
||||||
LocalFlowFilterBarPadding provides settings.flowFilterBarPadding,
|
LocalFlowFilterBarPadding provides settings.flowFilterBarPadding,
|
||||||
LocalFlowFilterBarTonalElevation provides settings.flowFilterBarTonalElevation,
|
LocalFlowFilterBarTonalElevation provides settings.flowFilterBarTonalElevation,
|
||||||
|
|
||||||
|
LocalInitialPage provides settings.initialPage,
|
||||||
|
LocalInitialFilter provides settings.initialFilter,
|
||||||
|
|
||||||
LocalLanguages provides settings.languages,
|
LocalLanguages provides settings.languages,
|
||||||
) {
|
) {
|
||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val LocalNewVersionNumber = compositionLocalOf { NewVersionNumberPreference.default }
|
||||||
|
val LocalSkipVersionNumber = compositionLocalOf { SkipVersionNumberPreference.default }
|
||||||
|
val LocalNewVersionPublishDate = compositionLocalOf { NewVersionPublishDatePreference.default }
|
||||||
|
val LocalNewVersionLog = compositionLocalOf { NewVersionLogPreference.default }
|
||||||
|
val LocalNewVersionSize = compositionLocalOf { NewVersionSizePreference.default }
|
||||||
|
val LocalNewVersionDownloadUrl = compositionLocalOf { NewVersionDownloadUrlPreference.default }
|
||||||
|
|
||||||
val LocalThemeIndex =
|
val LocalThemeIndex =
|
||||||
compositionLocalOf { ThemeIndexPreference.default }
|
compositionLocalOf { ThemeIndexPreference.default }
|
||||||
val LocalCustomPrimaryColor =
|
val LocalCustomPrimaryColor =
|
||||||
|
@ -169,5 +162,9 @@ val LocalFlowArticleListDateStickyHeader =
|
||||||
val LocalFlowArticleListTonalElevation =
|
val LocalFlowArticleListTonalElevation =
|
||||||
compositionLocalOf<FlowArticleListTonalElevationPreference> { FlowArticleListTonalElevationPreference.default }
|
compositionLocalOf<FlowArticleListTonalElevationPreference> { FlowArticleListTonalElevationPreference.default }
|
||||||
|
|
||||||
|
val LocalInitialPage = compositionLocalOf<InitialPagePreference> { InitialPagePreference.default }
|
||||||
|
val LocalInitialFilter =
|
||||||
|
compositionLocalOf<InitialFilterPreference> { InitialFilterPreference.default }
|
||||||
|
|
||||||
val LocalLanguages =
|
val LocalLanguages =
|
||||||
compositionLocalOf<LanguagesPreference> { LanguagesPreference.default }
|
compositionLocalOf<LanguagesPreference> { LanguagesPreference.default }
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
package me.ash.reader.data.preference
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.datastore.preferences.core.Preferences
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import me.ash.reader.data.model.Version
|
||||||
|
import me.ash.reader.data.model.toVersion
|
||||||
|
import me.ash.reader.ui.ext.DataStoreKeys
|
||||||
|
import me.ash.reader.ui.ext.dataStore
|
||||||
|
import me.ash.reader.ui.ext.put
|
||||||
|
|
||||||
|
object SkipVersionNumberPreference {
|
||||||
|
val default = Version()
|
||||||
|
|
||||||
|
fun put(context: Context, scope: CoroutineScope, value: String) {
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
context.dataStore.put(DataStoreKeys.SkipVersionNumber, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fromPreferences(preferences: Preferences) =
|
||||||
|
preferences[DataStoreKeys.SkipVersionNumber.key].toVersion()
|
||||||
|
}
|
|
@ -2,14 +2,15 @@ package me.ash.reader.data.repository
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.hilt.work.HiltWorker
|
|
||||||
import androidx.paging.PagingSource
|
import androidx.paging.PagingSource
|
||||||
import androidx.work.*
|
import androidx.work.CoroutineWorker
|
||||||
import dagger.assisted.Assisted
|
import androidx.work.ExistingPeriodicWorkPolicy
|
||||||
import dagger.assisted.AssistedInject
|
import androidx.work.ListenableWorker
|
||||||
|
import androidx.work.WorkManager
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
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 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,7 +18,6 @@ import me.ash.reader.data.dao.GroupDao
|
||||||
import me.ash.reader.data.entity.*
|
import me.ash.reader.data.entity.*
|
||||||
import me.ash.reader.ui.ext.currentAccountId
|
import me.ash.reader.ui.ext.currentAccountId
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
abstract class AbstractRssRepository constructor(
|
abstract class AbstractRssRepository constructor(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
|
@ -99,7 +99,7 @@ abstract class AbstractRssRepository constructor(
|
||||||
fun pullImportant(
|
fun pullImportant(
|
||||||
isStarred: Boolean = false,
|
isStarred: Boolean = false,
|
||||||
isUnread: Boolean = false,
|
isUnread: Boolean = false,
|
||||||
): Flow<List<ImportantCount>> {
|
): Flow<Map<String, Int>> {
|
||||||
val accountId = context.currentAccountId
|
val accountId = context.currentAccountId
|
||||||
Log.i(
|
Log.i(
|
||||||
"RLog",
|
"RLog",
|
||||||
|
@ -111,6 +111,12 @@ abstract class AbstractRssRepository constructor(
|
||||||
isUnread -> articleDao
|
isUnread -> articleDao
|
||||||
.queryImportantCountWhenIsUnread(accountId, isUnread)
|
.queryImportantCountWhenIsUnread(accountId, isUnread)
|
||||||
else -> articleDao.queryImportantCountWhenIsAll(accountId)
|
else -> articleDao.queryImportantCountWhenIsAll(accountId)
|
||||||
|
}.mapLatest {
|
||||||
|
mapOf(
|
||||||
|
*(it.map {
|
||||||
|
it.feedId to it.important
|
||||||
|
}.toTypedArray())
|
||||||
|
)
|
||||||
}.flowOn(dispatcherIO)
|
}.flowOn(dispatcherIO)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -130,10 +136,6 @@ abstract class AbstractRssRepository constructor(
|
||||||
return feedDao.queryByLink(context.currentAccountId, url).isNotEmpty()
|
return feedDao.queryByLink(context.currentAccountId, url).isNotEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun peekWork(): String {
|
|
||||||
return workManager.getWorkInfosByTag("sync").get().size.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun updateGroup(group: Group) {
|
suspend fun updateGroup(group: Group) {
|
||||||
groupDao.update(group)
|
groupDao.update(group)
|
||||||
}
|
}
|
||||||
|
@ -207,34 +209,3 @@ abstract class AbstractRssRepository constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@HiltWorker
|
|
||||||
class SyncWorker @AssistedInject constructor(
|
|
||||||
@Assisted context: Context,
|
|
||||||
@Assisted workerParams: WorkerParameters,
|
|
||||||
private val rssRepository: RssRepository,
|
|
||||||
) : CoroutineWorker(context, workerParams) {
|
|
||||||
|
|
||||||
override suspend fun doWork(): Result {
|
|
||||||
Log.i("RLog", "doWork: ")
|
|
||||||
return rssRepository.get().sync(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val WORK_NAME = "article.sync"
|
|
||||||
|
|
||||||
val UUID: UUID
|
|
||||||
|
|
||||||
val repeatingRequest = PeriodicWorkRequestBuilder<SyncWorker>(
|
|
||||||
15, TimeUnit.MINUTES
|
|
||||||
).setConstraints(
|
|
||||||
Constraints.Builder()
|
|
||||||
.build()
|
|
||||||
).addTag(WORK_NAME).build().also {
|
|
||||||
UUID = it.id
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setIsSyncing(boolean: Boolean) = workDataOf("isSyncing" to boolean)
|
|
||||||
fun Data.getIsSyncing(): Boolean = getBoolean("isSyncing", false)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,163 +0,0 @@
|
||||||
//package me.ash.reader.data.repository
|
|
||||||
//
|
|
||||||
//import android.content.Context
|
|
||||||
//import android.util.Log
|
|
||||||
//import androidx.work.WorkManager
|
|
||||||
//import dagger.hilt.android.qualifiers.ApplicationContext
|
|
||||||
//import kotlinx.coroutines.CoroutineDispatcher
|
|
||||||
//import kotlinx.coroutines.CoroutineScope
|
|
||||||
//import kotlinx.coroutines.launch
|
|
||||||
//import kotlinx.coroutines.sync.withLock
|
|
||||||
//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.entity.Article
|
|
||||||
//import me.ash.reader.data.entity.Feed
|
|
||||||
//import me.ash.reader.data.entity.Group
|
|
||||||
//import me.ash.reader.data.module.ApplicationScope
|
|
||||||
//import me.ash.reader.data.module.DispatcherDefault
|
|
||||||
//import me.ash.reader.data.module.DispatcherIO
|
|
||||||
//import me.ash.reader.data.source.FeverApiDataSource
|
|
||||||
//import me.ash.reader.data.source.RssNetworkDataSource
|
|
||||||
//import me.ash.reader.ui.ext.currentAccountId
|
|
||||||
//import me.ash.reader.ui.ext.spacerDollar
|
|
||||||
//import net.dankito.readability4j.extended.Readability4JExtended
|
|
||||||
//import java.util.*
|
|
||||||
//import javax.inject.Inject
|
|
||||||
//import kotlin.collections.set
|
|
||||||
//
|
|
||||||
//class FeverRssRepository @Inject constructor(
|
|
||||||
// @ApplicationContext
|
|
||||||
// private val context: Context,
|
|
||||||
// private val articleDao: ArticleDao,
|
|
||||||
// private val feedDao: FeedDao,
|
|
||||||
// private val groupDao: GroupDao,
|
|
||||||
// private val rssHelper: RssHelper,
|
|
||||||
// private val feverApiDataSource: FeverApiDataSource,
|
|
||||||
// private val accountDao: AccountDao,
|
|
||||||
// rssNetworkDataSource: RssNetworkDataSource,
|
|
||||||
// @ApplicationScope
|
|
||||||
// private val applicationScope: CoroutineScope,
|
|
||||||
// @DispatcherDefault
|
|
||||||
// private val dispatcherDefault: CoroutineDispatcher,
|
|
||||||
// @DispatcherIO
|
|
||||||
// private val dispatcherIO: CoroutineDispatcher,
|
|
||||||
// workManager: WorkManager,
|
|
||||||
//) : AbstractRssRepository(
|
|
||||||
// context, accountDao, articleDao, groupDao,
|
|
||||||
// feedDao, rssNetworkDataSource, workManager,
|
|
||||||
// dispatcherIO
|
|
||||||
//) {
|
|
||||||
// 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 {
|
|
||||||
// return UUID.randomUUID().toString().also {
|
|
||||||
// groupDao.insert(
|
|
||||||
// Group(
|
|
||||||
// id = it,
|
|
||||||
// name = name,
|
|
||||||
// accountId = context.currentAccountId
|
|
||||||
// )
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// override suspend fun sync() {
|
|
||||||
// applicationScope.launch(dispatcherDefault) {
|
|
||||||
// mutex.withLock {
|
|
||||||
// val accountId = context.currentAccountId
|
|
||||||
//
|
|
||||||
// updateSyncState {
|
|
||||||
// it.copy(
|
|
||||||
// feedCount = 1,
|
|
||||||
// syncedCount = 1,
|
|
||||||
// currentFeedName = "Fever"
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// if (feedDao.queryAll(accountId).isNullOrEmpty()) {
|
|
||||||
// // Temporary add feeds
|
|
||||||
// val feverFeeds = feverApiDataSource.feeds().execute().body()!!.feeds
|
|
||||||
// val feverGroupsBody = feverApiDataSource.groups().execute().body()!!
|
|
||||||
// Log.i("RLog", "Fever groups: $feverGroupsBody")
|
|
||||||
// feverGroupsBody.groups.forEach {
|
|
||||||
// groupDao.insert(
|
|
||||||
// Group(
|
|
||||||
// id = accountId.spacerDollar(it.id),
|
|
||||||
// name = it.title,
|
|
||||||
// accountId = accountId,
|
|
||||||
// )
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
// val feverFeedsGroupsMap = mutableMapOf<Int, Int>()
|
|
||||||
// feverGroupsBody.feeds_groups.forEach { item ->
|
|
||||||
// item.feed_ids
|
|
||||||
// .split(",")
|
|
||||||
// .map { it.toInt() }
|
|
||||||
// .forEach { id ->
|
|
||||||
// feverFeedsGroupsMap[id] = item.group_id
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// val feeds = feverFeeds.map {
|
|
||||||
// Feed(
|
|
||||||
// id = accountId.spacerDollar(it.id),
|
|
||||||
// name = it.title,
|
|
||||||
// url = it.url,
|
|
||||||
// groupId = feverFeedsGroupsMap[it.id].toString(),
|
|
||||||
// accountId = accountId
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
// feedDao.insertList(feeds)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Add articles
|
|
||||||
// val articles = mutableListOf<Article>()
|
|
||||||
// feverApiDataSource.itemsBySince(since = 1647444325925621L)
|
|
||||||
// .execute().body()!!.items
|
|
||||||
// .forEach {
|
|
||||||
// articles.add(
|
|
||||||
// Article(
|
|
||||||
// id = accountId.spacerDollar(it.id),
|
|
||||||
// date = Date(it.created_on_time * 1000),
|
|
||||||
// title = it.title,
|
|
||||||
// author = it.author,
|
|
||||||
// rawDescription = it.html,
|
|
||||||
// shortDescription = (
|
|
||||||
// Readability4JExtended("", it.html)
|
|
||||||
// .parse().textContent ?: ""
|
|
||||||
// ).take(100).trim(),
|
|
||||||
// link = it.url,
|
|
||||||
// accountId = accountId,
|
|
||||||
// feedId = it.feed_id.toString(),
|
|
||||||
// isUnread = it.is_read == 0,
|
|
||||||
// isStarred = it.is_saved == 1,
|
|
||||||
// )
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
// articleDao.insertList(articles)
|
|
||||||
//
|
|
||||||
// // Complete sync
|
|
||||||
// accountDao.update(accountDao.queryById(accountId)!!.apply {
|
|
||||||
// updateAt = Date()
|
|
||||||
// })
|
|
||||||
// updateSyncState {
|
|
||||||
// it.copy(
|
|
||||||
// feedCount = 0,
|
|
||||||
// syncedCount = 0,
|
|
||||||
// currentFeedName = ""
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//}
|
|
|
@ -1,12 +1,7 @@
|
||||||
package me.ash.reader.data.repository
|
package me.ash.reader.data.repository
|
||||||
|
|
||||||
import android.app.*
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
|
||||||
import android.graphics.BitmapFactory
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.core.app.NotificationCompat
|
|
||||||
import androidx.core.app.NotificationManagerCompat
|
|
||||||
import androidx.work.CoroutineWorker
|
import androidx.work.CoroutineWorker
|
||||||
import androidx.work.ListenableWorker
|
import androidx.work.ListenableWorker
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
|
@ -15,8 +10,6 @@ import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import me.ash.reader.MainActivity
|
|
||||||
import me.ash.reader.R
|
|
||||||
import me.ash.reader.data.dao.AccountDao
|
import me.ash.reader.data.dao.AccountDao
|
||||||
import me.ash.reader.data.dao.ArticleDao
|
import me.ash.reader.data.dao.ArticleDao
|
||||||
import me.ash.reader.data.dao.FeedDao
|
import me.ash.reader.data.dao.FeedDao
|
||||||
|
@ -30,8 +23,6 @@ import me.ash.reader.data.module.DispatcherIO
|
||||||
import me.ash.reader.data.repository.SyncWorker.Companion.setIsSyncing
|
import me.ash.reader.data.repository.SyncWorker.Companion.setIsSyncing
|
||||||
import me.ash.reader.ui.ext.currentAccountId
|
import me.ash.reader.ui.ext.currentAccountId
|
||||||
import me.ash.reader.ui.ext.spacerDollar
|
import me.ash.reader.ui.ext.spacerDollar
|
||||||
import me.ash.reader.ui.page.common.ExtraName
|
|
||||||
import me.ash.reader.ui.page.common.NotificationGroupName
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ -41,6 +32,7 @@ class LocalRssRepository @Inject constructor(
|
||||||
private val articleDao: ArticleDao,
|
private val articleDao: ArticleDao,
|
||||||
private val feedDao: FeedDao,
|
private val feedDao: FeedDao,
|
||||||
private val rssHelper: RssHelper,
|
private val rssHelper: RssHelper,
|
||||||
|
private val notificationHelper: NotificationHelper,
|
||||||
private val accountDao: AccountDao,
|
private val accountDao: AccountDao,
|
||||||
private val groupDao: GroupDao,
|
private val groupDao: GroupDao,
|
||||||
@DispatcherDefault
|
@DispatcherDefault
|
||||||
|
@ -52,16 +44,6 @@ class LocalRssRepository @Inject constructor(
|
||||||
context, accountDao, articleDao, groupDao,
|
context, accountDao, articleDao, groupDao,
|
||||||
feedDao, workManager, dispatcherIO
|
feedDao, workManager, dispatcherIO
|
||||||
) {
|
) {
|
||||||
private val notificationManager: NotificationManagerCompat =
|
|
||||||
NotificationManagerCompat.from(context).apply {
|
|
||||||
createNotificationChannel(
|
|
||||||
NotificationChannel(
|
|
||||||
NotificationGroupName.ARTICLE_UPDATE,
|
|
||||||
NotificationGroupName.ARTICLE_UPDATE,
|
|
||||||
NotificationManager.IMPORTANCE_DEFAULT
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun updateArticleInfo(article: Article) {
|
override suspend fun updateArticleInfo(article: Article) {
|
||||||
articleDao.update(article)
|
articleDao.update(article)
|
||||||
|
@ -98,7 +80,7 @@ class LocalRssRepository @Inject constructor(
|
||||||
.awaitAll()
|
.awaitAll()
|
||||||
.forEach {
|
.forEach {
|
||||||
if (it.isNotify) {
|
if (it.isNotify) {
|
||||||
notify(
|
notificationHelper.notify(
|
||||||
FeedWithArticle(
|
FeedWithArticle(
|
||||||
it.feedWithArticle.feed,
|
it.feedWithArticle.feed,
|
||||||
articleDao.insertIfNotExist(it.feedWithArticle.articles)
|
articleDao.insertIfNotExist(it.feedWithArticle.articles)
|
||||||
|
@ -183,75 +165,4 @@ class LocalRssRepository @Inject constructor(
|
||||||
isNotify = articles.isNotEmpty() && feed.isNotification
|
isNotify = articles.isNotEmpty() && feed.isNotification
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun notify(
|
|
||||||
feedWithArticle: FeedWithArticle,
|
|
||||||
) {
|
|
||||||
notificationManager.createNotificationChannelGroup(
|
|
||||||
NotificationChannelGroup(
|
|
||||||
feedWithArticle.feed.id,
|
|
||||||
feedWithArticle.feed.name
|
|
||||||
)
|
|
||||||
)
|
|
||||||
feedWithArticle.articles.forEach { article ->
|
|
||||||
val builder = NotificationCompat.Builder(context, NotificationGroupName.ARTICLE_UPDATE)
|
|
||||||
.setSmallIcon(R.drawable.ic_notification)
|
|
||||||
.setLargeIcon(
|
|
||||||
(BitmapFactory.decodeResource(
|
|
||||||
context.resources,
|
|
||||||
R.drawable.ic_notification
|
|
||||||
))
|
|
||||||
)
|
|
||||||
.setContentTitle(article.title)
|
|
||||||
.setContentIntent(
|
|
||||||
PendingIntent.getActivity(
|
|
||||||
context,
|
|
||||||
Random().nextInt() + article.id.hashCode(),
|
|
||||||
Intent(context, MainActivity::class.java).apply {
|
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or
|
|
||||||
Intent.FLAG_ACTIVITY_CLEAR_TASK
|
|
||||||
putExtra(
|
|
||||||
ExtraName.ARTICLE_ID,
|
|
||||||
article.id
|
|
||||||
)
|
|
||||||
},
|
|
||||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.setGroup(feedWithArticle.feed.id)
|
|
||||||
.setStyle(
|
|
||||||
NotificationCompat.BigTextStyle()
|
|
||||||
.bigText(article.shortDescription)
|
|
||||||
.setSummaryText(feedWithArticle.feed.name)
|
|
||||||
)
|
|
||||||
|
|
||||||
notificationManager.notify(
|
|
||||||
Random().nextInt() + article.id.hashCode(),
|
|
||||||
builder.build().apply {
|
|
||||||
flags = Notification.FLAG_AUTO_CANCEL
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (feedWithArticle.articles.size > 1) {
|
|
||||||
notificationManager.notify(
|
|
||||||
Random().nextInt() + feedWithArticle.feed.id.hashCode(),
|
|
||||||
NotificationCompat.Builder(context, NotificationGroupName.ARTICLE_UPDATE)
|
|
||||||
.setSmallIcon(R.drawable.ic_notification)
|
|
||||||
.setLargeIcon(
|
|
||||||
(BitmapFactory.decodeResource(
|
|
||||||
context.resources,
|
|
||||||
R.drawable.ic_notification
|
|
||||||
))
|
|
||||||
)
|
|
||||||
.setStyle(
|
|
||||||
NotificationCompat.InboxStyle()
|
|
||||||
.setSummaryText(feedWithArticle.feed.name)
|
|
||||||
)
|
|
||||||
.setGroup(feedWithArticle.feed.id)
|
|
||||||
.setGroupSummary(true)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -0,0 +1,103 @@
|
||||||
|
package me.ash.reader.data.repository
|
||||||
|
|
||||||
|
import android.app.*
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import me.ash.reader.MainActivity
|
||||||
|
import me.ash.reader.R
|
||||||
|
import me.ash.reader.data.entity.FeedWithArticle
|
||||||
|
import me.ash.reader.ui.page.common.ExtraName
|
||||||
|
import me.ash.reader.ui.page.common.NotificationGroupName
|
||||||
|
import java.util.*
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class NotificationHelper @Inject constructor(
|
||||||
|
@ApplicationContext
|
||||||
|
private val context: Context,
|
||||||
|
) {
|
||||||
|
private val notificationManager: NotificationManagerCompat =
|
||||||
|
NotificationManagerCompat.from(context).apply {
|
||||||
|
createNotificationChannel(
|
||||||
|
NotificationChannel(
|
||||||
|
NotificationGroupName.ARTICLE_UPDATE,
|
||||||
|
NotificationGroupName.ARTICLE_UPDATE,
|
||||||
|
NotificationManager.IMPORTANCE_DEFAULT
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun notify(
|
||||||
|
feedWithArticle: FeedWithArticle,
|
||||||
|
) {
|
||||||
|
notificationManager.createNotificationChannelGroup(
|
||||||
|
NotificationChannelGroup(
|
||||||
|
feedWithArticle.feed.id,
|
||||||
|
feedWithArticle.feed.name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
feedWithArticle.articles.forEach { article ->
|
||||||
|
val builder = NotificationCompat.Builder(context, NotificationGroupName.ARTICLE_UPDATE)
|
||||||
|
.setSmallIcon(R.drawable.ic_notification)
|
||||||
|
.setLargeIcon(
|
||||||
|
(BitmapFactory.decodeResource(
|
||||||
|
context.resources,
|
||||||
|
R.drawable.ic_notification
|
||||||
|
))
|
||||||
|
)
|
||||||
|
.setContentTitle(article.title)
|
||||||
|
.setContentIntent(
|
||||||
|
PendingIntent.getActivity(
|
||||||
|
context,
|
||||||
|
Random().nextInt() + article.id.hashCode(),
|
||||||
|
Intent(context, MainActivity::class.java).apply {
|
||||||
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||||
|
Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
|
putExtra(
|
||||||
|
ExtraName.ARTICLE_ID,
|
||||||
|
article.id
|
||||||
|
)
|
||||||
|
},
|
||||||
|
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.setGroup(feedWithArticle.feed.id)
|
||||||
|
.setStyle(
|
||||||
|
NotificationCompat.BigTextStyle()
|
||||||
|
.bigText(article.shortDescription)
|
||||||
|
.setSummaryText(feedWithArticle.feed.name)
|
||||||
|
)
|
||||||
|
|
||||||
|
notificationManager.notify(
|
||||||
|
Random().nextInt() + article.id.hashCode(),
|
||||||
|
builder.build().apply {
|
||||||
|
flags = Notification.FLAG_AUTO_CANCEL
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (feedWithArticle.articles.size > 1) {
|
||||||
|
notificationManager.notify(
|
||||||
|
Random().nextInt() + feedWithArticle.feed.id.hashCode(),
|
||||||
|
NotificationCompat.Builder(context, NotificationGroupName.ARTICLE_UPDATE)
|
||||||
|
.setSmallIcon(R.drawable.ic_notification)
|
||||||
|
.setLargeIcon(
|
||||||
|
(BitmapFactory.decodeResource(
|
||||||
|
context.resources,
|
||||||
|
R.drawable.ic_notification
|
||||||
|
))
|
||||||
|
)
|
||||||
|
.setStyle(
|
||||||
|
NotificationCompat.InboxStyle()
|
||||||
|
.setSummaryText(feedWithArticle.feed.name)
|
||||||
|
)
|
||||||
|
.setGroup(feedWithArticle.feed.id)
|
||||||
|
.setGroupSummary(true)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -43,7 +43,7 @@ class OpmlRepository @Inject constructor(
|
||||||
repeatList.add(it)
|
repeatList.add(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
feedDao.insertList((groupWithFeed.feeds subtract repeatList).toList())
|
feedDao.insertList((groupWithFeed.feeds subtract repeatList.toSet()).toList())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,7 +54,7 @@ class OpmlRepository @Inject constructor(
|
||||||
Opml(
|
Opml(
|
||||||
"2.0",
|
"2.0",
|
||||||
Head(
|
Head(
|
||||||
accountDao.queryById(context.currentAccountId).name,
|
accountDao.queryById(context.currentAccountId)?.name,
|
||||||
Date().toString(), null, null, null,
|
Date().toString(), null, null, null,
|
||||||
null, null, null, null,
|
null, null, null, null,
|
||||||
null, null, null, null,
|
null, null, null, null,
|
||||||
|
|
|
@ -4,27 +4,28 @@ import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.emptyFlow
|
import kotlinx.coroutines.flow.emptyFlow
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import me.ash.reader.R
|
import me.ash.reader.R
|
||||||
import me.ash.reader.data.entity.toVersion
|
import me.ash.reader.data.model.toVersion
|
||||||
import me.ash.reader.data.module.ApplicationScope
|
|
||||||
import me.ash.reader.data.module.DispatcherIO
|
import me.ash.reader.data.module.DispatcherIO
|
||||||
import me.ash.reader.data.module.DispatcherMain
|
import me.ash.reader.data.module.DispatcherMain
|
||||||
import me.ash.reader.data.source.AppNetworkDataSource
|
import me.ash.reader.data.preference.*
|
||||||
|
import me.ash.reader.data.preference.NewVersionSizePreference.formatSize
|
||||||
import me.ash.reader.data.source.Download
|
import me.ash.reader.data.source.Download
|
||||||
|
import me.ash.reader.data.source.RYNetworkDataSource
|
||||||
import me.ash.reader.data.source.downloadToFileWithProgress
|
import me.ash.reader.data.source.downloadToFileWithProgress
|
||||||
import me.ash.reader.ui.ext.*
|
import me.ash.reader.ui.ext.getCurrentVersion
|
||||||
|
import me.ash.reader.ui.ext.getLatestApk
|
||||||
|
import me.ash.reader.ui.ext.showToast
|
||||||
|
import me.ash.reader.ui.ext.skipVersionNumber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class AppRepository @Inject constructor(
|
class RYRepository @Inject constructor(
|
||||||
@ApplicationContext
|
@ApplicationContext
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val appNetworkDataSource: AppNetworkDataSource,
|
private val RYNetworkDataSource: RYNetworkDataSource,
|
||||||
@ApplicationScope
|
|
||||||
private val applicationScope: CoroutineScope,
|
|
||||||
@DispatcherIO
|
@DispatcherIO
|
||||||
private val dispatcherIO: CoroutineDispatcher,
|
private val dispatcherIO: CoroutineDispatcher,
|
||||||
@DispatcherMain
|
@DispatcherMain
|
||||||
|
@ -33,7 +34,7 @@ class AppRepository @Inject constructor(
|
||||||
suspend fun checkUpdate(showToast: Boolean = true): Boolean? = withContext(dispatcherIO) {
|
suspend fun checkUpdate(showToast: Boolean = true): Boolean? = withContext(dispatcherIO) {
|
||||||
try {
|
try {
|
||||||
val response =
|
val response =
|
||||||
appNetworkDataSource.getReleaseLatest(context.getString(R.string.update_link))
|
RYNetworkDataSource.getReleaseLatest(context.getString(R.string.update_link))
|
||||||
when {
|
when {
|
||||||
response.code() == 403 -> {
|
response.code() == 403 -> {
|
||||||
withContext(dispatcherMain) {
|
withContext(dispatcherMain) {
|
||||||
|
@ -50,31 +51,22 @@ class AppRepository @Inject constructor(
|
||||||
}
|
}
|
||||||
val latest = response.body()!!
|
val latest = response.body()!!
|
||||||
val latestVersion = latest.tag_name.toVersion()
|
val latestVersion = latest.tag_name.toVersion()
|
||||||
// val latestVersion = "0.7.3".toVersion()
|
// val latestVersion = "1.0.0".toVersion()
|
||||||
val skipVersion = context.skipVersionNumber.toVersion()
|
val skipVersion = context.skipVersionNumber.toVersion()
|
||||||
val currentVersion = context.getCurrentVersion()
|
val currentVersion = context.getCurrentVersion()
|
||||||
val latestLog = latest.body ?: ""
|
val latestLog = latest.body ?: ""
|
||||||
val latestPublishDate = latest.published_at ?: latest.created_at ?: ""
|
val latestPublishDate = latest.published_at ?: latest.created_at ?: ""
|
||||||
val latestSize = latest.assets
|
val latestSize = latest.assets?.first()?.size ?: 0
|
||||||
?.first()
|
val latestDownloadUrl = latest.assets?.first()?.browser_download_url ?: ""
|
||||||
?.size
|
|
||||||
?: 0
|
|
||||||
val latestDownloadUrl = latest.assets
|
|
||||||
?.first()
|
|
||||||
?.browser_download_url
|
|
||||||
?: ""
|
|
||||||
|
|
||||||
Log.i("RLog", "current version $currentVersion")
|
Log.i("RLog", "current version $currentVersion")
|
||||||
if (latestVersion.whetherNeedUpdate(currentVersion, skipVersion)) {
|
if (latestVersion.whetherNeedUpdate(currentVersion, skipVersion)) {
|
||||||
Log.i("RLog", "new version $latestVersion")
|
Log.i("RLog", "new version $latestVersion")
|
||||||
context.dataStore.put(
|
NewVersionNumberPreference.put(context, this, latestVersion.toString())
|
||||||
DataStoreKeys.NewVersionNumber,
|
NewVersionLogPreference.put(context, this, latestLog)
|
||||||
latestVersion.toString()
|
NewVersionPublishDatePreference.put(context, this, latestPublishDate)
|
||||||
)
|
NewVersionSizePreference.put(context, this, latestSize.formatSize())
|
||||||
context.dataStore.put(DataStoreKeys.NewVersionLog, latestLog)
|
NewVersionDownloadUrlPreference.put(context, this, latestDownloadUrl)
|
||||||
context.dataStore.put(DataStoreKeys.NewVersionPublishDate, latestPublishDate)
|
|
||||||
context.dataStore.put(DataStoreKeys.NewVersionSize, latestSize)
|
|
||||||
context.dataStore.put(DataStoreKeys.NewVersionDownloadUrl, latestDownloadUrl)
|
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
|
@ -93,7 +85,7 @@ class AppRepository @Inject constructor(
|
||||||
withContext(dispatcherIO) {
|
withContext(dispatcherIO) {
|
||||||
Log.i("RLog", "downloadFile start: $url")
|
Log.i("RLog", "downloadFile start: $url")
|
||||||
try {
|
try {
|
||||||
return@withContext appNetworkDataSource.downloadFile(url)
|
return@withContext RYNetworkDataSource.downloadFile(url)
|
||||||
.downloadToFileWithProgress(context.getLatestApk())
|
.downloadToFileWithProgress(context.getLatestApk())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
|
@ -21,8 +21,6 @@ import net.dankito.readability4j.extended.Readability4JExtended
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.text.ParsePosition
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ -31,6 +29,7 @@ class RssHelper @Inject constructor(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
@DispatcherIO
|
@DispatcherIO
|
||||||
private val dispatcherIO: CoroutineDispatcher,
|
private val dispatcherIO: CoroutineDispatcher,
|
||||||
|
private val okHttpClient: OkHttpClient,
|
||||||
) {
|
) {
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
suspend fun searchFeed(feedLink: String): FeedWithArticle {
|
suspend fun searchFeed(feedLink: String): FeedWithArticle {
|
||||||
|
@ -58,7 +57,7 @@ class RssHelper @Inject constructor(
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
suspend fun parseFullContent(link: String, title: String): String {
|
suspend fun parseFullContent(link: String, title: String): String {
|
||||||
return withContext(dispatcherIO) {
|
return withContext(dispatcherIO) {
|
||||||
val response = OkHttpClient()
|
val response = okHttpClient
|
||||||
.newCall(Request.Builder().url(link).build())
|
.newCall(Request.Builder().url(link).build())
|
||||||
.execute()
|
.execute()
|
||||||
val content = response.body!!.string()
|
val content = response.body!!.string()
|
||||||
|
@ -85,7 +84,12 @@ class RssHelper @Inject constructor(
|
||||||
return withContext(dispatcherIO) {
|
return withContext(dispatcherIO) {
|
||||||
val a = mutableListOf<Article>()
|
val a = mutableListOf<Article>()
|
||||||
val accountId = context.currentAccountId
|
val accountId = context.currentAccountId
|
||||||
val parseRss: SyndFeed = SyndFeedInput().build(XmlReader(URL(feed.url)))
|
val parseRss: SyndFeed = SyndFeedInput().build(
|
||||||
|
XmlReader(URL(feed.url).openConnection().apply {
|
||||||
|
connectTimeout = 5000
|
||||||
|
readTimeout = 5000
|
||||||
|
})
|
||||||
|
)
|
||||||
parseRss.entries.forEach {
|
parseRss.entries.forEach {
|
||||||
if (latestLink != null && latestLink == it.link) return@withContext a
|
if (latestLink != null && latestLink == it.link) return@withContext a
|
||||||
val desc = it.description?.value
|
val desc = it.description?.value
|
||||||
|
@ -110,13 +114,13 @@ class RssHelper @Inject constructor(
|
||||||
date = it.publishedDate ?: it.updatedDate ?: Date(),
|
date = it.publishedDate ?: it.updatedDate ?: Date(),
|
||||||
title = Html.fromHtml(it.title.toString()).toString(),
|
title = Html.fromHtml(it.title.toString()).toString(),
|
||||||
author = it.author,
|
author = it.author,
|
||||||
rawDescription = (desc ?: content) ?: "",
|
rawDescription = (content ?: desc) ?: "",
|
||||||
shortDescription = (Readability4JExtended("", desc ?: content ?: "")
|
shortDescription = (Readability4JExtended("", desc ?: content ?: "")
|
||||||
.parse().textContent ?: "")
|
.parse().textContent ?: "")
|
||||||
.take(100)
|
.take(100)
|
||||||
.trim(),
|
.trim(),
|
||||||
fullContent = content,
|
fullContent = content,
|
||||||
img = findImg((desc ?: content) ?: ""),
|
img = findImg((content ?: desc) ?: ""),
|
||||||
link = it.link ?: "",
|
link = it.link ?: "",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -182,27 +186,4 @@ class RssHelper @Inject constructor(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseDate(
|
|
||||||
inputDate: String, patterns: Array<String> = arrayOf(
|
|
||||||
"yyyy-MM-dd'T'HH:mm:ss'Z'",
|
|
||||||
"yyyy-MM-dd",
|
|
||||||
"yyyy-MM-dd HH:mm:ss",
|
|
||||||
"yyyyMMdd",
|
|
||||||
"yyyy/MM/dd",
|
|
||||||
"yyyy年MM月dd日",
|
|
||||||
"yyyy MM dd",
|
|
||||||
)
|
|
||||||
): Date? {
|
|
||||||
val df = SimpleDateFormat()
|
|
||||||
for (pattern in patterns) {
|
|
||||||
df.applyPattern(pattern)
|
|
||||||
df.isLenient = false
|
|
||||||
val date = df.parse(inputDate, ParsePosition(0))
|
|
||||||
if (date != null) {
|
|
||||||
return date
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -11,6 +11,13 @@ class StringsRepository @Inject constructor(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
) {
|
) {
|
||||||
fun getString(resId: Int, vararg formatArgs: Any) = context.getString(resId, *formatArgs)
|
fun getString(resId: Int, vararg formatArgs: Any) = context.getString(resId, *formatArgs)
|
||||||
fun getQuantityString(resId: Int, quantity: Int, vararg formatArgs: Any) = context.resources.getQuantityString(resId, quantity, *formatArgs)
|
|
||||||
fun formatAsString(date: Date?) = date?.formatAsString(context)
|
fun getQuantityString(resId: Int, quantity: Int, vararg formatArgs: Any) =
|
||||||
|
context.resources.getQuantityString(resId, quantity, *formatArgs)
|
||||||
|
|
||||||
|
fun formatAsString(
|
||||||
|
date: Date?,
|
||||||
|
onlyHourMinute: Boolean? = false,
|
||||||
|
atHourMinute: Boolean? = false
|
||||||
|
) = date?.formatAsString(context, onlyHourMinute, atHourMinute)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
package me.ash.reader.data.repository
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.hilt.work.HiltWorker
|
||||||
|
import androidx.work.*
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import java.util.*
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
@HiltWorker
|
||||||
|
class SyncWorker @AssistedInject constructor(
|
||||||
|
@Assisted context: Context,
|
||||||
|
@Assisted workerParams: WorkerParameters,
|
||||||
|
private val rssRepository: RssRepository,
|
||||||
|
) : CoroutineWorker(context, workerParams) {
|
||||||
|
|
||||||
|
override suspend fun doWork(): Result {
|
||||||
|
Log.i("RLog", "doWork: ")
|
||||||
|
return rssRepository.get().sync(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val WORK_NAME = "article.sync"
|
||||||
|
|
||||||
|
val UUID: UUID
|
||||||
|
|
||||||
|
val repeatingRequest = PeriodicWorkRequestBuilder<SyncWorker>(
|
||||||
|
15, TimeUnit.MINUTES
|
||||||
|
).setConstraints(
|
||||||
|
Constraints.Builder()
|
||||||
|
.build()
|
||||||
|
).addTag(WORK_NAME).build().also {
|
||||||
|
UUID = it.id
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setIsSyncing(boolean: Boolean) = workDataOf("isSyncing" to boolean)
|
||||||
|
fun Data.getIsSyncing(): Boolean = getBoolean("isSyncing", false)
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,21 +18,21 @@ import java.util.*
|
||||||
entities = [Account::class, Feed::class, Article::class, Group::class],
|
entities = [Account::class, Feed::class, Article::class, Group::class],
|
||||||
version = 2,
|
version = 2,
|
||||||
)
|
)
|
||||||
@TypeConverters(ReaderDatabase.Converters::class)
|
@TypeConverters(RYDatabase.Converters::class)
|
||||||
abstract class ReaderDatabase : RoomDatabase() {
|
abstract class RYDatabase : RoomDatabase() {
|
||||||
abstract fun accountDao(): AccountDao
|
abstract fun accountDao(): AccountDao
|
||||||
abstract fun feedDao(): FeedDao
|
abstract fun feedDao(): FeedDao
|
||||||
abstract fun articleDao(): ArticleDao
|
abstract fun articleDao(): ArticleDao
|
||||||
abstract fun groupDao(): GroupDao
|
abstract fun groupDao(): GroupDao
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private var instance: ReaderDatabase? = null
|
private var instance: RYDatabase? = null
|
||||||
|
|
||||||
fun getInstance(context: Context): ReaderDatabase {
|
fun getInstance(context: Context): RYDatabase {
|
||||||
return instance ?: synchronized(this) {
|
return instance ?: synchronized(this) {
|
||||||
instance ?: Room.databaseBuilder(
|
instance ?: Room.databaseBuilder(
|
||||||
context.applicationContext,
|
context.applicationContext,
|
||||||
ReaderDatabase::class.java,
|
RYDatabase::class.java,
|
||||||
"Reader"
|
"Reader"
|
||||||
).addMigrations(*allMigrations).build().also {
|
).addMigrations(*allMigrations).build().also {
|
||||||
instance = it
|
instance = it
|
|
@ -5,7 +5,6 @@ import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.flow.flowOn
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import me.ash.reader.data.entity.LatestRelease
|
|
||||||
import okhttp3.ResponseBody
|
import okhttp3.ResponseBody
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
import retrofit2.Retrofit
|
import retrofit2.Retrofit
|
||||||
|
@ -15,13 +14,7 @@ import retrofit2.http.Streaming
|
||||||
import retrofit2.http.Url
|
import retrofit2.http.Url
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
sealed class Download {
|
interface RYNetworkDataSource {
|
||||||
object NotYet : Download()
|
|
||||||
data class Progress(val percent: Int) : Download()
|
|
||||||
data class Finished(val file: File) : Download()
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AppNetworkDataSource {
|
|
||||||
@GET
|
@GET
|
||||||
suspend fun getReleaseLatest(@Url url: String): Response<LatestRelease>
|
suspend fun getReleaseLatest(@Url url: String): Response<LatestRelease>
|
||||||
|
|
||||||
|
@ -30,14 +23,14 @@ interface AppNetworkDataSource {
|
||||||
suspend fun downloadFile(@Url url: String): ResponseBody
|
suspend fun downloadFile(@Url url: String): ResponseBody
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private var instance: AppNetworkDataSource? = null
|
private var instance: RYNetworkDataSource? = null
|
||||||
|
|
||||||
fun getInstance(): AppNetworkDataSource {
|
fun getInstance(): RYNetworkDataSource {
|
||||||
return instance ?: synchronized(this) {
|
return instance ?: synchronized(this) {
|
||||||
instance ?: Retrofit.Builder()
|
instance ?: Retrofit.Builder()
|
||||||
.baseUrl("https://api.github.com/")
|
.baseUrl("https://api.github.com/")
|
||||||
.addConverterFactory(GsonConverterFactory.create())
|
.addConverterFactory(GsonConverterFactory.create())
|
||||||
.build().create(AppNetworkDataSource::class.java).also {
|
.build().create(RYNetworkDataSource::class.java).also {
|
||||||
instance = it
|
instance = it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -92,4 +85,32 @@ fun ResponseBody.downloadToFileWithProgress(saveFile: File): Flow<Download> =
|
||||||
saveFile.delete()
|
saveFile.delete()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.flowOn(Dispatchers.IO).distinctUntilChanged()
|
}.flowOn(Dispatchers.IO).distinctUntilChanged()
|
||||||
|
|
||||||
|
data class LatestRelease(
|
||||||
|
val html_url: String? = null,
|
||||||
|
val tag_name: String? = null,
|
||||||
|
val name: String? = null,
|
||||||
|
val draft: Boolean? = null,
|
||||||
|
val prerelease: Boolean? = null,
|
||||||
|
val created_at: String? = null,
|
||||||
|
val published_at: String? = null,
|
||||||
|
val assets: List<AssetsItem>? = null,
|
||||||
|
val body: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class AssetsItem(
|
||||||
|
val name: String? = null,
|
||||||
|
val content_type: String? = null,
|
||||||
|
val size: Int? = null,
|
||||||
|
val download_count: Int? = null,
|
||||||
|
val created_at: String? = null,
|
||||||
|
val updated_at: String? = null,
|
||||||
|
val browser_download_url: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
sealed class Download {
|
||||||
|
object NotYet : Download()
|
||||||
|
data class Progress(val percent: Int) : Download()
|
||||||
|
data class Finished(val file: File) : Download()
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package me.ash.reader.ui.page.home
|
package me.ash.reader.ui.component
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
|
@ -1,4 +1,4 @@
|
||||||
package me.ash.reader.ui.page.home
|
package me.ash.reader.ui.component
|
||||||
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.view.SoundEffectConstants
|
import android.view.SoundEffectConstants
|
||||||
|
@ -12,16 +12,15 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalView
|
import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import me.ash.reader.data.entity.Filter
|
import me.ash.reader.data.model.Filter
|
||||||
|
import me.ash.reader.data.model.getName
|
||||||
import me.ash.reader.data.preference.FlowFilterBarStylePreference
|
import me.ash.reader.data.preference.FlowFilterBarStylePreference
|
||||||
import me.ash.reader.data.preference.LocalThemeIndex
|
import me.ash.reader.data.preference.LocalThemeIndex
|
||||||
import me.ash.reader.ui.ext.getName
|
|
||||||
import me.ash.reader.ui.ext.surfaceColorAtElevation
|
import me.ash.reader.ui.ext.surfaceColorAtElevation
|
||||||
import me.ash.reader.ui.theme.palette.onDark
|
import me.ash.reader.ui.theme.palette.onDark
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun FilterBar(
|
fun FilterBar(
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
filter: Filter,
|
filter: Filter,
|
||||||
filterBarStyle: Int,
|
filterBarStyle: Int,
|
||||||
filterBarFilled: Boolean,
|
filterBarFilled: Boolean,
|
||||||
|
@ -39,11 +38,7 @@ fun FilterBar(
|
||||||
tonalElevation = filterBarTonalElevation,
|
tonalElevation = filterBarTonalElevation,
|
||||||
) {
|
) {
|
||||||
Spacer(modifier = Modifier.width(filterBarPadding))
|
Spacer(modifier = Modifier.width(filterBarPadding))
|
||||||
listOf(
|
Filter.values.forEach { item ->
|
||||||
Filter.Starred,
|
|
||||||
Filter.Unread,
|
|
||||||
Filter.All,
|
|
||||||
).forEach { item ->
|
|
||||||
NavigationBarItem(
|
NavigationBarItem(
|
||||||
// modifier = Modifier.height(60.dp),
|
// modifier = Modifier.height(60.dp),
|
||||||
alwaysShowLabel = when (filterBarStyle) {
|
alwaysShowLabel = when (filterBarStyle) {
|
|
@ -1,6 +1,6 @@
|
||||||
package me.ash.reader.ui.component
|
package me.ash.reader.ui.component.base
|
||||||
|
|
||||||
import androidx.compose.animation.*
|
import RYExtensibleVisibility
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.statusBars
|
import androidx.compose.foundation.layout.statusBars
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
@ -38,11 +38,7 @@ fun AnimatedPopup(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
AnimatedVisibility(
|
RYExtensibleVisibility(visible = visible) {
|
||||||
visible = visible,
|
|
||||||
enter = fadeIn() + expandVertically(),
|
|
||||||
exit = fadeOut() + shrinkVertically(),
|
|
||||||
) {
|
|
||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package me.ash.reader.ui.component
|
package me.ash.reader.ui.component.base
|
||||||
|
|
||||||
import androidx.compose.animation.*
|
import androidx.compose.animation.*
|
||||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
import androidx.compose.animation.core.FastOutSlowInEasing
|
|
@ -6,7 +6,7 @@
|
||||||
* @modifier Ashinch
|
* @modifier Ashinch
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.ash.reader.ui.component
|
package me.ash.reader.ui.component.base
|
||||||
|
|
||||||
import android.view.SoundEffectConstants
|
import android.view.SoundEffectConstants
|
||||||
import androidx.compose.animation.Crossfade
|
import androidx.compose.animation.Crossfade
|
|
@ -1,4 +1,4 @@
|
||||||
package me.ash.reader.ui.component
|
package me.ash.reader.ui.component.base
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
|
@ -1,4 +1,4 @@
|
||||||
package me.ash.reader.ui.component
|
package me.ash.reader.ui.component.base
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
@ -7,7 +7,7 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BlockRadioGroupButton(
|
fun BlockRadioButton(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
selected: Int = 0,
|
selected: Int = 0,
|
||||||
onSelected: (Int) -> Unit,
|
onSelected: (Int) -> Unit,
|
|
@ -1,4 +1,4 @@
|
||||||
package me.ash.reader.ui.component
|
package me.ash.reader.ui.component.base
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
|
@ -1,4 +1,4 @@
|
||||||
package me.ash.reader.ui.component
|
package me.ash.reader.ui.component.base
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
|
@ -1,4 +1,4 @@
|
||||||
package me.ash.reader.ui.component
|
package me.ash.reader.ui.component.base
|
||||||
|
|
||||||
import androidx.compose.foundation.horizontalScroll
|
import androidx.compose.foundation.horizontalScroll
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
@ -32,7 +32,7 @@ fun ClipboardTextField(
|
||||||
) {
|
) {
|
||||||
Column(modifier = modifier) {
|
Column(modifier = modifier) {
|
||||||
Spacer(modifier = Modifier.height(10.dp))
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
TextField(
|
RYTextField(
|
||||||
readOnly = readOnly,
|
readOnly = readOnly,
|
||||||
value = value,
|
value = value,
|
||||||
onValueChange = onValueChange,
|
onValueChange = onValueChange,
|
|
@ -1,4 +1,4 @@
|
||||||
package me.ash.reader.ui.component
|
package me.ash.reader.ui.component.base
|
||||||
|
|
||||||
import androidx.compose.foundation.shape.CornerBasedShape
|
import androidx.compose.foundation.shape.CornerBasedShape
|
||||||
import androidx.compose.foundation.shape.CornerSize
|
import androidx.compose.foundation.shape.CornerSize
|
|
@ -1,6 +1,6 @@
|
||||||
package me.ash.reader.ui.component
|
package me.ash.reader.ui.component.base
|
||||||
|
|
||||||
import androidx.compose.animation.*
|
import RYExtensibleVisibility
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
@ -41,11 +41,7 @@ fun DisplayText(
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
AnimatedVisibility(
|
RYExtensibleVisibility(visible = desc.isNotEmpty()) {
|
||||||
visible = desc.isNotEmpty(),
|
|
||||||
enter = fadeIn() + expandVertically(),
|
|
||||||
exit = fadeOut() + shrinkVertically(),
|
|
||||||
) {
|
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.height(16.dp),
|
modifier = Modifier.height(16.dp),
|
||||||
text = desc,
|
text = desc,
|
|
@ -1,4 +1,4 @@
|
||||||
package me.ash.reader.ui.component
|
package me.ash.reader.ui.component.base
|
||||||
|
|
||||||
import android.graphics.drawable.PictureDrawable
|
import android.graphics.drawable.PictureDrawable
|
||||||
import androidx.compose.animation.Crossfade
|
import androidx.compose.animation.Crossfade
|
||||||
|
@ -41,7 +41,7 @@ fun DynamicSVGImage(
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
Crossfade(targetState = pic) {
|
Crossfade(targetState = pic) {
|
||||||
AsyncImage(
|
RYAsyncImage(
|
||||||
contentDescription = contentDescription,
|
contentDescription = contentDescription,
|
||||||
data = it,
|
data = it,
|
||||||
placeholder = null,
|
placeholder = null,
|
|
@ -1,4 +1,4 @@
|
||||||
package me.ash.reader.ui.component
|
package me.ash.reader.ui.component.base
|
||||||
|
|
||||||
import android.view.HapticFeedbackConstants
|
import android.view.HapticFeedbackConstants
|
||||||
import android.view.SoundEffectConstants
|
import android.view.SoundEffectConstants
|
|
@ -1,4 +1,4 @@
|
||||||
package me.ash.reader.ui.component
|
package me.ash.reader.ui.component.base
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
|
@ -1,7 +1,7 @@
|
||||||
package me.ash.reader.ui.component
|
package me.ash.reader.ui.component.base
|
||||||
|
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
@ -10,17 +10,16 @@ import androidx.compose.ui.graphics.DefaultAlpha
|
||||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||||
import androidx.compose.ui.graphics.painter.Painter
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import coil.compose.rememberImagePainter
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import coil.compose.LocalImageLoader
|
|
||||||
import coil.request.ImageRequest
|
|
||||||
import coil.size.Precision
|
import coil.size.Precision
|
||||||
import coil.size.Scale
|
import coil.size.Scale
|
||||||
import coil.size.Size
|
import coil.size.Size
|
||||||
import me.ash.reader.R
|
import me.ash.reader.R
|
||||||
|
|
||||||
|
val SIZE_1000 = Size(1000, 1000)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AsyncImage(
|
fun RYAsyncImage(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
data: Any? = null,
|
data: Any? = null,
|
||||||
size: Size = Size.ORIGINAL,
|
size: Size = Size.ORIGINAL,
|
||||||
|
@ -31,34 +30,51 @@ fun AsyncImage(
|
||||||
@DrawableRes placeholder: Int? = R.drawable.ic_hourglass_empty_black_24dp,
|
@DrawableRes placeholder: Int? = R.drawable.ic_hourglass_empty_black_24dp,
|
||||||
@DrawableRes error: Int? = R.drawable.ic_broken_image_black_24dp,
|
@DrawableRes error: Int? = R.drawable.ic_broken_image_black_24dp,
|
||||||
) {
|
) {
|
||||||
coil.compose.AsyncImage(
|
Image(
|
||||||
modifier = modifier,
|
painter = rememberImagePainter(
|
||||||
model = ImageRequest
|
data = data,
|
||||||
.Builder(LocalContext.current)
|
builder = {
|
||||||
.data(data)
|
if (placeholder != null) placeholder(placeholder)
|
||||||
.crossfade(true)
|
if (error != null) error(error)
|
||||||
.scale(scale)
|
crossfade(true)
|
||||||
.precision(precision)
|
scale(scale)
|
||||||
.size(size)
|
precision(precision)
|
||||||
.build(),
|
size(size)
|
||||||
|
},
|
||||||
|
),
|
||||||
contentDescription = contentDescription,
|
contentDescription = contentDescription,
|
||||||
contentScale = contentScale,
|
contentScale = contentScale,
|
||||||
imageLoader = LocalImageLoader.current,
|
modifier = modifier,
|
||||||
placeholder = placeholder?.run {
|
|
||||||
forwardingPainter(
|
|
||||||
painter = painterResource(this),
|
|
||||||
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurfaceVariant),
|
|
||||||
alpha = 0.1f,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
error = error?.run {
|
|
||||||
forwardingPainter(
|
|
||||||
painter = painterResource(this),
|
|
||||||
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurfaceVariant),
|
|
||||||
alpha = 0.1f,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// coil.compose.AsyncImage(
|
||||||
|
// modifier = modifier,
|
||||||
|
// model = ImageRequest
|
||||||
|
// .Builder(LocalContext.current)
|
||||||
|
// .data(data)
|
||||||
|
// .crossfade(true)
|
||||||
|
// .scale(scale)
|
||||||
|
// .precision(precision)
|
||||||
|
// .size(size)
|
||||||
|
// .build(),
|
||||||
|
// contentDescription = contentDescription,
|
||||||
|
// contentScale = contentScale,
|
||||||
|
// imageLoader = LocalImageLoader.current,
|
||||||
|
// placeholder = placeholder?.run {
|
||||||
|
// forwardingPainter(
|
||||||
|
// painter = painterResource(this),
|
||||||
|
// colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurfaceVariant),
|
||||||
|
// alpha = 0.1f,
|
||||||
|
// )
|
||||||
|
// },
|
||||||
|
// error = error?.run {
|
||||||
|
// forwardingPainter(
|
||||||
|
// painter = painterResource(this),
|
||||||
|
// colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurfaceVariant),
|
||||||
|
// alpha = 0.1f,
|
||||||
|
// )
|
||||||
|
// },
|
||||||
|
// )
|
||||||
}
|
}
|
||||||
|
|
||||||
// From: https://gist.github.com/colinrtwhite/c2966e0b8584b4cdf0a5b05786b20ae1
|
// From: https://gist.github.com/colinrtwhite/c2966e0b8584b4cdf0a5b05786b20ae1
|
|
@ -1,4 +1,4 @@
|
||||||
package me.ash.reader.ui.component
|
package me.ash.reader.ui.component.base
|
||||||
|
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
@ -6,7 +6,7 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Dialog(
|
fun RYDialog(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
visible: Boolean,
|
visible: Boolean,
|
||||||
properties: DialogProperties = DialogProperties(),
|
properties: DialogProperties = DialogProperties(),
|
|
@ -0,0 +1,15 @@
|
||||||
|
import androidx.compose.animation.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RYExtensibleVisibility(
|
||||||
|
visible: Boolean,
|
||||||
|
content: @Composable AnimatedVisibilityScope.() -> Unit
|
||||||
|
) {
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = visible,
|
||||||
|
enter = fadeIn() + expandVertically(),
|
||||||
|
exit = fadeOut() + shrinkVertically(),
|
||||||
|
content = content,
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
package me.ash.reader.ui.component.base
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import me.ash.reader.ui.ext.surfaceColorAtElevation
|
||||||
|
import me.ash.reader.ui.theme.palette.onDark
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun RYScaffold(
|
||||||
|
containerColor: Color = MaterialTheme.colorScheme.surface,
|
||||||
|
topBarTonalElevation: Dp = 0.dp,
|
||||||
|
containerTonalElevation: Dp = 0.dp,
|
||||||
|
navigationIcon: (@Composable () -> Unit)? = null,
|
||||||
|
actions: (@Composable RowScope.() -> Unit)? = null,
|
||||||
|
bottomBar: (@Composable () -> Unit)? = null,
|
||||||
|
floatingActionButton: (@Composable () -> Unit)? = null,
|
||||||
|
content: @Composable () -> Unit = {},
|
||||||
|
) {
|
||||||
|
Scaffold(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(
|
||||||
|
MaterialTheme.colorScheme.surfaceColorAtElevation(
|
||||||
|
topBarTonalElevation,
|
||||||
|
color = containerColor
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.statusBarsPadding(),
|
||||||
|
// .run {
|
||||||
|
// if (bottomBar != null || floatingActionButton != null) {
|
||||||
|
// navigationBarsPadding()
|
||||||
|
// } else {
|
||||||
|
// this
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(
|
||||||
|
containerTonalElevation,
|
||||||
|
color = containerColor
|
||||||
|
) onDark MaterialTheme.colorScheme.surface,
|
||||||
|
topBar = {
|
||||||
|
if (navigationIcon != null || actions != null) {
|
||||||
|
SmallTopAppBar(
|
||||||
|
title = {},
|
||||||
|
colors = TopAppBarDefaults.smallTopAppBarColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(
|
||||||
|
topBarTonalElevation, color = containerColor
|
||||||
|
),
|
||||||
|
),
|
||||||
|
navigationIcon = { navigationIcon?.invoke() },
|
||||||
|
actions = { actions?.invoke(this) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
content = {
|
||||||
|
Column {
|
||||||
|
Spacer(modifier = Modifier.height(it.calculateTopPadding()))
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bottomBar = { bottomBar?.invoke() },
|
||||||
|
floatingActionButton = { floatingActionButton?.invoke() },
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package me.ash.reader.ui.component
|
package me.ash.reader.ui.component.base
|
||||||
|
|
||||||
import androidx.compose.foundation.BorderStroke
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
@ -26,7 +26,7 @@ import me.ash.reader.ui.theme.palette.alwaysLight
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterialApi::class)
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun SelectionChip(
|
fun RYSelectionChip(
|
||||||
content: String,
|
content: String,
|
||||||
selected: Boolean,
|
selected: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
|
@ -6,7 +6,7 @@
|
||||||
* @modifier Ashinch
|
* @modifier Ashinch
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.ash.reader.ui.component
|
package me.ash.reader.ui.component.base
|
||||||
|
|
||||||
import androidx.compose.animation.animateColorAsState
|
import androidx.compose.animation.animateColorAsState
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
|
@ -31,7 +31,7 @@ import me.ash.reader.ui.theme.palette.onDark
|
||||||
|
|
||||||
// TODO: ripple & swipe
|
// TODO: ripple & swipe
|
||||||
@Composable
|
@Composable
|
||||||
fun Switch(
|
fun RYSwitch(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
activated: Boolean,
|
activated: Boolean,
|
||||||
enable: Boolean = true,
|
enable: Boolean = true,
|
||||||
|
@ -101,7 +101,7 @@ fun SwitchHeadline(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Box(Modifier.padding(start = 20.dp)) {
|
Box(Modifier.padding(start = 20.dp)) {
|
||||||
Switch(activated = activated)
|
RYSwitch(activated = activated)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package me.ash.reader.ui.component
|
package me.ash.reader.ui.component.base
|
||||||
|
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
@ -19,7 +19,7 @@ import kotlinx.coroutines.delay
|
||||||
import me.ash.reader.R
|
import me.ash.reader.R
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TextField(
|
fun RYTextField(
|
||||||
readOnly: Boolean,
|
readOnly: Boolean,
|
||||||
value: String,
|
value: String,
|
||||||
onValueChange: (String) -> Unit,
|
onValueChange: (String) -> Unit,
|
||||||
|
@ -39,7 +39,7 @@ fun TextField(
|
||||||
TextField(
|
TextField(
|
||||||
modifier = Modifier.focusRequester(focusRequester),
|
modifier = Modifier.focusRequester(focusRequester),
|
||||||
colors = TextFieldDefaults.textFieldColors(
|
colors = TextFieldDefaults.textFieldColors(
|
||||||
backgroundColor = Color.Transparent,
|
containerColor = Color.Transparent,
|
||||||
),
|
),
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
enabled = !readOnly,
|
enabled = !readOnly,
|
|
@ -1,4 +1,4 @@
|
||||||
package me.ash.reader.ui.component
|
package me.ash.reader.ui.component.base
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
@ -19,9 +19,8 @@ import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.text.style.BaselineShift
|
import androidx.compose.ui.text.style.BaselineShift
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.google.accompanist.pager.ExperimentalPagerApi
|
|
||||||
|
|
||||||
@OptIn(ExperimentalPagerApi::class, ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun RadioDialog(
|
fun RadioDialog(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
@ -30,7 +29,7 @@ fun RadioDialog(
|
||||||
options: List<RadioDialogOption> = emptyList(),
|
options: List<RadioDialogOption> = emptyList(),
|
||||||
onDismissRequest: () -> Unit = {},
|
onDismissRequest: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
Dialog(
|
RYDialog(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
visible = visible,
|
visible = visible,
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
|
@ -1,4 +1,4 @@
|
||||||
package me.ash.reader.ui.component
|
package me.ash.reader.ui.component.base
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
|
@ -1,4 +1,4 @@
|
||||||
package me.ash.reader.ui.component
|
package me.ash.reader.ui.component.base
|
||||||
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
|
@ -1,4 +1,4 @@
|
||||||
package me.ash.reader.ui.component
|
package me.ash.reader.ui.component.base
|
||||||
|
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
@ -13,10 +13,8 @@ import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
import com.google.accompanist.pager.ExperimentalPagerApi
|
|
||||||
import me.ash.reader.R
|
import me.ash.reader.R
|
||||||
|
|
||||||
@OptIn(ExperimentalPagerApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TextFieldDialog(
|
fun TextFieldDialog(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
@ -37,7 +35,7 @@ fun TextFieldDialog(
|
||||||
) {
|
) {
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
|
|
||||||
Dialog(
|
RYDialog(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
visible = visible,
|
visible = visible,
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
|
@ -1,4 +1,4 @@
|
||||||
package me.ash.reader.ui.component
|
package me.ash.reader.ui.component.base
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
|
@ -1,4 +1,4 @@
|
||||||
package me.ash.reader.ui.component
|
package me.ash.reader.ui.component.base
|
||||||
|
|
||||||
import androidx.compose.animation.animateContentSize
|
import androidx.compose.animation.animateContentSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
|
@ -1,4 +1,4 @@
|
||||||
package me.ash.reader.ui.component
|
package me.ash.reader.ui.component.base
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
|
@ -33,7 +33,6 @@ import androidx.compose.foundation.text.selection.DisableSelection
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.ExperimentalComposeApi
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.RectangleShape
|
import androidx.compose.ui.graphics.RectangleShape
|
||||||
|
@ -48,12 +47,11 @@ import androidx.compose.ui.text.style.BaselineShift
|
||||||
import androidx.compose.ui.text.style.TextDecoration
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import coil.annotation.ExperimentalCoilApi
|
|
||||||
import coil.size.Precision
|
import coil.size.Precision
|
||||||
import coil.size.Size
|
import coil.size.Size
|
||||||
import coil.size.pxOrElse
|
import coil.size.pxOrElse
|
||||||
import me.ash.reader.R
|
import me.ash.reader.R
|
||||||
import me.ash.reader.ui.component.AsyncImage
|
import me.ash.reader.ui.component.base.RYAsyncImage
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
import org.jsoup.helper.StringUtil
|
import org.jsoup.helper.StringUtil
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
|
@ -178,7 +176,6 @@ private fun LazyListScope.formatCodeBlock(
|
||||||
composer.terminateCurrentText()
|
composer.terminateCurrentText()
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalComposeApi::class, ExperimentalCoilApi::class)
|
|
||||||
private fun TextComposer.appendTextChildren(
|
private fun TextComposer.appendTextChildren(
|
||||||
nodes: List<Node>,
|
nodes: List<Node>,
|
||||||
preFormatted: Boolean = false,
|
preFormatted: Boolean = false,
|
||||||
|
@ -241,7 +238,7 @@ private fun TextComposer.appendTextChildren(
|
||||||
withComposableStyle(
|
withComposableStyle(
|
||||||
style = { h5Style().toSpanStyle() }
|
style = { h5Style().toSpanStyle() }
|
||||||
) {
|
) {
|
||||||
append(element.text())
|
append("\n${element.text()}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -250,7 +247,7 @@ private fun TextComposer.appendTextChildren(
|
||||||
withComposableStyle(
|
withComposableStyle(
|
||||||
style = { h5Style().toSpanStyle() }
|
style = { h5Style().toSpanStyle() }
|
||||||
) {
|
) {
|
||||||
append(element.text())
|
append("\n${element.text()}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -259,7 +256,7 @@ private fun TextComposer.appendTextChildren(
|
||||||
withComposableStyle(
|
withComposableStyle(
|
||||||
style = { h5Style().toSpanStyle() }
|
style = { h5Style().toSpanStyle() }
|
||||||
) {
|
) {
|
||||||
append(element.text())
|
append("\n${element.text()}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -268,7 +265,7 @@ private fun TextComposer.appendTextChildren(
|
||||||
withComposableStyle(
|
withComposableStyle(
|
||||||
style = { h5Style().toSpanStyle() }
|
style = { h5Style().toSpanStyle() }
|
||||||
) {
|
) {
|
||||||
append(element.text())
|
append("\n${element.text()}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -277,7 +274,7 @@ private fun TextComposer.appendTextChildren(
|
||||||
withComposableStyle(
|
withComposableStyle(
|
||||||
style = { h5Style().toSpanStyle() }
|
style = { h5Style().toSpanStyle() }
|
||||||
) {
|
) {
|
||||||
append(element.text())
|
append("\n${element.text()}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -286,7 +283,7 @@ private fun TextComposer.appendTextChildren(
|
||||||
withComposableStyle(
|
withComposableStyle(
|
||||||
style = { h5Style().toSpanStyle() }
|
style = { h5Style().toSpanStyle() }
|
||||||
) {
|
) {
|
||||||
append(element.text())
|
append("\n${element.text()}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -445,6 +442,7 @@ private fun TextComposer.appendTextChildren(
|
||||||
// .padding(horizontal = PADDING_HORIZONTAL.dp)
|
// .padding(horizontal = PADDING_HORIZONTAL.dp)
|
||||||
.width(MAX_CONTENT_WIDTH.dp)
|
.width(MAX_CONTENT_WIDTH.dp)
|
||||||
) {
|
) {
|
||||||
|
Spacer(modifier = Modifier.height(PADDING_HORIZONTAL.dp))
|
||||||
DisableSelection {
|
DisableSelection {
|
||||||
BoxWithConstraints(
|
BoxWithConstraints(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
@ -468,8 +466,12 @@ private fun TextComposer.appendTextChildren(
|
||||||
// }
|
// }
|
||||||
) {
|
) {
|
||||||
val imageSize = maxImageSize()
|
val imageSize = maxImageSize()
|
||||||
AsyncImage(
|
RYAsyncImage(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = PADDING_HORIZONTAL.dp)
|
||||||
|
.clip(IMAGE_SHAPE)
|
||||||
|
.clickable { },
|
||||||
data = imageCandidates.getBestImageForMaxSize(
|
data = imageCandidates.getBestImageForMaxSize(
|
||||||
pixelDensity = pixelDensity(),
|
pixelDensity = pixelDensity(),
|
||||||
maxSize = imageSize,
|
maxSize = imageSize,
|
||||||
|
@ -594,12 +596,14 @@ private fun TextComposer.appendTextChildren(
|
||||||
BoxWithConstraints(
|
BoxWithConstraints(
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
AsyncImage(
|
RYAsyncImage(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = PADDING_HORIZONTAL.dp)
|
||||||
|
.clip(IMAGE_SHAPE)
|
||||||
.clickable {
|
.clickable {
|
||||||
onLinkClick(video.link)
|
onLinkClick(video.link)
|
||||||
}
|
},
|
||||||
.fillMaxWidth(),
|
|
||||||
data = video.imageUrl,
|
data = video.imageUrl,
|
||||||
size = maxImageSize(),
|
size = maxImageSize(),
|
||||||
contentDescription = stringResource(R.string.touch_to_play_video),
|
contentDescription = stringResource(R.string.touch_to_play_video),
|
||||||
|
@ -646,7 +650,6 @@ private fun TextComposer.appendTextChildren(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalStdlibApi::class)
|
|
||||||
private fun String.asFontFamily(): FontFamily? = when (this.lowercase()) {
|
private fun String.asFontFamily(): FontFamily? = when (this.lowercase()) {
|
||||||
"monospace" -> FontFamily.Monospace
|
"monospace" -> FontFamily.Monospace
|
||||||
"serif" -> FontFamily.Serif
|
"serif" -> FontFamily.Serif
|
||||||
|
|
|
@ -27,7 +27,8 @@ import android.util.Log
|
||||||
import androidx.compose.foundation.lazy.LazyListScope
|
import androidx.compose.foundation.lazy.LazyListScope
|
||||||
import me.ash.reader.R
|
import me.ash.reader.R
|
||||||
|
|
||||||
fun LazyListScope.reader(
|
@Suppress("FunctionName")
|
||||||
|
fun LazyListScope.Reader(
|
||||||
context: Context,
|
context: Context,
|
||||||
link: String,
|
link: String,
|
||||||
content: String,
|
content: String,
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
|
|
||||||
package me.ash.reader.ui.component.reader
|
package me.ash.reader.ui.component.reader
|
||||||
|
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
@ -27,12 +28,14 @@ import androidx.compose.ui.text.SpanStyle
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextDecoration
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import me.ash.reader.ui.ext.alphaLN
|
import me.ash.reader.ui.ext.alphaLN
|
||||||
|
|
||||||
const val PADDING_HORIZONTAL = 24.0
|
const val PADDING_HORIZONTAL = 24.0
|
||||||
const val MAX_CONTENT_WIDTH = 840.0
|
const val MAX_CONTENT_WIDTH = 840.0
|
||||||
|
val IMAGE_SHAPE = RoundedCornerShape(32.dp)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun bodyForeground(): Color =
|
fun bodyForeground(): Color =
|
||||||
|
@ -71,7 +74,7 @@ fun h4Style(): TextStyle =
|
||||||
@Composable
|
@Composable
|
||||||
fun h5Style(): TextStyle =
|
fun h5Style(): TextStyle =
|
||||||
MaterialTheme.typography.headlineSmall.copy(
|
MaterialTheme.typography.headlineSmall.copy(
|
||||||
color = bodyForeground()
|
color = bodyForeground(),
|
||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -83,7 +86,8 @@ fun h6Style(): TextStyle =
|
||||||
@Composable
|
@Composable
|
||||||
fun captionStyle(): TextStyle =
|
fun captionStyle(): TextStyle =
|
||||||
MaterialTheme.typography.bodySmall.copy(
|
MaterialTheme.typography.bodySmall.copy(
|
||||||
color = bodyForeground().copy(alpha = 0.6f)
|
color = bodyForeground().copy(alpha = 0.6f),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
|
@ -79,7 +79,7 @@ class TextComposer(
|
||||||
) -> R
|
) -> R
|
||||||
): R {
|
): R {
|
||||||
val url = link ?: findClosestLink()
|
val url = link ?: findClosestLink()
|
||||||
builder.ensureDoubleNewline()
|
//builder.ensureDoubleNewline()
|
||||||
terminateCurrentText()
|
terminateCurrentText()
|
||||||
val onClick: (() -> Unit)? = if (url?.isNotBlank() == true) {
|
val onClick: (() -> Unit)? = if (url?.isNotBlank() == true) {
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,16 +1,19 @@
|
||||||
package me.ash.reader.ui.ext
|
package me.ash.reader.ui.ext
|
||||||
|
|
||||||
import androidx.compose.material3.ColorScheme
|
import androidx.compose.material3.ColorScheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.compositeOver
|
import androidx.compose.ui.graphics.compositeOver
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import kotlin.math.ln
|
import kotlin.math.ln
|
||||||
|
|
||||||
|
@Composable
|
||||||
fun ColorScheme.surfaceColorAtElevation(
|
fun ColorScheme.surfaceColorAtElevation(
|
||||||
elevation: Dp,
|
elevation: Dp,
|
||||||
color: Color = surface,
|
color: Color = surface,
|
||||||
): Color = color.atElevation(surfaceTint, elevation)
|
): Color = remember(this, elevation, color) { color.atElevation(surfaceTint, elevation) }
|
||||||
|
|
||||||
fun Color.atElevation(
|
fun Color.atElevation(
|
||||||
sourceColor: Color,
|
sourceColor: Color,
|
||||||
|
|
|
@ -4,11 +4,13 @@ import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.ContextWrapper
|
import android.content.ContextWrapper
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import me.ash.reader.data.entity.Version
|
import me.ash.reader.R
|
||||||
import me.ash.reader.data.entity.toVersion
|
import me.ash.reader.data.model.Version
|
||||||
|
import me.ash.reader.data.model.toVersion
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
fun Context.findActivity(): Activity? = when (this) {
|
fun Context.findActivity(): Activity? = when (this) {
|
||||||
|
@ -53,4 +55,19 @@ fun Context.showToast(message: String?, duration: Int = Toast.LENGTH_SHORT) {
|
||||||
|
|
||||||
fun Context.showToastLong(message: String?) {
|
fun Context.showToastLong(message: String?) {
|
||||||
showToast(message, Toast.LENGTH_LONG)
|
showToast(message, Toast.LENGTH_LONG)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.share(content: String) {
|
||||||
|
startActivity(Intent.createChooser(Intent(Intent.ACTION_SEND).apply {
|
||||||
|
putExtra(
|
||||||
|
Intent.EXTRA_TEXT,
|
||||||
|
content,
|
||||||
|
)
|
||||||
|
type = "text/plain"
|
||||||
|
}, getString(R.string.share)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.openURL(url: String?) {
|
||||||
|
url?.takeIf { it.trim().isNotEmpty() }
|
||||||
|
?.let { startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(it))) }
|
||||||
}
|
}
|
|
@ -15,16 +15,6 @@ import java.io.IOException
|
||||||
|
|
||||||
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
|
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
|
||||||
|
|
||||||
val Context.newVersionPublishDate: String
|
|
||||||
get() = this.dataStore.get(DataStoreKeys.NewVersionPublishDate) ?: ""
|
|
||||||
val Context.newVersionLog: String
|
|
||||||
get() = this.dataStore.get(DataStoreKeys.NewVersionLog) ?: ""
|
|
||||||
val Context.newVersionSize: Int
|
|
||||||
get() = this.dataStore.get(DataStoreKeys.NewVersionSize) ?: 0
|
|
||||||
val Context.newVersionDownloadUrl: String
|
|
||||||
get() = this.dataStore.get(DataStoreKeys.NewVersionDownloadUrl) ?: ""
|
|
||||||
val Context.newVersionNumber: String
|
|
||||||
get() = this.dataStore.get(DataStoreKeys.NewVersionNumber) ?: ""
|
|
||||||
val Context.skipVersionNumber: String
|
val Context.skipVersionNumber: String
|
||||||
get() = this.dataStore.get(DataStoreKeys.SkipVersionNumber) ?: ""
|
get() = this.dataStore.get(DataStoreKeys.SkipVersionNumber) ?: ""
|
||||||
val Context.isFirstLaunch: Boolean
|
val Context.isFirstLaunch: Boolean
|
||||||
|
@ -93,9 +83,9 @@ sealed class DataStoreKeys<T> {
|
||||||
get() = stringPreferencesKey("newVersionLog")
|
get() = stringPreferencesKey("newVersionLog")
|
||||||
}
|
}
|
||||||
|
|
||||||
object NewVersionSize : DataStoreKeys<Int>() {
|
object NewVersionSize : DataStoreKeys<String>() {
|
||||||
override val key: Preferences.Key<Int>
|
override val key: Preferences.Key<String>
|
||||||
get() = intPreferencesKey("newVersionSize")
|
get() = stringPreferencesKey("newVersionSizeString")
|
||||||
}
|
}
|
||||||
|
|
||||||
object NewVersionDownloadUrl : DataStoreKeys<String>() {
|
object NewVersionDownloadUrl : DataStoreKeys<String>() {
|
||||||
|
|
|
@ -4,6 +4,7 @@ import android.content.Context
|
||||||
import androidx.core.os.ConfigurationCompat
|
import androidx.core.os.ConfigurationCompat
|
||||||
import me.ash.reader.R
|
import me.ash.reader.R
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
|
import java.text.ParsePosition
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
@ -40,4 +41,27 @@ fun Date.formatAsString(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.parseToDate(
|
||||||
|
patterns: Array<String> = arrayOf(
|
||||||
|
"yyyy-MM-dd'T'HH:mm:ss'Z'",
|
||||||
|
"yyyy-MM-dd",
|
||||||
|
"yyyy-MM-dd HH:mm:ss",
|
||||||
|
"yyyyMMdd",
|
||||||
|
"yyyy/MM/dd",
|
||||||
|
"yyyy年MM月dd日",
|
||||||
|
"yyyy MM dd",
|
||||||
|
)
|
||||||
|
): Date? {
|
||||||
|
val df = SimpleDateFormat()
|
||||||
|
for (pattern in patterns) {
|
||||||
|
df.applyPattern(pattern)
|
||||||
|
df.isLenient = false
|
||||||
|
val date = df.parse(this, ParsePosition(0))
|
||||||
|
if (date != null) {
|
||||||
|
return date
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
}
|
}
|
|
@ -1,13 +0,0 @@
|
||||||
package me.ash.reader.ui.ext
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import me.ash.reader.R
|
|
||||||
import me.ash.reader.data.entity.Filter
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun Filter.getName(): String = when (this) {
|
|
||||||
Filter.Unread -> stringResource(R.string.unread)
|
|
||||||
Filter.Starred -> stringResource(R.string.starred)
|
|
||||||
else -> stringResource(R.string.all)
|
|
||||||
}
|
|
11
app/src/main/java/me/ash/reader/ui/ext/FlavorExt.kt
Normal file
11
app/src/main/java/me/ash/reader/ui/ext/FlavorExt.kt
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
@file:Suppress("SpellCheckingInspection")
|
||||||
|
|
||||||
|
package me.ash.reader.ui.ext
|
||||||
|
|
||||||
|
import me.ash.reader.BuildConfig
|
||||||
|
|
||||||
|
const val GITHUB = "github"
|
||||||
|
const val FDROID = "fdroid"
|
||||||
|
|
||||||
|
const val isFdroid = BuildConfig.FLAVOR == FDROID
|
||||||
|
const val notFdroid = !isFdroid
|
|
@ -1,8 +1,7 @@
|
||||||
package me.ash.reader.ui.ext
|
package me.ash.reader.ui.ext
|
||||||
|
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.paging.compose.LazyPagingItems
|
import androidx.paging.compose.LazyPagingItems
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
|
||||||
|
@ -27,4 +26,34 @@ fun <T : Any> LazyPagingItems<T>.rememberLazyListState(): LazyListState {
|
||||||
// Return rememberLazyListState (normal case).
|
// Return rememberLazyListState (normal case).
|
||||||
else -> androidx.compose.foundation.lazy.rememberLazyListState()
|
else -> androidx.compose.foundation.lazy.rememberLazyListState()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: To be improved
|
||||||
|
*
|
||||||
|
* Returns whether the LazyListState is currently in the
|
||||||
|
* downward scrolling state.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun LazyListState.isScrollDown(): Boolean {
|
||||||
|
var isScrollDown by remember { mutableStateOf(false) }
|
||||||
|
var preItemIndex by remember { mutableStateOf(0) }
|
||||||
|
var preScrollStartOffset by remember { mutableStateOf(0) }
|
||||||
|
|
||||||
|
LaunchedEffect(this) {
|
||||||
|
snapshotFlow { isScrollInProgress }.collect {
|
||||||
|
if (isScrollInProgress) {
|
||||||
|
isScrollDown = when {
|
||||||
|
firstVisibleItemIndex > preItemIndex -> true
|
||||||
|
firstVisibleItemScrollOffset < preItemIndex -> false
|
||||||
|
else -> firstVisibleItemScrollOffset > preScrollStartOffset
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
preItemIndex = firstVisibleItemIndex
|
||||||
|
preScrollStartOffset = firstVisibleItemScrollOffset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return isScrollDown
|
||||||
}
|
}
|
|
@ -52,6 +52,7 @@ import androidx.compose.ui.composed
|
||||||
import androidx.compose.ui.draw.CacheDrawScope
|
import androidx.compose.ui.draw.CacheDrawScope
|
||||||
import androidx.compose.ui.draw.DrawResult
|
import androidx.compose.ui.draw.DrawResult
|
||||||
import androidx.compose.ui.draw.drawWithCache
|
import androidx.compose.ui.draw.drawWithCache
|
||||||
|
import androidx.compose.ui.geometry.CornerRadius
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.geometry.Size
|
import androidx.compose.ui.geometry.Size
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
@ -173,11 +174,15 @@ private fun CacheDrawScope.onDrawScrollbar(
|
||||||
|
|
||||||
return {
|
return {
|
||||||
if (showScrollbar) {
|
if (showScrollbar) {
|
||||||
drawRect(
|
drawRoundRect(
|
||||||
color = color,
|
color = color,
|
||||||
topLeft = topLeft,
|
topLeft = topLeft,
|
||||||
size = size,
|
size = size,
|
||||||
alpha = alpha()
|
alpha = alpha(),
|
||||||
|
cornerRadius = CornerRadius(
|
||||||
|
x = size.width,
|
||||||
|
y = size.width,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -217,7 +222,7 @@ private fun Modifier.drawScrollbar(
|
||||||
val alpha = remember { Animatable(0f) }
|
val alpha = remember { Animatable(0f) }
|
||||||
LaunchedEffect(scrolled, alpha) {
|
LaunchedEffect(scrolled, alpha) {
|
||||||
scrolled.collectLatest {
|
scrolled.collectLatest {
|
||||||
alpha.snapTo(1f)
|
alpha.snapTo(0.3f)
|
||||||
delay(ViewConfiguration.getScrollDefaultDelay().toLong())
|
delay(ViewConfiguration.getScrollDefaultDelay().toLong())
|
||||||
alpha.animateTo(0f, animationSpec = FadeOutAnimationSpec)
|
alpha.animateTo(0f, animationSpec = FadeOutAnimationSpec)
|
||||||
}
|
}
|
||||||
|
@ -231,7 +236,7 @@ private fun Modifier.drawScrollbar(
|
||||||
|
|
||||||
// Calculate thickness here to workaround https://issuetracker.google.com/issues/206972664
|
// Calculate thickness here to workaround https://issuetracker.google.com/issues/206972664
|
||||||
val thickness = with(LocalDensity.current) { Thickness.toPx() }
|
val thickness = with(LocalDensity.current) { Thickness.toPx() }
|
||||||
val color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f)
|
val color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
Modifier
|
Modifier
|
||||||
.nestedScroll(nestedScrollConnection)
|
.nestedScroll(nestedScrollConnection)
|
||||||
.drawWithCache {
|
.drawWithCache {
|
||||||
|
|
|
@ -12,22 +12,21 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import com.google.accompanist.navigation.animation.AnimatedNavHost
|
import com.google.accompanist.navigation.animation.AnimatedNavHost
|
||||||
import com.google.accompanist.navigation.animation.rememberAnimatedNavController
|
import com.google.accompanist.navigation.animation.rememberAnimatedNavController
|
||||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||||
import me.ash.reader.data.entity.Filter
|
import me.ash.reader.data.model.Filter
|
||||||
import me.ash.reader.data.preference.LocalDarkTheme
|
import me.ash.reader.data.preference.LocalDarkTheme
|
||||||
import me.ash.reader.ui.ext.*
|
import me.ash.reader.ui.ext.*
|
||||||
import me.ash.reader.ui.page.home.HomeViewAction
|
|
||||||
import me.ash.reader.ui.page.home.HomeViewModel
|
import me.ash.reader.ui.page.home.HomeViewModel
|
||||||
import me.ash.reader.ui.page.home.feeds.FeedsPage
|
import me.ash.reader.ui.page.home.feeds.FeedsPage
|
||||||
import me.ash.reader.ui.page.home.flow.FlowPage
|
import me.ash.reader.ui.page.home.flow.FlowPage
|
||||||
import me.ash.reader.ui.page.home.read.ReadPage
|
import me.ash.reader.ui.page.home.reading.ReadingPage
|
||||||
import me.ash.reader.ui.page.settings.SettingsPage
|
import me.ash.reader.ui.page.settings.SettingsPage
|
||||||
import me.ash.reader.ui.page.settings.color.ColorAndStyle
|
import me.ash.reader.ui.page.settings.color.ColorAndStylePage
|
||||||
import me.ash.reader.ui.page.settings.color.DarkTheme
|
import me.ash.reader.ui.page.settings.color.DarkThemePage
|
||||||
import me.ash.reader.ui.page.settings.color.feeds.FeedsPageStyle
|
import me.ash.reader.ui.page.settings.color.feeds.FeedsPageStylePage
|
||||||
import me.ash.reader.ui.page.settings.color.flow.FlowPageStyle
|
import me.ash.reader.ui.page.settings.color.flow.FlowPageStylePage
|
||||||
import me.ash.reader.ui.page.settings.interaction.Interaction
|
import me.ash.reader.ui.page.settings.interaction.InteractionPage
|
||||||
import me.ash.reader.ui.page.settings.languages.Languages
|
import me.ash.reader.ui.page.settings.languages.LanguagesPage
|
||||||
import me.ash.reader.ui.page.settings.tips.TipsAndSupport
|
import me.ash.reader.ui.page.settings.tips.TipsAndSupportPage
|
||||||
import me.ash.reader.ui.page.startup.StartupPage
|
import me.ash.reader.ui.page.startup.StartupPage
|
||||||
import me.ash.reader.ui.theme.AppTheme
|
import me.ash.reader.ui.theme.AppTheme
|
||||||
|
|
||||||
|
@ -37,7 +36,7 @@ fun HomeEntry(
|
||||||
homeViewModel: HomeViewModel = hiltViewModel(),
|
homeViewModel: HomeViewModel = hiltViewModel(),
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val filterState = homeViewModel.filterState.collectAsStateValue()
|
val filterUiState = homeViewModel.filterUiState.collectAsStateValue()
|
||||||
val navController = rememberAnimatedNavController()
|
val navController = rememberAnimatedNavController()
|
||||||
|
|
||||||
val intent by rememberSaveable { mutableStateOf(context.findActivity()?.intent) }
|
val intent by rememberSaveable { mutableStateOf(context.findActivity()?.intent) }
|
||||||
|
@ -57,16 +56,14 @@ fun HomeEntry(
|
||||||
// Other initial pages
|
// Other initial pages
|
||||||
}
|
}
|
||||||
|
|
||||||
homeViewModel.dispatch(
|
homeViewModel.changeFilter(
|
||||||
HomeViewAction.ChangeFilter(
|
filterUiState.copy(
|
||||||
filterState.copy(
|
filter = when (context.initialFilter) {
|
||||||
filter = when (context.initialFilter) {
|
0 -> Filter.Starred
|
||||||
0 -> Filter.Starred
|
1 -> Filter.Unread
|
||||||
1 -> Filter.Unread
|
2 -> Filter.All
|
||||||
2 -> Filter.All
|
else -> Filter.All
|
||||||
else -> Filter.All
|
}
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -114,7 +111,7 @@ fun HomeEntry(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
animatedComposable(route = "${RouteName.READING}/{articleId}") {
|
animatedComposable(route = "${RouteName.READING}/{articleId}") {
|
||||||
ReadPage(navController = navController)
|
ReadingPage(navController = navController)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
|
@ -124,31 +121,31 @@ fun HomeEntry(
|
||||||
|
|
||||||
// Color & Style
|
// Color & Style
|
||||||
animatedComposable(route = RouteName.COLOR_AND_STYLE) {
|
animatedComposable(route = RouteName.COLOR_AND_STYLE) {
|
||||||
ColorAndStyle(navController)
|
ColorAndStylePage(navController)
|
||||||
}
|
}
|
||||||
animatedComposable(route = RouteName.DARK_THEME) {
|
animatedComposable(route = RouteName.DARK_THEME) {
|
||||||
DarkTheme(navController)
|
DarkThemePage(navController)
|
||||||
}
|
}
|
||||||
animatedComposable(route = RouteName.FEEDS_PAGE_STYLE) {
|
animatedComposable(route = RouteName.FEEDS_PAGE_STYLE) {
|
||||||
FeedsPageStyle(navController)
|
FeedsPageStylePage(navController)
|
||||||
}
|
}
|
||||||
animatedComposable(route = RouteName.FLOW_PAGE_STYLE) {
|
animatedComposable(route = RouteName.FLOW_PAGE_STYLE) {
|
||||||
FlowPageStyle(navController)
|
FlowPageStylePage(navController)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interaction
|
// Interaction
|
||||||
animatedComposable(route = RouteName.INTERACTION) {
|
animatedComposable(route = RouteName.INTERACTION) {
|
||||||
Interaction(navController)
|
InteractionPage(navController)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Languages
|
// Languages
|
||||||
animatedComposable(route = RouteName.LANGUAGES) {
|
animatedComposable(route = RouteName.LANGUAGES) {
|
||||||
Languages(navController = navController)
|
LanguagesPage(navController = navController)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tips & Support
|
// Tips & Support
|
||||||
animatedComposable(route = RouteName.TIPS_AND_SUPPORT) {
|
animatedComposable(route = RouteName.TIPS_AND_SUPPORT) {
|
||||||
TipsAndSupport(navController)
|
TipsAndSupportPage(navController)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,8 +7,8 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import me.ash.reader.data.entity.Feed
|
import me.ash.reader.data.entity.Feed
|
||||||
import me.ash.reader.data.entity.Filter
|
|
||||||
import me.ash.reader.data.entity.Group
|
import me.ash.reader.data.entity.Group
|
||||||
|
import me.ash.reader.data.model.Filter
|
||||||
import me.ash.reader.data.module.ApplicationScope
|
import me.ash.reader.data.module.ApplicationScope
|
||||||
import me.ash.reader.data.repository.RssRepository
|
import me.ash.reader.data.repository.RssRepository
|
||||||
import me.ash.reader.data.repository.StringsRepository
|
import me.ash.reader.data.repository.StringsRepository
|
||||||
|
@ -24,30 +24,20 @@ class HomeViewModel @Inject constructor(
|
||||||
private val applicationScope: CoroutineScope,
|
private val applicationScope: CoroutineScope,
|
||||||
private val workManager: WorkManager,
|
private val workManager: WorkManager,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
private val _homeUiState = MutableStateFlow(HomeUiState())
|
||||||
|
val homeUiState: StateFlow<HomeUiState> = _homeUiState.asStateFlow()
|
||||||
|
|
||||||
private val _viewState = MutableStateFlow(HomeViewState())
|
private val _filterUiState = MutableStateFlow(FilterState())
|
||||||
val viewState: StateFlow<HomeViewState> = _viewState.asStateFlow()
|
val filterUiState = _filterUiState.asStateFlow()
|
||||||
|
|
||||||
private val _filterState = MutableStateFlow(FilterState())
|
|
||||||
val filterState = _filterState.asStateFlow()
|
|
||||||
|
|
||||||
val syncWorkLiveData = workManager.getWorkInfoByIdLiveData(SyncWorker.UUID)
|
val syncWorkLiveData = workManager.getWorkInfoByIdLiveData(SyncWorker.UUID)
|
||||||
|
|
||||||
fun dispatch(action: HomeViewAction) {
|
fun sync() {
|
||||||
when (action) {
|
|
||||||
is HomeViewAction.Sync -> sync()
|
|
||||||
is HomeViewAction.ChangeFilter -> changeFilter(action.filterState)
|
|
||||||
is HomeViewAction.FetchArticles -> fetchArticles()
|
|
||||||
is HomeViewAction.InputSearchContent -> inputSearchContent(action.content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun sync() {
|
|
||||||
rssRepository.get().doSync()
|
rssRepository.get().doSync()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun changeFilter(filterState: FilterState) {
|
fun changeFilter(filterState: FilterState) {
|
||||||
_filterState.update {
|
_filterUiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
group = filterState.group,
|
group = filterState.group,
|
||||||
feed = filterState.feed,
|
feed = filterState.feed,
|
||||||
|
@ -57,28 +47,40 @@ class HomeViewModel @Inject constructor(
|
||||||
fetchArticles()
|
fetchArticles()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fetchArticles() {
|
fun fetchArticles() {
|
||||||
_viewState.update {
|
_homeUiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
pagingData = Pager(PagingConfig(pageSize = 50)) {
|
pagingData = Pager(
|
||||||
if (_viewState.value.searchContent.isNotBlank()) {
|
config = PagingConfig(
|
||||||
|
pageSize = 100,
|
||||||
|
enablePlaceholders = false,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
if (_homeUiState.value.searchContent.isNotBlank()) {
|
||||||
rssRepository.get().searchArticles(
|
rssRepository.get().searchArticles(
|
||||||
content = _viewState.value.searchContent.trim(),
|
content = _homeUiState.value.searchContent.trim(),
|
||||||
groupId = _filterState.value.group?.id,
|
groupId = _filterUiState.value.group?.id,
|
||||||
feedId = _filterState.value.feed?.id,
|
feedId = _filterUiState.value.feed?.id,
|
||||||
isStarred = _filterState.value.filter.isStarred(),
|
isStarred = _filterUiState.value.filter.isStarred(),
|
||||||
isUnread = _filterState.value.filter.isUnread(),
|
isUnread = _filterUiState.value.filter.isUnread(),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
rssRepository.get().pullArticles(
|
rssRepository.get().pullArticles(
|
||||||
groupId = _filterState.value.group?.id,
|
groupId = _filterUiState.value.group?.id,
|
||||||
feedId = _filterState.value.feed?.id,
|
feedId = _filterUiState.value.feed?.id,
|
||||||
isStarred = _filterState.value.filter.isStarred(),
|
isStarred = _filterUiState.value.filter.isStarred(),
|
||||||
isUnread = _filterState.value.filter.isUnread(),
|
isUnread = _filterUiState.value.filter.isUnread(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}.flow.map {
|
}.flow.map {
|
||||||
it.map { FlowItemView.Article(it) }.insertSeparators { before, after ->
|
it.map {
|
||||||
|
FlowItemView.Article(it.apply {
|
||||||
|
article.dateString = stringsRepository.formatAsString(
|
||||||
|
date = article.date,
|
||||||
|
onlyHourMinute = true
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}.insertSeparators { before, after ->
|
||||||
val beforeDate =
|
val beforeDate =
|
||||||
stringsRepository.formatAsString(before?.articleWithFeed?.article?.date)
|
stringsRepository.formatAsString(before?.articleWithFeed?.article?.date)
|
||||||
val afterDate =
|
val afterDate =
|
||||||
|
@ -94,8 +96,8 @@ class HomeViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun inputSearchContent(content: String) {
|
fun inputSearchContent(content: String) {
|
||||||
_viewState.update {
|
_homeUiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
searchContent = content,
|
searchContent = content,
|
||||||
)
|
)
|
||||||
|
@ -110,21 +112,7 @@ data class FilterState(
|
||||||
val filter: Filter = Filter.All,
|
val filter: Filter = Filter.All,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class HomeViewState(
|
data class HomeUiState(
|
||||||
val pagingData: Flow<PagingData<FlowItemView>> = emptyFlow(),
|
val pagingData: Flow<PagingData<FlowItemView>> = emptyFlow(),
|
||||||
val searchContent: String = "",
|
val searchContent: String = "",
|
||||||
)
|
)
|
||||||
|
|
||||||
sealed class HomeViewAction {
|
|
||||||
object Sync : HomeViewAction()
|
|
||||||
|
|
||||||
data class ChangeFilter(
|
|
||||||
val filterState: FilterState
|
|
||||||
) : HomeViewAction()
|
|
||||||
|
|
||||||
object FetchArticles : HomeViewAction()
|
|
||||||
|
|
||||||
data class InputSearchContent(
|
|
||||||
val content: String,
|
|
||||||
) : HomeViewAction()
|
|
||||||
}
|
|
|
@ -1,96 +1,99 @@
|
||||||
package me.ash.reader.ui.page.home.feeds
|
package me.ash.reader.ui.page.home.feeds
|
||||||
|
|
||||||
|
import RYExtensibleVisibility
|
||||||
import android.view.HapticFeedbackConstants
|
import android.view.HapticFeedbackConstants
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material3.Badge
|
import androidx.compose.material3.Badge
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.graphics.RectangleShape
|
||||||
import androidx.compose.ui.platform.LocalView
|
import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.Dp
|
|
||||||
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 me.ash.reader.data.entity.Feed
|
import me.ash.reader.data.entity.Feed
|
||||||
import me.ash.reader.ui.page.home.FeedIcon
|
import me.ash.reader.ui.component.FeedIcon
|
||||||
import me.ash.reader.ui.page.home.feeds.option.feed.FeedOptionViewAction
|
import me.ash.reader.ui.page.home.feeds.drawer.feed.FeedOptionViewModel
|
||||||
import me.ash.reader.ui.page.home.feeds.option.feed.FeedOptionViewModel
|
import me.ash.reader.ui.theme.ShapeBottom32
|
||||||
import kotlin.math.ln
|
|
||||||
|
|
||||||
@OptIn(
|
@OptIn(
|
||||||
androidx.compose.foundation.ExperimentalFoundationApi::class,
|
androidx.compose.foundation.ExperimentalFoundationApi::class,
|
||||||
androidx.compose.material.ExperimentalMaterialApi::class,
|
|
||||||
)
|
)
|
||||||
@Composable
|
@Composable
|
||||||
fun FeedItem(
|
fun FeedItem(
|
||||||
feed: Feed,
|
feed: Feed,
|
||||||
|
alpha: Float = 1f,
|
||||||
|
badgeAlpha: Float = 1f,
|
||||||
|
isEnded: Boolean = false,
|
||||||
|
isExpanded: () -> Boolean,
|
||||||
feedOptionViewModel: FeedOptionViewModel = hiltViewModel(),
|
feedOptionViewModel: FeedOptionViewModel = hiltViewModel(),
|
||||||
tonalElevation: Dp,
|
|
||||||
onClick: () -> Unit = {},
|
onClick: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val view = LocalView.current
|
val view = LocalView.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val tonalElevationAlpha by remember {
|
|
||||||
derivedStateOf {
|
|
||||||
(ln(tonalElevation.value + 1.4f) + 2f) / 100f
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row(
|
RYExtensibleVisibility(visible = isExpanded()) {
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 14.dp)
|
|
||||||
.clip(RoundedCornerShape(32.dp))
|
|
||||||
.combinedClickable(
|
|
||||||
onClick = {
|
|
||||||
onClick()
|
|
||||||
},
|
|
||||||
onLongClick = {
|
|
||||||
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
|
||||||
feedOptionViewModel.dispatch(FeedOptionViewAction.Show(scope, feed.id))
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.padding(vertical = 14.dp),
|
|
||||||
) {
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(start = 14.dp, end = 6.dp),
|
.padding(horizontal = 16.dp)
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
.background(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
color = MaterialTheme.colorScheme.secondary.copy(alpha = alpha),
|
||||||
) {
|
shape = if (isEnded) ShapeBottom32 else RectangleShape,
|
||||||
Row(modifier = Modifier.weight(1f)) {
|
|
||||||
FeedIcon(feed.name)
|
|
||||||
Text(
|
|
||||||
modifier = Modifier.padding(start = 12.dp, end = 6.dp),
|
|
||||||
text = feed.name,
|
|
||||||
style = MaterialTheme.typography.labelLarge,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
)
|
)
|
||||||
}
|
.combinedClickable(
|
||||||
if ((feed.important ?: 0) != 0) {
|
onClick = {
|
||||||
Badge(
|
onClick()
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceTint.copy(
|
|
||||||
alpha = tonalElevationAlpha
|
|
||||||
),
|
|
||||||
contentColor = MaterialTheme.colorScheme.outline,
|
|
||||||
content = {
|
|
||||||
Text(
|
|
||||||
text = feed.important.toString(),
|
|
||||||
style = MaterialTheme.typography.labelSmall
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
|
onLongClick = {
|
||||||
|
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
||||||
|
feedOptionViewModel.showDrawer(scope, feed.id)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
.padding(horizontal = 14.dp)
|
||||||
|
.padding(top = 14.dp, bottom = if (isEnded) 22.dp else 14.dp),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(start = 14.dp, end = 6.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Row(modifier = Modifier.weight(1f)) {
|
||||||
|
FeedIcon(feed.name)
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.padding(start = 12.dp, end = 6.dp),
|
||||||
|
text = feed.name,
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if ((feed.important ?: 0) != 0) {
|
||||||
|
Badge(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceTint.copy(
|
||||||
|
alpha = badgeAlpha
|
||||||
|
),
|
||||||
|
contentColor = MaterialTheme.colorScheme.outline,
|
||||||
|
content = {
|
||||||
|
Text(
|
||||||
|
text = feed.important.toString(),
|
||||||
|
style = MaterialTheme.typography.labelSmall
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
package me.ash.reader.ui.page.home.feeds
|
package me.ash.reader.ui.page.home.feeds
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.animation.core.*
|
import androidx.compose.animation.core.*
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
@ -15,7 +13,8 @@ import androidx.compose.material.icons.outlined.KeyboardArrowRight
|
||||||
import androidx.compose.material.icons.outlined.Settings
|
import androidx.compose.material.icons.outlined.Settings
|
||||||
import androidx.compose.material.icons.rounded.Add
|
import androidx.compose.material.icons.rounded.Add
|
||||||
import androidx.compose.material.icons.rounded.Refresh
|
import androidx.compose.material.icons.rounded.Refresh
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.rotate
|
import androidx.compose.ui.draw.rotate
|
||||||
|
@ -26,31 +25,26 @@ 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 kotlinx.coroutines.flow.map
|
|
||||||
import me.ash.reader.R
|
import me.ash.reader.R
|
||||||
import me.ash.reader.data.entity.toVersion
|
import me.ash.reader.data.model.getName
|
||||||
import me.ash.reader.data.preference.*
|
import me.ash.reader.data.preference.*
|
||||||
import me.ash.reader.data.repository.SyncWorker.Companion.getIsSyncing
|
import me.ash.reader.data.repository.SyncWorker.Companion.getIsSyncing
|
||||||
import me.ash.reader.ui.component.Banner
|
import me.ash.reader.ui.component.FilterBar
|
||||||
import me.ash.reader.ui.component.DisplayText
|
import me.ash.reader.ui.component.base.*
|
||||||
import me.ash.reader.ui.component.FeedbackIconButton
|
import me.ash.reader.ui.ext.alphaLN
|
||||||
import me.ash.reader.ui.component.Subtitle
|
import me.ash.reader.ui.ext.collectAsStateValue
|
||||||
import me.ash.reader.ui.ext.*
|
import me.ash.reader.ui.ext.findActivity
|
||||||
|
import me.ash.reader.ui.ext.getCurrentVersion
|
||||||
import me.ash.reader.ui.page.common.RouteName
|
import me.ash.reader.ui.page.common.RouteName
|
||||||
import me.ash.reader.ui.page.home.FilterBar
|
|
||||||
import me.ash.reader.ui.page.home.FilterState
|
import me.ash.reader.ui.page.home.FilterState
|
||||||
import me.ash.reader.ui.page.home.HomeViewAction
|
|
||||||
import me.ash.reader.ui.page.home.HomeViewModel
|
import me.ash.reader.ui.page.home.HomeViewModel
|
||||||
import me.ash.reader.ui.page.home.feeds.option.feed.FeedOptionDrawer
|
import me.ash.reader.ui.page.home.feeds.drawer.feed.FeedOptionDrawer
|
||||||
import me.ash.reader.ui.page.home.feeds.option.group.GroupOptionDrawer
|
import me.ash.reader.ui.page.home.feeds.drawer.group.GroupOptionDrawer
|
||||||
import me.ash.reader.ui.page.home.feeds.subscribe.SubscribeDialog
|
import me.ash.reader.ui.page.home.feeds.subscribe.SubscribeDialog
|
||||||
import me.ash.reader.ui.page.home.feeds.subscribe.SubscribeViewAction
|
|
||||||
import me.ash.reader.ui.page.home.feeds.subscribe.SubscribeViewModel
|
import me.ash.reader.ui.page.home.feeds.subscribe.SubscribeViewModel
|
||||||
import me.ash.reader.ui.theme.palette.onDark
|
import kotlin.math.ln
|
||||||
|
|
||||||
@SuppressLint("FlowOperatorInvokedInComposition")
|
|
||||||
@OptIn(
|
@OptIn(
|
||||||
ExperimentalMaterial3Api::class, com.google.accompanist.pager.ExperimentalPagerApi::class,
|
|
||||||
androidx.compose.foundation.ExperimentalFoundationApi::class
|
androidx.compose.foundation.ExperimentalFoundationApi::class
|
||||||
)
|
)
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -69,20 +63,12 @@ fun FeedsPage(
|
||||||
val filterBarPadding = LocalFeedsFilterBarPadding.current
|
val filterBarPadding = LocalFeedsFilterBarPadding.current
|
||||||
val filterBarTonalElevation = LocalFeedsFilterBarTonalElevation.current
|
val filterBarTonalElevation = LocalFeedsFilterBarTonalElevation.current
|
||||||
|
|
||||||
val feedsViewState = feedsViewModel.viewState.collectAsStateValue()
|
val feedsUiState = feedsViewModel.feedsUiState.collectAsStateValue()
|
||||||
val filterState = homeViewModel.filterState.collectAsStateValue()
|
val filterUiState = homeViewModel.filterUiState.collectAsStateValue()
|
||||||
|
|
||||||
val skipVersion = context.dataStore.data
|
val newVersion = LocalNewVersionNumber.current
|
||||||
.map { it[DataStoreKeys.SkipVersionNumber.key] ?: "" }
|
val skipVersion = LocalSkipVersionNumber.current
|
||||||
.collectAsState(initial = "")
|
val currentVersion = remember { context.getCurrentVersion() }
|
||||||
.value
|
|
||||||
.toVersion()
|
|
||||||
val latestVersion = context.dataStore.data
|
|
||||||
.map { it[DataStoreKeys.NewVersionNumber.key] ?: "" }
|
|
||||||
.collectAsState(initial = "")
|
|
||||||
.value
|
|
||||||
.toVersion()
|
|
||||||
val currentVersion by remember { mutableStateOf(context.getCurrentVersion()) }
|
|
||||||
|
|
||||||
val owner = LocalLifecycleOwner.current
|
val owner = LocalLifecycleOwner.current
|
||||||
var isSyncing by remember { mutableStateOf(false) }
|
var isSyncing by remember { mutableStateOf(false) }
|
||||||
|
@ -102,22 +88,40 @@ fun FeedsPage(
|
||||||
val launcher = rememberLauncherForActivityResult(
|
val launcher = rememberLauncherForActivityResult(
|
||||||
ActivityResultContracts.CreateDocument()
|
ActivityResultContracts.CreateDocument()
|
||||||
) { result ->
|
) { result ->
|
||||||
feedsViewModel.dispatch(FeedsViewAction.ExportAsString { string ->
|
feedsViewModel.exportAsOpml { string ->
|
||||||
result?.let { uri ->
|
result?.let { uri ->
|
||||||
context.contentResolver.openOutputStream(uri)?.let { outputStream ->
|
context.contentResolver.openOutputStream(uri)?.use { outputStream ->
|
||||||
outputStream.write(string.toByteArray())
|
outputStream.write(string.toByteArray())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val feedBadgeAlpha by remember { derivedStateOf { (ln(groupListTonalElevation.value + 1.4f) + 2f) / 100f } }
|
||||||
|
val groupAlpha by remember { derivedStateOf { groupListTonalElevation.value.dp.alphaLN(weight = 1.2f) } }
|
||||||
|
val groupIndicatorAlpha by remember {
|
||||||
|
derivedStateOf {
|
||||||
|
groupListTonalElevation.value.dp.alphaLN(
|
||||||
|
weight = 1.4f
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val groupsVisible = remember(feedsUiState.groupWithFeedList) {
|
||||||
|
mutableStateMapOf(
|
||||||
|
*(feedsUiState.groupWithFeedList.filterIsInstance<GroupFeedsView.Group>().map {
|
||||||
|
it.group.id to groupListExpand.value
|
||||||
|
}.toTypedArray())
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
feedsViewModel.dispatch(FeedsViewAction.FetchAccount)
|
feedsViewModel.fetchAccount()
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(filterState) {
|
LaunchedEffect(filterUiState) {
|
||||||
snapshotFlow { filterState }.collect {
|
snapshotFlow { filterUiState }.collect {
|
||||||
feedsViewModel.dispatch(FeedsViewAction.FetchData(it))
|
feedsViewModel.fetchData(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -125,52 +129,38 @@ fun FeedsPage(
|
||||||
context.findActivity()?.moveTaskToBack(false)
|
context.findActivity()?.moveTaskToBack(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
RYScaffold(
|
||||||
modifier = Modifier
|
topBarTonalElevation = topBarTonalElevation.value.dp,
|
||||||
.background(MaterialTheme.colorScheme.surfaceColorAtElevation(topBarTonalElevation.value.dp))
|
containerTonalElevation = groupListTonalElevation.value.dp,
|
||||||
.statusBarsPadding(),
|
navigationIcon = {
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(
|
FeedbackIconButton(
|
||||||
groupListTonalElevation.value.dp
|
modifier = Modifier.size(20.dp),
|
||||||
) onDark MaterialTheme.colorScheme.surface,
|
imageVector = Icons.Outlined.Settings,
|
||||||
topBar = {
|
contentDescription = stringResource(R.string.settings),
|
||||||
SmallTopAppBar(
|
tint = MaterialTheme.colorScheme.onSurface,
|
||||||
colors = TopAppBarDefaults.smallTopAppBarColors(
|
showBadge = newVersion.whetherNeedUpdate(currentVersion, skipVersion),
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(
|
) {
|
||||||
topBarTonalElevation.value.dp
|
navController.navigate(RouteName.SETTINGS) {
|
||||||
),
|
launchSingleTop = true
|
||||||
),
|
|
||||||
title = {},
|
|
||||||
navigationIcon = {
|
|
||||||
FeedbackIconButton(
|
|
||||||
modifier = Modifier.size(20.dp),
|
|
||||||
imageVector = Icons.Outlined.Settings,
|
|
||||||
contentDescription = stringResource(R.string.settings),
|
|
||||||
tint = MaterialTheme.colorScheme.onSurface,
|
|
||||||
showBadge = latestVersion.whetherNeedUpdate(currentVersion, skipVersion),
|
|
||||||
) {
|
|
||||||
navController.navigate(RouteName.SETTINGS) {
|
|
||||||
launchSingleTop = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actions = {
|
|
||||||
FeedbackIconButton(
|
|
||||||
modifier = Modifier.rotate(if (isSyncing) angle else 0f),
|
|
||||||
imageVector = Icons.Rounded.Refresh,
|
|
||||||
contentDescription = stringResource(R.string.refresh),
|
|
||||||
tint = MaterialTheme.colorScheme.onSurface,
|
|
||||||
) {
|
|
||||||
if (!isSyncing) homeViewModel.dispatch(HomeViewAction.Sync)
|
|
||||||
}
|
|
||||||
FeedbackIconButton(
|
|
||||||
imageVector = Icons.Rounded.Add,
|
|
||||||
contentDescription = stringResource(R.string.subscribe),
|
|
||||||
tint = MaterialTheme.colorScheme.onSurface,
|
|
||||||
) {
|
|
||||||
subscribeViewModel.dispatch(SubscribeViewAction.Show)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
FeedbackIconButton(
|
||||||
|
modifier = Modifier.rotate(if (isSyncing) angle else 0f),
|
||||||
|
imageVector = Icons.Rounded.Refresh,
|
||||||
|
contentDescription = stringResource(R.string.refresh),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurface,
|
||||||
|
) {
|
||||||
|
if (!isSyncing) homeViewModel.sync()
|
||||||
|
}
|
||||||
|
FeedbackIconButton(
|
||||||
|
imageVector = Icons.Rounded.Add,
|
||||||
|
contentDescription = stringResource(R.string.subscribe),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurface,
|
||||||
|
) {
|
||||||
|
subscribeViewModel.showDrawer()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
content = {
|
content = {
|
||||||
LazyColumn {
|
LazyColumn {
|
||||||
|
@ -183,15 +173,15 @@ fun FeedsPage(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
text = feedsViewState.account?.name ?: stringResource(R.string.read_you),
|
text = feedsUiState.account?.name ?: stringResource(R.string.read_you),
|
||||||
desc = if (isSyncing) stringResource(R.string.syncing) else "",
|
desc = if (isSyncing) stringResource(R.string.syncing) else "",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
Banner(
|
Banner(
|
||||||
title = filterState.filter.getName(),
|
title = filterUiState.filter.getName(),
|
||||||
desc = feedsViewState.importantCount.ifEmpty { stringResource(R.string.loading) },
|
desc = feedsUiState.importantSum.ifEmpty { stringResource(R.string.loading) },
|
||||||
icon = filterState.filter.iconOutline,
|
icon = filterUiState.filter.iconOutline,
|
||||||
action = {
|
action = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.KeyboardArrowRight,
|
imageVector = Icons.Outlined.KeyboardArrowRight,
|
||||||
|
@ -202,7 +192,7 @@ fun FeedsPage(
|
||||||
filterChange(
|
filterChange(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
homeViewModel = homeViewModel,
|
homeViewModel = homeViewModel,
|
||||||
filterState = filterState.copy(
|
filterState = filterUiState.copy(
|
||||||
group = null,
|
group = null,
|
||||||
feed = null,
|
feed = null,
|
||||||
)
|
)
|
||||||
|
@ -217,40 +207,51 @@ fun FeedsPage(
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
}
|
}
|
||||||
itemsIndexed(feedsViewState.groupWithFeedList) { index, groupWithFeed ->
|
itemsIndexed(feedsUiState.groupWithFeedList) { index, groupWithFeed ->
|
||||||
// Crossfade(targetState = groupWithFeed) { groupWithFeed ->
|
when (groupWithFeed) {
|
||||||
Column {
|
is GroupFeedsView.Group -> {
|
||||||
GroupItem(
|
if (index != 0) {
|
||||||
isExpanded = groupListExpand.value,
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
tonalElevation = groupListTonalElevation.value.dp,
|
}
|
||||||
group = groupWithFeed.group,
|
GroupItem(
|
||||||
feeds = groupWithFeed.feeds,
|
isExpanded = { groupsVisible[groupWithFeed.group.id] ?: false },
|
||||||
groupOnClick = {
|
group = groupWithFeed.group,
|
||||||
|
alpha = groupAlpha,
|
||||||
|
indicatorAlpha = groupIndicatorAlpha,
|
||||||
|
onExpanded = {
|
||||||
|
groupsVisible[groupWithFeed.group.id] =
|
||||||
|
!(groupsVisible[groupWithFeed.group.id] ?: false)
|
||||||
|
}
|
||||||
|
) {
|
||||||
filterChange(
|
filterChange(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
homeViewModel = homeViewModel,
|
homeViewModel = homeViewModel,
|
||||||
filterState = filterState.copy(
|
filterState = filterUiState.copy(
|
||||||
group = groupWithFeed.group,
|
group = groupWithFeed.group,
|
||||||
feed = null,
|
feed = null,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
feedOnClick = { feed ->
|
}
|
||||||
|
is GroupFeedsView.Feed -> {
|
||||||
|
FeedItem(
|
||||||
|
feed = groupWithFeed.feed,
|
||||||
|
alpha = groupAlpha,
|
||||||
|
badgeAlpha = feedBadgeAlpha,
|
||||||
|
isEnded = index != feedsUiState.groupWithFeedList.lastIndex && feedsUiState.groupWithFeedList[index + 1] is GroupFeedsView.Group,
|
||||||
|
isExpanded = { groupsVisible[groupWithFeed.feed.groupId] ?: false },
|
||||||
|
) {
|
||||||
filterChange(
|
filterChange(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
homeViewModel = homeViewModel,
|
homeViewModel = homeViewModel,
|
||||||
filterState = filterState.copy(
|
filterState = filterUiState.copy(
|
||||||
group = null,
|
group = null,
|
||||||
feed = feed,
|
feed = groupWithFeed.feed,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
|
||||||
if (index != feedsViewState.groupWithFeedList.lastIndex) {
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
Spacer(modifier = Modifier.height(128.dp))
|
Spacer(modifier = Modifier.height(128.dp))
|
||||||
|
@ -260,7 +261,7 @@ fun FeedsPage(
|
||||||
},
|
},
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
FilterBar(
|
FilterBar(
|
||||||
filter = filterState.filter,
|
filter = filterUiState.filter,
|
||||||
filterBarStyle = filterBarStyle.value,
|
filterBarStyle = filterBarStyle.value,
|
||||||
filterBarFilled = filterBarFilled.value,
|
filterBarFilled = filterBarFilled.value,
|
||||||
filterBarPadding = filterBarPadding.dp,
|
filterBarPadding = filterBarPadding.dp,
|
||||||
|
@ -269,7 +270,7 @@ fun FeedsPage(
|
||||||
filterChange(
|
filterChange(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
homeViewModel = homeViewModel,
|
homeViewModel = homeViewModel,
|
||||||
filterState = filterState.copy(filter = it),
|
filterState = filterUiState.copy(filter = it),
|
||||||
isNavigate = false,
|
isNavigate = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -287,7 +288,7 @@ private fun filterChange(
|
||||||
filterState: FilterState,
|
filterState: FilterState,
|
||||||
isNavigate: Boolean = true,
|
isNavigate: Boolean = true,
|
||||||
) {
|
) {
|
||||||
homeViewModel.dispatch(HomeViewAction.ChangeFilter(filterState))
|
homeViewModel.changeFilter(filterState)
|
||||||
if (isNavigate) {
|
if (isNavigate) {
|
||||||
navController.navigate(RouteName.FLOW) {
|
navController.navigate(RouteName.FLOW) {
|
||||||
launchSingleTop = true
|
launchSingleTop = true
|
||||||
|
|
|
@ -2,15 +2,17 @@ package me.ash.reader.ui.page.home.feeds
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
|
import androidx.compose.ui.util.fastForEach
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import me.ash.reader.R
|
import me.ash.reader.R
|
||||||
import me.ash.reader.data.entity.Account
|
import me.ash.reader.data.entity.Account
|
||||||
import me.ash.reader.data.entity.GroupWithFeed
|
import me.ash.reader.data.module.DispatcherDefault
|
||||||
|
import me.ash.reader.data.module.DispatcherIO
|
||||||
import me.ash.reader.data.repository.AccountRepository
|
import me.ash.reader.data.repository.AccountRepository
|
||||||
import me.ash.reader.data.repository.OpmlRepository
|
import me.ash.reader.data.repository.OpmlRepository
|
||||||
import me.ash.reader.data.repository.RssRepository
|
import me.ash.reader.data.repository.RssRepository
|
||||||
|
@ -24,22 +26,17 @@ class FeedsViewModel @Inject constructor(
|
||||||
private val rssRepository: RssRepository,
|
private val rssRepository: RssRepository,
|
||||||
private val opmlRepository: OpmlRepository,
|
private val opmlRepository: OpmlRepository,
|
||||||
private val stringsRepository: StringsRepository,
|
private val stringsRepository: StringsRepository,
|
||||||
|
@DispatcherDefault
|
||||||
|
private val dispatcherDefault: CoroutineDispatcher,
|
||||||
|
@DispatcherIO
|
||||||
|
private val dispatcherIO: CoroutineDispatcher,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val _viewState = MutableStateFlow(FeedsViewState())
|
private val _feedsUiState = MutableStateFlow(FeedsUiState())
|
||||||
val viewState: StateFlow<FeedsViewState> = _viewState.asStateFlow()
|
val feedsUiState: StateFlow<FeedsUiState> = _feedsUiState.asStateFlow()
|
||||||
|
|
||||||
fun dispatch(action: FeedsViewAction) {
|
fun fetchAccount() {
|
||||||
when (action) {
|
viewModelScope.launch(dispatcherIO) {
|
||||||
is FeedsViewAction.FetchAccount -> fetchAccount()
|
_feedsUiState.update {
|
||||||
is FeedsViewAction.FetchData -> fetchData(action.filterState)
|
|
||||||
is FeedsViewAction.ExportAsString -> exportAsOpml(action.callback)
|
|
||||||
is FeedsViewAction.ScrollToItem -> scrollToItem(action.index)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun fetchAccount() {
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
_viewState.update {
|
|
||||||
it.copy(
|
it.copy(
|
||||||
account = accountRepository.getCurrentAccount()
|
account = accountRepository.getCurrentAccount()
|
||||||
)
|
)
|
||||||
|
@ -47,8 +44,8 @@ class FeedsViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun exportAsOpml(callback: (String) -> Unit = {}) {
|
fun exportAsOpml(callback: (String) -> Unit = {}) {
|
||||||
viewModelScope.launch(Dispatchers.Default) {
|
viewModelScope.launch(dispatcherDefault) {
|
||||||
try {
|
try {
|
||||||
callback(opmlRepository.saveToString())
|
callback(opmlRepository.saveToString())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
@ -57,8 +54,8 @@ class FeedsViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fetchData(filterState: FilterState) {
|
fun fetchData(filterState: FilterState) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(dispatcherIO) {
|
||||||
pullFeeds(
|
pullFeeds(
|
||||||
isStarred = filterState.filter.isStarred(),
|
isStarred = filterState.filter.isStarred(),
|
||||||
isUnread = filterState.filter.isUnread(),
|
isUnread = filterState.filter.isUnread(),
|
||||||
|
@ -70,85 +67,64 @@ class FeedsViewModel @Inject constructor(
|
||||||
combine(
|
combine(
|
||||||
rssRepository.get().pullFeeds(),
|
rssRepository.get().pullFeeds(),
|
||||||
rssRepository.get().pullImportant(isStarred, isUnread),
|
rssRepository.get().pullImportant(isStarred, isUnread),
|
||||||
) { groupWithFeedList, importantList ->
|
) { groupWithFeedList, importantMap ->
|
||||||
val groupImportantMap = mutableMapOf<String, Int>()
|
groupWithFeedList.fastForEach {
|
||||||
val feedImportantMap = mutableMapOf<String, Int>()
|
var groupImportant = 0
|
||||||
importantList.groupBy { it.groupId }.forEach { (i, list) ->
|
it.feeds.fastForEach {
|
||||||
var groupImportantSum = 0
|
it.important = importantMap[it.id]
|
||||||
list.forEach {
|
groupImportant += it.important ?: 0
|
||||||
feedImportantMap[it.feedId] = it.important
|
|
||||||
groupImportantSum += it.important
|
|
||||||
}
|
|
||||||
groupImportantMap[i] = groupImportantSum
|
|
||||||
}
|
|
||||||
val groupsIt = groupWithFeedList.iterator()
|
|
||||||
while (groupsIt.hasNext()) {
|
|
||||||
val groupWithFeed = groupsIt.next()
|
|
||||||
val groupImportant = groupImportantMap[groupWithFeed.group.id]
|
|
||||||
if (groupImportant == null && (isStarred || isUnread)) {
|
|
||||||
groupsIt.remove()
|
|
||||||
} else {
|
|
||||||
groupWithFeed.group.important = groupImportant
|
|
||||||
val feedsIt = groupWithFeed.feeds.iterator()
|
|
||||||
while (feedsIt.hasNext()) {
|
|
||||||
val feed = feedsIt.next()
|
|
||||||
val feedImportant = feedImportantMap[feed.id]
|
|
||||||
if (feedImportant == null && (isStarred || isUnread)) {
|
|
||||||
feedsIt.remove()
|
|
||||||
} else {
|
|
||||||
feed.important = feedImportant
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
it.group.important = groupImportant
|
||||||
}
|
}
|
||||||
groupWithFeedList
|
groupWithFeedList
|
||||||
}.onEach { groupWithFeedList ->
|
}.mapLatest { groupWithFeedList ->
|
||||||
_viewState.update {
|
_feedsUiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
importantCount = groupWithFeedList.sumOf { it.group.important ?: 0 }.run {
|
importantSum = groupWithFeedList.sumOf { it.group.important ?: 0 }.run {
|
||||||
when {
|
when {
|
||||||
isStarred -> stringsRepository.getQuantityString(R.plurals.starred_desc, this, this)
|
isStarred -> stringsRepository.getQuantityString(
|
||||||
isUnread -> stringsRepository.getQuantityString(R.plurals.unread_desc, this, this)
|
R.plurals.starred_desc,
|
||||||
else -> stringsRepository.getQuantityString(R.plurals.all_desc, this, this)
|
this,
|
||||||
|
this
|
||||||
|
)
|
||||||
|
isUnread -> stringsRepository.getQuantityString(
|
||||||
|
R.plurals.unread_desc,
|
||||||
|
this,
|
||||||
|
this
|
||||||
|
)
|
||||||
|
else -> stringsRepository.getQuantityString(
|
||||||
|
R.plurals.all_desc,
|
||||||
|
this,
|
||||||
|
this
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
groupWithFeedList = groupWithFeedList,
|
groupWithFeedList = groupWithFeedList.map {
|
||||||
feedsVisible = List(groupWithFeedList.size, init = { true })
|
mutableListOf<GroupFeedsView>(GroupFeedsView.Group(it.group)).apply {
|
||||||
|
addAll(
|
||||||
|
it.feeds.map {
|
||||||
|
GroupFeedsView.Feed(it)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.flatten(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}.catch {
|
}.catch {
|
||||||
Log.e("RLog", "catch in articleRepository.pullFeeds(): ${it.message}")
|
Log.e("RLog", "catch in articleRepository.pullFeeds(): ${it.message}")
|
||||||
}.flowOn(Dispatchers.Default).collect()
|
}.flowOn(dispatcherDefault).collect()
|
||||||
}
|
|
||||||
|
|
||||||
private fun scrollToItem(index: Int) {
|
|
||||||
viewModelScope.launch {
|
|
||||||
_viewState.value.listState.scrollToItem(index)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class FeedsViewState(
|
data class FeedsUiState(
|
||||||
val account: Account? = null,
|
val account: Account? = null,
|
||||||
val importantCount: String = "",
|
val importantSum: String = "",
|
||||||
val groupWithFeedList: List<GroupWithFeed> = emptyList(),
|
val groupWithFeedList: List<GroupFeedsView> = emptyList(),
|
||||||
val feedsVisible: List<Boolean> = emptyList(),
|
|
||||||
val listState: LazyListState = LazyListState(),
|
val listState: LazyListState = LazyListState(),
|
||||||
val groupsVisible: Boolean = true,
|
val groupsVisible: Boolean = true,
|
||||||
)
|
)
|
||||||
|
|
||||||
sealed class FeedsViewAction {
|
sealed class GroupFeedsView {
|
||||||
data class FetchData(
|
class Group(val group: me.ash.reader.data.entity.Group) : GroupFeedsView()
|
||||||
val filterState: FilterState,
|
class Feed(val feed: me.ash.reader.data.entity.Feed) : GroupFeedsView()
|
||||||
) : FeedsViewAction()
|
|
||||||
|
|
||||||
object FetchAccount : FeedsViewAction()
|
|
||||||
|
|
||||||
data class ExportAsString(
|
|
||||||
val callback: (String) -> Unit = {}
|
|
||||||
) : FeedsViewAction()
|
|
||||||
|
|
||||||
data class ScrollToItem(
|
|
||||||
val index: Int
|
|
||||||
) : FeedsViewAction()
|
|
||||||
}
|
}
|
|
@ -1,59 +1,56 @@
|
||||||
package me.ash.reader.ui.page.home.feeds
|
package me.ash.reader.ui.page.home.feeds
|
||||||
|
|
||||||
import android.view.HapticFeedbackConstants
|
import android.view.HapticFeedbackConstants
|
||||||
import androidx.compose.animation.*
|
import androidx.compose.animation.animateContentSize
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.rounded.ExpandLess
|
import androidx.compose.material.icons.rounded.ExpandLess
|
||||||
import androidx.compose.material.icons.rounded.ExpandMore
|
import androidx.compose.material.icons.rounded.ExpandMore
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.platform.LocalView
|
import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.Dp
|
|
||||||
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 me.ash.reader.R
|
import me.ash.reader.R
|
||||||
import me.ash.reader.data.entity.Feed
|
|
||||||
import me.ash.reader.data.entity.Group
|
import me.ash.reader.data.entity.Group
|
||||||
import me.ash.reader.ui.ext.alphaLN
|
import me.ash.reader.ui.page.home.feeds.drawer.group.GroupOptionViewModel
|
||||||
import me.ash.reader.ui.page.home.feeds.option.group.GroupOptionViewAction
|
import me.ash.reader.ui.theme.Shape32
|
||||||
import me.ash.reader.ui.page.home.feeds.option.group.GroupOptionViewModel
|
import me.ash.reader.ui.theme.ShapeTop32
|
||||||
|
|
||||||
@OptIn(androidx.compose.foundation.ExperimentalFoundationApi::class)
|
@OptIn(androidx.compose.foundation.ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun GroupItem(
|
fun GroupItem(
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
tonalElevation: Dp,
|
|
||||||
group: Group,
|
group: Group,
|
||||||
feeds: List<Feed>,
|
alpha: Float = 1f,
|
||||||
isExpanded: Boolean = true,
|
indicatorAlpha: Float = 1f,
|
||||||
|
isExpanded: () -> Boolean,
|
||||||
groupOptionViewModel: GroupOptionViewModel = hiltViewModel(),
|
groupOptionViewModel: GroupOptionViewModel = hiltViewModel(),
|
||||||
|
onExpanded: () -> Unit = {},
|
||||||
groupOnClick: () -> Unit = {},
|
groupOnClick: () -> Unit = {},
|
||||||
feedOnClick: (feed: Feed) -> Unit = {},
|
|
||||||
) {
|
) {
|
||||||
val view = LocalView.current
|
val view = LocalView.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
var expanded by remember { mutableStateOf(isExpanded) }
|
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.animateContentSize()
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
.clip(RoundedCornerShape(32.dp))
|
.clip(if (isExpanded()) ShapeTop32 else Shape32)
|
||||||
.background(
|
.background(
|
||||||
MaterialTheme.colorScheme.secondary.copy(alpha = tonalElevation.alphaLN(weight = 1.2f))
|
MaterialTheme.colorScheme.secondary.copy(alpha = alpha)
|
||||||
)
|
)
|
||||||
.combinedClickable(
|
.combinedClickable(
|
||||||
onClick = {
|
onClick = {
|
||||||
|
@ -61,13 +58,13 @@ fun GroupItem(
|
||||||
},
|
},
|
||||||
onLongClick = {
|
onLongClick = {
|
||||||
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
||||||
groupOptionViewModel.dispatch(GroupOptionViewAction.Show(scope, group.id))
|
groupOptionViewModel.showDrawer(scope, group.id)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.padding(top = 22.dp)
|
.padding(top = 22.dp)
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
|
@ -87,42 +84,21 @@ fun GroupItem(
|
||||||
.size(24.dp)
|
.size(24.dp)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(
|
.background(
|
||||||
MaterialTheme.colorScheme.surfaceTint.copy(
|
MaterialTheme.colorScheme.surfaceTint.copy(alpha = indicatorAlpha)
|
||||||
alpha = tonalElevation.alphaLN(weight = 1.4f)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
.clickable {
|
.clickable {
|
||||||
expanded = !expanded
|
onExpanded()
|
||||||
},
|
},
|
||||||
horizontalArrangement = Arrangement.Center,
|
horizontalArrangement = Arrangement.Center,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = if (expanded) Icons.Rounded.ExpandLess else Icons.Rounded.ExpandMore,
|
imageVector = if (isExpanded()) Icons.Rounded.ExpandLess else Icons.Rounded.ExpandMore,
|
||||||
contentDescription = stringResource(if (expanded) R.string.expand_less else R.string.expand_more),
|
contentDescription = stringResource(if (isExpanded()) R.string.expand_less else R.string.expand_more),
|
||||||
tint = MaterialTheme.colorScheme.onSecondaryContainer,
|
tint = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(22.dp))
|
Spacer(modifier = Modifier.height(22.dp))
|
||||||
AnimatedVisibility(
|
|
||||||
visible = expanded,
|
|
||||||
enter = fadeIn() + expandVertically(),
|
|
||||||
exit = fadeOut() + shrinkVertically(),
|
|
||||||
) {
|
|
||||||
Column {
|
|
||||||
feeds.forEach { feed ->
|
|
||||||
FeedItem(
|
|
||||||
feed = feed,
|
|
||||||
tonalElevation = tonalElevation,
|
|
||||||
) {
|
|
||||||
feedOnClick(feed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (feeds.isNotEmpty()) {
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package me.ash.reader.ui.page.home.feeds.option.feed
|
package me.ash.reader.ui.page.home.feeds.drawer.feed
|
||||||
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.DeleteForever
|
import androidx.compose.material.icons.outlined.DeleteForever
|
||||||
|
@ -7,32 +7,28 @@ import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import com.google.accompanist.pager.ExperimentalPagerApi
|
|
||||||
import me.ash.reader.R
|
import me.ash.reader.R
|
||||||
import me.ash.reader.ui.component.Dialog
|
import me.ash.reader.ui.component.base.RYDialog
|
||||||
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.ext.showToast
|
||||||
|
|
||||||
@OptIn(ExperimentalPagerApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ClearFeedDialog(
|
fun ClearFeedDialog(
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
feedName: String,
|
feedName: String,
|
||||||
viewModel: FeedOptionViewModel = hiltViewModel(),
|
feedOptionViewModel: FeedOptionViewModel = hiltViewModel(),
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val viewState = viewModel.viewState.collectAsStateValue()
|
val feedOptionUiState = feedOptionViewModel.feedOptionUiState.collectAsStateValue()
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val toastString = stringResource(R.string.clear_articles_in_feed_toast, feedName)
|
val toastString = stringResource(R.string.clear_articles_in_feed_toast, feedName)
|
||||||
|
|
||||||
Dialog(
|
RYDialog(
|
||||||
visible = viewState.clearDialogVisible,
|
visible = feedOptionUiState.clearDialogVisible,
|
||||||
onDismissRequest = {
|
onDismissRequest = {
|
||||||
viewModel.dispatch(FeedOptionViewAction.HideClearDialog)
|
feedOptionViewModel.hideClearDialog()
|
||||||
},
|
},
|
||||||
icon = {
|
icon = {
|
||||||
Icon(
|
Icon(
|
||||||
|
@ -49,11 +45,11 @@ fun ClearFeedDialog(
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.dispatch(FeedOptionViewAction.Clear {
|
feedOptionViewModel.clearFeed {
|
||||||
viewModel.dispatch(FeedOptionViewAction.HideClearDialog)
|
feedOptionViewModel.hideClearDialog()
|
||||||
viewModel.dispatch(FeedOptionViewAction.Hide(scope))
|
feedOptionViewModel.hideDrawer(scope)
|
||||||
context.showToast(toastString)
|
context.showToast(toastString)
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
|
@ -64,7 +60,7 @@ fun ClearFeedDialog(
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.dispatch(FeedOptionViewAction.HideClearDialog)
|
feedOptionViewModel.hideClearDialog()
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user