commit
db5aa7aca6
|
@ -116,7 +116,7 @@ dependencies {
|
|||
implementation "org.conscrypt:conscrypt-android:2.5.2"
|
||||
|
||||
// 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:converter-gson:$retrofit2"
|
||||
|
||||
|
@ -166,9 +166,9 @@ dependencies {
|
|||
// https://developer.android.com/jetpack/androidx/releases/compose-material
|
||||
implementation "androidx.compose.material:material:$compose"
|
||||
implementation "androidx.compose.material:material-icons-extended:$compose"
|
||||
debugImplementation "androidx.compose.ui:ui-tooling:$compose"
|
||||
implementation "androidx.compose.ui:ui-tooling-preview:$compose"
|
||||
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose"
|
||||
debugImplementation "androidx.compose.ui:ui-tooling:$compose"
|
||||
|
||||
// hilt
|
||||
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" />
|
||||
|
||||
<!-- Disable automatic updates in F-Droid -->
|
||||
<!-- <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />-->
|
||||
|
||||
<application
|
||||
android:name=".App"
|
||||
android:name=".RYApp"
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
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.DispatcherDefault
|
||||
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.ReaderDatabase
|
||||
import me.ash.reader.data.source.RYDatabase
|
||||
import me.ash.reader.data.source.RYNetworkDataSource
|
||||
import me.ash.reader.ui.ext.*
|
||||
import okhttp3.Cache
|
||||
import okhttp3.OkHttpClient
|
||||
import org.conscrypt.Conscrypt
|
||||
import java.io.File
|
||||
import java.security.Security
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltAndroidApp
|
||||
class App : Application(), Configuration.Provider {
|
||||
class RYApp : Application(), Configuration.Provider {
|
||||
init {
|
||||
// From: https://gitlab.com/spacecowboy/Feeder
|
||||
// Install Conscrypt to handle TLSv1.3 pre Android10
|
||||
|
@ -33,7 +30,7 @@ class App : Application(), Configuration.Provider {
|
|||
}
|
||||
|
||||
@Inject
|
||||
lateinit var readerDatabase: ReaderDatabase
|
||||
lateinit var RYDatabase: RYDatabase
|
||||
|
||||
@Inject
|
||||
lateinit var workerFactory: HiltWorkerFactory
|
||||
|
@ -42,7 +39,7 @@ class App : Application(), Configuration.Provider {
|
|||
lateinit var workManager: WorkManager
|
||||
|
||||
@Inject
|
||||
lateinit var appNetworkDataSource: AppNetworkDataSource
|
||||
lateinit var RYNetworkDataSource: RYNetworkDataSource
|
||||
|
||||
@Inject
|
||||
lateinit var opmlLocalDataSource: OpmlLocalDataSource
|
||||
|
@ -51,7 +48,10 @@ class App : Application(), Configuration.Provider {
|
|||
lateinit var rssHelper: RssHelper
|
||||
|
||||
@Inject
|
||||
lateinit var appRepository: AppRepository
|
||||
lateinit var notificationHelper: NotificationHelper
|
||||
|
||||
@Inject
|
||||
lateinit var ryRepository: RYRepository
|
||||
|
||||
@Inject
|
||||
lateinit var stringsRepository: StringsRepository
|
||||
|
@ -62,9 +62,6 @@ class App : Application(), Configuration.Provider {
|
|||
@Inject
|
||||
lateinit var localRssRepository: LocalRssRepository
|
||||
|
||||
// @Inject
|
||||
// lateinit var feverRssRepository: FeverRssRepository
|
||||
|
||||
@Inject
|
||||
lateinit var opmlRepository: OpmlRepository
|
||||
|
||||
|
@ -79,6 +76,9 @@ class App : Application(), Configuration.Provider {
|
|||
@DispatcherDefault
|
||||
lateinit var dispatcherDefault: CoroutineDispatcher
|
||||
|
||||
@Inject
|
||||
lateinit var okHttpClient: OkHttpClient
|
||||
|
||||
@Inject
|
||||
lateinit var imageLoader: ImageLoader
|
||||
|
||||
|
@ -89,7 +89,7 @@ class App : Application(), Configuration.Provider {
|
|||
applicationScope.launch(dispatcherDefault) {
|
||||
accountInit()
|
||||
workerInit()
|
||||
if (BuildConfig.FLAVOR != "fdroid") {
|
||||
if (notFdroid) {
|
||||
checkUpdate()
|
||||
}
|
||||
}
|
||||
|
@ -116,7 +116,7 @@ class App : Application(), Configuration.Provider {
|
|||
it.del()
|
||||
}
|
||||
}
|
||||
appRepository.checkUpdate(showToast = false)
|
||||
ryRepository.checkUpdate(showToast = false)
|
||||
}
|
||||
|
||||
override fun getWorkManagerConfiguration(): Configuration =
|
||||
|
@ -125,28 +125,3 @@ class App : Application(), Configuration.Provider {
|
|||
.setMinimumLoggingLevel(android.util.Log.DEBUG)
|
||||
.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
|
||||
"""
|
||||
)
|
||||
suspend fun queryById(id: Int): Account
|
||||
suspend fun queryById(id: Int): Account?
|
||||
|
||||
@Insert
|
||||
suspend fun insert(account: Account): Long
|
||||
|
|
|
@ -5,7 +5,7 @@ import androidx.room.*
|
|||
import kotlinx.coroutines.flow.Flow
|
||||
import me.ash.reader.data.entity.Article
|
||||
import me.ash.reader.data.entity.ArticleWithFeed
|
||||
import me.ash.reader.data.entity.ImportantCount
|
||||
import me.ash.reader.data.model.ImportantCount
|
||||
import java.util.*
|
||||
|
||||
@Dao
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
package me.ash.reader.data.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.PrimaryKey
|
||||
import androidx.room.*
|
||||
import java.util.*
|
||||
|
||||
@Entity(
|
||||
|
@ -18,13 +15,13 @@ import java.util.*
|
|||
)
|
||||
data class Article(
|
||||
@PrimaryKey
|
||||
val id: String,
|
||||
var id: String,
|
||||
@ColumnInfo
|
||||
val date: Date,
|
||||
var date: Date,
|
||||
@ColumnInfo
|
||||
val title: String,
|
||||
var title: String,
|
||||
@ColumnInfo
|
||||
val author: String? = null,
|
||||
var author: String? = null,
|
||||
@ColumnInfo
|
||||
var rawDescription: String,
|
||||
@ColumnInfo
|
||||
|
@ -32,17 +29,20 @@ data class Article(
|
|||
@ColumnInfo
|
||||
var fullContent: String? = null,
|
||||
@ColumnInfo
|
||||
val img: String? = null,
|
||||
var img: String? = null,
|
||||
@ColumnInfo
|
||||
val link: String,
|
||||
var link: String,
|
||||
@ColumnInfo(index = true)
|
||||
val feedId: String,
|
||||
var feedId: String,
|
||||
@ColumnInfo(index = true)
|
||||
val accountId: Int,
|
||||
var accountId: Int,
|
||||
@ColumnInfo(defaultValue = "true")
|
||||
var isUnread: Boolean = true,
|
||||
@ColumnInfo(defaultValue = "false")
|
||||
var isStarred: Boolean = false,
|
||||
@ColumnInfo(defaultValue = "false")
|
||||
var isReadLater: Boolean = false,
|
||||
)
|
||||
) {
|
||||
@Ignore
|
||||
var dateString: String? = null
|
||||
}
|
|
@ -5,7 +5,7 @@ import androidx.room.Relation
|
|||
|
||||
data class ArticleWithFeed(
|
||||
@Embedded
|
||||
val article: Article,
|
||||
var article: Article,
|
||||
@Relation(parentColumn = "feedId", entityColumn = "id")
|
||||
val feed: Feed,
|
||||
var feed: Feed,
|
||||
)
|
||||
|
|
|
@ -14,17 +14,17 @@ import androidx.room.*
|
|||
)
|
||||
data class Feed(
|
||||
@PrimaryKey
|
||||
val id: String,
|
||||
var id: String,
|
||||
@ColumnInfo
|
||||
val name: String,
|
||||
var name: String,
|
||||
@ColumnInfo
|
||||
var icon: String? = null,
|
||||
@ColumnInfo
|
||||
val url: String,
|
||||
var url: String,
|
||||
@ColumnInfo(index = true)
|
||||
var groupId: String,
|
||||
@ColumnInfo(index = true)
|
||||
val accountId: Int,
|
||||
var accountId: Int,
|
||||
@ColumnInfo(defaultValue = "false")
|
||||
var isNotification: Boolean = false,
|
||||
@ColumnInfo(defaultValue = "false")
|
||||
|
|
|
@ -5,7 +5,7 @@ import androidx.room.Relation
|
|||
|
||||
data class FeedWithArticle(
|
||||
@Embedded
|
||||
val feed: Feed,
|
||||
var feed: Feed,
|
||||
@Relation(parentColumn = "id", entityColumn = "feedId")
|
||||
val articles: List<Article>
|
||||
var articles: List<Article>
|
||||
)
|
||||
|
|
|
@ -5,7 +5,7 @@ import androidx.room.Relation
|
|||
|
||||
data class FeedWithGroup(
|
||||
@Embedded
|
||||
val feed: Feed,
|
||||
var feed: Feed,
|
||||
@Relation(parentColumn = "groupId", entityColumn = "id")
|
||||
val group: Group
|
||||
var group: Group
|
||||
)
|
||||
|
|
|
@ -8,11 +8,11 @@ import androidx.room.PrimaryKey
|
|||
@Entity(tableName = "group")
|
||||
data class Group(
|
||||
@PrimaryKey
|
||||
val id: String,
|
||||
var id: String,
|
||||
@ColumnInfo
|
||||
val name: String,
|
||||
var name: String,
|
||||
@ColumnInfo(index = true)
|
||||
val accountId: Int,
|
||||
var accountId: Int,
|
||||
) {
|
||||
@Ignore
|
||||
var important: Int? = 0
|
||||
|
|
|
@ -5,7 +5,7 @@ import androidx.room.Relation
|
|||
|
||||
data class GroupWithFeed(
|
||||
@Embedded
|
||||
val group: Group,
|
||||
var group: Group,
|
||||
@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.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.StarOutline
|
||||
import androidx.compose.material.icons.rounded.Subject
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import me.ash.reader.R
|
||||
|
||||
class Filter(
|
||||
val index: Int,
|
||||
|
@ -33,5 +36,13 @@ class Filter(
|
|||
iconOutline = 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(
|
||||
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>) {
|
||||
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.FeedDao
|
||||
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
|
||||
|
||||
@Module
|
||||
|
@ -19,26 +19,26 @@ object DatabaseModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideArticleDao(readerDatabase: ReaderDatabase): ArticleDao =
|
||||
readerDatabase.articleDao()
|
||||
fun provideArticleDao(RYDatabase: RYDatabase): ArticleDao =
|
||||
RYDatabase.articleDao()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideFeedDao(readerDatabase: ReaderDatabase): FeedDao =
|
||||
readerDatabase.feedDao()
|
||||
fun provideFeedDao(RYDatabase: RYDatabase): FeedDao =
|
||||
RYDatabase.feedDao()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGroupDao(readerDatabase: ReaderDatabase): GroupDao =
|
||||
readerDatabase.groupDao()
|
||||
fun provideGroupDao(RYDatabase: RYDatabase): GroupDao =
|
||||
RYDatabase.groupDao()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAccountDao(readerDatabase: ReaderDatabase): AccountDao =
|
||||
readerDatabase.accountDao()
|
||||
fun provideAccountDao(RYDatabase: RYDatabase): AccountDao =
|
||||
RYDatabase.accountDao()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideReaderDatabase(@ApplicationContext context: Context): ReaderDatabase =
|
||||
ReaderDatabase.getInstance(context)
|
||||
fun provideReaderDatabase(@ApplicationContext context: Context): RYDatabase =
|
||||
RYDatabase.getInstance(context)
|
||||
}
|
|
@ -15,7 +15,7 @@ import dagger.hilt.InstallIn
|
|||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import me.ash.reader.cachingHttpClient
|
||||
import okhttp3.OkHttpClient
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
|
@ -25,16 +25,11 @@ object ImageLoaderModule {
|
|||
@Provides
|
||||
@Singleton
|
||||
fun provideImageLoader(
|
||||
@ApplicationContext context: Context
|
||||
@ApplicationContext context: Context,
|
||||
okHttpClient: OkHttpClient,
|
||||
): ImageLoader {
|
||||
return ImageLoader.Builder(context)
|
||||
.okHttpClient(
|
||||
okHttpClient = cachingHttpClient(
|
||||
cacheDirectory = context.cacheDir.resolve("http")
|
||||
).newBuilder()
|
||||
//.addNetworkInterceptor(UserAgentInterceptor)
|
||||
.build()
|
||||
)
|
||||
.okHttpClient(okHttpClient)
|
||||
.dispatcher(Dispatchers.Default) // This slightly improves scrolling performance
|
||||
.components{
|
||||
add(SvgDecoder.Factory())
|
||||
|
|
|
@ -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.hilt.InstallIn
|
||||
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.GoogleReaderApiDataSource
|
||||
import javax.inject.Singleton
|
||||
|
@ -15,8 +15,8 @@ object RetrofitModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAppNetworkDataSource(): AppNetworkDataSource =
|
||||
AppNetworkDataSource.getInstance()
|
||||
fun provideAppNetworkDataSource(): RYNetworkDataSource =
|
||||
RYNetworkDataSource.getInstance()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
|
|
|
@ -3,6 +3,7 @@ package me.ash.reader.data.preference
|
|||
import android.content.Context
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -33,6 +34,7 @@ sealed class DarkThemePreference(val value: Int) : Preference() {
|
|||
}
|
||||
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
fun isDarkTheme(): Boolean = when (this) {
|
||||
UseDeviceTheme -> isSystemInDarkTheme()
|
||||
ON -> true
|
||||
|
|
|
@ -4,17 +4,18 @@ import android.content.Context
|
|||
import androidx.datastore.preferences.core.Preferences
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import me.ash.reader.data.constant.ElevationTokens
|
||||
import me.ash.reader.ui.ext.DataStoreKeys
|
||||
import me.ash.reader.ui.ext.dataStore
|
||||
import me.ash.reader.ui.ext.put
|
||||
|
||||
sealed class FeedsFilterBarTonalElevationPreference(val value: Int) : Preference() {
|
||||
object Level0 : FeedsFilterBarTonalElevationPreference(0)
|
||||
object Level1 : FeedsFilterBarTonalElevationPreference(1)
|
||||
object Level2 : FeedsFilterBarTonalElevationPreference(3)
|
||||
object Level3 : FeedsFilterBarTonalElevationPreference(6)
|
||||
object Level4 : FeedsFilterBarTonalElevationPreference(8)
|
||||
object Level5 : FeedsFilterBarTonalElevationPreference(12)
|
||||
object Level0 : FeedsFilterBarTonalElevationPreference(ElevationTokens.Level0)
|
||||
object Level1 : FeedsFilterBarTonalElevationPreference(ElevationTokens.Level1)
|
||||
object Level2 : FeedsFilterBarTonalElevationPreference(ElevationTokens.Level2)
|
||||
object Level3 : FeedsFilterBarTonalElevationPreference(ElevationTokens.Level3)
|
||||
object Level4 : FeedsFilterBarTonalElevationPreference(ElevationTokens.Level4)
|
||||
object Level5 : FeedsFilterBarTonalElevationPreference(ElevationTokens.Level5)
|
||||
|
||||
override fun put(context: Context, scope: CoroutineScope) {
|
||||
scope.launch {
|
||||
|
@ -27,12 +28,12 @@ sealed class FeedsFilterBarTonalElevationPreference(val value: Int) : Preference
|
|||
|
||||
fun getDesc(context: Context): String =
|
||||
when (this) {
|
||||
Level0 -> "Level 0 (0dp)"
|
||||
Level1 -> "Level 1 (1dp)"
|
||||
Level2 -> "Level 2 (3dp)"
|
||||
Level3 -> "Level 3 (6dp)"
|
||||
Level4 -> "Level 4 (8dp)"
|
||||
Level5 -> "Level 5 (12dp)"
|
||||
Level0 -> "Level 0 (${ElevationTokens.Level0}dp)"
|
||||
Level1 -> "Level 1 (${ElevationTokens.Level1}dp)"
|
||||
Level2 -> "Level 2 (${ElevationTokens.Level2}dp)"
|
||||
Level3 -> "Level 3 (${ElevationTokens.Level3}dp)"
|
||||
Level4 -> "Level 4 (${ElevationTokens.Level4}dp)"
|
||||
Level5 -> "Level 5 (${ElevationTokens.Level5}dp)"
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -41,13 +42,14 @@ sealed class FeedsFilterBarTonalElevationPreference(val value: Int) : Preference
|
|||
|
||||
fun fromPreferences(preferences: Preferences) =
|
||||
when (preferences[DataStoreKeys.FeedsFilterBarTonalElevation.key]) {
|
||||
0 -> Level0
|
||||
1 -> Level1
|
||||
3 -> Level2
|
||||
6 -> Level3
|
||||
8 -> Level4
|
||||
12 -> Level5
|
||||
ElevationTokens.Level0 -> Level0
|
||||
ElevationTokens.Level1 -> Level1
|
||||
ElevationTokens.Level2 -> Level2
|
||||
ElevationTokens.Level3 -> Level3
|
||||
ElevationTokens.Level4 -> Level4
|
||||
ElevationTokens.Level5 -> Level5
|
||||
else -> default
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,17 +4,18 @@ import android.content.Context
|
|||
import androidx.datastore.preferences.core.Preferences
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import me.ash.reader.data.constant.ElevationTokens
|
||||
import me.ash.reader.ui.ext.DataStoreKeys
|
||||
import me.ash.reader.ui.ext.dataStore
|
||||
import me.ash.reader.ui.ext.put
|
||||
|
||||
sealed class FeedsGroupListTonalElevationPreference(val value: Int) : Preference() {
|
||||
object Level0 : FeedsGroupListTonalElevationPreference(0)
|
||||
object Level1 : FeedsGroupListTonalElevationPreference(1)
|
||||
object Level2 : FeedsGroupListTonalElevationPreference(3)
|
||||
object Level3 : FeedsGroupListTonalElevationPreference(6)
|
||||
object Level4 : FeedsGroupListTonalElevationPreference(8)
|
||||
object Level5 : FeedsGroupListTonalElevationPreference(12)
|
||||
object Level0 : FeedsGroupListTonalElevationPreference(ElevationTokens.Level0)
|
||||
object Level1 : FeedsGroupListTonalElevationPreference(ElevationTokens.Level1)
|
||||
object Level2 : FeedsGroupListTonalElevationPreference(ElevationTokens.Level2)
|
||||
object Level3 : FeedsGroupListTonalElevationPreference(ElevationTokens.Level3)
|
||||
object Level4 : FeedsGroupListTonalElevationPreference(ElevationTokens.Level4)
|
||||
object Level5 : FeedsGroupListTonalElevationPreference(ElevationTokens.Level5)
|
||||
|
||||
override fun put(context: Context, scope: CoroutineScope) {
|
||||
scope.launch {
|
||||
|
@ -27,12 +28,12 @@ sealed class FeedsGroupListTonalElevationPreference(val value: Int) : Preference
|
|||
|
||||
fun getDesc(context: Context): String =
|
||||
when (this) {
|
||||
Level0 -> "Level 0 (0dp)"
|
||||
Level1 -> "Level 1 (1dp)"
|
||||
Level2 -> "Level 2 (3dp)"
|
||||
Level3 -> "Level 3 (6dp)"
|
||||
Level4 -> "Level 4 (8dp)"
|
||||
Level5 -> "Level 5 (12dp)"
|
||||
Level0 -> "Level 0 (${ElevationTokens.Level0}dp)"
|
||||
Level1 -> "Level 1 (${ElevationTokens.Level1}dp)"
|
||||
Level2 -> "Level 2 (${ElevationTokens.Level2}dp)"
|
||||
Level3 -> "Level 3 (${ElevationTokens.Level3}dp)"
|
||||
Level4 -> "Level 4 (${ElevationTokens.Level4}dp)"
|
||||
Level5 -> "Level 5 (${ElevationTokens.Level5}dp)"
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -41,12 +42,12 @@ sealed class FeedsGroupListTonalElevationPreference(val value: Int) : Preference
|
|||
|
||||
fun fromPreferences(preferences: Preferences) =
|
||||
when (preferences[DataStoreKeys.FeedsGroupListTonalElevation.key]) {
|
||||
0 -> Level0
|
||||
1 -> Level1
|
||||
3 -> Level2
|
||||
6 -> Level3
|
||||
8 -> Level4
|
||||
12 -> Level5
|
||||
ElevationTokens.Level0 -> Level0
|
||||
ElevationTokens.Level1 -> Level1
|
||||
ElevationTokens.Level2 -> Level2
|
||||
ElevationTokens.Level3 -> Level3
|
||||
ElevationTokens.Level4 -> Level4
|
||||
ElevationTokens.Level5 -> Level5
|
||||
else -> default
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,17 +4,18 @@ import android.content.Context
|
|||
import androidx.datastore.preferences.core.Preferences
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import me.ash.reader.data.constant.ElevationTokens
|
||||
import me.ash.reader.ui.ext.DataStoreKeys
|
||||
import me.ash.reader.ui.ext.dataStore
|
||||
import me.ash.reader.ui.ext.put
|
||||
|
||||
sealed class FeedsTopBarTonalElevationPreference(val value: Int) : Preference() {
|
||||
object Level0 : FeedsTopBarTonalElevationPreference(0)
|
||||
object Level1 : FeedsTopBarTonalElevationPreference(1)
|
||||
object Level2 : FeedsTopBarTonalElevationPreference(3)
|
||||
object Level3 : FeedsTopBarTonalElevationPreference(6)
|
||||
object Level4 : FeedsTopBarTonalElevationPreference(8)
|
||||
object Level5 : FeedsTopBarTonalElevationPreference(12)
|
||||
object Level0 : FeedsTopBarTonalElevationPreference(ElevationTokens.Level0)
|
||||
object Level1 : FeedsTopBarTonalElevationPreference(ElevationTokens.Level1)
|
||||
object Level2 : FeedsTopBarTonalElevationPreference(ElevationTokens.Level2)
|
||||
object Level3 : FeedsTopBarTonalElevationPreference(ElevationTokens.Level3)
|
||||
object Level4 : FeedsTopBarTonalElevationPreference(ElevationTokens.Level4)
|
||||
object Level5 : FeedsTopBarTonalElevationPreference(ElevationTokens.Level5)
|
||||
|
||||
override fun put(context: Context, scope: CoroutineScope) {
|
||||
scope.launch {
|
||||
|
@ -27,12 +28,12 @@ sealed class FeedsTopBarTonalElevationPreference(val value: Int) : Preference()
|
|||
|
||||
fun getDesc(context: Context): String =
|
||||
when (this) {
|
||||
Level0 -> "Level 0 (0dp)"
|
||||
Level1 -> "Level 1 (1dp)"
|
||||
Level2 -> "Level 2 (3dp)"
|
||||
Level3 -> "Level 3 (6dp)"
|
||||
Level4 -> "Level 4 (8dp)"
|
||||
Level5 -> "Level 5 (12dp)"
|
||||
Level0 -> "Level 0 (${ElevationTokens.Level0}dp)"
|
||||
Level1 -> "Level 1 (${ElevationTokens.Level1}dp)"
|
||||
Level2 -> "Level 2 (${ElevationTokens.Level2}dp)"
|
||||
Level3 -> "Level 3 (${ElevationTokens.Level3}dp)"
|
||||
Level4 -> "Level 4 (${ElevationTokens.Level4}dp)"
|
||||
Level5 -> "Level 5 (${ElevationTokens.Level5}dp)"
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -41,12 +42,12 @@ sealed class FeedsTopBarTonalElevationPreference(val value: Int) : Preference()
|
|||
|
||||
fun fromPreferences(preferences: Preferences) =
|
||||
when (preferences[DataStoreKeys.FeedsTopBarTonalElevation.key]) {
|
||||
0 -> Level0
|
||||
1 -> Level1
|
||||
3 -> Level2
|
||||
6 -> Level3
|
||||
8 -> Level4
|
||||
12 -> Level5
|
||||
ElevationTokens.Level0 -> Level0
|
||||
ElevationTokens.Level1 -> Level1
|
||||
ElevationTokens.Level2 -> Level2
|
||||
ElevationTokens.Level3 -> Level3
|
||||
ElevationTokens.Level4 -> Level4
|
||||
ElevationTokens.Level5 -> Level5
|
||||
else -> default
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,17 +4,18 @@ import android.content.Context
|
|||
import androidx.datastore.preferences.core.Preferences
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import me.ash.reader.data.constant.ElevationTokens
|
||||
import me.ash.reader.ui.ext.DataStoreKeys
|
||||
import me.ash.reader.ui.ext.dataStore
|
||||
import me.ash.reader.ui.ext.put
|
||||
|
||||
sealed class FlowArticleListTonalElevationPreference(val value: Int) : Preference() {
|
||||
object Level0 : FlowArticleListTonalElevationPreference(0)
|
||||
object Level1 : FlowArticleListTonalElevationPreference(1)
|
||||
object Level2 : FlowArticleListTonalElevationPreference(3)
|
||||
object Level3 : FlowArticleListTonalElevationPreference(6)
|
||||
object Level4 : FlowArticleListTonalElevationPreference(8)
|
||||
object Level5 : FlowArticleListTonalElevationPreference(12)
|
||||
object Level0 : FlowArticleListTonalElevationPreference(ElevationTokens.Level0)
|
||||
object Level1 : FlowArticleListTonalElevationPreference(ElevationTokens.Level1)
|
||||
object Level2 : FlowArticleListTonalElevationPreference(ElevationTokens.Level2)
|
||||
object Level3 : FlowArticleListTonalElevationPreference(ElevationTokens.Level3)
|
||||
object Level4 : FlowArticleListTonalElevationPreference(ElevationTokens.Level4)
|
||||
object Level5 : FlowArticleListTonalElevationPreference(ElevationTokens.Level5)
|
||||
|
||||
override fun put(context: Context, scope: CoroutineScope) {
|
||||
scope.launch {
|
||||
|
@ -27,12 +28,12 @@ sealed class FlowArticleListTonalElevationPreference(val value: Int) : Preferenc
|
|||
|
||||
fun getDesc(context: Context): String =
|
||||
when (this) {
|
||||
Level0 -> "Level 0 (0dp)"
|
||||
Level1 -> "Level 1 (1dp)"
|
||||
Level2 -> "Level 2 (3dp)"
|
||||
Level3 -> "Level 3 (6dp)"
|
||||
Level4 -> "Level 4 (8dp)"
|
||||
Level5 -> "Level 5 (12dp)"
|
||||
Level0 -> "Level 0 (${ElevationTokens.Level0}dp)"
|
||||
Level1 -> "Level 1 (${ElevationTokens.Level1}dp)"
|
||||
Level2 -> "Level 2 (${ElevationTokens.Level2}dp)"
|
||||
Level3 -> "Level 3 (${ElevationTokens.Level3}dp)"
|
||||
Level4 -> "Level 4 (${ElevationTokens.Level4}dp)"
|
||||
Level5 -> "Level 5 (${ElevationTokens.Level5}dp)"
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -41,12 +42,12 @@ sealed class FlowArticleListTonalElevationPreference(val value: Int) : Preferenc
|
|||
|
||||
fun fromPreferences(preferences: Preferences) =
|
||||
when (preferences[DataStoreKeys.FlowArticleListTonalElevation.key]) {
|
||||
0 -> Level0
|
||||
1 -> Level1
|
||||
3 -> Level2
|
||||
6 -> Level3
|
||||
8 -> Level4
|
||||
12 -> Level5
|
||||
ElevationTokens.Level0 -> Level0
|
||||
ElevationTokens.Level1 -> Level1
|
||||
ElevationTokens.Level2 -> Level2
|
||||
ElevationTokens.Level3 -> Level3
|
||||
ElevationTokens.Level4 -> Level4
|
||||
ElevationTokens.Level5 -> Level5
|
||||
else -> default
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,17 +4,18 @@ import android.content.Context
|
|||
import androidx.datastore.preferences.core.Preferences
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import me.ash.reader.data.constant.ElevationTokens
|
||||
import me.ash.reader.ui.ext.DataStoreKeys
|
||||
import me.ash.reader.ui.ext.dataStore
|
||||
import me.ash.reader.ui.ext.put
|
||||
|
||||
sealed class FlowFilterBarTonalElevationPreference(val value: Int) : Preference() {
|
||||
object Level0 : FlowFilterBarTonalElevationPreference(0)
|
||||
object Level1 : FlowFilterBarTonalElevationPreference(1)
|
||||
object Level2 : FlowFilterBarTonalElevationPreference(3)
|
||||
object Level3 : FlowFilterBarTonalElevationPreference(6)
|
||||
object Level4 : FlowFilterBarTonalElevationPreference(8)
|
||||
object Level5 : FlowFilterBarTonalElevationPreference(12)
|
||||
object Level0 : FlowFilterBarTonalElevationPreference(ElevationTokens.Level0)
|
||||
object Level1 : FlowFilterBarTonalElevationPreference(ElevationTokens.Level1)
|
||||
object Level2 : FlowFilterBarTonalElevationPreference(ElevationTokens.Level2)
|
||||
object Level3 : FlowFilterBarTonalElevationPreference(ElevationTokens.Level3)
|
||||
object Level4 : FlowFilterBarTonalElevationPreference(ElevationTokens.Level4)
|
||||
object Level5 : FlowFilterBarTonalElevationPreference(ElevationTokens.Level5)
|
||||
|
||||
override fun put(context: Context, scope: CoroutineScope) {
|
||||
scope.launch {
|
||||
|
@ -27,12 +28,12 @@ sealed class FlowFilterBarTonalElevationPreference(val value: Int) : Preference(
|
|||
|
||||
fun getDesc(context: Context): String =
|
||||
when (this) {
|
||||
Level0 -> "Level 0 (0dp)"
|
||||
Level1 -> "Level 1 (1dp)"
|
||||
Level2 -> "Level 2 (3dp)"
|
||||
Level3 -> "Level 3 (6dp)"
|
||||
Level4 -> "Level 4 (8dp)"
|
||||
Level5 -> "Level 5 (12dp)"
|
||||
Level0 -> "Level 0 (${ElevationTokens.Level0}dp)"
|
||||
Level1 -> "Level 1 (${ElevationTokens.Level1}dp)"
|
||||
Level2 -> "Level 2 (${ElevationTokens.Level2}dp)"
|
||||
Level3 -> "Level 3 (${ElevationTokens.Level3}dp)"
|
||||
Level4 -> "Level 4 (${ElevationTokens.Level4}dp)"
|
||||
Level5 -> "Level 5 (${ElevationTokens.Level5}dp)"
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -41,12 +42,12 @@ sealed class FlowFilterBarTonalElevationPreference(val value: Int) : Preference(
|
|||
|
||||
fun fromPreferences(preferences: Preferences) =
|
||||
when (preferences[DataStoreKeys.FlowFilterBarTonalElevation.key]) {
|
||||
0 -> Level0
|
||||
1 -> Level1
|
||||
3 -> Level2
|
||||
6 -> Level3
|
||||
8 -> Level4
|
||||
12 -> Level5
|
||||
ElevationTokens.Level0 -> Level0
|
||||
ElevationTokens.Level1 -> Level1
|
||||
ElevationTokens.Level2 -> Level2
|
||||
ElevationTokens.Level3 -> Level3
|
||||
ElevationTokens.Level4 -> Level4
|
||||
ElevationTokens.Level5 -> Level5
|
||||
else -> default
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,17 +4,18 @@ import android.content.Context
|
|||
import androidx.datastore.preferences.core.Preferences
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import me.ash.reader.data.constant.ElevationTokens
|
||||
import me.ash.reader.ui.ext.DataStoreKeys
|
||||
import me.ash.reader.ui.ext.dataStore
|
||||
import me.ash.reader.ui.ext.put
|
||||
|
||||
sealed class FlowTopBarTonalElevationPreference(val value: Int) : Preference() {
|
||||
object Level0 : FlowTopBarTonalElevationPreference(0)
|
||||
object Level1 : FlowTopBarTonalElevationPreference(1)
|
||||
object Level2 : FlowTopBarTonalElevationPreference(3)
|
||||
object Level3 : FlowTopBarTonalElevationPreference(6)
|
||||
object Level4 : FlowTopBarTonalElevationPreference(8)
|
||||
object Level5 : FlowTopBarTonalElevationPreference(12)
|
||||
object Level0 : FlowTopBarTonalElevationPreference(ElevationTokens.Level0)
|
||||
object Level1 : FlowTopBarTonalElevationPreference(ElevationTokens.Level1)
|
||||
object Level2 : FlowTopBarTonalElevationPreference(ElevationTokens.Level2)
|
||||
object Level3 : FlowTopBarTonalElevationPreference(ElevationTokens.Level3)
|
||||
object Level4 : FlowTopBarTonalElevationPreference(ElevationTokens.Level4)
|
||||
object Level5 : FlowTopBarTonalElevationPreference(ElevationTokens.Level5)
|
||||
|
||||
override fun put(context: Context, scope: CoroutineScope) {
|
||||
scope.launch {
|
||||
|
@ -27,12 +28,12 @@ sealed class FlowTopBarTonalElevationPreference(val value: Int) : Preference() {
|
|||
|
||||
fun getDesc(context: Context): String =
|
||||
when (this) {
|
||||
Level0 -> "Level 0 (0dp)"
|
||||
Level1 -> "Level 1 (1dp)"
|
||||
Level2 -> "Level 2 (3dp)"
|
||||
Level3 -> "Level 3 (6dp)"
|
||||
Level4 -> "Level 4 (8dp)"
|
||||
Level5 -> "Level 5 (12dp)"
|
||||
Level0 -> "Level 0 (${ElevationTokens.Level0}dp)"
|
||||
Level1 -> "Level 1 (${ElevationTokens.Level1}dp)"
|
||||
Level2 -> "Level 2 (${ElevationTokens.Level2}dp)"
|
||||
Level3 -> "Level 3 (${ElevationTokens.Level3}dp)"
|
||||
Level4 -> "Level 4 (${ElevationTokens.Level4}dp)"
|
||||
Level5 -> "Level 5 (${ElevationTokens.Level5}dp)"
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -41,12 +42,12 @@ sealed class FlowTopBarTonalElevationPreference(val value: Int) : Preference() {
|
|||
|
||||
fun fromPreferences(preferences: Preferences) =
|
||||
when (preferences[DataStoreKeys.FlowTopBarTonalElevation.key]) {
|
||||
0 -> Level0
|
||||
1 -> Level1
|
||||
3 -> Level2
|
||||
6 -> Level3
|
||||
8 -> Level4
|
||||
12 -> Level5
|
||||
ElevationTokens.Level0 -> Level0
|
||||
ElevationTokens.Level1 -> Level1
|
||||
ElevationTokens.Level2 -> Level2
|
||||
ElevationTokens.Level3 -> Level3
|
||||
ElevationTokens.Level4 -> Level4
|
||||
ElevationTokens.Level5 -> Level5
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
sealed class Preference {
|
||||
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.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
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.dataStore
|
||||
|
||||
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 customPrimaryColor: String = CustomPrimaryColorPreference.default,
|
||||
val darkTheme: DarkThemePreference = DarkThemePreference.default,
|
||||
|
@ -38,43 +45,12 @@ data class Settings(
|
|||
val flowArticleListDateStickyHeader: FlowArticleListDateStickyHeaderPreference = FlowArticleListDateStickyHeaderPreference.default,
|
||||
val flowArticleListTonalElevation: FlowArticleListTonalElevationPreference = FlowArticleListTonalElevationPreference.default,
|
||||
|
||||
val initialPage: InitialPagePreference = InitialPagePreference.default,
|
||||
val initialFilter: InitialFilterPreference = InitialFilterPreference.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
|
||||
fun SettingsProvider(
|
||||
content: @Composable () -> Unit,
|
||||
|
@ -88,6 +64,13 @@ fun SettingsProvider(
|
|||
}.collectAsStateValue(initial = Settings())
|
||||
|
||||
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,
|
||||
LocalCustomPrimaryColor provides settings.customPrimaryColor,
|
||||
LocalDarkTheme provides settings.darkTheme,
|
||||
|
@ -114,12 +97,22 @@ fun SettingsProvider(
|
|||
LocalFlowFilterBarPadding provides settings.flowFilterBarPadding,
|
||||
LocalFlowFilterBarTonalElevation provides settings.flowFilterBarTonalElevation,
|
||||
|
||||
LocalInitialPage provides settings.initialPage,
|
||||
LocalInitialFilter provides settings.initialFilter,
|
||||
|
||||
LocalLanguages provides settings.languages,
|
||||
) {
|
||||
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 =
|
||||
compositionLocalOf { ThemeIndexPreference.default }
|
||||
val LocalCustomPrimaryColor =
|
||||
|
@ -169,5 +162,9 @@ val LocalFlowArticleListDateStickyHeader =
|
|||
val LocalFlowArticleListTonalElevation =
|
||||
compositionLocalOf<FlowArticleListTonalElevationPreference> { FlowArticleListTonalElevationPreference.default }
|
||||
|
||||
val LocalInitialPage = compositionLocalOf<InitialPagePreference> { InitialPagePreference.default }
|
||||
val LocalInitialFilter =
|
||||
compositionLocalOf<InitialFilterPreference> { InitialFilterPreference.default }
|
||||
|
||||
val LocalLanguages =
|
||||
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.util.Log
|
||||
import androidx.hilt.work.HiltWorker
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.work.*
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.ExistingPeriodicWorkPolicy
|
||||
import androidx.work.ListenableWorker
|
||||
import androidx.work.WorkManager
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import me.ash.reader.data.dao.AccountDao
|
||||
import me.ash.reader.data.dao.ArticleDao
|
||||
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.ui.ext.currentAccountId
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
abstract class AbstractRssRepository constructor(
|
||||
private val context: Context,
|
||||
|
@ -99,7 +99,7 @@ abstract class AbstractRssRepository constructor(
|
|||
fun pullImportant(
|
||||
isStarred: Boolean = false,
|
||||
isUnread: Boolean = false,
|
||||
): Flow<List<ImportantCount>> {
|
||||
): Flow<Map<String, Int>> {
|
||||
val accountId = context.currentAccountId
|
||||
Log.i(
|
||||
"RLog",
|
||||
|
@ -111,6 +111,12 @@ abstract class AbstractRssRepository constructor(
|
|||
isUnread -> articleDao
|
||||
.queryImportantCountWhenIsUnread(accountId, isUnread)
|
||||
else -> articleDao.queryImportantCountWhenIsAll(accountId)
|
||||
}.mapLatest {
|
||||
mapOf(
|
||||
*(it.map {
|
||||
it.feedId to it.important
|
||||
}.toTypedArray())
|
||||
)
|
||||
}.flowOn(dispatcherIO)
|
||||
}
|
||||
|
||||
|
@ -130,10 +136,6 @@ abstract class AbstractRssRepository constructor(
|
|||
return feedDao.queryByLink(context.currentAccountId, url).isNotEmpty()
|
||||
}
|
||||
|
||||
fun peekWork(): String {
|
||||
return workManager.getWorkInfosByTag("sync").get().size.toString()
|
||||
}
|
||||
|
||||
suspend fun updateGroup(group: 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
|
||||
|
||||
import android.app.*
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.BitmapFactory
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.ListenableWorker
|
||||
import androidx.work.WorkManager
|
||||
|
@ -15,8 +10,6 @@ import kotlinx.coroutines.CoroutineDispatcher
|
|||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
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.ArticleDao
|
||||
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.ui.ext.currentAccountId
|
||||
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 javax.inject.Inject
|
||||
|
||||
|
@ -41,6 +32,7 @@ class LocalRssRepository @Inject constructor(
|
|||
private val articleDao: ArticleDao,
|
||||
private val feedDao: FeedDao,
|
||||
private val rssHelper: RssHelper,
|
||||
private val notificationHelper: NotificationHelper,
|
||||
private val accountDao: AccountDao,
|
||||
private val groupDao: GroupDao,
|
||||
@DispatcherDefault
|
||||
|
@ -52,16 +44,6 @@ class LocalRssRepository @Inject constructor(
|
|||
context, accountDao, articleDao, groupDao,
|
||||
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) {
|
||||
articleDao.update(article)
|
||||
|
@ -98,7 +80,7 @@ class LocalRssRepository @Inject constructor(
|
|||
.awaitAll()
|
||||
.forEach {
|
||||
if (it.isNotify) {
|
||||
notify(
|
||||
notificationHelper.notify(
|
||||
FeedWithArticle(
|
||||
it.feedWithArticle.feed,
|
||||
articleDao.insertIfNotExist(it.feedWithArticle.articles)
|
||||
|
@ -183,75 +165,4 @@ class LocalRssRepository @Inject constructor(
|
|||
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)
|
||||
}
|
||||
}
|
||||
feedDao.insertList((groupWithFeed.feeds subtract repeatList).toList())
|
||||
feedDao.insertList((groupWithFeed.feeds subtract repeatList.toSet()).toList())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -54,7 +54,7 @@ class OpmlRepository @Inject constructor(
|
|||
Opml(
|
||||
"2.0",
|
||||
Head(
|
||||
accountDao.queryById(context.currentAccountId).name,
|
||||
accountDao.queryById(context.currentAccountId)?.name,
|
||||
Date().toString(), null, null, null,
|
||||
null, null, null, null,
|
||||
null, null, null, null,
|
||||
|
|
|
@ -4,27 +4,28 @@ import android.content.Context
|
|||
import android.util.Log
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.withContext
|
||||
import me.ash.reader.R
|
||||
import me.ash.reader.data.entity.toVersion
|
||||
import me.ash.reader.data.module.ApplicationScope
|
||||
import me.ash.reader.data.model.toVersion
|
||||
import me.ash.reader.data.module.DispatcherIO
|
||||
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.RYNetworkDataSource
|
||||
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
|
||||
|
||||
class AppRepository @Inject constructor(
|
||||
class RYRepository @Inject constructor(
|
||||
@ApplicationContext
|
||||
private val context: Context,
|
||||
private val appNetworkDataSource: AppNetworkDataSource,
|
||||
@ApplicationScope
|
||||
private val applicationScope: CoroutineScope,
|
||||
private val RYNetworkDataSource: RYNetworkDataSource,
|
||||
@DispatcherIO
|
||||
private val dispatcherIO: CoroutineDispatcher,
|
||||
@DispatcherMain
|
||||
|
@ -33,7 +34,7 @@ class AppRepository @Inject constructor(
|
|||
suspend fun checkUpdate(showToast: Boolean = true): Boolean? = withContext(dispatcherIO) {
|
||||
try {
|
||||
val response =
|
||||
appNetworkDataSource.getReleaseLatest(context.getString(R.string.update_link))
|
||||
RYNetworkDataSource.getReleaseLatest(context.getString(R.string.update_link))
|
||||
when {
|
||||
response.code() == 403 -> {
|
||||
withContext(dispatcherMain) {
|
||||
|
@ -50,31 +51,22 @@ class AppRepository @Inject constructor(
|
|||
}
|
||||
val latest = response.body()!!
|
||||
val latestVersion = latest.tag_name.toVersion()
|
||||
// val latestVersion = "0.7.3".toVersion()
|
||||
// val latestVersion = "1.0.0".toVersion()
|
||||
val skipVersion = context.skipVersionNumber.toVersion()
|
||||
val currentVersion = context.getCurrentVersion()
|
||||
val latestLog = latest.body ?: ""
|
||||
val latestPublishDate = latest.published_at ?: latest.created_at ?: ""
|
||||
val latestSize = latest.assets
|
||||
?.first()
|
||||
?.size
|
||||
?: 0
|
||||
val latestDownloadUrl = latest.assets
|
||||
?.first()
|
||||
?.browser_download_url
|
||||
?: ""
|
||||
val latestSize = latest.assets?.first()?.size ?: 0
|
||||
val latestDownloadUrl = latest.assets?.first()?.browser_download_url ?: ""
|
||||
|
||||
Log.i("RLog", "current version $currentVersion")
|
||||
if (latestVersion.whetherNeedUpdate(currentVersion, skipVersion)) {
|
||||
Log.i("RLog", "new version $latestVersion")
|
||||
context.dataStore.put(
|
||||
DataStoreKeys.NewVersionNumber,
|
||||
latestVersion.toString()
|
||||
)
|
||||
context.dataStore.put(DataStoreKeys.NewVersionLog, latestLog)
|
||||
context.dataStore.put(DataStoreKeys.NewVersionPublishDate, latestPublishDate)
|
||||
context.dataStore.put(DataStoreKeys.NewVersionSize, latestSize)
|
||||
context.dataStore.put(DataStoreKeys.NewVersionDownloadUrl, latestDownloadUrl)
|
||||
NewVersionNumberPreference.put(context, this, latestVersion.toString())
|
||||
NewVersionLogPreference.put(context, this, latestLog)
|
||||
NewVersionPublishDatePreference.put(context, this, latestPublishDate)
|
||||
NewVersionSizePreference.put(context, this, latestSize.formatSize())
|
||||
NewVersionDownloadUrlPreference.put(context, this, latestDownloadUrl)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
|
@ -93,7 +85,7 @@ class AppRepository @Inject constructor(
|
|||
withContext(dispatcherIO) {
|
||||
Log.i("RLog", "downloadFile start: $url")
|
||||
try {
|
||||
return@withContext appNetworkDataSource.downloadFile(url)
|
||||
return@withContext RYNetworkDataSource.downloadFile(url)
|
||||
.downloadToFileWithProgress(context.getLatestApk())
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
|
@ -21,8 +21,6 @@ import net.dankito.readability4j.extended.Readability4JExtended
|
|||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import java.net.URL
|
||||
import java.text.ParsePosition
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -31,6 +29,7 @@ class RssHelper @Inject constructor(
|
|||
private val context: Context,
|
||||
@DispatcherIO
|
||||
private val dispatcherIO: CoroutineDispatcher,
|
||||
private val okHttpClient: OkHttpClient,
|
||||
) {
|
||||
@Throws(Exception::class)
|
||||
suspend fun searchFeed(feedLink: String): FeedWithArticle {
|
||||
|
@ -58,7 +57,7 @@ class RssHelper @Inject constructor(
|
|||
@Throws(Exception::class)
|
||||
suspend fun parseFullContent(link: String, title: String): String {
|
||||
return withContext(dispatcherIO) {
|
||||
val response = OkHttpClient()
|
||||
val response = okHttpClient
|
||||
.newCall(Request.Builder().url(link).build())
|
||||
.execute()
|
||||
val content = response.body!!.string()
|
||||
|
@ -85,7 +84,12 @@ class RssHelper @Inject constructor(
|
|||
return withContext(dispatcherIO) {
|
||||
val a = mutableListOf<Article>()
|
||||
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 {
|
||||
if (latestLink != null && latestLink == it.link) return@withContext a
|
||||
val desc = it.description?.value
|
||||
|
@ -110,13 +114,13 @@ class RssHelper @Inject constructor(
|
|||
date = it.publishedDate ?: it.updatedDate ?: Date(),
|
||||
title = Html.fromHtml(it.title.toString()).toString(),
|
||||
author = it.author,
|
||||
rawDescription = (desc ?: content) ?: "",
|
||||
rawDescription = (content ?: desc) ?: "",
|
||||
shortDescription = (Readability4JExtended("", desc ?: content ?: "")
|
||||
.parse().textContent ?: "")
|
||||
.take(100)
|
||||
.trim(),
|
||||
fullContent = content,
|
||||
img = findImg((desc ?: content) ?: ""),
|
||||
img = findImg((content ?: desc) ?: ""),
|
||||
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,
|
||||
) {
|
||||
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],
|
||||
version = 2,
|
||||
)
|
||||
@TypeConverters(ReaderDatabase.Converters::class)
|
||||
abstract class ReaderDatabase : RoomDatabase() {
|
||||
@TypeConverters(RYDatabase.Converters::class)
|
||||
abstract class RYDatabase : RoomDatabase() {
|
||||
abstract fun accountDao(): AccountDao
|
||||
abstract fun feedDao(): FeedDao
|
||||
abstract fun articleDao(): ArticleDao
|
||||
abstract fun groupDao(): GroupDao
|
||||
|
||||
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) {
|
||||
instance ?: Room.databaseBuilder(
|
||||
context.applicationContext,
|
||||
ReaderDatabase::class.java,
|
||||
RYDatabase::class.java,
|
||||
"Reader"
|
||||
).addMigrations(*allMigrations).build().also {
|
||||
instance = it
|
|
@ -5,7 +5,6 @@ import kotlinx.coroutines.flow.Flow
|
|||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import me.ash.reader.data.entity.LatestRelease
|
||||
import okhttp3.ResponseBody
|
||||
import retrofit2.Response
|
||||
import retrofit2.Retrofit
|
||||
|
@ -15,13 +14,7 @@ import retrofit2.http.Streaming
|
|||
import retrofit2.http.Url
|
||||
import java.io.File
|
||||
|
||||
sealed class Download {
|
||||
object NotYet : Download()
|
||||
data class Progress(val percent: Int) : Download()
|
||||
data class Finished(val file: File) : Download()
|
||||
}
|
||||
|
||||
interface AppNetworkDataSource {
|
||||
interface RYNetworkDataSource {
|
||||
@GET
|
||||
suspend fun getReleaseLatest(@Url url: String): Response<LatestRelease>
|
||||
|
||||
|
@ -30,14 +23,14 @@ interface AppNetworkDataSource {
|
|||
suspend fun downloadFile(@Url url: String): ResponseBody
|
||||
|
||||
companion object {
|
||||
private var instance: AppNetworkDataSource? = null
|
||||
private var instance: RYNetworkDataSource? = null
|
||||
|
||||
fun getInstance(): AppNetworkDataSource {
|
||||
fun getInstance(): RYNetworkDataSource {
|
||||
return instance ?: synchronized(this) {
|
||||
instance ?: Retrofit.Builder()
|
||||
.baseUrl("https://api.github.com/")
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.build().create(AppNetworkDataSource::class.java).also {
|
||||
.build().create(RYNetworkDataSource::class.java).also {
|
||||
instance = it
|
||||
}
|
||||
}
|
||||
|
@ -93,3 +86,31 @@ fun ResponseBody.downloadToFileWithProgress(saveFile: File): Flow<Download> =
|
|||
}
|
||||
}
|
||||
}.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.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.view.SoundEffectConstants
|
||||
|
@ -12,16 +12,15 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
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.LocalThemeIndex
|
||||
import me.ash.reader.ui.ext.getName
|
||||
import me.ash.reader.ui.ext.surfaceColorAtElevation
|
||||
import me.ash.reader.ui.theme.palette.onDark
|
||||
|
||||
@Composable
|
||||
fun FilterBar(
|
||||
modifier: Modifier = Modifier,
|
||||
filter: Filter,
|
||||
filterBarStyle: Int,
|
||||
filterBarFilled: Boolean,
|
||||
|
@ -39,11 +38,7 @@ fun FilterBar(
|
|||
tonalElevation = filterBarTonalElevation,
|
||||
) {
|
||||
Spacer(modifier = Modifier.width(filterBarPadding))
|
||||
listOf(
|
||||
Filter.Starred,
|
||||
Filter.Unread,
|
||||
Filter.All,
|
||||
).forEach { item ->
|
||||
Filter.values.forEach { item ->
|
||||
NavigationBarItem(
|
||||
// modifier = Modifier.height(60.dp),
|
||||
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.statusBars
|
||||
import androidx.compose.runtime.Composable
|
||||
|
@ -38,11 +38,7 @@ fun AnimatedPopup(
|
|||
}
|
||||
},
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = fadeOut() + shrinkVertically(),
|
||||
) {
|
||||
RYExtensibleVisibility(visible = visible) {
|
||||
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.core.FastOutSlowInEasing
|
|
@ -6,7 +6,7 @@
|
|||
* @modifier Ashinch
|
||||
*/
|
||||
|
||||
package me.ash.reader.ui.component
|
||||
package me.ash.reader.ui.component.base
|
||||
|
||||
import android.view.SoundEffectConstants
|
||||
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.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.runtime.Composable
|
||||
|
@ -7,7 +7,7 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun BlockRadioGroupButton(
|
||||
fun BlockRadioButton(
|
||||
modifier: Modifier = Modifier,
|
||||
selected: Int = 0,
|
||||
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.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.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.layout.Column
|
||||
|
@ -32,7 +32,7 @@ fun ClipboardTextField(
|
|||
) {
|
||||
Column(modifier = modifier) {
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
TextField(
|
||||
RYTextField(
|
||||
readOnly = readOnly,
|
||||
value = value,
|
||||
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.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.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
|
@ -41,11 +41,7 @@ fun DisplayText(
|
|||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
AnimatedVisibility(
|
||||
visible = desc.isNotEmpty(),
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = fadeOut() + shrinkVertically(),
|
||||
) {
|
||||
RYExtensibleVisibility(visible = desc.isNotEmpty()) {
|
||||
Text(
|
||||
modifier = Modifier.height(16.dp),
|
||||
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 androidx.compose.animation.Crossfade
|
||||
|
@ -41,7 +41,7 @@ fun DynamicSVGImage(
|
|||
},
|
||||
) {
|
||||
Crossfade(targetState = pic) {
|
||||
AsyncImage(
|
||||
RYAsyncImage(
|
||||
contentDescription = contentDescription,
|
||||
data = it,
|
||||
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.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.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.compose.material3.MaterialTheme
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.Modifier
|
||||
|
@ -10,19 +10,16 @@ import androidx.compose.ui.graphics.DefaultAlpha
|
|||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import coil.compose.LocalImageLoader
|
||||
import coil.request.ImageRequest
|
||||
import coil.compose.rememberImagePainter
|
||||
import coil.size.Precision
|
||||
import coil.size.Scale
|
||||
import coil.size.Size
|
||||
import me.ash.reader.R
|
||||
|
||||
val Size_1000 = Size(1000, 1000)
|
||||
val SIZE_1000 = Size(1000, 1000)
|
||||
|
||||
@Composable
|
||||
fun AsyncImage(
|
||||
fun RYAsyncImage(
|
||||
modifier: Modifier = Modifier,
|
||||
data: Any? = null,
|
||||
size: Size = Size.ORIGINAL,
|
||||
|
@ -33,34 +30,51 @@ fun AsyncImage(
|
|||
@DrawableRes placeholder: Int? = R.drawable.ic_hourglass_empty_black_24dp,
|
||||
@DrawableRes error: Int? = R.drawable.ic_broken_image_black_24dp,
|
||||
) {
|
||||
coil.compose.AsyncImage(
|
||||
modifier = modifier,
|
||||
model = ImageRequest
|
||||
.Builder(LocalContext.current)
|
||||
.data(data)
|
||||
.crossfade(true)
|
||||
.scale(scale)
|
||||
.precision(precision)
|
||||
.size(size)
|
||||
.build(),
|
||||
Image(
|
||||
painter = rememberImagePainter(
|
||||
data = data,
|
||||
builder = {
|
||||
if (placeholder != null) placeholder(placeholder)
|
||||
if (error != null) error(error)
|
||||
crossfade(true)
|
||||
scale(scale)
|
||||
precision(precision)
|
||||
size(size)
|
||||
},
|
||||
),
|
||||
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,
|
||||
)
|
||||
},
|
||||
modifier = modifier,
|
||||
)
|
||||
|
||||
// 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
|
|
@ -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.runtime.Composable
|
||||
|
@ -6,7 +6,7 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.window.DialogProperties
|
||||
|
||||
@Composable
|
||||
fun Dialog(
|
||||
fun RYDialog(
|
||||
modifier: Modifier = Modifier,
|
||||
visible: Boolean,
|
||||
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.interaction.MutableInteractionSource
|
||||
|
@ -26,7 +26,7 @@ import me.ash.reader.ui.theme.palette.alwaysLight
|
|||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun SelectionChip(
|
||||
fun RYSelectionChip(
|
||||
content: String,
|
||||
selected: Boolean,
|
||||
modifier: Modifier = Modifier,
|
|
@ -6,7 +6,7 @@
|
|||
* @modifier Ashinch
|
||||
*/
|
||||
|
||||
package me.ash.reader.ui.component
|
||||
package me.ash.reader.ui.component.base
|
||||
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
|
@ -31,7 +31,7 @@ import me.ash.reader.ui.theme.palette.onDark
|
|||
|
||||
// TODO: ripple & swipe
|
||||
@Composable
|
||||
fun Switch(
|
||||
fun RYSwitch(
|
||||
modifier: Modifier = Modifier,
|
||||
activated: Boolean,
|
||||
enable: Boolean = true,
|
||||
|
@ -101,7 +101,7 @@ fun SwitchHeadline(
|
|||
)
|
||||
}
|
||||
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.KeyboardOptions
|
||||
|
@ -19,7 +19,7 @@ import kotlinx.coroutines.delay
|
|||
import me.ash.reader.R
|
||||
|
||||
@Composable
|
||||
fun TextField(
|
||||
fun RYTextField(
|
||||
readOnly: Boolean,
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
|
@ -39,7 +39,7 @@ fun TextField(
|
|||
TextField(
|
||||
modifier = Modifier.focusRequester(focusRequester),
|
||||
colors = TextFieldDefaults.textFieldColors(
|
||||
backgroundColor = Color.Transparent,
|
||||
containerColor = Color.Transparent,
|
||||
),
|
||||
maxLines = 1,
|
||||
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.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.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||
|
||||
@OptIn(ExperimentalPagerApi::class, ExperimentalMaterial3Api::class)
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun RadioDialog(
|
||||
modifier: Modifier = Modifier,
|
||||
|
@ -30,7 +29,7 @@ fun RadioDialog(
|
|||
options: List<RadioDialogOption> = emptyList(),
|
||||
onDismissRequest: () -> Unit = {},
|
||||
) {
|
||||
Dialog(
|
||||
RYDialog(
|
||||
modifier = modifier,
|
||||
visible = visible,
|
||||
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.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.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.MaterialTheme
|
||||
|
@ -13,10 +13,8 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||
import me.ash.reader.R
|
||||
|
||||
@OptIn(ExperimentalPagerApi::class)
|
||||
@Composable
|
||||
fun TextFieldDialog(
|
||||
modifier: Modifier = Modifier,
|
||||
|
@ -37,7 +35,7 @@ fun TextFieldDialog(
|
|||
) {
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
Dialog(
|
||||
RYDialog(
|
||||
modifier = modifier,
|
||||
visible = visible,
|
||||
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.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.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.net.Uri
|
|
@ -33,7 +33,6 @@ import androidx.compose.foundation.text.selection.DisableSelection
|
|||
import androidx.compose.material.Text
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ExperimentalComposeApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
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.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.annotation.ExperimentalCoilApi
|
||||
import coil.size.Precision
|
||||
import coil.size.Size
|
||||
import coil.size.pxOrElse
|
||||
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.helper.StringUtil
|
||||
import org.jsoup.nodes.Element
|
||||
|
@ -178,7 +176,6 @@ private fun LazyListScope.formatCodeBlock(
|
|||
composer.terminateCurrentText()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalComposeApi::class, ExperimentalCoilApi::class)
|
||||
private fun TextComposer.appendTextChildren(
|
||||
nodes: List<Node>,
|
||||
preFormatted: Boolean = false,
|
||||
|
@ -241,7 +238,7 @@ private fun TextComposer.appendTextChildren(
|
|||
withComposableStyle(
|
||||
style = { h5Style().toSpanStyle() }
|
||||
) {
|
||||
append(element.text())
|
||||
append("\n${element.text()}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -250,7 +247,7 @@ private fun TextComposer.appendTextChildren(
|
|||
withComposableStyle(
|
||||
style = { h5Style().toSpanStyle() }
|
||||
) {
|
||||
append(element.text())
|
||||
append("\n${element.text()}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -259,7 +256,7 @@ private fun TextComposer.appendTextChildren(
|
|||
withComposableStyle(
|
||||
style = { h5Style().toSpanStyle() }
|
||||
) {
|
||||
append(element.text())
|
||||
append("\n${element.text()}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -268,7 +265,7 @@ private fun TextComposer.appendTextChildren(
|
|||
withComposableStyle(
|
||||
style = { h5Style().toSpanStyle() }
|
||||
) {
|
||||
append(element.text())
|
||||
append("\n${element.text()}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -277,7 +274,7 @@ private fun TextComposer.appendTextChildren(
|
|||
withComposableStyle(
|
||||
style = { h5Style().toSpanStyle() }
|
||||
) {
|
||||
append(element.text())
|
||||
append("\n${element.text()}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -286,7 +283,7 @@ private fun TextComposer.appendTextChildren(
|
|||
withComposableStyle(
|
||||
style = { h5Style().toSpanStyle() }
|
||||
) {
|
||||
append(element.text())
|
||||
append("\n${element.text()}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -445,6 +442,7 @@ private fun TextComposer.appendTextChildren(
|
|||
// .padding(horizontal = PADDING_HORIZONTAL.dp)
|
||||
.width(MAX_CONTENT_WIDTH.dp)
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(PADDING_HORIZONTAL.dp))
|
||||
DisableSelection {
|
||||
BoxWithConstraints(
|
||||
modifier = Modifier
|
||||
|
@ -468,8 +466,12 @@ private fun TextComposer.appendTextChildren(
|
|||
// }
|
||||
) {
|
||||
val imageSize = maxImageSize()
|
||||
AsyncImage(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
RYAsyncImage(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = PADDING_HORIZONTAL.dp)
|
||||
.clip(IMAGE_SHAPE)
|
||||
.clickable { },
|
||||
data = imageCandidates.getBestImageForMaxSize(
|
||||
pixelDensity = pixelDensity(),
|
||||
maxSize = imageSize,
|
||||
|
@ -594,12 +596,14 @@ private fun TextComposer.appendTextChildren(
|
|||
BoxWithConstraints(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
AsyncImage(
|
||||
RYAsyncImage(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = PADDING_HORIZONTAL.dp)
|
||||
.clip(IMAGE_SHAPE)
|
||||
.clickable {
|
||||
onLinkClick(video.link)
|
||||
}
|
||||
.fillMaxWidth(),
|
||||
},
|
||||
data = video.imageUrl,
|
||||
size = maxImageSize(),
|
||||
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()) {
|
||||
"monospace" -> FontFamily.Monospace
|
||||
"serif" -> FontFamily.Serif
|
||||
|
|
|
@ -27,7 +27,8 @@ import android.util.Log
|
|||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import me.ash.reader.R
|
||||
|
||||
fun LazyListScope.reader(
|
||||
@Suppress("FunctionName")
|
||||
fun LazyListScope.Reader(
|
||||
context: Context,
|
||||
link: String,
|
||||
content: String,
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
|
||||
package me.ash.reader.ui.component.reader
|
||||
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
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.font.FontFamily
|
||||
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.unit.dp
|
||||
import me.ash.reader.ui.ext.alphaLN
|
||||
|
||||
const val PADDING_HORIZONTAL = 24.0
|
||||
const val MAX_CONTENT_WIDTH = 840.0
|
||||
val IMAGE_SHAPE = RoundedCornerShape(32.dp)
|
||||
|
||||
@Composable
|
||||
fun bodyForeground(): Color =
|
||||
|
@ -71,7 +74,7 @@ fun h4Style(): TextStyle =
|
|||
@Composable
|
||||
fun h5Style(): TextStyle =
|
||||
MaterialTheme.typography.headlineSmall.copy(
|
||||
color = bodyForeground()
|
||||
color = bodyForeground(),
|
||||
)
|
||||
|
||||
@Composable
|
||||
|
@ -83,7 +86,8 @@ fun h6Style(): TextStyle =
|
|||
@Composable
|
||||
fun captionStyle(): TextStyle =
|
||||
MaterialTheme.typography.bodySmall.copy(
|
||||
color = bodyForeground().copy(alpha = 0.6f)
|
||||
color = bodyForeground().copy(alpha = 0.6f),
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
|
||||
@Composable
|
||||
|
|
|
@ -79,7 +79,7 @@ class TextComposer(
|
|||
) -> R
|
||||
): R {
|
||||
val url = link ?: findClosestLink()
|
||||
builder.ensureDoubleNewline()
|
||||
//builder.ensureDoubleNewline()
|
||||
terminateCurrentText()
|
||||
val onClick: (() -> Unit)? = if (url?.isNotBlank() == true) {
|
||||
{
|
||||
|
|
|
@ -1,16 +1,19 @@
|
|||
package me.ash.reader.ui.ext
|
||||
|
||||
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.compositeOver
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlin.math.ln
|
||||
|
||||
@Composable
|
||||
fun ColorScheme.surfaceColorAtElevation(
|
||||
elevation: Dp,
|
||||
color: Color = surface,
|
||||
): Color = color.atElevation(surfaceTint, elevation)
|
||||
): Color = remember(this, elevation, color) { color.atElevation(surfaceTint, elevation) }
|
||||
|
||||
fun Color.atElevation(
|
||||
sourceColor: Color,
|
||||
|
|
|
@ -4,11 +4,13 @@ import android.app.Activity
|
|||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.FileProvider
|
||||
import me.ash.reader.data.entity.Version
|
||||
import me.ash.reader.data.entity.toVersion
|
||||
import me.ash.reader.R
|
||||
import me.ash.reader.data.model.Version
|
||||
import me.ash.reader.data.model.toVersion
|
||||
import java.io.File
|
||||
|
||||
fun Context.findActivity(): Activity? = when (this) {
|
||||
|
@ -54,3 +56,18 @@ fun Context.showToast(message: String?, duration: Int = Toast.LENGTH_SHORT) {
|
|||
fun Context.showToastLong(message: String?) {
|
||||
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.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
|
||||
get() = this.dataStore.get(DataStoreKeys.SkipVersionNumber) ?: ""
|
||||
val Context.isFirstLaunch: Boolean
|
||||
|
@ -93,9 +83,9 @@ sealed class DataStoreKeys<T> {
|
|||
get() = stringPreferencesKey("newVersionLog")
|
||||
}
|
||||
|
||||
object NewVersionSize : DataStoreKeys<Int>() {
|
||||
override val key: Preferences.Key<Int>
|
||||
get() = intPreferencesKey("newVersionSize")
|
||||
object NewVersionSize : DataStoreKeys<String>() {
|
||||
override val key: Preferences.Key<String>
|
||||
get() = stringPreferencesKey("newVersionSizeString")
|
||||
}
|
||||
|
||||
object NewVersionDownloadUrl : DataStoreKeys<String>() {
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.content.Context
|
|||
import androidx.core.os.ConfigurationCompat
|
||||
import me.ash.reader.R
|
||||
import java.text.DateFormat
|
||||
import java.text.ParsePosition
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
|
@ -41,3 +42,26 @@ 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
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import kotlin.math.abs
|
||||
|
||||
|
@ -28,3 +27,33 @@ fun <T : Any> LazyPagingItems<T>.rememberLazyListState(): LazyListState {
|
|||
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.DrawResult
|
||||
import androidx.compose.ui.draw.drawWithCache
|
||||
import androidx.compose.ui.geometry.CornerRadius
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
@ -173,11 +174,15 @@ private fun CacheDrawScope.onDrawScrollbar(
|
|||
|
||||
return {
|
||||
if (showScrollbar) {
|
||||
drawRect(
|
||||
drawRoundRect(
|
||||
color = color,
|
||||
topLeft = topLeft,
|
||||
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) }
|
||||
LaunchedEffect(scrolled, alpha) {
|
||||
scrolled.collectLatest {
|
||||
alpha.snapTo(1f)
|
||||
alpha.snapTo(0.3f)
|
||||
delay(ViewConfiguration.getScrollDefaultDelay().toLong())
|
||||
alpha.animateTo(0f, animationSpec = FadeOutAnimationSpec)
|
||||
}
|
||||
|
@ -231,7 +236,7 @@ private fun Modifier.drawScrollbar(
|
|||
|
||||
// Calculate thickness here to workaround https://issuetracker.google.com/issues/206972664
|
||||
val thickness = with(LocalDensity.current) { Thickness.toPx() }
|
||||
val color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f)
|
||||
val color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
Modifier
|
||||
.nestedScroll(nestedScrollConnection)
|
||||
.drawWithCache {
|
||||
|
|
|
@ -12,22 +12,21 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
|||
import com.google.accompanist.navigation.animation.AnimatedNavHost
|
||||
import com.google.accompanist.navigation.animation.rememberAnimatedNavController
|
||||
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.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.feeds.FeedsPage
|
||||
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.color.ColorAndStyle
|
||||
import me.ash.reader.ui.page.settings.color.DarkTheme
|
||||
import me.ash.reader.ui.page.settings.color.feeds.FeedsPageStyle
|
||||
import me.ash.reader.ui.page.settings.color.flow.FlowPageStyle
|
||||
import me.ash.reader.ui.page.settings.interaction.Interaction
|
||||
import me.ash.reader.ui.page.settings.languages.Languages
|
||||
import me.ash.reader.ui.page.settings.tips.TipsAndSupport
|
||||
import me.ash.reader.ui.page.settings.color.ColorAndStylePage
|
||||
import me.ash.reader.ui.page.settings.color.DarkThemePage
|
||||
import me.ash.reader.ui.page.settings.color.feeds.FeedsPageStylePage
|
||||
import me.ash.reader.ui.page.settings.color.flow.FlowPageStylePage
|
||||
import me.ash.reader.ui.page.settings.interaction.InteractionPage
|
||||
import me.ash.reader.ui.page.settings.languages.LanguagesPage
|
||||
import me.ash.reader.ui.page.settings.tips.TipsAndSupportPage
|
||||
import me.ash.reader.ui.page.startup.StartupPage
|
||||
import me.ash.reader.ui.theme.AppTheme
|
||||
|
||||
|
@ -37,7 +36,7 @@ fun HomeEntry(
|
|||
homeViewModel: HomeViewModel = hiltViewModel(),
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val filterState = homeViewModel.filterState.collectAsStateValue()
|
||||
val filterUiState = homeViewModel.filterUiState.collectAsStateValue()
|
||||
val navController = rememberAnimatedNavController()
|
||||
|
||||
val intent by rememberSaveable { mutableStateOf(context.findActivity()?.intent) }
|
||||
|
@ -57,9 +56,8 @@ fun HomeEntry(
|
|||
// Other initial pages
|
||||
}
|
||||
|
||||
homeViewModel.dispatch(
|
||||
HomeViewAction.ChangeFilter(
|
||||
filterState.copy(
|
||||
homeViewModel.changeFilter(
|
||||
filterUiState.copy(
|
||||
filter = when (context.initialFilter) {
|
||||
0 -> Filter.Starred
|
||||
1 -> Filter.Unread
|
||||
|
@ -68,7 +66,6 @@ fun HomeEntry(
|
|||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(openArticleId) {
|
||||
|
@ -114,7 +111,7 @@ fun HomeEntry(
|
|||
)
|
||||
}
|
||||
animatedComposable(route = "${RouteName.READING}/{articleId}") {
|
||||
ReadPage(navController = navController)
|
||||
ReadingPage(navController = navController)
|
||||
}
|
||||
|
||||
// Settings
|
||||
|
@ -124,31 +121,31 @@ fun HomeEntry(
|
|||
|
||||
// Color & Style
|
||||
animatedComposable(route = RouteName.COLOR_AND_STYLE) {
|
||||
ColorAndStyle(navController)
|
||||
ColorAndStylePage(navController)
|
||||
}
|
||||
animatedComposable(route = RouteName.DARK_THEME) {
|
||||
DarkTheme(navController)
|
||||
DarkThemePage(navController)
|
||||
}
|
||||
animatedComposable(route = RouteName.FEEDS_PAGE_STYLE) {
|
||||
FeedsPageStyle(navController)
|
||||
FeedsPageStylePage(navController)
|
||||
}
|
||||
animatedComposable(route = RouteName.FLOW_PAGE_STYLE) {
|
||||
FlowPageStyle(navController)
|
||||
FlowPageStylePage(navController)
|
||||
}
|
||||
|
||||
// Interaction
|
||||
animatedComposable(route = RouteName.INTERACTION) {
|
||||
Interaction(navController)
|
||||
InteractionPage(navController)
|
||||
}
|
||||
|
||||
// Languages
|
||||
animatedComposable(route = RouteName.LANGUAGES) {
|
||||
Languages(navController = navController)
|
||||
LanguagesPage(navController = navController)
|
||||
}
|
||||
|
||||
// Tips & 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.flow.*
|
||||
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.model.Filter
|
||||
import me.ash.reader.data.module.ApplicationScope
|
||||
import me.ash.reader.data.repository.RssRepository
|
||||
import me.ash.reader.data.repository.StringsRepository
|
||||
|
@ -24,30 +24,20 @@ class HomeViewModel @Inject constructor(
|
|||
private val applicationScope: CoroutineScope,
|
||||
private val workManager: WorkManager,
|
||||
) : ViewModel() {
|
||||
private val _homeUiState = MutableStateFlow(HomeUiState())
|
||||
val homeUiState: StateFlow<HomeUiState> = _homeUiState.asStateFlow()
|
||||
|
||||
private val _viewState = MutableStateFlow(HomeViewState())
|
||||
val viewState: StateFlow<HomeViewState> = _viewState.asStateFlow()
|
||||
|
||||
private val _filterState = MutableStateFlow(FilterState())
|
||||
val filterState = _filterState.asStateFlow()
|
||||
private val _filterUiState = MutableStateFlow(FilterState())
|
||||
val filterUiState = _filterUiState.asStateFlow()
|
||||
|
||||
val syncWorkLiveData = workManager.getWorkInfoByIdLiveData(SyncWorker.UUID)
|
||||
|
||||
fun dispatch(action: HomeViewAction) {
|
||||
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() {
|
||||
fun sync() {
|
||||
rssRepository.get().doSync()
|
||||
}
|
||||
|
||||
private fun changeFilter(filterState: FilterState) {
|
||||
_filterState.update {
|
||||
fun changeFilter(filterState: FilterState) {
|
||||
_filterUiState.update {
|
||||
it.copy(
|
||||
group = filterState.group,
|
||||
feed = filterState.feed,
|
||||
|
@ -57,28 +47,40 @@ class HomeViewModel @Inject constructor(
|
|||
fetchArticles()
|
||||
}
|
||||
|
||||
private fun fetchArticles() {
|
||||
_viewState.update {
|
||||
fun fetchArticles() {
|
||||
_homeUiState.update {
|
||||
it.copy(
|
||||
pagingData = Pager(PagingConfig(pageSize = 50)) {
|
||||
if (_viewState.value.searchContent.isNotBlank()) {
|
||||
pagingData = Pager(
|
||||
config = PagingConfig(
|
||||
pageSize = 100,
|
||||
enablePlaceholders = false,
|
||||
)
|
||||
) {
|
||||
if (_homeUiState.value.searchContent.isNotBlank()) {
|
||||
rssRepository.get().searchArticles(
|
||||
content = _viewState.value.searchContent.trim(),
|
||||
groupId = _filterState.value.group?.id,
|
||||
feedId = _filterState.value.feed?.id,
|
||||
isStarred = _filterState.value.filter.isStarred(),
|
||||
isUnread = _filterState.value.filter.isUnread(),
|
||||
content = _homeUiState.value.searchContent.trim(),
|
||||
groupId = _filterUiState.value.group?.id,
|
||||
feedId = _filterUiState.value.feed?.id,
|
||||
isStarred = _filterUiState.value.filter.isStarred(),
|
||||
isUnread = _filterUiState.value.filter.isUnread(),
|
||||
)
|
||||
} else {
|
||||
rssRepository.get().pullArticles(
|
||||
groupId = _filterState.value.group?.id,
|
||||
feedId = _filterState.value.feed?.id,
|
||||
isStarred = _filterState.value.filter.isStarred(),
|
||||
isUnread = _filterState.value.filter.isUnread(),
|
||||
groupId = _filterUiState.value.group?.id,
|
||||
feedId = _filterUiState.value.feed?.id,
|
||||
isStarred = _filterUiState.value.filter.isStarred(),
|
||||
isUnread = _filterUiState.value.filter.isUnread(),
|
||||
)
|
||||
}
|
||||
}.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 =
|
||||
stringsRepository.formatAsString(before?.articleWithFeed?.article?.date)
|
||||
val afterDate =
|
||||
|
@ -94,8 +96,8 @@ class HomeViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun inputSearchContent(content: String) {
|
||||
_viewState.update {
|
||||
fun inputSearchContent(content: String) {
|
||||
_homeUiState.update {
|
||||
it.copy(
|
||||
searchContent = content,
|
||||
)
|
||||
|
@ -110,21 +112,7 @@ data class FilterState(
|
|||
val filter: Filter = Filter.All,
|
||||
)
|
||||
|
||||
data class HomeViewState(
|
||||
data class HomeUiState(
|
||||
val pagingData: Flow<PagingData<FlowItemView>> = emptyFlow(),
|
||||
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,64 +1,66 @@
|
|||
package me.ash.reader.ui.page.home.feeds
|
||||
|
||||
import RYExtensibleVisibility
|
||||
import android.view.HapticFeedbackConstants
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Badge
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
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.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import me.ash.reader.data.entity.Feed
|
||||
import me.ash.reader.ui.page.home.FeedIcon
|
||||
import me.ash.reader.ui.page.home.feeds.option.feed.FeedOptionViewAction
|
||||
import me.ash.reader.ui.page.home.feeds.option.feed.FeedOptionViewModel
|
||||
import kotlin.math.ln
|
||||
import me.ash.reader.ui.component.FeedIcon
|
||||
import me.ash.reader.ui.page.home.feeds.drawer.feed.FeedOptionViewModel
|
||||
import me.ash.reader.ui.theme.ShapeBottom32
|
||||
|
||||
@OptIn(
|
||||
androidx.compose.foundation.ExperimentalFoundationApi::class,
|
||||
androidx.compose.material.ExperimentalMaterialApi::class,
|
||||
)
|
||||
@Composable
|
||||
fun FeedItem(
|
||||
feed: Feed,
|
||||
alpha: Float = 1f,
|
||||
badgeAlpha: Float = 1f,
|
||||
isEnded: Boolean = false,
|
||||
isExpanded: () -> Boolean,
|
||||
feedOptionViewModel: FeedOptionViewModel = hiltViewModel(),
|
||||
tonalElevation: Dp,
|
||||
onClick: () -> Unit = {},
|
||||
) {
|
||||
val view = LocalView.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val tonalElevationAlpha by remember {
|
||||
derivedStateOf {
|
||||
(ln(tonalElevation.value + 1.4f) + 2f) / 100f
|
||||
}
|
||||
}
|
||||
|
||||
RYExtensibleVisibility(visible = isExpanded()) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 14.dp)
|
||||
.clip(RoundedCornerShape(32.dp))
|
||||
.padding(horizontal = 16.dp)
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.secondary.copy(alpha = alpha),
|
||||
shape = if (isEnded) ShapeBottom32 else RectangleShape,
|
||||
)
|
||||
.combinedClickable(
|
||||
onClick = {
|
||||
onClick()
|
||||
},
|
||||
onLongClick = {
|
||||
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
||||
feedOptionViewModel.dispatch(FeedOptionViewAction.Show(scope, feed.id))
|
||||
feedOptionViewModel.showDrawer(scope, feed.id)
|
||||
}
|
||||
)
|
||||
.padding(vertical = 14.dp),
|
||||
.padding(horizontal = 14.dp)
|
||||
.padding(top = 14.dp, bottom = if (isEnded) 22.dp else 14.dp),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
|
@ -81,7 +83,7 @@ fun FeedItem(
|
|||
if ((feed.important ?: 0) != 0) {
|
||||
Badge(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceTint.copy(
|
||||
alpha = tonalElevationAlpha
|
||||
alpha = badgeAlpha
|
||||
),
|
||||
contentColor = MaterialTheme.colorScheme.outline,
|
||||
content = {
|
||||
|
@ -94,4 +96,5 @@ fun FeedItem(
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,11 +1,9 @@
|
|||
package me.ash.reader.ui.page.home.feeds
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.*
|
||||
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.rounded.Add
|
||||
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.ui.Modifier
|
||||
import androidx.compose.ui.draw.rotate
|
||||
|
@ -26,31 +25,26 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.navigation.NavHostController
|
||||
import kotlinx.coroutines.flow.map
|
||||
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.repository.SyncWorker.Companion.getIsSyncing
|
||||
import me.ash.reader.ui.component.Banner
|
||||
import me.ash.reader.ui.component.DisplayText
|
||||
import me.ash.reader.ui.component.FeedbackIconButton
|
||||
import me.ash.reader.ui.component.Subtitle
|
||||
import me.ash.reader.ui.ext.*
|
||||
import me.ash.reader.ui.component.FilterBar
|
||||
import me.ash.reader.ui.component.base.*
|
||||
import me.ash.reader.ui.ext.alphaLN
|
||||
import me.ash.reader.ui.ext.collectAsStateValue
|
||||
import me.ash.reader.ui.ext.findActivity
|
||||
import me.ash.reader.ui.ext.getCurrentVersion
|
||||
import me.ash.reader.ui.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.HomeViewAction
|
||||
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.option.group.GroupOptionDrawer
|
||||
import me.ash.reader.ui.page.home.feeds.drawer.feed.FeedOptionDrawer
|
||||
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.SubscribeViewAction
|
||||
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(
|
||||
ExperimentalMaterial3Api::class, com.google.accompanist.pager.ExperimentalPagerApi::class,
|
||||
androidx.compose.foundation.ExperimentalFoundationApi::class
|
||||
)
|
||||
@Composable
|
||||
|
@ -69,20 +63,12 @@ fun FeedsPage(
|
|||
val filterBarPadding = LocalFeedsFilterBarPadding.current
|
||||
val filterBarTonalElevation = LocalFeedsFilterBarTonalElevation.current
|
||||
|
||||
val feedsViewState = feedsViewModel.viewState.collectAsStateValue()
|
||||
val filterState = homeViewModel.filterState.collectAsStateValue()
|
||||
val feedsUiState = feedsViewModel.feedsUiState.collectAsStateValue()
|
||||
val filterUiState = homeViewModel.filterUiState.collectAsStateValue()
|
||||
|
||||
val skipVersion = context.dataStore.data
|
||||
.map { it[DataStoreKeys.SkipVersionNumber.key] ?: "" }
|
||||
.collectAsState(initial = "")
|
||||
.value
|
||||
.toVersion()
|
||||
val latestVersion = context.dataStore.data
|
||||
.map { it[DataStoreKeys.NewVersionNumber.key] ?: "" }
|
||||
.collectAsState(initial = "")
|
||||
.value
|
||||
.toVersion()
|
||||
val currentVersion by remember { mutableStateOf(context.getCurrentVersion()) }
|
||||
val newVersion = LocalNewVersionNumber.current
|
||||
val skipVersion = LocalSkipVersionNumber.current
|
||||
val currentVersion = remember { context.getCurrentVersion() }
|
||||
|
||||
val owner = LocalLifecycleOwner.current
|
||||
var isSyncing by remember { mutableStateOf(false) }
|
||||
|
@ -102,22 +88,40 @@ fun FeedsPage(
|
|||
val launcher = rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.CreateDocument()
|
||||
) { result ->
|
||||
feedsViewModel.dispatch(FeedsViewAction.ExportAsString { string ->
|
||||
feedsViewModel.exportAsOpml { string ->
|
||||
result?.let { uri ->
|
||||
context.contentResolver.openOutputStream(uri)?.let { outputStream ->
|
||||
context.contentResolver.openOutputStream(uri)?.use { outputStream ->
|
||||
outputStream.write(string.toByteArray())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
val feedBadgeAlpha by remember { derivedStateOf { (ln(groupListTonalElevation.value + 1.4f) + 2f) / 100f } }
|
||||
val groupAlpha by remember { derivedStateOf { groupListTonalElevation.value.dp.alphaLN(weight = 1.2f) } }
|
||||
val groupIndicatorAlpha by remember {
|
||||
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) {
|
||||
feedsViewModel.dispatch(FeedsViewAction.FetchAccount)
|
||||
feedsViewModel.fetchAccount()
|
||||
}
|
||||
|
||||
LaunchedEffect(filterState) {
|
||||
snapshotFlow { filterState }.collect {
|
||||
feedsViewModel.dispatch(FeedsViewAction.FetchData(it))
|
||||
LaunchedEffect(filterUiState) {
|
||||
snapshotFlow { filterUiState }.collect {
|
||||
feedsViewModel.fetchData(it)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -125,28 +129,16 @@ fun FeedsPage(
|
|||
context.findActivity()?.moveTaskToBack(false)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier
|
||||
.background(MaterialTheme.colorScheme.surfaceColorAtElevation(topBarTonalElevation.value.dp))
|
||||
.statusBarsPadding(),
|
||||
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(
|
||||
groupListTonalElevation.value.dp
|
||||
) onDark MaterialTheme.colorScheme.surface,
|
||||
topBar = {
|
||||
SmallTopAppBar(
|
||||
colors = TopAppBarDefaults.smallTopAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(
|
||||
topBarTonalElevation.value.dp
|
||||
),
|
||||
),
|
||||
title = {},
|
||||
RYScaffold(
|
||||
topBarTonalElevation = topBarTonalElevation.value.dp,
|
||||
containerTonalElevation = groupListTonalElevation.value.dp,
|
||||
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),
|
||||
showBadge = newVersion.whetherNeedUpdate(currentVersion, skipVersion),
|
||||
) {
|
||||
navController.navigate(RouteName.SETTINGS) {
|
||||
launchSingleTop = true
|
||||
|
@ -160,17 +152,15 @@ fun FeedsPage(
|
|||
contentDescription = stringResource(R.string.refresh),
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
) {
|
||||
if (!isSyncing) homeViewModel.dispatch(HomeViewAction.Sync)
|
||||
if (!isSyncing) homeViewModel.sync()
|
||||
}
|
||||
FeedbackIconButton(
|
||||
imageVector = Icons.Rounded.Add,
|
||||
contentDescription = stringResource(R.string.subscribe),
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
) {
|
||||
subscribeViewModel.dispatch(SubscribeViewAction.Show)
|
||||
subscribeViewModel.showDrawer()
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
content = {
|
||||
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 "",
|
||||
)
|
||||
}
|
||||
item {
|
||||
Banner(
|
||||
title = filterState.filter.getName(),
|
||||
desc = feedsViewState.importantCount.ifEmpty { stringResource(R.string.loading) },
|
||||
icon = filterState.filter.iconOutline,
|
||||
title = filterUiState.filter.getName(),
|
||||
desc = feedsUiState.importantSum.ifEmpty { stringResource(R.string.loading) },
|
||||
icon = filterUiState.filter.iconOutline,
|
||||
action = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.KeyboardArrowRight,
|
||||
|
@ -202,7 +192,7 @@ fun FeedsPage(
|
|||
filterChange(
|
||||
navController = navController,
|
||||
homeViewModel = homeViewModel,
|
||||
filterState = filterState.copy(
|
||||
filterState = filterUiState.copy(
|
||||
group = null,
|
||||
feed = null,
|
||||
)
|
||||
|
@ -217,40 +207,51 @@ fun FeedsPage(
|
|||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
itemsIndexed(feedsViewState.groupWithFeedList) { index, groupWithFeed ->
|
||||
// Crossfade(targetState = groupWithFeed) { groupWithFeed ->
|
||||
Column {
|
||||
itemsIndexed(feedsUiState.groupWithFeedList) { index, groupWithFeed ->
|
||||
when (groupWithFeed) {
|
||||
is GroupFeedsView.Group -> {
|
||||
if (index != 0) {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
GroupItem(
|
||||
isExpanded = groupListExpand.value,
|
||||
tonalElevation = groupListTonalElevation.value.dp,
|
||||
isExpanded = { groupsVisible[groupWithFeed.group.id] ?: false },
|
||||
group = groupWithFeed.group,
|
||||
feeds = groupWithFeed.feeds,
|
||||
groupOnClick = {
|
||||
alpha = groupAlpha,
|
||||
indicatorAlpha = groupIndicatorAlpha,
|
||||
onExpanded = {
|
||||
groupsVisible[groupWithFeed.group.id] =
|
||||
!(groupsVisible[groupWithFeed.group.id] ?: false)
|
||||
}
|
||||
) {
|
||||
filterChange(
|
||||
navController = navController,
|
||||
homeViewModel = homeViewModel,
|
||||
filterState = filterState.copy(
|
||||
filterState = filterUiState.copy(
|
||||
group = groupWithFeed.group,
|
||||
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(
|
||||
navController = navController,
|
||||
homeViewModel = homeViewModel,
|
||||
filterState = filterState.copy(
|
||||
filterState = filterUiState.copy(
|
||||
group = null,
|
||||
feed = feed,
|
||||
feed = groupWithFeed.feed,
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
if (index != feedsViewState.groupWithFeedList.lastIndex) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
// }
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(128.dp))
|
||||
|
@ -260,7 +261,7 @@ fun FeedsPage(
|
|||
},
|
||||
bottomBar = {
|
||||
FilterBar(
|
||||
filter = filterState.filter,
|
||||
filter = filterUiState.filter,
|
||||
filterBarStyle = filterBarStyle.value,
|
||||
filterBarFilled = filterBarFilled.value,
|
||||
filterBarPadding = filterBarPadding.dp,
|
||||
|
@ -269,7 +270,7 @@ fun FeedsPage(
|
|||
filterChange(
|
||||
navController = navController,
|
||||
homeViewModel = homeViewModel,
|
||||
filterState = filterState.copy(filter = it),
|
||||
filterState = filterUiState.copy(filter = it),
|
||||
isNavigate = false,
|
||||
)
|
||||
}
|
||||
|
@ -287,7 +288,7 @@ private fun filterChange(
|
|||
filterState: FilterState,
|
||||
isNavigate: Boolean = true,
|
||||
) {
|
||||
homeViewModel.dispatch(HomeViewAction.ChangeFilter(filterState))
|
||||
homeViewModel.changeFilter(filterState)
|
||||
if (isNavigate) {
|
||||
navController.navigate(RouteName.FLOW) {
|
||||
launchSingleTop = true
|
||||
|
|
|
@ -2,15 +2,17 @@ package me.ash.reader.ui.page.home.feeds
|
|||
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import me.ash.reader.R
|
||||
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.OpmlRepository
|
||||
import me.ash.reader.data.repository.RssRepository
|
||||
|
@ -24,22 +26,17 @@ class FeedsViewModel @Inject constructor(
|
|||
private val rssRepository: RssRepository,
|
||||
private val opmlRepository: OpmlRepository,
|
||||
private val stringsRepository: StringsRepository,
|
||||
@DispatcherDefault
|
||||
private val dispatcherDefault: CoroutineDispatcher,
|
||||
@DispatcherIO
|
||||
private val dispatcherIO: CoroutineDispatcher,
|
||||
) : ViewModel() {
|
||||
private val _viewState = MutableStateFlow(FeedsViewState())
|
||||
val viewState: StateFlow<FeedsViewState> = _viewState.asStateFlow()
|
||||
private val _feedsUiState = MutableStateFlow(FeedsUiState())
|
||||
val feedsUiState: StateFlow<FeedsUiState> = _feedsUiState.asStateFlow()
|
||||
|
||||
fun dispatch(action: FeedsViewAction) {
|
||||
when (action) {
|
||||
is FeedsViewAction.FetchAccount -> fetchAccount()
|
||||
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 {
|
||||
fun fetchAccount() {
|
||||
viewModelScope.launch(dispatcherIO) {
|
||||
_feedsUiState.update {
|
||||
it.copy(
|
||||
account = accountRepository.getCurrentAccount()
|
||||
)
|
||||
|
@ -47,8 +44,8 @@ class FeedsViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun exportAsOpml(callback: (String) -> Unit = {}) {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
fun exportAsOpml(callback: (String) -> Unit = {}) {
|
||||
viewModelScope.launch(dispatcherDefault) {
|
||||
try {
|
||||
callback(opmlRepository.saveToString())
|
||||
} catch (e: Exception) {
|
||||
|
@ -57,8 +54,8 @@ class FeedsViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun fetchData(filterState: FilterState) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
fun fetchData(filterState: FilterState) {
|
||||
viewModelScope.launch(dispatcherIO) {
|
||||
pullFeeds(
|
||||
isStarred = filterState.filter.isStarred(),
|
||||
isUnread = filterState.filter.isUnread(),
|
||||
|
@ -70,85 +67,64 @@ class FeedsViewModel @Inject constructor(
|
|||
combine(
|
||||
rssRepository.get().pullFeeds(),
|
||||
rssRepository.get().pullImportant(isStarred, isUnread),
|
||||
) { groupWithFeedList, importantList ->
|
||||
val groupImportantMap = mutableMapOf<String, Int>()
|
||||
val feedImportantMap = mutableMapOf<String, Int>()
|
||||
importantList.groupBy { it.groupId }.forEach { (i, list) ->
|
||||
var groupImportantSum = 0
|
||||
list.forEach {
|
||||
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
|
||||
}
|
||||
}
|
||||
) { groupWithFeedList, importantMap ->
|
||||
groupWithFeedList.fastForEach {
|
||||
var groupImportant = 0
|
||||
it.feeds.fastForEach {
|
||||
it.important = importantMap[it.id]
|
||||
groupImportant += it.important ?: 0
|
||||
}
|
||||
it.group.important = groupImportant
|
||||
}
|
||||
groupWithFeedList
|
||||
}.onEach { groupWithFeedList ->
|
||||
_viewState.update {
|
||||
}.mapLatest { groupWithFeedList ->
|
||||
_feedsUiState.update {
|
||||
it.copy(
|
||||
importantCount = groupWithFeedList.sumOf { it.group.important ?: 0 }.run {
|
||||
importantSum = groupWithFeedList.sumOf { it.group.important ?: 0 }.run {
|
||||
when {
|
||||
isStarred -> stringsRepository.getQuantityString(R.plurals.starred_desc, this, this)
|
||||
isUnread -> stringsRepository.getQuantityString(R.plurals.unread_desc, this, this)
|
||||
else -> stringsRepository.getQuantityString(R.plurals.all_desc, this, this)
|
||||
isStarred -> stringsRepository.getQuantityString(
|
||||
R.plurals.starred_desc,
|
||||
this,
|
||||
this
|
||||
)
|
||||
isUnread -> stringsRepository.getQuantityString(
|
||||
R.plurals.unread_desc,
|
||||
this,
|
||||
this
|
||||
)
|
||||
else -> stringsRepository.getQuantityString(
|
||||
R.plurals.all_desc,
|
||||
this,
|
||||
this
|
||||
)
|
||||
}
|
||||
},
|
||||
groupWithFeedList = groupWithFeedList,
|
||||
feedsVisible = List(groupWithFeedList.size, init = { true })
|
||||
groupWithFeedList = groupWithFeedList.map {
|
||||
mutableListOf<GroupFeedsView>(GroupFeedsView.Group(it.group)).apply {
|
||||
addAll(
|
||||
it.feeds.map {
|
||||
GroupFeedsView.Feed(it)
|
||||
}
|
||||
)
|
||||
}
|
||||
}.flatten(),
|
||||
)
|
||||
}
|
||||
}.catch {
|
||||
Log.e("RLog", "catch in articleRepository.pullFeeds(): ${it.message}")
|
||||
}.flowOn(Dispatchers.Default).collect()
|
||||
}
|
||||
|
||||
private fun scrollToItem(index: Int) {
|
||||
viewModelScope.launch {
|
||||
_viewState.value.listState.scrollToItem(index)
|
||||
}
|
||||
}.flowOn(dispatcherDefault).collect()
|
||||
}
|
||||
}
|
||||
|
||||
data class FeedsViewState(
|
||||
data class FeedsUiState(
|
||||
val account: Account? = null,
|
||||
val importantCount: String = "",
|
||||
val groupWithFeedList: List<GroupWithFeed> = emptyList(),
|
||||
val feedsVisible: List<Boolean> = emptyList(),
|
||||
val importantSum: String = "",
|
||||
val groupWithFeedList: List<GroupFeedsView> = emptyList(),
|
||||
val listState: LazyListState = LazyListState(),
|
||||
val groupsVisible: Boolean = true,
|
||||
)
|
||||
|
||||
sealed class FeedsViewAction {
|
||||
data class FetchData(
|
||||
val filterState: FilterState,
|
||||
) : FeedsViewAction()
|
||||
|
||||
object FetchAccount : FeedsViewAction()
|
||||
|
||||
data class ExportAsString(
|
||||
val callback: (String) -> Unit = {}
|
||||
) : FeedsViewAction()
|
||||
|
||||
data class ScrollToItem(
|
||||
val index: Int
|
||||
) : FeedsViewAction()
|
||||
sealed class GroupFeedsView {
|
||||
class Group(val group: me.ash.reader.data.entity.Group) : GroupFeedsView()
|
||||
class Feed(val feed: me.ash.reader.data.entity.Feed) : GroupFeedsView()
|
||||
}
|
|
@ -1,59 +1,56 @@
|
|||
package me.ash.reader.ui.page.home.feeds
|
||||
|
||||
import android.view.HapticFeedbackConstants
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.ExpandLess
|
||||
import androidx.compose.material.icons.rounded.ExpandMore
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
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.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import me.ash.reader.R
|
||||
import me.ash.reader.data.entity.Feed
|
||||
import me.ash.reader.data.entity.Group
|
||||
import me.ash.reader.ui.ext.alphaLN
|
||||
import me.ash.reader.ui.page.home.feeds.option.group.GroupOptionViewAction
|
||||
import me.ash.reader.ui.page.home.feeds.option.group.GroupOptionViewModel
|
||||
import me.ash.reader.ui.page.home.feeds.drawer.group.GroupOptionViewModel
|
||||
import me.ash.reader.ui.theme.Shape32
|
||||
import me.ash.reader.ui.theme.ShapeTop32
|
||||
|
||||
@OptIn(androidx.compose.foundation.ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun GroupItem(
|
||||
modifier: Modifier = Modifier,
|
||||
tonalElevation: Dp,
|
||||
group: Group,
|
||||
feeds: List<Feed>,
|
||||
isExpanded: Boolean = true,
|
||||
alpha: Float = 1f,
|
||||
indicatorAlpha: Float = 1f,
|
||||
isExpanded: () -> Boolean,
|
||||
groupOptionViewModel: GroupOptionViewModel = hiltViewModel(),
|
||||
onExpanded: () -> Unit = {},
|
||||
groupOnClick: () -> Unit = {},
|
||||
feedOnClick: (feed: Feed) -> Unit = {},
|
||||
) {
|
||||
val view = LocalView.current
|
||||
val scope = rememberCoroutineScope()
|
||||
var expanded by remember { mutableStateOf(isExpanded) }
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.animateContentSize()
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
.clip(RoundedCornerShape(32.dp))
|
||||
.clip(if (isExpanded()) ShapeTop32 else Shape32)
|
||||
.background(
|
||||
MaterialTheme.colorScheme.secondary.copy(alpha = tonalElevation.alphaLN(weight = 1.2f))
|
||||
MaterialTheme.colorScheme.secondary.copy(alpha = alpha)
|
||||
)
|
||||
.combinedClickable(
|
||||
onClick = {
|
||||
|
@ -61,13 +58,13 @@ fun GroupItem(
|
|||
},
|
||||
onLongClick = {
|
||||
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
||||
groupOptionViewModel.dispatch(GroupOptionViewAction.Show(scope, group.id))
|
||||
groupOptionViewModel.showDrawer(scope, group.id)
|
||||
}
|
||||
)
|
||||
.padding(top = 22.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
|
@ -87,42 +84,21 @@ fun GroupItem(
|
|||
.size(24.dp)
|
||||
.clip(CircleShape)
|
||||
.background(
|
||||
MaterialTheme.colorScheme.surfaceTint.copy(
|
||||
alpha = tonalElevation.alphaLN(weight = 1.4f)
|
||||
)
|
||||
MaterialTheme.colorScheme.surfaceTint.copy(alpha = indicatorAlpha)
|
||||
)
|
||||
.clickable {
|
||||
expanded = !expanded
|
||||
onExpanded()
|
||||
},
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (expanded) Icons.Rounded.ExpandLess else Icons.Rounded.ExpandMore,
|
||||
contentDescription = stringResource(if (expanded) R.string.expand_less else R.string.expand_more),
|
||||
imageVector = if (isExpanded()) Icons.Rounded.ExpandLess else Icons.Rounded.ExpandMore,
|
||||
contentDescription = stringResource(if (isExpanded()) R.string.expand_less else R.string.expand_more),
|
||||
tint = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
)
|
||||
}
|
||||
}
|
||||
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.outlined.DeleteForever
|
||||
|
@ -7,32 +7,28 @@ import androidx.compose.material3.Text
|
|||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||
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.showToast
|
||||
|
||||
@OptIn(ExperimentalPagerApi::class)
|
||||
@Composable
|
||||
fun ClearFeedDialog(
|
||||
modifier: Modifier = Modifier,
|
||||
feedName: String,
|
||||
viewModel: FeedOptionViewModel = hiltViewModel(),
|
||||
feedOptionViewModel: FeedOptionViewModel = hiltViewModel(),
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val viewState = viewModel.viewState.collectAsStateValue()
|
||||
val feedOptionUiState = feedOptionViewModel.feedOptionUiState.collectAsStateValue()
|
||||
val scope = rememberCoroutineScope()
|
||||
val toastString = stringResource(R.string.clear_articles_in_feed_toast, feedName)
|
||||
|
||||
Dialog(
|
||||
visible = viewState.clearDialogVisible,
|
||||
RYDialog(
|
||||
visible = feedOptionUiState.clearDialogVisible,
|
||||
onDismissRequest = {
|
||||
viewModel.dispatch(FeedOptionViewAction.HideClearDialog)
|
||||
feedOptionViewModel.hideClearDialog()
|
||||
},
|
||||
icon = {
|
||||
Icon(
|
||||
|
@ -49,11 +45,11 @@ fun ClearFeedDialog(
|
|||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
viewModel.dispatch(FeedOptionViewAction.Clear {
|
||||
viewModel.dispatch(FeedOptionViewAction.HideClearDialog)
|
||||
viewModel.dispatch(FeedOptionViewAction.Hide(scope))
|
||||
feedOptionViewModel.clearFeed {
|
||||
feedOptionViewModel.hideClearDialog()
|
||||
feedOptionViewModel.hideDrawer(scope)
|
||||
context.showToast(toastString)
|
||||
})
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
|
@ -64,7 +60,7 @@ fun ClearFeedDialog(
|
|||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
viewModel.dispatch(FeedOptionViewAction.HideClearDialog)
|
||||
feedOptionViewModel.hideClearDialog()
|
||||
}
|
||||
) {
|
||||
Text(
|
|
@ -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.outlined.DeleteForever
|
||||
|
@ -7,32 +7,28 @@ import androidx.compose.material3.Text
|
|||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||
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.showToast
|
||||
|
||||
@OptIn(ExperimentalPagerApi::class)
|
||||
@Composable
|
||||
fun DeleteFeedDialog(
|
||||
modifier: Modifier = Modifier,
|
||||
feedName: String,
|
||||
viewModel: FeedOptionViewModel = hiltViewModel(),
|
||||
feedOptionViewModel: FeedOptionViewModel = hiltViewModel(),
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val viewState = viewModel.viewState.collectAsStateValue()
|
||||
val feedOptionUiState = feedOptionViewModel.feedOptionUiState.collectAsStateValue()
|
||||
val scope = rememberCoroutineScope()
|
||||
val toastString = stringResource(R.string.delete_toast, feedName)
|
||||
|
||||
Dialog(
|
||||
visible = viewState.deleteDialogVisible,
|
||||
RYDialog(
|
||||
visible = feedOptionUiState.deleteDialogVisible,
|
||||
onDismissRequest = {
|
||||
viewModel.dispatch(FeedOptionViewAction.HideDeleteDialog)
|
||||
feedOptionViewModel.hideDeleteDialog()
|
||||
},
|
||||
icon = {
|
||||
Icon(
|
||||
|
@ -49,11 +45,11 @@ fun DeleteFeedDialog(
|
|||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
viewModel.dispatch(FeedOptionViewAction.Delete {
|
||||
viewModel.dispatch(FeedOptionViewAction.HideDeleteDialog)
|
||||
viewModel.dispatch(FeedOptionViewAction.Hide(scope))
|
||||
feedOptionViewModel.delete {
|
||||
feedOptionViewModel.hideDeleteDialog()
|
||||
feedOptionViewModel.hideDrawer(scope)
|
||||
context.showToast(toastString)
|
||||
})
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
|
@ -64,7 +60,7 @@ fun DeleteFeedDialog(
|
|||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
viewModel.dispatch(FeedOptionViewAction.HideDeleteDialog)
|
||||
feedOptionViewModel.hideDeleteDialog()
|
||||
}
|
||||
) {
|
||||
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