Merge pull request #80 from Ashinch/feature/optimize

Feature/optimize
This commit is contained in:
Ashinch 2022-06-01 14:01:31 +08:00 committed by GitHub
commit db5aa7aca6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
148 changed files with 3955 additions and 3717 deletions

View File

@ -116,7 +116,7 @@ dependencies {
implementation "org.conscrypt:conscrypt-android:2.5.2" implementation "org.conscrypt:conscrypt-android:2.5.2"
// https://square.github.io/okhttp/changelogs/changelog/ // https://square.github.io/okhttp/changelogs/changelog/
implementation "com.squareup.okhttp3:okhttp:5.0.0-alpha.6" implementation "com.squareup.okhttp3:okhttp:$okhttp"
implementation "com.squareup.retrofit2:retrofit:$retrofit2" implementation "com.squareup.retrofit2:retrofit:$retrofit2"
implementation "com.squareup.retrofit2:converter-gson:$retrofit2" implementation "com.squareup.retrofit2:converter-gson:$retrofit2"
@ -166,9 +166,9 @@ dependencies {
// https://developer.android.com/jetpack/androidx/releases/compose-material // https://developer.android.com/jetpack/androidx/releases/compose-material
implementation "androidx.compose.material:material:$compose" implementation "androidx.compose.material:material:$compose"
implementation "androidx.compose.material:material-icons-extended:$compose" implementation "androidx.compose.material:material-icons-extended:$compose"
debugImplementation "androidx.compose.ui:ui-tooling:$compose"
implementation "androidx.compose.ui:ui-tooling-preview:$compose" implementation "androidx.compose.ui:ui-tooling-preview:$compose"
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose" androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose"
debugImplementation "androidx.compose.ui:ui-tooling:$compose"
// hilt // hilt
implementation "androidx.hilt:hilt-work:1.0.0" implementation "androidx.hilt:hilt-work:1.0.0"

View 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')"
]
}
}

View File

@ -5,11 +5,8 @@
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<!-- Disable automatic updates in F-Droid -->
<!-- <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />-->
<application <application
android:name=".App" android:name=".RYApp"
android:allowBackup="true" android:allowBackup="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/read_you" android:label="@string/read_you"

View File

@ -12,20 +12,17 @@ import kotlinx.coroutines.launch
import me.ash.reader.data.module.ApplicationScope import me.ash.reader.data.module.ApplicationScope
import me.ash.reader.data.module.DispatcherDefault import me.ash.reader.data.module.DispatcherDefault
import me.ash.reader.data.repository.* import me.ash.reader.data.repository.*
import me.ash.reader.data.source.AppNetworkDataSource
import me.ash.reader.data.source.OpmlLocalDataSource import me.ash.reader.data.source.OpmlLocalDataSource
import me.ash.reader.data.source.ReaderDatabase import me.ash.reader.data.source.RYDatabase
import me.ash.reader.data.source.RYNetworkDataSource
import me.ash.reader.ui.ext.* import me.ash.reader.ui.ext.*
import okhttp3.Cache
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import org.conscrypt.Conscrypt import org.conscrypt.Conscrypt
import java.io.File
import java.security.Security import java.security.Security
import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
@HiltAndroidApp @HiltAndroidApp
class App : Application(), Configuration.Provider { class RYApp : Application(), Configuration.Provider {
init { init {
// From: https://gitlab.com/spacecowboy/Feeder // From: https://gitlab.com/spacecowboy/Feeder
// Install Conscrypt to handle TLSv1.3 pre Android10 // Install Conscrypt to handle TLSv1.3 pre Android10
@ -33,7 +30,7 @@ class App : Application(), Configuration.Provider {
} }
@Inject @Inject
lateinit var readerDatabase: ReaderDatabase lateinit var RYDatabase: RYDatabase
@Inject @Inject
lateinit var workerFactory: HiltWorkerFactory lateinit var workerFactory: HiltWorkerFactory
@ -42,7 +39,7 @@ class App : Application(), Configuration.Provider {
lateinit var workManager: WorkManager lateinit var workManager: WorkManager
@Inject @Inject
lateinit var appNetworkDataSource: AppNetworkDataSource lateinit var RYNetworkDataSource: RYNetworkDataSource
@Inject @Inject
lateinit var opmlLocalDataSource: OpmlLocalDataSource lateinit var opmlLocalDataSource: OpmlLocalDataSource
@ -51,7 +48,10 @@ class App : Application(), Configuration.Provider {
lateinit var rssHelper: RssHelper lateinit var rssHelper: RssHelper
@Inject @Inject
lateinit var appRepository: AppRepository lateinit var notificationHelper: NotificationHelper
@Inject
lateinit var ryRepository: RYRepository
@Inject @Inject
lateinit var stringsRepository: StringsRepository lateinit var stringsRepository: StringsRepository
@ -62,9 +62,6 @@ class App : Application(), Configuration.Provider {
@Inject @Inject
lateinit var localRssRepository: LocalRssRepository lateinit var localRssRepository: LocalRssRepository
// @Inject
// lateinit var feverRssRepository: FeverRssRepository
@Inject @Inject
lateinit var opmlRepository: OpmlRepository lateinit var opmlRepository: OpmlRepository
@ -79,6 +76,9 @@ class App : Application(), Configuration.Provider {
@DispatcherDefault @DispatcherDefault
lateinit var dispatcherDefault: CoroutineDispatcher lateinit var dispatcherDefault: CoroutineDispatcher
@Inject
lateinit var okHttpClient: OkHttpClient
@Inject @Inject
lateinit var imageLoader: ImageLoader lateinit var imageLoader: ImageLoader
@ -89,7 +89,7 @@ class App : Application(), Configuration.Provider {
applicationScope.launch(dispatcherDefault) { applicationScope.launch(dispatcherDefault) {
accountInit() accountInit()
workerInit() workerInit()
if (BuildConfig.FLAVOR != "fdroid") { if (notFdroid) {
checkUpdate() checkUpdate()
} }
} }
@ -116,7 +116,7 @@ class App : Application(), Configuration.Provider {
it.del() it.del()
} }
} }
appRepository.checkUpdate(showToast = false) ryRepository.checkUpdate(showToast = false)
} }
override fun getWorkManagerConfiguration(): Configuration = override fun getWorkManagerConfiguration(): Configuration =
@ -125,28 +125,3 @@ class App : Application(), Configuration.Provider {
.setMinimumLoggingLevel(android.util.Log.DEBUG) .setMinimumLoggingLevel(android.util.Log.DEBUG)
.build() .build()
} }
fun cachingHttpClient(
cacheDirectory: File? = null,
cacheSize: Long = 10L * 1024L * 1024L,
trustAllCerts: Boolean = true,
connectTimeoutSecs: Long = 30L,
readTimeoutSecs: Long = 30L
): OkHttpClient {
val builder: OkHttpClient.Builder = OkHttpClient.Builder()
if (cacheDirectory != null) {
builder.cache(Cache(cacheDirectory, cacheSize))
}
builder
.connectTimeout(connectTimeoutSecs, TimeUnit.SECONDS)
.readTimeout(readTimeoutSecs, TimeUnit.SECONDS)
.followRedirects(true)
// if (trustAllCerts) {
// builder.trustAllCerts()
// }
return builder.build()
}

View File

@ -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
}

View File

@ -18,7 +18,7 @@ interface AccountDao {
WHERE id = :id WHERE id = :id
""" """
) )
suspend fun queryById(id: Int): Account suspend fun queryById(id: Int): Account?
@Insert @Insert
suspend fun insert(account: Account): Long suspend fun insert(account: Account): Long

View File

@ -5,7 +5,7 @@ import androidx.room.*
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import me.ash.reader.data.entity.Article import me.ash.reader.data.entity.Article
import me.ash.reader.data.entity.ArticleWithFeed import me.ash.reader.data.entity.ArticleWithFeed
import me.ash.reader.data.entity.ImportantCount import me.ash.reader.data.model.ImportantCount
import java.util.* import java.util.*
@Dao @Dao

View File

@ -1,9 +1,6 @@
package me.ash.reader.data.entity package me.ash.reader.data.entity
import androidx.room.ColumnInfo import androidx.room.*
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
import java.util.* import java.util.*
@Entity( @Entity(
@ -18,13 +15,13 @@ import java.util.*
) )
data class Article( data class Article(
@PrimaryKey @PrimaryKey
val id: String, var id: String,
@ColumnInfo @ColumnInfo
val date: Date, var date: Date,
@ColumnInfo @ColumnInfo
val title: String, var title: String,
@ColumnInfo @ColumnInfo
val author: String? = null, var author: String? = null,
@ColumnInfo @ColumnInfo
var rawDescription: String, var rawDescription: String,
@ColumnInfo @ColumnInfo
@ -32,17 +29,20 @@ data class Article(
@ColumnInfo @ColumnInfo
var fullContent: String? = null, var fullContent: String? = null,
@ColumnInfo @ColumnInfo
val img: String? = null, var img: String? = null,
@ColumnInfo @ColumnInfo
val link: String, var link: String,
@ColumnInfo(index = true) @ColumnInfo(index = true)
val feedId: String, var feedId: String,
@ColumnInfo(index = true) @ColumnInfo(index = true)
val accountId: Int, var accountId: Int,
@ColumnInfo(defaultValue = "true") @ColumnInfo(defaultValue = "true")
var isUnread: Boolean = true, var isUnread: Boolean = true,
@ColumnInfo(defaultValue = "false") @ColumnInfo(defaultValue = "false")
var isStarred: Boolean = false, var isStarred: Boolean = false,
@ColumnInfo(defaultValue = "false") @ColumnInfo(defaultValue = "false")
var isReadLater: Boolean = false, var isReadLater: Boolean = false,
) ) {
@Ignore
var dateString: String? = null
}

View File

@ -5,7 +5,7 @@ import androidx.room.Relation
data class ArticleWithFeed( data class ArticleWithFeed(
@Embedded @Embedded
val article: Article, var article: Article,
@Relation(parentColumn = "feedId", entityColumn = "id") @Relation(parentColumn = "feedId", entityColumn = "id")
val feed: Feed, var feed: Feed,
) )

View File

@ -14,17 +14,17 @@ import androidx.room.*
) )
data class Feed( data class Feed(
@PrimaryKey @PrimaryKey
val id: String, var id: String,
@ColumnInfo @ColumnInfo
val name: String, var name: String,
@ColumnInfo @ColumnInfo
var icon: String? = null, var icon: String? = null,
@ColumnInfo @ColumnInfo
val url: String, var url: String,
@ColumnInfo(index = true) @ColumnInfo(index = true)
var groupId: String, var groupId: String,
@ColumnInfo(index = true) @ColumnInfo(index = true)
val accountId: Int, var accountId: Int,
@ColumnInfo(defaultValue = "false") @ColumnInfo(defaultValue = "false")
var isNotification: Boolean = false, var isNotification: Boolean = false,
@ColumnInfo(defaultValue = "false") @ColumnInfo(defaultValue = "false")

View File

@ -5,7 +5,7 @@ import androidx.room.Relation
data class FeedWithArticle( data class FeedWithArticle(
@Embedded @Embedded
val feed: Feed, var feed: Feed,
@Relation(parentColumn = "id", entityColumn = "feedId") @Relation(parentColumn = "id", entityColumn = "feedId")
val articles: List<Article> var articles: List<Article>
) )

View File

@ -5,7 +5,7 @@ import androidx.room.Relation
data class FeedWithGroup( data class FeedWithGroup(
@Embedded @Embedded
val feed: Feed, var feed: Feed,
@Relation(parentColumn = "groupId", entityColumn = "id") @Relation(parentColumn = "groupId", entityColumn = "id")
val group: Group var group: Group
) )

View File

@ -8,11 +8,11 @@ import androidx.room.PrimaryKey
@Entity(tableName = "group") @Entity(tableName = "group")
data class Group( data class Group(
@PrimaryKey @PrimaryKey
val id: String, var id: String,
@ColumnInfo @ColumnInfo
val name: String, var name: String,
@ColumnInfo(index = true) @ColumnInfo(index = true)
val accountId: Int, var accountId: Int,
) { ) {
@Ignore @Ignore
var important: Int? = 0 var important: Int? = 0

View File

@ -5,7 +5,7 @@ import androidx.room.Relation
data class GroupWithFeed( data class GroupWithFeed(
@Embedded @Embedded
val group: Group, var group: Group,
@Relation(parentColumn = "id", entityColumn = "groupId") @Relation(parentColumn = "id", entityColumn = "groupId")
val feeds: MutableList<Feed> var feeds: MutableList<Feed>
) )

View File

@ -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,
)

View File

@ -1,4 +1,4 @@
package me.ash.reader.data.entity package me.ash.reader.data.model
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.FiberManualRecord import androidx.compose.material.icons.outlined.FiberManualRecord
@ -6,7 +6,10 @@ import androidx.compose.material.icons.rounded.FiberManualRecord
import androidx.compose.material.icons.rounded.Star import androidx.compose.material.icons.rounded.Star
import androidx.compose.material.icons.rounded.StarOutline import androidx.compose.material.icons.rounded.StarOutline
import androidx.compose.material.icons.rounded.Subject import androidx.compose.material.icons.rounded.Subject
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import me.ash.reader.R
class Filter( class Filter(
val index: Int, val index: Int,
@ -33,5 +36,13 @@ class Filter(
iconOutline = Icons.Rounded.Subject, iconOutline = Icons.Rounded.Subject,
iconFilled = Icons.Rounded.Subject, iconFilled = Icons.Rounded.Subject,
) )
val values = listOf(Starred, Unread, All)
} }
} }
@Composable
fun Filter.getName(): String = when (this) {
Filter.Unread -> stringResource(R.string.unread)
Filter.Starred -> stringResource(R.string.starred)
else -> stringResource(R.string.all)
}

View File

@ -1,4 +1,4 @@
package me.ash.reader.data.entity package me.ash.reader.data.model
data class ImportantCount( data class ImportantCount(
val important: Int, val important: Int,

View File

@ -0,0 +1,2 @@
package me.ash.reader.data.model

View File

@ -1,4 +1,4 @@
package me.ash.reader.data.entity package me.ash.reader.data.model
class Version(identifiers: List<String>) { class Version(identifiers: List<String>) {
private var major: Int = 0 private var major: Int = 0

View File

@ -10,7 +10,7 @@ import me.ash.reader.data.dao.AccountDao
import me.ash.reader.data.dao.ArticleDao import me.ash.reader.data.dao.ArticleDao
import me.ash.reader.data.dao.FeedDao import me.ash.reader.data.dao.FeedDao
import me.ash.reader.data.dao.GroupDao import me.ash.reader.data.dao.GroupDao
import me.ash.reader.data.source.ReaderDatabase import me.ash.reader.data.source.RYDatabase
import javax.inject.Singleton import javax.inject.Singleton
@Module @Module
@ -19,26 +19,26 @@ object DatabaseModule {
@Provides @Provides
@Singleton @Singleton
fun provideArticleDao(readerDatabase: ReaderDatabase): ArticleDao = fun provideArticleDao(RYDatabase: RYDatabase): ArticleDao =
readerDatabase.articleDao() RYDatabase.articleDao()
@Provides @Provides
@Singleton @Singleton
fun provideFeedDao(readerDatabase: ReaderDatabase): FeedDao = fun provideFeedDao(RYDatabase: RYDatabase): FeedDao =
readerDatabase.feedDao() RYDatabase.feedDao()
@Provides @Provides
@Singleton @Singleton
fun provideGroupDao(readerDatabase: ReaderDatabase): GroupDao = fun provideGroupDao(RYDatabase: RYDatabase): GroupDao =
readerDatabase.groupDao() RYDatabase.groupDao()
@Provides @Provides
@Singleton @Singleton
fun provideAccountDao(readerDatabase: ReaderDatabase): AccountDao = fun provideAccountDao(RYDatabase: RYDatabase): AccountDao =
readerDatabase.accountDao() RYDatabase.accountDao()
@Provides @Provides
@Singleton @Singleton
fun provideReaderDatabase(@ApplicationContext context: Context): ReaderDatabase = fun provideReaderDatabase(@ApplicationContext context: Context): RYDatabase =
ReaderDatabase.getInstance(context) RYDatabase.getInstance(context)
} }

View File

@ -15,7 +15,7 @@ import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import me.ash.reader.cachingHttpClient import okhttp3.OkHttpClient
import javax.inject.Singleton import javax.inject.Singleton
@Module @Module
@ -25,16 +25,11 @@ object ImageLoaderModule {
@Provides @Provides
@Singleton @Singleton
fun provideImageLoader( fun provideImageLoader(
@ApplicationContext context: Context @ApplicationContext context: Context,
okHttpClient: OkHttpClient,
): ImageLoader { ): ImageLoader {
return ImageLoader.Builder(context) return ImageLoader.Builder(context)
.okHttpClient( .okHttpClient(okHttpClient)
okHttpClient = cachingHttpClient(
cacheDirectory = context.cacheDir.resolve("http")
).newBuilder()
//.addNetworkInterceptor(UserAgentInterceptor)
.build()
)
.dispatcher(Dispatchers.Default) // This slightly improves scrolling performance .dispatcher(Dispatchers.Default) // This slightly improves scrolling performance
.components{ .components{
add(SvgDecoder.Factory()) add(SvgDecoder.Factory())

View File

@ -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})"

View File

@ -4,7 +4,7 @@ import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import me.ash.reader.data.source.AppNetworkDataSource import me.ash.reader.data.source.RYNetworkDataSource
import me.ash.reader.data.source.FeverApiDataSource import me.ash.reader.data.source.FeverApiDataSource
import me.ash.reader.data.source.GoogleReaderApiDataSource import me.ash.reader.data.source.GoogleReaderApiDataSource
import javax.inject.Singleton import javax.inject.Singleton
@ -15,8 +15,8 @@ object RetrofitModule {
@Provides @Provides
@Singleton @Singleton
fun provideAppNetworkDataSource(): AppNetworkDataSource = fun provideAppNetworkDataSource(): RYNetworkDataSource =
AppNetworkDataSource.getInstance() RYNetworkDataSource.getInstance()
@Provides @Provides
@Singleton @Singleton

View File

@ -3,6 +3,7 @@ package me.ash.reader.data.preference
import android.content.Context import android.content.Context
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.Preferences
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -33,6 +34,7 @@ sealed class DarkThemePreference(val value: Int) : Preference() {
} }
@Composable @Composable
@ReadOnlyComposable
fun isDarkTheme(): Boolean = when (this) { fun isDarkTheme(): Boolean = when (this) {
UseDeviceTheme -> isSystemInDarkTheme() UseDeviceTheme -> isSystemInDarkTheme()
ON -> true ON -> true

View File

@ -4,17 +4,18 @@ import android.content.Context
import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.Preferences
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.ash.reader.data.constant.ElevationTokens
import me.ash.reader.ui.ext.DataStoreKeys import me.ash.reader.ui.ext.DataStoreKeys
import me.ash.reader.ui.ext.dataStore import me.ash.reader.ui.ext.dataStore
import me.ash.reader.ui.ext.put import me.ash.reader.ui.ext.put
sealed class FeedsFilterBarTonalElevationPreference(val value: Int) : Preference() { sealed class FeedsFilterBarTonalElevationPreference(val value: Int) : Preference() {
object Level0 : FeedsFilterBarTonalElevationPreference(0) object Level0 : FeedsFilterBarTonalElevationPreference(ElevationTokens.Level0)
object Level1 : FeedsFilterBarTonalElevationPreference(1) object Level1 : FeedsFilterBarTonalElevationPreference(ElevationTokens.Level1)
object Level2 : FeedsFilterBarTonalElevationPreference(3) object Level2 : FeedsFilterBarTonalElevationPreference(ElevationTokens.Level2)
object Level3 : FeedsFilterBarTonalElevationPreference(6) object Level3 : FeedsFilterBarTonalElevationPreference(ElevationTokens.Level3)
object Level4 : FeedsFilterBarTonalElevationPreference(8) object Level4 : FeedsFilterBarTonalElevationPreference(ElevationTokens.Level4)
object Level5 : FeedsFilterBarTonalElevationPreference(12) object Level5 : FeedsFilterBarTonalElevationPreference(ElevationTokens.Level5)
override fun put(context: Context, scope: CoroutineScope) { override fun put(context: Context, scope: CoroutineScope) {
scope.launch { scope.launch {
@ -27,12 +28,12 @@ sealed class FeedsFilterBarTonalElevationPreference(val value: Int) : Preference
fun getDesc(context: Context): String = fun getDesc(context: Context): String =
when (this) { when (this) {
Level0 -> "Level 0 (0dp)" Level0 -> "Level 0 (${ElevationTokens.Level0}dp)"
Level1 -> "Level 1 (1dp)" Level1 -> "Level 1 (${ElevationTokens.Level1}dp)"
Level2 -> "Level 2 (3dp)" Level2 -> "Level 2 (${ElevationTokens.Level2}dp)"
Level3 -> "Level 3 (6dp)" Level3 -> "Level 3 (${ElevationTokens.Level3}dp)"
Level4 -> "Level 4 (8dp)" Level4 -> "Level 4 (${ElevationTokens.Level4}dp)"
Level5 -> "Level 5 (12dp)" Level5 -> "Level 5 (${ElevationTokens.Level5}dp)"
} }
companion object { companion object {
@ -41,13 +42,14 @@ sealed class FeedsFilterBarTonalElevationPreference(val value: Int) : Preference
fun fromPreferences(preferences: Preferences) = fun fromPreferences(preferences: Preferences) =
when (preferences[DataStoreKeys.FeedsFilterBarTonalElevation.key]) { when (preferences[DataStoreKeys.FeedsFilterBarTonalElevation.key]) {
0 -> Level0 ElevationTokens.Level0 -> Level0
1 -> Level1 ElevationTokens.Level1 -> Level1
3 -> Level2 ElevationTokens.Level2 -> Level2
6 -> Level3 ElevationTokens.Level3 -> Level3
8 -> Level4 ElevationTokens.Level4 -> Level4
12 -> Level5 ElevationTokens.Level5 -> Level5
else -> default else -> default
} }
} }
} }

View File

@ -4,17 +4,18 @@ import android.content.Context
import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.Preferences
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.ash.reader.data.constant.ElevationTokens
import me.ash.reader.ui.ext.DataStoreKeys import me.ash.reader.ui.ext.DataStoreKeys
import me.ash.reader.ui.ext.dataStore import me.ash.reader.ui.ext.dataStore
import me.ash.reader.ui.ext.put import me.ash.reader.ui.ext.put
sealed class FeedsGroupListTonalElevationPreference(val value: Int) : Preference() { sealed class FeedsGroupListTonalElevationPreference(val value: Int) : Preference() {
object Level0 : FeedsGroupListTonalElevationPreference(0) object Level0 : FeedsGroupListTonalElevationPreference(ElevationTokens.Level0)
object Level1 : FeedsGroupListTonalElevationPreference(1) object Level1 : FeedsGroupListTonalElevationPreference(ElevationTokens.Level1)
object Level2 : FeedsGroupListTonalElevationPreference(3) object Level2 : FeedsGroupListTonalElevationPreference(ElevationTokens.Level2)
object Level3 : FeedsGroupListTonalElevationPreference(6) object Level3 : FeedsGroupListTonalElevationPreference(ElevationTokens.Level3)
object Level4 : FeedsGroupListTonalElevationPreference(8) object Level4 : FeedsGroupListTonalElevationPreference(ElevationTokens.Level4)
object Level5 : FeedsGroupListTonalElevationPreference(12) object Level5 : FeedsGroupListTonalElevationPreference(ElevationTokens.Level5)
override fun put(context: Context, scope: CoroutineScope) { override fun put(context: Context, scope: CoroutineScope) {
scope.launch { scope.launch {
@ -27,12 +28,12 @@ sealed class FeedsGroupListTonalElevationPreference(val value: Int) : Preference
fun getDesc(context: Context): String = fun getDesc(context: Context): String =
when (this) { when (this) {
Level0 -> "Level 0 (0dp)" Level0 -> "Level 0 (${ElevationTokens.Level0}dp)"
Level1 -> "Level 1 (1dp)" Level1 -> "Level 1 (${ElevationTokens.Level1}dp)"
Level2 -> "Level 2 (3dp)" Level2 -> "Level 2 (${ElevationTokens.Level2}dp)"
Level3 -> "Level 3 (6dp)" Level3 -> "Level 3 (${ElevationTokens.Level3}dp)"
Level4 -> "Level 4 (8dp)" Level4 -> "Level 4 (${ElevationTokens.Level4}dp)"
Level5 -> "Level 5 (12dp)" Level5 -> "Level 5 (${ElevationTokens.Level5}dp)"
} }
companion object { companion object {
@ -41,12 +42,12 @@ sealed class FeedsGroupListTonalElevationPreference(val value: Int) : Preference
fun fromPreferences(preferences: Preferences) = fun fromPreferences(preferences: Preferences) =
when (preferences[DataStoreKeys.FeedsGroupListTonalElevation.key]) { when (preferences[DataStoreKeys.FeedsGroupListTonalElevation.key]) {
0 -> Level0 ElevationTokens.Level0 -> Level0
1 -> Level1 ElevationTokens.Level1 -> Level1
3 -> Level2 ElevationTokens.Level2 -> Level2
6 -> Level3 ElevationTokens.Level3 -> Level3
8 -> Level4 ElevationTokens.Level4 -> Level4
12 -> Level5 ElevationTokens.Level5 -> Level5
else -> default else -> default
} }
} }

View File

@ -4,17 +4,18 @@ import android.content.Context
import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.Preferences
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.ash.reader.data.constant.ElevationTokens
import me.ash.reader.ui.ext.DataStoreKeys import me.ash.reader.ui.ext.DataStoreKeys
import me.ash.reader.ui.ext.dataStore import me.ash.reader.ui.ext.dataStore
import me.ash.reader.ui.ext.put import me.ash.reader.ui.ext.put
sealed class FeedsTopBarTonalElevationPreference(val value: Int) : Preference() { sealed class FeedsTopBarTonalElevationPreference(val value: Int) : Preference() {
object Level0 : FeedsTopBarTonalElevationPreference(0) object Level0 : FeedsTopBarTonalElevationPreference(ElevationTokens.Level0)
object Level1 : FeedsTopBarTonalElevationPreference(1) object Level1 : FeedsTopBarTonalElevationPreference(ElevationTokens.Level1)
object Level2 : FeedsTopBarTonalElevationPreference(3) object Level2 : FeedsTopBarTonalElevationPreference(ElevationTokens.Level2)
object Level3 : FeedsTopBarTonalElevationPreference(6) object Level3 : FeedsTopBarTonalElevationPreference(ElevationTokens.Level3)
object Level4 : FeedsTopBarTonalElevationPreference(8) object Level4 : FeedsTopBarTonalElevationPreference(ElevationTokens.Level4)
object Level5 : FeedsTopBarTonalElevationPreference(12) object Level5 : FeedsTopBarTonalElevationPreference(ElevationTokens.Level5)
override fun put(context: Context, scope: CoroutineScope) { override fun put(context: Context, scope: CoroutineScope) {
scope.launch { scope.launch {
@ -27,12 +28,12 @@ sealed class FeedsTopBarTonalElevationPreference(val value: Int) : Preference()
fun getDesc(context: Context): String = fun getDesc(context: Context): String =
when (this) { when (this) {
Level0 -> "Level 0 (0dp)" Level0 -> "Level 0 (${ElevationTokens.Level0}dp)"
Level1 -> "Level 1 (1dp)" Level1 -> "Level 1 (${ElevationTokens.Level1}dp)"
Level2 -> "Level 2 (3dp)" Level2 -> "Level 2 (${ElevationTokens.Level2}dp)"
Level3 -> "Level 3 (6dp)" Level3 -> "Level 3 (${ElevationTokens.Level3}dp)"
Level4 -> "Level 4 (8dp)" Level4 -> "Level 4 (${ElevationTokens.Level4}dp)"
Level5 -> "Level 5 (12dp)" Level5 -> "Level 5 (${ElevationTokens.Level5}dp)"
} }
companion object { companion object {
@ -41,12 +42,12 @@ sealed class FeedsTopBarTonalElevationPreference(val value: Int) : Preference()
fun fromPreferences(preferences: Preferences) = fun fromPreferences(preferences: Preferences) =
when (preferences[DataStoreKeys.FeedsTopBarTonalElevation.key]) { when (preferences[DataStoreKeys.FeedsTopBarTonalElevation.key]) {
0 -> Level0 ElevationTokens.Level0 -> Level0
1 -> Level1 ElevationTokens.Level1 -> Level1
3 -> Level2 ElevationTokens.Level2 -> Level2
6 -> Level3 ElevationTokens.Level3 -> Level3
8 -> Level4 ElevationTokens.Level4 -> Level4
12 -> Level5 ElevationTokens.Level5 -> Level5
else -> default else -> default
} }
} }

View File

@ -4,17 +4,18 @@ import android.content.Context
import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.Preferences
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.ash.reader.data.constant.ElevationTokens
import me.ash.reader.ui.ext.DataStoreKeys import me.ash.reader.ui.ext.DataStoreKeys
import me.ash.reader.ui.ext.dataStore import me.ash.reader.ui.ext.dataStore
import me.ash.reader.ui.ext.put import me.ash.reader.ui.ext.put
sealed class FlowArticleListTonalElevationPreference(val value: Int) : Preference() { sealed class FlowArticleListTonalElevationPreference(val value: Int) : Preference() {
object Level0 : FlowArticleListTonalElevationPreference(0) object Level0 : FlowArticleListTonalElevationPreference(ElevationTokens.Level0)
object Level1 : FlowArticleListTonalElevationPreference(1) object Level1 : FlowArticleListTonalElevationPreference(ElevationTokens.Level1)
object Level2 : FlowArticleListTonalElevationPreference(3) object Level2 : FlowArticleListTonalElevationPreference(ElevationTokens.Level2)
object Level3 : FlowArticleListTonalElevationPreference(6) object Level3 : FlowArticleListTonalElevationPreference(ElevationTokens.Level3)
object Level4 : FlowArticleListTonalElevationPreference(8) object Level4 : FlowArticleListTonalElevationPreference(ElevationTokens.Level4)
object Level5 : FlowArticleListTonalElevationPreference(12) object Level5 : FlowArticleListTonalElevationPreference(ElevationTokens.Level5)
override fun put(context: Context, scope: CoroutineScope) { override fun put(context: Context, scope: CoroutineScope) {
scope.launch { scope.launch {
@ -27,12 +28,12 @@ sealed class FlowArticleListTonalElevationPreference(val value: Int) : Preferenc
fun getDesc(context: Context): String = fun getDesc(context: Context): String =
when (this) { when (this) {
Level0 -> "Level 0 (0dp)" Level0 -> "Level 0 (${ElevationTokens.Level0}dp)"
Level1 -> "Level 1 (1dp)" Level1 -> "Level 1 (${ElevationTokens.Level1}dp)"
Level2 -> "Level 2 (3dp)" Level2 -> "Level 2 (${ElevationTokens.Level2}dp)"
Level3 -> "Level 3 (6dp)" Level3 -> "Level 3 (${ElevationTokens.Level3}dp)"
Level4 -> "Level 4 (8dp)" Level4 -> "Level 4 (${ElevationTokens.Level4}dp)"
Level5 -> "Level 5 (12dp)" Level5 -> "Level 5 (${ElevationTokens.Level5}dp)"
} }
companion object { companion object {
@ -41,12 +42,12 @@ sealed class FlowArticleListTonalElevationPreference(val value: Int) : Preferenc
fun fromPreferences(preferences: Preferences) = fun fromPreferences(preferences: Preferences) =
when (preferences[DataStoreKeys.FlowArticleListTonalElevation.key]) { when (preferences[DataStoreKeys.FlowArticleListTonalElevation.key]) {
0 -> Level0 ElevationTokens.Level0 -> Level0
1 -> Level1 ElevationTokens.Level1 -> Level1
3 -> Level2 ElevationTokens.Level2 -> Level2
6 -> Level3 ElevationTokens.Level3 -> Level3
8 -> Level4 ElevationTokens.Level4 -> Level4
12 -> Level5 ElevationTokens.Level5 -> Level5
else -> default else -> default
} }
} }

View File

@ -4,17 +4,18 @@ import android.content.Context
import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.Preferences
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.ash.reader.data.constant.ElevationTokens
import me.ash.reader.ui.ext.DataStoreKeys import me.ash.reader.ui.ext.DataStoreKeys
import me.ash.reader.ui.ext.dataStore import me.ash.reader.ui.ext.dataStore
import me.ash.reader.ui.ext.put import me.ash.reader.ui.ext.put
sealed class FlowFilterBarTonalElevationPreference(val value: Int) : Preference() { sealed class FlowFilterBarTonalElevationPreference(val value: Int) : Preference() {
object Level0 : FlowFilterBarTonalElevationPreference(0) object Level0 : FlowFilterBarTonalElevationPreference(ElevationTokens.Level0)
object Level1 : FlowFilterBarTonalElevationPreference(1) object Level1 : FlowFilterBarTonalElevationPreference(ElevationTokens.Level1)
object Level2 : FlowFilterBarTonalElevationPreference(3) object Level2 : FlowFilterBarTonalElevationPreference(ElevationTokens.Level2)
object Level3 : FlowFilterBarTonalElevationPreference(6) object Level3 : FlowFilterBarTonalElevationPreference(ElevationTokens.Level3)
object Level4 : FlowFilterBarTonalElevationPreference(8) object Level4 : FlowFilterBarTonalElevationPreference(ElevationTokens.Level4)
object Level5 : FlowFilterBarTonalElevationPreference(12) object Level5 : FlowFilterBarTonalElevationPreference(ElevationTokens.Level5)
override fun put(context: Context, scope: CoroutineScope) { override fun put(context: Context, scope: CoroutineScope) {
scope.launch { scope.launch {
@ -27,12 +28,12 @@ sealed class FlowFilterBarTonalElevationPreference(val value: Int) : Preference(
fun getDesc(context: Context): String = fun getDesc(context: Context): String =
when (this) { when (this) {
Level0 -> "Level 0 (0dp)" Level0 -> "Level 0 (${ElevationTokens.Level0}dp)"
Level1 -> "Level 1 (1dp)" Level1 -> "Level 1 (${ElevationTokens.Level1}dp)"
Level2 -> "Level 2 (3dp)" Level2 -> "Level 2 (${ElevationTokens.Level2}dp)"
Level3 -> "Level 3 (6dp)" Level3 -> "Level 3 (${ElevationTokens.Level3}dp)"
Level4 -> "Level 4 (8dp)" Level4 -> "Level 4 (${ElevationTokens.Level4}dp)"
Level5 -> "Level 5 (12dp)" Level5 -> "Level 5 (${ElevationTokens.Level5}dp)"
} }
companion object { companion object {
@ -41,12 +42,12 @@ sealed class FlowFilterBarTonalElevationPreference(val value: Int) : Preference(
fun fromPreferences(preferences: Preferences) = fun fromPreferences(preferences: Preferences) =
when (preferences[DataStoreKeys.FlowFilterBarTonalElevation.key]) { when (preferences[DataStoreKeys.FlowFilterBarTonalElevation.key]) {
0 -> Level0 ElevationTokens.Level0 -> Level0
1 -> Level1 ElevationTokens.Level1 -> Level1
3 -> Level2 ElevationTokens.Level2 -> Level2
6 -> Level3 ElevationTokens.Level3 -> Level3
8 -> Level4 ElevationTokens.Level4 -> Level4
12 -> Level5 ElevationTokens.Level5 -> Level5
else -> default else -> default
} }
} }

View File

@ -4,17 +4,18 @@ import android.content.Context
import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.Preferences
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.ash.reader.data.constant.ElevationTokens
import me.ash.reader.ui.ext.DataStoreKeys import me.ash.reader.ui.ext.DataStoreKeys
import me.ash.reader.ui.ext.dataStore import me.ash.reader.ui.ext.dataStore
import me.ash.reader.ui.ext.put import me.ash.reader.ui.ext.put
sealed class FlowTopBarTonalElevationPreference(val value: Int) : Preference() { sealed class FlowTopBarTonalElevationPreference(val value: Int) : Preference() {
object Level0 : FlowTopBarTonalElevationPreference(0) object Level0 : FlowTopBarTonalElevationPreference(ElevationTokens.Level0)
object Level1 : FlowTopBarTonalElevationPreference(1) object Level1 : FlowTopBarTonalElevationPreference(ElevationTokens.Level1)
object Level2 : FlowTopBarTonalElevationPreference(3) object Level2 : FlowTopBarTonalElevationPreference(ElevationTokens.Level2)
object Level3 : FlowTopBarTonalElevationPreference(6) object Level3 : FlowTopBarTonalElevationPreference(ElevationTokens.Level3)
object Level4 : FlowTopBarTonalElevationPreference(8) object Level4 : FlowTopBarTonalElevationPreference(ElevationTokens.Level4)
object Level5 : FlowTopBarTonalElevationPreference(12) object Level5 : FlowTopBarTonalElevationPreference(ElevationTokens.Level5)
override fun put(context: Context, scope: CoroutineScope) { override fun put(context: Context, scope: CoroutineScope) {
scope.launch { scope.launch {
@ -27,12 +28,12 @@ sealed class FlowTopBarTonalElevationPreference(val value: Int) : Preference() {
fun getDesc(context: Context): String = fun getDesc(context: Context): String =
when (this) { when (this) {
Level0 -> "Level 0 (0dp)" Level0 -> "Level 0 (${ElevationTokens.Level0}dp)"
Level1 -> "Level 1 (1dp)" Level1 -> "Level 1 (${ElevationTokens.Level1}dp)"
Level2 -> "Level 2 (3dp)" Level2 -> "Level 2 (${ElevationTokens.Level2}dp)"
Level3 -> "Level 3 (6dp)" Level3 -> "Level 3 (${ElevationTokens.Level3}dp)"
Level4 -> "Level 4 (8dp)" Level4 -> "Level 4 (${ElevationTokens.Level4}dp)"
Level5 -> "Level 5 (12dp)" Level5 -> "Level 5 (${ElevationTokens.Level5}dp)"
} }
companion object { companion object {
@ -41,12 +42,12 @@ sealed class FlowTopBarTonalElevationPreference(val value: Int) : Preference() {
fun fromPreferences(preferences: Preferences) = fun fromPreferences(preferences: Preferences) =
when (preferences[DataStoreKeys.FlowTopBarTonalElevation.key]) { when (preferences[DataStoreKeys.FlowTopBarTonalElevation.key]) {
0 -> Level0 ElevationTokens.Level0 -> Level0
1 -> Level1 ElevationTokens.Level1 -> Level1
3 -> Level2 ElevationTokens.Level2 -> Level2
6 -> Level3 ElevationTokens.Level3 -> Level3
8 -> Level4 ElevationTokens.Level4 -> Level4
12 -> Level5 ElevationTokens.Level5 -> Level5
else -> default else -> default
} }
} }

View File

@ -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
}
}
}

View File

@ -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
}
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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()
}

View File

@ -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
}

View File

@ -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
}

View File

@ -1,8 +1,53 @@
package me.ash.reader.data.preference package me.ash.reader.data.preference
import android.content.Context import android.content.Context
import androidx.datastore.preferences.core.Preferences
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
sealed class Preference { sealed class Preference {
abstract fun put(context: Context, scope: CoroutineScope) abstract fun put(context: Context, scope: CoroutineScope)
} }
fun Preferences.toSettings(): Settings {
return Settings(
newVersionNumber = NewVersionNumberPreference.fromPreferences(this),
skipVersionNumber = SkipVersionNumberPreference.fromPreferences(this),
newVersionPublishDate = NewVersionPublishDatePreference.fromPreferences(this),
newVersionLog = NewVersionLogPreference.fromPreferences(this),
newVersionSize = NewVersionSizePreference.fromPreferences(this),
newVersionDownloadUrl = NewVersionDownloadUrlPreference.fromPreferences(this),
themeIndex = ThemeIndexPreference.fromPreferences(this),
customPrimaryColor = CustomPrimaryColorPreference.fromPreferences(this),
darkTheme = DarkThemePreference.fromPreferences(this),
amoledDarkTheme = AmoledDarkThemePreference.fromPreferences(this),
feedsFilterBarStyle = FeedsFilterBarStylePreference.fromPreferences(this),
feedsFilterBarFilled = FeedsFilterBarFilledPreference.fromPreferences(this),
feedsFilterBarPadding = FeedsFilterBarPaddingPreference.fromPreferences(this),
feedsFilterBarTonalElevation = FeedsFilterBarTonalElevationPreference.fromPreferences(this),
feedsTopBarTonalElevation = FeedsTopBarTonalElevationPreference.fromPreferences(this),
feedsGroupListExpand = FeedsGroupListExpandPreference.fromPreferences(this),
feedsGroupListTonalElevation = FeedsGroupListTonalElevationPreference.fromPreferences(this),
flowFilterBarStyle = FlowFilterBarStylePreference.fromPreferences(this),
flowFilterBarFilled = FlowFilterBarFilledPreference.fromPreferences(this),
flowFilterBarPadding = FlowFilterBarPaddingPreference.fromPreferences(this),
flowFilterBarTonalElevation = FlowFilterBarTonalElevationPreference.fromPreferences(this),
flowTopBarTonalElevation = FlowTopBarTonalElevationPreference.fromPreferences(this),
flowArticleListFeedIcon = FlowArticleListFeedIconPreference.fromPreferences(this),
flowArticleListFeedName = FlowArticleListFeedNamePreference.fromPreferences(this),
flowArticleListImage = FlowArticleListImagePreference.fromPreferences(this),
flowArticleListDesc = FlowArticleListDescPreference.fromPreferences(this),
flowArticleListTime = FlowArticleListTimePreference.fromPreferences(this),
flowArticleListDateStickyHeader = FlowArticleListDateStickyHeaderPreference.fromPreferences(
this
),
flowArticleListTonalElevation = FlowArticleListTonalElevationPreference.fromPreferences(this),
initialPage = InitialPagePreference.fromPreferences(this),
initialFilter = InitialFilterPreference.fromPreferences(this),
languages = LanguagesPreference.fromPreferences(this),
)
}

View File

@ -6,12 +6,19 @@ import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.datastore.preferences.core.Preferences
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import me.ash.reader.data.model.Version
import me.ash.reader.ui.ext.collectAsStateValue import me.ash.reader.ui.ext.collectAsStateValue
import me.ash.reader.ui.ext.dataStore import me.ash.reader.ui.ext.dataStore
data class Settings( data class Settings(
val newVersionNumber: Version = NewVersionNumberPreference.default,
val skipVersionNumber: Version = SkipVersionNumberPreference.default,
val newVersionPublishDate: String = NewVersionPublishDatePreference.default,
val newVersionLog: String = NewVersionLogPreference.default,
val newVersionSize: String = NewVersionSizePreference.default,
val newVersionDownloadUrl: String = NewVersionDownloadUrlPreference.default,
val themeIndex: Int = ThemeIndexPreference.default, val themeIndex: Int = ThemeIndexPreference.default,
val customPrimaryColor: String = CustomPrimaryColorPreference.default, val customPrimaryColor: String = CustomPrimaryColorPreference.default,
val darkTheme: DarkThemePreference = DarkThemePreference.default, val darkTheme: DarkThemePreference = DarkThemePreference.default,
@ -38,43 +45,12 @@ data class Settings(
val flowArticleListDateStickyHeader: FlowArticleListDateStickyHeaderPreference = FlowArticleListDateStickyHeaderPreference.default, val flowArticleListDateStickyHeader: FlowArticleListDateStickyHeaderPreference = FlowArticleListDateStickyHeaderPreference.default,
val flowArticleListTonalElevation: FlowArticleListTonalElevationPreference = FlowArticleListTonalElevationPreference.default, val flowArticleListTonalElevation: FlowArticleListTonalElevationPreference = FlowArticleListTonalElevationPreference.default,
val initialPage: InitialPagePreference = InitialPagePreference.default,
val initialFilter: InitialFilterPreference = InitialFilterPreference.default,
val languages: LanguagesPreference = LanguagesPreference.default, val languages: LanguagesPreference = LanguagesPreference.default,
) )
fun Preferences.toSettings(): Settings {
return Settings(
themeIndex = ThemeIndexPreference.fromPreferences(this),
customPrimaryColor = CustomPrimaryColorPreference.fromPreferences(this),
darkTheme = DarkThemePreference.fromPreferences(this),
amoledDarkTheme = AmoledDarkThemePreference.fromPreferences(this),
feedsFilterBarStyle = FeedsFilterBarStylePreference.fromPreferences(this),
feedsFilterBarFilled = FeedsFilterBarFilledPreference.fromPreferences(this),
feedsFilterBarPadding = FeedsFilterBarPaddingPreference.fromPreferences(this),
feedsFilterBarTonalElevation = FeedsFilterBarTonalElevationPreference.fromPreferences(this),
feedsTopBarTonalElevation = FeedsTopBarTonalElevationPreference.fromPreferences(this),
feedsGroupListExpand = FeedsGroupListExpandPreference.fromPreferences(this),
feedsGroupListTonalElevation = FeedsGroupListTonalElevationPreference.fromPreferences(this),
flowFilterBarStyle = FlowFilterBarStylePreference.fromPreferences(this),
flowFilterBarFilled = FlowFilterBarFilledPreference.fromPreferences(this),
flowFilterBarPadding = FlowFilterBarPaddingPreference.fromPreferences(this),
flowFilterBarTonalElevation = FlowFilterBarTonalElevationPreference.fromPreferences(this),
flowTopBarTonalElevation = FlowTopBarTonalElevationPreference.fromPreferences(this),
flowArticleListFeedIcon = FlowArticleListFeedIconPreference.fromPreferences(this),
flowArticleListFeedName = FlowArticleListFeedNamePreference.fromPreferences(this),
flowArticleListImage = FlowArticleListImagePreference.fromPreferences(this),
flowArticleListDesc = FlowArticleListDescPreference.fromPreferences(this),
flowArticleListTime = FlowArticleListTimePreference.fromPreferences(this),
flowArticleListDateStickyHeader = FlowArticleListDateStickyHeaderPreference.fromPreferences(
this
),
flowArticleListTonalElevation = FlowArticleListTonalElevationPreference.fromPreferences(this),
languages = LanguagesPreference.fromPreferences(this),
)
}
@Composable @Composable
fun SettingsProvider( fun SettingsProvider(
content: @Composable () -> Unit, content: @Composable () -> Unit,
@ -88,6 +64,13 @@ fun SettingsProvider(
}.collectAsStateValue(initial = Settings()) }.collectAsStateValue(initial = Settings())
CompositionLocalProvider( CompositionLocalProvider(
LocalNewVersionNumber provides settings.newVersionNumber,
LocalSkipVersionNumber provides settings.skipVersionNumber,
LocalNewVersionPublishDate provides settings.newVersionPublishDate,
LocalNewVersionLog provides settings.newVersionLog,
LocalNewVersionSize provides settings.newVersionSize,
LocalNewVersionDownloadUrl provides settings.newVersionDownloadUrl,
LocalThemeIndex provides settings.themeIndex, LocalThemeIndex provides settings.themeIndex,
LocalCustomPrimaryColor provides settings.customPrimaryColor, LocalCustomPrimaryColor provides settings.customPrimaryColor,
LocalDarkTheme provides settings.darkTheme, LocalDarkTheme provides settings.darkTheme,
@ -114,12 +97,22 @@ fun SettingsProvider(
LocalFlowFilterBarPadding provides settings.flowFilterBarPadding, LocalFlowFilterBarPadding provides settings.flowFilterBarPadding,
LocalFlowFilterBarTonalElevation provides settings.flowFilterBarTonalElevation, LocalFlowFilterBarTonalElevation provides settings.flowFilterBarTonalElevation,
LocalInitialPage provides settings.initialPage,
LocalInitialFilter provides settings.initialFilter,
LocalLanguages provides settings.languages, LocalLanguages provides settings.languages,
) { ) {
content() content()
} }
} }
val LocalNewVersionNumber = compositionLocalOf { NewVersionNumberPreference.default }
val LocalSkipVersionNumber = compositionLocalOf { SkipVersionNumberPreference.default }
val LocalNewVersionPublishDate = compositionLocalOf { NewVersionPublishDatePreference.default }
val LocalNewVersionLog = compositionLocalOf { NewVersionLogPreference.default }
val LocalNewVersionSize = compositionLocalOf { NewVersionSizePreference.default }
val LocalNewVersionDownloadUrl = compositionLocalOf { NewVersionDownloadUrlPreference.default }
val LocalThemeIndex = val LocalThemeIndex =
compositionLocalOf { ThemeIndexPreference.default } compositionLocalOf { ThemeIndexPreference.default }
val LocalCustomPrimaryColor = val LocalCustomPrimaryColor =
@ -169,5 +162,9 @@ val LocalFlowArticleListDateStickyHeader =
val LocalFlowArticleListTonalElevation = val LocalFlowArticleListTonalElevation =
compositionLocalOf<FlowArticleListTonalElevationPreference> { FlowArticleListTonalElevationPreference.default } compositionLocalOf<FlowArticleListTonalElevationPreference> { FlowArticleListTonalElevationPreference.default }
val LocalInitialPage = compositionLocalOf<InitialPagePreference> { InitialPagePreference.default }
val LocalInitialFilter =
compositionLocalOf<InitialFilterPreference> { InitialFilterPreference.default }
val LocalLanguages = val LocalLanguages =
compositionLocalOf<LanguagesPreference> { LanguagesPreference.default } compositionLocalOf<LanguagesPreference> { LanguagesPreference.default }

View File

@ -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()
}

View File

@ -2,14 +2,15 @@ package me.ash.reader.data.repository
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
import androidx.hilt.work.HiltWorker
import androidx.paging.PagingSource import androidx.paging.PagingSource
import androidx.work.* import androidx.work.CoroutineWorker
import dagger.assisted.Assisted import androidx.work.ExistingPeriodicWorkPolicy
import dagger.assisted.AssistedInject import androidx.work.ListenableWorker
import androidx.work.WorkManager
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.mapLatest
import me.ash.reader.data.dao.AccountDao import me.ash.reader.data.dao.AccountDao
import me.ash.reader.data.dao.ArticleDao import me.ash.reader.data.dao.ArticleDao
import me.ash.reader.data.dao.FeedDao import me.ash.reader.data.dao.FeedDao
@ -17,7 +18,6 @@ import me.ash.reader.data.dao.GroupDao
import me.ash.reader.data.entity.* import me.ash.reader.data.entity.*
import me.ash.reader.ui.ext.currentAccountId import me.ash.reader.ui.ext.currentAccountId
import java.util.* import java.util.*
import java.util.concurrent.TimeUnit
abstract class AbstractRssRepository constructor( abstract class AbstractRssRepository constructor(
private val context: Context, private val context: Context,
@ -99,7 +99,7 @@ abstract class AbstractRssRepository constructor(
fun pullImportant( fun pullImportant(
isStarred: Boolean = false, isStarred: Boolean = false,
isUnread: Boolean = false, isUnread: Boolean = false,
): Flow<List<ImportantCount>> { ): Flow<Map<String, Int>> {
val accountId = context.currentAccountId val accountId = context.currentAccountId
Log.i( Log.i(
"RLog", "RLog",
@ -111,6 +111,12 @@ abstract class AbstractRssRepository constructor(
isUnread -> articleDao isUnread -> articleDao
.queryImportantCountWhenIsUnread(accountId, isUnread) .queryImportantCountWhenIsUnread(accountId, isUnread)
else -> articleDao.queryImportantCountWhenIsAll(accountId) else -> articleDao.queryImportantCountWhenIsAll(accountId)
}.mapLatest {
mapOf(
*(it.map {
it.feedId to it.important
}.toTypedArray())
)
}.flowOn(dispatcherIO) }.flowOn(dispatcherIO)
} }
@ -130,10 +136,6 @@ abstract class AbstractRssRepository constructor(
return feedDao.queryByLink(context.currentAccountId, url).isNotEmpty() return feedDao.queryByLink(context.currentAccountId, url).isNotEmpty()
} }
fun peekWork(): String {
return workManager.getWorkInfosByTag("sync").get().size.toString()
}
suspend fun updateGroup(group: Group) { suspend fun updateGroup(group: Group) {
groupDao.update(group) groupDao.update(group)
} }
@ -207,34 +209,3 @@ abstract class AbstractRssRepository constructor(
} }
} }
} }
@HiltWorker
class SyncWorker @AssistedInject constructor(
@Assisted context: Context,
@Assisted workerParams: WorkerParameters,
private val rssRepository: RssRepository,
) : CoroutineWorker(context, workerParams) {
override suspend fun doWork(): Result {
Log.i("RLog", "doWork: ")
return rssRepository.get().sync(this)
}
companion object {
const val WORK_NAME = "article.sync"
val UUID: UUID
val repeatingRequest = PeriodicWorkRequestBuilder<SyncWorker>(
15, TimeUnit.MINUTES
).setConstraints(
Constraints.Builder()
.build()
).addTag(WORK_NAME).build().also {
UUID = it.id
}
fun setIsSyncing(boolean: Boolean) = workDataOf("isSyncing" to boolean)
fun Data.getIsSyncing(): Boolean = getBoolean("isSyncing", false)
}
}

View File

@ -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 = ""
// )
// }
// }
// }
// }
//}

View File

@ -1,12 +1,7 @@
package me.ash.reader.data.repository package me.ash.reader.data.repository
import android.app.*
import android.content.Context import android.content.Context
import android.content.Intent
import android.graphics.BitmapFactory
import android.util.Log import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.work.CoroutineWorker import androidx.work.CoroutineWorker
import androidx.work.ListenableWorker import androidx.work.ListenableWorker
import androidx.work.WorkManager import androidx.work.WorkManager
@ -15,8 +10,6 @@ import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import me.ash.reader.MainActivity
import me.ash.reader.R
import me.ash.reader.data.dao.AccountDao import me.ash.reader.data.dao.AccountDao
import me.ash.reader.data.dao.ArticleDao import me.ash.reader.data.dao.ArticleDao
import me.ash.reader.data.dao.FeedDao import me.ash.reader.data.dao.FeedDao
@ -30,8 +23,6 @@ import me.ash.reader.data.module.DispatcherIO
import me.ash.reader.data.repository.SyncWorker.Companion.setIsSyncing import me.ash.reader.data.repository.SyncWorker.Companion.setIsSyncing
import me.ash.reader.ui.ext.currentAccountId import me.ash.reader.ui.ext.currentAccountId
import me.ash.reader.ui.ext.spacerDollar import me.ash.reader.ui.ext.spacerDollar
import me.ash.reader.ui.page.common.ExtraName
import me.ash.reader.ui.page.common.NotificationGroupName
import java.util.* import java.util.*
import javax.inject.Inject import javax.inject.Inject
@ -41,6 +32,7 @@ class LocalRssRepository @Inject constructor(
private val articleDao: ArticleDao, private val articleDao: ArticleDao,
private val feedDao: FeedDao, private val feedDao: FeedDao,
private val rssHelper: RssHelper, private val rssHelper: RssHelper,
private val notificationHelper: NotificationHelper,
private val accountDao: AccountDao, private val accountDao: AccountDao,
private val groupDao: GroupDao, private val groupDao: GroupDao,
@DispatcherDefault @DispatcherDefault
@ -52,16 +44,6 @@ class LocalRssRepository @Inject constructor(
context, accountDao, articleDao, groupDao, context, accountDao, articleDao, groupDao,
feedDao, workManager, dispatcherIO feedDao, workManager, dispatcherIO
) { ) {
private val notificationManager: NotificationManagerCompat =
NotificationManagerCompat.from(context).apply {
createNotificationChannel(
NotificationChannel(
NotificationGroupName.ARTICLE_UPDATE,
NotificationGroupName.ARTICLE_UPDATE,
NotificationManager.IMPORTANCE_DEFAULT
)
)
}
override suspend fun updateArticleInfo(article: Article) { override suspend fun updateArticleInfo(article: Article) {
articleDao.update(article) articleDao.update(article)
@ -98,7 +80,7 @@ class LocalRssRepository @Inject constructor(
.awaitAll() .awaitAll()
.forEach { .forEach {
if (it.isNotify) { if (it.isNotify) {
notify( notificationHelper.notify(
FeedWithArticle( FeedWithArticle(
it.feedWithArticle.feed, it.feedWithArticle.feed,
articleDao.insertIfNotExist(it.feedWithArticle.articles) articleDao.insertIfNotExist(it.feedWithArticle.articles)
@ -183,75 +165,4 @@ class LocalRssRepository @Inject constructor(
isNotify = articles.isNotEmpty() && feed.isNotification isNotify = articles.isNotEmpty() && feed.isNotification
) )
} }
private fun notify(
feedWithArticle: FeedWithArticle,
) {
notificationManager.createNotificationChannelGroup(
NotificationChannelGroup(
feedWithArticle.feed.id,
feedWithArticle.feed.name
)
)
feedWithArticle.articles.forEach { article ->
val builder = NotificationCompat.Builder(context, NotificationGroupName.ARTICLE_UPDATE)
.setSmallIcon(R.drawable.ic_notification)
.setLargeIcon(
(BitmapFactory.decodeResource(
context.resources,
R.drawable.ic_notification
))
)
.setContentTitle(article.title)
.setContentIntent(
PendingIntent.getActivity(
context,
Random().nextInt() + article.id.hashCode(),
Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_CLEAR_TASK
putExtra(
ExtraName.ARTICLE_ID,
article.id
)
},
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
)
.setGroup(feedWithArticle.feed.id)
.setStyle(
NotificationCompat.BigTextStyle()
.bigText(article.shortDescription)
.setSummaryText(feedWithArticle.feed.name)
)
notificationManager.notify(
Random().nextInt() + article.id.hashCode(),
builder.build().apply {
flags = Notification.FLAG_AUTO_CANCEL
}
)
}
if (feedWithArticle.articles.size > 1) {
notificationManager.notify(
Random().nextInt() + feedWithArticle.feed.id.hashCode(),
NotificationCompat.Builder(context, NotificationGroupName.ARTICLE_UPDATE)
.setSmallIcon(R.drawable.ic_notification)
.setLargeIcon(
(BitmapFactory.decodeResource(
context.resources,
R.drawable.ic_notification
))
)
.setStyle(
NotificationCompat.InboxStyle()
.setSummaryText(feedWithArticle.feed.name)
)
.setGroup(feedWithArticle.feed.id)
.setGroupSummary(true)
.build()
)
}
}
} }

View File

@ -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()
)
}
}
}

View File

@ -43,7 +43,7 @@ class OpmlRepository @Inject constructor(
repeatList.add(it) repeatList.add(it)
} }
} }
feedDao.insertList((groupWithFeed.feeds subtract repeatList).toList()) feedDao.insertList((groupWithFeed.feeds subtract repeatList.toSet()).toList())
} }
} }
@ -54,7 +54,7 @@ class OpmlRepository @Inject constructor(
Opml( Opml(
"2.0", "2.0",
Head( Head(
accountDao.queryById(context.currentAccountId).name, accountDao.queryById(context.currentAccountId)?.name,
Date().toString(), null, null, null, Date().toString(), null, null, null,
null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null,

View File

@ -4,27 +4,28 @@ import android.content.Context
import android.util.Log import android.util.Log
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import me.ash.reader.R import me.ash.reader.R
import me.ash.reader.data.entity.toVersion import me.ash.reader.data.model.toVersion
import me.ash.reader.data.module.ApplicationScope
import me.ash.reader.data.module.DispatcherIO import me.ash.reader.data.module.DispatcherIO
import me.ash.reader.data.module.DispatcherMain import me.ash.reader.data.module.DispatcherMain
import me.ash.reader.data.source.AppNetworkDataSource import me.ash.reader.data.preference.*
import me.ash.reader.data.preference.NewVersionSizePreference.formatSize
import me.ash.reader.data.source.Download import me.ash.reader.data.source.Download
import me.ash.reader.data.source.RYNetworkDataSource
import me.ash.reader.data.source.downloadToFileWithProgress import me.ash.reader.data.source.downloadToFileWithProgress
import me.ash.reader.ui.ext.* import me.ash.reader.ui.ext.getCurrentVersion
import me.ash.reader.ui.ext.getLatestApk
import me.ash.reader.ui.ext.showToast
import me.ash.reader.ui.ext.skipVersionNumber
import javax.inject.Inject import javax.inject.Inject
class AppRepository @Inject constructor( class RYRepository @Inject constructor(
@ApplicationContext @ApplicationContext
private val context: Context, private val context: Context,
private val appNetworkDataSource: AppNetworkDataSource, private val RYNetworkDataSource: RYNetworkDataSource,
@ApplicationScope
private val applicationScope: CoroutineScope,
@DispatcherIO @DispatcherIO
private val dispatcherIO: CoroutineDispatcher, private val dispatcherIO: CoroutineDispatcher,
@DispatcherMain @DispatcherMain
@ -33,7 +34,7 @@ class AppRepository @Inject constructor(
suspend fun checkUpdate(showToast: Boolean = true): Boolean? = withContext(dispatcherIO) { suspend fun checkUpdate(showToast: Boolean = true): Boolean? = withContext(dispatcherIO) {
try { try {
val response = val response =
appNetworkDataSource.getReleaseLatest(context.getString(R.string.update_link)) RYNetworkDataSource.getReleaseLatest(context.getString(R.string.update_link))
when { when {
response.code() == 403 -> { response.code() == 403 -> {
withContext(dispatcherMain) { withContext(dispatcherMain) {
@ -50,31 +51,22 @@ class AppRepository @Inject constructor(
} }
val latest = response.body()!! val latest = response.body()!!
val latestVersion = latest.tag_name.toVersion() val latestVersion = latest.tag_name.toVersion()
// val latestVersion = "0.7.3".toVersion() // val latestVersion = "1.0.0".toVersion()
val skipVersion = context.skipVersionNumber.toVersion() val skipVersion = context.skipVersionNumber.toVersion()
val currentVersion = context.getCurrentVersion() val currentVersion = context.getCurrentVersion()
val latestLog = latest.body ?: "" val latestLog = latest.body ?: ""
val latestPublishDate = latest.published_at ?: latest.created_at ?: "" val latestPublishDate = latest.published_at ?: latest.created_at ?: ""
val latestSize = latest.assets val latestSize = latest.assets?.first()?.size ?: 0
?.first() val latestDownloadUrl = latest.assets?.first()?.browser_download_url ?: ""
?.size
?: 0
val latestDownloadUrl = latest.assets
?.first()
?.browser_download_url
?: ""
Log.i("RLog", "current version $currentVersion") Log.i("RLog", "current version $currentVersion")
if (latestVersion.whetherNeedUpdate(currentVersion, skipVersion)) { if (latestVersion.whetherNeedUpdate(currentVersion, skipVersion)) {
Log.i("RLog", "new version $latestVersion") Log.i("RLog", "new version $latestVersion")
context.dataStore.put( NewVersionNumberPreference.put(context, this, latestVersion.toString())
DataStoreKeys.NewVersionNumber, NewVersionLogPreference.put(context, this, latestLog)
latestVersion.toString() NewVersionPublishDatePreference.put(context, this, latestPublishDate)
) NewVersionSizePreference.put(context, this, latestSize.formatSize())
context.dataStore.put(DataStoreKeys.NewVersionLog, latestLog) NewVersionDownloadUrlPreference.put(context, this, latestDownloadUrl)
context.dataStore.put(DataStoreKeys.NewVersionPublishDate, latestPublishDate)
context.dataStore.put(DataStoreKeys.NewVersionSize, latestSize)
context.dataStore.put(DataStoreKeys.NewVersionDownloadUrl, latestDownloadUrl)
true true
} else { } else {
false false
@ -93,7 +85,7 @@ class AppRepository @Inject constructor(
withContext(dispatcherIO) { withContext(dispatcherIO) {
Log.i("RLog", "downloadFile start: $url") Log.i("RLog", "downloadFile start: $url")
try { try {
return@withContext appNetworkDataSource.downloadFile(url) return@withContext RYNetworkDataSource.downloadFile(url)
.downloadToFileWithProgress(context.getLatestApk()) .downloadToFileWithProgress(context.getLatestApk())
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()

View File

@ -21,8 +21,6 @@ import net.dankito.readability4j.extended.Readability4JExtended
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import java.net.URL import java.net.URL
import java.text.ParsePosition
import java.text.SimpleDateFormat
import java.util.* import java.util.*
import javax.inject.Inject import javax.inject.Inject
@ -31,6 +29,7 @@ class RssHelper @Inject constructor(
private val context: Context, private val context: Context,
@DispatcherIO @DispatcherIO
private val dispatcherIO: CoroutineDispatcher, private val dispatcherIO: CoroutineDispatcher,
private val okHttpClient: OkHttpClient,
) { ) {
@Throws(Exception::class) @Throws(Exception::class)
suspend fun searchFeed(feedLink: String): FeedWithArticle { suspend fun searchFeed(feedLink: String): FeedWithArticle {
@ -58,7 +57,7 @@ class RssHelper @Inject constructor(
@Throws(Exception::class) @Throws(Exception::class)
suspend fun parseFullContent(link: String, title: String): String { suspend fun parseFullContent(link: String, title: String): String {
return withContext(dispatcherIO) { return withContext(dispatcherIO) {
val response = OkHttpClient() val response = okHttpClient
.newCall(Request.Builder().url(link).build()) .newCall(Request.Builder().url(link).build())
.execute() .execute()
val content = response.body!!.string() val content = response.body!!.string()
@ -85,7 +84,12 @@ class RssHelper @Inject constructor(
return withContext(dispatcherIO) { return withContext(dispatcherIO) {
val a = mutableListOf<Article>() val a = mutableListOf<Article>()
val accountId = context.currentAccountId val accountId = context.currentAccountId
val parseRss: SyndFeed = SyndFeedInput().build(XmlReader(URL(feed.url))) val parseRss: SyndFeed = SyndFeedInput().build(
XmlReader(URL(feed.url).openConnection().apply {
connectTimeout = 5000
readTimeout = 5000
})
)
parseRss.entries.forEach { parseRss.entries.forEach {
if (latestLink != null && latestLink == it.link) return@withContext a if (latestLink != null && latestLink == it.link) return@withContext a
val desc = it.description?.value val desc = it.description?.value
@ -110,13 +114,13 @@ class RssHelper @Inject constructor(
date = it.publishedDate ?: it.updatedDate ?: Date(), date = it.publishedDate ?: it.updatedDate ?: Date(),
title = Html.fromHtml(it.title.toString()).toString(), title = Html.fromHtml(it.title.toString()).toString(),
author = it.author, author = it.author,
rawDescription = (desc ?: content) ?: "", rawDescription = (content ?: desc) ?: "",
shortDescription = (Readability4JExtended("", desc ?: content ?: "") shortDescription = (Readability4JExtended("", desc ?: content ?: "")
.parse().textContent ?: "") .parse().textContent ?: "")
.take(100) .take(100)
.trim(), .trim(),
fullContent = content, fullContent = content,
img = findImg((desc ?: content) ?: ""), img = findImg((content ?: desc) ?: ""),
link = it.link ?: "", link = it.link ?: "",
) )
) )
@ -182,27 +186,4 @@ class RssHelper @Inject constructor(
} }
) )
} }
private fun parseDate(
inputDate: String, patterns: Array<String> = arrayOf(
"yyyy-MM-dd'T'HH:mm:ss'Z'",
"yyyy-MM-dd",
"yyyy-MM-dd HH:mm:ss",
"yyyyMMdd",
"yyyy/MM/dd",
"yyyy年MM月dd日",
"yyyy MM dd",
)
): Date? {
val df = SimpleDateFormat()
for (pattern in patterns) {
df.applyPattern(pattern)
df.isLenient = false
val date = df.parse(inputDate, ParsePosition(0))
if (date != null) {
return date
}
}
return null
}
} }

View File

@ -11,6 +11,13 @@ class StringsRepository @Inject constructor(
private val context: Context, private val context: Context,
) { ) {
fun getString(resId: Int, vararg formatArgs: Any) = context.getString(resId, *formatArgs) fun getString(resId: Int, vararg formatArgs: Any) = context.getString(resId, *formatArgs)
fun getQuantityString(resId: Int, quantity: Int, vararg formatArgs: Any) = context.resources.getQuantityString(resId, quantity, *formatArgs)
fun formatAsString(date: Date?) = date?.formatAsString(context) fun getQuantityString(resId: Int, quantity: Int, vararg formatArgs: Any) =
context.resources.getQuantityString(resId, quantity, *formatArgs)
fun formatAsString(
date: Date?,
onlyHourMinute: Boolean? = false,
atHourMinute: Boolean? = false
) = date?.formatAsString(context, onlyHourMinute, atHourMinute)
} }

View File

@ -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)
}
}

View File

@ -18,21 +18,21 @@ import java.util.*
entities = [Account::class, Feed::class, Article::class, Group::class], entities = [Account::class, Feed::class, Article::class, Group::class],
version = 2, version = 2,
) )
@TypeConverters(ReaderDatabase.Converters::class) @TypeConverters(RYDatabase.Converters::class)
abstract class ReaderDatabase : RoomDatabase() { abstract class RYDatabase : RoomDatabase() {
abstract fun accountDao(): AccountDao abstract fun accountDao(): AccountDao
abstract fun feedDao(): FeedDao abstract fun feedDao(): FeedDao
abstract fun articleDao(): ArticleDao abstract fun articleDao(): ArticleDao
abstract fun groupDao(): GroupDao abstract fun groupDao(): GroupDao
companion object { companion object {
private var instance: ReaderDatabase? = null private var instance: RYDatabase? = null
fun getInstance(context: Context): ReaderDatabase { fun getInstance(context: Context): RYDatabase {
return instance ?: synchronized(this) { return instance ?: synchronized(this) {
instance ?: Room.databaseBuilder( instance ?: Room.databaseBuilder(
context.applicationContext, context.applicationContext,
ReaderDatabase::class.java, RYDatabase::class.java,
"Reader" "Reader"
).addMigrations(*allMigrations).build().also { ).addMigrations(*allMigrations).build().also {
instance = it instance = it

View File

@ -5,7 +5,6 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import me.ash.reader.data.entity.LatestRelease
import okhttp3.ResponseBody import okhttp3.ResponseBody
import retrofit2.Response import retrofit2.Response
import retrofit2.Retrofit import retrofit2.Retrofit
@ -15,13 +14,7 @@ import retrofit2.http.Streaming
import retrofit2.http.Url import retrofit2.http.Url
import java.io.File import java.io.File
sealed class Download { interface RYNetworkDataSource {
object NotYet : Download()
data class Progress(val percent: Int) : Download()
data class Finished(val file: File) : Download()
}
interface AppNetworkDataSource {
@GET @GET
suspend fun getReleaseLatest(@Url url: String): Response<LatestRelease> suspend fun getReleaseLatest(@Url url: String): Response<LatestRelease>
@ -30,14 +23,14 @@ interface AppNetworkDataSource {
suspend fun downloadFile(@Url url: String): ResponseBody suspend fun downloadFile(@Url url: String): ResponseBody
companion object { companion object {
private var instance: AppNetworkDataSource? = null private var instance: RYNetworkDataSource? = null
fun getInstance(): AppNetworkDataSource { fun getInstance(): RYNetworkDataSource {
return instance ?: synchronized(this) { return instance ?: synchronized(this) {
instance ?: Retrofit.Builder() instance ?: Retrofit.Builder()
.baseUrl("https://api.github.com/") .baseUrl("https://api.github.com/")
.addConverterFactory(GsonConverterFactory.create()) .addConverterFactory(GsonConverterFactory.create())
.build().create(AppNetworkDataSource::class.java).also { .build().create(RYNetworkDataSource::class.java).also {
instance = it instance = it
} }
} }
@ -93,3 +86,31 @@ fun ResponseBody.downloadToFileWithProgress(saveFile: File): Flow<Download> =
} }
} }
}.flowOn(Dispatchers.IO).distinctUntilChanged() }.flowOn(Dispatchers.IO).distinctUntilChanged()
data class LatestRelease(
val html_url: String? = null,
val tag_name: String? = null,
val name: String? = null,
val draft: Boolean? = null,
val prerelease: Boolean? = null,
val created_at: String? = null,
val published_at: String? = null,
val assets: List<AssetsItem>? = null,
val body: String? = null,
)
data class AssetsItem(
val name: String? = null,
val content_type: String? = null,
val size: Int? = null,
val download_count: Int? = null,
val created_at: String? = null,
val updated_at: String? = null,
val browser_download_url: String? = null,
)
sealed class Download {
object NotYet : Download()
data class Progress(val percent: Int) : Download()
data class Finished(val file: File) : Download()
}

View File

@ -1,4 +1,4 @@
package me.ash.reader.ui.page.home package me.ash.reader.ui.component
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row

View File

@ -1,4 +1,4 @@
package me.ash.reader.ui.page.home package me.ash.reader.ui.component
import android.os.Build import android.os.Build
import android.view.SoundEffectConstants import android.view.SoundEffectConstants
@ -12,16 +12,15 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import me.ash.reader.data.entity.Filter import me.ash.reader.data.model.Filter
import me.ash.reader.data.model.getName
import me.ash.reader.data.preference.FlowFilterBarStylePreference import me.ash.reader.data.preference.FlowFilterBarStylePreference
import me.ash.reader.data.preference.LocalThemeIndex import me.ash.reader.data.preference.LocalThemeIndex
import me.ash.reader.ui.ext.getName
import me.ash.reader.ui.ext.surfaceColorAtElevation import me.ash.reader.ui.ext.surfaceColorAtElevation
import me.ash.reader.ui.theme.palette.onDark import me.ash.reader.ui.theme.palette.onDark
@Composable @Composable
fun FilterBar( fun FilterBar(
modifier: Modifier = Modifier,
filter: Filter, filter: Filter,
filterBarStyle: Int, filterBarStyle: Int,
filterBarFilled: Boolean, filterBarFilled: Boolean,
@ -39,11 +38,7 @@ fun FilterBar(
tonalElevation = filterBarTonalElevation, tonalElevation = filterBarTonalElevation,
) { ) {
Spacer(modifier = Modifier.width(filterBarPadding)) Spacer(modifier = Modifier.width(filterBarPadding))
listOf( Filter.values.forEach { item ->
Filter.Starred,
Filter.Unread,
Filter.All,
).forEach { item ->
NavigationBarItem( NavigationBarItem(
// modifier = Modifier.height(60.dp), // modifier = Modifier.height(60.dp),
alwaysShowLabel = when (filterBarStyle) { alwaysShowLabel = when (filterBarStyle) {

View File

@ -1,6 +1,6 @@
package me.ash.reader.ui.component package me.ash.reader.ui.component.base
import androidx.compose.animation.* import RYExtensibleVisibility
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.statusBars
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -38,11 +38,7 @@ fun AnimatedPopup(
} }
}, },
) { ) {
AnimatedVisibility( RYExtensibleVisibility(visible = visible) {
visible = visible,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(),
) {
content() content()
} }
} }

View File

@ -1,4 +1,4 @@
package me.ash.reader.ui.component package me.ash.reader.ui.component.base
import androidx.compose.animation.* import androidx.compose.animation.*
import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.FastOutSlowInEasing

View File

@ -6,7 +6,7 @@
* @modifier Ashinch * @modifier Ashinch
*/ */
package me.ash.reader.ui.component package me.ash.reader.ui.component.base
import android.view.SoundEffectConstants import android.view.SoundEffectConstants
import androidx.compose.animation.Crossfade import androidx.compose.animation.Crossfade

View File

@ -1,4 +1,4 @@
package me.ash.reader.ui.component package me.ash.reader.ui.component.base
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable

View File

@ -1,4 +1,4 @@
package me.ash.reader.ui.component package me.ash.reader.ui.component.base
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -7,7 +7,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@Composable @Composable
fun BlockRadioGroupButton( fun BlockRadioButton(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
selected: Int = 0, selected: Int = 0,
onSelected: (Int) -> Unit, onSelected: (Int) -> Unit,

View File

@ -1,4 +1,4 @@
package me.ash.reader.ui.component package me.ash.reader.ui.component.base
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*

View File

@ -1,4 +1,4 @@
package me.ash.reader.ui.component package me.ash.reader.ui.component.base
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon import androidx.compose.material3.Icon

View File

@ -1,4 +1,4 @@
package me.ash.reader.ui.component package me.ash.reader.ui.component.base
import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -32,7 +32,7 @@ fun ClipboardTextField(
) { ) {
Column(modifier = modifier) { Column(modifier = modifier) {
Spacer(modifier = Modifier.height(10.dp)) Spacer(modifier = Modifier.height(10.dp))
TextField( RYTextField(
readOnly = readOnly, readOnly = readOnly,
value = value, value = value,
onValueChange = onValueChange, onValueChange = onValueChange,

View File

@ -1,4 +1,4 @@
package me.ash.reader.ui.component package me.ash.reader.ui.component.base
import androidx.compose.foundation.shape.CornerBasedShape import androidx.compose.foundation.shape.CornerBasedShape
import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.shape.CornerSize

View File

@ -1,6 +1,6 @@
package me.ash.reader.ui.component package me.ash.reader.ui.component.base
import androidx.compose.animation.* import RYExtensibleVisibility
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
@ -41,11 +41,7 @@ fun DisplayText(
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
) )
AnimatedVisibility( RYExtensibleVisibility(visible = desc.isNotEmpty()) {
visible = desc.isNotEmpty(),
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(),
) {
Text( Text(
modifier = Modifier.height(16.dp), modifier = Modifier.height(16.dp),
text = desc, text = desc,

View File

@ -1,4 +1,4 @@
package me.ash.reader.ui.component package me.ash.reader.ui.component.base
import android.graphics.drawable.PictureDrawable import android.graphics.drawable.PictureDrawable
import androidx.compose.animation.Crossfade import androidx.compose.animation.Crossfade
@ -41,7 +41,7 @@ fun DynamicSVGImage(
}, },
) { ) {
Crossfade(targetState = pic) { Crossfade(targetState = pic) {
AsyncImage( RYAsyncImage(
contentDescription = contentDescription, contentDescription = contentDescription,
data = it, data = it,
placeholder = null, placeholder = null,

View File

@ -1,4 +1,4 @@
package me.ash.reader.ui.component package me.ash.reader.ui.component.base
import android.view.HapticFeedbackConstants import android.view.HapticFeedbackConstants
import android.view.SoundEffectConstants import android.view.SoundEffectConstants

View File

@ -1,4 +1,4 @@
package me.ash.reader.ui.component package me.ash.reader.ui.component.base
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenu

View File

@ -1,7 +1,7 @@
package me.ash.reader.ui.component package me.ash.reader.ui.component.base
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.compose.material3.MaterialTheme import androidx.compose.foundation.Image
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -10,19 +10,16 @@ import androidx.compose.ui.graphics.DefaultAlpha
import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import coil.compose.rememberImagePainter
import androidx.compose.ui.res.painterResource
import coil.compose.LocalImageLoader
import coil.request.ImageRequest
import coil.size.Precision import coil.size.Precision
import coil.size.Scale import coil.size.Scale
import coil.size.Size import coil.size.Size
import me.ash.reader.R import me.ash.reader.R
val Size_1000 = Size(1000, 1000) val SIZE_1000 = Size(1000, 1000)
@Composable @Composable
fun AsyncImage( fun RYAsyncImage(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
data: Any? = null, data: Any? = null,
size: Size = Size.ORIGINAL, size: Size = Size.ORIGINAL,
@ -33,34 +30,51 @@ fun AsyncImage(
@DrawableRes placeholder: Int? = R.drawable.ic_hourglass_empty_black_24dp, @DrawableRes placeholder: Int? = R.drawable.ic_hourglass_empty_black_24dp,
@DrawableRes error: Int? = R.drawable.ic_broken_image_black_24dp, @DrawableRes error: Int? = R.drawable.ic_broken_image_black_24dp,
) { ) {
coil.compose.AsyncImage( Image(
modifier = modifier, painter = rememberImagePainter(
model = ImageRequest data = data,
.Builder(LocalContext.current) builder = {
.data(data) if (placeholder != null) placeholder(placeholder)
.crossfade(true) if (error != null) error(error)
.scale(scale) crossfade(true)
.precision(precision) scale(scale)
.size(size) precision(precision)
.build(), size(size)
},
),
contentDescription = contentDescription, contentDescription = contentDescription,
contentScale = contentScale, contentScale = contentScale,
imageLoader = LocalImageLoader.current, modifier = modifier,
placeholder = placeholder?.run {
forwardingPainter(
painter = painterResource(this),
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurfaceVariant),
alpha = 0.1f,
)
},
error = error?.run {
forwardingPainter(
painter = painterResource(this),
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurfaceVariant),
alpha = 0.1f,
)
},
) )
// coil.compose.AsyncImage(
// modifier = modifier,
// model = ImageRequest
// .Builder(LocalContext.current)
// .data(data)
// .crossfade(true)
// .scale(scale)
// .precision(precision)
// .size(size)
// .build(),
// contentDescription = contentDescription,
// contentScale = contentScale,
// imageLoader = LocalImageLoader.current,
// placeholder = placeholder?.run {
// forwardingPainter(
// painter = painterResource(this),
// colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurfaceVariant),
// alpha = 0.1f,
// )
// },
// error = error?.run {
// forwardingPainter(
// painter = painterResource(this),
// colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurfaceVariant),
// alpha = 0.1f,
// )
// },
// )
} }
// From: https://gist.github.com/colinrtwhite/c2966e0b8584b4cdf0a5b05786b20ae1 // From: https://gist.github.com/colinrtwhite/c2966e0b8584b4cdf0a5b05786b20ae1

View File

@ -1,4 +1,4 @@
package me.ash.reader.ui.component package me.ash.reader.ui.component.base
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -6,7 +6,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
@Composable @Composable
fun Dialog( fun RYDialog(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
visible: Boolean, visible: Boolean,
properties: DialogProperties = DialogProperties(), properties: DialogProperties = DialogProperties(),

View File

@ -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,
)
}

View File

@ -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() },
)
}

View File

@ -1,4 +1,4 @@
package me.ash.reader.ui.component package me.ash.reader.ui.component.base
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
@ -26,7 +26,7 @@ import me.ash.reader.ui.theme.palette.alwaysLight
@OptIn(ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
fun SelectionChip( fun RYSelectionChip(
content: String, content: String,
selected: Boolean, selected: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,

View File

@ -6,7 +6,7 @@
* @modifier Ashinch * @modifier Ashinch
*/ */
package me.ash.reader.ui.component package me.ash.reader.ui.component.base
import androidx.compose.animation.animateColorAsState import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateDpAsState
@ -31,7 +31,7 @@ import me.ash.reader.ui.theme.palette.onDark
// TODO: ripple & swipe // TODO: ripple & swipe
@Composable @Composable
fun Switch( fun RYSwitch(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
activated: Boolean, activated: Boolean,
enable: Boolean = true, enable: Boolean = true,
@ -101,7 +101,7 @@ fun SwitchHeadline(
) )
} }
Box(Modifier.padding(start = 20.dp)) { Box(Modifier.padding(start = 20.dp)) {
Switch(activated = activated) RYSwitch(activated = activated)
} }
} }
} }

View File

@ -1,4 +1,4 @@
package me.ash.reader.ui.component package me.ash.reader.ui.component.base
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
@ -19,7 +19,7 @@ import kotlinx.coroutines.delay
import me.ash.reader.R import me.ash.reader.R
@Composable @Composable
fun TextField( fun RYTextField(
readOnly: Boolean, readOnly: Boolean,
value: String, value: String,
onValueChange: (String) -> Unit, onValueChange: (String) -> Unit,
@ -39,7 +39,7 @@ fun TextField(
TextField( TextField(
modifier = Modifier.focusRequester(focusRequester), modifier = Modifier.focusRequester(focusRequester),
colors = TextFieldDefaults.textFieldColors( colors = TextFieldDefaults.textFieldColors(
backgroundColor = Color.Transparent, containerColor = Color.Transparent,
), ),
maxLines = 1, maxLines = 1,
enabled = !readOnly, enabled = !readOnly,

View File

@ -1,4 +1,4 @@
package me.ash.reader.ui.component package me.ash.reader.ui.component.base
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@ -19,9 +19,8 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.style.BaselineShift import androidx.compose.ui.text.style.BaselineShift
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.google.accompanist.pager.ExperimentalPagerApi
@OptIn(ExperimentalPagerApi::class, ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun RadioDialog( fun RadioDialog(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
@ -30,7 +29,7 @@ fun RadioDialog(
options: List<RadioDialogOption> = emptyList(), options: List<RadioDialogOption> = emptyList(),
onDismissRequest: () -> Unit = {}, onDismissRequest: () -> Unit = {},
) { ) {
Dialog( RYDialog(
modifier = modifier, modifier = modifier,
visible = visible, visible = visible,
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,

View File

@ -1,4 +1,4 @@
package me.ash.reader.ui.component package me.ash.reader.ui.component.base
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding

View File

@ -1,4 +1,4 @@
package me.ash.reader.ui.component package me.ash.reader.ui.component.base
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable

View File

@ -1,4 +1,4 @@
package me.ash.reader.ui.component package me.ash.reader.ui.component.base
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -13,10 +13,8 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import com.google.accompanist.pager.ExperimentalPagerApi
import me.ash.reader.R import me.ash.reader.R
@OptIn(ExperimentalPagerApi::class)
@Composable @Composable
fun TextFieldDialog( fun TextFieldDialog(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
@ -37,7 +35,7 @@ fun TextFieldDialog(
) { ) {
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
Dialog( RYDialog(
modifier = modifier, modifier = modifier,
visible = visible, visible = visible,
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,

View File

@ -1,4 +1,4 @@
package me.ash.reader.ui.component package me.ash.reader.ui.component.base
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons

View File

@ -1,4 +1,4 @@
package me.ash.reader.ui.component package me.ash.reader.ui.component.base
import androidx.compose.animation.animateContentSize import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth

View File

@ -1,4 +1,4 @@
package me.ash.reader.ui.component package me.ash.reader.ui.component.base
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri

View File

@ -33,7 +33,6 @@ import androidx.compose.foundation.text.selection.DisableSelection
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.ExperimentalComposeApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.RectangleShape
@ -48,12 +47,11 @@ import androidx.compose.ui.text.style.BaselineShift
import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil.annotation.ExperimentalCoilApi
import coil.size.Precision import coil.size.Precision
import coil.size.Size import coil.size.Size
import coil.size.pxOrElse import coil.size.pxOrElse
import me.ash.reader.R import me.ash.reader.R
import me.ash.reader.ui.component.AsyncImage import me.ash.reader.ui.component.base.RYAsyncImage
import org.jsoup.Jsoup import org.jsoup.Jsoup
import org.jsoup.helper.StringUtil import org.jsoup.helper.StringUtil
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
@ -178,7 +176,6 @@ private fun LazyListScope.formatCodeBlock(
composer.terminateCurrentText() composer.terminateCurrentText()
} }
@OptIn(ExperimentalComposeApi::class, ExperimentalCoilApi::class)
private fun TextComposer.appendTextChildren( private fun TextComposer.appendTextChildren(
nodes: List<Node>, nodes: List<Node>,
preFormatted: Boolean = false, preFormatted: Boolean = false,
@ -241,7 +238,7 @@ private fun TextComposer.appendTextChildren(
withComposableStyle( withComposableStyle(
style = { h5Style().toSpanStyle() } style = { h5Style().toSpanStyle() }
) { ) {
append(element.text()) append("\n${element.text()}")
} }
} }
} }
@ -250,7 +247,7 @@ private fun TextComposer.appendTextChildren(
withComposableStyle( withComposableStyle(
style = { h5Style().toSpanStyle() } style = { h5Style().toSpanStyle() }
) { ) {
append(element.text()) append("\n${element.text()}")
} }
} }
} }
@ -259,7 +256,7 @@ private fun TextComposer.appendTextChildren(
withComposableStyle( withComposableStyle(
style = { h5Style().toSpanStyle() } style = { h5Style().toSpanStyle() }
) { ) {
append(element.text()) append("\n${element.text()}")
} }
} }
} }
@ -268,7 +265,7 @@ private fun TextComposer.appendTextChildren(
withComposableStyle( withComposableStyle(
style = { h5Style().toSpanStyle() } style = { h5Style().toSpanStyle() }
) { ) {
append(element.text()) append("\n${element.text()}")
} }
} }
} }
@ -277,7 +274,7 @@ private fun TextComposer.appendTextChildren(
withComposableStyle( withComposableStyle(
style = { h5Style().toSpanStyle() } style = { h5Style().toSpanStyle() }
) { ) {
append(element.text()) append("\n${element.text()}")
} }
} }
} }
@ -286,7 +283,7 @@ private fun TextComposer.appendTextChildren(
withComposableStyle( withComposableStyle(
style = { h5Style().toSpanStyle() } style = { h5Style().toSpanStyle() }
) { ) {
append(element.text()) append("\n${element.text()}")
} }
} }
} }
@ -445,6 +442,7 @@ private fun TextComposer.appendTextChildren(
// .padding(horizontal = PADDING_HORIZONTAL.dp) // .padding(horizontal = PADDING_HORIZONTAL.dp)
.width(MAX_CONTENT_WIDTH.dp) .width(MAX_CONTENT_WIDTH.dp)
) { ) {
Spacer(modifier = Modifier.height(PADDING_HORIZONTAL.dp))
DisableSelection { DisableSelection {
BoxWithConstraints( BoxWithConstraints(
modifier = Modifier modifier = Modifier
@ -468,8 +466,12 @@ private fun TextComposer.appendTextChildren(
// } // }
) { ) {
val imageSize = maxImageSize() val imageSize = maxImageSize()
AsyncImage( RYAsyncImage(
modifier = Modifier.fillMaxWidth(), modifier = Modifier
.fillMaxWidth()
.padding(horizontal = PADDING_HORIZONTAL.dp)
.clip(IMAGE_SHAPE)
.clickable { },
data = imageCandidates.getBestImageForMaxSize( data = imageCandidates.getBestImageForMaxSize(
pixelDensity = pixelDensity(), pixelDensity = pixelDensity(),
maxSize = imageSize, maxSize = imageSize,
@ -594,12 +596,14 @@ private fun TextComposer.appendTextChildren(
BoxWithConstraints( BoxWithConstraints(
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) { ) {
AsyncImage( RYAsyncImage(
modifier = Modifier modifier = Modifier
.fillMaxWidth()
.padding(horizontal = PADDING_HORIZONTAL.dp)
.clip(IMAGE_SHAPE)
.clickable { .clickable {
onLinkClick(video.link) onLinkClick(video.link)
} },
.fillMaxWidth(),
data = video.imageUrl, data = video.imageUrl,
size = maxImageSize(), size = maxImageSize(),
contentDescription = stringResource(R.string.touch_to_play_video), contentDescription = stringResource(R.string.touch_to_play_video),
@ -646,7 +650,6 @@ private fun TextComposer.appendTextChildren(
} }
} }
@OptIn(ExperimentalStdlibApi::class)
private fun String.asFontFamily(): FontFamily? = when (this.lowercase()) { private fun String.asFontFamily(): FontFamily? = when (this.lowercase()) {
"monospace" -> FontFamily.Monospace "monospace" -> FontFamily.Monospace
"serif" -> FontFamily.Serif "serif" -> FontFamily.Serif

View File

@ -27,7 +27,8 @@ import android.util.Log
import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyListScope
import me.ash.reader.R import me.ash.reader.R
fun LazyListScope.reader( @Suppress("FunctionName")
fun LazyListScope.Reader(
context: Context, context: Context,
link: String, link: String,
content: String, content: String,

View File

@ -20,6 +20,7 @@
package me.ash.reader.ui.component.reader package me.ash.reader.ui.component.reader
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@ -27,12 +28,14 @@ import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import me.ash.reader.ui.ext.alphaLN import me.ash.reader.ui.ext.alphaLN
const val PADDING_HORIZONTAL = 24.0 const val PADDING_HORIZONTAL = 24.0
const val MAX_CONTENT_WIDTH = 840.0 const val MAX_CONTENT_WIDTH = 840.0
val IMAGE_SHAPE = RoundedCornerShape(32.dp)
@Composable @Composable
fun bodyForeground(): Color = fun bodyForeground(): Color =
@ -71,7 +74,7 @@ fun h4Style(): TextStyle =
@Composable @Composable
fun h5Style(): TextStyle = fun h5Style(): TextStyle =
MaterialTheme.typography.headlineSmall.copy( MaterialTheme.typography.headlineSmall.copy(
color = bodyForeground() color = bodyForeground(),
) )
@Composable @Composable
@ -83,7 +86,8 @@ fun h6Style(): TextStyle =
@Composable @Composable
fun captionStyle(): TextStyle = fun captionStyle(): TextStyle =
MaterialTheme.typography.bodySmall.copy( MaterialTheme.typography.bodySmall.copy(
color = bodyForeground().copy(alpha = 0.6f) color = bodyForeground().copy(alpha = 0.6f),
textAlign = TextAlign.Center,
) )
@Composable @Composable

View File

@ -79,7 +79,7 @@ class TextComposer(
) -> R ) -> R
): R { ): R {
val url = link ?: findClosestLink() val url = link ?: findClosestLink()
builder.ensureDoubleNewline() //builder.ensureDoubleNewline()
terminateCurrentText() terminateCurrentText()
val onClick: (() -> Unit)? = if (url?.isNotBlank() == true) { val onClick: (() -> Unit)? = if (url?.isNotBlank() == true) {
{ {

View File

@ -1,16 +1,19 @@
package me.ash.reader.ui.ext package me.ash.reader.ui.ext
import androidx.compose.material3.ColorScheme import androidx.compose.material3.ColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.compositeOver import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import kotlin.math.ln import kotlin.math.ln
@Composable
fun ColorScheme.surfaceColorAtElevation( fun ColorScheme.surfaceColorAtElevation(
elevation: Dp, elevation: Dp,
color: Color = surface, color: Color = surface,
): Color = color.atElevation(surfaceTint, elevation) ): Color = remember(this, elevation, color) { color.atElevation(surfaceTint, elevation) }
fun Color.atElevation( fun Color.atElevation(
sourceColor: Color, sourceColor: Color,

View File

@ -4,11 +4,13 @@ import android.app.Activity
import android.content.Context import android.content.Context
import android.content.ContextWrapper import android.content.ContextWrapper
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import me.ash.reader.data.entity.Version import me.ash.reader.R
import me.ash.reader.data.entity.toVersion import me.ash.reader.data.model.Version
import me.ash.reader.data.model.toVersion
import java.io.File import java.io.File
fun Context.findActivity(): Activity? = when (this) { fun Context.findActivity(): Activity? = when (this) {
@ -54,3 +56,18 @@ fun Context.showToast(message: String?, duration: Int = Toast.LENGTH_SHORT) {
fun Context.showToastLong(message: String?) { fun Context.showToastLong(message: String?) {
showToast(message, Toast.LENGTH_LONG) showToast(message, Toast.LENGTH_LONG)
} }
fun Context.share(content: String) {
startActivity(Intent.createChooser(Intent(Intent.ACTION_SEND).apply {
putExtra(
Intent.EXTRA_TEXT,
content,
)
type = "text/plain"
}, getString(R.string.share)))
}
fun Context.openURL(url: String?) {
url?.takeIf { it.trim().isNotEmpty() }
?.let { startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(it))) }
}

View File

@ -15,16 +15,6 @@ import java.io.IOException
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings") val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
val Context.newVersionPublishDate: String
get() = this.dataStore.get(DataStoreKeys.NewVersionPublishDate) ?: ""
val Context.newVersionLog: String
get() = this.dataStore.get(DataStoreKeys.NewVersionLog) ?: ""
val Context.newVersionSize: Int
get() = this.dataStore.get(DataStoreKeys.NewVersionSize) ?: 0
val Context.newVersionDownloadUrl: String
get() = this.dataStore.get(DataStoreKeys.NewVersionDownloadUrl) ?: ""
val Context.newVersionNumber: String
get() = this.dataStore.get(DataStoreKeys.NewVersionNumber) ?: ""
val Context.skipVersionNumber: String val Context.skipVersionNumber: String
get() = this.dataStore.get(DataStoreKeys.SkipVersionNumber) ?: "" get() = this.dataStore.get(DataStoreKeys.SkipVersionNumber) ?: ""
val Context.isFirstLaunch: Boolean val Context.isFirstLaunch: Boolean
@ -93,9 +83,9 @@ sealed class DataStoreKeys<T> {
get() = stringPreferencesKey("newVersionLog") get() = stringPreferencesKey("newVersionLog")
} }
object NewVersionSize : DataStoreKeys<Int>() { object NewVersionSize : DataStoreKeys<String>() {
override val key: Preferences.Key<Int> override val key: Preferences.Key<String>
get() = intPreferencesKey("newVersionSize") get() = stringPreferencesKey("newVersionSizeString")
} }
object NewVersionDownloadUrl : DataStoreKeys<String>() { object NewVersionDownloadUrl : DataStoreKeys<String>() {

View File

@ -4,6 +4,7 @@ import android.content.Context
import androidx.core.os.ConfigurationCompat import androidx.core.os.ConfigurationCompat
import me.ash.reader.R import me.ash.reader.R
import java.text.DateFormat import java.text.DateFormat
import java.text.ParsePosition
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
@ -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
}

View File

@ -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)
}

View 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

View File

@ -1,8 +1,7 @@
package me.ash.reader.ui.ext package me.ash.reader.ui.ext
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.runtime.remember
import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.LazyPagingItems
import kotlin.math.abs import kotlin.math.abs
@ -28,3 +27,33 @@ fun <T : Any> LazyPagingItems<T>.rememberLazyListState(): LazyListState {
else -> androidx.compose.foundation.lazy.rememberLazyListState() else -> androidx.compose.foundation.lazy.rememberLazyListState()
} }
} }
/**
* TODO: To be improved
*
* Returns whether the LazyListState is currently in the
* downward scrolling state.
*/
@Composable
fun LazyListState.isScrollDown(): Boolean {
var isScrollDown by remember { mutableStateOf(false) }
var preItemIndex by remember { mutableStateOf(0) }
var preScrollStartOffset by remember { mutableStateOf(0) }
LaunchedEffect(this) {
snapshotFlow { isScrollInProgress }.collect {
if (isScrollInProgress) {
isScrollDown = when {
firstVisibleItemIndex > preItemIndex -> true
firstVisibleItemScrollOffset < preItemIndex -> false
else -> firstVisibleItemScrollOffset > preScrollStartOffset
}
} else {
preItemIndex = firstVisibleItemIndex
preScrollStartOffset = firstVisibleItemScrollOffset
}
}
}
return isScrollDown
}

View File

@ -52,6 +52,7 @@ import androidx.compose.ui.composed
import androidx.compose.ui.draw.CacheDrawScope import androidx.compose.ui.draw.CacheDrawScope
import androidx.compose.ui.draw.DrawResult import androidx.compose.ui.draw.DrawResult
import androidx.compose.ui.draw.drawWithCache import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@ -173,11 +174,15 @@ private fun CacheDrawScope.onDrawScrollbar(
return { return {
if (showScrollbar) { if (showScrollbar) {
drawRect( drawRoundRect(
color = color, color = color,
topLeft = topLeft, topLeft = topLeft,
size = size, size = size,
alpha = alpha() alpha = alpha(),
cornerRadius = CornerRadius(
x = size.width,
y = size.width,
)
) )
} }
} }
@ -217,7 +222,7 @@ private fun Modifier.drawScrollbar(
val alpha = remember { Animatable(0f) } val alpha = remember { Animatable(0f) }
LaunchedEffect(scrolled, alpha) { LaunchedEffect(scrolled, alpha) {
scrolled.collectLatest { scrolled.collectLatest {
alpha.snapTo(1f) alpha.snapTo(0.3f)
delay(ViewConfiguration.getScrollDefaultDelay().toLong()) delay(ViewConfiguration.getScrollDefaultDelay().toLong())
alpha.animateTo(0f, animationSpec = FadeOutAnimationSpec) alpha.animateTo(0f, animationSpec = FadeOutAnimationSpec)
} }
@ -231,7 +236,7 @@ private fun Modifier.drawScrollbar(
// Calculate thickness here to workaround https://issuetracker.google.com/issues/206972664 // Calculate thickness here to workaround https://issuetracker.google.com/issues/206972664
val thickness = with(LocalDensity.current) { Thickness.toPx() } val thickness = with(LocalDensity.current) { Thickness.toPx() }
val color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) val color = MaterialTheme.colorScheme.onSurfaceVariant
Modifier Modifier
.nestedScroll(nestedScrollConnection) .nestedScroll(nestedScrollConnection)
.drawWithCache { .drawWithCache {

View File

@ -12,22 +12,21 @@ import androidx.hilt.navigation.compose.hiltViewModel
import com.google.accompanist.navigation.animation.AnimatedNavHost import com.google.accompanist.navigation.animation.AnimatedNavHost
import com.google.accompanist.navigation.animation.rememberAnimatedNavController import com.google.accompanist.navigation.animation.rememberAnimatedNavController
import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.google.accompanist.systemuicontroller.rememberSystemUiController
import me.ash.reader.data.entity.Filter import me.ash.reader.data.model.Filter
import me.ash.reader.data.preference.LocalDarkTheme import me.ash.reader.data.preference.LocalDarkTheme
import me.ash.reader.ui.ext.* import me.ash.reader.ui.ext.*
import me.ash.reader.ui.page.home.HomeViewAction
import me.ash.reader.ui.page.home.HomeViewModel import me.ash.reader.ui.page.home.HomeViewModel
import me.ash.reader.ui.page.home.feeds.FeedsPage import me.ash.reader.ui.page.home.feeds.FeedsPage
import me.ash.reader.ui.page.home.flow.FlowPage import me.ash.reader.ui.page.home.flow.FlowPage
import me.ash.reader.ui.page.home.read.ReadPage import me.ash.reader.ui.page.home.reading.ReadingPage
import me.ash.reader.ui.page.settings.SettingsPage import me.ash.reader.ui.page.settings.SettingsPage
import me.ash.reader.ui.page.settings.color.ColorAndStyle import me.ash.reader.ui.page.settings.color.ColorAndStylePage
import me.ash.reader.ui.page.settings.color.DarkTheme import me.ash.reader.ui.page.settings.color.DarkThemePage
import me.ash.reader.ui.page.settings.color.feeds.FeedsPageStyle import me.ash.reader.ui.page.settings.color.feeds.FeedsPageStylePage
import me.ash.reader.ui.page.settings.color.flow.FlowPageStyle import me.ash.reader.ui.page.settings.color.flow.FlowPageStylePage
import me.ash.reader.ui.page.settings.interaction.Interaction import me.ash.reader.ui.page.settings.interaction.InteractionPage
import me.ash.reader.ui.page.settings.languages.Languages import me.ash.reader.ui.page.settings.languages.LanguagesPage
import me.ash.reader.ui.page.settings.tips.TipsAndSupport import me.ash.reader.ui.page.settings.tips.TipsAndSupportPage
import me.ash.reader.ui.page.startup.StartupPage import me.ash.reader.ui.page.startup.StartupPage
import me.ash.reader.ui.theme.AppTheme import me.ash.reader.ui.theme.AppTheme
@ -37,7 +36,7 @@ fun HomeEntry(
homeViewModel: HomeViewModel = hiltViewModel(), homeViewModel: HomeViewModel = hiltViewModel(),
) { ) {
val context = LocalContext.current val context = LocalContext.current
val filterState = homeViewModel.filterState.collectAsStateValue() val filterUiState = homeViewModel.filterUiState.collectAsStateValue()
val navController = rememberAnimatedNavController() val navController = rememberAnimatedNavController()
val intent by rememberSaveable { mutableStateOf(context.findActivity()?.intent) } val intent by rememberSaveable { mutableStateOf(context.findActivity()?.intent) }
@ -57,16 +56,14 @@ fun HomeEntry(
// Other initial pages // Other initial pages
} }
homeViewModel.dispatch( homeViewModel.changeFilter(
HomeViewAction.ChangeFilter( filterUiState.copy(
filterState.copy( filter = when (context.initialFilter) {
filter = when (context.initialFilter) { 0 -> Filter.Starred
0 -> Filter.Starred 1 -> Filter.Unread
1 -> Filter.Unread 2 -> Filter.All
2 -> Filter.All else -> Filter.All
else -> Filter.All }
}
)
) )
) )
} }
@ -114,7 +111,7 @@ fun HomeEntry(
) )
} }
animatedComposable(route = "${RouteName.READING}/{articleId}") { animatedComposable(route = "${RouteName.READING}/{articleId}") {
ReadPage(navController = navController) ReadingPage(navController = navController)
} }
// Settings // Settings
@ -124,31 +121,31 @@ fun HomeEntry(
// Color & Style // Color & Style
animatedComposable(route = RouteName.COLOR_AND_STYLE) { animatedComposable(route = RouteName.COLOR_AND_STYLE) {
ColorAndStyle(navController) ColorAndStylePage(navController)
} }
animatedComposable(route = RouteName.DARK_THEME) { animatedComposable(route = RouteName.DARK_THEME) {
DarkTheme(navController) DarkThemePage(navController)
} }
animatedComposable(route = RouteName.FEEDS_PAGE_STYLE) { animatedComposable(route = RouteName.FEEDS_PAGE_STYLE) {
FeedsPageStyle(navController) FeedsPageStylePage(navController)
} }
animatedComposable(route = RouteName.FLOW_PAGE_STYLE) { animatedComposable(route = RouteName.FLOW_PAGE_STYLE) {
FlowPageStyle(navController) FlowPageStylePage(navController)
} }
// Interaction // Interaction
animatedComposable(route = RouteName.INTERACTION) { animatedComposable(route = RouteName.INTERACTION) {
Interaction(navController) InteractionPage(navController)
} }
// Languages // Languages
animatedComposable(route = RouteName.LANGUAGES) { animatedComposable(route = RouteName.LANGUAGES) {
Languages(navController = navController) LanguagesPage(navController = navController)
} }
// Tips & Support // Tips & Support
animatedComposable(route = RouteName.TIPS_AND_SUPPORT) { animatedComposable(route = RouteName.TIPS_AND_SUPPORT) {
TipsAndSupport(navController) TipsAndSupportPage(navController)
} }
} }
} }

View File

@ -7,8 +7,8 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import me.ash.reader.data.entity.Feed import me.ash.reader.data.entity.Feed
import me.ash.reader.data.entity.Filter
import me.ash.reader.data.entity.Group import me.ash.reader.data.entity.Group
import me.ash.reader.data.model.Filter
import me.ash.reader.data.module.ApplicationScope import me.ash.reader.data.module.ApplicationScope
import me.ash.reader.data.repository.RssRepository import me.ash.reader.data.repository.RssRepository
import me.ash.reader.data.repository.StringsRepository import me.ash.reader.data.repository.StringsRepository
@ -24,30 +24,20 @@ class HomeViewModel @Inject constructor(
private val applicationScope: CoroutineScope, private val applicationScope: CoroutineScope,
private val workManager: WorkManager, private val workManager: WorkManager,
) : ViewModel() { ) : ViewModel() {
private val _homeUiState = MutableStateFlow(HomeUiState())
val homeUiState: StateFlow<HomeUiState> = _homeUiState.asStateFlow()
private val _viewState = MutableStateFlow(HomeViewState()) private val _filterUiState = MutableStateFlow(FilterState())
val viewState: StateFlow<HomeViewState> = _viewState.asStateFlow() val filterUiState = _filterUiState.asStateFlow()
private val _filterState = MutableStateFlow(FilterState())
val filterState = _filterState.asStateFlow()
val syncWorkLiveData = workManager.getWorkInfoByIdLiveData(SyncWorker.UUID) val syncWorkLiveData = workManager.getWorkInfoByIdLiveData(SyncWorker.UUID)
fun dispatch(action: HomeViewAction) { fun sync() {
when (action) {
is HomeViewAction.Sync -> sync()
is HomeViewAction.ChangeFilter -> changeFilter(action.filterState)
is HomeViewAction.FetchArticles -> fetchArticles()
is HomeViewAction.InputSearchContent -> inputSearchContent(action.content)
}
}
private fun sync() {
rssRepository.get().doSync() rssRepository.get().doSync()
} }
private fun changeFilter(filterState: FilterState) { fun changeFilter(filterState: FilterState) {
_filterState.update { _filterUiState.update {
it.copy( it.copy(
group = filterState.group, group = filterState.group,
feed = filterState.feed, feed = filterState.feed,
@ -57,28 +47,40 @@ class HomeViewModel @Inject constructor(
fetchArticles() fetchArticles()
} }
private fun fetchArticles() { fun fetchArticles() {
_viewState.update { _homeUiState.update {
it.copy( it.copy(
pagingData = Pager(PagingConfig(pageSize = 50)) { pagingData = Pager(
if (_viewState.value.searchContent.isNotBlank()) { config = PagingConfig(
pageSize = 100,
enablePlaceholders = false,
)
) {
if (_homeUiState.value.searchContent.isNotBlank()) {
rssRepository.get().searchArticles( rssRepository.get().searchArticles(
content = _viewState.value.searchContent.trim(), content = _homeUiState.value.searchContent.trim(),
groupId = _filterState.value.group?.id, groupId = _filterUiState.value.group?.id,
feedId = _filterState.value.feed?.id, feedId = _filterUiState.value.feed?.id,
isStarred = _filterState.value.filter.isStarred(), isStarred = _filterUiState.value.filter.isStarred(),
isUnread = _filterState.value.filter.isUnread(), isUnread = _filterUiState.value.filter.isUnread(),
) )
} else { } else {
rssRepository.get().pullArticles( rssRepository.get().pullArticles(
groupId = _filterState.value.group?.id, groupId = _filterUiState.value.group?.id,
feedId = _filterState.value.feed?.id, feedId = _filterUiState.value.feed?.id,
isStarred = _filterState.value.filter.isStarred(), isStarred = _filterUiState.value.filter.isStarred(),
isUnread = _filterState.value.filter.isUnread(), isUnread = _filterUiState.value.filter.isUnread(),
) )
} }
}.flow.map { }.flow.map {
it.map { FlowItemView.Article(it) }.insertSeparators { before, after -> it.map {
FlowItemView.Article(it.apply {
article.dateString = stringsRepository.formatAsString(
date = article.date,
onlyHourMinute = true
)
})
}.insertSeparators { before, after ->
val beforeDate = val beforeDate =
stringsRepository.formatAsString(before?.articleWithFeed?.article?.date) stringsRepository.formatAsString(before?.articleWithFeed?.article?.date)
val afterDate = val afterDate =
@ -94,8 +96,8 @@ class HomeViewModel @Inject constructor(
} }
} }
private fun inputSearchContent(content: String) { fun inputSearchContent(content: String) {
_viewState.update { _homeUiState.update {
it.copy( it.copy(
searchContent = content, searchContent = content,
) )
@ -110,21 +112,7 @@ data class FilterState(
val filter: Filter = Filter.All, val filter: Filter = Filter.All,
) )
data class HomeViewState( data class HomeUiState(
val pagingData: Flow<PagingData<FlowItemView>> = emptyFlow(), val pagingData: Flow<PagingData<FlowItemView>> = emptyFlow(),
val searchContent: String = "", val searchContent: String = "",
) )
sealed class HomeViewAction {
object Sync : HomeViewAction()
data class ChangeFilter(
val filterState: FilterState
) : HomeViewAction()
object FetchArticles : HomeViewAction()
data class InputSearchContent(
val content: String,
) : HomeViewAction()
}

View File

@ -1,96 +1,99 @@
package me.ash.reader.ui.page.home.feeds package me.ash.reader.ui.page.home.feeds
import RYExtensibleVisibility
import android.view.HapticFeedbackConstants import android.view.HapticFeedbackConstants
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Badge import androidx.compose.material3.Badge
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.* import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import me.ash.reader.data.entity.Feed import me.ash.reader.data.entity.Feed
import me.ash.reader.ui.page.home.FeedIcon import me.ash.reader.ui.component.FeedIcon
import me.ash.reader.ui.page.home.feeds.option.feed.FeedOptionViewAction import me.ash.reader.ui.page.home.feeds.drawer.feed.FeedOptionViewModel
import me.ash.reader.ui.page.home.feeds.option.feed.FeedOptionViewModel import me.ash.reader.ui.theme.ShapeBottom32
import kotlin.math.ln
@OptIn( @OptIn(
androidx.compose.foundation.ExperimentalFoundationApi::class, androidx.compose.foundation.ExperimentalFoundationApi::class,
androidx.compose.material.ExperimentalMaterialApi::class,
) )
@Composable @Composable
fun FeedItem( fun FeedItem(
feed: Feed, feed: Feed,
alpha: Float = 1f,
badgeAlpha: Float = 1f,
isEnded: Boolean = false,
isExpanded: () -> Boolean,
feedOptionViewModel: FeedOptionViewModel = hiltViewModel(), feedOptionViewModel: FeedOptionViewModel = hiltViewModel(),
tonalElevation: Dp,
onClick: () -> Unit = {}, onClick: () -> Unit = {},
) { ) {
val view = LocalView.current val view = LocalView.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val tonalElevationAlpha by remember {
derivedStateOf {
(ln(tonalElevation.value + 1.4f) + 2f) / 100f
}
}
Row( RYExtensibleVisibility(visible = isExpanded()) {
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 14.dp)
.clip(RoundedCornerShape(32.dp))
.combinedClickable(
onClick = {
onClick()
},
onLongClick = {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
feedOptionViewModel.dispatch(FeedOptionViewAction.Show(scope, feed.id))
}
)
.padding(vertical = 14.dp),
) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(start = 14.dp, end = 6.dp), .padding(horizontal = 16.dp)
horizontalArrangement = Arrangement.SpaceBetween, .background(
verticalAlignment = Alignment.CenterVertically, color = MaterialTheme.colorScheme.secondary.copy(alpha = alpha),
) { shape = if (isEnded) ShapeBottom32 else RectangleShape,
Row(modifier = Modifier.weight(1f)) {
FeedIcon(feed.name)
Text(
modifier = Modifier.padding(start = 12.dp, end = 6.dp),
text = feed.name,
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
) )
} .combinedClickable(
if ((feed.important ?: 0) != 0) { onClick = {
Badge( onClick()
containerColor = MaterialTheme.colorScheme.surfaceTint.copy(
alpha = tonalElevationAlpha
),
contentColor = MaterialTheme.colorScheme.outline,
content = {
Text(
text = feed.important.toString(),
style = MaterialTheme.typography.labelSmall
)
}, },
onLongClick = {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
feedOptionViewModel.showDrawer(scope, feed.id)
}
) )
.padding(horizontal = 14.dp)
.padding(top = 14.dp, bottom = if (isEnded) 22.dp else 14.dp),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(start = 14.dp, end = 6.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Row(modifier = Modifier.weight(1f)) {
FeedIcon(feed.name)
Text(
modifier = Modifier.padding(start = 12.dp, end = 6.dp),
text = feed.name,
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
if ((feed.important ?: 0) != 0) {
Badge(
containerColor = MaterialTheme.colorScheme.surfaceTint.copy(
alpha = badgeAlpha
),
contentColor = MaterialTheme.colorScheme.outline,
content = {
Text(
text = feed.important.toString(),
style = MaterialTheme.typography.labelSmall
)
},
)
}
} }
} }
} }

View File

@ -1,11 +1,9 @@
package me.ash.reader.ui.page.home.feeds package me.ash.reader.ui.page.home.feeds
import android.annotation.SuppressLint
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.core.* import androidx.compose.animation.core.*
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
@ -15,7 +13,8 @@ import androidx.compose.material.icons.outlined.KeyboardArrowRight
import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material.icons.rounded.Add import androidx.compose.material.icons.rounded.Add
import androidx.compose.material.icons.rounded.Refresh import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.material3.* import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.rotate
@ -26,31 +25,26 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import kotlinx.coroutines.flow.map
import me.ash.reader.R import me.ash.reader.R
import me.ash.reader.data.entity.toVersion import me.ash.reader.data.model.getName
import me.ash.reader.data.preference.* import me.ash.reader.data.preference.*
import me.ash.reader.data.repository.SyncWorker.Companion.getIsSyncing import me.ash.reader.data.repository.SyncWorker.Companion.getIsSyncing
import me.ash.reader.ui.component.Banner import me.ash.reader.ui.component.FilterBar
import me.ash.reader.ui.component.DisplayText import me.ash.reader.ui.component.base.*
import me.ash.reader.ui.component.FeedbackIconButton import me.ash.reader.ui.ext.alphaLN
import me.ash.reader.ui.component.Subtitle import me.ash.reader.ui.ext.collectAsStateValue
import me.ash.reader.ui.ext.* import me.ash.reader.ui.ext.findActivity
import me.ash.reader.ui.ext.getCurrentVersion
import me.ash.reader.ui.page.common.RouteName import me.ash.reader.ui.page.common.RouteName
import me.ash.reader.ui.page.home.FilterBar
import me.ash.reader.ui.page.home.FilterState import me.ash.reader.ui.page.home.FilterState
import me.ash.reader.ui.page.home.HomeViewAction
import me.ash.reader.ui.page.home.HomeViewModel import me.ash.reader.ui.page.home.HomeViewModel
import me.ash.reader.ui.page.home.feeds.option.feed.FeedOptionDrawer import me.ash.reader.ui.page.home.feeds.drawer.feed.FeedOptionDrawer
import me.ash.reader.ui.page.home.feeds.option.group.GroupOptionDrawer import me.ash.reader.ui.page.home.feeds.drawer.group.GroupOptionDrawer
import me.ash.reader.ui.page.home.feeds.subscribe.SubscribeDialog import me.ash.reader.ui.page.home.feeds.subscribe.SubscribeDialog
import me.ash.reader.ui.page.home.feeds.subscribe.SubscribeViewAction
import me.ash.reader.ui.page.home.feeds.subscribe.SubscribeViewModel import me.ash.reader.ui.page.home.feeds.subscribe.SubscribeViewModel
import me.ash.reader.ui.theme.palette.onDark import kotlin.math.ln
@SuppressLint("FlowOperatorInvokedInComposition")
@OptIn( @OptIn(
ExperimentalMaterial3Api::class, com.google.accompanist.pager.ExperimentalPagerApi::class,
androidx.compose.foundation.ExperimentalFoundationApi::class androidx.compose.foundation.ExperimentalFoundationApi::class
) )
@Composable @Composable
@ -69,20 +63,12 @@ fun FeedsPage(
val filterBarPadding = LocalFeedsFilterBarPadding.current val filterBarPadding = LocalFeedsFilterBarPadding.current
val filterBarTonalElevation = LocalFeedsFilterBarTonalElevation.current val filterBarTonalElevation = LocalFeedsFilterBarTonalElevation.current
val feedsViewState = feedsViewModel.viewState.collectAsStateValue() val feedsUiState = feedsViewModel.feedsUiState.collectAsStateValue()
val filterState = homeViewModel.filterState.collectAsStateValue() val filterUiState = homeViewModel.filterUiState.collectAsStateValue()
val skipVersion = context.dataStore.data val newVersion = LocalNewVersionNumber.current
.map { it[DataStoreKeys.SkipVersionNumber.key] ?: "" } val skipVersion = LocalSkipVersionNumber.current
.collectAsState(initial = "") val currentVersion = remember { context.getCurrentVersion() }
.value
.toVersion()
val latestVersion = context.dataStore.data
.map { it[DataStoreKeys.NewVersionNumber.key] ?: "" }
.collectAsState(initial = "")
.value
.toVersion()
val currentVersion by remember { mutableStateOf(context.getCurrentVersion()) }
val owner = LocalLifecycleOwner.current val owner = LocalLifecycleOwner.current
var isSyncing by remember { mutableStateOf(false) } var isSyncing by remember { mutableStateOf(false) }
@ -102,22 +88,40 @@ fun FeedsPage(
val launcher = rememberLauncherForActivityResult( val launcher = rememberLauncherForActivityResult(
ActivityResultContracts.CreateDocument() ActivityResultContracts.CreateDocument()
) { result -> ) { result ->
feedsViewModel.dispatch(FeedsViewAction.ExportAsString { string -> feedsViewModel.exportAsOpml { string ->
result?.let { uri -> result?.let { uri ->
context.contentResolver.openOutputStream(uri)?.let { outputStream -> context.contentResolver.openOutputStream(uri)?.use { outputStream ->
outputStream.write(string.toByteArray()) outputStream.write(string.toByteArray())
} }
} }
}) }
}
val feedBadgeAlpha by remember { derivedStateOf { (ln(groupListTonalElevation.value + 1.4f) + 2f) / 100f } }
val groupAlpha by remember { derivedStateOf { groupListTonalElevation.value.dp.alphaLN(weight = 1.2f) } }
val groupIndicatorAlpha by remember {
derivedStateOf {
groupListTonalElevation.value.dp.alphaLN(
weight = 1.4f
)
}
}
val groupsVisible = remember(feedsUiState.groupWithFeedList) {
mutableStateMapOf(
*(feedsUiState.groupWithFeedList.filterIsInstance<GroupFeedsView.Group>().map {
it.group.id to groupListExpand.value
}.toTypedArray())
)
} }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
feedsViewModel.dispatch(FeedsViewAction.FetchAccount) feedsViewModel.fetchAccount()
} }
LaunchedEffect(filterState) { LaunchedEffect(filterUiState) {
snapshotFlow { filterState }.collect { snapshotFlow { filterUiState }.collect {
feedsViewModel.dispatch(FeedsViewAction.FetchData(it)) feedsViewModel.fetchData(it)
} }
} }
@ -125,52 +129,38 @@ fun FeedsPage(
context.findActivity()?.moveTaskToBack(false) context.findActivity()?.moveTaskToBack(false)
} }
Scaffold( RYScaffold(
modifier = Modifier topBarTonalElevation = topBarTonalElevation.value.dp,
.background(MaterialTheme.colorScheme.surfaceColorAtElevation(topBarTonalElevation.value.dp)) containerTonalElevation = groupListTonalElevation.value.dp,
.statusBarsPadding(), navigationIcon = {
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation( FeedbackIconButton(
groupListTonalElevation.value.dp modifier = Modifier.size(20.dp),
) onDark MaterialTheme.colorScheme.surface, imageVector = Icons.Outlined.Settings,
topBar = { contentDescription = stringResource(R.string.settings),
SmallTopAppBar( tint = MaterialTheme.colorScheme.onSurface,
colors = TopAppBarDefaults.smallTopAppBarColors( showBadge = newVersion.whetherNeedUpdate(currentVersion, skipVersion),
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation( ) {
topBarTonalElevation.value.dp navController.navigate(RouteName.SETTINGS) {
), launchSingleTop = true
),
title = {},
navigationIcon = {
FeedbackIconButton(
modifier = Modifier.size(20.dp),
imageVector = Icons.Outlined.Settings,
contentDescription = stringResource(R.string.settings),
tint = MaterialTheme.colorScheme.onSurface,
showBadge = latestVersion.whetherNeedUpdate(currentVersion, skipVersion),
) {
navController.navigate(RouteName.SETTINGS) {
launchSingleTop = true
}
}
},
actions = {
FeedbackIconButton(
modifier = Modifier.rotate(if (isSyncing) angle else 0f),
imageVector = Icons.Rounded.Refresh,
contentDescription = stringResource(R.string.refresh),
tint = MaterialTheme.colorScheme.onSurface,
) {
if (!isSyncing) homeViewModel.dispatch(HomeViewAction.Sync)
}
FeedbackIconButton(
imageVector = Icons.Rounded.Add,
contentDescription = stringResource(R.string.subscribe),
tint = MaterialTheme.colorScheme.onSurface,
) {
subscribeViewModel.dispatch(SubscribeViewAction.Show)
}
} }
) }
},
actions = {
FeedbackIconButton(
modifier = Modifier.rotate(if (isSyncing) angle else 0f),
imageVector = Icons.Rounded.Refresh,
contentDescription = stringResource(R.string.refresh),
tint = MaterialTheme.colorScheme.onSurface,
) {
if (!isSyncing) homeViewModel.sync()
}
FeedbackIconButton(
imageVector = Icons.Rounded.Add,
contentDescription = stringResource(R.string.subscribe),
tint = MaterialTheme.colorScheme.onSurface,
) {
subscribeViewModel.showDrawer()
}
}, },
content = { content = {
LazyColumn { LazyColumn {
@ -183,15 +173,15 @@ fun FeedsPage(
} }
) )
}, },
text = feedsViewState.account?.name ?: stringResource(R.string.read_you), text = feedsUiState.account?.name ?: stringResource(R.string.read_you),
desc = if (isSyncing) stringResource(R.string.syncing) else "", desc = if (isSyncing) stringResource(R.string.syncing) else "",
) )
} }
item { item {
Banner( Banner(
title = filterState.filter.getName(), title = filterUiState.filter.getName(),
desc = feedsViewState.importantCount.ifEmpty { stringResource(R.string.loading) }, desc = feedsUiState.importantSum.ifEmpty { stringResource(R.string.loading) },
icon = filterState.filter.iconOutline, icon = filterUiState.filter.iconOutline,
action = { action = {
Icon( Icon(
imageVector = Icons.Outlined.KeyboardArrowRight, imageVector = Icons.Outlined.KeyboardArrowRight,
@ -202,7 +192,7 @@ fun FeedsPage(
filterChange( filterChange(
navController = navController, navController = navController,
homeViewModel = homeViewModel, homeViewModel = homeViewModel,
filterState = filterState.copy( filterState = filterUiState.copy(
group = null, group = null,
feed = null, feed = null,
) )
@ -217,40 +207,51 @@ fun FeedsPage(
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
} }
itemsIndexed(feedsViewState.groupWithFeedList) { index, groupWithFeed -> itemsIndexed(feedsUiState.groupWithFeedList) { index, groupWithFeed ->
// Crossfade(targetState = groupWithFeed) { groupWithFeed -> when (groupWithFeed) {
Column { is GroupFeedsView.Group -> {
GroupItem( if (index != 0) {
isExpanded = groupListExpand.value, Spacer(modifier = Modifier.height(16.dp))
tonalElevation = groupListTonalElevation.value.dp, }
group = groupWithFeed.group, GroupItem(
feeds = groupWithFeed.feeds, isExpanded = { groupsVisible[groupWithFeed.group.id] ?: false },
groupOnClick = { group = groupWithFeed.group,
alpha = groupAlpha,
indicatorAlpha = groupIndicatorAlpha,
onExpanded = {
groupsVisible[groupWithFeed.group.id] =
!(groupsVisible[groupWithFeed.group.id] ?: false)
}
) {
filterChange( filterChange(
navController = navController, navController = navController,
homeViewModel = homeViewModel, homeViewModel = homeViewModel,
filterState = filterState.copy( filterState = filterUiState.copy(
group = groupWithFeed.group, group = groupWithFeed.group,
feed = null, feed = null,
) )
) )
}, }
feedOnClick = { feed -> }
is GroupFeedsView.Feed -> {
FeedItem(
feed = groupWithFeed.feed,
alpha = groupAlpha,
badgeAlpha = feedBadgeAlpha,
isEnded = index != feedsUiState.groupWithFeedList.lastIndex && feedsUiState.groupWithFeedList[index + 1] is GroupFeedsView.Group,
isExpanded = { groupsVisible[groupWithFeed.feed.groupId] ?: false },
) {
filterChange( filterChange(
navController = navController, navController = navController,
homeViewModel = homeViewModel, homeViewModel = homeViewModel,
filterState = filterState.copy( filterState = filterUiState.copy(
group = null, group = null,
feed = feed, feed = groupWithFeed.feed,
) )
) )
} }
)
if (index != feedsViewState.groupWithFeedList.lastIndex) {
Spacer(modifier = Modifier.height(8.dp))
} }
} }
// }
} }
item { item {
Spacer(modifier = Modifier.height(128.dp)) Spacer(modifier = Modifier.height(128.dp))
@ -260,7 +261,7 @@ fun FeedsPage(
}, },
bottomBar = { bottomBar = {
FilterBar( FilterBar(
filter = filterState.filter, filter = filterUiState.filter,
filterBarStyle = filterBarStyle.value, filterBarStyle = filterBarStyle.value,
filterBarFilled = filterBarFilled.value, filterBarFilled = filterBarFilled.value,
filterBarPadding = filterBarPadding.dp, filterBarPadding = filterBarPadding.dp,
@ -269,7 +270,7 @@ fun FeedsPage(
filterChange( filterChange(
navController = navController, navController = navController,
homeViewModel = homeViewModel, homeViewModel = homeViewModel,
filterState = filterState.copy(filter = it), filterState = filterUiState.copy(filter = it),
isNavigate = false, isNavigate = false,
) )
} }
@ -287,7 +288,7 @@ private fun filterChange(
filterState: FilterState, filterState: FilterState,
isNavigate: Boolean = true, isNavigate: Boolean = true,
) { ) {
homeViewModel.dispatch(HomeViewAction.ChangeFilter(filterState)) homeViewModel.changeFilter(filterState)
if (isNavigate) { if (isNavigate) {
navController.navigate(RouteName.FLOW) { navController.navigate(RouteName.FLOW) {
launchSingleTop = true launchSingleTop = true

View File

@ -2,15 +2,17 @@ package me.ash.reader.ui.page.home.feeds
import android.util.Log import android.util.Log
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.ui.util.fastForEach
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.ash.reader.R import me.ash.reader.R
import me.ash.reader.data.entity.Account import me.ash.reader.data.entity.Account
import me.ash.reader.data.entity.GroupWithFeed import me.ash.reader.data.module.DispatcherDefault
import me.ash.reader.data.module.DispatcherIO
import me.ash.reader.data.repository.AccountRepository import me.ash.reader.data.repository.AccountRepository
import me.ash.reader.data.repository.OpmlRepository import me.ash.reader.data.repository.OpmlRepository
import me.ash.reader.data.repository.RssRepository import me.ash.reader.data.repository.RssRepository
@ -24,22 +26,17 @@ class FeedsViewModel @Inject constructor(
private val rssRepository: RssRepository, private val rssRepository: RssRepository,
private val opmlRepository: OpmlRepository, private val opmlRepository: OpmlRepository,
private val stringsRepository: StringsRepository, private val stringsRepository: StringsRepository,
@DispatcherDefault
private val dispatcherDefault: CoroutineDispatcher,
@DispatcherIO
private val dispatcherIO: CoroutineDispatcher,
) : ViewModel() { ) : ViewModel() {
private val _viewState = MutableStateFlow(FeedsViewState()) private val _feedsUiState = MutableStateFlow(FeedsUiState())
val viewState: StateFlow<FeedsViewState> = _viewState.asStateFlow() val feedsUiState: StateFlow<FeedsUiState> = _feedsUiState.asStateFlow()
fun dispatch(action: FeedsViewAction) { fun fetchAccount() {
when (action) { viewModelScope.launch(dispatcherIO) {
is FeedsViewAction.FetchAccount -> fetchAccount() _feedsUiState.update {
is FeedsViewAction.FetchData -> fetchData(action.filterState)
is FeedsViewAction.ExportAsString -> exportAsOpml(action.callback)
is FeedsViewAction.ScrollToItem -> scrollToItem(action.index)
}
}
private fun fetchAccount() {
viewModelScope.launch(Dispatchers.IO) {
_viewState.update {
it.copy( it.copy(
account = accountRepository.getCurrentAccount() account = accountRepository.getCurrentAccount()
) )
@ -47,8 +44,8 @@ class FeedsViewModel @Inject constructor(
} }
} }
private fun exportAsOpml(callback: (String) -> Unit = {}) { fun exportAsOpml(callback: (String) -> Unit = {}) {
viewModelScope.launch(Dispatchers.Default) { viewModelScope.launch(dispatcherDefault) {
try { try {
callback(opmlRepository.saveToString()) callback(opmlRepository.saveToString())
} catch (e: Exception) { } catch (e: Exception) {
@ -57,8 +54,8 @@ class FeedsViewModel @Inject constructor(
} }
} }
private fun fetchData(filterState: FilterState) { fun fetchData(filterState: FilterState) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(dispatcherIO) {
pullFeeds( pullFeeds(
isStarred = filterState.filter.isStarred(), isStarred = filterState.filter.isStarred(),
isUnread = filterState.filter.isUnread(), isUnread = filterState.filter.isUnread(),
@ -70,85 +67,64 @@ class FeedsViewModel @Inject constructor(
combine( combine(
rssRepository.get().pullFeeds(), rssRepository.get().pullFeeds(),
rssRepository.get().pullImportant(isStarred, isUnread), rssRepository.get().pullImportant(isStarred, isUnread),
) { groupWithFeedList, importantList -> ) { groupWithFeedList, importantMap ->
val groupImportantMap = mutableMapOf<String, Int>() groupWithFeedList.fastForEach {
val feedImportantMap = mutableMapOf<String, Int>() var groupImportant = 0
importantList.groupBy { it.groupId }.forEach { (i, list) -> it.feeds.fastForEach {
var groupImportantSum = 0 it.important = importantMap[it.id]
list.forEach { groupImportant += it.important ?: 0
feedImportantMap[it.feedId] = it.important
groupImportantSum += it.important
}
groupImportantMap[i] = groupImportantSum
}
val groupsIt = groupWithFeedList.iterator()
while (groupsIt.hasNext()) {
val groupWithFeed = groupsIt.next()
val groupImportant = groupImportantMap[groupWithFeed.group.id]
if (groupImportant == null && (isStarred || isUnread)) {
groupsIt.remove()
} else {
groupWithFeed.group.important = groupImportant
val feedsIt = groupWithFeed.feeds.iterator()
while (feedsIt.hasNext()) {
val feed = feedsIt.next()
val feedImportant = feedImportantMap[feed.id]
if (feedImportant == null && (isStarred || isUnread)) {
feedsIt.remove()
} else {
feed.important = feedImportant
}
}
} }
it.group.important = groupImportant
} }
groupWithFeedList groupWithFeedList
}.onEach { groupWithFeedList -> }.mapLatest { groupWithFeedList ->
_viewState.update { _feedsUiState.update {
it.copy( it.copy(
importantCount = groupWithFeedList.sumOf { it.group.important ?: 0 }.run { importantSum = groupWithFeedList.sumOf { it.group.important ?: 0 }.run {
when { when {
isStarred -> stringsRepository.getQuantityString(R.plurals.starred_desc, this, this) isStarred -> stringsRepository.getQuantityString(
isUnread -> stringsRepository.getQuantityString(R.plurals.unread_desc, this, this) R.plurals.starred_desc,
else -> stringsRepository.getQuantityString(R.plurals.all_desc, this, this) this,
this
)
isUnread -> stringsRepository.getQuantityString(
R.plurals.unread_desc,
this,
this
)
else -> stringsRepository.getQuantityString(
R.plurals.all_desc,
this,
this
)
} }
}, },
groupWithFeedList = groupWithFeedList, groupWithFeedList = groupWithFeedList.map {
feedsVisible = List(groupWithFeedList.size, init = { true }) mutableListOf<GroupFeedsView>(GroupFeedsView.Group(it.group)).apply {
addAll(
it.feeds.map {
GroupFeedsView.Feed(it)
}
)
}
}.flatten(),
) )
} }
}.catch { }.catch {
Log.e("RLog", "catch in articleRepository.pullFeeds(): ${it.message}") Log.e("RLog", "catch in articleRepository.pullFeeds(): ${it.message}")
}.flowOn(Dispatchers.Default).collect() }.flowOn(dispatcherDefault).collect()
}
private fun scrollToItem(index: Int) {
viewModelScope.launch {
_viewState.value.listState.scrollToItem(index)
}
} }
} }
data class FeedsViewState( data class FeedsUiState(
val account: Account? = null, val account: Account? = null,
val importantCount: String = "", val importantSum: String = "",
val groupWithFeedList: List<GroupWithFeed> = emptyList(), val groupWithFeedList: List<GroupFeedsView> = emptyList(),
val feedsVisible: List<Boolean> = emptyList(),
val listState: LazyListState = LazyListState(), val listState: LazyListState = LazyListState(),
val groupsVisible: Boolean = true, val groupsVisible: Boolean = true,
) )
sealed class FeedsViewAction { sealed class GroupFeedsView {
data class FetchData( class Group(val group: me.ash.reader.data.entity.Group) : GroupFeedsView()
val filterState: FilterState, class Feed(val feed: me.ash.reader.data.entity.Feed) : GroupFeedsView()
) : FeedsViewAction()
object FetchAccount : FeedsViewAction()
data class ExportAsString(
val callback: (String) -> Unit = {}
) : FeedsViewAction()
data class ScrollToItem(
val index: Int
) : FeedsViewAction()
} }

View File

@ -1,59 +1,56 @@
package me.ash.reader.ui.page.home.feeds package me.ash.reader.ui.page.home.feeds
import android.view.HapticFeedbackConstants import android.view.HapticFeedbackConstants
import androidx.compose.animation.* import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.ExpandLess import androidx.compose.material.icons.rounded.ExpandLess
import androidx.compose.material.icons.rounded.ExpandMore import androidx.compose.material.icons.rounded.ExpandMore
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.* import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import me.ash.reader.R import me.ash.reader.R
import me.ash.reader.data.entity.Feed
import me.ash.reader.data.entity.Group import me.ash.reader.data.entity.Group
import me.ash.reader.ui.ext.alphaLN import me.ash.reader.ui.page.home.feeds.drawer.group.GroupOptionViewModel
import me.ash.reader.ui.page.home.feeds.option.group.GroupOptionViewAction import me.ash.reader.ui.theme.Shape32
import me.ash.reader.ui.page.home.feeds.option.group.GroupOptionViewModel import me.ash.reader.ui.theme.ShapeTop32
@OptIn(androidx.compose.foundation.ExperimentalFoundationApi::class) @OptIn(androidx.compose.foundation.ExperimentalFoundationApi::class)
@Composable @Composable
fun GroupItem( fun GroupItem(
modifier: Modifier = Modifier,
tonalElevation: Dp,
group: Group, group: Group,
feeds: List<Feed>, alpha: Float = 1f,
isExpanded: Boolean = true, indicatorAlpha: Float = 1f,
isExpanded: () -> Boolean,
groupOptionViewModel: GroupOptionViewModel = hiltViewModel(), groupOptionViewModel: GroupOptionViewModel = hiltViewModel(),
onExpanded: () -> Unit = {},
groupOnClick: () -> Unit = {}, groupOnClick: () -> Unit = {},
feedOnClick: (feed: Feed) -> Unit = {},
) { ) {
val view = LocalView.current val view = LocalView.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
var expanded by remember { mutableStateOf(isExpanded) }
Column( Column(
modifier = Modifier modifier = Modifier
.animateContentSize()
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
.clip(RoundedCornerShape(32.dp)) .clip(if (isExpanded()) ShapeTop32 else Shape32)
.background( .background(
MaterialTheme.colorScheme.secondary.copy(alpha = tonalElevation.alphaLN(weight = 1.2f)) MaterialTheme.colorScheme.secondary.copy(alpha = alpha)
) )
.combinedClickable( .combinedClickable(
onClick = { onClick = {
@ -61,13 +58,13 @@ fun GroupItem(
}, },
onLongClick = { onLongClick = {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
groupOptionViewModel.dispatch(GroupOptionViewAction.Show(scope, group.id)) groupOptionViewModel.showDrawer(scope, group.id)
} }
) )
.padding(top = 22.dp) .padding(top = 22.dp)
) { ) {
Row( Row(
modifier = modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
@ -87,42 +84,21 @@ fun GroupItem(
.size(24.dp) .size(24.dp)
.clip(CircleShape) .clip(CircleShape)
.background( .background(
MaterialTheme.colorScheme.surfaceTint.copy( MaterialTheme.colorScheme.surfaceTint.copy(alpha = indicatorAlpha)
alpha = tonalElevation.alphaLN(weight = 1.4f)
)
) )
.clickable { .clickable {
expanded = !expanded onExpanded()
}, },
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Icon( Icon(
imageVector = if (expanded) Icons.Rounded.ExpandLess else Icons.Rounded.ExpandMore, imageVector = if (isExpanded()) Icons.Rounded.ExpandLess else Icons.Rounded.ExpandMore,
contentDescription = stringResource(if (expanded) R.string.expand_less else R.string.expand_more), contentDescription = stringResource(if (isExpanded()) R.string.expand_less else R.string.expand_more),
tint = MaterialTheme.colorScheme.onSecondaryContainer, tint = MaterialTheme.colorScheme.onSecondaryContainer,
) )
} }
} }
Spacer(modifier = Modifier.height(22.dp)) Spacer(modifier = Modifier.height(22.dp))
AnimatedVisibility(
visible = expanded,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(),
) {
Column {
feeds.forEach { feed ->
FeedItem(
feed = feed,
tonalElevation = tonalElevation,
) {
feedOnClick(feed)
}
}
if (feeds.isNotEmpty()) {
Spacer(modifier = Modifier.height(16.dp))
}
}
}
} }
} }

View File

@ -1,4 +1,4 @@
package me.ash.reader.ui.page.home.feeds.option.feed package me.ash.reader.ui.page.home.feeds.drawer.feed
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.DeleteForever import androidx.compose.material.icons.outlined.DeleteForever
@ -7,32 +7,28 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import com.google.accompanist.pager.ExperimentalPagerApi
import me.ash.reader.R import me.ash.reader.R
import me.ash.reader.ui.component.Dialog import me.ash.reader.ui.component.base.RYDialog
import me.ash.reader.ui.ext.collectAsStateValue import me.ash.reader.ui.ext.collectAsStateValue
import me.ash.reader.ui.ext.showToast import me.ash.reader.ui.ext.showToast
@OptIn(ExperimentalPagerApi::class)
@Composable @Composable
fun ClearFeedDialog( fun ClearFeedDialog(
modifier: Modifier = Modifier,
feedName: String, feedName: String,
viewModel: FeedOptionViewModel = hiltViewModel(), feedOptionViewModel: FeedOptionViewModel = hiltViewModel(),
) { ) {
val context = LocalContext.current val context = LocalContext.current
val viewState = viewModel.viewState.collectAsStateValue() val feedOptionUiState = feedOptionViewModel.feedOptionUiState.collectAsStateValue()
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val toastString = stringResource(R.string.clear_articles_in_feed_toast, feedName) val toastString = stringResource(R.string.clear_articles_in_feed_toast, feedName)
Dialog( RYDialog(
visible = viewState.clearDialogVisible, visible = feedOptionUiState.clearDialogVisible,
onDismissRequest = { onDismissRequest = {
viewModel.dispatch(FeedOptionViewAction.HideClearDialog) feedOptionViewModel.hideClearDialog()
}, },
icon = { icon = {
Icon( Icon(
@ -49,11 +45,11 @@ fun ClearFeedDialog(
confirmButton = { confirmButton = {
TextButton( TextButton(
onClick = { onClick = {
viewModel.dispatch(FeedOptionViewAction.Clear { feedOptionViewModel.clearFeed {
viewModel.dispatch(FeedOptionViewAction.HideClearDialog) feedOptionViewModel.hideClearDialog()
viewModel.dispatch(FeedOptionViewAction.Hide(scope)) feedOptionViewModel.hideDrawer(scope)
context.showToast(toastString) context.showToast(toastString)
}) }
} }
) { ) {
Text( Text(
@ -64,7 +60,7 @@ fun ClearFeedDialog(
dismissButton = { dismissButton = {
TextButton( TextButton(
onClick = { onClick = {
viewModel.dispatch(FeedOptionViewAction.HideClearDialog) feedOptionViewModel.hideClearDialog()
} }
) { ) {
Text( Text(

View File

@ -1,4 +1,4 @@
package me.ash.reader.ui.page.home.feeds.option.feed package me.ash.reader.ui.page.home.feeds.drawer.feed
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.DeleteForever import androidx.compose.material.icons.outlined.DeleteForever
@ -7,32 +7,28 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import com.google.accompanist.pager.ExperimentalPagerApi
import me.ash.reader.R import me.ash.reader.R
import me.ash.reader.ui.component.Dialog import me.ash.reader.ui.component.base.RYDialog
import me.ash.reader.ui.ext.collectAsStateValue import me.ash.reader.ui.ext.collectAsStateValue
import me.ash.reader.ui.ext.showToast import me.ash.reader.ui.ext.showToast
@OptIn(ExperimentalPagerApi::class)
@Composable @Composable
fun DeleteFeedDialog( fun DeleteFeedDialog(
modifier: Modifier = Modifier,
feedName: String, feedName: String,
viewModel: FeedOptionViewModel = hiltViewModel(), feedOptionViewModel: FeedOptionViewModel = hiltViewModel(),
) { ) {
val context = LocalContext.current val context = LocalContext.current
val viewState = viewModel.viewState.collectAsStateValue() val feedOptionUiState = feedOptionViewModel.feedOptionUiState.collectAsStateValue()
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val toastString = stringResource(R.string.delete_toast, feedName) val toastString = stringResource(R.string.delete_toast, feedName)
Dialog( RYDialog(
visible = viewState.deleteDialogVisible, visible = feedOptionUiState.deleteDialogVisible,
onDismissRequest = { onDismissRequest = {
viewModel.dispatch(FeedOptionViewAction.HideDeleteDialog) feedOptionViewModel.hideDeleteDialog()
}, },
icon = { icon = {
Icon( Icon(
@ -49,11 +45,11 @@ fun DeleteFeedDialog(
confirmButton = { confirmButton = {
TextButton( TextButton(
onClick = { onClick = {
viewModel.dispatch(FeedOptionViewAction.Delete { feedOptionViewModel.delete {
viewModel.dispatch(FeedOptionViewAction.HideDeleteDialog) feedOptionViewModel.hideDeleteDialog()
viewModel.dispatch(FeedOptionViewAction.Hide(scope)) feedOptionViewModel.hideDrawer(scope)
context.showToast(toastString) context.showToast(toastString)
}) }
} }
) { ) {
Text( Text(
@ -64,7 +60,7 @@ fun DeleteFeedDialog(
dismissButton = { dismissButton = {
TextButton( TextButton(
onClick = { onClick = {
viewModel.dispatch(FeedOptionViewAction.HideDeleteDialog) feedOptionViewModel.hideDeleteDialog()
} }
) { ) {
Text( Text(

Some files were not shown because too many files have changed in this diff Show More