diff --git a/app/build.gradle b/app/build.gradle index 16cbc01..fbd5484 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -116,7 +116,7 @@ dependencies { implementation "org.conscrypt:conscrypt-android:2.5.2" // https://square.github.io/okhttp/changelogs/changelog/ - implementation "com.squareup.okhttp3:okhttp:5.0.0-alpha.6" + implementation "com.squareup.okhttp3:okhttp:$okhttp" implementation "com.squareup.retrofit2:retrofit:$retrofit2" implementation "com.squareup.retrofit2:converter-gson:$retrofit2" @@ -166,9 +166,9 @@ dependencies { // https://developer.android.com/jetpack/androidx/releases/compose-material implementation "androidx.compose.material:material:$compose" implementation "androidx.compose.material:material-icons-extended:$compose" + debugImplementation "androidx.compose.ui:ui-tooling:$compose" implementation "androidx.compose.ui:ui-tooling-preview:$compose" androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose" - debugImplementation "androidx.compose.ui:ui-tooling:$compose" // hilt implementation "androidx.hilt:hilt-work:1.0.0" diff --git a/app/schemas/me.ash.reader.data.source.RYDatabase/2.json b/app/schemas/me.ash.reader.data.source.RYDatabase/2.json new file mode 100644 index 0000000..5690ca5 --- /dev/null +++ b/app/schemas/me.ash.reader.data.source.RYDatabase/2.json @@ -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')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d275ec0..9976f51 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -5,11 +5,8 @@ - - - + var articles: List
) diff --git a/app/src/main/java/me/ash/reader/data/entity/FeedWithGroup.kt b/app/src/main/java/me/ash/reader/data/entity/FeedWithGroup.kt index 01d4087..bf254b7 100644 --- a/app/src/main/java/me/ash/reader/data/entity/FeedWithGroup.kt +++ b/app/src/main/java/me/ash/reader/data/entity/FeedWithGroup.kt @@ -5,7 +5,7 @@ import androidx.room.Relation data class FeedWithGroup( @Embedded - val feed: Feed, + var feed: Feed, @Relation(parentColumn = "groupId", entityColumn = "id") - val group: Group + var group: Group ) diff --git a/app/src/main/java/me/ash/reader/data/entity/Group.kt b/app/src/main/java/me/ash/reader/data/entity/Group.kt index 5aa7962..4929d4b 100644 --- a/app/src/main/java/me/ash/reader/data/entity/Group.kt +++ b/app/src/main/java/me/ash/reader/data/entity/Group.kt @@ -8,11 +8,11 @@ import androidx.room.PrimaryKey @Entity(tableName = "group") data class Group( @PrimaryKey - val id: String, + var id: String, @ColumnInfo - val name: String, + var name: String, @ColumnInfo(index = true) - val accountId: Int, + var accountId: Int, ) { @Ignore var important: Int? = 0 diff --git a/app/src/main/java/me/ash/reader/data/entity/GroupWithFeed.kt b/app/src/main/java/me/ash/reader/data/entity/GroupWithFeed.kt index 28f2c47..f15efdb 100644 --- a/app/src/main/java/me/ash/reader/data/entity/GroupWithFeed.kt +++ b/app/src/main/java/me/ash/reader/data/entity/GroupWithFeed.kt @@ -5,7 +5,7 @@ import androidx.room.Relation data class GroupWithFeed( @Embedded - val group: Group, + var group: Group, @Relation(parentColumn = "id", entityColumn = "groupId") - val feeds: MutableList + var feeds: MutableList ) diff --git a/app/src/main/java/me/ash/reader/data/entity/LatestRelease.kt b/app/src/main/java/me/ash/reader/data/entity/LatestRelease.kt deleted file mode 100644 index 000e7c9..0000000 --- a/app/src/main/java/me/ash/reader/data/entity/LatestRelease.kt +++ /dev/null @@ -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? = 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, -) \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/data/entity/Filter.kt b/app/src/main/java/me/ash/reader/data/model/Filter.kt similarity index 74% rename from app/src/main/java/me/ash/reader/data/entity/Filter.kt rename to app/src/main/java/me/ash/reader/data/model/Filter.kt index 738ebca..4d1ada5 100644 --- a/app/src/main/java/me/ash/reader/data/entity/Filter.kt +++ b/app/src/main/java/me/ash/reader/data/model/Filter.kt @@ -1,4 +1,4 @@ -package me.ash.reader.data.entity +package me.ash.reader.data.model import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.FiberManualRecord @@ -6,7 +6,10 @@ import androidx.compose.material.icons.rounded.FiberManualRecord import androidx.compose.material.icons.rounded.Star import androidx.compose.material.icons.rounded.StarOutline import androidx.compose.material.icons.rounded.Subject +import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import me.ash.reader.R class Filter( val index: Int, @@ -33,5 +36,13 @@ class Filter( iconOutline = Icons.Rounded.Subject, iconFilled = Icons.Rounded.Subject, ) + val values = listOf(Starred, Unread, All) } +} + +@Composable +fun Filter.getName(): String = when (this) { + Filter.Unread -> stringResource(R.string.unread) + Filter.Starred -> stringResource(R.string.starred) + else -> stringResource(R.string.all) } \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/data/entity/ImportantCount.kt b/app/src/main/java/me/ash/reader/data/model/ImportantCount.kt similarity index 75% rename from app/src/main/java/me/ash/reader/data/entity/ImportantCount.kt rename to app/src/main/java/me/ash/reader/data/model/ImportantCount.kt index c876bb6..5e6ffc0 100644 --- a/app/src/main/java/me/ash/reader/data/entity/ImportantCount.kt +++ b/app/src/main/java/me/ash/reader/data/model/ImportantCount.kt @@ -1,4 +1,4 @@ -package me.ash.reader.data.entity +package me.ash.reader.data.model data class ImportantCount( val important: Int, diff --git a/app/src/main/java/me/ash/reader/data/model/LatestRelease.kt b/app/src/main/java/me/ash/reader/data/model/LatestRelease.kt new file mode 100644 index 0000000..ff18b3c --- /dev/null +++ b/app/src/main/java/me/ash/reader/data/model/LatestRelease.kt @@ -0,0 +1,2 @@ +package me.ash.reader.data.model + diff --git a/app/src/main/java/me/ash/reader/data/entity/Version.kt b/app/src/main/java/me/ash/reader/data/model/Version.kt similarity index 96% rename from app/src/main/java/me/ash/reader/data/entity/Version.kt rename to app/src/main/java/me/ash/reader/data/model/Version.kt index 1d8243f..50ef322 100644 --- a/app/src/main/java/me/ash/reader/data/entity/Version.kt +++ b/app/src/main/java/me/ash/reader/data/model/Version.kt @@ -1,4 +1,4 @@ -package me.ash.reader.data.entity +package me.ash.reader.data.model class Version(identifiers: List) { private var major: Int = 0 diff --git a/app/src/main/java/me/ash/reader/data/module/DatabaseModule.kt b/app/src/main/java/me/ash/reader/data/module/DatabaseModule.kt index 6da4d8b..7f215b9 100644 --- a/app/src/main/java/me/ash/reader/data/module/DatabaseModule.kt +++ b/app/src/main/java/me/ash/reader/data/module/DatabaseModule.kt @@ -10,7 +10,7 @@ import me.ash.reader.data.dao.AccountDao import me.ash.reader.data.dao.ArticleDao import me.ash.reader.data.dao.FeedDao import me.ash.reader.data.dao.GroupDao -import me.ash.reader.data.source.ReaderDatabase +import me.ash.reader.data.source.RYDatabase import javax.inject.Singleton @Module @@ -19,26 +19,26 @@ object DatabaseModule { @Provides @Singleton - fun provideArticleDao(readerDatabase: ReaderDatabase): ArticleDao = - readerDatabase.articleDao() + fun provideArticleDao(RYDatabase: RYDatabase): ArticleDao = + RYDatabase.articleDao() @Provides @Singleton - fun provideFeedDao(readerDatabase: ReaderDatabase): FeedDao = - readerDatabase.feedDao() + fun provideFeedDao(RYDatabase: RYDatabase): FeedDao = + RYDatabase.feedDao() @Provides @Singleton - fun provideGroupDao(readerDatabase: ReaderDatabase): GroupDao = - readerDatabase.groupDao() + fun provideGroupDao(RYDatabase: RYDatabase): GroupDao = + RYDatabase.groupDao() @Provides @Singleton - fun provideAccountDao(readerDatabase: ReaderDatabase): AccountDao = - readerDatabase.accountDao() + fun provideAccountDao(RYDatabase: RYDatabase): AccountDao = + RYDatabase.accountDao() @Provides @Singleton - fun provideReaderDatabase(@ApplicationContext context: Context): ReaderDatabase = - ReaderDatabase.getInstance(context) + fun provideReaderDatabase(@ApplicationContext context: Context): RYDatabase = + RYDatabase.getInstance(context) } \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/data/module/ImageLoaderModule.kt b/app/src/main/java/me/ash/reader/data/module/ImageLoaderModule.kt index bd79879..795c2f1 100644 --- a/app/src/main/java/me/ash/reader/data/module/ImageLoaderModule.kt +++ b/app/src/main/java/me/ash/reader/data/module/ImageLoaderModule.kt @@ -15,7 +15,7 @@ import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import kotlinx.coroutines.Dispatchers -import me.ash.reader.cachingHttpClient +import okhttp3.OkHttpClient import javax.inject.Singleton @Module @@ -25,16 +25,11 @@ object ImageLoaderModule { @Provides @Singleton fun provideImageLoader( - @ApplicationContext context: Context + @ApplicationContext context: Context, + okHttpClient: OkHttpClient, ): ImageLoader { return ImageLoader.Builder(context) - .okHttpClient( - okHttpClient = cachingHttpClient( - cacheDirectory = context.cacheDir.resolve("http") - ).newBuilder() - //.addNetworkInterceptor(UserAgentInterceptor) - .build() - ) + .okHttpClient(okHttpClient) .dispatcher(Dispatchers.Default) // This slightly improves scrolling performance .components{ add(SvgDecoder.Factory()) @@ -59,4 +54,4 @@ object ImageLoaderModule { ) .build() } -} \ No newline at end of file +} diff --git a/app/src/main/java/me/ash/reader/data/module/OkHttpClientModule.kt b/app/src/main/java/me/ash/reader/data/module/OkHttpClientModule.kt new file mode 100644 index 0000000..279248b --- /dev/null +++ b/app/src/main/java/me/ash/reader/data/module/OkHttpClientModule.kt @@ -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 . + */ + +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?, authType: String?) { + } + + override fun checkServerTrusted(chain: Array?, authType: String?) { + } + + override fun getAcceptedIssuers(): Array = emptyArray() + } + + val sslContext = SSLContext.getInstance("TLS") + sslContext.init(null, arrayOf(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})" \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/data/module/RetrofitModule.kt b/app/src/main/java/me/ash/reader/data/module/RetrofitModule.kt index 2423b02..62ac758 100644 --- a/app/src/main/java/me/ash/reader/data/module/RetrofitModule.kt +++ b/app/src/main/java/me/ash/reader/data/module/RetrofitModule.kt @@ -4,7 +4,7 @@ import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import me.ash.reader.data.source.AppNetworkDataSource +import me.ash.reader.data.source.RYNetworkDataSource import me.ash.reader.data.source.FeverApiDataSource import me.ash.reader.data.source.GoogleReaderApiDataSource import javax.inject.Singleton @@ -15,8 +15,8 @@ object RetrofitModule { @Provides @Singleton - fun provideAppNetworkDataSource(): AppNetworkDataSource = - AppNetworkDataSource.getInstance() + fun provideAppNetworkDataSource(): RYNetworkDataSource = + RYNetworkDataSource.getInstance() @Provides @Singleton diff --git a/app/src/main/java/me/ash/reader/data/preference/DarkThemePreference.kt b/app/src/main/java/me/ash/reader/data/preference/DarkThemePreference.kt index a5329df..4047372 100644 --- a/app/src/main/java/me/ash/reader/data/preference/DarkThemePreference.kt +++ b/app/src/main/java/me/ash/reader/data/preference/DarkThemePreference.kt @@ -3,6 +3,7 @@ package me.ash.reader.data.preference import android.content.Context import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable import androidx.datastore.preferences.core.Preferences import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -33,6 +34,7 @@ sealed class DarkThemePreference(val value: Int) : Preference() { } @Composable + @ReadOnlyComposable fun isDarkTheme(): Boolean = when (this) { UseDeviceTheme -> isSystemInDarkTheme() ON -> true diff --git a/app/src/main/java/me/ash/reader/data/preference/FeedsFilterBarTonalElevationPreference.kt b/app/src/main/java/me/ash/reader/data/preference/FeedsFilterBarTonalElevationPreference.kt index ba08eb3..ecdd1cc 100644 --- a/app/src/main/java/me/ash/reader/data/preference/FeedsFilterBarTonalElevationPreference.kt +++ b/app/src/main/java/me/ash/reader/data/preference/FeedsFilterBarTonalElevationPreference.kt @@ -4,17 +4,18 @@ import android.content.Context import androidx.datastore.preferences.core.Preferences import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch +import me.ash.reader.data.constant.ElevationTokens import me.ash.reader.ui.ext.DataStoreKeys import me.ash.reader.ui.ext.dataStore import me.ash.reader.ui.ext.put sealed class FeedsFilterBarTonalElevationPreference(val value: Int) : Preference() { - object Level0 : FeedsFilterBarTonalElevationPreference(0) - object Level1 : FeedsFilterBarTonalElevationPreference(1) - object Level2 : FeedsFilterBarTonalElevationPreference(3) - object Level3 : FeedsFilterBarTonalElevationPreference(6) - object Level4 : FeedsFilterBarTonalElevationPreference(8) - object Level5 : FeedsFilterBarTonalElevationPreference(12) + object Level0 : FeedsFilterBarTonalElevationPreference(ElevationTokens.Level0) + object Level1 : FeedsFilterBarTonalElevationPreference(ElevationTokens.Level1) + object Level2 : FeedsFilterBarTonalElevationPreference(ElevationTokens.Level2) + object Level3 : FeedsFilterBarTonalElevationPreference(ElevationTokens.Level3) + object Level4 : FeedsFilterBarTonalElevationPreference(ElevationTokens.Level4) + object Level5 : FeedsFilterBarTonalElevationPreference(ElevationTokens.Level5) override fun put(context: Context, scope: CoroutineScope) { scope.launch { @@ -27,12 +28,12 @@ sealed class FeedsFilterBarTonalElevationPreference(val value: Int) : Preference fun getDesc(context: Context): String = when (this) { - Level0 -> "Level 0 (0dp)" - Level1 -> "Level 1 (1dp)" - Level2 -> "Level 2 (3dp)" - Level3 -> "Level 3 (6dp)" - Level4 -> "Level 4 (8dp)" - Level5 -> "Level 5 (12dp)" + Level0 -> "Level 0 (${ElevationTokens.Level0}dp)" + Level1 -> "Level 1 (${ElevationTokens.Level1}dp)" + Level2 -> "Level 2 (${ElevationTokens.Level2}dp)" + Level3 -> "Level 3 (${ElevationTokens.Level3}dp)" + Level4 -> "Level 4 (${ElevationTokens.Level4}dp)" + Level5 -> "Level 5 (${ElevationTokens.Level5}dp)" } companion object { @@ -41,13 +42,14 @@ sealed class FeedsFilterBarTonalElevationPreference(val value: Int) : Preference fun fromPreferences(preferences: Preferences) = when (preferences[DataStoreKeys.FeedsFilterBarTonalElevation.key]) { - 0 -> Level0 - 1 -> Level1 - 3 -> Level2 - 6 -> Level3 - 8 -> Level4 - 12 -> Level5 + ElevationTokens.Level0 -> Level0 + ElevationTokens.Level1 -> Level1 + ElevationTokens.Level2 -> Level2 + ElevationTokens.Level3 -> Level3 + ElevationTokens.Level4 -> Level4 + ElevationTokens.Level5 -> Level5 else -> default } } -} \ No newline at end of file +} + diff --git a/app/src/main/java/me/ash/reader/data/preference/FeedsGroupListTonalElevationPreference.kt b/app/src/main/java/me/ash/reader/data/preference/FeedsGroupListTonalElevationPreference.kt index c65afe0..0fbea55 100644 --- a/app/src/main/java/me/ash/reader/data/preference/FeedsGroupListTonalElevationPreference.kt +++ b/app/src/main/java/me/ash/reader/data/preference/FeedsGroupListTonalElevationPreference.kt @@ -4,17 +4,18 @@ import android.content.Context import androidx.datastore.preferences.core.Preferences import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch +import me.ash.reader.data.constant.ElevationTokens import me.ash.reader.ui.ext.DataStoreKeys import me.ash.reader.ui.ext.dataStore import me.ash.reader.ui.ext.put sealed class FeedsGroupListTonalElevationPreference(val value: Int) : Preference() { - object Level0 : FeedsGroupListTonalElevationPreference(0) - object Level1 : FeedsGroupListTonalElevationPreference(1) - object Level2 : FeedsGroupListTonalElevationPreference(3) - object Level3 : FeedsGroupListTonalElevationPreference(6) - object Level4 : FeedsGroupListTonalElevationPreference(8) - object Level5 : FeedsGroupListTonalElevationPreference(12) + object Level0 : FeedsGroupListTonalElevationPreference(ElevationTokens.Level0) + object Level1 : FeedsGroupListTonalElevationPreference(ElevationTokens.Level1) + object Level2 : FeedsGroupListTonalElevationPreference(ElevationTokens.Level2) + object Level3 : FeedsGroupListTonalElevationPreference(ElevationTokens.Level3) + object Level4 : FeedsGroupListTonalElevationPreference(ElevationTokens.Level4) + object Level5 : FeedsGroupListTonalElevationPreference(ElevationTokens.Level5) override fun put(context: Context, scope: CoroutineScope) { scope.launch { @@ -27,12 +28,12 @@ sealed class FeedsGroupListTonalElevationPreference(val value: Int) : Preference fun getDesc(context: Context): String = when (this) { - Level0 -> "Level 0 (0dp)" - Level1 -> "Level 1 (1dp)" - Level2 -> "Level 2 (3dp)" - Level3 -> "Level 3 (6dp)" - Level4 -> "Level 4 (8dp)" - Level5 -> "Level 5 (12dp)" + Level0 -> "Level 0 (${ElevationTokens.Level0}dp)" + Level1 -> "Level 1 (${ElevationTokens.Level1}dp)" + Level2 -> "Level 2 (${ElevationTokens.Level2}dp)" + Level3 -> "Level 3 (${ElevationTokens.Level3}dp)" + Level4 -> "Level 4 (${ElevationTokens.Level4}dp)" + Level5 -> "Level 5 (${ElevationTokens.Level5}dp)" } companion object { @@ -41,12 +42,12 @@ sealed class FeedsGroupListTonalElevationPreference(val value: Int) : Preference fun fromPreferences(preferences: Preferences) = when (preferences[DataStoreKeys.FeedsGroupListTonalElevation.key]) { - 0 -> Level0 - 1 -> Level1 - 3 -> Level2 - 6 -> Level3 - 8 -> Level4 - 12 -> Level5 + ElevationTokens.Level0 -> Level0 + ElevationTokens.Level1 -> Level1 + ElevationTokens.Level2 -> Level2 + ElevationTokens.Level3 -> Level3 + ElevationTokens.Level4 -> Level4 + ElevationTokens.Level5 -> Level5 else -> default } } diff --git a/app/src/main/java/me/ash/reader/data/preference/FeedsTopBarTonalElevationPreference.kt b/app/src/main/java/me/ash/reader/data/preference/FeedsTopBarTonalElevationPreference.kt index b541b4c..9769340 100644 --- a/app/src/main/java/me/ash/reader/data/preference/FeedsTopBarTonalElevationPreference.kt +++ b/app/src/main/java/me/ash/reader/data/preference/FeedsTopBarTonalElevationPreference.kt @@ -4,17 +4,18 @@ import android.content.Context import androidx.datastore.preferences.core.Preferences import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch +import me.ash.reader.data.constant.ElevationTokens import me.ash.reader.ui.ext.DataStoreKeys import me.ash.reader.ui.ext.dataStore import me.ash.reader.ui.ext.put sealed class FeedsTopBarTonalElevationPreference(val value: Int) : Preference() { - object Level0 : FeedsTopBarTonalElevationPreference(0) - object Level1 : FeedsTopBarTonalElevationPreference(1) - object Level2 : FeedsTopBarTonalElevationPreference(3) - object Level3 : FeedsTopBarTonalElevationPreference(6) - object Level4 : FeedsTopBarTonalElevationPreference(8) - object Level5 : FeedsTopBarTonalElevationPreference(12) + object Level0 : FeedsTopBarTonalElevationPreference(ElevationTokens.Level0) + object Level1 : FeedsTopBarTonalElevationPreference(ElevationTokens.Level1) + object Level2 : FeedsTopBarTonalElevationPreference(ElevationTokens.Level2) + object Level3 : FeedsTopBarTonalElevationPreference(ElevationTokens.Level3) + object Level4 : FeedsTopBarTonalElevationPreference(ElevationTokens.Level4) + object Level5 : FeedsTopBarTonalElevationPreference(ElevationTokens.Level5) override fun put(context: Context, scope: CoroutineScope) { scope.launch { @@ -27,12 +28,12 @@ sealed class FeedsTopBarTonalElevationPreference(val value: Int) : Preference() fun getDesc(context: Context): String = when (this) { - Level0 -> "Level 0 (0dp)" - Level1 -> "Level 1 (1dp)" - Level2 -> "Level 2 (3dp)" - Level3 -> "Level 3 (6dp)" - Level4 -> "Level 4 (8dp)" - Level5 -> "Level 5 (12dp)" + Level0 -> "Level 0 (${ElevationTokens.Level0}dp)" + Level1 -> "Level 1 (${ElevationTokens.Level1}dp)" + Level2 -> "Level 2 (${ElevationTokens.Level2}dp)" + Level3 -> "Level 3 (${ElevationTokens.Level3}dp)" + Level4 -> "Level 4 (${ElevationTokens.Level4}dp)" + Level5 -> "Level 5 (${ElevationTokens.Level5}dp)" } companion object { @@ -41,12 +42,12 @@ sealed class FeedsTopBarTonalElevationPreference(val value: Int) : Preference() fun fromPreferences(preferences: Preferences) = when (preferences[DataStoreKeys.FeedsTopBarTonalElevation.key]) { - 0 -> Level0 - 1 -> Level1 - 3 -> Level2 - 6 -> Level3 - 8 -> Level4 - 12 -> Level5 + ElevationTokens.Level0 -> Level0 + ElevationTokens.Level1 -> Level1 + ElevationTokens.Level2 -> Level2 + ElevationTokens.Level3 -> Level3 + ElevationTokens.Level4 -> Level4 + ElevationTokens.Level5 -> Level5 else -> default } } diff --git a/app/src/main/java/me/ash/reader/data/preference/FlowArticleListTonalElevationPreference.kt b/app/src/main/java/me/ash/reader/data/preference/FlowArticleListTonalElevationPreference.kt index 2d3dd1b..3ff0592 100644 --- a/app/src/main/java/me/ash/reader/data/preference/FlowArticleListTonalElevationPreference.kt +++ b/app/src/main/java/me/ash/reader/data/preference/FlowArticleListTonalElevationPreference.kt @@ -4,17 +4,18 @@ import android.content.Context import androidx.datastore.preferences.core.Preferences import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch +import me.ash.reader.data.constant.ElevationTokens import me.ash.reader.ui.ext.DataStoreKeys import me.ash.reader.ui.ext.dataStore import me.ash.reader.ui.ext.put sealed class FlowArticleListTonalElevationPreference(val value: Int) : Preference() { - object Level0 : FlowArticleListTonalElevationPreference(0) - object Level1 : FlowArticleListTonalElevationPreference(1) - object Level2 : FlowArticleListTonalElevationPreference(3) - object Level3 : FlowArticleListTonalElevationPreference(6) - object Level4 : FlowArticleListTonalElevationPreference(8) - object Level5 : FlowArticleListTonalElevationPreference(12) + object Level0 : FlowArticleListTonalElevationPreference(ElevationTokens.Level0) + object Level1 : FlowArticleListTonalElevationPreference(ElevationTokens.Level1) + object Level2 : FlowArticleListTonalElevationPreference(ElevationTokens.Level2) + object Level3 : FlowArticleListTonalElevationPreference(ElevationTokens.Level3) + object Level4 : FlowArticleListTonalElevationPreference(ElevationTokens.Level4) + object Level5 : FlowArticleListTonalElevationPreference(ElevationTokens.Level5) override fun put(context: Context, scope: CoroutineScope) { scope.launch { @@ -27,12 +28,12 @@ sealed class FlowArticleListTonalElevationPreference(val value: Int) : Preferenc fun getDesc(context: Context): String = when (this) { - Level0 -> "Level 0 (0dp)" - Level1 -> "Level 1 (1dp)" - Level2 -> "Level 2 (3dp)" - Level3 -> "Level 3 (6dp)" - Level4 -> "Level 4 (8dp)" - Level5 -> "Level 5 (12dp)" + Level0 -> "Level 0 (${ElevationTokens.Level0}dp)" + Level1 -> "Level 1 (${ElevationTokens.Level1}dp)" + Level2 -> "Level 2 (${ElevationTokens.Level2}dp)" + Level3 -> "Level 3 (${ElevationTokens.Level3}dp)" + Level4 -> "Level 4 (${ElevationTokens.Level4}dp)" + Level5 -> "Level 5 (${ElevationTokens.Level5}dp)" } companion object { @@ -41,12 +42,12 @@ sealed class FlowArticleListTonalElevationPreference(val value: Int) : Preferenc fun fromPreferences(preferences: Preferences) = when (preferences[DataStoreKeys.FlowArticleListTonalElevation.key]) { - 0 -> Level0 - 1 -> Level1 - 3 -> Level2 - 6 -> Level3 - 8 -> Level4 - 12 -> Level5 + ElevationTokens.Level0 -> Level0 + ElevationTokens.Level1 -> Level1 + ElevationTokens.Level2 -> Level2 + ElevationTokens.Level3 -> Level3 + ElevationTokens.Level4 -> Level4 + ElevationTokens.Level5 -> Level5 else -> default } } diff --git a/app/src/main/java/me/ash/reader/data/preference/FlowFilterBarTonalElevationPreference.kt b/app/src/main/java/me/ash/reader/data/preference/FlowFilterBarTonalElevationPreference.kt index 067d58c..45fcc5c 100644 --- a/app/src/main/java/me/ash/reader/data/preference/FlowFilterBarTonalElevationPreference.kt +++ b/app/src/main/java/me/ash/reader/data/preference/FlowFilterBarTonalElevationPreference.kt @@ -4,17 +4,18 @@ import android.content.Context import androidx.datastore.preferences.core.Preferences import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch +import me.ash.reader.data.constant.ElevationTokens import me.ash.reader.ui.ext.DataStoreKeys import me.ash.reader.ui.ext.dataStore import me.ash.reader.ui.ext.put sealed class FlowFilterBarTonalElevationPreference(val value: Int) : Preference() { - object Level0 : FlowFilterBarTonalElevationPreference(0) - object Level1 : FlowFilterBarTonalElevationPreference(1) - object Level2 : FlowFilterBarTonalElevationPreference(3) - object Level3 : FlowFilterBarTonalElevationPreference(6) - object Level4 : FlowFilterBarTonalElevationPreference(8) - object Level5 : FlowFilterBarTonalElevationPreference(12) + object Level0 : FlowFilterBarTonalElevationPreference(ElevationTokens.Level0) + object Level1 : FlowFilterBarTonalElevationPreference(ElevationTokens.Level1) + object Level2 : FlowFilterBarTonalElevationPreference(ElevationTokens.Level2) + object Level3 : FlowFilterBarTonalElevationPreference(ElevationTokens.Level3) + object Level4 : FlowFilterBarTonalElevationPreference(ElevationTokens.Level4) + object Level5 : FlowFilterBarTonalElevationPreference(ElevationTokens.Level5) override fun put(context: Context, scope: CoroutineScope) { scope.launch { @@ -27,12 +28,12 @@ sealed class FlowFilterBarTonalElevationPreference(val value: Int) : Preference( fun getDesc(context: Context): String = when (this) { - Level0 -> "Level 0 (0dp)" - Level1 -> "Level 1 (1dp)" - Level2 -> "Level 2 (3dp)" - Level3 -> "Level 3 (6dp)" - Level4 -> "Level 4 (8dp)" - Level5 -> "Level 5 (12dp)" + Level0 -> "Level 0 (${ElevationTokens.Level0}dp)" + Level1 -> "Level 1 (${ElevationTokens.Level1}dp)" + Level2 -> "Level 2 (${ElevationTokens.Level2}dp)" + Level3 -> "Level 3 (${ElevationTokens.Level3}dp)" + Level4 -> "Level 4 (${ElevationTokens.Level4}dp)" + Level5 -> "Level 5 (${ElevationTokens.Level5}dp)" } companion object { @@ -41,12 +42,12 @@ sealed class FlowFilterBarTonalElevationPreference(val value: Int) : Preference( fun fromPreferences(preferences: Preferences) = when (preferences[DataStoreKeys.FlowFilterBarTonalElevation.key]) { - 0 -> Level0 - 1 -> Level1 - 3 -> Level2 - 6 -> Level3 - 8 -> Level4 - 12 -> Level5 + ElevationTokens.Level0 -> Level0 + ElevationTokens.Level1 -> Level1 + ElevationTokens.Level2 -> Level2 + ElevationTokens.Level3 -> Level3 + ElevationTokens.Level4 -> Level4 + ElevationTokens.Level5 -> Level5 else -> default } } diff --git a/app/src/main/java/me/ash/reader/data/preference/FlowTopBarTonalElevationPreference.kt b/app/src/main/java/me/ash/reader/data/preference/FlowTopBarTonalElevationPreference.kt index 90938dc..20ad859 100644 --- a/app/src/main/java/me/ash/reader/data/preference/FlowTopBarTonalElevationPreference.kt +++ b/app/src/main/java/me/ash/reader/data/preference/FlowTopBarTonalElevationPreference.kt @@ -4,17 +4,18 @@ import android.content.Context import androidx.datastore.preferences.core.Preferences import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch +import me.ash.reader.data.constant.ElevationTokens import me.ash.reader.ui.ext.DataStoreKeys import me.ash.reader.ui.ext.dataStore import me.ash.reader.ui.ext.put sealed class FlowTopBarTonalElevationPreference(val value: Int) : Preference() { - object Level0 : FlowTopBarTonalElevationPreference(0) - object Level1 : FlowTopBarTonalElevationPreference(1) - object Level2 : FlowTopBarTonalElevationPreference(3) - object Level3 : FlowTopBarTonalElevationPreference(6) - object Level4 : FlowTopBarTonalElevationPreference(8) - object Level5 : FlowTopBarTonalElevationPreference(12) + object Level0 : FlowTopBarTonalElevationPreference(ElevationTokens.Level0) + object Level1 : FlowTopBarTonalElevationPreference(ElevationTokens.Level1) + object Level2 : FlowTopBarTonalElevationPreference(ElevationTokens.Level2) + object Level3 : FlowTopBarTonalElevationPreference(ElevationTokens.Level3) + object Level4 : FlowTopBarTonalElevationPreference(ElevationTokens.Level4) + object Level5 : FlowTopBarTonalElevationPreference(ElevationTokens.Level5) override fun put(context: Context, scope: CoroutineScope) { scope.launch { @@ -27,12 +28,12 @@ sealed class FlowTopBarTonalElevationPreference(val value: Int) : Preference() { fun getDesc(context: Context): String = when (this) { - Level0 -> "Level 0 (0dp)" - Level1 -> "Level 1 (1dp)" - Level2 -> "Level 2 (3dp)" - Level3 -> "Level 3 (6dp)" - Level4 -> "Level 4 (8dp)" - Level5 -> "Level 5 (12dp)" + Level0 -> "Level 0 (${ElevationTokens.Level0}dp)" + Level1 -> "Level 1 (${ElevationTokens.Level1}dp)" + Level2 -> "Level 2 (${ElevationTokens.Level2}dp)" + Level3 -> "Level 3 (${ElevationTokens.Level3}dp)" + Level4 -> "Level 4 (${ElevationTokens.Level4}dp)" + Level5 -> "Level 5 (${ElevationTokens.Level5}dp)" } companion object { @@ -41,12 +42,12 @@ sealed class FlowTopBarTonalElevationPreference(val value: Int) : Preference() { fun fromPreferences(preferences: Preferences) = when (preferences[DataStoreKeys.FlowTopBarTonalElevation.key]) { - 0 -> Level0 - 1 -> Level1 - 3 -> Level2 - 6 -> Level3 - 8 -> Level4 - 12 -> Level5 + ElevationTokens.Level0 -> Level0 + ElevationTokens.Level1 -> Level1 + ElevationTokens.Level2 -> Level2 + ElevationTokens.Level3 -> Level3 + ElevationTokens.Level4 -> Level4 + ElevationTokens.Level5 -> Level5 else -> default } } diff --git a/app/src/main/java/me/ash/reader/data/preference/InitialFilterPreference.kt b/app/src/main/java/me/ash/reader/data/preference/InitialFilterPreference.kt new file mode 100644 index 0000000..4f20d2e --- /dev/null +++ b/app/src/main/java/me/ash/reader/data/preference/InitialFilterPreference.kt @@ -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 + } + } +} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/data/preference/InitialPagePreference.kt b/app/src/main/java/me/ash/reader/data/preference/InitialPagePreference.kt new file mode 100644 index 0000000..fbf954b --- /dev/null +++ b/app/src/main/java/me/ash/reader/data/preference/InitialPagePreference.kt @@ -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 + } + } +} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/data/preference/NewVersionDownloadUrlPreference.kt b/app/src/main/java/me/ash/reader/data/preference/NewVersionDownloadUrlPreference.kt new file mode 100644 index 0000000..0b0a5f8 --- /dev/null +++ b/app/src/main/java/me/ash/reader/data/preference/NewVersionDownloadUrlPreference.kt @@ -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 +} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/data/preference/NewVersionLogPreference.kt b/app/src/main/java/me/ash/reader/data/preference/NewVersionLogPreference.kt new file mode 100644 index 0000000..151e7bf --- /dev/null +++ b/app/src/main/java/me/ash/reader/data/preference/NewVersionLogPreference.kt @@ -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 +} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/data/preference/NewVersionNumberPreference.kt b/app/src/main/java/me/ash/reader/data/preference/NewVersionNumberPreference.kt new file mode 100644 index 0000000..29424b1 --- /dev/null +++ b/app/src/main/java/me/ash/reader/data/preference/NewVersionNumberPreference.kt @@ -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() +} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/data/preference/NewVersionPublishDatePreference.kt b/app/src/main/java/me/ash/reader/data/preference/NewVersionPublishDatePreference.kt new file mode 100644 index 0000000..b7fd390 --- /dev/null +++ b/app/src/main/java/me/ash/reader/data/preference/NewVersionPublishDatePreference.kt @@ -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 +} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/data/preference/NewVersionSizePreference.kt b/app/src/main/java/me/ash/reader/data/preference/NewVersionSizePreference.kt new file mode 100644 index 0000000..eac3dff --- /dev/null +++ b/app/src/main/java/me/ash/reader/data/preference/NewVersionSizePreference.kt @@ -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 +} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/data/preference/Preference.kt b/app/src/main/java/me/ash/reader/data/preference/Preference.kt index cd451da..e4fba2b 100644 --- a/app/src/main/java/me/ash/reader/data/preference/Preference.kt +++ b/app/src/main/java/me/ash/reader/data/preference/Preference.kt @@ -1,8 +1,53 @@ package me.ash.reader.data.preference import android.content.Context +import androidx.datastore.preferences.core.Preferences import kotlinx.coroutines.CoroutineScope sealed class Preference { abstract fun put(context: Context, scope: CoroutineScope) +} + +fun Preferences.toSettings(): Settings { + return Settings( + newVersionNumber = NewVersionNumberPreference.fromPreferences(this), + skipVersionNumber = SkipVersionNumberPreference.fromPreferences(this), + newVersionPublishDate = NewVersionPublishDatePreference.fromPreferences(this), + newVersionLog = NewVersionLogPreference.fromPreferences(this), + newVersionSize = NewVersionSizePreference.fromPreferences(this), + newVersionDownloadUrl = NewVersionDownloadUrlPreference.fromPreferences(this), + + themeIndex = ThemeIndexPreference.fromPreferences(this), + customPrimaryColor = CustomPrimaryColorPreference.fromPreferences(this), + darkTheme = DarkThemePreference.fromPreferences(this), + amoledDarkTheme = AmoledDarkThemePreference.fromPreferences(this), + + feedsFilterBarStyle = FeedsFilterBarStylePreference.fromPreferences(this), + feedsFilterBarFilled = FeedsFilterBarFilledPreference.fromPreferences(this), + feedsFilterBarPadding = FeedsFilterBarPaddingPreference.fromPreferences(this), + feedsFilterBarTonalElevation = FeedsFilterBarTonalElevationPreference.fromPreferences(this), + feedsTopBarTonalElevation = FeedsTopBarTonalElevationPreference.fromPreferences(this), + feedsGroupListExpand = FeedsGroupListExpandPreference.fromPreferences(this), + feedsGroupListTonalElevation = FeedsGroupListTonalElevationPreference.fromPreferences(this), + + flowFilterBarStyle = FlowFilterBarStylePreference.fromPreferences(this), + flowFilterBarFilled = FlowFilterBarFilledPreference.fromPreferences(this), + flowFilterBarPadding = FlowFilterBarPaddingPreference.fromPreferences(this), + flowFilterBarTonalElevation = FlowFilterBarTonalElevationPreference.fromPreferences(this), + flowTopBarTonalElevation = FlowTopBarTonalElevationPreference.fromPreferences(this), + flowArticleListFeedIcon = FlowArticleListFeedIconPreference.fromPreferences(this), + flowArticleListFeedName = FlowArticleListFeedNamePreference.fromPreferences(this), + flowArticleListImage = FlowArticleListImagePreference.fromPreferences(this), + flowArticleListDesc = FlowArticleListDescPreference.fromPreferences(this), + flowArticleListTime = FlowArticleListTimePreference.fromPreferences(this), + flowArticleListDateStickyHeader = FlowArticleListDateStickyHeaderPreference.fromPreferences( + this + ), + flowArticleListTonalElevation = FlowArticleListTonalElevationPreference.fromPreferences(this), + + initialPage = InitialPagePreference.fromPreferences(this), + initialFilter = InitialFilterPreference.fromPreferences(this), + + languages = LanguagesPreference.fromPreferences(this), + ) } \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/data/preference/Settings.kt b/app/src/main/java/me/ash/reader/data/preference/Settings.kt index 70996c7..b5edcf2 100644 --- a/app/src/main/java/me/ash/reader/data/preference/Settings.kt +++ b/app/src/main/java/me/ash/reader/data/preference/Settings.kt @@ -6,12 +6,19 @@ import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext -import androidx.datastore.preferences.core.Preferences import kotlinx.coroutines.flow.map +import me.ash.reader.data.model.Version import me.ash.reader.ui.ext.collectAsStateValue import me.ash.reader.ui.ext.dataStore data class Settings( + val newVersionNumber: Version = NewVersionNumberPreference.default, + val skipVersionNumber: Version = SkipVersionNumberPreference.default, + val newVersionPublishDate: String = NewVersionPublishDatePreference.default, + val newVersionLog: String = NewVersionLogPreference.default, + val newVersionSize: String = NewVersionSizePreference.default, + val newVersionDownloadUrl: String = NewVersionDownloadUrlPreference.default, + val themeIndex: Int = ThemeIndexPreference.default, val customPrimaryColor: String = CustomPrimaryColorPreference.default, val darkTheme: DarkThemePreference = DarkThemePreference.default, @@ -38,43 +45,12 @@ data class Settings( val flowArticleListDateStickyHeader: FlowArticleListDateStickyHeaderPreference = FlowArticleListDateStickyHeaderPreference.default, val flowArticleListTonalElevation: FlowArticleListTonalElevationPreference = FlowArticleListTonalElevationPreference.default, + val initialPage: InitialPagePreference = InitialPagePreference.default, + val initialFilter: InitialFilterPreference = InitialFilterPreference.default, + val languages: LanguagesPreference = LanguagesPreference.default, ) -fun Preferences.toSettings(): Settings { - return Settings( - themeIndex = ThemeIndexPreference.fromPreferences(this), - customPrimaryColor = CustomPrimaryColorPreference.fromPreferences(this), - darkTheme = DarkThemePreference.fromPreferences(this), - amoledDarkTheme = AmoledDarkThemePreference.fromPreferences(this), - - feedsFilterBarStyle = FeedsFilterBarStylePreference.fromPreferences(this), - feedsFilterBarFilled = FeedsFilterBarFilledPreference.fromPreferences(this), - feedsFilterBarPadding = FeedsFilterBarPaddingPreference.fromPreferences(this), - feedsFilterBarTonalElevation = FeedsFilterBarTonalElevationPreference.fromPreferences(this), - feedsTopBarTonalElevation = FeedsTopBarTonalElevationPreference.fromPreferences(this), - feedsGroupListExpand = FeedsGroupListExpandPreference.fromPreferences(this), - feedsGroupListTonalElevation = FeedsGroupListTonalElevationPreference.fromPreferences(this), - - flowFilterBarStyle = FlowFilterBarStylePreference.fromPreferences(this), - flowFilterBarFilled = FlowFilterBarFilledPreference.fromPreferences(this), - flowFilterBarPadding = FlowFilterBarPaddingPreference.fromPreferences(this), - flowFilterBarTonalElevation = FlowFilterBarTonalElevationPreference.fromPreferences(this), - flowTopBarTonalElevation = FlowTopBarTonalElevationPreference.fromPreferences(this), - flowArticleListFeedIcon = FlowArticleListFeedIconPreference.fromPreferences(this), - flowArticleListFeedName = FlowArticleListFeedNamePreference.fromPreferences(this), - flowArticleListImage = FlowArticleListImagePreference.fromPreferences(this), - flowArticleListDesc = FlowArticleListDescPreference.fromPreferences(this), - flowArticleListTime = FlowArticleListTimePreference.fromPreferences(this), - flowArticleListDateStickyHeader = FlowArticleListDateStickyHeaderPreference.fromPreferences( - this - ), - flowArticleListTonalElevation = FlowArticleListTonalElevationPreference.fromPreferences(this), - - languages = LanguagesPreference.fromPreferences(this), - ) -} - @Composable fun SettingsProvider( content: @Composable () -> Unit, @@ -88,6 +64,13 @@ fun SettingsProvider( }.collectAsStateValue(initial = Settings()) CompositionLocalProvider( + LocalNewVersionNumber provides settings.newVersionNumber, + LocalSkipVersionNumber provides settings.skipVersionNumber, + LocalNewVersionPublishDate provides settings.newVersionPublishDate, + LocalNewVersionLog provides settings.newVersionLog, + LocalNewVersionSize provides settings.newVersionSize, + LocalNewVersionDownloadUrl provides settings.newVersionDownloadUrl, + LocalThemeIndex provides settings.themeIndex, LocalCustomPrimaryColor provides settings.customPrimaryColor, LocalDarkTheme provides settings.darkTheme, @@ -114,12 +97,22 @@ fun SettingsProvider( LocalFlowFilterBarPadding provides settings.flowFilterBarPadding, LocalFlowFilterBarTonalElevation provides settings.flowFilterBarTonalElevation, + LocalInitialPage provides settings.initialPage, + LocalInitialFilter provides settings.initialFilter, + LocalLanguages provides settings.languages, ) { content() } } +val LocalNewVersionNumber = compositionLocalOf { NewVersionNumberPreference.default } +val LocalSkipVersionNumber = compositionLocalOf { SkipVersionNumberPreference.default } +val LocalNewVersionPublishDate = compositionLocalOf { NewVersionPublishDatePreference.default } +val LocalNewVersionLog = compositionLocalOf { NewVersionLogPreference.default } +val LocalNewVersionSize = compositionLocalOf { NewVersionSizePreference.default } +val LocalNewVersionDownloadUrl = compositionLocalOf { NewVersionDownloadUrlPreference.default } + val LocalThemeIndex = compositionLocalOf { ThemeIndexPreference.default } val LocalCustomPrimaryColor = @@ -169,5 +162,9 @@ val LocalFlowArticleListDateStickyHeader = val LocalFlowArticleListTonalElevation = compositionLocalOf { FlowArticleListTonalElevationPreference.default } +val LocalInitialPage = compositionLocalOf { InitialPagePreference.default } +val LocalInitialFilter = + compositionLocalOf { InitialFilterPreference.default } + val LocalLanguages = compositionLocalOf { LanguagesPreference.default } diff --git a/app/src/main/java/me/ash/reader/data/preference/SkipVersionNumberPreference.kt b/app/src/main/java/me/ash/reader/data/preference/SkipVersionNumberPreference.kt new file mode 100644 index 0000000..d862c42 --- /dev/null +++ b/app/src/main/java/me/ash/reader/data/preference/SkipVersionNumberPreference.kt @@ -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() +} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/data/repository/AbstractRssRepository.kt b/app/src/main/java/me/ash/reader/data/repository/AbstractRssRepository.kt index 71eb82d..bfa666d 100644 --- a/app/src/main/java/me/ash/reader/data/repository/AbstractRssRepository.kt +++ b/app/src/main/java/me/ash/reader/data/repository/AbstractRssRepository.kt @@ -2,14 +2,15 @@ package me.ash.reader.data.repository import android.content.Context import android.util.Log -import androidx.hilt.work.HiltWorker import androidx.paging.PagingSource -import androidx.work.* -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject +import androidx.work.CoroutineWorker +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.ListenableWorker +import androidx.work.WorkManager import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.mapLatest import me.ash.reader.data.dao.AccountDao import me.ash.reader.data.dao.ArticleDao import me.ash.reader.data.dao.FeedDao @@ -17,7 +18,6 @@ import me.ash.reader.data.dao.GroupDao import me.ash.reader.data.entity.* import me.ash.reader.ui.ext.currentAccountId import java.util.* -import java.util.concurrent.TimeUnit abstract class AbstractRssRepository constructor( private val context: Context, @@ -99,7 +99,7 @@ abstract class AbstractRssRepository constructor( fun pullImportant( isStarred: Boolean = false, isUnread: Boolean = false, - ): Flow> { + ): Flow> { val accountId = context.currentAccountId Log.i( "RLog", @@ -111,6 +111,12 @@ abstract class AbstractRssRepository constructor( isUnread -> articleDao .queryImportantCountWhenIsUnread(accountId, isUnread) else -> articleDao.queryImportantCountWhenIsAll(accountId) + }.mapLatest { + mapOf( + *(it.map { + it.feedId to it.important + }.toTypedArray()) + ) }.flowOn(dispatcherIO) } @@ -130,10 +136,6 @@ abstract class AbstractRssRepository constructor( return feedDao.queryByLink(context.currentAccountId, url).isNotEmpty() } - fun peekWork(): String { - return workManager.getWorkInfosByTag("sync").get().size.toString() - } - suspend fun updateGroup(group: Group) { groupDao.update(group) } @@ -207,34 +209,3 @@ abstract class AbstractRssRepository constructor( } } } - -@HiltWorker -class SyncWorker @AssistedInject constructor( - @Assisted context: Context, - @Assisted workerParams: WorkerParameters, - private val rssRepository: RssRepository, -) : CoroutineWorker(context, workerParams) { - - override suspend fun doWork(): Result { - Log.i("RLog", "doWork: ") - return rssRepository.get().sync(this) - } - - companion object { - const val WORK_NAME = "article.sync" - - val UUID: UUID - - val repeatingRequest = PeriodicWorkRequestBuilder( - 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) - } -} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/data/repository/FeverRssRepository.kt b/app/src/main/java/me/ash/reader/data/repository/FeverRssRepository.kt deleted file mode 100644 index a7b8600..0000000 --- a/app/src/main/java/me/ash/reader/data/repository/FeverRssRepository.kt +++ /dev/null @@ -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
) { -// 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() -// 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
() -// 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 = "" -// ) -// } -// } -// } -// } -//} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/data/repository/LocalRssRepository.kt b/app/src/main/java/me/ash/reader/data/repository/LocalRssRepository.kt index d101a19..b09d0ad 100644 --- a/app/src/main/java/me/ash/reader/data/repository/LocalRssRepository.kt +++ b/app/src/main/java/me/ash/reader/data/repository/LocalRssRepository.kt @@ -1,12 +1,7 @@ package me.ash.reader.data.repository -import android.app.* import android.content.Context -import android.content.Intent -import android.graphics.BitmapFactory import android.util.Log -import androidx.core.app.NotificationCompat -import androidx.core.app.NotificationManagerCompat import androidx.work.CoroutineWorker import androidx.work.ListenableWorker import androidx.work.WorkManager @@ -15,8 +10,6 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.withContext -import me.ash.reader.MainActivity -import me.ash.reader.R import me.ash.reader.data.dao.AccountDao import me.ash.reader.data.dao.ArticleDao import me.ash.reader.data.dao.FeedDao @@ -30,8 +23,6 @@ import me.ash.reader.data.module.DispatcherIO import me.ash.reader.data.repository.SyncWorker.Companion.setIsSyncing import me.ash.reader.ui.ext.currentAccountId import me.ash.reader.ui.ext.spacerDollar -import me.ash.reader.ui.page.common.ExtraName -import me.ash.reader.ui.page.common.NotificationGroupName import java.util.* import javax.inject.Inject @@ -41,6 +32,7 @@ class LocalRssRepository @Inject constructor( private val articleDao: ArticleDao, private val feedDao: FeedDao, private val rssHelper: RssHelper, + private val notificationHelper: NotificationHelper, private val accountDao: AccountDao, private val groupDao: GroupDao, @DispatcherDefault @@ -52,16 +44,6 @@ class LocalRssRepository @Inject constructor( context, accountDao, articleDao, groupDao, feedDao, workManager, dispatcherIO ) { - private val notificationManager: NotificationManagerCompat = - NotificationManagerCompat.from(context).apply { - createNotificationChannel( - NotificationChannel( - NotificationGroupName.ARTICLE_UPDATE, - NotificationGroupName.ARTICLE_UPDATE, - NotificationManager.IMPORTANCE_DEFAULT - ) - ) - } override suspend fun updateArticleInfo(article: Article) { articleDao.update(article) @@ -98,7 +80,7 @@ class LocalRssRepository @Inject constructor( .awaitAll() .forEach { if (it.isNotify) { - notify( + notificationHelper.notify( FeedWithArticle( it.feedWithArticle.feed, articleDao.insertIfNotExist(it.feedWithArticle.articles) @@ -183,75 +165,4 @@ class LocalRssRepository @Inject constructor( isNotify = articles.isNotEmpty() && feed.isNotification ) } - - private fun notify( - feedWithArticle: FeedWithArticle, - ) { - notificationManager.createNotificationChannelGroup( - NotificationChannelGroup( - feedWithArticle.feed.id, - feedWithArticle.feed.name - ) - ) - feedWithArticle.articles.forEach { article -> - val builder = NotificationCompat.Builder(context, NotificationGroupName.ARTICLE_UPDATE) - .setSmallIcon(R.drawable.ic_notification) - .setLargeIcon( - (BitmapFactory.decodeResource( - context.resources, - R.drawable.ic_notification - )) - ) - .setContentTitle(article.title) - .setContentIntent( - PendingIntent.getActivity( - context, - Random().nextInt() + article.id.hashCode(), - Intent(context, MainActivity::class.java).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK or - Intent.FLAG_ACTIVITY_CLEAR_TASK - putExtra( - ExtraName.ARTICLE_ID, - article.id - ) - }, - PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT - ) - ) - .setGroup(feedWithArticle.feed.id) - .setStyle( - NotificationCompat.BigTextStyle() - .bigText(article.shortDescription) - .setSummaryText(feedWithArticle.feed.name) - ) - - notificationManager.notify( - Random().nextInt() + article.id.hashCode(), - builder.build().apply { - flags = Notification.FLAG_AUTO_CANCEL - } - ) - } - - if (feedWithArticle.articles.size > 1) { - notificationManager.notify( - Random().nextInt() + feedWithArticle.feed.id.hashCode(), - NotificationCompat.Builder(context, NotificationGroupName.ARTICLE_UPDATE) - .setSmallIcon(R.drawable.ic_notification) - .setLargeIcon( - (BitmapFactory.decodeResource( - context.resources, - R.drawable.ic_notification - )) - ) - .setStyle( - NotificationCompat.InboxStyle() - .setSummaryText(feedWithArticle.feed.name) - ) - .setGroup(feedWithArticle.feed.id) - .setGroupSummary(true) - .build() - ) - } - } } \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/data/repository/NotificationHelper.kt b/app/src/main/java/me/ash/reader/data/repository/NotificationHelper.kt new file mode 100644 index 0000000..ee421c8 --- /dev/null +++ b/app/src/main/java/me/ash/reader/data/repository/NotificationHelper.kt @@ -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() + ) + } + } +} diff --git a/app/src/main/java/me/ash/reader/data/repository/OpmlRepository.kt b/app/src/main/java/me/ash/reader/data/repository/OpmlRepository.kt index 304bed4..774be76 100644 --- a/app/src/main/java/me/ash/reader/data/repository/OpmlRepository.kt +++ b/app/src/main/java/me/ash/reader/data/repository/OpmlRepository.kt @@ -43,7 +43,7 @@ class OpmlRepository @Inject constructor( repeatList.add(it) } } - feedDao.insertList((groupWithFeed.feeds subtract repeatList).toList()) + feedDao.insertList((groupWithFeed.feeds subtract repeatList.toSet()).toList()) } } @@ -54,7 +54,7 @@ class OpmlRepository @Inject constructor( Opml( "2.0", Head( - accountDao.queryById(context.currentAccountId).name, + accountDao.queryById(context.currentAccountId)?.name, Date().toString(), null, null, null, null, null, null, null, null, null, null, null, diff --git a/app/src/main/java/me/ash/reader/data/repository/AppRepository.kt b/app/src/main/java/me/ash/reader/data/repository/RYRepository.kt similarity index 68% rename from app/src/main/java/me/ash/reader/data/repository/AppRepository.kt rename to app/src/main/java/me/ash/reader/data/repository/RYRepository.kt index 5d67c56..b9ce8ac 100644 --- a/app/src/main/java/me/ash/reader/data/repository/AppRepository.kt +++ b/app/src/main/java/me/ash/reader/data/repository/RYRepository.kt @@ -4,27 +4,28 @@ import android.content.Context import android.util.Log import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.withContext import me.ash.reader.R -import me.ash.reader.data.entity.toVersion -import me.ash.reader.data.module.ApplicationScope +import me.ash.reader.data.model.toVersion import me.ash.reader.data.module.DispatcherIO import me.ash.reader.data.module.DispatcherMain -import me.ash.reader.data.source.AppNetworkDataSource +import me.ash.reader.data.preference.* +import me.ash.reader.data.preference.NewVersionSizePreference.formatSize import me.ash.reader.data.source.Download +import me.ash.reader.data.source.RYNetworkDataSource import me.ash.reader.data.source.downloadToFileWithProgress -import me.ash.reader.ui.ext.* +import me.ash.reader.ui.ext.getCurrentVersion +import me.ash.reader.ui.ext.getLatestApk +import me.ash.reader.ui.ext.showToast +import me.ash.reader.ui.ext.skipVersionNumber import javax.inject.Inject -class AppRepository @Inject constructor( +class RYRepository @Inject constructor( @ApplicationContext private val context: Context, - private val appNetworkDataSource: AppNetworkDataSource, - @ApplicationScope - private val applicationScope: CoroutineScope, + private val RYNetworkDataSource: RYNetworkDataSource, @DispatcherIO private val dispatcherIO: CoroutineDispatcher, @DispatcherMain @@ -33,7 +34,7 @@ class AppRepository @Inject constructor( suspend fun checkUpdate(showToast: Boolean = true): Boolean? = withContext(dispatcherIO) { try { val response = - appNetworkDataSource.getReleaseLatest(context.getString(R.string.update_link)) + RYNetworkDataSource.getReleaseLatest(context.getString(R.string.update_link)) when { response.code() == 403 -> { withContext(dispatcherMain) { @@ -50,31 +51,22 @@ class AppRepository @Inject constructor( } val latest = response.body()!! val latestVersion = latest.tag_name.toVersion() -// val latestVersion = "0.7.3".toVersion() +// val latestVersion = "1.0.0".toVersion() val skipVersion = context.skipVersionNumber.toVersion() val currentVersion = context.getCurrentVersion() val latestLog = latest.body ?: "" val latestPublishDate = latest.published_at ?: latest.created_at ?: "" - val latestSize = latest.assets - ?.first() - ?.size - ?: 0 - val latestDownloadUrl = latest.assets - ?.first() - ?.browser_download_url - ?: "" + val latestSize = latest.assets?.first()?.size ?: 0 + val latestDownloadUrl = latest.assets?.first()?.browser_download_url ?: "" Log.i("RLog", "current version $currentVersion") if (latestVersion.whetherNeedUpdate(currentVersion, skipVersion)) { Log.i("RLog", "new version $latestVersion") - context.dataStore.put( - DataStoreKeys.NewVersionNumber, - latestVersion.toString() - ) - context.dataStore.put(DataStoreKeys.NewVersionLog, latestLog) - context.dataStore.put(DataStoreKeys.NewVersionPublishDate, latestPublishDate) - context.dataStore.put(DataStoreKeys.NewVersionSize, latestSize) - context.dataStore.put(DataStoreKeys.NewVersionDownloadUrl, latestDownloadUrl) + NewVersionNumberPreference.put(context, this, latestVersion.toString()) + NewVersionLogPreference.put(context, this, latestLog) + NewVersionPublishDatePreference.put(context, this, latestPublishDate) + NewVersionSizePreference.put(context, this, latestSize.formatSize()) + NewVersionDownloadUrlPreference.put(context, this, latestDownloadUrl) true } else { false @@ -93,7 +85,7 @@ class AppRepository @Inject constructor( withContext(dispatcherIO) { Log.i("RLog", "downloadFile start: $url") try { - return@withContext appNetworkDataSource.downloadFile(url) + return@withContext RYNetworkDataSource.downloadFile(url) .downloadToFileWithProgress(context.getLatestApk()) } catch (e: Exception) { e.printStackTrace() diff --git a/app/src/main/java/me/ash/reader/data/repository/RssHelper.kt b/app/src/main/java/me/ash/reader/data/repository/RssHelper.kt index 55e1b11..25528d0 100644 --- a/app/src/main/java/me/ash/reader/data/repository/RssHelper.kt +++ b/app/src/main/java/me/ash/reader/data/repository/RssHelper.kt @@ -21,8 +21,6 @@ import net.dankito.readability4j.extended.Readability4JExtended import okhttp3.OkHttpClient import okhttp3.Request import java.net.URL -import java.text.ParsePosition -import java.text.SimpleDateFormat import java.util.* import javax.inject.Inject @@ -31,6 +29,7 @@ class RssHelper @Inject constructor( private val context: Context, @DispatcherIO private val dispatcherIO: CoroutineDispatcher, + private val okHttpClient: OkHttpClient, ) { @Throws(Exception::class) suspend fun searchFeed(feedLink: String): FeedWithArticle { @@ -58,7 +57,7 @@ class RssHelper @Inject constructor( @Throws(Exception::class) suspend fun parseFullContent(link: String, title: String): String { return withContext(dispatcherIO) { - val response = OkHttpClient() + val response = okHttpClient .newCall(Request.Builder().url(link).build()) .execute() val content = response.body!!.string() @@ -85,7 +84,12 @@ class RssHelper @Inject constructor( return withContext(dispatcherIO) { val a = mutableListOf
() val accountId = context.currentAccountId - val parseRss: SyndFeed = SyndFeedInput().build(XmlReader(URL(feed.url))) + val parseRss: SyndFeed = SyndFeedInput().build( + XmlReader(URL(feed.url).openConnection().apply { + connectTimeout = 5000 + readTimeout = 5000 + }) + ) parseRss.entries.forEach { if (latestLink != null && latestLink == it.link) return@withContext a val desc = it.description?.value @@ -110,13 +114,13 @@ class RssHelper @Inject constructor( date = it.publishedDate ?: it.updatedDate ?: Date(), title = Html.fromHtml(it.title.toString()).toString(), author = it.author, - rawDescription = (desc ?: content) ?: "", + rawDescription = (content ?: desc) ?: "", shortDescription = (Readability4JExtended("", desc ?: content ?: "") .parse().textContent ?: "") .take(100) .trim(), fullContent = content, - img = findImg((desc ?: content) ?: ""), + img = findImg((content ?: desc) ?: ""), link = it.link ?: "", ) ) @@ -182,27 +186,4 @@ class RssHelper @Inject constructor( } ) } - - private fun parseDate( - inputDate: String, patterns: Array = 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 - } } \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/data/repository/StringsRepository.kt b/app/src/main/java/me/ash/reader/data/repository/StringsRepository.kt index fa8d76b..5497c77 100644 --- a/app/src/main/java/me/ash/reader/data/repository/StringsRepository.kt +++ b/app/src/main/java/me/ash/reader/data/repository/StringsRepository.kt @@ -11,6 +11,13 @@ class StringsRepository @Inject constructor( private val context: Context, ) { fun getString(resId: Int, vararg formatArgs: Any) = context.getString(resId, *formatArgs) - fun getQuantityString(resId: Int, quantity: Int, vararg formatArgs: Any) = context.resources.getQuantityString(resId, quantity, *formatArgs) - fun formatAsString(date: Date?) = date?.formatAsString(context) + + fun getQuantityString(resId: Int, quantity: Int, vararg formatArgs: Any) = + context.resources.getQuantityString(resId, quantity, *formatArgs) + + fun formatAsString( + date: Date?, + onlyHourMinute: Boolean? = false, + atHourMinute: Boolean? = false + ) = date?.formatAsString(context, onlyHourMinute, atHourMinute) } diff --git a/app/src/main/java/me/ash/reader/data/repository/SyncWorker.kt b/app/src/main/java/me/ash/reader/data/repository/SyncWorker.kt new file mode 100644 index 0000000..50cdaf2 --- /dev/null +++ b/app/src/main/java/me/ash/reader/data/repository/SyncWorker.kt @@ -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( + 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) + } +} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/data/source/ReaderDatabase.kt b/app/src/main/java/me/ash/reader/data/source/RYDatabase.kt similarity index 86% rename from app/src/main/java/me/ash/reader/data/source/ReaderDatabase.kt rename to app/src/main/java/me/ash/reader/data/source/RYDatabase.kt index 2fa0971..e3cf064 100644 --- a/app/src/main/java/me/ash/reader/data/source/ReaderDatabase.kt +++ b/app/src/main/java/me/ash/reader/data/source/RYDatabase.kt @@ -18,21 +18,21 @@ import java.util.* entities = [Account::class, Feed::class, Article::class, Group::class], version = 2, ) -@TypeConverters(ReaderDatabase.Converters::class) -abstract class ReaderDatabase : RoomDatabase() { +@TypeConverters(RYDatabase.Converters::class) +abstract class RYDatabase : RoomDatabase() { abstract fun accountDao(): AccountDao abstract fun feedDao(): FeedDao abstract fun articleDao(): ArticleDao abstract fun groupDao(): GroupDao companion object { - private var instance: ReaderDatabase? = null + private var instance: RYDatabase? = null - fun getInstance(context: Context): ReaderDatabase { + fun getInstance(context: Context): RYDatabase { return instance ?: synchronized(this) { instance ?: Room.databaseBuilder( context.applicationContext, - ReaderDatabase::class.java, + RYDatabase::class.java, "Reader" ).addMigrations(*allMigrations).build().also { instance = it diff --git a/app/src/main/java/me/ash/reader/data/source/AppNetworkDataSource.kt b/app/src/main/java/me/ash/reader/data/source/RYNetworkDataSource.kt similarity index 75% rename from app/src/main/java/me/ash/reader/data/source/AppNetworkDataSource.kt rename to app/src/main/java/me/ash/reader/data/source/RYNetworkDataSource.kt index a37d951..f1e4fdc 100644 --- a/app/src/main/java/me/ash/reader/data/source/AppNetworkDataSource.kt +++ b/app/src/main/java/me/ash/reader/data/source/RYNetworkDataSource.kt @@ -5,7 +5,6 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn -import me.ash.reader.data.entity.LatestRelease import okhttp3.ResponseBody import retrofit2.Response import retrofit2.Retrofit @@ -15,13 +14,7 @@ import retrofit2.http.Streaming import retrofit2.http.Url import java.io.File -sealed class Download { - object NotYet : Download() - data class Progress(val percent: Int) : Download() - data class Finished(val file: File) : Download() -} - -interface AppNetworkDataSource { +interface RYNetworkDataSource { @GET suspend fun getReleaseLatest(@Url url: String): Response @@ -30,14 +23,14 @@ interface AppNetworkDataSource { suspend fun downloadFile(@Url url: String): ResponseBody companion object { - private var instance: AppNetworkDataSource? = null + private var instance: RYNetworkDataSource? = null - fun getInstance(): AppNetworkDataSource { + fun getInstance(): RYNetworkDataSource { return instance ?: synchronized(this) { instance ?: Retrofit.Builder() .baseUrl("https://api.github.com/") .addConverterFactory(GsonConverterFactory.create()) - .build().create(AppNetworkDataSource::class.java).also { + .build().create(RYNetworkDataSource::class.java).also { instance = it } } @@ -92,4 +85,32 @@ fun ResponseBody.downloadToFileWithProgress(saveFile: File): Flow = saveFile.delete() } } - }.flowOn(Dispatchers.IO).distinctUntilChanged() \ No newline at end of file + }.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? = 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() +} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/page/home/FeedIcon.kt b/app/src/main/java/me/ash/reader/ui/component/FeedIcon.kt similarity index 97% rename from app/src/main/java/me/ash/reader/ui/page/home/FeedIcon.kt rename to app/src/main/java/me/ash/reader/ui/component/FeedIcon.kt index e52288c..d8b0030 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/FeedIcon.kt +++ b/app/src/main/java/me/ash/reader/ui/component/FeedIcon.kt @@ -1,4 +1,4 @@ -package me.ash.reader.ui.page.home +package me.ash.reader.ui.component import androidx.compose.foundation.background import androidx.compose.foundation.layout.Row diff --git a/app/src/main/java/me/ash/reader/ui/page/home/FilterBar.kt b/app/src/main/java/me/ash/reader/ui/component/FilterBar.kt similarity index 92% rename from app/src/main/java/me/ash/reader/ui/page/home/FilterBar.kt rename to app/src/main/java/me/ash/reader/ui/component/FilterBar.kt index a9307e7..f0d29d9 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/FilterBar.kt +++ b/app/src/main/java/me/ash/reader/ui/component/FilterBar.kt @@ -1,4 +1,4 @@ -package me.ash.reader.ui.page.home +package me.ash.reader.ui.component import android.os.Build import android.view.SoundEffectConstants @@ -12,16 +12,15 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalView import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp -import me.ash.reader.data.entity.Filter +import me.ash.reader.data.model.Filter +import me.ash.reader.data.model.getName import me.ash.reader.data.preference.FlowFilterBarStylePreference import me.ash.reader.data.preference.LocalThemeIndex -import me.ash.reader.ui.ext.getName import me.ash.reader.ui.ext.surfaceColorAtElevation import me.ash.reader.ui.theme.palette.onDark @Composable fun FilterBar( - modifier: Modifier = Modifier, filter: Filter, filterBarStyle: Int, filterBarFilled: Boolean, @@ -39,11 +38,7 @@ fun FilterBar( tonalElevation = filterBarTonalElevation, ) { Spacer(modifier = Modifier.width(filterBarPadding)) - listOf( - Filter.Starred, - Filter.Unread, - Filter.All, - ).forEach { item -> + Filter.values.forEach { item -> NavigationBarItem( // modifier = Modifier.height(60.dp), alwaysShowLabel = when (filterBarStyle) { diff --git a/app/src/main/java/me/ash/reader/ui/component/AnimatedPopup.kt b/app/src/main/java/me/ash/reader/ui/component/base/AnimatedPopup.kt similarity index 85% rename from app/src/main/java/me/ash/reader/ui/component/AnimatedPopup.kt rename to app/src/main/java/me/ash/reader/ui/component/base/AnimatedPopup.kt index f294bf6..9b873e6 100644 --- a/app/src/main/java/me/ash/reader/ui/component/AnimatedPopup.kt +++ b/app/src/main/java/me/ash/reader/ui/component/base/AnimatedPopup.kt @@ -1,6 +1,6 @@ -package me.ash.reader.ui.component +package me.ash.reader.ui.component.base -import androidx.compose.animation.* +import RYExtensibleVisibility import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.statusBars import androidx.compose.runtime.Composable @@ -38,11 +38,7 @@ fun AnimatedPopup( } }, ) { - AnimatedVisibility( - visible = visible, - enter = fadeIn() + expandVertically(), - exit = fadeOut() + shrinkVertically(), - ) { + RYExtensibleVisibility(visible = visible) { content() } } diff --git a/app/src/main/java/me/ash/reader/ui/component/AnimatedText.kt b/app/src/main/java/me/ash/reader/ui/component/base/AnimatedText.kt similarity index 98% rename from app/src/main/java/me/ash/reader/ui/component/AnimatedText.kt rename to app/src/main/java/me/ash/reader/ui/component/base/AnimatedText.kt index 614543c..7c7e76c 100644 --- a/app/src/main/java/me/ash/reader/ui/component/AnimatedText.kt +++ b/app/src/main/java/me/ash/reader/ui/component/base/AnimatedText.kt @@ -1,4 +1,4 @@ -package me.ash.reader.ui.component +package me.ash.reader.ui.component.base import androidx.compose.animation.* import androidx.compose.animation.core.FastOutSlowInEasing diff --git a/app/src/main/java/me/ash/reader/ui/component/Banner.kt b/app/src/main/java/me/ash/reader/ui/component/base/Banner.kt similarity index 98% rename from app/src/main/java/me/ash/reader/ui/component/Banner.kt rename to app/src/main/java/me/ash/reader/ui/component/base/Banner.kt index 3e235bf..e9d2807 100644 --- a/app/src/main/java/me/ash/reader/ui/component/Banner.kt +++ b/app/src/main/java/me/ash/reader/ui/component/base/Banner.kt @@ -6,7 +6,7 @@ * @modifier Ashinch */ -package me.ash.reader.ui.component +package me.ash.reader.ui.component.base import android.view.SoundEffectConstants import androidx.compose.animation.Crossfade diff --git a/app/src/main/java/me/ash/reader/ui/component/BlockButton.kt b/app/src/main/java/me/ash/reader/ui/component/base/BlockButton.kt similarity index 97% rename from app/src/main/java/me/ash/reader/ui/component/BlockButton.kt rename to app/src/main/java/me/ash/reader/ui/component/base/BlockButton.kt index 433b7e7..a73a361 100644 --- a/app/src/main/java/me/ash/reader/ui/component/BlockButton.kt +++ b/app/src/main/java/me/ash/reader/ui/component/base/BlockButton.kt @@ -1,4 +1,4 @@ -package me.ash.reader.ui.component +package me.ash.reader.ui.component.base import androidx.compose.foundation.background import androidx.compose.foundation.clickable diff --git a/app/src/main/java/me/ash/reader/ui/component/BlockRadioButton.kt b/app/src/main/java/me/ash/reader/ui/component/base/BlockRadioButton.kt similarity index 95% rename from app/src/main/java/me/ash/reader/ui/component/BlockRadioButton.kt rename to app/src/main/java/me/ash/reader/ui/component/base/BlockRadioButton.kt index d1633bb..533f643 100644 --- a/app/src/main/java/me/ash/reader/ui/component/BlockRadioButton.kt +++ b/app/src/main/java/me/ash/reader/ui/component/base/BlockRadioButton.kt @@ -1,4 +1,4 @@ -package me.ash.reader.ui.component +package me.ash.reader.ui.component.base import androidx.compose.foundation.layout.* import androidx.compose.runtime.Composable @@ -7,7 +7,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @Composable -fun BlockRadioGroupButton( +fun BlockRadioButton( modifier: Modifier = Modifier, selected: Int = 0, onSelected: (Int) -> Unit, diff --git a/app/src/main/java/me/ash/reader/ui/component/BottomDrawer.kt b/app/src/main/java/me/ash/reader/ui/component/base/BottomDrawer.kt similarity index 98% rename from app/src/main/java/me/ash/reader/ui/component/BottomDrawer.kt rename to app/src/main/java/me/ash/reader/ui/component/base/BottomDrawer.kt index ff09f00..095f61c 100644 --- a/app/src/main/java/me/ash/reader/ui/component/BottomDrawer.kt +++ b/app/src/main/java/me/ash/reader/ui/component/base/BottomDrawer.kt @@ -1,4 +1,4 @@ -package me.ash.reader.ui.component +package me.ash.reader.ui.component.base import androidx.compose.foundation.background import androidx.compose.foundation.layout.* diff --git a/app/src/main/java/me/ash/reader/ui/component/CanBeDisabledIconButton.kt b/app/src/main/java/me/ash/reader/ui/component/base/CanBeDisabledIconButton.kt similarity index 96% rename from app/src/main/java/me/ash/reader/ui/component/CanBeDisabledIconButton.kt rename to app/src/main/java/me/ash/reader/ui/component/base/CanBeDisabledIconButton.kt index 9d5852d..10388f9 100644 --- a/app/src/main/java/me/ash/reader/ui/component/CanBeDisabledIconButton.kt +++ b/app/src/main/java/me/ash/reader/ui/component/base/CanBeDisabledIconButton.kt @@ -1,4 +1,4 @@ -package me.ash.reader.ui.component +package me.ash.reader.ui.component.base import androidx.compose.foundation.layout.size import androidx.compose.material3.Icon diff --git a/app/src/main/java/me/ash/reader/ui/component/ClipboardTextField.kt b/app/src/main/java/me/ash/reader/ui/component/base/ClipboardTextField.kt similarity index 98% rename from app/src/main/java/me/ash/reader/ui/component/ClipboardTextField.kt rename to app/src/main/java/me/ash/reader/ui/component/base/ClipboardTextField.kt index 90dcd00..cff26ee 100644 --- a/app/src/main/java/me/ash/reader/ui/component/ClipboardTextField.kt +++ b/app/src/main/java/me/ash/reader/ui/component/base/ClipboardTextField.kt @@ -1,4 +1,4 @@ -package me.ash.reader.ui.component +package me.ash.reader.ui.component.base import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Column @@ -32,7 +32,7 @@ fun ClipboardTextField( ) { Column(modifier = modifier) { Spacer(modifier = Modifier.height(10.dp)) - TextField( + RYTextField( readOnly = readOnly, value = value, onValueChange = onValueChange, diff --git a/app/src/main/java/me/ash/reader/ui/component/CurlyCornerShape.kt b/app/src/main/java/me/ash/reader/ui/component/base/CurlyCornerShape.kt similarity index 98% rename from app/src/main/java/me/ash/reader/ui/component/CurlyCornerShape.kt rename to app/src/main/java/me/ash/reader/ui/component/base/CurlyCornerShape.kt index 39821e3..0cf4df3 100644 --- a/app/src/main/java/me/ash/reader/ui/component/CurlyCornerShape.kt +++ b/app/src/main/java/me/ash/reader/ui/component/base/CurlyCornerShape.kt @@ -1,4 +1,4 @@ -package me.ash.reader.ui.component +package me.ash.reader.ui.component.base import androidx.compose.foundation.shape.CornerBasedShape import androidx.compose.foundation.shape.CornerSize diff --git a/app/src/main/java/me/ash/reader/ui/component/DisplayText.kt b/app/src/main/java/me/ash/reader/ui/component/base/DisplayText.kt similarity index 87% rename from app/src/main/java/me/ash/reader/ui/component/DisplayText.kt rename to app/src/main/java/me/ash/reader/ui/component/base/DisplayText.kt index a2fffee..56ee1a5 100644 --- a/app/src/main/java/me/ash/reader/ui/component/DisplayText.kt +++ b/app/src/main/java/me/ash/reader/ui/component/base/DisplayText.kt @@ -1,6 +1,6 @@ -package me.ash.reader.ui.component +package me.ash.reader.ui.component.base -import androidx.compose.animation.* +import RYExtensibleVisibility import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -41,11 +41,7 @@ fun DisplayText( maxLines = 1, overflow = TextOverflow.Ellipsis, ) - AnimatedVisibility( - visible = desc.isNotEmpty(), - enter = fadeIn() + expandVertically(), - exit = fadeOut() + shrinkVertically(), - ) { + RYExtensibleVisibility(visible = desc.isNotEmpty()) { Text( modifier = Modifier.height(16.dp), text = desc, diff --git a/app/src/main/java/me/ash/reader/ui/component/DynamicSVGImage.kt b/app/src/main/java/me/ash/reader/ui/component/base/DynamicSVGImage.kt similarity index 95% rename from app/src/main/java/me/ash/reader/ui/component/DynamicSVGImage.kt rename to app/src/main/java/me/ash/reader/ui/component/base/DynamicSVGImage.kt index 5b93670..1240cd5 100644 --- a/app/src/main/java/me/ash/reader/ui/component/DynamicSVGImage.kt +++ b/app/src/main/java/me/ash/reader/ui/component/base/DynamicSVGImage.kt @@ -1,4 +1,4 @@ -package me.ash.reader.ui.component +package me.ash.reader.ui.component.base import android.graphics.drawable.PictureDrawable import androidx.compose.animation.Crossfade @@ -41,7 +41,7 @@ fun DynamicSVGImage( }, ) { Crossfade(targetState = pic) { - AsyncImage( + RYAsyncImage( contentDescription = contentDescription, data = it, placeholder = null, diff --git a/app/src/main/java/me/ash/reader/ui/component/FeedbackIconButton.kt b/app/src/main/java/me/ash/reader/ui/component/base/FeedbackIconButton.kt similarity index 98% rename from app/src/main/java/me/ash/reader/ui/component/FeedbackIconButton.kt rename to app/src/main/java/me/ash/reader/ui/component/base/FeedbackIconButton.kt index 9c2433d..20c8318 100644 --- a/app/src/main/java/me/ash/reader/ui/component/FeedbackIconButton.kt +++ b/app/src/main/java/me/ash/reader/ui/component/base/FeedbackIconButton.kt @@ -1,4 +1,4 @@ -package me.ash.reader.ui.component +package me.ash.reader.ui.component.base import android.view.HapticFeedbackConstants import android.view.SoundEffectConstants diff --git a/app/src/main/java/me/ash/reader/ui/component/Menu.kt b/app/src/main/java/me/ash/reader/ui/component/base/Menu.kt similarity index 97% rename from app/src/main/java/me/ash/reader/ui/component/Menu.kt rename to app/src/main/java/me/ash/reader/ui/component/base/Menu.kt index 984b225..1b41382 100644 --- a/app/src/main/java/me/ash/reader/ui/component/Menu.kt +++ b/app/src/main/java/me/ash/reader/ui/component/base/Menu.kt @@ -1,4 +1,4 @@ -package me.ash.reader.ui.component +package me.ash.reader.ui.component.base import androidx.compose.foundation.layout.Box import androidx.compose.material3.DropdownMenu diff --git a/app/src/main/java/me/ash/reader/ui/component/AsyncImage.kt b/app/src/main/java/me/ash/reader/ui/component/base/RYAsyncImage.kt similarity index 64% rename from app/src/main/java/me/ash/reader/ui/component/AsyncImage.kt rename to app/src/main/java/me/ash/reader/ui/component/base/RYAsyncImage.kt index 8ac3cb9..cfd4959 100644 --- a/app/src/main/java/me/ash/reader/ui/component/AsyncImage.kt +++ b/app/src/main/java/me/ash/reader/ui/component/base/RYAsyncImage.kt @@ -1,7 +1,7 @@ -package me.ash.reader.ui.component +package me.ash.reader.ui.component.base import androidx.annotation.DrawableRes -import androidx.compose.material3.MaterialTheme +import androidx.compose.foundation.Image import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.ui.Modifier @@ -10,19 +10,16 @@ import androidx.compose.ui.graphics.DefaultAlpha import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import coil.compose.LocalImageLoader -import coil.request.ImageRequest +import coil.compose.rememberImagePainter import coil.size.Precision import coil.size.Scale import coil.size.Size import me.ash.reader.R -val Size_1000 = Size(1000, 1000) +val SIZE_1000 = Size(1000, 1000) @Composable -fun AsyncImage( +fun RYAsyncImage( modifier: Modifier = Modifier, data: Any? = null, size: Size = Size.ORIGINAL, @@ -33,34 +30,51 @@ fun AsyncImage( @DrawableRes placeholder: Int? = R.drawable.ic_hourglass_empty_black_24dp, @DrawableRes error: Int? = R.drawable.ic_broken_image_black_24dp, ) { - coil.compose.AsyncImage( - modifier = modifier, - model = ImageRequest - .Builder(LocalContext.current) - .data(data) - .crossfade(true) - .scale(scale) - .precision(precision) - .size(size) - .build(), + Image( + painter = rememberImagePainter( + data = data, + builder = { + if (placeholder != null) placeholder(placeholder) + if (error != null) error(error) + crossfade(true) + scale(scale) + precision(precision) + size(size) + }, + ), contentDescription = contentDescription, contentScale = contentScale, - imageLoader = LocalImageLoader.current, - placeholder = placeholder?.run { - forwardingPainter( - painter = painterResource(this), - colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurfaceVariant), - alpha = 0.1f, - ) - }, - error = error?.run { - forwardingPainter( - painter = painterResource(this), - colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurfaceVariant), - alpha = 0.1f, - ) - }, + modifier = modifier, ) + +// coil.compose.AsyncImage( +// modifier = modifier, +// model = ImageRequest +// .Builder(LocalContext.current) +// .data(data) +// .crossfade(true) +// .scale(scale) +// .precision(precision) +// .size(size) +// .build(), +// contentDescription = contentDescription, +// contentScale = contentScale, +// imageLoader = LocalImageLoader.current, +// placeholder = placeholder?.run { +// forwardingPainter( +// painter = painterResource(this), +// colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurfaceVariant), +// alpha = 0.1f, +// ) +// }, +// error = error?.run { +// forwardingPainter( +// painter = painterResource(this), +// colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurfaceVariant), +// alpha = 0.1f, +// ) +// }, +// ) } // From: https://gist.github.com/colinrtwhite/c2966e0b8584b4cdf0a5b05786b20ae1 diff --git a/app/src/main/java/me/ash/reader/ui/component/Dialog.kt b/app/src/main/java/me/ash/reader/ui/component/base/RYDialog.kt similarity index 94% rename from app/src/main/java/me/ash/reader/ui/component/Dialog.kt rename to app/src/main/java/me/ash/reader/ui/component/base/RYDialog.kt index 5d90164..00d70f0 100644 --- a/app/src/main/java/me/ash/reader/ui/component/Dialog.kt +++ b/app/src/main/java/me/ash/reader/ui/component/base/RYDialog.kt @@ -1,4 +1,4 @@ -package me.ash.reader.ui.component +package me.ash.reader.ui.component.base import androidx.compose.material3.AlertDialog import androidx.compose.runtime.Composable @@ -6,7 +6,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.window.DialogProperties @Composable -fun Dialog( +fun RYDialog( modifier: Modifier = Modifier, visible: Boolean, properties: DialogProperties = DialogProperties(), diff --git a/app/src/main/java/me/ash/reader/ui/component/base/RYExtensibleVisibility.kt b/app/src/main/java/me/ash/reader/ui/component/base/RYExtensibleVisibility.kt new file mode 100644 index 0000000..b1485a5 --- /dev/null +++ b/app/src/main/java/me/ash/reader/ui/component/base/RYExtensibleVisibility.kt @@ -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, + ) +} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/component/base/RYScaffold.kt b/app/src/main/java/me/ash/reader/ui/component/base/RYScaffold.kt new file mode 100644 index 0000000..f707357 --- /dev/null +++ b/app/src/main/java/me/ash/reader/ui/component/base/RYScaffold.kt @@ -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() }, + ) +} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/component/SelectionChip.kt b/app/src/main/java/me/ash/reader/ui/component/base/RYSelectionChip.kt similarity index 97% rename from app/src/main/java/me/ash/reader/ui/component/SelectionChip.kt rename to app/src/main/java/me/ash/reader/ui/component/base/RYSelectionChip.kt index 5da94a1..68b14a9 100644 --- a/app/src/main/java/me/ash/reader/ui/component/SelectionChip.kt +++ b/app/src/main/java/me/ash/reader/ui/component/base/RYSelectionChip.kt @@ -1,4 +1,4 @@ -package me.ash.reader.ui.component +package me.ash.reader.ui.component.base import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.interaction.MutableInteractionSource @@ -26,7 +26,7 @@ import me.ash.reader.ui.theme.palette.alwaysLight @OptIn(ExperimentalMaterialApi::class) @Composable -fun SelectionChip( +fun RYSelectionChip( content: String, selected: Boolean, modifier: Modifier = Modifier, diff --git a/app/src/main/java/me/ash/reader/ui/component/Switch.kt b/app/src/main/java/me/ash/reader/ui/component/base/RYSwitch.kt similarity index 97% rename from app/src/main/java/me/ash/reader/ui/component/Switch.kt rename to app/src/main/java/me/ash/reader/ui/component/base/RYSwitch.kt index 53b545c..ce9b972 100644 --- a/app/src/main/java/me/ash/reader/ui/component/Switch.kt +++ b/app/src/main/java/me/ash/reader/ui/component/base/RYSwitch.kt @@ -6,7 +6,7 @@ * @modifier Ashinch */ -package me.ash.reader.ui.component +package me.ash.reader.ui.component.base import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.animateDpAsState @@ -31,7 +31,7 @@ import me.ash.reader.ui.theme.palette.onDark // TODO: ripple & swipe @Composable -fun Switch( +fun RYSwitch( modifier: Modifier = Modifier, activated: Boolean, enable: Boolean = true, @@ -101,7 +101,7 @@ fun SwitchHeadline( ) } Box(Modifier.padding(start = 20.dp)) { - Switch(activated = activated) + RYSwitch(activated = activated) } } } diff --git a/app/src/main/java/me/ash/reader/ui/component/TextField.kt b/app/src/main/java/me/ash/reader/ui/component/base/RYTextField.kt similarity index 96% rename from app/src/main/java/me/ash/reader/ui/component/TextField.kt rename to app/src/main/java/me/ash/reader/ui/component/base/RYTextField.kt index 63f11d2..e881e88 100644 --- a/app/src/main/java/me/ash/reader/ui/component/TextField.kt +++ b/app/src/main/java/me/ash/reader/ui/component/base/RYTextField.kt @@ -1,4 +1,4 @@ -package me.ash.reader.ui.component +package me.ash.reader.ui.component.base import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions @@ -19,7 +19,7 @@ import kotlinx.coroutines.delay import me.ash.reader.R @Composable -fun TextField( +fun RYTextField( readOnly: Boolean, value: String, onValueChange: (String) -> Unit, @@ -39,7 +39,7 @@ fun TextField( TextField( modifier = Modifier.focusRequester(focusRequester), colors = TextFieldDefaults.textFieldColors( - backgroundColor = Color.Transparent, + containerColor = Color.Transparent, ), maxLines = 1, enabled = !readOnly, diff --git a/app/src/main/java/me/ash/reader/ui/component/RadioDialog.kt b/app/src/main/java/me/ash/reader/ui/component/base/RadioDialog.kt similarity index 94% rename from app/src/main/java/me/ash/reader/ui/component/RadioDialog.kt rename to app/src/main/java/me/ash/reader/ui/component/base/RadioDialog.kt index ff1608c..8363208 100644 --- a/app/src/main/java/me/ash/reader/ui/component/RadioDialog.kt +++ b/app/src/main/java/me/ash/reader/ui/component/base/RadioDialog.kt @@ -1,4 +1,4 @@ -package me.ash.reader.ui.component +package me.ash.reader.ui.component.base import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Row @@ -19,9 +19,8 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.text.style.BaselineShift import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import com.google.accompanist.pager.ExperimentalPagerApi -@OptIn(ExperimentalPagerApi::class, ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class) @Composable fun RadioDialog( modifier: Modifier = Modifier, @@ -30,7 +29,7 @@ fun RadioDialog( options: List = emptyList(), onDismissRequest: () -> Unit = {}, ) { - Dialog( + RYDialog( modifier = modifier, visible = visible, onDismissRequest = onDismissRequest, diff --git a/app/src/main/java/me/ash/reader/ui/component/SubTitle.kt b/app/src/main/java/me/ash/reader/ui/component/base/SubTitle.kt similarity index 94% rename from app/src/main/java/me/ash/reader/ui/component/SubTitle.kt rename to app/src/main/java/me/ash/reader/ui/component/base/SubTitle.kt index ec50ae2..f45a5cb 100644 --- a/app/src/main/java/me/ash/reader/ui/component/SubTitle.kt +++ b/app/src/main/java/me/ash/reader/ui/component/base/SubTitle.kt @@ -1,4 +1,4 @@ -package me.ash.reader.ui.component +package me.ash.reader.ui.component.base import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding diff --git a/app/src/main/java/me/ash/reader/ui/component/SwipeRefresh.kt b/app/src/main/java/me/ash/reader/ui/component/base/SwipeRefresh.kt similarity index 96% rename from app/src/main/java/me/ash/reader/ui/component/SwipeRefresh.kt rename to app/src/main/java/me/ash/reader/ui/component/base/SwipeRefresh.kt index 0664f3a..5e2d311 100644 --- a/app/src/main/java/me/ash/reader/ui/component/SwipeRefresh.kt +++ b/app/src/main/java/me/ash/reader/ui/component/base/SwipeRefresh.kt @@ -1,4 +1,4 @@ -package me.ash.reader.ui.component +package me.ash.reader.ui.component.base import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable diff --git a/app/src/main/java/me/ash/reader/ui/component/TextFieldDialog.kt b/app/src/main/java/me/ash/reader/ui/component/base/TextFieldDialog.kt similarity index 95% rename from app/src/main/java/me/ash/reader/ui/component/TextFieldDialog.kt rename to app/src/main/java/me/ash/reader/ui/component/base/TextFieldDialog.kt index 4609764..4646188 100644 --- a/app/src/main/java/me/ash/reader/ui/component/TextFieldDialog.kt +++ b/app/src/main/java/me/ash/reader/ui/component/base/TextFieldDialog.kt @@ -1,4 +1,4 @@ -package me.ash.reader.ui.component +package me.ash.reader.ui.component.base import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -13,10 +13,8 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.window.DialogProperties -import com.google.accompanist.pager.ExperimentalPagerApi import me.ash.reader.R -@OptIn(ExperimentalPagerApi::class) @Composable fun TextFieldDialog( modifier: Modifier = Modifier, @@ -37,7 +35,7 @@ fun TextFieldDialog( ) { val focusManager = LocalFocusManager.current - Dialog( + RYDialog( modifier = modifier, visible = visible, onDismissRequest = onDismissRequest, diff --git a/app/src/main/java/me/ash/reader/ui/component/Tips.kt b/app/src/main/java/me/ash/reader/ui/component/base/Tips.kt similarity index 96% rename from app/src/main/java/me/ash/reader/ui/component/Tips.kt rename to app/src/main/java/me/ash/reader/ui/component/base/Tips.kt index bab7833..4f9d519 100644 --- a/app/src/main/java/me/ash/reader/ui/component/Tips.kt +++ b/app/src/main/java/me/ash/reader/ui/component/base/Tips.kt @@ -1,4 +1,4 @@ -package me.ash.reader.ui.component +package me.ash.reader.ui.component.base import androidx.compose.foundation.layout.* import androidx.compose.material.icons.Icons diff --git a/app/src/main/java/me/ash/reader/ui/component/ViewPager.kt b/app/src/main/java/me/ash/reader/ui/component/base/ViewPager.kt similarity index 96% rename from app/src/main/java/me/ash/reader/ui/component/ViewPager.kt rename to app/src/main/java/me/ash/reader/ui/component/base/ViewPager.kt index 4836c07..599c027 100644 --- a/app/src/main/java/me/ash/reader/ui/component/ViewPager.kt +++ b/app/src/main/java/me/ash/reader/ui/component/base/ViewPager.kt @@ -1,4 +1,4 @@ -package me.ash.reader.ui.component +package me.ash.reader.ui.component.base import androidx.compose.animation.animateContentSize import androidx.compose.foundation.layout.fillMaxWidth diff --git a/app/src/main/java/me/ash/reader/ui/component/WebView.kt b/app/src/main/java/me/ash/reader/ui/component/base/WebView.kt similarity index 99% rename from app/src/main/java/me/ash/reader/ui/component/WebView.kt rename to app/src/main/java/me/ash/reader/ui/component/base/WebView.kt index ea2634a..ae3cf06 100644 --- a/app/src/main/java/me/ash/reader/ui/component/WebView.kt +++ b/app/src/main/java/me/ash/reader/ui/component/base/WebView.kt @@ -1,4 +1,4 @@ -package me.ash.reader.ui.component +package me.ash.reader.ui.component.base import android.content.Intent import android.net.Uri diff --git a/app/src/main/java/me/ash/reader/ui/component/reader/HtmlToComposable.kt b/app/src/main/java/me/ash/reader/ui/component/reader/HtmlToComposable.kt index da8c1f1..1d98501 100644 --- a/app/src/main/java/me/ash/reader/ui/component/reader/HtmlToComposable.kt +++ b/app/src/main/java/me/ash/reader/ui/component/reader/HtmlToComposable.kt @@ -33,7 +33,6 @@ import androidx.compose.foundation.text.selection.DisableSelection import androidx.compose.material.Text import androidx.compose.material3.Surface import androidx.compose.runtime.Composable -import androidx.compose.runtime.ExperimentalComposeApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.RectangleShape @@ -48,12 +47,11 @@ import androidx.compose.ui.text.style.BaselineShift import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import coil.annotation.ExperimentalCoilApi import coil.size.Precision import coil.size.Size import coil.size.pxOrElse import me.ash.reader.R -import me.ash.reader.ui.component.AsyncImage +import me.ash.reader.ui.component.base.RYAsyncImage import org.jsoup.Jsoup import org.jsoup.helper.StringUtil import org.jsoup.nodes.Element @@ -178,7 +176,6 @@ private fun LazyListScope.formatCodeBlock( composer.terminateCurrentText() } -@OptIn(ExperimentalComposeApi::class, ExperimentalCoilApi::class) private fun TextComposer.appendTextChildren( nodes: List, preFormatted: Boolean = false, @@ -241,7 +238,7 @@ private fun TextComposer.appendTextChildren( withComposableStyle( style = { h5Style().toSpanStyle() } ) { - append(element.text()) + append("\n${element.text()}") } } } @@ -250,7 +247,7 @@ private fun TextComposer.appendTextChildren( withComposableStyle( style = { h5Style().toSpanStyle() } ) { - append(element.text()) + append("\n${element.text()}") } } } @@ -259,7 +256,7 @@ private fun TextComposer.appendTextChildren( withComposableStyle( style = { h5Style().toSpanStyle() } ) { - append(element.text()) + append("\n${element.text()}") } } } @@ -268,7 +265,7 @@ private fun TextComposer.appendTextChildren( withComposableStyle( style = { h5Style().toSpanStyle() } ) { - append(element.text()) + append("\n${element.text()}") } } } @@ -277,7 +274,7 @@ private fun TextComposer.appendTextChildren( withComposableStyle( style = { h5Style().toSpanStyle() } ) { - append(element.text()) + append("\n${element.text()}") } } } @@ -286,7 +283,7 @@ private fun TextComposer.appendTextChildren( withComposableStyle( style = { h5Style().toSpanStyle() } ) { - append(element.text()) + append("\n${element.text()}") } } } @@ -445,6 +442,7 @@ private fun TextComposer.appendTextChildren( // .padding(horizontal = PADDING_HORIZONTAL.dp) .width(MAX_CONTENT_WIDTH.dp) ) { + Spacer(modifier = Modifier.height(PADDING_HORIZONTAL.dp)) DisableSelection { BoxWithConstraints( modifier = Modifier @@ -468,8 +466,12 @@ private fun TextComposer.appendTextChildren( // } ) { val imageSize = maxImageSize() - AsyncImage( - modifier = Modifier.fillMaxWidth(), + RYAsyncImage( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = PADDING_HORIZONTAL.dp) + .clip(IMAGE_SHAPE) + .clickable { }, data = imageCandidates.getBestImageForMaxSize( pixelDensity = pixelDensity(), maxSize = imageSize, @@ -594,12 +596,14 @@ private fun TextComposer.appendTextChildren( BoxWithConstraints( modifier = Modifier.fillMaxWidth() ) { - AsyncImage( + RYAsyncImage( modifier = Modifier + .fillMaxWidth() + .padding(horizontal = PADDING_HORIZONTAL.dp) + .clip(IMAGE_SHAPE) .clickable { onLinkClick(video.link) - } - .fillMaxWidth(), + }, data = video.imageUrl, size = maxImageSize(), contentDescription = stringResource(R.string.touch_to_play_video), @@ -646,7 +650,6 @@ private fun TextComposer.appendTextChildren( } } -@OptIn(ExperimentalStdlibApi::class) private fun String.asFontFamily(): FontFamily? = when (this.lowercase()) { "monospace" -> FontFamily.Monospace "serif" -> FontFamily.Serif diff --git a/app/src/main/java/me/ash/reader/ui/component/reader/Reader.kt b/app/src/main/java/me/ash/reader/ui/component/reader/Reader.kt index 0a47d3a..464369b 100644 --- a/app/src/main/java/me/ash/reader/ui/component/reader/Reader.kt +++ b/app/src/main/java/me/ash/reader/ui/component/reader/Reader.kt @@ -27,7 +27,8 @@ import android.util.Log import androidx.compose.foundation.lazy.LazyListScope import me.ash.reader.R -fun LazyListScope.reader( +@Suppress("FunctionName") +fun LazyListScope.Reader( context: Context, link: String, content: String, diff --git a/app/src/main/java/me/ash/reader/ui/component/reader/Styles.kt b/app/src/main/java/me/ash/reader/ui/component/reader/Styles.kt index 7ed82e9..b1fa1b1 100644 --- a/app/src/main/java/me/ash/reader/ui/component/reader/Styles.kt +++ b/app/src/main/java/me/ash/reader/ui/component/reader/Styles.kt @@ -20,6 +20,7 @@ package me.ash.reader.ui.component.reader +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color @@ -27,12 +28,14 @@ import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.dp import me.ash.reader.ui.ext.alphaLN const val PADDING_HORIZONTAL = 24.0 const val MAX_CONTENT_WIDTH = 840.0 +val IMAGE_SHAPE = RoundedCornerShape(32.dp) @Composable fun bodyForeground(): Color = @@ -71,7 +74,7 @@ fun h4Style(): TextStyle = @Composable fun h5Style(): TextStyle = MaterialTheme.typography.headlineSmall.copy( - color = bodyForeground() + color = bodyForeground(), ) @Composable @@ -83,7 +86,8 @@ fun h6Style(): TextStyle = @Composable fun captionStyle(): TextStyle = MaterialTheme.typography.bodySmall.copy( - color = bodyForeground().copy(alpha = 0.6f) + color = bodyForeground().copy(alpha = 0.6f), + textAlign = TextAlign.Center, ) @Composable diff --git a/app/src/main/java/me/ash/reader/ui/component/reader/TextComposer.kt b/app/src/main/java/me/ash/reader/ui/component/reader/TextComposer.kt index e38d0e6..f3db0b1 100644 --- a/app/src/main/java/me/ash/reader/ui/component/reader/TextComposer.kt +++ b/app/src/main/java/me/ash/reader/ui/component/reader/TextComposer.kt @@ -79,7 +79,7 @@ class TextComposer( ) -> R ): R { val url = link ?: findClosestLink() - builder.ensureDoubleNewline() + //builder.ensureDoubleNewline() terminateCurrentText() val onClick: (() -> Unit)? = if (url?.isNotBlank() == true) { { diff --git a/app/src/main/java/me/ash/reader/ui/ext/ColorScheme.kt b/app/src/main/java/me/ash/reader/ui/ext/ColorScheme.kt index 1e2276f..a4c388d 100644 --- a/app/src/main/java/me/ash/reader/ui/ext/ColorScheme.kt +++ b/app/src/main/java/me/ash/reader/ui/ext/ColorScheme.kt @@ -1,16 +1,19 @@ package me.ash.reader.ui.ext import androidx.compose.material3.ColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.compositeOver import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import kotlin.math.ln +@Composable fun ColorScheme.surfaceColorAtElevation( elevation: Dp, color: Color = surface, -): Color = color.atElevation(surfaceTint, elevation) +): Color = remember(this, elevation, color) { color.atElevation(surfaceTint, elevation) } fun Color.atElevation( sourceColor: Color, diff --git a/app/src/main/java/me/ash/reader/ui/ext/ContextExt.kt b/app/src/main/java/me/ash/reader/ui/ext/ContextExt.kt index 203e575..8b084ba 100644 --- a/app/src/main/java/me/ash/reader/ui/ext/ContextExt.kt +++ b/app/src/main/java/me/ash/reader/ui/ext/ContextExt.kt @@ -4,11 +4,13 @@ import android.app.Activity import android.content.Context import android.content.ContextWrapper import android.content.Intent +import android.net.Uri import android.util.Log import android.widget.Toast import androidx.core.content.FileProvider -import me.ash.reader.data.entity.Version -import me.ash.reader.data.entity.toVersion +import me.ash.reader.R +import me.ash.reader.data.model.Version +import me.ash.reader.data.model.toVersion import java.io.File fun Context.findActivity(): Activity? = when (this) { @@ -53,4 +55,19 @@ fun Context.showToast(message: String?, duration: Int = Toast.LENGTH_SHORT) { fun Context.showToastLong(message: String?) { showToast(message, Toast.LENGTH_LONG) +} + +fun Context.share(content: String) { + startActivity(Intent.createChooser(Intent(Intent.ACTION_SEND).apply { + putExtra( + Intent.EXTRA_TEXT, + content, + ) + type = "text/plain" + }, getString(R.string.share))) +} + +fun Context.openURL(url: String?) { + url?.takeIf { it.trim().isNotEmpty() } + ?.let { startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(it))) } } \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/ext/DataStoreExt.kt b/app/src/main/java/me/ash/reader/ui/ext/DataStoreExt.kt index 53c3e0c..52ca3b1 100644 --- a/app/src/main/java/me/ash/reader/ui/ext/DataStoreExt.kt +++ b/app/src/main/java/me/ash/reader/ui/ext/DataStoreExt.kt @@ -15,16 +15,6 @@ import java.io.IOException val Context.dataStore: DataStore by preferencesDataStore(name = "settings") -val Context.newVersionPublishDate: String - get() = this.dataStore.get(DataStoreKeys.NewVersionPublishDate) ?: "" -val Context.newVersionLog: String - get() = this.dataStore.get(DataStoreKeys.NewVersionLog) ?: "" -val Context.newVersionSize: Int - get() = this.dataStore.get(DataStoreKeys.NewVersionSize) ?: 0 -val Context.newVersionDownloadUrl: String - get() = this.dataStore.get(DataStoreKeys.NewVersionDownloadUrl) ?: "" -val Context.newVersionNumber: String - get() = this.dataStore.get(DataStoreKeys.NewVersionNumber) ?: "" val Context.skipVersionNumber: String get() = this.dataStore.get(DataStoreKeys.SkipVersionNumber) ?: "" val Context.isFirstLaunch: Boolean @@ -93,9 +83,9 @@ sealed class DataStoreKeys { get() = stringPreferencesKey("newVersionLog") } - object NewVersionSize : DataStoreKeys() { - override val key: Preferences.Key - get() = intPreferencesKey("newVersionSize") + object NewVersionSize : DataStoreKeys() { + override val key: Preferences.Key + get() = stringPreferencesKey("newVersionSizeString") } object NewVersionDownloadUrl : DataStoreKeys() { diff --git a/app/src/main/java/me/ash/reader/ui/ext/DateExt.kt b/app/src/main/java/me/ash/reader/ui/ext/DateExt.kt index 887b285..e609595 100644 --- a/app/src/main/java/me/ash/reader/ui/ext/DateExt.kt +++ b/app/src/main/java/me/ash/reader/ui/ext/DateExt.kt @@ -4,6 +4,7 @@ import android.content.Context import androidx.core.os.ConfigurationCompat import me.ash.reader.R import java.text.DateFormat +import java.text.ParsePosition import java.text.SimpleDateFormat import java.util.* @@ -40,4 +41,27 @@ fun Date.formatAsString( } } } +} + +private fun String.parseToDate( + patterns: Array = 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 } \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/ext/FilterExt.kt b/app/src/main/java/me/ash/reader/ui/ext/FilterExt.kt deleted file mode 100644 index 8a2327a..0000000 --- a/app/src/main/java/me/ash/reader/ui/ext/FilterExt.kt +++ /dev/null @@ -1,13 +0,0 @@ -package me.ash.reader.ui.ext - -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import me.ash.reader.R -import me.ash.reader.data.entity.Filter - -@Composable -fun Filter.getName(): String = when (this) { - Filter.Unread -> stringResource(R.string.unread) - Filter.Starred -> stringResource(R.string.starred) - else -> stringResource(R.string.all) -} diff --git a/app/src/main/java/me/ash/reader/ui/ext/FlavorExt.kt b/app/src/main/java/me/ash/reader/ui/ext/FlavorExt.kt new file mode 100644 index 0000000..2a3e8b1 --- /dev/null +++ b/app/src/main/java/me/ash/reader/ui/ext/FlavorExt.kt @@ -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 \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/ext/LazyListStateExt.kt b/app/src/main/java/me/ash/reader/ui/ext/LazyListStateExt.kt index ca9eb77..dccea7c 100644 --- a/app/src/main/java/me/ash/reader/ui/ext/LazyListStateExt.kt +++ b/app/src/main/java/me/ash/reader/ui/ext/LazyListStateExt.kt @@ -1,8 +1,7 @@ package me.ash.reader.ui.ext import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember +import androidx.compose.runtime.* import androidx.paging.compose.LazyPagingItems import kotlin.math.abs @@ -27,4 +26,34 @@ fun LazyPagingItems.rememberLazyListState(): LazyListState { // Return rememberLazyListState (normal case). 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 } \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/ext/ScrollbarsExt.kt b/app/src/main/java/me/ash/reader/ui/ext/ScrollbarsExt.kt index ce85a11..d7c7184 100644 --- a/app/src/main/java/me/ash/reader/ui/ext/ScrollbarsExt.kt +++ b/app/src/main/java/me/ash/reader/ui/ext/ScrollbarsExt.kt @@ -52,6 +52,7 @@ import androidx.compose.ui.composed import androidx.compose.ui.draw.CacheDrawScope import androidx.compose.ui.draw.DrawResult import androidx.compose.ui.draw.drawWithCache +import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color @@ -173,11 +174,15 @@ private fun CacheDrawScope.onDrawScrollbar( return { if (showScrollbar) { - drawRect( + drawRoundRect( color = color, topLeft = topLeft, size = size, - alpha = alpha() + alpha = alpha(), + cornerRadius = CornerRadius( + x = size.width, + y = size.width, + ) ) } } @@ -217,7 +222,7 @@ private fun Modifier.drawScrollbar( val alpha = remember { Animatable(0f) } LaunchedEffect(scrolled, alpha) { scrolled.collectLatest { - alpha.snapTo(1f) + alpha.snapTo(0.3f) delay(ViewConfiguration.getScrollDefaultDelay().toLong()) alpha.animateTo(0f, animationSpec = FadeOutAnimationSpec) } @@ -231,7 +236,7 @@ private fun Modifier.drawScrollbar( // Calculate thickness here to workaround https://issuetracker.google.com/issues/206972664 val thickness = with(LocalDensity.current) { Thickness.toPx() } - val color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) + val color = MaterialTheme.colorScheme.onSurfaceVariant Modifier .nestedScroll(nestedScrollConnection) .drawWithCache { diff --git a/app/src/main/java/me/ash/reader/ui/page/common/HomeEntry.kt b/app/src/main/java/me/ash/reader/ui/page/common/HomeEntry.kt index 87b5015..92ca3b6 100644 --- a/app/src/main/java/me/ash/reader/ui/page/common/HomeEntry.kt +++ b/app/src/main/java/me/ash/reader/ui/page/common/HomeEntry.kt @@ -12,22 +12,21 @@ import androidx.hilt.navigation.compose.hiltViewModel import com.google.accompanist.navigation.animation.AnimatedNavHost import com.google.accompanist.navigation.animation.rememberAnimatedNavController import com.google.accompanist.systemuicontroller.rememberSystemUiController -import me.ash.reader.data.entity.Filter +import me.ash.reader.data.model.Filter import me.ash.reader.data.preference.LocalDarkTheme import me.ash.reader.ui.ext.* -import me.ash.reader.ui.page.home.HomeViewAction import me.ash.reader.ui.page.home.HomeViewModel import me.ash.reader.ui.page.home.feeds.FeedsPage import me.ash.reader.ui.page.home.flow.FlowPage -import me.ash.reader.ui.page.home.read.ReadPage +import me.ash.reader.ui.page.home.reading.ReadingPage import me.ash.reader.ui.page.settings.SettingsPage -import me.ash.reader.ui.page.settings.color.ColorAndStyle -import me.ash.reader.ui.page.settings.color.DarkTheme -import me.ash.reader.ui.page.settings.color.feeds.FeedsPageStyle -import me.ash.reader.ui.page.settings.color.flow.FlowPageStyle -import me.ash.reader.ui.page.settings.interaction.Interaction -import me.ash.reader.ui.page.settings.languages.Languages -import me.ash.reader.ui.page.settings.tips.TipsAndSupport +import me.ash.reader.ui.page.settings.color.ColorAndStylePage +import me.ash.reader.ui.page.settings.color.DarkThemePage +import me.ash.reader.ui.page.settings.color.feeds.FeedsPageStylePage +import me.ash.reader.ui.page.settings.color.flow.FlowPageStylePage +import me.ash.reader.ui.page.settings.interaction.InteractionPage +import me.ash.reader.ui.page.settings.languages.LanguagesPage +import me.ash.reader.ui.page.settings.tips.TipsAndSupportPage import me.ash.reader.ui.page.startup.StartupPage import me.ash.reader.ui.theme.AppTheme @@ -37,7 +36,7 @@ fun HomeEntry( homeViewModel: HomeViewModel = hiltViewModel(), ) { val context = LocalContext.current - val filterState = homeViewModel.filterState.collectAsStateValue() + val filterUiState = homeViewModel.filterUiState.collectAsStateValue() val navController = rememberAnimatedNavController() val intent by rememberSaveable { mutableStateOf(context.findActivity()?.intent) } @@ -57,16 +56,14 @@ fun HomeEntry( // Other initial pages } - homeViewModel.dispatch( - HomeViewAction.ChangeFilter( - filterState.copy( - filter = when (context.initialFilter) { - 0 -> Filter.Starred - 1 -> Filter.Unread - 2 -> Filter.All - else -> Filter.All - } - ) + homeViewModel.changeFilter( + filterUiState.copy( + filter = when (context.initialFilter) { + 0 -> Filter.Starred + 1 -> Filter.Unread + 2 -> Filter.All + else -> Filter.All + } ) ) } @@ -114,7 +111,7 @@ fun HomeEntry( ) } animatedComposable(route = "${RouteName.READING}/{articleId}") { - ReadPage(navController = navController) + ReadingPage(navController = navController) } // Settings @@ -124,31 +121,31 @@ fun HomeEntry( // Color & Style animatedComposable(route = RouteName.COLOR_AND_STYLE) { - ColorAndStyle(navController) + ColorAndStylePage(navController) } animatedComposable(route = RouteName.DARK_THEME) { - DarkTheme(navController) + DarkThemePage(navController) } animatedComposable(route = RouteName.FEEDS_PAGE_STYLE) { - FeedsPageStyle(navController) + FeedsPageStylePage(navController) } animatedComposable(route = RouteName.FLOW_PAGE_STYLE) { - FlowPageStyle(navController) + FlowPageStylePage(navController) } // Interaction animatedComposable(route = RouteName.INTERACTION) { - Interaction(navController) + InteractionPage(navController) } // Languages animatedComposable(route = RouteName.LANGUAGES) { - Languages(navController = navController) + LanguagesPage(navController = navController) } // Tips & Support animatedComposable(route = RouteName.TIPS_AND_SUPPORT) { - TipsAndSupport(navController) + TipsAndSupportPage(navController) } } } diff --git a/app/src/main/java/me/ash/reader/ui/page/home/HomeViewModel.kt b/app/src/main/java/me/ash/reader/ui/page/home/HomeViewModel.kt index 9149e23..56bebff 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/HomeViewModel.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/HomeViewModel.kt @@ -7,8 +7,8 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.* import me.ash.reader.data.entity.Feed -import me.ash.reader.data.entity.Filter import me.ash.reader.data.entity.Group +import me.ash.reader.data.model.Filter import me.ash.reader.data.module.ApplicationScope import me.ash.reader.data.repository.RssRepository import me.ash.reader.data.repository.StringsRepository @@ -24,30 +24,20 @@ class HomeViewModel @Inject constructor( private val applicationScope: CoroutineScope, private val workManager: WorkManager, ) : ViewModel() { + private val _homeUiState = MutableStateFlow(HomeUiState()) + val homeUiState: StateFlow = _homeUiState.asStateFlow() - private val _viewState = MutableStateFlow(HomeViewState()) - val viewState: StateFlow = _viewState.asStateFlow() - - private val _filterState = MutableStateFlow(FilterState()) - val filterState = _filterState.asStateFlow() + private val _filterUiState = MutableStateFlow(FilterState()) + val filterUiState = _filterUiState.asStateFlow() val syncWorkLiveData = workManager.getWorkInfoByIdLiveData(SyncWorker.UUID) - fun dispatch(action: HomeViewAction) { - when (action) { - is HomeViewAction.Sync -> sync() - is HomeViewAction.ChangeFilter -> changeFilter(action.filterState) - is HomeViewAction.FetchArticles -> fetchArticles() - is HomeViewAction.InputSearchContent -> inputSearchContent(action.content) - } - } - - private fun sync() { + fun sync() { rssRepository.get().doSync() } - private fun changeFilter(filterState: FilterState) { - _filterState.update { + fun changeFilter(filterState: FilterState) { + _filterUiState.update { it.copy( group = filterState.group, feed = filterState.feed, @@ -57,28 +47,40 @@ class HomeViewModel @Inject constructor( fetchArticles() } - private fun fetchArticles() { - _viewState.update { + fun fetchArticles() { + _homeUiState.update { it.copy( - pagingData = Pager(PagingConfig(pageSize = 50)) { - if (_viewState.value.searchContent.isNotBlank()) { + pagingData = Pager( + config = PagingConfig( + pageSize = 100, + enablePlaceholders = false, + ) + ) { + if (_homeUiState.value.searchContent.isNotBlank()) { rssRepository.get().searchArticles( - content = _viewState.value.searchContent.trim(), - groupId = _filterState.value.group?.id, - feedId = _filterState.value.feed?.id, - isStarred = _filterState.value.filter.isStarred(), - isUnread = _filterState.value.filter.isUnread(), + content = _homeUiState.value.searchContent.trim(), + groupId = _filterUiState.value.group?.id, + feedId = _filterUiState.value.feed?.id, + isStarred = _filterUiState.value.filter.isStarred(), + isUnread = _filterUiState.value.filter.isUnread(), ) } else { rssRepository.get().pullArticles( - groupId = _filterState.value.group?.id, - feedId = _filterState.value.feed?.id, - isStarred = _filterState.value.filter.isStarred(), - isUnread = _filterState.value.filter.isUnread(), + groupId = _filterUiState.value.group?.id, + feedId = _filterUiState.value.feed?.id, + isStarred = _filterUiState.value.filter.isStarred(), + isUnread = _filterUiState.value.filter.isUnread(), ) } }.flow.map { - it.map { FlowItemView.Article(it) }.insertSeparators { before, after -> + it.map { + FlowItemView.Article(it.apply { + article.dateString = stringsRepository.formatAsString( + date = article.date, + onlyHourMinute = true + ) + }) + }.insertSeparators { before, after -> val beforeDate = stringsRepository.formatAsString(before?.articleWithFeed?.article?.date) val afterDate = @@ -94,8 +96,8 @@ class HomeViewModel @Inject constructor( } } - private fun inputSearchContent(content: String) { - _viewState.update { + fun inputSearchContent(content: String) { + _homeUiState.update { it.copy( searchContent = content, ) @@ -110,21 +112,7 @@ data class FilterState( val filter: Filter = Filter.All, ) -data class HomeViewState( +data class HomeUiState( val pagingData: Flow> = emptyFlow(), val searchContent: String = "", -) - -sealed class HomeViewAction { - object Sync : HomeViewAction() - - data class ChangeFilter( - val filterState: FilterState - ) : HomeViewAction() - - object FetchArticles : HomeViewAction() - - data class InputSearchContent( - val content: String, - ) : HomeViewAction() -} \ No newline at end of file +) \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedItem.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedItem.kt index 43e1814..b854541 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedItem.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedItem.kt @@ -1,96 +1,99 @@ package me.ash.reader.ui.page.home.feeds +import RYExtensibleVisibility import android.view.HapticFeedbackConstants +import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Badge import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.platform.LocalView import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import me.ash.reader.data.entity.Feed -import me.ash.reader.ui.page.home.FeedIcon -import me.ash.reader.ui.page.home.feeds.option.feed.FeedOptionViewAction -import me.ash.reader.ui.page.home.feeds.option.feed.FeedOptionViewModel -import kotlin.math.ln +import me.ash.reader.ui.component.FeedIcon +import me.ash.reader.ui.page.home.feeds.drawer.feed.FeedOptionViewModel +import me.ash.reader.ui.theme.ShapeBottom32 @OptIn( androidx.compose.foundation.ExperimentalFoundationApi::class, - androidx.compose.material.ExperimentalMaterialApi::class, ) @Composable fun FeedItem( feed: Feed, + alpha: Float = 1f, + badgeAlpha: Float = 1f, + isEnded: Boolean = false, + isExpanded: () -> Boolean, feedOptionViewModel: FeedOptionViewModel = hiltViewModel(), - tonalElevation: Dp, onClick: () -> Unit = {}, ) { val view = LocalView.current val scope = rememberCoroutineScope() - val tonalElevationAlpha by remember { - derivedStateOf { - (ln(tonalElevation.value + 1.4f) + 2f) / 100f - } - } - Row( - 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), - ) { + RYExtensibleVisibility(visible = isExpanded()) { 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, + .padding(horizontal = 16.dp) + .background( + color = MaterialTheme.colorScheme.secondary.copy(alpha = alpha), + shape = if (isEnded) ShapeBottom32 else RectangleShape, ) - } - if ((feed.important ?: 0) != 0) { - Badge( - containerColor = MaterialTheme.colorScheme.surfaceTint.copy( - alpha = tonalElevationAlpha - ), - contentColor = MaterialTheme.colorScheme.outline, - content = { - Text( - text = feed.important.toString(), - style = MaterialTheme.typography.labelSmall - ) + .combinedClickable( + onClick = { + onClick() }, + 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 + ) + }, + ) + } } } } diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsPage.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsPage.kt index 4b22327..b144f93 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsPage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsPage.kt @@ -1,11 +1,9 @@ package me.ash.reader.ui.page.home.feeds -import android.annotation.SuppressLint import androidx.activity.compose.BackHandler import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.core.* -import androidx.compose.foundation.background import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn @@ -15,7 +13,8 @@ import androidx.compose.material.icons.outlined.KeyboardArrowRight import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.rounded.Add import androidx.compose.material.icons.rounded.Refresh -import androidx.compose.material3.* +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.draw.rotate @@ -26,31 +25,26 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavHostController -import kotlinx.coroutines.flow.map import me.ash.reader.R -import me.ash.reader.data.entity.toVersion +import me.ash.reader.data.model.getName import me.ash.reader.data.preference.* import me.ash.reader.data.repository.SyncWorker.Companion.getIsSyncing -import me.ash.reader.ui.component.Banner -import me.ash.reader.ui.component.DisplayText -import me.ash.reader.ui.component.FeedbackIconButton -import me.ash.reader.ui.component.Subtitle -import me.ash.reader.ui.ext.* +import me.ash.reader.ui.component.FilterBar +import me.ash.reader.ui.component.base.* +import me.ash.reader.ui.ext.alphaLN +import me.ash.reader.ui.ext.collectAsStateValue +import me.ash.reader.ui.ext.findActivity +import me.ash.reader.ui.ext.getCurrentVersion import me.ash.reader.ui.page.common.RouteName -import me.ash.reader.ui.page.home.FilterBar import me.ash.reader.ui.page.home.FilterState -import me.ash.reader.ui.page.home.HomeViewAction import me.ash.reader.ui.page.home.HomeViewModel -import me.ash.reader.ui.page.home.feeds.option.feed.FeedOptionDrawer -import me.ash.reader.ui.page.home.feeds.option.group.GroupOptionDrawer +import me.ash.reader.ui.page.home.feeds.drawer.feed.FeedOptionDrawer +import me.ash.reader.ui.page.home.feeds.drawer.group.GroupOptionDrawer import me.ash.reader.ui.page.home.feeds.subscribe.SubscribeDialog -import me.ash.reader.ui.page.home.feeds.subscribe.SubscribeViewAction import me.ash.reader.ui.page.home.feeds.subscribe.SubscribeViewModel -import me.ash.reader.ui.theme.palette.onDark +import kotlin.math.ln -@SuppressLint("FlowOperatorInvokedInComposition") @OptIn( - ExperimentalMaterial3Api::class, com.google.accompanist.pager.ExperimentalPagerApi::class, androidx.compose.foundation.ExperimentalFoundationApi::class ) @Composable @@ -69,20 +63,12 @@ fun FeedsPage( val filterBarPadding = LocalFeedsFilterBarPadding.current val filterBarTonalElevation = LocalFeedsFilterBarTonalElevation.current - val feedsViewState = feedsViewModel.viewState.collectAsStateValue() - val filterState = homeViewModel.filterState.collectAsStateValue() + val feedsUiState = feedsViewModel.feedsUiState.collectAsStateValue() + val filterUiState = homeViewModel.filterUiState.collectAsStateValue() - val skipVersion = context.dataStore.data - .map { it[DataStoreKeys.SkipVersionNumber.key] ?: "" } - .collectAsState(initial = "") - .value - .toVersion() - val latestVersion = context.dataStore.data - .map { it[DataStoreKeys.NewVersionNumber.key] ?: "" } - .collectAsState(initial = "") - .value - .toVersion() - val currentVersion by remember { mutableStateOf(context.getCurrentVersion()) } + val newVersion = LocalNewVersionNumber.current + val skipVersion = LocalSkipVersionNumber.current + val currentVersion = remember { context.getCurrentVersion() } val owner = LocalLifecycleOwner.current var isSyncing by remember { mutableStateOf(false) } @@ -102,22 +88,40 @@ fun FeedsPage( val launcher = rememberLauncherForActivityResult( ActivityResultContracts.CreateDocument() ) { result -> - feedsViewModel.dispatch(FeedsViewAction.ExportAsString { string -> + feedsViewModel.exportAsOpml { string -> result?.let { uri -> - context.contentResolver.openOutputStream(uri)?.let { outputStream -> + context.contentResolver.openOutputStream(uri)?.use { outputStream -> outputStream.write(string.toByteArray()) } } - }) + } + } + + val feedBadgeAlpha by remember { derivedStateOf { (ln(groupListTonalElevation.value + 1.4f) + 2f) / 100f } } + val groupAlpha by remember { derivedStateOf { groupListTonalElevation.value.dp.alphaLN(weight = 1.2f) } } + val groupIndicatorAlpha by remember { + derivedStateOf { + groupListTonalElevation.value.dp.alphaLN( + weight = 1.4f + ) + } + } + + val groupsVisible = remember(feedsUiState.groupWithFeedList) { + mutableStateMapOf( + *(feedsUiState.groupWithFeedList.filterIsInstance().map { + it.group.id to groupListExpand.value + }.toTypedArray()) + ) } LaunchedEffect(Unit) { - feedsViewModel.dispatch(FeedsViewAction.FetchAccount) + feedsViewModel.fetchAccount() } - LaunchedEffect(filterState) { - snapshotFlow { filterState }.collect { - feedsViewModel.dispatch(FeedsViewAction.FetchData(it)) + LaunchedEffect(filterUiState) { + snapshotFlow { filterUiState }.collect { + feedsViewModel.fetchData(it) } } @@ -125,52 +129,38 @@ fun FeedsPage( context.findActivity()?.moveTaskToBack(false) } - Scaffold( - modifier = Modifier - .background(MaterialTheme.colorScheme.surfaceColorAtElevation(topBarTonalElevation.value.dp)) - .statusBarsPadding(), - containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation( - groupListTonalElevation.value.dp - ) onDark MaterialTheme.colorScheme.surface, - topBar = { - SmallTopAppBar( - colors = TopAppBarDefaults.smallTopAppBarColors( - containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation( - topBarTonalElevation.value.dp - ), - ), - title = {}, - 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) - } + RYScaffold( + topBarTonalElevation = topBarTonalElevation.value.dp, + containerTonalElevation = groupListTonalElevation.value.dp, + navigationIcon = { + FeedbackIconButton( + modifier = Modifier.size(20.dp), + imageVector = Icons.Outlined.Settings, + contentDescription = stringResource(R.string.settings), + tint = MaterialTheme.colorScheme.onSurface, + showBadge = newVersion.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.sync() + } + FeedbackIconButton( + imageVector = Icons.Rounded.Add, + contentDescription = stringResource(R.string.subscribe), + tint = MaterialTheme.colorScheme.onSurface, + ) { + subscribeViewModel.showDrawer() + } }, content = { LazyColumn { @@ -183,15 +173,15 @@ fun FeedsPage( } ) }, - text = feedsViewState.account?.name ?: stringResource(R.string.read_you), + text = feedsUiState.account?.name ?: stringResource(R.string.read_you), desc = if (isSyncing) stringResource(R.string.syncing) else "", ) } item { Banner( - title = filterState.filter.getName(), - desc = feedsViewState.importantCount.ifEmpty { stringResource(R.string.loading) }, - icon = filterState.filter.iconOutline, + title = filterUiState.filter.getName(), + desc = feedsUiState.importantSum.ifEmpty { stringResource(R.string.loading) }, + icon = filterUiState.filter.iconOutline, action = { Icon( imageVector = Icons.Outlined.KeyboardArrowRight, @@ -202,7 +192,7 @@ fun FeedsPage( filterChange( navController = navController, homeViewModel = homeViewModel, - filterState = filterState.copy( + filterState = filterUiState.copy( group = null, feed = null, ) @@ -217,40 +207,51 @@ fun FeedsPage( ) Spacer(modifier = Modifier.height(8.dp)) } - itemsIndexed(feedsViewState.groupWithFeedList) { index, groupWithFeed -> -// Crossfade(targetState = groupWithFeed) { groupWithFeed -> - Column { - GroupItem( - isExpanded = groupListExpand.value, - tonalElevation = groupListTonalElevation.value.dp, - group = groupWithFeed.group, - feeds = groupWithFeed.feeds, - groupOnClick = { + itemsIndexed(feedsUiState.groupWithFeedList) { index, groupWithFeed -> + when (groupWithFeed) { + is GroupFeedsView.Group -> { + if (index != 0) { + Spacer(modifier = Modifier.height(16.dp)) + } + GroupItem( + isExpanded = { groupsVisible[groupWithFeed.group.id] ?: false }, + group = groupWithFeed.group, + alpha = groupAlpha, + indicatorAlpha = groupIndicatorAlpha, + onExpanded = { + groupsVisible[groupWithFeed.group.id] = + !(groupsVisible[groupWithFeed.group.id] ?: false) + } + ) { filterChange( navController = navController, homeViewModel = homeViewModel, - filterState = filterState.copy( + filterState = filterUiState.copy( group = groupWithFeed.group, feed = null, ) ) - }, - feedOnClick = { feed -> + } + } + is GroupFeedsView.Feed -> { + FeedItem( + feed = groupWithFeed.feed, + alpha = groupAlpha, + badgeAlpha = feedBadgeAlpha, + isEnded = index != feedsUiState.groupWithFeedList.lastIndex && feedsUiState.groupWithFeedList[index + 1] is GroupFeedsView.Group, + isExpanded = { groupsVisible[groupWithFeed.feed.groupId] ?: false }, + ) { filterChange( navController = navController, homeViewModel = homeViewModel, - filterState = filterState.copy( + filterState = filterUiState.copy( group = null, - feed = feed, + feed = groupWithFeed.feed, ) ) } - ) - if (index != feedsViewState.groupWithFeedList.lastIndex) { - Spacer(modifier = Modifier.height(8.dp)) } } -// } } item { Spacer(modifier = Modifier.height(128.dp)) @@ -260,7 +261,7 @@ fun FeedsPage( }, bottomBar = { FilterBar( - filter = filterState.filter, + filter = filterUiState.filter, filterBarStyle = filterBarStyle.value, filterBarFilled = filterBarFilled.value, filterBarPadding = filterBarPadding.dp, @@ -269,7 +270,7 @@ fun FeedsPage( filterChange( navController = navController, homeViewModel = homeViewModel, - filterState = filterState.copy(filter = it), + filterState = filterUiState.copy(filter = it), isNavigate = false, ) } @@ -287,7 +288,7 @@ private fun filterChange( filterState: FilterState, isNavigate: Boolean = true, ) { - homeViewModel.dispatch(HomeViewAction.ChangeFilter(filterState)) + homeViewModel.changeFilter(filterState) if (isNavigate) { navController.navigate(RouteName.FLOW) { launchSingleTop = true diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsViewModel.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsViewModel.kt index 43c9f63..08d4e5a 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsViewModel.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsViewModel.kt @@ -2,15 +2,17 @@ package me.ash.reader.ui.page.home.feeds import android.util.Log import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.ui.util.fastForEach import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import me.ash.reader.R import me.ash.reader.data.entity.Account -import me.ash.reader.data.entity.GroupWithFeed +import me.ash.reader.data.module.DispatcherDefault +import me.ash.reader.data.module.DispatcherIO import me.ash.reader.data.repository.AccountRepository import me.ash.reader.data.repository.OpmlRepository import me.ash.reader.data.repository.RssRepository @@ -24,22 +26,17 @@ class FeedsViewModel @Inject constructor( private val rssRepository: RssRepository, private val opmlRepository: OpmlRepository, private val stringsRepository: StringsRepository, + @DispatcherDefault + private val dispatcherDefault: CoroutineDispatcher, + @DispatcherIO + private val dispatcherIO: CoroutineDispatcher, ) : ViewModel() { - private val _viewState = MutableStateFlow(FeedsViewState()) - val viewState: StateFlow = _viewState.asStateFlow() + private val _feedsUiState = MutableStateFlow(FeedsUiState()) + val feedsUiState: StateFlow = _feedsUiState.asStateFlow() - fun dispatch(action: FeedsViewAction) { - when (action) { - is FeedsViewAction.FetchAccount -> fetchAccount() - is FeedsViewAction.FetchData -> fetchData(action.filterState) - is FeedsViewAction.ExportAsString -> exportAsOpml(action.callback) - is FeedsViewAction.ScrollToItem -> scrollToItem(action.index) - } - } - - private fun fetchAccount() { - viewModelScope.launch(Dispatchers.IO) { - _viewState.update { + fun fetchAccount() { + viewModelScope.launch(dispatcherIO) { + _feedsUiState.update { it.copy( account = accountRepository.getCurrentAccount() ) @@ -47,8 +44,8 @@ class FeedsViewModel @Inject constructor( } } - private fun exportAsOpml(callback: (String) -> Unit = {}) { - viewModelScope.launch(Dispatchers.Default) { + fun exportAsOpml(callback: (String) -> Unit = {}) { + viewModelScope.launch(dispatcherDefault) { try { callback(opmlRepository.saveToString()) } catch (e: Exception) { @@ -57,8 +54,8 @@ class FeedsViewModel @Inject constructor( } } - private fun fetchData(filterState: FilterState) { - viewModelScope.launch(Dispatchers.IO) { + fun fetchData(filterState: FilterState) { + viewModelScope.launch(dispatcherIO) { pullFeeds( isStarred = filterState.filter.isStarred(), isUnread = filterState.filter.isUnread(), @@ -70,85 +67,64 @@ class FeedsViewModel @Inject constructor( combine( rssRepository.get().pullFeeds(), rssRepository.get().pullImportant(isStarred, isUnread), - ) { groupWithFeedList, importantList -> - val groupImportantMap = mutableMapOf() - val feedImportantMap = mutableMapOf() - importantList.groupBy { it.groupId }.forEach { (i, list) -> - var groupImportantSum = 0 - list.forEach { - feedImportantMap[it.feedId] = it.important - groupImportantSum += it.important - } - groupImportantMap[i] = groupImportantSum - } - val groupsIt = groupWithFeedList.iterator() - while (groupsIt.hasNext()) { - val groupWithFeed = groupsIt.next() - val groupImportant = groupImportantMap[groupWithFeed.group.id] - if (groupImportant == null && (isStarred || isUnread)) { - groupsIt.remove() - } else { - groupWithFeed.group.important = groupImportant - val feedsIt = groupWithFeed.feeds.iterator() - while (feedsIt.hasNext()) { - val feed = feedsIt.next() - val feedImportant = feedImportantMap[feed.id] - if (feedImportant == null && (isStarred || isUnread)) { - feedsIt.remove() - } else { - feed.important = feedImportant - } - } + ) { groupWithFeedList, importantMap -> + groupWithFeedList.fastForEach { + var groupImportant = 0 + it.feeds.fastForEach { + it.important = importantMap[it.id] + groupImportant += it.important ?: 0 } + it.group.important = groupImportant } groupWithFeedList - }.onEach { groupWithFeedList -> - _viewState.update { + }.mapLatest { groupWithFeedList -> + _feedsUiState.update { it.copy( - importantCount = groupWithFeedList.sumOf { it.group.important ?: 0 }.run { + importantSum = groupWithFeedList.sumOf { it.group.important ?: 0 }.run { when { - isStarred -> stringsRepository.getQuantityString(R.plurals.starred_desc, this, this) - isUnread -> stringsRepository.getQuantityString(R.plurals.unread_desc, this, this) - else -> stringsRepository.getQuantityString(R.plurals.all_desc, this, this) + isStarred -> stringsRepository.getQuantityString( + R.plurals.starred_desc, + this, + this + ) + isUnread -> stringsRepository.getQuantityString( + R.plurals.unread_desc, + this, + this + ) + else -> stringsRepository.getQuantityString( + R.plurals.all_desc, + this, + this + ) } }, - groupWithFeedList = groupWithFeedList, - feedsVisible = List(groupWithFeedList.size, init = { true }) + groupWithFeedList = groupWithFeedList.map { + mutableListOf(GroupFeedsView.Group(it.group)).apply { + addAll( + it.feeds.map { + GroupFeedsView.Feed(it) + } + ) + } + }.flatten(), ) } }.catch { Log.e("RLog", "catch in articleRepository.pullFeeds(): ${it.message}") - }.flowOn(Dispatchers.Default).collect() - } - - private fun scrollToItem(index: Int) { - viewModelScope.launch { - _viewState.value.listState.scrollToItem(index) - } + }.flowOn(dispatcherDefault).collect() } } -data class FeedsViewState( +data class FeedsUiState( val account: Account? = null, - val importantCount: String = "", - val groupWithFeedList: List = emptyList(), - val feedsVisible: List = emptyList(), + val importantSum: String = "", + val groupWithFeedList: List = emptyList(), val listState: LazyListState = LazyListState(), val groupsVisible: Boolean = true, ) -sealed class FeedsViewAction { - data class FetchData( - val filterState: FilterState, - ) : FeedsViewAction() - - object FetchAccount : FeedsViewAction() - - data class ExportAsString( - val callback: (String) -> Unit = {} - ) : FeedsViewAction() - - data class ScrollToItem( - val index: Int - ) : FeedsViewAction() +sealed class GroupFeedsView { + class Group(val group: me.ash.reader.data.entity.Group) : GroupFeedsView() + class Feed(val feed: me.ash.reader.data.entity.Feed) : GroupFeedsView() } \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/GroupItem.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/GroupItem.kt index 9d6559f..e41ef52 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/GroupItem.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/GroupItem.kt @@ -1,59 +1,56 @@ package me.ash.reader.ui.page.home.feeds import android.view.HapticFeedbackConstants -import androidx.compose.animation.* +import androidx.compose.animation.animateContentSize import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.ExpandLess import androidx.compose.material.icons.rounded.ExpandMore import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import me.ash.reader.R -import me.ash.reader.data.entity.Feed import me.ash.reader.data.entity.Group -import me.ash.reader.ui.ext.alphaLN -import me.ash.reader.ui.page.home.feeds.option.group.GroupOptionViewAction -import me.ash.reader.ui.page.home.feeds.option.group.GroupOptionViewModel +import me.ash.reader.ui.page.home.feeds.drawer.group.GroupOptionViewModel +import me.ash.reader.ui.theme.Shape32 +import me.ash.reader.ui.theme.ShapeTop32 @OptIn(androidx.compose.foundation.ExperimentalFoundationApi::class) @Composable fun GroupItem( - modifier: Modifier = Modifier, - tonalElevation: Dp, group: Group, - feeds: List, - isExpanded: Boolean = true, + alpha: Float = 1f, + indicatorAlpha: Float = 1f, + isExpanded: () -> Boolean, groupOptionViewModel: GroupOptionViewModel = hiltViewModel(), + onExpanded: () -> Unit = {}, groupOnClick: () -> Unit = {}, - feedOnClick: (feed: Feed) -> Unit = {}, ) { val view = LocalView.current val scope = rememberCoroutineScope() - var expanded by remember { mutableStateOf(isExpanded) } Column( modifier = Modifier + .animateContentSize() .fillMaxWidth() .padding(horizontal = 16.dp) - .clip(RoundedCornerShape(32.dp)) + .clip(if (isExpanded()) ShapeTop32 else Shape32) .background( - MaterialTheme.colorScheme.secondary.copy(alpha = tonalElevation.alphaLN(weight = 1.2f)) + MaterialTheme.colorScheme.secondary.copy(alpha = alpha) ) .combinedClickable( onClick = { @@ -61,13 +58,13 @@ fun GroupItem( }, onLongClick = { view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) - groupOptionViewModel.dispatch(GroupOptionViewAction.Show(scope, group.id)) + groupOptionViewModel.showDrawer(scope, group.id) } ) .padding(top = 22.dp) ) { Row( - modifier = modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { @@ -87,42 +84,21 @@ fun GroupItem( .size(24.dp) .clip(CircleShape) .background( - MaterialTheme.colorScheme.surfaceTint.copy( - alpha = tonalElevation.alphaLN(weight = 1.4f) - ) + MaterialTheme.colorScheme.surfaceTint.copy(alpha = indicatorAlpha) ) .clickable { - expanded = !expanded + onExpanded() }, horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, ) { Icon( - imageVector = if (expanded) Icons.Rounded.ExpandLess else Icons.Rounded.ExpandMore, - contentDescription = stringResource(if (expanded) R.string.expand_less else R.string.expand_more), + imageVector = if (isExpanded()) Icons.Rounded.ExpandLess else Icons.Rounded.ExpandMore, + contentDescription = stringResource(if (isExpanded()) R.string.expand_less else R.string.expand_more), tint = MaterialTheme.colorScheme.onSecondaryContainer, ) } } Spacer(modifier = Modifier.height(22.dp)) - AnimatedVisibility( - visible = expanded, - enter = fadeIn() + expandVertically(), - exit = fadeOut() + shrinkVertically(), - ) { - Column { - feeds.forEach { feed -> - FeedItem( - feed = feed, - tonalElevation = tonalElevation, - ) { - feedOnClick(feed) - } - } - if (feeds.isNotEmpty()) { - Spacer(modifier = Modifier.height(16.dp)) - } - } - } } } \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/option/feed/ClearFeedDialog.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/feed/ClearFeedDialog.kt similarity index 68% rename from app/src/main/java/me/ash/reader/ui/page/home/feeds/option/feed/ClearFeedDialog.kt rename to app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/feed/ClearFeedDialog.kt index cad233b..5014237 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/option/feed/ClearFeedDialog.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/feed/ClearFeedDialog.kt @@ -1,4 +1,4 @@ -package me.ash.reader.ui.page.home.feeds.option.feed +package me.ash.reader.ui.page.home.feeds.drawer.feed import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.DeleteForever @@ -7,32 +7,28 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.hilt.navigation.compose.hiltViewModel -import com.google.accompanist.pager.ExperimentalPagerApi import me.ash.reader.R -import me.ash.reader.ui.component.Dialog +import me.ash.reader.ui.component.base.RYDialog import me.ash.reader.ui.ext.collectAsStateValue import me.ash.reader.ui.ext.showToast -@OptIn(ExperimentalPagerApi::class) @Composable fun ClearFeedDialog( - modifier: Modifier = Modifier, feedName: String, - viewModel: FeedOptionViewModel = hiltViewModel(), + feedOptionViewModel: FeedOptionViewModel = hiltViewModel(), ) { val context = LocalContext.current - val viewState = viewModel.viewState.collectAsStateValue() + val feedOptionUiState = feedOptionViewModel.feedOptionUiState.collectAsStateValue() val scope = rememberCoroutineScope() val toastString = stringResource(R.string.clear_articles_in_feed_toast, feedName) - Dialog( - visible = viewState.clearDialogVisible, + RYDialog( + visible = feedOptionUiState.clearDialogVisible, onDismissRequest = { - viewModel.dispatch(FeedOptionViewAction.HideClearDialog) + feedOptionViewModel.hideClearDialog() }, icon = { Icon( @@ -49,11 +45,11 @@ fun ClearFeedDialog( confirmButton = { TextButton( onClick = { - viewModel.dispatch(FeedOptionViewAction.Clear { - viewModel.dispatch(FeedOptionViewAction.HideClearDialog) - viewModel.dispatch(FeedOptionViewAction.Hide(scope)) + feedOptionViewModel.clearFeed { + feedOptionViewModel.hideClearDialog() + feedOptionViewModel.hideDrawer(scope) context.showToast(toastString) - }) + } } ) { Text( @@ -64,7 +60,7 @@ fun ClearFeedDialog( dismissButton = { TextButton( onClick = { - viewModel.dispatch(FeedOptionViewAction.HideClearDialog) + feedOptionViewModel.hideClearDialog() } ) { Text( diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/option/feed/DeleteFeedDialog.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/feed/DeleteFeedDialog.kt similarity index 67% rename from app/src/main/java/me/ash/reader/ui/page/home/feeds/option/feed/DeleteFeedDialog.kt rename to app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/feed/DeleteFeedDialog.kt index 22c8a8e..1bd1f1d 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/option/feed/DeleteFeedDialog.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/feed/DeleteFeedDialog.kt @@ -1,4 +1,4 @@ -package me.ash.reader.ui.page.home.feeds.option.feed +package me.ash.reader.ui.page.home.feeds.drawer.feed import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.DeleteForever @@ -7,32 +7,28 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.hilt.navigation.compose.hiltViewModel -import com.google.accompanist.pager.ExperimentalPagerApi import me.ash.reader.R -import me.ash.reader.ui.component.Dialog +import me.ash.reader.ui.component.base.RYDialog import me.ash.reader.ui.ext.collectAsStateValue import me.ash.reader.ui.ext.showToast -@OptIn(ExperimentalPagerApi::class) @Composable fun DeleteFeedDialog( - modifier: Modifier = Modifier, feedName: String, - viewModel: FeedOptionViewModel = hiltViewModel(), + feedOptionViewModel: FeedOptionViewModel = hiltViewModel(), ) { val context = LocalContext.current - val viewState = viewModel.viewState.collectAsStateValue() + val feedOptionUiState = feedOptionViewModel.feedOptionUiState.collectAsStateValue() val scope = rememberCoroutineScope() val toastString = stringResource(R.string.delete_toast, feedName) - Dialog( - visible = viewState.deleteDialogVisible, + RYDialog( + visible = feedOptionUiState.deleteDialogVisible, onDismissRequest = { - viewModel.dispatch(FeedOptionViewAction.HideDeleteDialog) + feedOptionViewModel.hideDeleteDialog() }, icon = { Icon( @@ -49,11 +45,11 @@ fun DeleteFeedDialog( confirmButton = { TextButton( onClick = { - viewModel.dispatch(FeedOptionViewAction.Delete { - viewModel.dispatch(FeedOptionViewAction.HideDeleteDialog) - viewModel.dispatch(FeedOptionViewAction.Hide(scope)) + feedOptionViewModel.delete { + feedOptionViewModel.hideDeleteDialog() + feedOptionViewModel.hideDrawer(scope) context.showToast(toastString) - }) + } } ) { Text( @@ -64,7 +60,7 @@ fun DeleteFeedDialog( dismissButton = { TextButton( onClick = { - viewModel.dispatch(FeedOptionViewAction.HideDeleteDialog) + feedOptionViewModel.hideDeleteDialog() } ) { Text( diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/option/feed/FeedOptionDrawer.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/feed/FeedOptionDrawer.kt similarity index 61% rename from app/src/main/java/me/ash/reader/ui/page/home/feeds/option/feed/FeedOptionDrawer.kt rename to app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/feed/FeedOptionDrawer.kt index 843b495..f6f47b0 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/option/feed/FeedOptionDrawer.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/feed/FeedOptionDrawer.kt @@ -1,5 +1,6 @@ -package me.ash.reader.ui.page.home.feeds.option.feed +package me.ash.reader.ui.page.home.feeds.drawer.feed +import android.view.HapticFeedbackConstants import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.* import androidx.compose.material.ExperimentalMaterialApi @@ -13,41 +14,43 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import kotlinx.coroutines.launch import me.ash.reader.R -import me.ash.reader.ui.component.BottomDrawer -import me.ash.reader.ui.component.TextFieldDialog +import me.ash.reader.ui.component.FeedIcon +import me.ash.reader.ui.component.base.BottomDrawer +import me.ash.reader.ui.component.base.TextFieldDialog import me.ash.reader.ui.ext.collectAsStateValue +import me.ash.reader.ui.ext.openURL import me.ash.reader.ui.ext.roundClick import me.ash.reader.ui.ext.showToast -import me.ash.reader.ui.page.home.FeedIcon import me.ash.reader.ui.page.home.feeds.subscribe.ResultView @OptIn(ExperimentalMaterialApi::class) @Composable fun FeedOptionDrawer( - modifier: Modifier = Modifier, feedOptionViewModel: FeedOptionViewModel = hiltViewModel(), content: @Composable () -> Unit = {}, ) { val context = LocalContext.current + val view = LocalView.current val scope = rememberCoroutineScope() - val viewState = feedOptionViewModel.viewState.collectAsStateValue() - val feed = viewState.feed - val toastString = stringResource(R.string.rename_toast, viewState.newName) + val feedOptionUiState = feedOptionViewModel.feedOptionUiState.collectAsStateValue() + val feed = feedOptionUiState.feed + val toastString = stringResource(R.string.rename_toast, feedOptionUiState.newName) - BackHandler(viewState.drawerState.isVisible) { + BackHandler(feedOptionUiState.drawerState.isVisible) { scope.launch { - viewState.drawerState.hide() + feedOptionUiState.drawerState.hide() } } BottomDrawer( - drawerState = viewState.drawerState, + drawerState = feedOptionUiState.drawerState, sheetContent = { Column(modifier = Modifier.navigationBarsPadding()) { Column( @@ -65,7 +68,7 @@ fun FeedOptionDrawer( Spacer(modifier = Modifier.height(16.dp)) Text( modifier = Modifier.roundClick { - feedOptionViewModel.dispatch(FeedOptionViewAction.ShowRenameDialog) + feedOptionViewModel.showRenameDialog() }, text = feed?.name ?: stringResource(R.string.unknown), style = MaterialTheme.typography.headlineSmall, @@ -77,32 +80,37 @@ fun FeedOptionDrawer( Spacer(modifier = Modifier.height(16.dp)) ResultView( link = feed?.url ?: stringResource(R.string.unknown), - groups = viewState.groups, - selectedAllowNotificationPreset = viewState.feed?.isNotification ?: false, - selectedParseFullContentPreset = viewState.feed?.isFullContent ?: false, + groups = feedOptionUiState.groups, + selectedAllowNotificationPreset = feedOptionUiState.feed?.isNotification + ?: false, + selectedParseFullContentPreset = feedOptionUiState.feed?.isFullContent ?: false, isMoveToGroup = true, showUnsubscribe = true, - selectedGroupId = viewState.feed?.groupId ?: "", + selectedGroupId = feedOptionUiState.feed?.groupId ?: "", allowNotificationPresetOnClick = { - feedOptionViewModel.dispatch(FeedOptionViewAction.ChangeAllowNotificationPreset) + feedOptionViewModel.changeAllowNotificationPreset() }, parseFullContentPresetOnClick = { - feedOptionViewModel.dispatch(FeedOptionViewAction.ChangeParseFullContentPreset) + feedOptionViewModel.changeParseFullContentPreset() }, clearArticlesOnClick = { - feedOptionViewModel.dispatch(FeedOptionViewAction.ShowClearDialog) + feedOptionViewModel.showClearDialog() }, unsubscribeOnClick = { - feedOptionViewModel.dispatch(FeedOptionViewAction.ShowDeleteDialog) + feedOptionViewModel.showDeleteDialog() }, onGroupClick = { - feedOptionViewModel.dispatch(FeedOptionViewAction.SelectedGroup(it)) + feedOptionViewModel.selectedGroup(it) }, onAddNewGroup = { - feedOptionViewModel.dispatch(FeedOptionViewAction.ShowNewGroupDialog) + feedOptionViewModel.showNewGroupDialog() }, onFeedUrlClick = { - feedOptionViewModel.dispatch(FeedOptionViewAction.ShowChangeUrlDialog) + context.openURL(feed?.url) + }, + onFeedUrlLongClick = { + view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) + feedOptionViewModel.showFeedUrlDialog() } ) } @@ -116,56 +124,56 @@ fun FeedOptionDrawer( ClearFeedDialog(feedName = feed?.name ?: "") TextFieldDialog( - visible = viewState.newGroupDialogVisible, + visible = feedOptionUiState.newGroupDialogVisible, title = stringResource(R.string.create_new_group), icon = Icons.Outlined.CreateNewFolder, - value = viewState.newGroupContent, + value = feedOptionUiState.newGroupContent, placeholder = stringResource(R.string.name), onValueChange = { - feedOptionViewModel.dispatch(FeedOptionViewAction.InputNewGroup(it)) + feedOptionViewModel.inputNewGroup(it) }, onDismissRequest = { - feedOptionViewModel.dispatch(FeedOptionViewAction.HideNewGroupDialog) + feedOptionViewModel.hideNewGroupDialog() }, onConfirm = { - feedOptionViewModel.dispatch(FeedOptionViewAction.AddNewGroup) + feedOptionViewModel.addNewGroup() } ) TextFieldDialog( - visible = viewState.renameDialogVisible, + visible = feedOptionUiState.renameDialogVisible, title = stringResource(R.string.rename), icon = Icons.Outlined.Edit, - value = viewState.newName, + value = feedOptionUiState.newName, placeholder = stringResource(R.string.name), onValueChange = { - feedOptionViewModel.dispatch(FeedOptionViewAction.InputNewName(it)) + feedOptionViewModel.inputNewName(it) }, onDismissRequest = { - feedOptionViewModel.dispatch(FeedOptionViewAction.HideRenameDialog) + feedOptionViewModel.hideRenameDialog() }, onConfirm = { - feedOptionViewModel.dispatch(FeedOptionViewAction.Rename) - feedOptionViewModel.dispatch(FeedOptionViewAction.Hide(scope)) + feedOptionViewModel.renameFeed() + feedOptionViewModel.hideDrawer(scope) context.showToast(toastString) } ) TextFieldDialog( - visible = viewState.changeUrlDialogVisible, + visible = feedOptionUiState.changeUrlDialogVisible, title = stringResource(R.string.change_url), icon = Icons.Outlined.Edit, - value = viewState.newUrl, + value = feedOptionUiState.newUrl, placeholder = stringResource(R.string.feed_url_placeholder), onValueChange = { - feedOptionViewModel.dispatch(FeedOptionViewAction.InputNewUrl(it)) + feedOptionViewModel.inputNewUrl(it) }, onDismissRequest = { - feedOptionViewModel.dispatch(FeedOptionViewAction.HideChangeUrlDialog) + feedOptionViewModel.hideFeedUrlDialog() }, onConfirm = { - feedOptionViewModel.dispatch(FeedOptionViewAction.ChangeUrl) - feedOptionViewModel.dispatch(FeedOptionViewAction.Hide(scope)) + feedOptionViewModel.changeFeedUrl() + feedOptionViewModel.hideDrawer(scope) } ) } \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/feed/FeedOptionViewModel.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/feed/FeedOptionViewModel.kt new file mode 100644 index 0000000..6ed3941 --- /dev/null +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/feed/FeedOptionViewModel.kt @@ -0,0 +1,300 @@ +package me.ash.reader.ui.page.home.feeds.drawer.feed + +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.ModalBottomSheetState +import androidx.compose.material.ModalBottomSheetValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import me.ash.reader.data.entity.Feed +import me.ash.reader.data.entity.Group +import me.ash.reader.data.module.DispatcherIO +import me.ash.reader.data.module.DispatcherMain +import me.ash.reader.data.repository.RssRepository +import javax.inject.Inject + +@OptIn(ExperimentalMaterialApi::class) +@HiltViewModel +class FeedOptionViewModel @Inject constructor( + private val rssRepository: RssRepository, + @DispatcherMain + private val dispatcherMain: CoroutineDispatcher, + @DispatcherIO + private val dispatcherIO: CoroutineDispatcher, +) : ViewModel() { + private val _feedOptionUiState = MutableStateFlow(FeedOptionUiState()) + val feedOptionUiState: StateFlow = _feedOptionUiState.asStateFlow() + + init { + viewModelScope.launch(dispatcherIO) { + rssRepository.get().pullGroups().collect { groups -> + _feedOptionUiState.update { + it.copy( + groups = groups + ) + } + } + } + } + + private suspend fun fetchFeed(feedId: String) { + val feed = rssRepository.get().findFeedById(feedId) + _feedOptionUiState.update { + it.copy( + feed = feed, + selectedGroupId = feed?.groupId ?: "", + ) + } + } + + fun showDrawer(scope: CoroutineScope, feedId: String) { + scope.launch { + fetchFeed(feedId) + _feedOptionUiState.value.drawerState.show() + } + } + + fun hideDrawer(scope: CoroutineScope) { + scope.launch { + _feedOptionUiState.value.drawerState.hide() + } + } + + fun showNewGroupDialog() { + _feedOptionUiState.update { + it.copy( + newGroupDialogVisible = true, + newGroupContent = "", + ) + } + } + + fun hideNewGroupDialog() { + _feedOptionUiState.update { + it.copy( + newGroupDialogVisible = false, + newGroupContent = "", + ) + } + } + + fun inputNewGroup(content: String) { + _feedOptionUiState.update { + it.copy( + newGroupContent = content + ) + } + } + + fun addNewGroup() { + if (_feedOptionUiState.value.newGroupContent.isNotBlank()) { + viewModelScope.launch { + selectedGroup(rssRepository.get().addGroup(_feedOptionUiState.value.newGroupContent)) + hideNewGroupDialog() + } + } + } + + fun selectedGroup(groupId: String) { + viewModelScope.launch(dispatcherIO) { + _feedOptionUiState.value.feed?.let { + rssRepository.get().updateFeed( + it.copy( + groupId = groupId + ) + ) + fetchFeed(it.id) + } + } + } + + fun changeParseFullContentPreset() { + viewModelScope.launch(dispatcherIO) { + _feedOptionUiState.value.feed?.let { + rssRepository.get().updateFeed( + it.copy( + isFullContent = !it.isFullContent + ) + ) + fetchFeed(it.id) + } + } + } + + fun changeAllowNotificationPreset() { + viewModelScope.launch(dispatcherIO) { + _feedOptionUiState.value.feed?.let { + rssRepository.get().updateFeed( + it.copy( + isNotification = !it.isNotification + ) + ) + fetchFeed(it.id) + } + } + } + + fun delete(callback: () -> Unit = {}) { + _feedOptionUiState.value.feed?.let { + viewModelScope.launch(dispatcherIO) { + rssRepository.get().deleteFeed(it) + withContext(dispatcherMain) { + callback() + } + } + } + } + + fun hideDeleteDialog() { + _feedOptionUiState.update { + it.copy( + deleteDialogVisible = false, + ) + } + } + + fun showDeleteDialog() { + _feedOptionUiState.update { + it.copy( + deleteDialogVisible = true, + ) + } + } + + fun showClearDialog() { + _feedOptionUiState.update { + it.copy( + clearDialogVisible = true, + ) + } + } + + fun hideClearDialog() { + _feedOptionUiState.update { + it.copy( + clearDialogVisible = false, + ) + } + } + + fun clearFeed(callback: () -> Unit = {}) { + _feedOptionUiState.value.feed?.let { + viewModelScope.launch(dispatcherIO) { + rssRepository.get().deleteArticles(feed = it) + withContext(dispatcherMain) { + callback() + } + } + } + } + + fun renameFeed() { + _feedOptionUiState.value.feed?.let { + viewModelScope.launch { + rssRepository.get().updateFeed( + it.copy( + name = _feedOptionUiState.value.newName + ) + ) + _feedOptionUiState.update { + it.copy( + renameDialogVisible = false, + ) + } + } + } + } + + fun showRenameDialog() { + _feedOptionUiState.update { + it.copy( + renameDialogVisible = true, + newName = _feedOptionUiState.value.feed?.name ?: "", + ) + } + } + + fun hideRenameDialog() { + _feedOptionUiState.update { + it.copy( + renameDialogVisible = false, + newName = "", + ) + } + } + + fun inputNewName(content: String) { + _feedOptionUiState.update { + it.copy( + newName = content + ) + } + } + + fun showFeedUrlDialog() { + _feedOptionUiState.update { + it.copy( + changeUrlDialogVisible = true, + newUrl = _feedOptionUiState.value.feed?.url ?: "", + ) + } + } + + fun hideFeedUrlDialog() { + _feedOptionUiState.update { + it.copy( + changeUrlDialogVisible = false, + newUrl = "", + ) + } + } + + fun inputNewUrl(content: String) { + _feedOptionUiState.update { + it.copy( + newUrl = content + ) + } + } + + fun changeFeedUrl() { + _feedOptionUiState.value.feed?.let { + viewModelScope.launch { + rssRepository.get().updateFeed( + it.copy( + url = _feedOptionUiState.value.newUrl + ) + ) + _feedOptionUiState.update { + it.copy( + changeUrlDialogVisible = false, + ) + } + } + } + } +} + +@OptIn(ExperimentalMaterialApi::class) +data class FeedOptionUiState( + var drawerState: ModalBottomSheetState = ModalBottomSheetState(ModalBottomSheetValue.Hidden), + val feed: Feed? = null, + val selectedGroupId: String = "", + val newGroupContent: String = "", + val newGroupDialogVisible: Boolean = false, + val groups: List = emptyList(), + val deleteDialogVisible: Boolean = false, + val clearDialogVisible: Boolean = false, + val newName: String = "", + val renameDialogVisible: Boolean = false, + val newUrl: String = "", + val changeUrlDialogVisible: Boolean = false, +) diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/option/group/AllAllowNotificationDialog.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/group/AllAllowNotificationDialog.kt similarity index 63% rename from app/src/main/java/me/ash/reader/ui/page/home/feeds/option/group/AllAllowNotificationDialog.kt rename to app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/group/AllAllowNotificationDialog.kt index 32b599d..afe1068 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/option/group/AllAllowNotificationDialog.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/group/AllAllowNotificationDialog.kt @@ -1,4 +1,4 @@ -package me.ash.reader.ui.page.home.feeds.option.group +package me.ash.reader.ui.page.home.feeds.drawer.group import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Notifications @@ -7,33 +7,29 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.hilt.navigation.compose.hiltViewModel -import com.google.accompanist.pager.ExperimentalPagerApi import me.ash.reader.R -import me.ash.reader.ui.component.Dialog +import me.ash.reader.ui.component.base.RYDialog import me.ash.reader.ui.ext.collectAsStateValue import me.ash.reader.ui.ext.showToast -@OptIn(ExperimentalPagerApi::class) @Composable fun AllAllowNotificationDialog( - modifier: Modifier = Modifier, groupName: String, - viewModel: GroupOptionViewModel = hiltViewModel(), + groupOptionViewModel: GroupOptionViewModel = hiltViewModel(), ) { val context = LocalContext.current - val viewState = viewModel.viewState.collectAsStateValue() + val groupOptionUiState = groupOptionViewModel.groupOptionUiState.collectAsStateValue() val scope = rememberCoroutineScope() val allowToastString = stringResource(R.string.all_allow_notification_toast, groupName) val denyToastString = stringResource(R.string.all_deny_notification_toast, groupName) - Dialog( - visible = viewState.allAllowNotificationDialogVisible, + RYDialog( + visible = groupOptionUiState.allAllowNotificationDialogVisible, onDismissRequest = { - viewModel.dispatch(GroupOptionViewAction.HideAllAllowNotificationDialog) + groupOptionViewModel.hideAllAllowNotificationDialog() }, icon = { Icon( @@ -50,11 +46,11 @@ fun AllAllowNotificationDialog( confirmButton = { TextButton( onClick = { - viewModel.dispatch(GroupOptionViewAction.AllAllowNotification(true) { - viewModel.dispatch(GroupOptionViewAction.HideAllAllowNotificationDialog) - viewModel.dispatch(GroupOptionViewAction.Hide(scope)) + groupOptionViewModel.allAllowNotification(true) { + groupOptionViewModel.hideAllAllowNotificationDialog() + groupOptionViewModel.hideDrawer(scope) context.showToast(allowToastString) - }) + } } ) { Text( @@ -65,11 +61,11 @@ fun AllAllowNotificationDialog( dismissButton = { TextButton( onClick = { - viewModel.dispatch(GroupOptionViewAction.AllAllowNotification(false) { - viewModel.dispatch(GroupOptionViewAction.HideAllAllowNotificationDialog) - viewModel.dispatch(GroupOptionViewAction.Hide(scope)) + groupOptionViewModel.allAllowNotification(false) { + groupOptionViewModel.hideAllAllowNotificationDialog() + groupOptionViewModel.hideDrawer(scope) context.showToast(denyToastString) - }) + } } ) { Text( diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/option/group/AllMoveToGroupDialog.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/group/AllMoveToGroupDialog.kt similarity index 63% rename from app/src/main/java/me/ash/reader/ui/page/home/feeds/option/group/AllMoveToGroupDialog.kt rename to app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/group/AllMoveToGroupDialog.kt index e5ddb62..408c225 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/option/group/AllMoveToGroupDialog.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/group/AllMoveToGroupDialog.kt @@ -1,4 +1,4 @@ -package me.ash.reader.ui.page.home.feeds.option.group +package me.ash.reader.ui.page.home.feeds.drawer.group import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.DriveFileMove @@ -7,33 +7,31 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.hilt.navigation.compose.hiltViewModel -import com.google.accompanist.pager.ExperimentalPagerApi import me.ash.reader.R -import me.ash.reader.ui.component.Dialog +import me.ash.reader.ui.component.base.RYDialog import me.ash.reader.ui.ext.collectAsStateValue import me.ash.reader.ui.ext.showToast -@OptIn(ExperimentalPagerApi::class) @Composable fun AllMoveToGroupDialog( - modifier: Modifier = Modifier, groupName: String, - viewModel: GroupOptionViewModel = hiltViewModel(), + groupOptionViewModel: GroupOptionViewModel = hiltViewModel(), ) { val context = LocalContext.current - val viewState = viewModel.viewState.collectAsStateValue() + val groupOptionUiState = groupOptionViewModel.groupOptionUiState.collectAsStateValue() val scope = rememberCoroutineScope() - val toastString = - stringResource(R.string.all_move_to_group_toast, viewState.targetGroup?.name ?: "") + val toastString = stringResource( + R.string.all_move_to_group_toast, + groupOptionUiState.targetGroup?.name ?: "" + ) - Dialog( - visible = viewState.allMoveToGroupDialogVisible, + RYDialog( + visible = groupOptionUiState.allMoveToGroupDialogVisible, onDismissRequest = { - viewModel.dispatch(GroupOptionViewAction.HideAllMoveToGroupDialog) + groupOptionViewModel.hideAllMoveToGroupDialog() }, icon = { Icon( @@ -49,18 +47,18 @@ fun AllMoveToGroupDialog( text = stringResource( R.string.all_move_to_group_tips, groupName, - viewState.targetGroup?.name ?: "", + groupOptionUiState.targetGroup?.name ?: "", ) ) }, confirmButton = { TextButton( onClick = { - viewModel.dispatch(GroupOptionViewAction.AllMoveToGroup { - viewModel.dispatch(GroupOptionViewAction.HideAllMoveToGroupDialog) - viewModel.dispatch(GroupOptionViewAction.Hide(scope)) + groupOptionViewModel.allMoveToGroup { + groupOptionViewModel.hideAllMoveToGroupDialog() + groupOptionViewModel.hideDrawer(scope) context.showToast(toastString) - }) + } } ) { Text( @@ -71,7 +69,7 @@ fun AllMoveToGroupDialog( dismissButton = { TextButton( onClick = { - viewModel.dispatch(GroupOptionViewAction.HideAllMoveToGroupDialog) + groupOptionViewModel.hideAllMoveToGroupDialog() } ) { Text( diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/option/group/AllParseFullContentDialog.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/group/AllParseFullContentDialog.kt similarity index 63% rename from app/src/main/java/me/ash/reader/ui/page/home/feeds/option/group/AllParseFullContentDialog.kt rename to app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/group/AllParseFullContentDialog.kt index c5db1da..7a5b747 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/option/group/AllParseFullContentDialog.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/group/AllParseFullContentDialog.kt @@ -1,4 +1,4 @@ -package me.ash.reader.ui.page.home.feeds.option.group +package me.ash.reader.ui.page.home.feeds.drawer.group import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Article @@ -7,33 +7,29 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.hilt.navigation.compose.hiltViewModel -import com.google.accompanist.pager.ExperimentalPagerApi import me.ash.reader.R -import me.ash.reader.ui.component.Dialog +import me.ash.reader.ui.component.base.RYDialog import me.ash.reader.ui.ext.collectAsStateValue import me.ash.reader.ui.ext.showToast -@OptIn(ExperimentalPagerApi::class) @Composable fun AllParseFullContentDialog( - modifier: Modifier = Modifier, groupName: String, - viewModel: GroupOptionViewModel = hiltViewModel(), + groupOptionViewModel: GroupOptionViewModel = hiltViewModel(), ) { val context = LocalContext.current - val viewState = viewModel.viewState.collectAsStateValue() + val groupOptionUiState = groupOptionViewModel.groupOptionUiState.collectAsStateValue() val scope = rememberCoroutineScope() val allowToastString = stringResource(R.string.all_parse_full_content_toast, groupName) val denyToastString = stringResource(R.string.all_deny_parse_full_content_toast, groupName) - Dialog( - visible = viewState.allParseFullContentDialogVisible, + RYDialog( + visible = groupOptionUiState.allParseFullContentDialogVisible, onDismissRequest = { - viewModel.dispatch(GroupOptionViewAction.HideAllParseFullContentDialog) + groupOptionViewModel.hideAllParseFullContentDialog() }, icon = { Icon( @@ -50,11 +46,11 @@ fun AllParseFullContentDialog( confirmButton = { TextButton( onClick = { - viewModel.dispatch(GroupOptionViewAction.AllParseFullContent(true) { - viewModel.dispatch(GroupOptionViewAction.HideAllParseFullContentDialog) - viewModel.dispatch(GroupOptionViewAction.Hide(scope)) + groupOptionViewModel.allParseFullContent(true) { + groupOptionViewModel.hideAllParseFullContentDialog() + groupOptionViewModel.hideDrawer(scope) context.showToast(allowToastString) - }) + } } ) { Text( @@ -65,11 +61,11 @@ fun AllParseFullContentDialog( dismissButton = { TextButton( onClick = { - viewModel.dispatch(GroupOptionViewAction.AllParseFullContent(false) { - viewModel.dispatch(GroupOptionViewAction.HideAllParseFullContentDialog) - viewModel.dispatch(GroupOptionViewAction.Hide(scope)) + groupOptionViewModel.allParseFullContent(false) { + groupOptionViewModel.hideAllParseFullContentDialog() + groupOptionViewModel.hideDrawer(scope) context.showToast(denyToastString) - }) + } } ) { Text( diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/option/group/ClearGroupDialog.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/group/ClearGroupDialog.kt similarity index 68% rename from app/src/main/java/me/ash/reader/ui/page/home/feeds/option/group/ClearGroupDialog.kt rename to app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/group/ClearGroupDialog.kt index b02403a..fadd2a2 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/option/group/ClearGroupDialog.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/group/ClearGroupDialog.kt @@ -1,4 +1,4 @@ -package me.ash.reader.ui.page.home.feeds.option.group +package me.ash.reader.ui.page.home.feeds.drawer.group import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.DeleteForever @@ -7,32 +7,28 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.hilt.navigation.compose.hiltViewModel -import com.google.accompanist.pager.ExperimentalPagerApi import me.ash.reader.R -import me.ash.reader.ui.component.Dialog +import me.ash.reader.ui.component.base.RYDialog import me.ash.reader.ui.ext.collectAsStateValue import me.ash.reader.ui.ext.showToast -@OptIn(ExperimentalPagerApi::class) @Composable fun ClearGroupDialog( - modifier: Modifier = Modifier, groupName: String, - viewModel: GroupOptionViewModel = hiltViewModel(), + groupOptionViewModel: GroupOptionViewModel = hiltViewModel(), ) { val context = LocalContext.current - val viewState = viewModel.viewState.collectAsStateValue() + val groupOptionUiState = groupOptionViewModel.groupOptionUiState.collectAsStateValue() val scope = rememberCoroutineScope() val toastString = stringResource(R.string.clear_articles_in_group_toast, groupName) - Dialog( - visible = viewState.clearDialogVisible, + RYDialog( + visible = groupOptionUiState.clearDialogVisible, onDismissRequest = { - viewModel.dispatch(GroupOptionViewAction.HideClearDialog) + groupOptionViewModel.hideClearDialog() }, icon = { Icon( @@ -49,11 +45,11 @@ fun ClearGroupDialog( confirmButton = { TextButton( onClick = { - viewModel.dispatch(GroupOptionViewAction.Clear { - viewModel.dispatch(GroupOptionViewAction.HideClearDialog) - viewModel.dispatch(GroupOptionViewAction.Hide(scope)) + groupOptionViewModel.clear { + groupOptionViewModel.hideClearDialog() + groupOptionViewModel.hideDrawer(scope) context.showToast(toastString) - }) + } } ) { Text( @@ -64,7 +60,7 @@ fun ClearGroupDialog( dismissButton = { TextButton( onClick = { - viewModel.dispatch(GroupOptionViewAction.HideClearDialog) + groupOptionViewModel.hideClearDialog() } ) { Text( diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/option/group/DeleteGroupDialog.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/group/DeleteGroupDialog.kt similarity index 67% rename from app/src/main/java/me/ash/reader/ui/page/home/feeds/option/group/DeleteGroupDialog.kt rename to app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/group/DeleteGroupDialog.kt index 8c39dc4..9809fec 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/option/group/DeleteGroupDialog.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/group/DeleteGroupDialog.kt @@ -1,4 +1,4 @@ -package me.ash.reader.ui.page.home.feeds.option.group +package me.ash.reader.ui.page.home.feeds.drawer.group import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.DeleteForever @@ -7,32 +7,28 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.hilt.navigation.compose.hiltViewModel -import com.google.accompanist.pager.ExperimentalPagerApi import me.ash.reader.R -import me.ash.reader.ui.component.Dialog +import me.ash.reader.ui.component.base.RYDialog import me.ash.reader.ui.ext.collectAsStateValue import me.ash.reader.ui.ext.showToast -@OptIn(ExperimentalPagerApi::class) @Composable fun DeleteGroupDialog( - modifier: Modifier = Modifier, groupName: String, - viewModel: GroupOptionViewModel = hiltViewModel(), + groupOptionViewModel: GroupOptionViewModel = hiltViewModel(), ) { val context = LocalContext.current - val viewState = viewModel.viewState.collectAsStateValue() + val groupOptionUiState = groupOptionViewModel.groupOptionUiState.collectAsStateValue() val scope = rememberCoroutineScope() val toastString = stringResource(R.string.delete_toast, groupName) - Dialog( - visible = viewState.deleteDialogVisible, + RYDialog( + visible = groupOptionUiState.deleteDialogVisible, onDismissRequest = { - viewModel.dispatch(GroupOptionViewAction.HideDeleteDialog) + groupOptionViewModel.hideDeleteDialog() }, icon = { Icon( @@ -49,11 +45,11 @@ fun DeleteGroupDialog( confirmButton = { TextButton( onClick = { - viewModel.dispatch(GroupOptionViewAction.Delete { - viewModel.dispatch(GroupOptionViewAction.HideDeleteDialog) - viewModel.dispatch(GroupOptionViewAction.Hide(scope)) + groupOptionViewModel.delete { + groupOptionViewModel.hideDeleteDialog() + groupOptionViewModel.hideDrawer(scope) context.showToast(toastString) - }) + } } ) { Text( @@ -64,7 +60,7 @@ fun DeleteGroupDialog( dismissButton = { TextButton( onClick = { - viewModel.dispatch(GroupOptionViewAction.HideDeleteDialog) + groupOptionViewModel.hideDeleteDialog() } ) { Text( diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/option/group/GroupOptionDrawer.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/group/GroupOptionDrawer.kt similarity index 77% rename from app/src/main/java/me/ash/reader/ui/page/home/feeds/option/group/GroupOptionDrawer.kt rename to app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/group/GroupOptionDrawer.kt index 480aa1f..b64d5aa 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/option/group/GroupOptionDrawer.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/group/GroupOptionDrawer.kt @@ -1,4 +1,4 @@ -package me.ash.reader.ui.page.home.feeds.option.group +package me.ash.reader.ui.page.home.feeds.drawer.group import android.content.Context import androidx.activity.compose.BackHandler @@ -33,33 +33,32 @@ import com.google.accompanist.flowlayout.MainAxisAlignment import kotlinx.coroutines.launch import me.ash.reader.R import me.ash.reader.data.entity.Group -import me.ash.reader.ui.component.BottomDrawer -import me.ash.reader.ui.component.SelectionChip -import me.ash.reader.ui.component.Subtitle -import me.ash.reader.ui.component.TextFieldDialog +import me.ash.reader.ui.component.base.BottomDrawer +import me.ash.reader.ui.component.base.RYSelectionChip +import me.ash.reader.ui.component.base.Subtitle +import me.ash.reader.ui.component.base.TextFieldDialog import me.ash.reader.ui.ext.* @OptIn(ExperimentalMaterialApi::class) @Composable fun GroupOptionDrawer( - modifier: Modifier = Modifier, groupOptionViewModel: GroupOptionViewModel = hiltViewModel(), content: @Composable () -> Unit = {}, ) { val context = LocalContext.current val scope = rememberCoroutineScope() - val viewState = groupOptionViewModel.viewState.collectAsStateValue() - val group = viewState.group - val toastString = stringResource(R.string.rename_toast, viewState.newName) + val groupOptionUiState = groupOptionViewModel.groupOptionUiState.collectAsStateValue() + val group = groupOptionUiState.group + val toastString = stringResource(R.string.rename_toast, groupOptionUiState.newName) - BackHandler(viewState.drawerState.isVisible) { + BackHandler(groupOptionUiState.drawerState.isVisible) { scope.launch { - viewState.drawerState.hide() + groupOptionUiState.drawerState.hide() } } BottomDrawer( - drawerState = viewState.drawerState, + drawerState = groupOptionUiState.drawerState, sheetContent = { Column(modifier = Modifier.navigationBarsPadding()) { Column( @@ -75,7 +74,7 @@ fun GroupOptionDrawer( Spacer(modifier = Modifier.height(16.dp)) Text( modifier = Modifier.roundClick { - groupOptionViewModel.dispatch(GroupOptionViewAction.ShowRenameDialog) + groupOptionViewModel.showRenameDialog() }, text = group?.name ?: stringResource(R.string.unknown), style = MaterialTheme.typography.headlineSmall, @@ -106,15 +105,15 @@ fun GroupOptionDrawer( Spacer(modifier = Modifier.height(10.dp)) Preset(groupOptionViewModel, group, context) - if (viewState.groups.size != 1) { + if (groupOptionUiState.groups.size != 1) { Spacer(modifier = Modifier.height(26.dp)) Subtitle(text = stringResource(R.string.move_to_group)) Spacer(modifier = Modifier.height(10.dp)) - if (viewState.groups.size > 6) { - LazyRowGroups(viewState, group, groupOptionViewModel) + if (groupOptionUiState.groups.size > 6) { + LazyRowGroups(groupOptionUiState, group, groupOptionViewModel) } else { - FlowRowGroups(viewState, group, groupOptionViewModel) + FlowRowGroups(groupOptionUiState, group, groupOptionViewModel) } } @@ -132,20 +131,20 @@ fun GroupOptionDrawer( AllParseFullContentDialog(groupName = group?.name ?: "") AllMoveToGroupDialog(groupName = group?.name ?: "") TextFieldDialog( - visible = viewState.renameDialogVisible, + visible = groupOptionUiState.renameDialogVisible, title = stringResource(R.string.rename), icon = Icons.Outlined.Edit, - value = viewState.newName, + value = groupOptionUiState.newName, placeholder = stringResource(R.string.name), onValueChange = { - groupOptionViewModel.dispatch(GroupOptionViewAction.InputNewName(it)) + groupOptionViewModel.inputNewName(it) }, onDismissRequest = { - groupOptionViewModel.dispatch(GroupOptionViewAction.HideRenameDialog) + groupOptionViewModel.hideRenameDialog() }, onConfirm = { - groupOptionViewModel.dispatch(GroupOptionViewAction.Rename) - groupOptionViewModel.dispatch(GroupOptionViewAction.Hide(scope)) + groupOptionViewModel.rename() + groupOptionViewModel.hideDrawer(scope) context.showToast(toastString) } ) @@ -163,7 +162,7 @@ private fun Preset( crossAxisSpacing = 10.dp, mainAxisSpacing = 10.dp, ) { - SelectionChip( + RYSelectionChip( modifier = Modifier.animateContentSize(), content = stringResource(R.string.allow_notification), selected = false, @@ -177,9 +176,9 @@ private fun Preset( ) }, ) { - groupOptionViewModel.dispatch(GroupOptionViewAction.ShowAllAllowNotificationDialog) + groupOptionViewModel.showAllAllowNotificationDialog() } - SelectionChip( + RYSelectionChip( modifier = Modifier.animateContentSize(), content = stringResource(R.string.parse_full_content), selected = false, @@ -193,22 +192,22 @@ private fun Preset( ) }, ) { - groupOptionViewModel.dispatch(GroupOptionViewAction.ShowAllParseFullContentDialog) + groupOptionViewModel.showAllParseFullContentDialog() } - SelectionChip( + RYSelectionChip( modifier = Modifier.animateContentSize(), content = stringResource(R.string.clear_articles), selected = false, ) { - groupOptionViewModel.dispatch(GroupOptionViewAction.ShowClearDialog) + groupOptionViewModel.showClearDialog() } if (group?.id != context.currentAccountId.getDefaultGroupId()) { - SelectionChip( + RYSelectionChip( modifier = Modifier.animateContentSize(), content = stringResource(R.string.delete_group), selected = false, ) { - groupOptionViewModel.dispatch(GroupOptionViewAction.ShowDeleteDialog) + groupOptionViewModel.showDeleteDialog() } } } @@ -216,7 +215,7 @@ private fun Preset( @Composable private fun FlowRowGroups( - viewState: GroupOptionViewState, + groupOptionUiState: GroupOptionUiState, group: Group?, groupOptionViewModel: GroupOptionViewModel ) { @@ -226,16 +225,14 @@ private fun FlowRowGroups( crossAxisSpacing = 10.dp, mainAxisSpacing = 10.dp, ) { - viewState.groups.forEach { + groupOptionUiState.groups.forEach { if (it.id != group?.id) { - SelectionChip( + RYSelectionChip( modifier = Modifier.animateContentSize(), content = it.name, selected = false, ) { - groupOptionViewModel.dispatch( - GroupOptionViewAction.ShowAllMoveToGroupDialog(it) - ) + groupOptionViewModel.showAllMoveToGroupDialog(it) } } } @@ -244,21 +241,19 @@ private fun FlowRowGroups( @Composable private fun LazyRowGroups( - viewState: GroupOptionViewState, + groupOptionUiState: GroupOptionUiState, group: Group?, groupOptionViewModel: GroupOptionViewModel ) { LazyRow { - items(viewState.groups) { + items(groupOptionUiState.groups) { if (it.id != group?.id) { - SelectionChip( + RYSelectionChip( modifier = Modifier.animateContentSize(), content = it.name, selected = false, ) { - groupOptionViewModel.dispatch( - GroupOptionViewAction.ShowAllMoveToGroupDialog(it) - ) + groupOptionViewModel.showAllMoveToGroupDialog(it) } } Spacer(modifier = Modifier.width(10.dp)) diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/group/GroupOptionViewModel.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/group/GroupOptionViewModel.kt new file mode 100644 index 0000000..1c2ee46 --- /dev/null +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/drawer/group/GroupOptionViewModel.kt @@ -0,0 +1,260 @@ +package me.ash.reader.ui.page.home.feeds.drawer.group + +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.ModalBottomSheetState +import androidx.compose.material.ModalBottomSheetValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import me.ash.reader.data.entity.Group +import me.ash.reader.data.module.DispatcherIO +import me.ash.reader.data.module.DispatcherMain +import me.ash.reader.data.repository.RssRepository +import javax.inject.Inject + +@OptIn(ExperimentalMaterialApi::class) +@HiltViewModel +class GroupOptionViewModel @Inject constructor( + private val rssRepository: RssRepository, + @DispatcherMain + private val dispatcherMain: CoroutineDispatcher, + @DispatcherIO + private val dispatcherIO: CoroutineDispatcher, +) : ViewModel() { + private val _groupOptionUiState = MutableStateFlow(GroupOptionUiState()) + val groupOptionUiState: StateFlow = _groupOptionUiState.asStateFlow() + + init { + viewModelScope.launch(dispatcherIO) { + rssRepository.get().pullGroups().collect { groups -> + _groupOptionUiState.update { + it.copy( + groups = groups + ) + } + } + } + } + + fun showDrawer(scope: CoroutineScope, groupId: String) { + scope.launch { + _groupOptionUiState.update { + it.copy( + group = rssRepository.get().findGroupById(groupId), + ) + } + _groupOptionUiState.value.drawerState.show() + } + } + + fun hideDrawer(scope: CoroutineScope) { + scope.launch { + _groupOptionUiState.value.drawerState.hide() + } + } + + fun allAllowNotification(isNotification: Boolean, callback: () -> Unit = {}) { + _groupOptionUiState.value.group?.let { + viewModelScope.launch(dispatcherIO) { + rssRepository.get().groupAllowNotification(it, isNotification) + withContext(dispatcherMain) { + callback() + } + } + } + } + + fun showAllAllowNotificationDialog() { + _groupOptionUiState.update { + it.copy( + allAllowNotificationDialogVisible = true, + ) + } + } + + fun hideAllAllowNotificationDialog() { + _groupOptionUiState.update { + it.copy( + allAllowNotificationDialogVisible = false, + ) + } + } + + fun allParseFullContent(isFullContent: Boolean, callback: () -> Unit = {}) { + _groupOptionUiState.value.group?.let { + viewModelScope.launch(dispatcherIO) { + rssRepository.get().groupParseFullContent(it, isFullContent) + withContext(dispatcherMain) { + callback() + } + } + } + } + + fun showAllParseFullContentDialog() { + _groupOptionUiState.update { + it.copy( + allParseFullContentDialogVisible = true, + ) + } + } + + fun hideAllParseFullContentDialog() { + _groupOptionUiState.update { + it.copy( + allParseFullContentDialogVisible = false, + ) + } + } + + fun delete(callback: () -> Unit = {}) { + _groupOptionUiState.value.group?.let { + viewModelScope.launch(dispatcherIO) { + rssRepository.get().deleteGroup(it) + withContext(dispatcherMain) { + callback() + } + } + } + } + + fun showDeleteDialog() { + _groupOptionUiState.update { + it.copy( + deleteDialogVisible = true, + ) + } + } + + fun hideDeleteDialog() { + _groupOptionUiState.update { + it.copy( + deleteDialogVisible = false, + ) + } + } + + fun showClearDialog() { + _groupOptionUiState.update { + it.copy( + clearDialogVisible = true, + ) + } + } + + fun hideClearDialog() { + _groupOptionUiState.update { + it.copy( + clearDialogVisible = false, + ) + } + } + + fun clear(callback: () -> Unit = {}) { + _groupOptionUiState.value.group?.let { + viewModelScope.launch(dispatcherIO) { + rssRepository.get().deleteArticles(group = it) + withContext(dispatcherMain) { + callback() + } + } + } + } + + fun allMoveToGroup(callback: () -> Unit) { + _groupOptionUiState.value.group?.let { group -> + _groupOptionUiState.value.targetGroup?.let { targetGroup -> + viewModelScope.launch(dispatcherIO) { + rssRepository.get().groupMoveToTargetGroup(group, targetGroup) + withContext(dispatcherMain) { + callback() + } + } + } + } + } + + fun showAllMoveToGroupDialog(targetGroup: Group) { + _groupOptionUiState.update { + it.copy( + targetGroup = targetGroup, + allMoveToGroupDialogVisible = true, + ) + } + } + + fun hideAllMoveToGroupDialog() { + _groupOptionUiState.update { + it.copy( + targetGroup = null, + allMoveToGroupDialogVisible = false, + ) + } + } + + fun rename() { + _groupOptionUiState.value.group?.let { + viewModelScope.launch { + rssRepository.get().updateGroup( + it.copy( + name = _groupOptionUiState.value.newName + ) + ) + _groupOptionUiState.update { + it.copy( + renameDialogVisible = false, + ) + } + } + } + } + + fun showRenameDialog() { + _groupOptionUiState.update { + it.copy( + renameDialogVisible = true, + newName = _groupOptionUiState.value.group?.name ?: "", + ) + } + } + + fun hideRenameDialog() { + _groupOptionUiState.update { + it.copy( + renameDialogVisible = false, + newName = "", + ) + } + } + + fun inputNewName(content: String) { + _groupOptionUiState.update { + it.copy( + newName = content + ) + } + } +} + +@OptIn(ExperimentalMaterialApi::class) +data class GroupOptionUiState( + var drawerState: ModalBottomSheetState = ModalBottomSheetState(ModalBottomSheetValue.Hidden), + val group: Group? = null, + val targetGroup: Group? = null, + val groups: List = emptyList(), + val allAllowNotificationDialogVisible: Boolean = false, + val allParseFullContentDialogVisible: Boolean = false, + val allMoveToGroupDialogVisible: Boolean = false, + val deleteDialogVisible: Boolean = false, + val clearDialogVisible: Boolean = false, + val newName: String = "", + val renameDialogVisible: Boolean = false, +) diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/option/feed/FeedOptionViewModel.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/option/feed/FeedOptionViewModel.kt deleted file mode 100644 index d31c510..0000000 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/option/feed/FeedOptionViewModel.kt +++ /dev/null @@ -1,353 +0,0 @@ -package me.ash.reader.ui.page.home.feeds.option.feed - -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.ModalBottomSheetState -import androidx.compose.material.ModalBottomSheetValue -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.google.accompanist.pager.ExperimentalPagerApi -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import me.ash.reader.data.entity.Feed -import me.ash.reader.data.entity.Group -import me.ash.reader.data.repository.RssRepository -import javax.inject.Inject - -@OptIn( - ExperimentalPagerApi::class, - ExperimentalMaterialApi::class -) -@HiltViewModel -class FeedOptionViewModel @Inject constructor( - private val rssRepository: RssRepository, -) : ViewModel() { - private val _viewState = MutableStateFlow(FeedOptionViewState()) - val viewState: StateFlow = _viewState.asStateFlow() - - init { - viewModelScope.launch(Dispatchers.IO) { - rssRepository.get().pullGroups().collect { groups -> - _viewState.update { - it.copy( - groups = groups - ) - } - } - } - } - - fun dispatch(action: FeedOptionViewAction) { - when (action) { - is FeedOptionViewAction.Show -> show(action.scope, action.feedId) - is FeedOptionViewAction.Hide -> hide(action.scope) - is FeedOptionViewAction.SelectedGroup -> selectedGroup(action.groupId) - is FeedOptionViewAction.InputNewGroup -> inputNewGroup(action.content) - is FeedOptionViewAction.ChangeAllowNotificationPreset -> changeAllowNotificationPreset() - is FeedOptionViewAction.ChangeParseFullContentPreset -> changeParseFullContentPreset() - is FeedOptionViewAction.ShowDeleteDialog -> showDeleteDialog() - is FeedOptionViewAction.HideDeleteDialog -> hideDeleteDialog() - is FeedOptionViewAction.Delete -> delete(action.callback) - is FeedOptionViewAction.ShowClearDialog -> showClearDialog() - is FeedOptionViewAction.HideClearDialog -> hideClearDialog() - is FeedOptionViewAction.Clear -> clear(action.callback) - is FeedOptionViewAction.AddNewGroup -> addNewGroup() - is FeedOptionViewAction.ShowNewGroupDialog -> changeNewGroupDialogVisible(true) - is FeedOptionViewAction.HideNewGroupDialog -> changeNewGroupDialogVisible(false) - is FeedOptionViewAction.InputNewName -> inputNewName(action.content) - is FeedOptionViewAction.Rename -> rename() - is FeedOptionViewAction.ShowRenameDialog -> changeRenameDialogVisible(true) - is FeedOptionViewAction.HideRenameDialog -> changeRenameDialogVisible(false) - is FeedOptionViewAction.InputNewUrl -> inputNewUrl(action.content) - is FeedOptionViewAction.ChangeUrl -> changeFeedUrl() - is FeedOptionViewAction.HideChangeUrlDialog -> changeFeedUrlDialogVisible(false) - is FeedOptionViewAction.ShowChangeUrlDialog -> changeFeedUrlDialogVisible(true) - } - } - - private suspend fun fetchFeed(feedId: String) { - val feed = rssRepository.get().findFeedById(feedId) - _viewState.update { - it.copy( - feed = feed, - selectedGroupId = feed?.groupId ?: "", - ) - } - } - - private fun show(scope: CoroutineScope, feedId: String) { - scope.launch { - fetchFeed(feedId) - _viewState.value.drawerState.show() - } - } - - private fun hide(scope: CoroutineScope) { - scope.launch { - _viewState.value.drawerState.hide() - } - } - - private fun changeNewGroupDialogVisible(visible: Boolean) { - _viewState.update { - it.copy( - newGroupDialogVisible = visible, - newGroupContent = "", - ) - } - } - - private fun inputNewGroup(content: String) { - _viewState.update { - it.copy( - newGroupContent = content - ) - } - } - - private fun addNewGroup() { - if (_viewState.value.newGroupContent.isNotBlank()) { - viewModelScope.launch { - selectedGroup(rssRepository.get().addGroup(_viewState.value.newGroupContent)) - changeNewGroupDialogVisible(false) - } - } - } - - private fun selectedGroup(groupId: String) { - viewModelScope.launch(Dispatchers.IO) { - _viewState.value.feed?.let { - rssRepository.get().updateFeed( - it.copy( - groupId = groupId - ) - ) - fetchFeed(it.id) - } - } - } - - private fun changeParseFullContentPreset() { - viewModelScope.launch(Dispatchers.IO) { - _viewState.value.feed?.let { - rssRepository.get().updateFeed( - it.copy( - isFullContent = !it.isFullContent - ) - ) - fetchFeed(it.id) - } - } - } - - private fun changeAllowNotificationPreset() { - viewModelScope.launch(Dispatchers.IO) { - _viewState.value.feed?.let { - rssRepository.get().updateFeed( - it.copy( - isNotification = !it.isNotification - ) - ) - fetchFeed(it.id) - } - } - } - - private fun delete(callback: () -> Unit = {}) { - _viewState.value.feed?.let { - viewModelScope.launch(Dispatchers.IO) { - rssRepository.get().deleteFeed(it) - withContext(Dispatchers.Main) { - callback() - } - } - } - } - - private fun hideDeleteDialog() { - _viewState.update { - it.copy( - deleteDialogVisible = false, - ) - } - } - - private fun showDeleteDialog() { - _viewState.update { - it.copy( - deleteDialogVisible = true, - ) - } - } - - private fun showClearDialog() { - _viewState.update { - it.copy( - clearDialogVisible = true, - ) - } - } - - private fun hideClearDialog() { - _viewState.update { - it.copy( - clearDialogVisible = false, - ) - } - } - - private fun clear(callback: () -> Unit = {}) { - _viewState.value.feed?.let { - viewModelScope.launch(Dispatchers.IO) { - rssRepository.get().deleteArticles(feed = it) - withContext(Dispatchers.Main) { - callback() - } - } - } - } - - private fun rename() { - _viewState.value.feed?.let { - viewModelScope.launch { - rssRepository.get().updateFeed( - it.copy( - name = _viewState.value.newName - ) - ) - _viewState.update { - it.copy( - renameDialogVisible = false, - ) - } - } - } - } - - private fun changeRenameDialogVisible(visible: Boolean) { - _viewState.update { - it.copy( - renameDialogVisible = visible, - newName = if (visible) _viewState.value.feed?.name ?: "" else "", - ) - } - } - - private fun inputNewName(content: String) { - _viewState.update { - it.copy( - newName = content - ) - } - } - - private fun changeFeedUrlDialogVisible(visible: Boolean) { - _viewState.update { - it.copy( - changeUrlDialogVisible = visible, - newUrl = if (visible) _viewState.value.feed?.url ?: "" else "", - ) - } - } - - private fun inputNewUrl(content: String) { - _viewState.update { - it.copy( - newUrl = content - ) - } - } - - private fun changeFeedUrl() { - _viewState.value.feed?.let { - viewModelScope.launch { - rssRepository.get().updateFeed( - it.copy( - url = _viewState.value.newUrl - ) - ) - _viewState.update { - it.copy( - changeUrlDialogVisible = false, - ) - } - } - } - } -} - -@OptIn(ExperimentalMaterialApi::class) -data class FeedOptionViewState( - var drawerState: ModalBottomSheetState = ModalBottomSheetState(ModalBottomSheetValue.Hidden), - val feed: Feed? = null, - val selectedGroupId: String = "", - val newGroupContent: String = "", - val newGroupDialogVisible: Boolean = false, - val groups: List = emptyList(), - val deleteDialogVisible: Boolean = false, - val clearDialogVisible: Boolean = false, - val newName: String = "", - val renameDialogVisible: Boolean = false, - val newUrl: String = "", - val changeUrlDialogVisible: Boolean = false, -) - -sealed class FeedOptionViewAction { - data class Show( - val scope: CoroutineScope, - val feedId: String - ) : FeedOptionViewAction() - - data class Hide( - val scope: CoroutineScope, - ) : FeedOptionViewAction() - - object ChangeAllowNotificationPreset : FeedOptionViewAction() - object ChangeParseFullContentPreset : FeedOptionViewAction() - - data class SelectedGroup( - val groupId: String - ) : FeedOptionViewAction() - - data class InputNewGroup( - val content: String - ) : FeedOptionViewAction() - - data class Delete( - val callback: () -> Unit = {} - ) : FeedOptionViewAction() - - object ShowDeleteDialog : FeedOptionViewAction() - object HideDeleteDialog : FeedOptionViewAction() - - data class Clear( - val callback: () -> Unit = {} - ) : FeedOptionViewAction() - - object ShowClearDialog : FeedOptionViewAction() - object HideClearDialog : FeedOptionViewAction() - - object ShowNewGroupDialog : FeedOptionViewAction() - object HideNewGroupDialog : FeedOptionViewAction() - object AddNewGroup : FeedOptionViewAction() - - object ShowRenameDialog : FeedOptionViewAction() - object HideRenameDialog : FeedOptionViewAction() - object Rename : FeedOptionViewAction() - data class InputNewName( - val content: String - ) : FeedOptionViewAction() - - object ShowChangeUrlDialog : FeedOptionViewAction() - object HideChangeUrlDialog : FeedOptionViewAction() - object ChangeUrl : FeedOptionViewAction() - data class InputNewUrl( - val content: String - ) : FeedOptionViewAction() -} diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/option/group/GroupOptionViewModel.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/option/group/GroupOptionViewModel.kt deleted file mode 100644 index b696c8d..0000000 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/option/group/GroupOptionViewModel.kt +++ /dev/null @@ -1,319 +0,0 @@ -package me.ash.reader.ui.page.home.feeds.option.group - -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.ModalBottomSheetState -import androidx.compose.material.ModalBottomSheetValue -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.google.accompanist.pager.ExperimentalPagerApi -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import me.ash.reader.data.entity.Group -import me.ash.reader.data.repository.RssRepository -import javax.inject.Inject - -@OptIn( - ExperimentalPagerApi::class, - ExperimentalMaterialApi::class -) -@HiltViewModel -class GroupOptionViewModel @Inject constructor( - private val rssRepository: RssRepository, -) : ViewModel() { - private val _viewState = MutableStateFlow(GroupOptionViewState()) - val viewState: StateFlow = _viewState.asStateFlow() - - init { - viewModelScope.launch(Dispatchers.IO) { - rssRepository.get().pullGroups().collect { groups -> - _viewState.update { - it.copy( - groups = groups - ) - } - } - } - } - - fun dispatch(action: GroupOptionViewAction) { - when (action) { - is GroupOptionViewAction.Show -> show(action.scope, action.groupId) - is GroupOptionViewAction.Hide -> hide(action.scope) - is GroupOptionViewAction.ShowDeleteDialog -> changeDeleteDialogVisible(true) - is GroupOptionViewAction.HideDeleteDialog -> changeDeleteDialogVisible(false) - is GroupOptionViewAction.Delete -> delete(action.callback) - - is GroupOptionViewAction.ShowClearDialog -> showClearDialog() - is GroupOptionViewAction.HideClearDialog -> hideClearDialog() - is GroupOptionViewAction.Clear -> clear(action.callback) - - is GroupOptionViewAction.ShowAllAllowNotificationDialog -> - changeAllAllowNotificationDialogVisible(true) - is GroupOptionViewAction.HideAllAllowNotificationDialog -> - changeAllAllowNotificationDialogVisible(false) - is GroupOptionViewAction.AllAllowNotification -> - allAllowNotification(action.isNotification, action.callback) - - is GroupOptionViewAction.ShowAllParseFullContentDialog -> - changeAllParseFullContentDialogVisible(true) - is GroupOptionViewAction.HideAllParseFullContentDialog -> - changeAllParseFullContentDialogVisible(false) - is GroupOptionViewAction.AllParseFullContent -> - allParseFullContent(action.isFullContent, action.callback) - - is GroupOptionViewAction.ShowAllMoveToGroupDialog -> - changeAllMoveToGroupDialogVisible(action.targetGroup, true) - is GroupOptionViewAction.HideAllMoveToGroupDialog -> - changeAllMoveToGroupDialogVisible(visible = false) - is GroupOptionViewAction.AllMoveToGroup -> - allMoveToGroup(action.callback) - - is GroupOptionViewAction.InputNewName -> inputNewName(action.content) - is GroupOptionViewAction.Rename -> rename() - is GroupOptionViewAction.ShowRenameDialog -> changeRenameDialogVisible(true) - is GroupOptionViewAction.HideRenameDialog -> changeRenameDialogVisible(false) - } - } - - private suspend fun fetchGroup(groupId: String) { - val group = rssRepository.get().findGroupById(groupId) - _viewState.update { - it.copy( - group = group, - ) - } - } - - private fun show(scope: CoroutineScope, groupId: String) { - scope.launch { - fetchGroup(groupId) - _viewState.value.drawerState.show() - } - } - - private fun hide(scope: CoroutineScope) { - scope.launch { - _viewState.value.drawerState.hide() - } - } - - private fun allAllowNotification(isNotification: Boolean, callback: () -> Unit = {}) { - _viewState.value.group?.let { - viewModelScope.launch(Dispatchers.IO) { - rssRepository.get().groupAllowNotification(it, isNotification) - withContext(Dispatchers.Main) { - callback() - } - } - } - } - - private fun changeAllAllowNotificationDialogVisible(visible: Boolean) { - _viewState.update { - it.copy( - allAllowNotificationDialogVisible = visible, - ) - } - } - - private fun allParseFullContent(isFullContent: Boolean, callback: () -> Unit = {}) { - _viewState.value.group?.let { - viewModelScope.launch(Dispatchers.IO) { - rssRepository.get().groupParseFullContent(it, isFullContent) - withContext(Dispatchers.Main) { - callback() - } - } - } - } - - private fun changeAllParseFullContentDialogVisible(visible: Boolean) { - _viewState.update { - it.copy( - allParseFullContentDialogVisible = visible, - ) - } - } - - private fun delete(callback: () -> Unit = {}) { - _viewState.value.group?.let { - viewModelScope.launch(Dispatchers.IO) { - rssRepository.get().deleteGroup(it) - withContext(Dispatchers.Main) { - callback() - } - } - } - } - - private fun changeDeleteDialogVisible(visible: Boolean) { - _viewState.update { - it.copy( - deleteDialogVisible = visible, - ) - } - } - - private fun showClearDialog() { - _viewState.update { - it.copy( - clearDialogVisible = true, - ) - } - } - - private fun hideClearDialog() { - _viewState.update { - it.copy( - clearDialogVisible = false, - ) - } - } - - private fun clear(callback: () -> Unit = {}) { - _viewState.value.group?.let { - viewModelScope.launch(Dispatchers.IO) { - rssRepository.get().deleteArticles(group = it) - withContext(Dispatchers.Main) { - callback() - } - } - } - } - - private fun allMoveToGroup(callback: () -> Unit) { - _viewState.value.group?.let { group -> - _viewState.value.targetGroup?.let { targetGroup -> - viewModelScope.launch(Dispatchers.IO) { - rssRepository.get().groupMoveToTargetGroup(group, targetGroup) - withContext(Dispatchers.Main) { - callback() - } - } - } - } - } - - private fun changeAllMoveToGroupDialogVisible(targetGroup: Group? = null, visible: Boolean) { - _viewState.update { - it.copy( - targetGroup = if (visible) targetGroup else null, - allMoveToGroupDialogVisible = visible, - ) - } - } - - private fun rename() { - _viewState.value.group?.let { - viewModelScope.launch { - rssRepository.get().updateGroup( - it.copy( - name = _viewState.value.newName - ) - ) - _viewState.update { - it.copy( - renameDialogVisible = false, - ) - } - } - } - } - - private fun changeRenameDialogVisible(visible: Boolean) { - _viewState.update { - it.copy( - renameDialogVisible = visible, - newName = if (visible) _viewState.value.group?.name ?: "" else "", - ) - } - } - - private fun inputNewName(content: String) { - _viewState.update { - it.copy( - newName = content - ) - } - } -} - -@OptIn(ExperimentalMaterialApi::class) -data class GroupOptionViewState( - var drawerState: ModalBottomSheetState = ModalBottomSheetState(ModalBottomSheetValue.Hidden), - val group: Group? = null, - val targetGroup: Group? = null, - val groups: List = emptyList(), - val allAllowNotificationDialogVisible: Boolean = false, - val allParseFullContentDialogVisible: Boolean = false, - val allMoveToGroupDialogVisible: Boolean = false, - val deleteDialogVisible: Boolean = false, - val clearDialogVisible: Boolean = false, - val newName: String = "", - val renameDialogVisible: Boolean = false, -) - -sealed class GroupOptionViewAction { - data class Show( - val scope: CoroutineScope, - val groupId: String - ) : GroupOptionViewAction() - - data class Hide( - val scope: CoroutineScope, - ) : GroupOptionViewAction() - - data class Delete( - val callback: () -> Unit = {} - ) : GroupOptionViewAction() - - object ShowDeleteDialog : GroupOptionViewAction() - object HideDeleteDialog : GroupOptionViewAction() - - data class Clear( - val callback: () -> Unit = {} - ) : GroupOptionViewAction() - - object ShowClearDialog : GroupOptionViewAction() - object HideClearDialog : GroupOptionViewAction() - - data class AllParseFullContent( - val isFullContent: Boolean, - val callback: () -> Unit = {} - ) : GroupOptionViewAction() - - object ShowAllParseFullContentDialog : GroupOptionViewAction() - object HideAllParseFullContentDialog : GroupOptionViewAction() - - data class AllAllowNotification( - val isNotification: Boolean, - val callback: () -> Unit = {} - ) : GroupOptionViewAction() - - object ShowAllAllowNotificationDialog : GroupOptionViewAction() - object HideAllAllowNotificationDialog : GroupOptionViewAction() - - data class AllMoveToGroup( - val callback: () -> Unit = {} - ) : GroupOptionViewAction() - - data class ShowAllMoveToGroupDialog( - val targetGroup: Group - ) : GroupOptionViewAction() - - object HideAllMoveToGroupDialog : GroupOptionViewAction() - - object ShowRenameDialog : GroupOptionViewAction() - object HideRenameDialog : GroupOptionViewAction() - object Rename : GroupOptionViewAction() - data class InputNewName( - val content: String - ) : GroupOptionViewAction() -} diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/subscribe/ResultView.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/subscribe/ResultView.kt index 65894e6..cadeb24 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/subscribe/ResultView.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/subscribe/ResultView.kt @@ -1,15 +1,11 @@ package me.ash.reader.ui.page.home.feeds.subscribe import androidx.compose.animation.animateContentSize -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable +import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.text.selection.SelectionContainer -import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Add import androidx.compose.material.icons.outlined.Article @@ -30,9 +26,8 @@ import com.google.accompanist.flowlayout.FlowRow import com.google.accompanist.flowlayout.MainAxisAlignment import me.ash.reader.R import me.ash.reader.data.entity.Group -import me.ash.reader.ui.component.SelectionChip -import me.ash.reader.ui.component.Subtitle -import me.ash.reader.ui.ext.roundClick +import me.ash.reader.ui.component.base.RYSelectionChip +import me.ash.reader.ui.component.base.Subtitle import me.ash.reader.ui.theme.palette.alwaysLight @Composable @@ -51,7 +46,8 @@ fun ResultView( unsubscribeOnClick: () -> Unit = {}, onGroupClick: (groupId: String) -> Unit = {}, onAddNewGroup: () -> Unit = {}, - onFeedUrlClick: () -> Unit = {} + onFeedUrlClick: () -> Unit = {}, + onFeedUrlLongClick: () -> Unit = {}, ) { LaunchedEffect(Unit) { if (groups.isNotEmpty() && selectedGroupId.isEmpty()) onGroupClick(groups.first().id) @@ -60,7 +56,11 @@ fun ResultView( Column( modifier = modifier.verticalScroll(rememberScrollState()) ) { - EditableUrl(text = link, onFeedUrlClick) + EditableUrl( + text = link, + onClick = onFeedUrlClick, + onLongClick = onFeedUrlLongClick, + ) Spacer(modifier = Modifier.height(26.dp)) Preset( @@ -85,27 +85,30 @@ fun ResultView( } } +@OptIn(ExperimentalFoundationApi::class) @Composable private fun EditableUrl( text: String, - onClick: () -> Unit + onClick: () -> Unit, + onLongClick: () -> Unit, ) { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center ) { - SelectionContainer { - Text( - modifier = Modifier.roundClick { - onClick() - }, - text = text, - color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f), - style = MaterialTheme.typography.bodyMedium, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } + Text( + modifier = Modifier + .clip(MaterialTheme.shapes.small) + .combinedClickable( + onClick = onClick, + onLongClick = onLongClick, + ), + text = text, + color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f), + style = MaterialTheme.typography.bodyMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) } } @@ -127,7 +130,7 @@ private fun Preset( crossAxisSpacing = 10.dp, mainAxisSpacing = 10.dp, ) { - SelectionChip( + RYSelectionChip( modifier = Modifier.animateContentSize(), content = stringResource(R.string.allow_notification), selected = selectedAllowNotificationPreset, @@ -144,7 +147,7 @@ private fun Preset( ) { allowNotificationPresetOnClick() } - SelectionChip( + RYSelectionChip( modifier = Modifier.animateContentSize(), content = stringResource(R.string.parse_full_content), selected = selectedParseFullContentPreset, @@ -162,14 +165,14 @@ private fun Preset( parseFullContentPresetOnClick() } if (showUnsubscribe) { - SelectionChip( + RYSelectionChip( modifier = Modifier.animateContentSize(), content = stringResource(R.string.clear_articles), selected = false, ) { clearArticlesOnClick() } - SelectionChip( + RYSelectionChip( modifier = Modifier.animateContentSize(), content = stringResource(R.string.unsubscribe), selected = false, @@ -196,7 +199,7 @@ private fun AddToGroup( verticalAlignment = Alignment.CenterVertically, ) { items(groups) { - SelectionChip( + RYSelectionChip( modifier = Modifier.animateContentSize(), content = it.name, selected = it.id == selectedGroupId, @@ -215,7 +218,7 @@ private fun AddToGroup( mainAxisSpacing = 10.dp, ) { groups.forEach { - SelectionChip( + RYSelectionChip( modifier = Modifier.animateContentSize(), content = it.name, selected = it.id == selectedGroupId, diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/subscribe/SubscribeDialog.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/subscribe/SubscribeDialog.kt index 522341b..9d122b2 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/subscribe/SubscribeDialog.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/subscribe/SubscribeDialog.kt @@ -25,9 +25,9 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogProperties import androidx.hilt.navigation.compose.hiltViewModel import me.ash.reader.R -import me.ash.reader.ui.component.ClipboardTextField -import me.ash.reader.ui.component.Dialog -import me.ash.reader.ui.component.TextFieldDialog +import me.ash.reader.ui.component.base.ClipboardTextField +import me.ash.reader.ui.component.base.RYDialog +import me.ash.reader.ui.component.base.TextFieldDialog import me.ash.reader.ui.ext.collectAsStateValue @OptIn( @@ -41,32 +41,32 @@ fun SubscribeDialog( ) { val context = LocalContext.current val focusManager = LocalFocusManager.current - val viewState = subscribeViewModel.viewState.collectAsStateValue() - val groupsState = viewState.groups.collectAsState(initial = emptyList()) + val subscribeUiState = subscribeViewModel.subscribeUiState.collectAsStateValue() + val groupsState = subscribeUiState.groups.collectAsState(initial = emptyList()) val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { it?.let { uri -> - context.contentResolver.openInputStream(uri)?.let { inputStream -> - subscribeViewModel.dispatch(SubscribeViewAction.ImportFromInputStream(inputStream)) + context.contentResolver.openInputStream(uri)?.use { inputStream -> + subscribeViewModel.importFromInputStream(inputStream) } } } - LaunchedEffect(viewState.visible) { - if (viewState.visible) { - subscribeViewModel.dispatch(SubscribeViewAction.Init) + LaunchedEffect(subscribeUiState.visible) { + if (subscribeUiState.visible) { + subscribeViewModel.init() } else { - subscribeViewModel.dispatch(SubscribeViewAction.Reset) - subscribeViewModel.dispatch(SubscribeViewAction.SwitchPage(true)) + subscribeViewModel.reset() + subscribeViewModel.switchPage(true) } } - Dialog( + RYDialog( modifier = Modifier.padding(horizontal = 44.dp), - visible = viewState.visible, + visible = subscribeUiState.visible, properties = DialogProperties(usePlatformDefaultWidth = false), onDismissRequest = { focusManager.clearFocus() - subscribeViewModel.dispatch(SubscribeViewAction.Hide) + subscribeViewModel.hideDrawer() }, icon = { Icon( @@ -76,10 +76,10 @@ fun SubscribeDialog( }, title = { Text( - text = if (viewState.isSearchPage) { - viewState.title + text = if (subscribeUiState.isSearchPage) { + subscribeUiState.title } else { - viewState.feed?.name ?: stringResource(R.string.unknown) + subscribeUiState.feed?.name ?: stringResource(R.string.unknown) }, maxLines = 1, overflow = TextOverflow.Ellipsis, @@ -87,7 +87,7 @@ fun SubscribeDialog( }, text = { AnimatedContent( - targetState = viewState.isSearchPage, + targetState = subscribeUiState.isSearchPage, transitionSpec = { slideInHorizontally { width -> width } + fadeIn() with slideOutHorizontally { width -> -width } + fadeOut() @@ -95,55 +95,55 @@ fun SubscribeDialog( ) { targetExpanded -> if (targetExpanded) { ClipboardTextField( - readOnly = viewState.lockLinkInput, - value = viewState.linkContent, + readOnly = subscribeUiState.lockLinkInput, + value = subscribeUiState.linkContent, onValueChange = { - subscribeViewModel.dispatch(SubscribeViewAction.InputLink(it)) + subscribeViewModel.inputLink(it) }, placeholder = stringResource(R.string.feed_or_site_url), - errorText = viewState.errorMessage, + errorText = subscribeUiState.errorMessage, imeAction = ImeAction.Search, focusManager = focusManager, onConfirm = { - subscribeViewModel.dispatch(SubscribeViewAction.Search) + subscribeViewModel.search() }, ) } else { ResultView( - link = viewState.linkContent, + link = subscribeUiState.linkContent, groups = groupsState.value, - selectedAllowNotificationPreset = viewState.allowNotificationPreset, - selectedParseFullContentPreset = viewState.parseFullContentPreset, - selectedGroupId = viewState.selectedGroupId, + selectedAllowNotificationPreset = subscribeUiState.allowNotificationPreset, + selectedParseFullContentPreset = subscribeUiState.parseFullContentPreset, + selectedGroupId = subscribeUiState.selectedGroupId, allowNotificationPresetOnClick = { - subscribeViewModel.dispatch(SubscribeViewAction.ChangeAllowNotificationPreset) + subscribeViewModel.changeAllowNotificationPreset() }, parseFullContentPresetOnClick = { - subscribeViewModel.dispatch(SubscribeViewAction.ChangeParseFullContentPreset) + subscribeViewModel.changeParseFullContentPreset() }, onGroupClick = { - subscribeViewModel.dispatch(SubscribeViewAction.SelectedGroup(it)) + subscribeViewModel.selectedGroup(it) }, onAddNewGroup = { - subscribeViewModel.dispatch(SubscribeViewAction.ShowNewGroupDialog) + subscribeViewModel.showNewGroupDialog() }, ) } } }, confirmButton = { - if (viewState.isSearchPage) { + if (subscribeUiState.isSearchPage) { TextButton( - enabled = viewState.linkContent.isNotBlank() - && viewState.title != stringResource(R.string.searching), + enabled = subscribeUiState.linkContent.isNotBlank() + && subscribeUiState.title != stringResource(R.string.searching), onClick = { focusManager.clearFocus() - subscribeViewModel.dispatch(SubscribeViewAction.Search) + subscribeViewModel.search() } ) { Text( text = stringResource(R.string.search), - color = if (viewState.linkContent.isNotBlank()) { + color = if (subscribeUiState.linkContent.isNotBlank()) { Color.Unspecified } else { MaterialTheme.colorScheme.outline.copy(alpha = 0.7f) @@ -154,7 +154,7 @@ fun SubscribeDialog( TextButton( onClick = { focusManager.clearFocus() - subscribeViewModel.dispatch(SubscribeViewAction.Subscribe) + subscribeViewModel.subscribe() } ) { Text(stringResource(R.string.subscribe)) @@ -162,12 +162,12 @@ fun SubscribeDialog( } }, dismissButton = { - if (viewState.isSearchPage) { + if (subscribeUiState.isSearchPage) { TextButton( onClick = { focusManager.clearFocus() launcher.launch("*/*") - subscribeViewModel.dispatch(SubscribeViewAction.Hide) + subscribeViewModel.hideDrawer() } ) { Text(text = stringResource(R.string.import_from_opml)) @@ -176,7 +176,7 @@ fun SubscribeDialog( TextButton( onClick = { focusManager.clearFocus() - subscribeViewModel.dispatch(SubscribeViewAction.Hide) + subscribeViewModel.hideDrawer() } ) { Text(text = stringResource(R.string.cancel)) @@ -186,19 +186,19 @@ fun SubscribeDialog( ) TextFieldDialog( - visible = viewState.newGroupDialogVisible, + visible = subscribeUiState.newGroupDialogVisible, title = stringResource(R.string.create_new_group), icon = Icons.Outlined.CreateNewFolder, - value = viewState.newGroupContent, + value = subscribeUiState.newGroupContent, placeholder = stringResource(R.string.name), onValueChange = { - subscribeViewModel.dispatch(SubscribeViewAction.InputNewGroup(it)) + subscribeViewModel.inputNewGroup(it) }, onDismissRequest = { - subscribeViewModel.dispatch(SubscribeViewAction.HideNewGroupDialog) + subscribeViewModel.hideNewGroupDialog() }, onConfirm = { - subscribeViewModel.dispatch(SubscribeViewAction.AddNewGroup) + subscribeViewModel.addNewGroup() } ) } \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/subscribe/SubscribeViewModel.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/subscribe/SubscribeViewModel.kt index 2b4f807..a891ff4 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/subscribe/SubscribeViewModel.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/subscribe/SubscribeViewModel.kt @@ -3,11 +3,8 @@ package me.ash.reader.ui.page.home.feeds.subscribe import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.google.accompanist.pager.ExperimentalPagerApi import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import kotlinx.coroutines.async import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import me.ash.reader.R @@ -22,7 +19,6 @@ import me.ash.reader.ui.ext.formatUrl import java.io.InputStream import javax.inject.Inject -@OptIn(ExperimentalPagerApi::class) @HiltViewModel class SubscribeViewModel @Inject constructor( private val opmlRepository: OpmlRepository, @@ -30,35 +26,12 @@ class SubscribeViewModel @Inject constructor( private val rssHelper: RssHelper, private val stringsRepository: StringsRepository, ) : ViewModel() { - private val _viewState = MutableStateFlow(SubscribeViewState()) - val viewState: StateFlow = _viewState.asStateFlow() + private val _subscribeUiState = MutableStateFlow(SubscribeUiState()) + val subscribeUiState: StateFlow = _subscribeUiState.asStateFlow() private var searchJob: Job? = null - fun dispatch(action: SubscribeViewAction) { - when (action) { - is SubscribeViewAction.Init -> init() - is SubscribeViewAction.Reset -> reset() - is SubscribeViewAction.Show -> changeVisible(true) - is SubscribeViewAction.Hide -> changeVisible(false) - is SubscribeViewAction.ShowNewGroupDialog -> changeNewGroupDialogVisible(true) - is SubscribeViewAction.HideNewGroupDialog -> changeNewGroupDialogVisible(false) - is SubscribeViewAction.SwitchPage -> switchPage(action.isSearchPage) - is SubscribeViewAction.ImportFromInputStream -> importFromInputStream(action.inputStream) - is SubscribeViewAction.InputLink -> inputLink(action.content) - is SubscribeViewAction.Search -> search() - is SubscribeViewAction.ChangeAllowNotificationPreset -> - changeAllowNotificationPreset() - is SubscribeViewAction.ChangeParseFullContentPreset -> - changeParseFullContentPreset() - is SubscribeViewAction.SelectedGroup -> selectedGroup(action.groupId) - is SubscribeViewAction.InputNewGroup -> inputNewGroup(action.content) - is SubscribeViewAction.AddNewGroup -> addNewGroup() - is SubscribeViewAction.Subscribe -> subscribe() - } - } - - private fun init() { - _viewState.update { + fun init() { + _subscribeUiState.update { it.copy( title = stringsRepository.getString(R.string.subscribe), groups = rssRepository.get().pullGroups(), @@ -66,18 +39,18 @@ class SubscribeViewModel @Inject constructor( } } - private fun reset() { + fun reset() { searchJob?.cancel() searchJob = null - _viewState.update { - SubscribeViewState().copy( + _subscribeUiState.update { + SubscribeUiState().copy( title = stringsRepository.getString(R.string.subscribe), ) } } - private fun importFromInputStream(inputStream: InputStream) { - viewModelScope.launch(Dispatchers.IO) { + fun importFromInputStream(inputStream: InputStream) { + viewModelScope.launch { try { opmlRepository.saveToDatabase(inputStream) rssRepository.get().doSync() @@ -87,38 +60,35 @@ class SubscribeViewModel @Inject constructor( } } - private fun subscribe() { - val feed = _viewState.value.feed ?: return - val articles = _viewState.value.articles - viewModelScope.launch(Dispatchers.IO) { - val groupId = async { - _viewState.value.selectedGroupId - } + fun subscribe() { + val feed = _subscribeUiState.value.feed ?: return + val articles = _subscribeUiState.value.articles + viewModelScope.launch { rssRepository.get().subscribe( feed.copy( - groupId = groupId.await(), - isNotification = _viewState.value.allowNotificationPreset, - isFullContent = _viewState.value.parseFullContentPreset, + groupId = _subscribeUiState.value.selectedGroupId, + isNotification = _subscribeUiState.value.allowNotificationPreset, + isFullContent = _subscribeUiState.value.parseFullContentPreset, ), articles ) - changeVisible(false) + hideDrawer() } } - private fun selectedGroup(groupId: String) { - _viewState.update { + fun selectedGroup(groupId: String) { + _subscribeUiState.update { it.copy( selectedGroupId = groupId, ) } } - private fun addNewGroup() { - if (_viewState.value.newGroupContent.isNotBlank()) { + fun addNewGroup() { + if (_subscribeUiState.value.newGroupContent.isNotBlank()) { viewModelScope.launch { - selectedGroup(rssRepository.get().addGroup(_viewState.value.newGroupContent)) - changeNewGroupDialogVisible(false) - _viewState.update { + selectedGroup(rssRepository.get().addGroup(_subscribeUiState.value.newGroupContent)) + hideNewGroupDialog() + _subscribeUiState.update { it.copy( newGroupContent = "", ) @@ -127,48 +97,48 @@ class SubscribeViewModel @Inject constructor( } } - private fun changeParseFullContentPreset() { - _viewState.update { + fun changeParseFullContentPreset() { + _subscribeUiState.update { it.copy( - parseFullContentPreset = !_viewState.value.parseFullContentPreset + parseFullContentPreset = !_subscribeUiState.value.parseFullContentPreset ) } } - private fun changeAllowNotificationPreset() { - _viewState.update { + fun changeAllowNotificationPreset() { + _subscribeUiState.update { it.copy( - allowNotificationPreset = !_viewState.value.allowNotificationPreset + allowNotificationPreset = !_subscribeUiState.value.allowNotificationPreset ) } } - private fun search() { + fun search() { searchJob?.cancel() - viewModelScope.launch(Dispatchers.IO) { + viewModelScope.launch { try { - _viewState.update { + _subscribeUiState.update { it.copy( errorMessage = "", ) } - _viewState.value.linkContent.formatUrl().let { str -> - if (str != _viewState.value.linkContent) { - _viewState.update { + _subscribeUiState.value.linkContent.formatUrl().let { str -> + if (str != _subscribeUiState.value.linkContent) { + _subscribeUiState.update { it.copy( linkContent = str ) } } } - _viewState.update { + _subscribeUiState.update { it.copy( title = stringsRepository.getString(R.string.searching), lockLinkInput = true, ) } - if (rssRepository.get().isFeedExist(_viewState.value.linkContent)) { - _viewState.update { + if (rssRepository.get().isFeedExist(_subscribeUiState.value.linkContent)) { + _subscribeUiState.update { it.copy( title = stringsRepository.getString(R.string.subscribe), errorMessage = stringsRepository.getString(R.string.already_subscribed), @@ -177,8 +147,8 @@ class SubscribeViewModel @Inject constructor( } return@launch } - val feedWithArticle = rssHelper.searchFeed(_viewState.value.linkContent) - _viewState.update { + val feedWithArticle = rssHelper.searchFeed(_subscribeUiState.value.linkContent) + _subscribeUiState.update { it.copy( feed = feedWithArticle.feed, articles = feedWithArticle.articles, @@ -187,7 +157,7 @@ class SubscribeViewModel @Inject constructor( switchPage(false) } catch (e: Exception) { e.printStackTrace() - _viewState.update { + _subscribeUiState.update { it.copy( title = stringsRepository.getString(R.string.subscribe), errorMessage = e.message ?: stringsRepository.getString(R.string.unknown), @@ -200,8 +170,8 @@ class SubscribeViewModel @Inject constructor( } } - private fun inputLink(content: String) { - _viewState.update { + fun inputLink(content: String) { + _subscribeUiState.update { it.copy( linkContent = content, errorMessage = "", @@ -209,32 +179,48 @@ class SubscribeViewModel @Inject constructor( } } - private fun inputNewGroup(content: String) { - _viewState.update { + fun inputNewGroup(content: String) { + _subscribeUiState.update { it.copy( newGroupContent = content ) } } - private fun changeVisible(visible: Boolean) { - _viewState.update { + fun showDrawer() { + _subscribeUiState.update { it.copy( - visible = visible + visible = true ) } } - private fun changeNewGroupDialogVisible(visible: Boolean) { - _viewState.update { + fun hideDrawer() { + _subscribeUiState.update { it.copy( - newGroupDialogVisible = visible, + visible = false ) } } - private fun switchPage(isSearchPage: Boolean) { - _viewState.update { + fun showNewGroupDialog() { + _subscribeUiState.update { + it.copy( + newGroupDialogVisible = true, + ) + } + } + + fun hideNewGroupDialog() { + _subscribeUiState.update { + it.copy( + newGroupDialogVisible = false, + ) + } + } + + fun switchPage(isSearchPage: Boolean) { + _subscribeUiState.update { it.copy( isSearchPage = isSearchPage ) @@ -242,8 +228,7 @@ class SubscribeViewModel @Inject constructor( } } -@OptIn(ExperimentalPagerApi::class) -data class SubscribeViewState( +data class SubscribeUiState( val visible: Boolean = false, val title: String = "", val errorMessage: String = "", @@ -259,42 +244,3 @@ data class SubscribeViewState( val groups: Flow> = emptyFlow(), val isSearchPage: Boolean = true, ) - -sealed class SubscribeViewAction { - object Init : SubscribeViewAction() - object Reset : SubscribeViewAction() - - object Show : SubscribeViewAction() - object Hide : SubscribeViewAction() - - object ShowNewGroupDialog : SubscribeViewAction() - object HideNewGroupDialog : SubscribeViewAction() - object AddNewGroup : SubscribeViewAction() - - data class SwitchPage( - val isSearchPage: Boolean - ) : SubscribeViewAction() - - data class ImportFromInputStream( - val inputStream: InputStream - ) : SubscribeViewAction() - - data class InputLink( - val content: String - ) : SubscribeViewAction() - - object Search : SubscribeViewAction() - - object ChangeAllowNotificationPreset : SubscribeViewAction() - object ChangeParseFullContentPreset : SubscribeViewAction() - - data class SelectedGroup( - val groupId: String - ) : SubscribeViewAction() - - data class InputNewGroup( - val content: String - ) : SubscribeViewAction() - - object Subscribe : SubscribeViewAction() -} diff --git a/app/src/main/java/me/ash/reader/ui/page/home/flow/ArticleItem.kt b/app/src/main/java/me/ash/reader/ui/page/home/flow/ArticleItem.kt index 045f6cd..012a939 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/flow/ArticleItem.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/flow/ArticleItem.kt @@ -2,7 +2,6 @@ package me.ash.reader.ui.page.home.flow import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Star import androidx.compose.material3.Icon @@ -14,7 +13,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -23,17 +21,16 @@ import coil.size.Scale import me.ash.reader.R import me.ash.reader.data.entity.ArticleWithFeed import me.ash.reader.data.preference.* -import me.ash.reader.ui.component.AsyncImage -import me.ash.reader.ui.component.Size_1000 -import me.ash.reader.ui.ext.formatAsString -import me.ash.reader.ui.page.home.FeedIcon +import me.ash.reader.ui.component.FeedIcon +import me.ash.reader.ui.component.base.RYAsyncImage +import me.ash.reader.ui.component.base.SIZE_1000 +import me.ash.reader.ui.theme.Shape20 @Composable fun ArticleItem( articleWithFeed: ArticleWithFeed, onClick: (ArticleWithFeed) -> Unit = {}, ) { - val context = LocalContext.current val articleListFeedIcon = LocalFlowArticleListFeedIcon.current val articleListFeedName = LocalFlowArticleListFeedName.current val articleListImage = LocalFlowArticleListImage.current @@ -43,12 +40,12 @@ fun ArticleItem( Column( modifier = Modifier .padding(horizontal = 12.dp) - .clip(RoundedCornerShape(20.dp)) + .clip(Shape20) .clickable { onClick(articleWithFeed) } .padding(horizontal = 12.dp, vertical = 12.dp) .alpha(if (articleWithFeed.article.isStarred || articleWithFeed.article.isUnread) 1f else 0.5f), ) { - // Upper + // Top Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, @@ -80,28 +77,27 @@ fun ArticleItem( if (articleWithFeed.article.isStarred) { Icon( modifier = Modifier + .alpha(0.7f) .size(14.dp) .padding(end = 2.dp), imageVector = Icons.Rounded.Star, contentDescription = stringResource(R.string.starred), - tint = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f), + tint = MaterialTheme.colorScheme.outline, ) } // Date Text( - text = articleWithFeed.article.date.formatAsString( - context, - onlyHourMinute = true - ), - color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f), + modifier = Modifier.alpha(0.7f), + text = articleWithFeed.article.dateString ?: "", + color = MaterialTheme.colorScheme.outline, style = MaterialTheme.typography.labelMedium, ) } } } - // Lower + // Bottom Row( modifier = Modifier.fillMaxWidth(), ) { @@ -128,8 +124,9 @@ fun ArticleItem( // Description if (articleListDesc.value && articleWithFeed.article.shortDescription.isNotBlank()) { Text( + modifier = Modifier.alpha(0.7f), text = articleWithFeed.article.shortDescription, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), + color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodySmall, maxLines = 2, overflow = TextOverflow.Ellipsis, @@ -139,15 +136,15 @@ fun ArticleItem( // Image if (articleWithFeed.article.img != null && articleListImage.value) { - AsyncImage( + RYAsyncImage( modifier = Modifier .padding(start = 10.dp) .size(80.dp) - .clip(RoundedCornerShape(20.dp)), + .clip(Shape20), data = articleWithFeed.article.img, scale = Scale.FILL, precision = Precision.INEXACT, - size = Size_1000, + size = SIZE_1000, contentScale = ContentScale.Crop, ) } diff --git a/app/src/main/java/me/ash/reader/ui/page/home/flow/ArticleList.kt b/app/src/main/java/me/ash/reader/ui/page/home/flow/ArticleList.kt index c0aaf27..6483ede 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/flow/ArticleList.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/flow/ArticleList.kt @@ -13,8 +13,8 @@ import me.ash.reader.data.entity.ArticleWithFeed @OptIn(ExperimentalFoundationApi::class) fun LazyListScope.ArticleList( pagingItems: LazyPagingItems, - articleListFeedIcon: Boolean, - articleListDateStickyHeader: Boolean, + isShowFeedIcon: Boolean, + isShowStickyHeader: Boolean, articleListTonalElevation: Int, onClick: (ArticleWithFeed) -> Unit = {}, ) { @@ -31,13 +31,13 @@ fun LazyListScope.ArticleList( } is FlowItemView.Date -> { if (item.showSpacer) item { Spacer(modifier = Modifier.height(40.dp)) } - if (articleListDateStickyHeader) { + if (isShowStickyHeader) { stickyHeader(key = item.date) { - StickyHeader(item.date, articleListFeedIcon, articleListTonalElevation) + StickyHeader(item.date, isShowFeedIcon, articleListTonalElevation) } } else { item(key = item.date) { - StickyHeader(item.date, articleListFeedIcon, articleListTonalElevation) + StickyHeader(item.date, isShowFeedIcon, articleListTonalElevation) } } } diff --git a/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowPage.kt b/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowPage.kt index d6a1abd..231dbed 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowPage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowPage.kt @@ -1,8 +1,7 @@ package me.ash.reader.ui.page.home.flow +import RYExtensibleVisibility import androidx.activity.compose.BackHandler -import androidx.compose.animation.* -import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState @@ -10,7 +9,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.ArrowBack import androidx.compose.material.icons.rounded.DoneAll import androidx.compose.material.icons.rounded.Search -import androidx.compose.material3.* +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester @@ -20,28 +19,23 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavHostController -import androidx.paging.LoadState import androidx.paging.compose.collectAsLazyPagingItems import kotlinx.coroutines.delay import kotlinx.coroutines.launch import me.ash.reader.R +import me.ash.reader.data.model.getName import me.ash.reader.data.preference.* import me.ash.reader.data.repository.SyncWorker.Companion.getIsSyncing -import me.ash.reader.ui.component.DisplayText -import me.ash.reader.ui.component.FeedbackIconButton -import me.ash.reader.ui.component.SwipeRefresh +import me.ash.reader.ui.component.FilterBar +import me.ash.reader.ui.component.base.DisplayText +import me.ash.reader.ui.component.base.FeedbackIconButton +import me.ash.reader.ui.component.base.RYScaffold +import me.ash.reader.ui.component.base.SwipeRefresh import me.ash.reader.ui.ext.collectAsStateValue -import me.ash.reader.ui.ext.getName -import me.ash.reader.ui.ext.surfaceColorAtElevation import me.ash.reader.ui.page.common.RouteName -import me.ash.reader.ui.page.home.FilterBar -import me.ash.reader.ui.page.home.FilterState -import me.ash.reader.ui.page.home.HomeViewAction import me.ash.reader.ui.page.home.HomeViewModel -import me.ash.reader.ui.theme.palette.onDark @OptIn( - ExperimentalMaterial3Api::class, com.google.accompanist.pager.ExperimentalPagerApi::class, androidx.compose.ui.ExperimentalComposeUiApi::class, ) @@ -51,8 +45,6 @@ fun FlowPage( flowViewModel: FlowViewModel = hiltViewModel(), homeViewModel: HomeViewModel, ) { - val homeViewView = homeViewModel.viewState.collectAsStateValue() - val pagingItems = homeViewView.pagingData.collectAsLazyPagingItems() val keyboardController = LocalSoftwareKeyboardController.current val topBarTonalElevation = LocalFlowTopBarTonalElevation.current val articleListTonalElevation = LocalFlowArticleListTonalElevation.current @@ -63,16 +55,18 @@ fun FlowPage( val filterBarPadding = LocalFlowFilterBarPadding.current val filterBarTonalElevation = LocalFlowFilterBarTonalElevation.current + val homeUiState = homeViewModel.homeUiState.collectAsStateValue() + val flowUiState = flowViewModel.flowUiState.collectAsStateValue() + val filterUiState = homeViewModel.filterUiState.collectAsStateValue() + val pagingItems = homeUiState.pagingData.collectAsLazyPagingItems() + val listState = + if (pagingItems.itemCount > 0) flowUiState.listState else rememberLazyListState() + val scope = rememberCoroutineScope() val focusRequester = remember { FocusRequester() } var markAsRead by remember { mutableStateOf(false) } var onSearch by remember { mutableStateOf(false) } - val viewState = flowViewModel.viewState.collectAsStateValue() - val filterState = homeViewModel.filterState.collectAsStateValue() - val homeViewState = homeViewModel.viewState.collectAsStateValue() - val listState = if (pagingItems.itemCount > 0) viewState.listState else rememberLazyListState() - val owner = LocalLifecycleOwner.current var isSyncing by remember { mutableStateOf(false) } homeViewModel.syncWorkLiveData.observe(owner) { @@ -86,15 +80,15 @@ fun FlowPage( focusRequester.requestFocus() } else { keyboardController?.hide() - if (homeViewState.searchContent.isNotBlank()) { - homeViewModel.dispatch(HomeViewAction.InputSearchContent("")) + if (homeUiState.searchContent.isNotBlank()) { + homeViewModel.inputSearchContent("") } } } } - LaunchedEffect(viewState.listState) { - snapshotFlow { viewState.listState.firstVisibleItemIndex }.collect { + LaunchedEffect(flowUiState.listState) { + snapshotFlow { flowUiState.listState.firstVisibleItemIndex }.collect { if (it > 0) { keyboardController?.hide() } @@ -105,81 +99,63 @@ fun FlowPage( onSearch = false } - Scaffold( - modifier = Modifier - .background(MaterialTheme.colorScheme.surfaceColorAtElevation(topBarTonalElevation.value.dp)) - .statusBarsPadding(), - containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation( - articleListTonalElevation.value.dp - ) onDark MaterialTheme.colorScheme.surface, - topBar = { - SmallTopAppBar( - title = {}, - colors = TopAppBarDefaults.smallTopAppBarColors( - containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation( - topBarTonalElevation.value.dp - ), - ), - navigationIcon = { - FeedbackIconButton( - imageVector = Icons.Rounded.ArrowBack, - contentDescription = stringResource(R.string.back), - tint = MaterialTheme.colorScheme.onSurface - ) { + RYScaffold( + topBarTonalElevation = topBarTonalElevation.value.dp, + containerTonalElevation = articleListTonalElevation.value.dp, + navigationIcon = { + FeedbackIconButton( + imageVector = Icons.Rounded.ArrowBack, + contentDescription = stringResource(R.string.back), + tint = MaterialTheme.colorScheme.onSurface + ) { + onSearch = false + if (navController.previousBackStackEntry == null) { + navController.navigate(RouteName.FEEDS) { + launchSingleTop = true + } + } else { + navController.popBackStack() + } + } + }, + actions = { + RYExtensibleVisibility(visible = !filterUiState.filter.isStarred()) { + FeedbackIconButton( + imageVector = Icons.Rounded.DoneAll, + contentDescription = stringResource(R.string.mark_all_as_read), + tint = if (markAsRead) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurface + }, + ) { + scope.launch { + flowUiState.listState.scrollToItem(0) + markAsRead = !markAsRead onSearch = false - if (navController.previousBackStackEntry == null) { - navController.navigate(RouteName.FEEDS) { - launchSingleTop = true - } - } else { - navController.popBackStack() - } - } - }, - actions = { - AnimatedVisibility( - visible = !filterState.filter.isStarred(), - enter = fadeIn() + expandVertically(), - exit = fadeOut() + shrinkVertically(), - ) { - FeedbackIconButton( - imageVector = Icons.Rounded.DoneAll, - contentDescription = stringResource(R.string.mark_all_as_read), - tint = if (markAsRead) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.onSurface - }, - ) { - scope.launch { - viewState.listState.scrollToItem(0) - markAsRead = !markAsRead - onSearch = false - } - } - } - FeedbackIconButton( - imageVector = Icons.Rounded.Search, - contentDescription = stringResource(R.string.search), - tint = if (onSearch) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.onSurface - }, - ) { - scope.launch { - viewState.listState.scrollToItem(0) - onSearch = !onSearch - } } } - ) + } + FeedbackIconButton( + imageVector = Icons.Rounded.Search, + contentDescription = stringResource(R.string.search), + tint = if (onSearch) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurface + }, + ) { + scope.launch { + flowUiState.listState.scrollToItem(0) + onSearch = !onSearch + } + } }, content = { SwipeRefresh( onRefresh = { if (!isSyncing) { - flowViewModel.dispatch(FlowViewAction.Sync) + flowViewModel.sync() } } ) { @@ -188,12 +164,16 @@ fun FlowPage( state = listState, ) { item { - DisplayTextHeader(filterState, isSyncing, articleListFeedIcon.value) - AnimatedVisibility( - visible = markAsRead, - enter = fadeIn() + expandVertically(), - exit = fadeOut() + shrinkVertically(), - ) { + DisplayText( + modifier = Modifier.padding(start = if (articleListFeedIcon.value) 30.dp else 0.dp), + text = when { + filterUiState.group != null -> filterUiState.group.name + filterUiState.feed != null -> filterUiState.feed.name + else -> filterUiState.filter.getName() + }, + desc = if (isSyncing) stringResource(R.string.syncing) else "", + ) + RYExtensibleVisibility(visible = markAsRead) { Spacer(modifier = Modifier.height((56 + 24 + 10).dp)) } MarkAsReadBar( @@ -204,45 +184,39 @@ fun FlowPage( }, ) { markAsRead = false - flowViewModel.dispatch( - FlowViewAction.MarkAsRead( - groupId = filterState.group?.id, - feedId = filterState.feed?.id, - articleId = null, - markAsReadBefore = it, - ) + flowViewModel.markAsRead( + groupId = filterUiState.group?.id, + feedId = filterUiState.feed?.id, + articleId = null, + markAsReadBefore = it, ) } - AnimatedVisibility( - visible = onSearch, - enter = fadeIn() + expandVertically(), - exit = fadeOut() + shrinkVertically(), - ) { + RYExtensibleVisibility(visible = onSearch) { SearchBar( - value = homeViewState.searchContent, + value = homeUiState.searchContent, placeholder = when { - filterState.group != null -> stringResource( + filterUiState.group != null -> stringResource( R.string.search_for_in, - filterState.filter.getName(), - filterState.group.name + filterUiState.filter.getName(), + filterUiState.group.name ) - filterState.feed != null -> stringResource( + filterUiState.feed != null -> stringResource( R.string.search_for_in, - filterState.filter.getName(), - filterState.feed.name + filterUiState.filter.getName(), + filterUiState.feed.name ) else -> stringResource( R.string.search_for, - filterState.filter.getName() + filterUiState.filter.getName() ) }, focusRequester = focusRequester, onValueChange = { - homeViewModel.dispatch(HomeViewAction.InputSearchContent(it)) + homeViewModel.inputSearchContent(it) }, onClose = { onSearch = false - homeViewModel.dispatch(HomeViewAction.InputSearchContent("")) + homeViewModel.inputSearchContent("") } ) Spacer(modifier = Modifier.height((56 + 24 + 10).dp)) @@ -250,8 +224,8 @@ fun FlowPage( } ArticleList( pagingItems = pagingItems, - articleListFeedIcon = articleListFeedIcon.value, - articleListDateStickyHeader = articleListDateStickyHeader.value, + isShowFeedIcon = articleListFeedIcon.value, + isShowStickyHeader = articleListDateStickyHeader.value, articleListTonalElevation = articleListTonalElevation.value, ) { onSearch = false @@ -260,10 +234,7 @@ fun FlowPage( } } item { - Spacer(modifier = Modifier.height(64.dp)) - if (pagingItems.loadState.source.refresh is LoadState.NotLoading && pagingItems.itemCount != 0) { - Spacer(modifier = Modifier.height(64.dp)) - } + Spacer(modifier = Modifier.height(128.dp)) Spacer(modifier = Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars)) } } @@ -271,33 +242,16 @@ fun FlowPage( }, bottomBar = { FilterBar( - filter = filterState.filter, + filter = filterUiState.filter, filterBarStyle = filterBarStyle.value, filterBarFilled = filterBarFilled.value, filterBarPadding = filterBarPadding.dp, filterBarTonalElevation = filterBarTonalElevation.value.dp, ) { - flowViewModel.dispatch(FlowViewAction.ScrollToItem(0)) - homeViewModel.dispatch(HomeViewAction.ChangeFilter(filterState.copy(filter = it))) - homeViewModel.dispatch(HomeViewAction.FetchArticles) + flowViewModel.scrollToItem(0) + homeViewModel.changeFilter(filterUiState.copy(filter = it)) + homeViewModel.fetchArticles() } } ) } - -@Composable -private fun DisplayTextHeader( - filterState: FilterState, - isSyncing: Boolean, - articleListFeedIcon: Boolean, -) { - DisplayText( - modifier = Modifier.padding(start = if (articleListFeedIcon) 30.dp else 0.dp), - text = when { - filterState.group != null -> filterState.group.name - filterState.feed != null -> filterState.feed.name - else -> filterState.filter.getName() - }, - desc = if (isSyncing) stringResource(R.string.syncing) else "", - ) -} diff --git a/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowViewModel.kt b/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowViewModel.kt index 257609b..392045f 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowViewModel.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowViewModel.kt @@ -7,52 +7,30 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import me.ash.reader.data.entity.ArticleWithFeed import me.ash.reader.data.repository.RssRepository import java.util.* -import javax.annotation.concurrent.Immutable import javax.inject.Inject @HiltViewModel class FlowViewModel @Inject constructor( private val rssRepository: RssRepository, ) : ViewModel() { - private val _viewState = MutableStateFlow(ArticleViewState()) - val viewState: StateFlow = _viewState.asStateFlow() + private val _flowUiState = MutableStateFlow(FlowUiState()) + val flowUiState: StateFlow = _flowUiState.asStateFlow() - fun dispatch(action: FlowViewAction) { - when (action) { - is FlowViewAction.Sync -> sync() - is FlowViewAction.ChangeIsBack -> changeIsBack(action.isBack) - is FlowViewAction.ScrollToItem -> scrollToItem(action.index) - is FlowViewAction.MarkAsRead -> markAsRead( - action.groupId, - action.feedId, - action.articleId, - action.markAsReadBefore, - ) - } - } - - private fun sync() { + fun sync() { rssRepository.get().doSync() } - private fun scrollToItem(index: Int) { + fun scrollToItem(index: Int) { viewModelScope.launch { - _viewState.value.listState.scrollToItem(index) + _flowUiState.value.listState.scrollToItem(index) } } - private fun changeIsBack(isBack: Boolean) { - _viewState.update { - it.copy(isBack = isBack) - } - } - - private fun markAsRead( + fun markAsRead( groupId: String?, feedId: String?, articleId: String?, @@ -84,32 +62,13 @@ class FlowViewModel @Inject constructor( } } -data class ArticleViewState( +data class FlowUiState( val filterImportant: Int = 0, val listState: LazyListState = LazyListState(), val isBack: Boolean = false, val syncWorkInfo: String = "", ) -sealed class FlowViewAction { - object Sync : FlowViewAction() - - data class ChangeIsBack( - val isBack: Boolean - ) : FlowViewAction() - - data class ScrollToItem( - val index: Int - ) : FlowViewAction() - - data class MarkAsRead( - val groupId: String?, - val feedId: String?, - val articleId: String?, - val markAsReadBefore: MarkAsReadBefore - ) : FlowViewAction() -} - enum class MarkAsReadBefore { SevenDays, ThreeDays, @@ -117,10 +76,7 @@ enum class MarkAsReadBefore { All, } -@Immutable sealed class FlowItemView { - @Immutable class Article(val articleWithFeed: ArticleWithFeed) : FlowItemView() - @Immutable class Date(val date: String, val showSpacer: Boolean) : FlowItemView() } \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/page/home/flow/MarkAsReadBar.kt b/app/src/main/java/me/ash/reader/ui/page/home/flow/MarkAsReadBar.kt index 9458e08..5393f19 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/flow/MarkAsReadBar.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/flow/MarkAsReadBar.kt @@ -23,7 +23,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import me.ash.reader.R -import me.ash.reader.ui.component.AnimatedPopup +import me.ash.reader.ui.component.base.AnimatedPopup import me.ash.reader.ui.theme.palette.alwaysLight @Composable diff --git a/app/src/main/java/me/ash/reader/ui/page/home/flow/SearchBar.kt b/app/src/main/java/me/ash/reader/ui/page/home/flow/SearchBar.kt index c327c92..9d07147 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/flow/SearchBar.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/flow/SearchBar.kt @@ -12,19 +12,19 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.style.BaselineShift import androidx.compose.ui.unit.dp import me.ash.reader.R +import me.ash.reader.data.constant.ElevationTokens @Composable fun SearchBar( - modifier: Modifier = Modifier, value: String, placeholder: String = "", focusRequester: FocusRequester = remember { FocusRequester() }, @@ -39,7 +39,7 @@ fun SearchBar( .padding(horizontal = 24.dp) .fillMaxWidth(), shape = CircleShape, - tonalElevation = 3.dp + tonalElevation = ElevationTokens.Level2.dp ) { Row( modifier = Modifier.fillMaxSize(), @@ -62,7 +62,8 @@ fun SearchBar( .fillMaxWidth() .focusRequester(focusRequester), colors = TextFieldDefaults.textFieldColors( - backgroundColor = Color.Transparent, + textColor = MaterialTheme.colorScheme.onSurfaceVariant, + containerColor = Color.Transparent, focusedIndicatorColor = Color.Transparent, unfocusedIndicatorColor = Color.Transparent, ), @@ -70,17 +71,13 @@ fun SearchBar( onValueChange = { onValueChange(it) }, placeholder = { Text( + modifier = Modifier.alpha(0.7f), text = placeholder, style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy( - alpha = 0.7f - ), + color = MaterialTheme.colorScheme.onSurfaceVariant, ) }, - textStyle = MaterialTheme.typography.bodyLarge.copy( - color = MaterialTheme.colorScheme.onSurfaceVariant, - baselineShift = BaselineShift(0.1f) - ), + textStyle = MaterialTheme.typography.bodyLarge, singleLine = true, keyboardOptions = KeyboardOptions( imeAction = ImeAction.Done diff --git a/app/src/main/java/me/ash/reader/ui/page/home/flow/StickyHeader.kt b/app/src/main/java/me/ash/reader/ui/page/home/flow/StickyHeader.kt index dcda056..3f8518b 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/flow/StickyHeader.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/flow/StickyHeader.kt @@ -15,8 +15,8 @@ import me.ash.reader.ui.theme.palette.onDark @Composable fun StickyHeader( - currentItemDay: String, - articleListFeedIcon: Boolean, + dateString: String, + isShowFeedIcon: Boolean, articleListTonalElevation: Int, ) { Row( @@ -30,10 +30,10 @@ fun StickyHeader( ) { Text( modifier = Modifier.padding( - start = if (articleListFeedIcon) 54.dp else 24.dp, + start = if (isShowFeedIcon) 54.dp else 24.dp, bottom = 4.dp ), - text = currentItemDay, + text = dateString, color = MaterialTheme.colorScheme.primary, style = MaterialTheme.typography.labelLarge, ) diff --git a/app/src/main/java/me/ash/reader/ui/page/home/read/ReadBar.kt b/app/src/main/java/me/ash/reader/ui/page/home/read/ReadBar.kt deleted file mode 100644 index ec642b1..0000000 --- a/app/src/main/java/me/ash/reader/ui/page/home/read/ReadBar.kt +++ /dev/null @@ -1,142 +0,0 @@ -package me.ash.reader.ui.page.home.read - -import android.view.HapticFeedbackConstants -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.FiberManualRecord -import androidx.compose.material.icons.outlined.Article -import androidx.compose.material.icons.outlined.FiberManualRecord -import androidx.compose.material.icons.outlined.Headphones -import androidx.compose.material.icons.rounded.Article -import androidx.compose.material.icons.rounded.ExpandMore -import androidx.compose.material.icons.rounded.Star -import androidx.compose.material.icons.rounded.StarOutline -import androidx.compose.material3.Divider -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.compose.ui.zIndex -import me.ash.reader.R -import me.ash.reader.ui.component.CanBeDisabledIconButton - -@Composable -fun ReadBar( - modifier: Modifier = Modifier, - disabled: Boolean, - isUnread: Boolean, - isStarred: Boolean, - isFullContent: Boolean, - unreadOnClick: (afterIsUnread: Boolean) -> Unit = {}, - starredOnClick: (afterIsStarred: Boolean) -> Unit = {}, - fullContentOnClick: (afterIsFullContent: Boolean) -> Unit = {}, -) { - val view = LocalView.current - var fullContent by remember { mutableStateOf(isFullContent) } - - Surface( - modifier = Modifier - .background(MaterialTheme.colorScheme.surface) - .navigationBarsPadding(), - tonalElevation = 0.dp, - ) { - Box( - modifier = Modifier.height(60.dp) - ) { - Box { - Divider( - modifier = Modifier - .fillMaxWidth() - .height(1.dp) - .zIndex(1f), - color = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.24f) - ) - } - Row( - modifier = Modifier.fillMaxSize(), - horizontalArrangement = Arrangement.SpaceAround, - verticalAlignment = Alignment.CenterVertically, - ) { - CanBeDisabledIconButton( - modifier = Modifier.size(40.dp), - disabled = disabled, - imageVector = if (isUnread) { - Icons.Filled.FiberManualRecord - } else { - Icons.Outlined.FiberManualRecord - }, - contentDescription = stringResource(if (isUnread) R.string.mark_as_read else R.string.mark_as_unread), - tint = if (isUnread) { - MaterialTheme.colorScheme.onSecondaryContainer - } else { - MaterialTheme.colorScheme.outline - }, - ) { - view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) - unreadOnClick(!isUnread) - } - CanBeDisabledIconButton( - modifier = Modifier.size(40.dp), - disabled = disabled, - imageVector = if (isStarred) { - Icons.Rounded.Star - } else { - Icons.Rounded.StarOutline - }, - contentDescription = stringResource(if (isStarred) R.string.mark_as_unstar else R.string.mark_as_starred), - tint = if (isStarred) { - MaterialTheme.colorScheme.onSecondaryContainer - } else { - MaterialTheme.colorScheme.outline - }, - ) { - view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) - starredOnClick(!isStarred) - } - CanBeDisabledIconButton( - disabled = true, - modifier = Modifier.size(40.dp), - imageVector = Icons.Rounded.ExpandMore, - contentDescription = "Next Article", - tint = MaterialTheme.colorScheme.outline, - ) { - view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) - } - CanBeDisabledIconButton( - modifier = Modifier.size(36.dp), - disabled = true, - imageVector = Icons.Outlined.Headphones, - contentDescription = "Add Tag", - tint = MaterialTheme.colorScheme.outline, - ) { - view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) - } - CanBeDisabledIconButton( - disabled = disabled, - modifier = Modifier.size(40.dp), - imageVector = if (fullContent) { - Icons.Rounded.Article - } else { - Icons.Outlined.Article - }, - contentDescription = stringResource(R.string.parse_full_content), - tint = if (fullContent) { - MaterialTheme.colorScheme.onSecondaryContainer - } else { - MaterialTheme.colorScheme.outline - }, - ) { - view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) - val afterIsFullContent = !fullContent - fullContent = afterIsFullContent - fullContentOnClick(afterIsFullContent) - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/page/home/read/ReadPage.kt b/app/src/main/java/me/ash/reader/ui/page/home/read/ReadPage.kt deleted file mode 100644 index 940819a..0000000 --- a/app/src/main/java/me/ash/reader/ui/page/home/read/ReadPage.kt +++ /dev/null @@ -1,277 +0,0 @@ -package me.ash.reader.ui.page.home.read - -import android.content.Intent -import android.util.Log -import androidx.compose.animation.* -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.text.selection.DisableSelection -import androidx.compose.foundation.text.selection.SelectionContainer -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Share -import androidx.compose.material.icons.rounded.Close -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.compose.ui.zIndex -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.navigation.NavHostController -import me.ash.reader.R -import me.ash.reader.data.entity.ArticleWithFeed -import me.ash.reader.ui.component.FeedbackIconButton -import me.ash.reader.ui.component.reader.reader -import me.ash.reader.ui.ext.collectAsStateValue -import me.ash.reader.ui.ext.drawVerticalScrollbar - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun ReadPage( - navController: NavHostController, - readViewModel: ReadViewModel = hiltViewModel(), -) { - val viewState = readViewModel.viewState.collectAsStateValue() - val isScrollDown = viewState.listState.isScrollDown() - - LaunchedEffect(Unit) { - navController.currentBackStackEntryFlow.collect { - it.arguments?.getString("articleId")?.let { - readViewModel.dispatch(ReadViewAction.InitData(it)) - } - } - } - - LaunchedEffect(viewState.articleWithFeed?.article?.id) { - Log.i("RLog", "ReadPage: ${viewState.articleWithFeed}") - viewState.articleWithFeed?.let { - if (it.article.isUnread) { - readViewModel.dispatch(ReadViewAction.MarkUnread(false)) - } - } - } - - Scaffold( - containerColor = MaterialTheme.colorScheme.surface, - topBar = {}, - content = { - Box(Modifier.fillMaxSize()) { - Box( - modifier = Modifier - .fillMaxSize() - .zIndex(1f), - contentAlignment = Alignment.TopCenter - ) { - TopBar( - isShow = viewState.articleWithFeed == null || !isScrollDown, - title = viewState.articleWithFeed?.article?.title, - link = viewState.articleWithFeed?.article?.link, - onClose = { - navController.popBackStack() - }, - ) - } - Content( - content = viewState.content ?: "", - articleWithFeed = viewState.articleWithFeed, - isLoading = viewState.isLoading, - listState = viewState.listState, - ) - Box( - modifier = Modifier - .fillMaxSize() - .zIndex(1f), - contentAlignment = Alignment.BottomCenter - ) { - BottomBar( - isShow = viewState.articleWithFeed != null && !isScrollDown, - articleWithFeed = viewState.articleWithFeed, - unreadOnClick = { - readViewModel.dispatch(ReadViewAction.MarkUnread(it)) - }, - starredOnClick = { - readViewModel.dispatch(ReadViewAction.MarkStarred(it)) - }, - fullContentOnClick = { afterIsFullContent -> - if (afterIsFullContent) readViewModel.dispatch(ReadViewAction.RenderFullContent) - else readViewModel.dispatch(ReadViewAction.RenderDescriptionContent) - }, - ) - } - } - }, - bottomBar = {} - ) -} - -@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 -} - -@Composable -private fun TopBar( - isShow: Boolean, - title: String? = "", - link: String? = "", - onClose: () -> Unit = {}, -) { - val context = LocalContext.current - - AnimatedVisibility( - visible = isShow, - enter = fadeIn() + expandVertically(), - exit = fadeOut() + shrinkVertically(), - ) { - SmallTopAppBar( - modifier = Modifier.statusBarsPadding(), - colors = TopAppBarDefaults.smallTopAppBarColors( - containerColor = MaterialTheme.colorScheme.surface, - ), - title = {}, - navigationIcon = { - FeedbackIconButton( - imageVector = Icons.Rounded.Close, - contentDescription = stringResource(R.string.close), - tint = MaterialTheme.colorScheme.onSurface - ) { - onClose() - } - }, - actions = { - FeedbackIconButton( - modifier = Modifier.size(20.dp), - imageVector = Icons.Outlined.Share, - contentDescription = stringResource(R.string.share), - tint = MaterialTheme.colorScheme.onSurface, - ) { - context.startActivity(Intent.createChooser(Intent(Intent.ACTION_SEND).apply { - putExtra( - Intent.EXTRA_TEXT, - title?.takeIf { it.isNotBlank() }?.let { it + "\n" } + link - ) - type = "text/plain" - }, context.getString(R.string.share))) - } - } - ) - } -} - -@Composable -private fun Content( - content: String, - articleWithFeed: ArticleWithFeed?, - listState: LazyListState, - isLoading: Boolean, -) { - if (articleWithFeed == null) return - val context = LocalContext.current - - SelectionContainer { - LazyColumn( - modifier = Modifier - .fillMaxSize() - .statusBarsPadding() - .navigationBarsPadding() - .drawVerticalScrollbar(listState), - state = listState, - ) { - item { - Spacer(modifier = Modifier.height(64.dp)) - Spacer(modifier = Modifier.height(22.dp)) - Column( - modifier = Modifier - .padding(horizontal = 12.dp) - ) { - DisableSelection { - Header(articleWithFeed) - } - } - } - item { - Spacer(modifier = Modifier.height(22.dp)) - AnimatedVisibility( - visible = isLoading, - enter = fadeIn() + expandVertically(), - exit = fadeOut() + shrinkVertically(), - ) { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center, - ) { - Column { - Spacer(modifier = Modifier.height(22.dp)) - CircularProgressIndicator( - modifier = Modifier - .size(30.dp), - color = MaterialTheme.colorScheme.onSurface, - ) - Spacer(modifier = Modifier.height(22.dp)) - } - } - } - } - if (!isLoading) { - reader( - context = context, - link = articleWithFeed.article.link, - content = content - ) - } - item { - Spacer(modifier = Modifier.height(64.dp)) - Spacer(modifier = Modifier.height(64.dp)) - } - } - } -} - -@Composable -private fun BottomBar( - isShow: Boolean, - articleWithFeed: ArticleWithFeed?, - unreadOnClick: (afterIsUnread: Boolean) -> Unit = {}, - starredOnClick: (afterIsStarred: Boolean) -> Unit = {}, - fullContentOnClick: (afterIsFullContent: Boolean) -> Unit = {}, -) { - articleWithFeed?.let { - AnimatedVisibility( - visible = isShow, - enter = fadeIn() + expandVertically(), - exit = fadeOut() + shrinkVertically(), - ) { - ReadBar( - disabled = false, - isUnread = articleWithFeed.article.isUnread, - isStarred = articleWithFeed.article.isStarred, - isFullContent = articleWithFeed.feed.isFullContent, - unreadOnClick = unreadOnClick, - starredOnClick = starredOnClick, - fullContentOnClick = fullContentOnClick, - ) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/page/home/reading/BottomBar.kt b/app/src/main/java/me/ash/reader/ui/page/home/reading/BottomBar.kt new file mode 100644 index 0000000..0dbead5 --- /dev/null +++ b/app/src/main/java/me/ash/reader/ui/page/home/reading/BottomBar.kt @@ -0,0 +1,131 @@ +package me.ash.reader.ui.page.home.reading + +import RYExtensibleVisibility +import android.view.HapticFeedbackConstants +import androidx.compose.foundation.layout.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.FiberManualRecord +import androidx.compose.material.icons.outlined.Article +import androidx.compose.material.icons.outlined.FiberManualRecord +import androidx.compose.material.icons.outlined.Headphones +import androidx.compose.material.icons.rounded.Article +import androidx.compose.material.icons.rounded.ExpandMore +import androidx.compose.material.icons.rounded.Star +import androidx.compose.material.icons.rounded.StarOutline +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex +import me.ash.reader.R +import me.ash.reader.ui.component.base.CanBeDisabledIconButton + +@Composable +fun BottomBar( + isShow: Boolean, + isUnread: Boolean, + isStarred: Boolean, + isFullContent: Boolean, + onUnread: (isUnread: Boolean) -> Unit = {}, + onStarred: (isStarred: Boolean) -> Unit = {}, + onFullContent: (isFullContent: Boolean) -> Unit = {}, +) { + Box( + modifier = Modifier + .fillMaxSize() + .zIndex(1f), + contentAlignment = Alignment.BottomCenter + ) { + RYExtensibleVisibility(visible = isShow) { + val view = LocalView.current + + Surface(modifier = Modifier.navigationBarsPadding()) { + // TODO: Component styles await refactoring + Row( + modifier = Modifier + .fillMaxWidth() + .height(60.dp), + horizontalArrangement = Arrangement.SpaceAround, + verticalAlignment = Alignment.CenterVertically, + ) { + CanBeDisabledIconButton( + modifier = Modifier.size(40.dp), + disabled = false, + imageVector = if (isUnread) { + Icons.Filled.FiberManualRecord + } else { + Icons.Outlined.FiberManualRecord + }, + contentDescription = stringResource(if (isUnread) R.string.mark_as_read else R.string.mark_as_unread), + tint = if (isUnread) { + MaterialTheme.colorScheme.onSecondaryContainer + } else { + MaterialTheme.colorScheme.outline + }, + ) { + view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) + onUnread(!isUnread) + } + CanBeDisabledIconButton( + modifier = Modifier.size(40.dp), + disabled = false, + imageVector = if (isStarred) { + Icons.Rounded.Star + } else { + Icons.Rounded.StarOutline + }, + contentDescription = stringResource(if (isStarred) R.string.mark_as_unstar else R.string.mark_as_starred), + tint = if (isStarred) { + MaterialTheme.colorScheme.onSecondaryContainer + } else { + MaterialTheme.colorScheme.outline + }, + ) { + view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) + onStarred(!isStarred) + } + CanBeDisabledIconButton( + disabled = true, + modifier = Modifier.size(40.dp), + imageVector = Icons.Rounded.ExpandMore, + contentDescription = "Next Article", + tint = MaterialTheme.colorScheme.outline, + ) { + view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) + } + CanBeDisabledIconButton( + modifier = Modifier.size(36.dp), + disabled = true, + imageVector = Icons.Outlined.Headphones, + contentDescription = "Add Tag", + tint = MaterialTheme.colorScheme.outline, + ) { + view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) + } + CanBeDisabledIconButton( + disabled = false, + modifier = Modifier.size(40.dp), + imageVector = if (isFullContent) { + Icons.Rounded.Article + } else { + Icons.Outlined.Article + }, + contentDescription = stringResource(R.string.parse_full_content), + tint = if (isFullContent) { + MaterialTheme.colorScheme.onSecondaryContainer + } else { + MaterialTheme.colorScheme.outline + }, + ) { + view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) + onFullContent(!isFullContent) + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/page/home/reading/Content.kt b/app/src/main/java/me/ash/reader/ui/page/home/reading/Content.kt new file mode 100644 index 0000000..b1883ad --- /dev/null +++ b/app/src/main/java/me/ash/reader/ui/page/home/reading/Content.kt @@ -0,0 +1,101 @@ +package me.ash.reader.ui.page.home.reading + +import RYExtensibleVisibility +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.text.selection.DisableSelection +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import me.ash.reader.ui.component.reader.Reader +import me.ash.reader.ui.ext.drawVerticalScrollbar +import java.util.* + +@Composable +fun Content( + content: String, + feedName: String, + title: String, + author: String? = null, + link: String? = null, + publishedDate: Date, + listState: LazyListState, + isLoading: Boolean, + isShowToolBar: Boolean, +) { + val context = LocalContext.current + + SelectionContainer { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .statusBarsPadding() + .run { + if (isShowToolBar) { + navigationBarsPadding() + } else { + this + } + } + .drawVerticalScrollbar(listState), + state = listState, + ) { + item { + // Top bar height + Spacer(modifier = Modifier.height(64.dp)) + // padding + Spacer(modifier = Modifier.height(22.dp)) + Column( + modifier = Modifier + .padding(horizontal = 12.dp) + ) { + DisableSelection { + Header( + feedName = feedName, + title = title, + author = author, + link = link, + publishedDate = publishedDate, + ) + } + } + } + item { + Spacer(modifier = Modifier.height(22.dp)) + RYExtensibleVisibility(visible = isLoading) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Column { + Spacer(modifier = Modifier.height(22.dp)) + CircularProgressIndicator( + modifier = Modifier + .size(30.dp), + color = MaterialTheme.colorScheme.onSurface, + ) + Spacer(modifier = Modifier.height(22.dp)) + } + } + } + } + if (!isLoading) { + Reader( + context = context, + link = link ?: "", + content = content + ) + } + item { + Spacer(modifier = Modifier.height(128.dp)) + Spacer(modifier = Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars)) + } + } + } +} diff --git a/app/src/main/java/me/ash/reader/ui/page/home/read/Header.kt b/app/src/main/java/me/ash/reader/ui/page/home/reading/Header.kt similarity index 55% rename from app/src/main/java/me/ash/reader/ui/page/home/read/Header.kt rename to app/src/main/java/me/ash/reader/ui/page/home/reading/Header.kt index 44cccb2..b50823a 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/read/Header.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/reading/Header.kt @@ -1,62 +1,67 @@ -package me.ash.reader.ui.page.home.read +package me.ash.reader.ui.page.home.reading -import android.content.Intent -import android.net.Uri import androidx.compose.foundation.layout.* import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp -import me.ash.reader.data.entity.ArticleWithFeed import me.ash.reader.ui.ext.formatAsString +import me.ash.reader.ui.ext.openURL import me.ash.reader.ui.ext.roundClick +import java.util.* @Composable fun Header( - articleWithFeed: ArticleWithFeed, + feedName: String, + title: String, + author: String? = null, + link: String? = null, + publishedDate: Date, ) { val context = LocalContext.current + val dateString = remember(publishedDate) { + publishedDate.formatAsString(context, atHourMinute = true) + } Column( modifier = Modifier .fillMaxWidth() .roundClick { - articleWithFeed.article.link.let { - if (it.isNotEmpty()) { - context.startActivity( - Intent(Intent.ACTION_VIEW, Uri.parse(articleWithFeed.article.link)) - ) - } - } + context.openURL(link) } .padding(12.dp) ) { Text( - text = articleWithFeed.article.date.formatAsString(context, atHourMinute = true), - color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f), + modifier = Modifier.alpha(0.7f), + text = dateString, + color = MaterialTheme.colorScheme.outline, style = MaterialTheme.typography.labelMedium, ) Spacer(modifier = Modifier.height(4.dp)) Text( - text = articleWithFeed.article.title, + text = title, color = MaterialTheme.colorScheme.onSurface, style = MaterialTheme.typography.headlineLarge, ) Spacer(modifier = Modifier.height(4.dp)) - articleWithFeed.article.author?.let { + author?.let { if (it.isNotEmpty()) { Text( + modifier = Modifier.alpha(0.7f), text = it, - color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f), + color = MaterialTheme.colorScheme.outline, style = MaterialTheme.typography.labelMedium, ) } } Text( - text = articleWithFeed.feed.name, - color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f), + modifier = Modifier.alpha(0.7f), + text = feedName, + color = MaterialTheme.colorScheme.outline, style = MaterialTheme.typography.labelMedium, ) } diff --git a/app/src/main/java/me/ash/reader/ui/page/home/reading/ReadingPage.kt b/app/src/main/java/me/ash/reader/ui/page/home/reading/ReadingPage.kt new file mode 100644 index 0000000..d198734 --- /dev/null +++ b/app/src/main/java/me/ash/reader/ui/page/home/reading/ReadingPage.kt @@ -0,0 +1,92 @@ +package me.ash.reader.ui.page.home.reading + +import android.util.Log +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavHostController +import me.ash.reader.ui.component.base.RYScaffold +import me.ash.reader.ui.ext.collectAsStateValue +import me.ash.reader.ui.ext.isScrollDown + +@Composable +fun ReadingPage( + navController: NavHostController, + readingViewModel: ReadingViewModel = hiltViewModel(), +) { + val readingUiState = readingViewModel.readingUiState.collectAsStateValue() + val isShowToolBar = + readingUiState.articleWithFeed != null && !readingUiState.listState.isScrollDown() + + LaunchedEffect(Unit) { + navController.currentBackStackEntryFlow.collect { + it.arguments?.getString("articleId")?.let { + readingViewModel.initData(it) + } + } + } + + LaunchedEffect(readingUiState.articleWithFeed?.article?.id) { + Log.i("RLog", "ReadPage: ${readingUiState.articleWithFeed}") + readingUiState.articleWithFeed?.let { + if (it.article.isUnread) { + readingViewModel.markUnread(false) + } + } + } + + RYScaffold( + content = { + Log.i("RLog", "TopBar: recomposition") + + Box(modifier = Modifier.fillMaxSize()) { + // Top Bar + TopBar( + isShow = isShowToolBar, + title = readingUiState.articleWithFeed?.article?.title, + link = readingUiState.articleWithFeed?.article?.link, + onClose = { + navController.popBackStack() + }, + ) + + // Content + if (readingUiState.articleWithFeed != null) { + Content( + content = readingUiState.content ?: "", + feedName = readingUiState.articleWithFeed.feed.name, + title = readingUiState.articleWithFeed.article.title, + author = readingUiState.articleWithFeed.article.author, + link = readingUiState.articleWithFeed.article.link, + publishedDate = readingUiState.articleWithFeed.article.date, + isLoading = readingUiState.isLoading, + listState = readingUiState.listState, + isShowToolBar = isShowToolBar, + ) + } + // Bottom Bar + if (readingUiState.articleWithFeed != null) { + BottomBar( + isShow = isShowToolBar, + isUnread = readingUiState.articleWithFeed.article.isUnread, + isStarred = readingUiState.articleWithFeed.article.isStarred, + isFullContent = readingUiState.isFullContent, + onUnread = { + readingViewModel.markUnread(it) + }, + onStarred = { + readingViewModel.markStarred(it) + }, + onFullContent = { + if (it) readingViewModel.renderFullContent() + else readingViewModel.renderDescriptionContent() + }, + ) + } + } + } + ) +} diff --git a/app/src/main/java/me/ash/reader/ui/page/home/read/ReadViewModel.kt b/app/src/main/java/me/ash/reader/ui/page/home/reading/ReadingViewModel.kt similarity index 50% rename from app/src/main/java/me/ash/reader/ui/page/home/read/ReadViewModel.kt rename to app/src/main/java/me/ash/reader/ui/page/home/reading/ReadingViewModel.kt index 1aeadf2..8095268 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/read/ReadViewModel.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/reading/ReadingViewModel.kt @@ -1,4 +1,4 @@ -package me.ash.reader.ui.page.home.read +package me.ash.reader.ui.page.home.reading import android.util.Log import androidx.compose.foundation.lazy.LazyListState @@ -17,80 +17,72 @@ import me.ash.reader.data.repository.RssRepository import javax.inject.Inject @HiltViewModel -class ReadViewModel @Inject constructor( - val rssRepository: RssRepository, +class ReadingViewModel @Inject constructor( + private val rssRepository: RssRepository, private val rssHelper: RssHelper, ) : ViewModel() { - private val _viewState = MutableStateFlow(ReadViewState()) - val viewState: StateFlow = _viewState.asStateFlow() + private val _readingUiState = MutableStateFlow(ReadingUiState()) + val readingUiState: StateFlow = _readingUiState.asStateFlow() - fun dispatch(action: ReadViewAction) { - when (action) { - is ReadViewAction.InitData -> bindArticleWithFeed(action.articleId) - is ReadViewAction.RenderDescriptionContent -> renderDescriptionContent() - is ReadViewAction.RenderFullContent -> renderFullContent() - is ReadViewAction.MarkUnread -> markUnread(action.isUnread) - is ReadViewAction.MarkStarred -> markStarred(action.isStarred) - is ReadViewAction.ClearArticle -> clearArticle() - is ReadViewAction.ChangeLoading -> changeLoading(action.isLoading) - } - } - - private fun bindArticleWithFeed(articleId: String) { - changeLoading(true) + fun initData(articleId: String) { + showLoading() viewModelScope.launch { - _viewState.update { - it.copy(articleWithFeed = rssRepository.get().findArticleById(articleId)) + _readingUiState.update { + it.copy( + articleWithFeed = rssRepository.get().findArticleById(articleId) + ) } - _viewState.value.articleWithFeed?.let { + _readingUiState.value.articleWithFeed?.let { if (it.feed.isFullContent) internalRenderFullContent() else renderDescriptionContent() } - changeLoading(false) + hideLoading() } } - private fun renderDescriptionContent() { - _viewState.update { + fun renderDescriptionContent() { + _readingUiState.update { it.copy( content = it.articleWithFeed?.article?.fullContent ?: it.articleWithFeed?.article?.rawDescription ?: "", + isFullContent = false ) } } - private fun renderFullContent() { + fun renderFullContent() { viewModelScope.launch { internalRenderFullContent() } } private suspend fun internalRenderFullContent() { - changeLoading(true) + showLoading() try { - _viewState.update { + _readingUiState.update { it.copy( content = rssHelper.parseFullContent( - _viewState.value.articleWithFeed?.article?.link ?: "", - _viewState.value.articleWithFeed?.article?.title ?: "" - ) + _readingUiState.value.articleWithFeed?.article?.link ?: "", + _readingUiState.value.articleWithFeed?.article?.title ?: "" + ), + isFullContent = true ) } } catch (e: Exception) { Log.i("RLog", "renderFullContent: ${e.message}") - _viewState.update { + _readingUiState.update { it.copy( content = e.message ) } } - changeLoading(false) + hideLoading() } - private fun markUnread(isUnread: Boolean) { - val articleWithFeed = _viewState.value.articleWithFeed ?: return + fun markUnread(isUnread: Boolean) { + val articleWithFeed = _readingUiState.value.articleWithFeed ?: return viewModelScope.launch { - _viewState.update { + _readingUiState.update { it.copy( articleWithFeed = articleWithFeed.copy( article = articleWithFeed.article.copy( @@ -102,17 +94,17 @@ class ReadViewModel @Inject constructor( rssRepository.get().markAsRead( groupId = null, feedId = null, - articleId = _viewState.value.articleWithFeed!!.article.id, + articleId = _readingUiState.value.articleWithFeed!!.article.id, before = null, isUnread = isUnread, ) } } - private fun markStarred(isStarred: Boolean) { - val articleWithFeed = _viewState.value.articleWithFeed ?: return + fun markStarred(isStarred: Boolean) { + val articleWithFeed = _readingUiState.value.articleWithFeed ?: return viewModelScope.launch(Dispatchers.IO) { - _viewState.update { + _readingUiState.update { it.copy( articleWithFeed = articleWithFeed.copy( article = articleWithFeed.article.copy( @@ -129,47 +121,23 @@ class ReadViewModel @Inject constructor( } } - private fun clearArticle() { - _viewState.update { - it.copy(articleWithFeed = null) + private fun showLoading() { + _readingUiState.update { + it.copy(isLoading = true) } } - private fun changeLoading(isLoading: Boolean) { - _viewState.update { - it.copy(isLoading = isLoading) + private fun hideLoading() { + _readingUiState.update { + it.copy(isLoading = false) } } } -data class ReadViewState( +data class ReadingUiState( val articleWithFeed: ArticleWithFeed? = null, val content: String? = null, + val isFullContent: Boolean = false, val isLoading: Boolean = true, -// val scrollState: ScrollState = ScrollState(0), val listState: LazyListState = LazyListState(), -) - -sealed class ReadViewAction { - data class InitData( - val articleId: String, - ) : ReadViewAction() - - object RenderDescriptionContent : ReadViewAction() - - object RenderFullContent : ReadViewAction() - - data class MarkUnread( - val isUnread: Boolean, - ) : ReadViewAction() - - data class MarkStarred( - val isStarred: Boolean, - ) : ReadViewAction() - - object ClearArticle : ReadViewAction() - - data class ChangeLoading( - val isLoading: Boolean - ) : ReadViewAction() -} \ No newline at end of file +) \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/page/home/reading/TopBar.kt b/app/src/main/java/me/ash/reader/ui/page/home/reading/TopBar.kt new file mode 100644 index 0000000..4d88925 --- /dev/null +++ b/app/src/main/java/me/ash/reader/ui/page/home/reading/TopBar.kt @@ -0,0 +1,72 @@ +package me.ash.reader.ui.page.home.reading + +import RYExtensibleVisibility +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Share +import androidx.compose.material.icons.rounded.Close +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SmallTopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex +import me.ash.reader.R +import me.ash.reader.ui.component.base.FeedbackIconButton +import me.ash.reader.ui.ext.share + +@Composable +fun TopBar( + isShow: Boolean, + title: String? = "", + link: String? = "", + onClose: () -> Unit = {}, +) { + val context = LocalContext.current + + Box( + modifier = Modifier + .fillMaxSize() + .zIndex(1f), + contentAlignment = Alignment.TopCenter + ) { + RYExtensibleVisibility(visible = isShow) { + SmallTopAppBar( + modifier = Modifier.statusBarsPadding(), + colors = TopAppBarDefaults.smallTopAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + ), + title = {}, + navigationIcon = { + FeedbackIconButton( + imageVector = Icons.Rounded.Close, + contentDescription = stringResource(R.string.close), + tint = MaterialTheme.colorScheme.onSurface + ) { + onClose() + } + }, + actions = { + FeedbackIconButton( + modifier = Modifier.size(20.dp), + imageVector = Icons.Outlined.Share, + contentDescription = stringResource(R.string.share), + tint = MaterialTheme.colorScheme.onSurface, + ) { + context.share(title + ?.takeIf { it.isNotBlank() } + ?.let { it + "\n" } + link + ) + } + } + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/page/settings/SettingsPage.kt b/app/src/main/java/me/ash/reader/ui/page/settings/SettingsPage.kt index 5a88950..6821916 100644 --- a/app/src/main/java/me/ash/reader/ui/page/settings/SettingsPage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/settings/SettingsPage.kt @@ -1,15 +1,17 @@ package me.ash.reader.ui.page.settings -import android.annotation.SuppressLint -import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.* import androidx.compose.material.icons.rounded.ArrowBack import androidx.compose.material.icons.rounded.Close -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource @@ -17,62 +19,39 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavHostController -import kotlinx.coroutines.flow.map import me.ash.reader.R -import me.ash.reader.data.entity.toVersion -import me.ash.reader.ui.component.Banner -import me.ash.reader.ui.component.DisplayText -import me.ash.reader.ui.component.FeedbackIconButton -import me.ash.reader.ui.ext.DataStoreKeys -import me.ash.reader.ui.ext.dataStore +import me.ash.reader.data.preference.LocalNewVersionNumber +import me.ash.reader.data.preference.LocalSkipVersionNumber +import me.ash.reader.ui.component.base.Banner +import me.ash.reader.ui.component.base.DisplayText +import me.ash.reader.ui.component.base.FeedbackIconButton +import me.ash.reader.ui.component.base.RYScaffold import me.ash.reader.ui.ext.getCurrentVersion import me.ash.reader.ui.page.common.RouteName import me.ash.reader.ui.page.settings.tips.UpdateDialog -import me.ash.reader.ui.page.settings.tips.UpdateViewAction import me.ash.reader.ui.page.settings.tips.UpdateViewModel import me.ash.reader.ui.theme.palette.onLight -@SuppressLint("FlowOperatorInvokedInComposition") -@OptIn(ExperimentalMaterial3Api::class) @Composable fun SettingsPage( navController: NavHostController, updateViewModel: UpdateViewModel = hiltViewModel(), ) { val context = LocalContext.current - val skipVersion = context.dataStore.data - .map { it[DataStoreKeys.SkipVersionNumber.key] ?: "" } - .collectAsState(initial = "") - .value - .toVersion() - val latestVersion = context.dataStore.data - .map { it[DataStoreKeys.NewVersionNumber.key] ?: "" } - .collectAsState(initial = "") - .value - .toVersion() + val newVersion = LocalNewVersionNumber.current + val skipVersion = LocalSkipVersionNumber.current val currentVersion by remember { mutableStateOf(context.getCurrentVersion()) } - Scaffold( - modifier = Modifier - .background(MaterialTheme.colorScheme.surface onLight MaterialTheme.colorScheme.inverseOnSurface) - .statusBarsPadding() - .navigationBarsPadding(), + RYScaffold( containerColor = MaterialTheme.colorScheme.surface onLight MaterialTheme.colorScheme.inverseOnSurface, - topBar = { - SmallTopAppBar( - colors = TopAppBarDefaults.smallTopAppBarColors(containerColor = MaterialTheme.colorScheme.surface onLight MaterialTheme.colorScheme.inverseOnSurface), - title = {}, - navigationIcon = { - FeedbackIconButton( - imageVector = Icons.Rounded.ArrowBack, - contentDescription = stringResource(R.string.back), - tint = MaterialTheme.colorScheme.onSurface - ) { - navController.popBackStack() - } - }, - actions = {} - ) + navigationIcon = { + FeedbackIconButton( + imageVector = Icons.Rounded.ArrowBack, + contentDescription = stringResource(R.string.back), + tint = MaterialTheme.colorScheme.onSurface + ) { + navController.popBackStack() + } }, content = { LazyColumn { @@ -81,13 +60,13 @@ fun SettingsPage( } item { Box { - if (latestVersion.whetherNeedUpdate(currentVersion, skipVersion)) { + if (newVersion.whetherNeedUpdate(currentVersion, skipVersion)) { Banner( modifier = Modifier.zIndex(1f), title = stringResource(R.string.get_new_updates), desc = stringResource( R.string.get_new_updates_desc, - latestVersion.toString(), + newVersion.toString(), ), icon = Icons.Outlined.Lightbulb, action = { @@ -97,7 +76,7 @@ fun SettingsPage( ) }, ) { - updateViewModel.dispatch(UpdateViewAction.Show) + updateViewModel.showDialog() } } Banner( @@ -159,6 +138,8 @@ fun SettingsPage( launchSingleTop = true } } + } + item { Spacer(modifier = Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars)) } } diff --git a/app/src/main/java/me/ash/reader/ui/page/settings/color/ColorAndStyle.kt b/app/src/main/java/me/ash/reader/ui/page/settings/color/ColorAndStylePage.kt similarity index 91% rename from app/src/main/java/me/ash/reader/ui/page/settings/color/ColorAndStyle.kt rename to app/src/main/java/me/ash/reader/ui/page/settings/color/ColorAndStylePage.kt index 012a448..9f5b697 100644 --- a/app/src/main/java/me/ash/reader/ui/page/settings/color/ColorAndStyle.kt +++ b/app/src/main/java/me/ash/reader/ui/page/settings/color/ColorAndStylePage.kt @@ -1,6 +1,5 @@ package me.ash.reader.ui.page.settings.color -import android.annotation.SuppressLint import android.content.Context import android.os.Build import androidx.compose.animation.* @@ -27,7 +26,7 @@ import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController import me.ash.reader.R import me.ash.reader.data.preference.* -import me.ash.reader.ui.component.* +import me.ash.reader.ui.component.base.* import me.ash.reader.ui.page.common.RouteName import me.ash.reader.ui.page.settings.SettingItem import me.ash.reader.ui.svg.PALETTE @@ -36,10 +35,9 @@ import me.ash.reader.ui.theme.palette.* import me.ash.reader.ui.theme.palette.TonalPalettes.Companion.toTonalPalettes import me.ash.reader.ui.theme.palette.dynamic.extractTonalPalettesFromUserWallpaper -@SuppressLint("FlowOperatorInvokedInComposition") @OptIn(ExperimentalMaterial3Api::class) @Composable -fun ColorAndStyle( +fun ColorAndStylePage( navController: NavHostController, ) { val context = LocalContext.current @@ -52,29 +50,16 @@ fun ColorAndStyle( val wallpaperTonalPalettes = extractTonalPalettesFromUserWallpaper() var radioButtonSelected by remember { mutableStateOf(if (themeIndex > 4) 0 else 1) } - Scaffold( - modifier = Modifier - .background(MaterialTheme.colorScheme.surface onLight MaterialTheme.colorScheme.inverseOnSurface) - .statusBarsPadding() - .navigationBarsPadding(), + RYScaffold( containerColor = MaterialTheme.colorScheme.surface onLight MaterialTheme.colorScheme.inverseOnSurface, - topBar = { - SmallTopAppBar( - colors = TopAppBarDefaults.smallTopAppBarColors( - containerColor = MaterialTheme.colorScheme.surface onLight MaterialTheme.colorScheme.inverseOnSurface - ), - title = {}, - navigationIcon = { - FeedbackIconButton( - imageVector = Icons.Rounded.ArrowBack, - contentDescription = stringResource(R.string.back), - tint = MaterialTheme.colorScheme.onSurface - ) { - navController.popBackStack() - } - }, - actions = {} - ) + navigationIcon = { + FeedbackIconButton( + imageVector = Icons.Rounded.ArrowBack, + contentDescription = stringResource(R.string.back), + tint = MaterialTheme.colorScheme.onSurface + ) { + navController.popBackStack() + } }, content = { LazyColumn { @@ -105,7 +90,7 @@ fun ColorAndStyle( Spacer(modifier = Modifier.height(24.dp)) } item { - BlockRadioGroupButton( + BlockRadioButton( selected = radioButtonSelected, onSelected = { radioButtonSelected = it }, itemRadioGroups = listOf( @@ -157,7 +142,7 @@ fun ColorAndStyle( } }, ) { - Switch( + RYSwitch( activated = darkTheme.isDarkTheme() ) { darkThemeNot.put(context, scope) @@ -197,7 +182,9 @@ fun ColorAndStyle( enable = false, onClick = {}, ) {} - Spacer(modifier = Modifier.height(24.dp)) + } + item { + Spacer(modifier = Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars)) } } } diff --git a/app/src/main/java/me/ash/reader/ui/page/settings/color/DarkTheme.kt b/app/src/main/java/me/ash/reader/ui/page/settings/color/DarkThemePage.kt similarity index 63% rename from app/src/main/java/me/ash/reader/ui/page/settings/color/DarkTheme.kt rename to app/src/main/java/me/ash/reader/ui/page/settings/color/DarkThemePage.kt index 83c0d11..be16636 100644 --- a/app/src/main/java/me/ash/reader/ui/page/settings/color/DarkTheme.kt +++ b/app/src/main/java/me/ash/reader/ui/page/settings/color/DarkThemePage.kt @@ -1,13 +1,12 @@ package me.ash.reader.ui.page.settings.color -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.ArrowBack -import androidx.compose.material3.* +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier @@ -20,16 +19,13 @@ import me.ash.reader.data.preference.DarkThemePreference import me.ash.reader.data.preference.LocalAmoledDarkTheme import me.ash.reader.data.preference.LocalDarkTheme import me.ash.reader.data.preference.not -import me.ash.reader.ui.component.DisplayText -import me.ash.reader.ui.component.FeedbackIconButton -import me.ash.reader.ui.component.Subtitle -import me.ash.reader.ui.component.Switch +import me.ash.reader.ui.component.base.* import me.ash.reader.ui.page.settings.SettingItem import me.ash.reader.ui.theme.palette.onLight @OptIn(ExperimentalMaterial3Api::class) @Composable -fun DarkTheme( +fun DarkThemePage( navController: NavHostController, ) { val context = LocalContext.current @@ -37,29 +33,16 @@ fun DarkTheme( val amoledDarkTheme = LocalAmoledDarkTheme.current val scope = rememberCoroutineScope() - Scaffold( - modifier = Modifier - .background(MaterialTheme.colorScheme.surface onLight MaterialTheme.colorScheme.inverseOnSurface) - .statusBarsPadding() - .navigationBarsPadding(), + RYScaffold( containerColor = MaterialTheme.colorScheme.surface onLight MaterialTheme.colorScheme.inverseOnSurface, - topBar = { - SmallTopAppBar( - colors = TopAppBarDefaults.smallTopAppBarColors( - containerColor = MaterialTheme.colorScheme.surface onLight MaterialTheme.colorScheme.inverseOnSurface - ), - title = {}, - navigationIcon = { - FeedbackIconButton( - imageVector = Icons.Rounded.ArrowBack, - contentDescription = stringResource(R.string.back), - tint = MaterialTheme.colorScheme.onSurface - ) { - navController.popBackStack() - } - }, - actions = {} - ) + navigationIcon = { + FeedbackIconButton( + imageVector = Icons.Rounded.ArrowBack, + contentDescription = stringResource(R.string.back), + tint = MaterialTheme.colorScheme.onSurface + ) { + navController.popBackStack() + } }, content = { LazyColumn { @@ -89,11 +72,14 @@ fun DarkTheme( (!amoledDarkTheme).put(context, scope) }, ) { - Switch(activated = amoledDarkTheme.value) { + RYSwitch(activated = amoledDarkTheme.value) { (!amoledDarkTheme).put(context, scope) } } } + item { + Spacer(modifier = Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars)) + } } } ) diff --git a/app/src/main/java/me/ash/reader/ui/page/settings/color/feeds/FeedsPagePreview.kt b/app/src/main/java/me/ash/reader/ui/page/settings/color/feeds/FeedsPagePreview.kt new file mode 100644 index 0000000..1408f40 --- /dev/null +++ b/app/src/main/java/me/ash/reader/ui/page/settings/color/feeds/FeedsPagePreview.kt @@ -0,0 +1,143 @@ +package me.ash.reader.ui.page.settings.color.feeds + +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Add +import androidx.compose.material.icons.rounded.ArrowBack +import androidx.compose.material.icons.rounded.Refresh +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SmallTopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import me.ash.reader.R +import me.ash.reader.data.entity.Feed +import me.ash.reader.data.entity.Group +import me.ash.reader.data.model.Filter +import me.ash.reader.data.preference.FeedsGroupListExpandPreference +import me.ash.reader.data.preference.FeedsGroupListTonalElevationPreference +import me.ash.reader.data.preference.FeedsTopBarTonalElevationPreference +import me.ash.reader.ui.component.FilterBar +import me.ash.reader.ui.component.base.FeedbackIconButton +import me.ash.reader.ui.ext.alphaLN +import me.ash.reader.ui.ext.surfaceColorAtElevation +import me.ash.reader.ui.page.home.feeds.FeedItem +import me.ash.reader.ui.page.home.feeds.GroupItem +import me.ash.reader.ui.theme.palette.onDark +import kotlin.math.ln + +@Composable +fun FeedsPagePreview( + topBarTonalElevation: FeedsTopBarTonalElevationPreference, + groupListExpand: FeedsGroupListExpandPreference, + groupListTonalElevation: FeedsGroupListTonalElevationPreference, + filterBarStyle: Int, + filterBarFilled: Boolean, + filterBarPadding: Dp, + filterBarTonalElevation: Dp, +) { + var filter by remember { mutableStateOf(Filter.Unread) } + 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 + ) + } + } + + Column( + modifier = Modifier + .animateContentSize() + .background( + color = MaterialTheme.colorScheme.surfaceColorAtElevation( + groupListTonalElevation.value.dp + ) onDark MaterialTheme.colorScheme.surface, + shape = RoundedCornerShape(24.dp) + ) + ) { + SmallTopAppBar( + title = {}, + colors = TopAppBarDefaults.smallTopAppBarColors( + containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation( + topBarTonalElevation.value.dp + ), + ), + navigationIcon = { + FeedbackIconButton( + imageVector = Icons.Rounded.ArrowBack, + contentDescription = stringResource(R.string.back), + tint = MaterialTheme.colorScheme.onSurface + ) + }, + actions = { + FeedbackIconButton( + imageVector = Icons.Rounded.Refresh, + contentDescription = stringResource(R.string.refresh), + tint = MaterialTheme.colorScheme.onSurface, + ) + FeedbackIconButton( + imageVector = Icons.Rounded.Add, + contentDescription = stringResource(R.string.subscribe), + tint = MaterialTheme.colorScheme.onSurface, + ) + } + ) + Spacer(modifier = Modifier.height(12.dp)) + GroupItem( + isExpanded = { groupListExpand.value }, + group = generateGroupPreview(), + alpha = groupAlpha, + indicatorAlpha = groupIndicatorAlpha, + ) + FeedItem( + feed = generateFeedPreview(), + alpha = groupAlpha, + badgeAlpha = feedBadgeAlpha, + isEnded = true, + isExpanded = { true }, + ) + Spacer(modifier = Modifier.height(12.dp)) + FilterBar( + filter = filter, + filterBarStyle = filterBarStyle, + filterBarFilled = filterBarFilled, + filterBarPadding = filterBarPadding, + filterBarTonalElevation = filterBarTonalElevation, + ) { + filter = it + } + } +} + +@Stable +@Composable +fun generateFeedPreview(): Feed = + Feed( + id = "", + name = stringResource(R.string.preview_feed_name), + icon = "", + accountId = 0, + groupId = "", + url = "", + ).apply { + important = 100 + } + +@Stable +@Composable +fun generateGroupPreview(): Group = + Group( + id = "", + name = stringResource(R.string.defaults), + accountId = 0, + ) \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/page/settings/color/feeds/FeedsPageStyle.kt b/app/src/main/java/me/ash/reader/ui/page/settings/color/feeds/FeedsPageStylePage.kt similarity index 67% rename from app/src/main/java/me/ash/reader/ui/page/settings/color/feeds/FeedsPageStyle.kt rename to app/src/main/java/me/ash/reader/ui/page/settings/color/feeds/FeedsPageStylePage.kt index a7fce97..3a1a953 100644 --- a/app/src/main/java/me/ash/reader/ui/page/settings/color/feeds/FeedsPageStyle.kt +++ b/app/src/main/java/me/ash/reader/ui/page/settings/color/feeds/FeedsPageStylePage.kt @@ -1,41 +1,29 @@ package me.ash.reader.ui.page.settings.color.feeds -import androidx.compose.animation.animateContentSize import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Add import androidx.compose.material.icons.rounded.ArrowBack -import androidx.compose.material.icons.rounded.Refresh -import androidx.compose.material3.* +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController import me.ash.reader.R -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.preference.* -import me.ash.reader.ui.component.* -import me.ash.reader.ui.ext.surfaceColorAtElevation -import me.ash.reader.ui.page.home.FilterBar -import me.ash.reader.ui.page.home.feeds.GroupItem +import me.ash.reader.ui.component.base.* import me.ash.reader.ui.page.settings.SettingItem -import me.ash.reader.ui.theme.palette.onDark import me.ash.reader.ui.theme.palette.onLight -@OptIn(ExperimentalMaterial3Api::class) @Composable -fun FeedsPageStyle( +fun FeedsPageStylePage( navController: NavHostController, ) { val context = LocalContext.current @@ -57,29 +45,16 @@ fun FeedsPageStyle( var filterBarPaddingValue: Int? by remember { mutableStateOf(filterBarPadding) } - Scaffold( - modifier = Modifier - .background(MaterialTheme.colorScheme.surface onLight MaterialTheme.colorScheme.inverseOnSurface) - .statusBarsPadding() - .navigationBarsPadding(), + RYScaffold( containerColor = MaterialTheme.colorScheme.surface onLight MaterialTheme.colorScheme.inverseOnSurface, - topBar = { - SmallTopAppBar( - colors = TopAppBarDefaults.smallTopAppBarColors( - containerColor = MaterialTheme.colorScheme.surface onLight MaterialTheme.colorScheme.inverseOnSurface - ), - title = {}, - navigationIcon = { - FeedbackIconButton( - imageVector = Icons.Rounded.ArrowBack, - contentDescription = stringResource(R.string.back), - tint = MaterialTheme.colorScheme.onSurface - ) { - navController.popBackStack() - } - }, - actions = {} - ) + navigationIcon = { + FeedbackIconButton( + imageVector = Icons.Rounded.ArrowBack, + contentDescription = stringResource(R.string.back), + tint = MaterialTheme.colorScheme.onSurface + ) { + navController.popBackStack() + } }, content = { LazyColumn { @@ -144,7 +119,7 @@ fun FeedsPageStyle( (!groupListExpand).put(context, scope) }, ) { - Switch(activated = groupListExpand.value) { + RYSwitch(activated = groupListExpand.value) { (!groupListExpand).put(context, scope) } } @@ -178,7 +153,7 @@ fun FeedsPageStyle( (!filterBarFilled).put(context, scope) }, ) { - Switch(activated = filterBarFilled.value) { + RYSwitch(activated = filterBarFilled.value) { (!filterBarFilled).put(context, scope) } } @@ -197,7 +172,9 @@ fun FeedsPageStyle( filterBarTonalElevationDialogVisible = true }, ) {} - Spacer(modifier = Modifier.height(24.dp)) + } + item { + Spacer(modifier = Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars)) } } } @@ -280,88 +257,3 @@ fun FeedsPageStyle( groupListTonalElevationDialogVisible = false } } - -@Composable -fun FeedsPagePreview( - topBarTonalElevation: FeedsTopBarTonalElevationPreference, - groupListExpand: FeedsGroupListExpandPreference, - groupListTonalElevation: FeedsGroupListTonalElevationPreference, - filterBarStyle: Int, - filterBarFilled: Boolean, - filterBarPadding: Dp, - filterBarTonalElevation: Dp, -) { - var filter by remember { mutableStateOf(Filter.Unread) } - - Column( - modifier = Modifier - .animateContentSize() - .background( - color = MaterialTheme.colorScheme.surfaceColorAtElevation( - groupListTonalElevation.value.dp - ) onDark MaterialTheme.colorScheme.surface, - shape = RoundedCornerShape(24.dp) - ) - ) { - SmallTopAppBar( - title = {}, - colors = TopAppBarDefaults.smallTopAppBarColors( - containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation( - topBarTonalElevation.value.dp - ), - ), - navigationIcon = { - FeedbackIconButton( - imageVector = Icons.Rounded.ArrowBack, - contentDescription = stringResource(R.string.back), - tint = MaterialTheme.colorScheme.onSurface - ) {} - }, - actions = { - FeedbackIconButton( - imageVector = Icons.Rounded.Refresh, - contentDescription = stringResource(R.string.refresh), - tint = MaterialTheme.colorScheme.onSurface, - ) {} - FeedbackIconButton( - imageVector = Icons.Rounded.Add, - contentDescription = stringResource(R.string.subscribe), - tint = MaterialTheme.colorScheme.onSurface, - ) {} - } - ) - Spacer(modifier = Modifier.height(12.dp)) - GroupItem( - isExpanded = groupListExpand.value, - tonalElevation = groupListTonalElevation.value.dp, - group = Group( - id = "", - name = stringResource(R.string.defaults), - accountId = 0, - ), - feeds = listOf( - Feed( - id = "", - name = stringResource(R.string.preview_feed_name), - icon = "", - accountId = 0, - groupId = "", - url = "", - ).apply { - important = 100 - } - ), - ) - Spacer(modifier = Modifier.height(12.dp)) - FilterBar( - modifier = Modifier.padding(horizontal = 12.dp), - filter = filter, - filterBarStyle = filterBarStyle, - filterBarFilled = filterBarFilled, - filterBarPadding = filterBarPadding, - filterBarTonalElevation = filterBarTonalElevation, - ) { - filter = it - } - } -} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/page/settings/color/flow/FlowPagePreview.kt b/app/src/main/java/me/ash/reader/ui/page/settings/color/flow/FlowPagePreview.kt new file mode 100644 index 0000000..d3101fb --- /dev/null +++ b/app/src/main/java/me/ash/reader/ui/page/settings/color/flow/FlowPagePreview.kt @@ -0,0 +1,124 @@ +package me.ash.reader.ui.page.settings.color.flow + +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.ArrowBack +import androidx.compose.material.icons.rounded.DoneAll +import androidx.compose.material.icons.rounded.Search +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SmallTopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import me.ash.reader.R +import me.ash.reader.data.entity.Article +import me.ash.reader.data.entity.ArticleWithFeed +import me.ash.reader.data.entity.Feed +import me.ash.reader.data.model.Filter +import me.ash.reader.data.preference.FlowArticleListTonalElevationPreference +import me.ash.reader.data.preference.FlowTopBarTonalElevationPreference +import me.ash.reader.ui.component.FilterBar +import me.ash.reader.ui.component.base.FeedbackIconButton +import me.ash.reader.ui.ext.surfaceColorAtElevation +import me.ash.reader.ui.page.home.flow.ArticleItem +import me.ash.reader.ui.theme.palette.onDark +import java.util.* + +@Composable +fun FlowPagePreview( + topBarTonalElevation: FlowTopBarTonalElevationPreference, + articleListTonalElevation: FlowArticleListTonalElevationPreference, + filterBarStyle: Int, + filterBarFilled: Boolean, + filterBarPadding: Dp, + filterBarTonalElevation: Dp, +) { + var filter by remember { mutableStateOf(Filter.Unread) } + + Column( + modifier = Modifier + .animateContentSize() + .background( + color = MaterialTheme.colorScheme.surfaceColorAtElevation( + articleListTonalElevation.value.dp + ) onDark MaterialTheme.colorScheme.surface, + shape = RoundedCornerShape(24.dp) + ) + ) { + SmallTopAppBar( + title = {}, + colors = TopAppBarDefaults.smallTopAppBarColors( + containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation( + topBarTonalElevation.value.dp + ), + ), + navigationIcon = { + FeedbackIconButton( + imageVector = Icons.Rounded.ArrowBack, + contentDescription = stringResource(R.string.back), + tint = MaterialTheme.colorScheme.onSurface + ) {} + }, + actions = { + FeedbackIconButton( + imageVector = Icons.Rounded.DoneAll, + contentDescription = stringResource(R.string.mark_all_as_read), + tint = MaterialTheme.colorScheme.onSurface, + ) {} + FeedbackIconButton( + imageVector = Icons.Rounded.Search, + contentDescription = stringResource(R.string.search), + tint = MaterialTheme.colorScheme.onSurface, + ) {} + } + ) + Spacer(modifier = Modifier.height(12.dp)) + ArticleItem( + articleWithFeed = generateArticleWithFeedPreview(), + ) + Spacer(modifier = Modifier.height(12.dp)) + FilterBar( + filter = filter, + filterBarStyle = filterBarStyle, + filterBarFilled = filterBarFilled, + filterBarPadding = filterBarPadding, + filterBarTonalElevation = filterBarTonalElevation, + ) { + filter = it + } + } +} + +@Stable +@Composable +fun generateArticleWithFeedPreview(): ArticleWithFeed = + ArticleWithFeed( + Article( + id = "", + title = stringResource(R.string.preview_article_title), + shortDescription = stringResource(R.string.preview_article_desc), + rawDescription = stringResource(R.string.preview_article_desc), + link = "", + feedId = "", + accountId = 0, + date = Date(1654053729L), + isStarred = true, + img = "https://images.unsplash.com/photo-1544716278-ca5e3f4abd8c?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1yZWxhdGVkfDJ8fHxlbnwwfHx8fA%3D%3D&auto=format&fit=crop&w=800&q=60" + ), + feed = Feed( + id = "", + name = stringResource(R.string.preview_feed_name), + icon = "", + accountId = 0, + groupId = "", + url = "", + ), + ) diff --git a/app/src/main/java/me/ash/reader/ui/page/settings/color/flow/FlowPageStyle.kt b/app/src/main/java/me/ash/reader/ui/page/settings/color/flow/FlowPageStylePage.kt similarity index 68% rename from app/src/main/java/me/ash/reader/ui/page/settings/color/flow/FlowPageStyle.kt rename to app/src/main/java/me/ash/reader/ui/page/settings/color/flow/FlowPageStylePage.kt index a07e097..add9a0d 100644 --- a/app/src/main/java/me/ash/reader/ui/page/settings/color/flow/FlowPageStyle.kt +++ b/app/src/main/java/me/ash/reader/ui/page/settings/color/flow/FlowPageStylePage.kt @@ -1,7 +1,5 @@ package me.ash.reader.ui.page.settings.color.flow -import android.annotation.SuppressLint -import androidx.compose.animation.animateContentSize import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -9,37 +7,23 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.ArrowBack -import androidx.compose.material.icons.rounded.DoneAll -import androidx.compose.material.icons.rounded.Search -import androidx.compose.material3.* +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController import me.ash.reader.R -import me.ash.reader.data.entity.Article -import me.ash.reader.data.entity.ArticleWithFeed -import me.ash.reader.data.entity.Feed -import me.ash.reader.data.entity.Filter import me.ash.reader.data.preference.* -import me.ash.reader.ui.component.* -import me.ash.reader.ui.ext.surfaceColorAtElevation -import me.ash.reader.ui.page.home.FilterBar -import me.ash.reader.ui.page.home.flow.ArticleItem +import me.ash.reader.ui.component.base.* import me.ash.reader.ui.page.settings.SettingItem -import me.ash.reader.ui.theme.palette.onDark import me.ash.reader.ui.theme.palette.onLight -import java.util.* -@SuppressLint("FlowOperatorInvokedInComposition") -@OptIn(ExperimentalMaterial3Api::class) @Composable -fun FlowPageStyle( +fun FlowPageStylePage( navController: NavHostController, ) { val context = LocalContext.current @@ -66,29 +50,16 @@ fun FlowPageStyle( var filterBarPaddingValue: Int? by remember { mutableStateOf(filterBarPadding) } - Scaffold( - modifier = Modifier - .background(MaterialTheme.colorScheme.surface onLight MaterialTheme.colorScheme.inverseOnSurface) - .statusBarsPadding() - .navigationBarsPadding(), + RYScaffold( containerColor = MaterialTheme.colorScheme.surface onLight MaterialTheme.colorScheme.inverseOnSurface, - topBar = { - SmallTopAppBar( - colors = TopAppBarDefaults.smallTopAppBarColors( - containerColor = MaterialTheme.colorScheme.surface onLight MaterialTheme.colorScheme.inverseOnSurface - ), - title = {}, - navigationIcon = { - FeedbackIconButton( - imageVector = Icons.Rounded.ArrowBack, - contentDescription = stringResource(R.string.back), - tint = MaterialTheme.colorScheme.onSurface - ) { - navController.popBackStack() - } - }, - actions = {} - ) + navigationIcon = { + FeedbackIconButton( + imageVector = Icons.Rounded.ArrowBack, + contentDescription = stringResource(R.string.back), + tint = MaterialTheme.colorScheme.onSurface + ) { + navController.popBackStack() + } }, content = { LazyColumn { @@ -158,7 +129,7 @@ fun FlowPageStyle( (!articleListFeedIcon).put(context, scope) }, ) { - Switch(activated = articleListFeedIcon.value) { + RYSwitch(activated = articleListFeedIcon.value) { (!articleListFeedIcon).put(context, scope) } } @@ -168,7 +139,7 @@ fun FlowPageStyle( (!articleListFeedName).put(context, scope) }, ) { - Switch(activated = articleListFeedName.value) { + RYSwitch(activated = articleListFeedName.value) { (!articleListFeedName).put(context, scope) } } @@ -178,7 +149,7 @@ fun FlowPageStyle( (!articleListImage).put(context, scope) }, ) { - Switch(activated = articleListImage.value) { + RYSwitch(activated = articleListImage.value) { (!articleListImage).put(context, scope) } } @@ -188,7 +159,7 @@ fun FlowPageStyle( (!articleListDesc).put(context, scope) }, ) { - Switch(activated = articleListDesc.value) { + RYSwitch(activated = articleListDesc.value) { (!articleListDesc).put(context, scope) } } @@ -198,7 +169,7 @@ fun FlowPageStyle( (!articleListTime).put(context, scope) }, ) { - Switch(activated = articleListTime.value) { + RYSwitch(activated = articleListTime.value) { (!articleListTime).put(context, scope) } } @@ -208,7 +179,7 @@ fun FlowPageStyle( (!articleListStickyDate).put(context, scope) }, ) { - Switch(activated = articleListStickyDate.value) { + RYSwitch(activated = articleListStickyDate.value) { (!articleListStickyDate).put(context, scope) } } @@ -242,7 +213,7 @@ fun FlowPageStyle( (!filterBarFilled).put(context, scope) }, ) { - Switch(activated = filterBarFilled.value) { + RYSwitch(activated = filterBarFilled.value) { (!filterBarFilled).put(context, scope) } } @@ -261,7 +232,9 @@ fun FlowPageStyle( filterBarTonalElevationDialogVisible = true }, ) {} - Spacer(modifier = Modifier.height(24.dp)) + } + item { + Spacer(modifier = Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars)) } } } @@ -344,90 +317,3 @@ fun FlowPageStyle( articleListTonalElevationDialogVisible = false } } - -@Composable -fun FlowPagePreview( - topBarTonalElevation: FlowTopBarTonalElevationPreference, - articleListTonalElevation: FlowArticleListTonalElevationPreference, - filterBarStyle: Int, - filterBarFilled: Boolean, - filterBarPadding: Dp, - filterBarTonalElevation: Dp, -) { - var filter by remember { mutableStateOf(Filter.Unread) } - - Column( - modifier = Modifier - .animateContentSize() - .background( - color = MaterialTheme.colorScheme.surfaceColorAtElevation( - articleListTonalElevation.value.dp - ) onDark MaterialTheme.colorScheme.surface, - shape = RoundedCornerShape(24.dp) - ) - ) { - SmallTopAppBar( - title = {}, - colors = TopAppBarDefaults.smallTopAppBarColors( - containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation( - topBarTonalElevation.value.dp - ), - ), - navigationIcon = { - FeedbackIconButton( - imageVector = Icons.Rounded.ArrowBack, - contentDescription = stringResource(R.string.back), - tint = MaterialTheme.colorScheme.onSurface - ) {} - }, - actions = { - FeedbackIconButton( - imageVector = Icons.Rounded.DoneAll, - contentDescription = stringResource(R.string.mark_all_as_read), - tint = MaterialTheme.colorScheme.onSurface, - ) {} - FeedbackIconButton( - imageVector = Icons.Rounded.Search, - contentDescription = stringResource(R.string.search), - tint = MaterialTheme.colorScheme.onSurface, - ) {} - } - ) - Spacer(modifier = Modifier.height(12.dp)) - ArticleItem( - articleWithFeed = ArticleWithFeed( - Article( - id = "", - title = stringResource(R.string.preview_article_title), - shortDescription = stringResource(R.string.preview_article_desc), - rawDescription = stringResource(R.string.preview_article_desc), - link = "", - feedId = "", - accountId = 0, - date = Date(), - isStarred = true, - img = "https://images.unsplash.com/photo-1544716278-ca5e3f4abd8c?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1yZWxhdGVkfDJ8fHxlbnwwfHx8fA%3D%3D&auto=format&fit=crop&w=800&q=60" - ), - feed = Feed( - id = "", - name = stringResource(R.string.preview_feed_name), - icon = "", - accountId = 0, - groupId = "", - url = "", - ), - ) - ) - Spacer(modifier = Modifier.height(12.dp)) - FilterBar( - modifier = Modifier.padding(horizontal = 12.dp), - filter = filter, - filterBarStyle = filterBarStyle, - filterBarFilled = filterBarFilled, - filterBarPadding = filterBarPadding, - filterBarTonalElevation = filterBarTonalElevation, - ) { - filter = it - } - } -} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/page/settings/interaction/Interaction.kt b/app/src/main/java/me/ash/reader/ui/page/settings/interaction/Interaction.kt deleted file mode 100644 index ca362b6..0000000 --- a/app/src/main/java/me/ash/reader/ui/page/settings/interaction/Interaction.kt +++ /dev/null @@ -1,165 +0,0 @@ -package me.ash.reader.ui.page.settings.interaction - -import android.annotation.SuppressLint -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.ArrowBack -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.navigation.NavHostController -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch -import me.ash.reader.R -import me.ash.reader.ui.component.* -import me.ash.reader.ui.ext.DataStoreKeys -import me.ash.reader.ui.ext.dataStore -import me.ash.reader.ui.ext.put -import me.ash.reader.ui.page.settings.SettingItem -import me.ash.reader.ui.theme.palette.onLight - -@SuppressLint("FlowOperatorInvokedInComposition") -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun Interaction( - navController: NavHostController, -) { - val context = LocalContext.current - val scope = rememberCoroutineScope() - var initialPageDialogVisible by remember { mutableStateOf(false) } - var initialFilterDialogVisible by remember { mutableStateOf(false) } - - val initialPage = context.dataStore.data - .map { it[DataStoreKeys.InitialPage.key] ?: 0 } - .collectAsState(initial = 0).value - - val initialFilter = context.dataStore.data - .map { it[DataStoreKeys.InitialFilter.key] ?: 2 } - .collectAsState(initial = 2).value - - Scaffold( - modifier = Modifier - .background(MaterialTheme.colorScheme.surface onLight MaterialTheme.colorScheme.inverseOnSurface) - .statusBarsPadding() - .navigationBarsPadding(), - containerColor = MaterialTheme.colorScheme.surface onLight MaterialTheme.colorScheme.inverseOnSurface, - topBar = { - SmallTopAppBar( - colors = TopAppBarDefaults.smallTopAppBarColors( - containerColor = MaterialTheme.colorScheme.surface onLight MaterialTheme.colorScheme.inverseOnSurface - ), - title = {}, - navigationIcon = { - FeedbackIconButton( - imageVector = Icons.Rounded.ArrowBack, - contentDescription = stringResource(R.string.back), - tint = MaterialTheme.colorScheme.onSurface - ) { - navController.popBackStack() - } - }, - actions = {} - ) - }, - content = { - LazyColumn { - item { - DisplayText(text = stringResource(R.string.interaction), desc = "") - Spacer(modifier = Modifier.height(16.dp)) - } - item { - Subtitle( - modifier = Modifier.padding(horizontal = 24.dp), - text = stringResource(R.string.on_start), - ) - SettingItem( - title = stringResource(R.string.initial_page), - desc = when (initialPage) { - 0 -> stringResource(R.string.feeds_page) - 1 -> stringResource(R.string.flow_page) - else -> "" - }, - onClick = { - initialPageDialogVisible = true - }, - ) {} - SettingItem( - title = stringResource(R.string.initial_filter), - desc = when (initialFilter) { - 0 -> stringResource(R.string.starred) - 1 -> stringResource(R.string.unread) - 2 -> stringResource(R.string.all) - else -> "" - }, - onClick = { - initialFilterDialogVisible = true - }, - ) {} - } - } - } - ) - - RadioDialog( - visible = initialPageDialogVisible, - title = stringResource(R.string.initial_page), - options = listOf( - RadioDialogOption( - text = stringResource(R.string.feeds_page), - selected = initialPage == 0, - ) { - scope.launch { - context.dataStore.put(DataStoreKeys.InitialPage, 0) - } - }, - RadioDialogOption( - text = stringResource(R.string.flow_page), - selected = initialPage == 1, - ) { - scope.launch { - context.dataStore.put(DataStoreKeys.InitialPage, 1) - } - }, - ), - ) { - initialPageDialogVisible = false - } - - RadioDialog( - visible = initialFilterDialogVisible, - title = stringResource(R.string.initial_filter), - options = listOf( - RadioDialogOption( - text = stringResource(R.string.starred), - selected = initialFilter == 0, - ) { - scope.launch { - context.dataStore.put(DataStoreKeys.InitialFilter, 0) - } - }, - RadioDialogOption( - text = stringResource(R.string.unread), - selected = initialFilter == 1, - ) { - scope.launch { - context.dataStore.put(DataStoreKeys.InitialFilter, 1) - } - }, - RadioDialogOption( - text = stringResource(R.string.all), - selected = initialFilter == 2, - ) { - scope.launch { - context.dataStore.put(DataStoreKeys.InitialFilter, 2) - } - }, - ), - ) { - initialFilterDialogVisible = false - } -} diff --git a/app/src/main/java/me/ash/reader/ui/page/settings/interaction/InteractionPage.kt b/app/src/main/java/me/ash/reader/ui/page/settings/interaction/InteractionPage.kt new file mode 100644 index 0000000..e8b39cc --- /dev/null +++ b/app/src/main/java/me/ash/reader/ui/page/settings/interaction/InteractionPage.kt @@ -0,0 +1,107 @@ +package me.ash.reader.ui.page.settings.interaction + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.ArrowBack +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.navigation.NavHostController +import me.ash.reader.R +import me.ash.reader.data.preference.InitialFilterPreference +import me.ash.reader.data.preference.InitialPagePreference +import me.ash.reader.data.preference.LocalInitialFilter +import me.ash.reader.data.preference.LocalInitialPage +import me.ash.reader.ui.component.base.* +import me.ash.reader.ui.page.settings.SettingItem +import me.ash.reader.ui.theme.palette.onLight + +@Composable +fun InteractionPage( + navController: NavHostController, +) { + val context = LocalContext.current + val initialPage = LocalInitialPage.current + val initialFilter = LocalInitialFilter.current + val scope = rememberCoroutineScope() + var initialPageDialogVisible by remember { mutableStateOf(false) } + var initialFilterDialogVisible by remember { mutableStateOf(false) } + + RYScaffold( + containerColor = MaterialTheme.colorScheme.surface onLight MaterialTheme.colorScheme.inverseOnSurface, + navigationIcon = { + FeedbackIconButton( + imageVector = Icons.Rounded.ArrowBack, + contentDescription = stringResource(R.string.back), + tint = MaterialTheme.colorScheme.onSurface + ) { + navController.popBackStack() + } + }, + content = { + LazyColumn { + item { + DisplayText(text = stringResource(R.string.interaction), desc = "") + Spacer(modifier = Modifier.height(16.dp)) + } + item { + Subtitle( + modifier = Modifier.padding(horizontal = 24.dp), + text = stringResource(R.string.on_start), + ) + SettingItem( + title = stringResource(R.string.initial_page), + desc = initialPage.getDesc(context), + onClick = { + initialPageDialogVisible = true + }, + ) {} + SettingItem( + title = stringResource(R.string.initial_filter), + desc = initialFilter.getDesc(context), + onClick = { + initialFilterDialogVisible = true + }, + ) {} + } + item { + Spacer(modifier = Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars)) + } + } + } + ) + + RadioDialog( + visible = initialPageDialogVisible, + title = stringResource(R.string.initial_page), + options = InitialPagePreference.values.map { + RadioDialogOption( + text = it.getDesc(context), + selected = it == initialPage, + ) { + it.put(context, scope) + } + }, + ) { + initialPageDialogVisible = false + } + + RadioDialog( + visible = initialFilterDialogVisible, + title = stringResource(R.string.initial_filter), + options = InitialFilterPreference.values.map { + RadioDialogOption( + text = it.getDesc(context), + selected = it == initialFilter, + ) { + it.put(context, scope) + } + }, + ) { + initialFilterDialogVisible = false + } +} diff --git a/app/src/main/java/me/ash/reader/ui/page/settings/languages/Languages.kt b/app/src/main/java/me/ash/reader/ui/page/settings/languages/LanguagesPage.kt similarity index 67% rename from app/src/main/java/me/ash/reader/ui/page/settings/languages/Languages.kt rename to app/src/main/java/me/ash/reader/ui/page/settings/languages/LanguagesPage.kt index 66edf01..0268562 100644 --- a/app/src/main/java/me/ash/reader/ui/page/settings/languages/Languages.kt +++ b/app/src/main/java/me/ash/reader/ui/page/settings/languages/LanguagesPage.kt @@ -2,17 +2,16 @@ package me.ash.reader.ui.page.settings.languages import android.content.Intent import android.net.Uri -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.KeyboardArrowRight import androidx.compose.material.icons.outlined.Lightbulb import androidx.compose.material.icons.rounded.ArrowBack -import androidx.compose.material3.* +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier @@ -23,44 +22,32 @@ import androidx.navigation.NavHostController import me.ash.reader.R import me.ash.reader.data.preference.LanguagesPreference import me.ash.reader.data.preference.LocalLanguages -import me.ash.reader.ui.component.Banner -import me.ash.reader.ui.component.DisplayText -import me.ash.reader.ui.component.FeedbackIconButton +import me.ash.reader.ui.component.base.Banner +import me.ash.reader.ui.component.base.DisplayText +import me.ash.reader.ui.component.base.FeedbackIconButton +import me.ash.reader.ui.component.base.RYScaffold import me.ash.reader.ui.page.settings.SettingItem import me.ash.reader.ui.theme.palette.onLight @OptIn(ExperimentalMaterial3Api::class) @Composable -fun Languages( +fun LanguagesPage( navController: NavHostController, ) { val context = LocalContext.current val languages = LocalLanguages.current val scope = rememberCoroutineScope() - Scaffold( - modifier = Modifier - .background(MaterialTheme.colorScheme.surface onLight MaterialTheme.colorScheme.inverseOnSurface) - .statusBarsPadding() - .navigationBarsPadding(), + RYScaffold( containerColor = MaterialTheme.colorScheme.surface onLight MaterialTheme.colorScheme.inverseOnSurface, - topBar = { - SmallTopAppBar( - colors = TopAppBarDefaults.smallTopAppBarColors( - containerColor = MaterialTheme.colorScheme.surface onLight MaterialTheme.colorScheme.inverseOnSurface - ), - title = {}, - navigationIcon = { - FeedbackIconButton( - imageVector = Icons.Rounded.ArrowBack, - contentDescription = stringResource(R.string.back), - tint = MaterialTheme.colorScheme.onSurface - ) { - navController.popBackStack() - } - }, - actions = {} - ) + navigationIcon = { + FeedbackIconButton( + imageVector = Icons.Rounded.ArrowBack, + contentDescription = stringResource(R.string.back), + tint = MaterialTheme.colorScheme.onSurface + ) { + navController.popBackStack() + } }, content = { LazyColumn { @@ -100,6 +87,9 @@ fun Languages( } } } + item { + Spacer(modifier = Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars)) + } } } ) diff --git a/app/src/main/java/me/ash/reader/ui/page/settings/tips/TipsAndSupport.kt b/app/src/main/java/me/ash/reader/ui/page/settings/tips/TipsAndSupportPage.kt similarity index 83% rename from app/src/main/java/me/ash/reader/ui/page/settings/tips/TipsAndSupport.kt rename to app/src/main/java/me/ash/reader/ui/page/settings/tips/TipsAndSupportPage.kt index b9f0b76..c2d7b9a 100644 --- a/app/src/main/java/me/ash/reader/ui/page/settings/tips/TipsAndSupport.kt +++ b/app/src/main/java/me/ash/reader/ui/page/settings/tips/TipsAndSupportPage.kt @@ -36,21 +36,20 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavHostController import me.ash.reader.R -import me.ash.reader.ui.component.CurlyCornerShape -import me.ash.reader.ui.component.FeedbackIconButton +import me.ash.reader.ui.component.base.CurlyCornerShape +import me.ash.reader.ui.component.base.FeedbackIconButton +import me.ash.reader.ui.component.base.RYScaffold import me.ash.reader.ui.ext.* import me.ash.reader.ui.theme.palette.alwaysLight import me.ash.reader.ui.theme.palette.onLight -@OptIn(ExperimentalMaterial3Api::class) @Composable -fun TipsAndSupport( +fun TipsAndSupportPage( navController: NavHostController, updateViewModel: UpdateViewModel = hiltViewModel(), ) { val context = LocalContext.current val view = LocalView.current - val scope = rememberCoroutineScope() var currentVersion by remember { mutableStateOf("") } var clickTime by remember { mutableStateOf(System.currentTimeMillis() - 2000) } var pressAMP by remember { mutableStateOf(16f) } @@ -63,38 +62,26 @@ fun TipsAndSupport( currentVersion = context.getCurrentVersion().toString() } - Scaffold( - modifier = Modifier - .background(MaterialTheme.colorScheme.surface onLight MaterialTheme.colorScheme.inverseOnSurface) - .statusBarsPadding() - .navigationBarsPadding(), + RYScaffold( containerColor = MaterialTheme.colorScheme.surface onLight MaterialTheme.colorScheme.inverseOnSurface, - topBar = { - SmallTopAppBar( - colors = TopAppBarDefaults.smallTopAppBarColors( - containerColor = MaterialTheme.colorScheme.surface onLight MaterialTheme.colorScheme.inverseOnSurface - ), - title = {}, - navigationIcon = { - FeedbackIconButton( - imageVector = Icons.Rounded.ArrowBack, - contentDescription = stringResource(R.string.back), - tint = MaterialTheme.colorScheme.onSurface - ) { - navController.popBackStack() - } - }, - actions = { - FeedbackIconButton( - modifier = Modifier.size(20.dp), - imageVector = Icons.Rounded.Balance, - contentDescription = stringResource(R.string.open_source_licenses), - tint = MaterialTheme.colorScheme.onSurface - ) { - context.showToast(context.getString(R.string.coming_soon)) - } - } - ) + navigationIcon = { + FeedbackIconButton( + imageVector = Icons.Rounded.ArrowBack, + contentDescription = stringResource(R.string.back), + tint = MaterialTheme.colorScheme.onSurface + ) { + navController.popBackStack() + } + }, + actions = { + FeedbackIconButton( + modifier = Modifier.size(20.dp), + imageVector = Icons.Rounded.Balance, + contentDescription = stringResource(R.string.open_source_licenses), + tint = MaterialTheme.colorScheme.onSurface + ) { + context.showToast(context.getString(R.string.coming_soon)) + } }, content = { LazyColumn( @@ -117,23 +104,21 @@ fun TipsAndSupport( onTap = { if (System.currentTimeMillis() - clickTime > 2000) { clickTime = System.currentTimeMillis() - updateViewModel.dispatch( - UpdateViewAction.CheckUpdate( - { - context.showToast(context.getString(R.string.checking_updates)) - context.dataStore.put( - DataStoreKeys.SkipVersionNumber, - "" + updateViewModel.checkUpdate( + { + context.showToast(context.getString(R.string.checking_updates)) + context.dataStore.put( + DataStoreKeys.SkipVersionNumber, + "" + ) + }, + { + if (!it) { + context.showToast( + context.getString(R.string.is_latest_version) ) - }, - { - if (!it) { - context.showToast( - context.getString(R.string.is_latest_version) - ) - } } - ) + } ) } else { clickTime = System.currentTimeMillis() diff --git a/app/src/main/java/me/ash/reader/ui/page/settings/tips/UpdateDialog.kt b/app/src/main/java/me/ash/reader/ui/page/settings/tips/UpdateDialog.kt index 081058b..5bce517 100644 --- a/app/src/main/java/me/ash/reader/ui/page/settings/tips/UpdateDialog.kt +++ b/app/src/main/java/me/ash/reader/ui/page/settings/tips/UpdateDialog.kt @@ -1,7 +1,6 @@ package me.ash.reader.ui.page.settings.tips import android.Manifest -import android.annotation.SuppressLint import android.content.Intent import android.net.Uri import android.provider.Settings @@ -30,43 +29,26 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel -import com.google.accompanist.pager.ExperimentalPagerApi import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch import me.ash.reader.R +import me.ash.reader.data.preference.* import me.ash.reader.data.source.Download -import me.ash.reader.ui.component.Dialog -import me.ash.reader.ui.ext.* +import me.ash.reader.ui.component.base.RYDialog +import me.ash.reader.ui.ext.collectAsStateValue +import me.ash.reader.ui.ext.installLatestApk -@SuppressLint("FlowOperatorInvokedInComposition") -@OptIn(ExperimentalPagerApi::class) @Composable fun UpdateDialog( updateViewModel: UpdateViewModel = hiltViewModel(), ) { val context = LocalContext.current - val viewState = updateViewModel.viewState.collectAsStateValue() - val downloadState = viewState.downloadFlow.collectAsState(initial = Download.NotYet).value + val updateUiState = updateViewModel.updateUiState.collectAsStateValue() + val downloadState = updateUiState.downloadFlow.collectAsState(initial = Download.NotYet).value val scope = rememberCoroutineScope { Dispatchers.IO } - val newVersionNumber = context.dataStore.data - .map { it[DataStoreKeys.NewVersionNumber.key] ?: "" } - .collectAsState(initial = "") - .value - val newVersionPublishDate = context.dataStore.data - .map { it[DataStoreKeys.NewVersionPublishDate.key] ?: "" } - .collectAsState(initial = "") - .value - val newVersionLog = context.dataStore.data - .map { it[DataStoreKeys.NewVersionLog.key] ?: "" } - .collectAsState(initial = "") - .value - val newVersionSize = " " + context.dataStore.data - .map { it[DataStoreKeys.NewVersionSize.key] ?: 0 } - .map { it / 1024f / 1024f } - .map { if (it > 0f) " ${String.format("%.2f", it)} MB" else "" } - .collectAsState(initial = 0) - .value + val newVersionNumber = LocalNewVersionNumber.current + val newVersionPublishDate = LocalNewVersionPublishDate.current + val newVersionLog = LocalNewVersionLog.current + val newVersionSize = LocalNewVersionSize.current val settings = rememberLauncherForActivityResult( ActivityResultContracts.StartActivityForResult() @@ -89,10 +71,10 @@ fun UpdateDialog( } } - Dialog( + RYDialog( modifier = Modifier.heightIn(max = 400.dp), - visible = viewState.updateDialogVisible, - onDismissRequest = { updateViewModel.dispatch(UpdateViewAction.Hide) }, + visible = updateUiState.updateDialogVisible, + onDismissRequest = { updateViewModel.hideDialog() }, icon = { Icon( imageVector = Icons.Rounded.Update, @@ -106,7 +88,7 @@ fun UpdateDialog( Text(text = stringResource(R.string.change_log)) Spacer(modifier = Modifier.height(16.dp)) Text( - text = "$newVersionPublishDate$newVersionSize", + text = "$newVersionPublishDate $newVersionSize", color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f), style = MaterialTheme.typography.bodyMedium, ) @@ -127,7 +109,7 @@ fun UpdateDialog( context.startActivity( Intent( Intent.ACTION_VIEW, - Uri.parse(context.getString(R.string.github_link)), + Uri.parse("${context.getString(R.string.github_link)}/releases/latest"), ) ) // Disable automatic updates in F-Droid @@ -164,10 +146,8 @@ fun UpdateDialog( if (downloadState !is Download.Progress) { TextButton( onClick = { - scope.launch { - context.dataStore.put(DataStoreKeys.SkipVersionNumber, newVersionNumber) - updateViewModel.dispatch(UpdateViewAction.Hide) - } + SkipVersionNumberPreference.put(context, scope, newVersionNumber.toString()) + updateViewModel.hideDialog() } ) { Text(text = stringResource(R.string.skip_this_version)) diff --git a/app/src/main/java/me/ash/reader/ui/page/settings/tips/UpdateViewModel.kt b/app/src/main/java/me/ash/reader/ui/page/settings/tips/UpdateViewModel.kt index 03739ca..502759e 100644 --- a/app/src/main/java/me/ash/reader/ui/page/settings/tips/UpdateViewModel.kt +++ b/app/src/main/java/me/ash/reader/ui/page/settings/tips/UpdateViewModel.kt @@ -5,40 +5,32 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import me.ash.reader.BuildConfig -import me.ash.reader.data.repository.AppRepository +import me.ash.reader.data.repository.RYRepository import me.ash.reader.data.source.Download +import me.ash.reader.ui.ext.notFdroid import javax.inject.Inject @HiltViewModel class UpdateViewModel @Inject constructor( - private val appRepository: AppRepository, + private val ryRepository: RYRepository, ) : ViewModel() { - private val _viewState = MutableStateFlow(UpdateViewState()) - val viewState: StateFlow = _viewState.asStateFlow() + private val _updateUiState = MutableStateFlow(UpdateUiState()) + val updateUiState: StateFlow = _updateUiState.asStateFlow() - fun dispatch(action: UpdateViewAction) { - when (action) { - is UpdateViewAction.Show -> changeUpdateDialogVisible(true) - is UpdateViewAction.Hide -> changeUpdateDialogVisible(false) - is UpdateViewAction.CheckUpdate -> checkUpdate( - action.preProcessor, - action.postProcessor - ) - is UpdateViewAction.DownloadUpdate -> downloadUpdate(action.url) - } - } - - private fun checkUpdate( + fun checkUpdate( preProcessor: suspend () -> Unit = {}, postProcessor: suspend (Boolean) -> Unit = {} ) { - if (BuildConfig.FLAVOR != "fdroid") { + if (notFdroid) { viewModelScope.launch { preProcessor() - appRepository.checkUpdate().let { + ryRepository.checkUpdate().let { it?.let { - changeUpdateDialogVisible(it) + if (it) { + showDialog() + } else { + hideDialog() + } postProcessor(it) } } @@ -46,45 +38,39 @@ class UpdateViewModel @Inject constructor( } } - private fun changeUpdateDialogVisible(visible: Boolean) { - _viewState.update { + fun showDialog() { + _updateUiState.update { it.copy( - updateDialogVisible = visible + updateDialogVisible = true ) } } - private fun downloadUpdate(url: String) { + fun hideDialog() { + _updateUiState.update { + it.copy( + updateDialogVisible = false + ) + } + } + + fun downloadUpdate(url: String) { viewModelScope.launch { - _viewState.update { + _updateUiState.update { it.copy( downloadFlow = flow { emit(Download.Progress(0)) } ) } - _viewState.update { + _updateUiState.update { it.copy( - downloadFlow = appRepository.downloadFile(url) + downloadFlow = ryRepository.downloadFile(url) ) } } } } -data class UpdateViewState( +data class UpdateUiState( val updateDialogVisible: Boolean = false, val downloadFlow: Flow = emptyFlow(), ) - -sealed class UpdateViewAction { - object Show : UpdateViewAction() - object Hide : UpdateViewAction() - - data class CheckUpdate( - val preProcessor: suspend () -> Unit = {}, - val postProcessor: suspend (Boolean) -> Unit = {} - ) : UpdateViewAction() - - data class DownloadUpdate( - val url: String, - ) : UpdateViewAction() -} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/page/startup/StartupPage.kt b/app/src/main/java/me/ash/reader/ui/page/startup/StartupPage.kt index cc30fbd..36ac290 100644 --- a/app/src/main/java/me/ash/reader/ui/page/startup/StartupPage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/startup/StartupPage.kt @@ -2,8 +2,9 @@ package me.ash.reader.ui.page.startup import android.content.Intent import android.net.Uri -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.CheckCircleOutline @@ -18,9 +19,10 @@ import androidx.navigation.NavHostController import com.ireward.htmlcompose.HtmlText import kotlinx.coroutines.launch import me.ash.reader.R -import me.ash.reader.ui.component.DisplayText -import me.ash.reader.ui.component.DynamicSVGImage -import me.ash.reader.ui.component.Tips +import me.ash.reader.ui.component.base.DisplayText +import me.ash.reader.ui.component.base.DynamicSVGImage +import me.ash.reader.ui.component.base.RYScaffold +import me.ash.reader.ui.component.base.Tips import me.ash.reader.ui.ext.DataStoreKeys import me.ash.reader.ui.ext.dataStore import me.ash.reader.ui.ext.put @@ -28,7 +30,6 @@ import me.ash.reader.ui.page.common.RouteName import me.ash.reader.ui.svg.SVGString import me.ash.reader.ui.svg.WELCOME -@OptIn(ExperimentalMaterial3Api::class) @Composable fun StartupPage( navController: NavHostController, @@ -36,12 +37,7 @@ fun StartupPage( val context = LocalContext.current val scope = rememberCoroutineScope() - Scaffold( - modifier = Modifier - .background(MaterialTheme.colorScheme.surface) - .statusBarsPadding() - .navigationBarsPadding(), - topBar = {}, + RYScaffold( content = { LazyColumn { item { @@ -87,16 +83,7 @@ fun StartupPage( } } }, - bottomBar = { -// Row( -// modifier = Modifier -// .fillMaxWidth() -// .padding(24.dp), -// horizontalArrangement = Arrangement.End, -// verticalAlignment = Alignment.CenterVertically, -// ) { -// } - }, + bottomBar = null, floatingActionButton = { ExtendedFloatingActionButton( onClick = { diff --git a/app/src/main/java/me/ash/reader/ui/theme/Shapes.kt b/app/src/main/java/me/ash/reader/ui/theme/Shapes.kt new file mode 100644 index 0000000..9905264 --- /dev/null +++ b/app/src/main/java/me/ash/reader/ui/theme/Shapes.kt @@ -0,0 +1,26 @@ +package me.ash.reader.ui.theme + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Shapes +import androidx.compose.runtime.Stable +import androidx.compose.ui.unit.dp + +val Shapes = Shapes( + extraSmall = RoundedCornerShape(4.0.dp), + small = RoundedCornerShape(8.0.dp), + medium = RoundedCornerShape(12.0.dp), + large = RoundedCornerShape(16.0.dp), + extraLarge = RoundedCornerShape(28.0.dp) +) + +@Stable +val Shape20 = RoundedCornerShape(20.0.dp) + +@Stable +val Shape32 = RoundedCornerShape(32.0.dp) + +@Stable +val ShapeTop32 = RoundedCornerShape(32.0.dp, 32.0.dp, 0.0.dp, 0.0.dp) + +@Stable +val ShapeBottom32 = RoundedCornerShape(0.0.dp, 0.0.dp, 32.0.dp, 32.0.dp) diff --git a/app/src/main/java/me/ash/reader/ui/theme/Theme.kt b/app/src/main/java/me/ash/reader/ui/theme/Theme.kt index f09a07d..0696c1a 100644 --- a/app/src/main/java/me/ash/reader/ui/theme/Theme.kt +++ b/app/src/main/java/me/ash/reader/ui/theme/Theme.kt @@ -40,7 +40,8 @@ fun AppTheme( if (useDarkTheme) dynamicDarkColorScheme() else dynamicLightColorScheme(), typography = AppTypography, - content = content + shapes = Shapes, + content = content, ) } } diff --git a/app/src/main/res/drawable/ic_broken_image_black_24dp.xml b/app/src/main/res/drawable/ic_broken_image_black_24dp.xml index 6872d2e..aa7e72d 100644 --- a/app/src/main/res/drawable/ic_broken_image_black_24dp.xml +++ b/app/src/main/res/drawable/ic_broken_image_black_24dp.xml @@ -5,5 +5,6 @@ android:viewportHeight="24"> + android:fillColor="#000000" + android:fillAlpha="0.3"/> diff --git a/app/src/main/res/drawable/ic_hourglass_empty_black_24dp.xml b/app/src/main/res/drawable/ic_hourglass_empty_black_24dp.xml index ca58686..5f4bd70 100644 --- a/app/src/main/res/drawable/ic_hourglass_empty_black_24dp.xml +++ b/app/src/main/res/drawable/ic_hourglass_empty_black_24dp.xml @@ -5,5 +5,6 @@ android:viewportHeight="24"> + android:fillColor="#000000" + android:fillAlpha="0.3"/> diff --git a/build.gradle b/build.gradle index 02bfb71..3cea2b7 100644 --- a/build.gradle +++ b/build.gradle @@ -1,20 +1,37 @@ buildscript { ext { - compose = '1.2.0-alpha07' - accompanist = '0.24.6-alpha' - material3 = '1.0.0-alpha09' - lifecycle = '2.5.0-alpha06' - navigation = '2.5.0-alpha04' + // https://developer.android.com/jetpack/androidx/releases/compose-ui + compose = '1.2.0-beta02' + // https://github.com/google/accompanist/releases + accompanist = '0.24.7-alpha' + // https://developer.android.com/jetpack/androidx/releases/compose-material3 + material3 = '1.0.0-alpha12' + // https://developer.android.com/jetpack/androidx/releases/lifecycle + lifecycle = '2.5.0-rc01' + // https://developer.android.com/jetpack/androidx/releases/navigation + navigation = '2.5.0-rc01' + // https://developer.android.com/jetpack/androidx/releases/paging paging = '3.1.1' + // https://developer.android.com/jetpack/androidx/releases/room room = '2.5.0-alpha01' + // https://developer.android.com/jetpack/androidx/releases/datastore datastore = '1.0.0' + // https://developer.android.com/jetpack/androidx/releases/work work = '2.8.0-alpha02' - profileinstaller = '1.2.0-alpha02' + // https://developer.android.com/jetpack/androidx/releases/profileinstaller + profileinstaller = '1.2.0-beta02' + // https://square.github.io/okhttp/changelogs/changelog/ + okhttp = '5.0.0-alpha.7' retrofit2 = '2.9.0' - coil = '2.0.0-rc03' + // https://coil-kt.github.io/coil/changelog/ + coil = '2.1.0' + // https://mvnrepository.com/artifact/com.rometools/rome rome = '1.18.0' + // https://github.com/dankito/Readability4J readability4j = '1.0.8' + // https://github.com/mdewilde/opml-parser opmlParser = '2.2.0' + // http://bigbadaboom.github.io/androidsvg/release_notes.html androidSVG = '1.4' } @@ -27,7 +44,7 @@ buildscript { plugins { id 'com.android.application' version '7.1.1' apply false id 'com.android.library' version '7.1.1' apply false - id 'org.jetbrains.kotlin.android' version '1.6.10' apply false + id 'org.jetbrains.kotlin.android' version '1.6.21' apply false } task clean(type: Delete) {