From 7e0a2c29991a1ccd9ff7f627735940f8e3284137 Mon Sep 17 00:00:00 2001 From: SkyD666 Date: Wed, 5 Jun 2024 23:37:13 +0800 Subject: [PATCH] [feature|optimize|fix] Support configure the tonal elevation of some components of Feed (#55), Article, and Search screens; optimize the layout of Article screen and search screen (#53); fix the wrong article deduplication issue (#54) --- app/build.gradle.kts | 4 +- app/proguard-rules.pro | 6 +- .../com/skyd/anivu/ext/PaddingValuesExt.kt | 4 + .../java/com/skyd/anivu/ext/PreferenceExt.kt | 14 + .../com/skyd/anivu/model/bean/ArticleBean.kt | 3 + .../com/skyd/anivu/model/db/AppDatabase.kt | 5 +- .../com/skyd/anivu/model/db/dao/ArticleDao.kt | 44 ++- .../anivu/model/db/migration/Migration6To7.kt | 12 + .../skyd/anivu/model/preference/Settings.kt | 28 ++ .../ArticleItemTonalElevationPreference.kt | 26 ++ .../ArticleListTonalElevationPreference.kt | 26 ++ .../ArticleTopBarTonalElevationPreference.kt | 26 ++ .../feed/FeedListTonalElevationPreference.kt | 32 ++ .../FeedTopBarTonalElevationPreference.kt | 26 ++ .../SearchListTonalElevationPreference.kt | 26 ++ .../SearchTopBarTonalElevationPreference.kt | 26 ++ .../model/repository/ArticleRepository.kt | 3 +- .../skyd/anivu/model/repository/RssHelper.kt | 5 +- .../skyd/anivu/ui/component/AniVuTopBar.kt | 11 +- .../adapter/proxy/Article1Proxy.kt | 274 ++++++++++------ .../adapter/proxy/Feed1Proxy.kt | 33 +- .../adapter/proxy/Group1Proxy.kt | 6 +- .../ui/fragment/article/ArticleFragment.kt | 300 +++++++++--------- .../skyd/anivu/ui/fragment/article/Filter.kt | 256 +++++++++++++++ .../skyd/anivu/ui/fragment/feed/FeedScreen.kt | 26 +- .../anivu/ui/fragment/media/MediaFragment.kt | 2 +- .../ui/fragment/search/SearchFragment.kt | 54 +++- .../settings/appearance/AppearanceFragment.kt | 28 +- .../article/ArticleStyleFragment.kt | 163 ++++++++++ .../appearance/feed/FeedStyleFragment.kt | 124 +++++++- .../appearance/search/SearchStyleFragment.kt | 132 ++++++++ .../com/skyd/anivu/ui/local/LocalValue.kt | 20 ++ .../java/com/skyd/anivu/ui/theme/Theme.kt | 4 +- app/src/main/res/navigation/nav_graph.xml | 18 +- app/src/main/res/values-zh-rCN/strings.xml | 25 +- app/src/main/res/values-zh-rTW/strings.xml | 4 +- app/src/main/res/values/strings.xml | 25 +- 37 files changed, 1504 insertions(+), 317 deletions(-) create mode 100644 app/src/main/java/com/skyd/anivu/model/db/migration/Migration6To7.kt create mode 100644 app/src/main/java/com/skyd/anivu/model/preference/appearance/article/ArticleItemTonalElevationPreference.kt create mode 100644 app/src/main/java/com/skyd/anivu/model/preference/appearance/article/ArticleListTonalElevationPreference.kt create mode 100644 app/src/main/java/com/skyd/anivu/model/preference/appearance/article/ArticleTopBarTonalElevationPreference.kt create mode 100644 app/src/main/java/com/skyd/anivu/model/preference/appearance/feed/FeedListTonalElevationPreference.kt create mode 100644 app/src/main/java/com/skyd/anivu/model/preference/appearance/feed/FeedTopBarTonalElevationPreference.kt create mode 100644 app/src/main/java/com/skyd/anivu/model/preference/appearance/search/SearchListTonalElevationPreference.kt create mode 100644 app/src/main/java/com/skyd/anivu/model/preference/appearance/search/SearchTopBarTonalElevationPreference.kt create mode 100644 app/src/main/java/com/skyd/anivu/ui/fragment/article/Filter.kt create mode 100644 app/src/main/java/com/skyd/anivu/ui/fragment/settings/appearance/article/ArticleStyleFragment.kt create mode 100644 app/src/main/java/com/skyd/anivu/ui/fragment/settings/appearance/search/SearchStyleFragment.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 60abc726..a1d008d7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -20,8 +20,8 @@ android { applicationId = "com.skyd.anivu" minSdk = 24 targetSdk = 34 - versionCode = 17 - versionName = "1.1-beta45" + versionCode = 18 + versionName = "1.1-beta46" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 609fc700..2d6cc7cc 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -113,8 +113,4 @@ public static final ** CREATOR; -dontwarn com.arthenica.smartexception.java.Exceptions # MPV --keep,allowoptimization class is.xyz.mpv.MPVLib { public protected *; } - --dontwarn org.xmlpull.v1.** --keep class org.xmlpull.** { *; } --keepclassmembers class org.xmlpull.** { *; } \ No newline at end of file +-keep,allowoptimization class is.xyz.mpv.MPVLib { public protected *; } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ext/PaddingValuesExt.kt b/app/src/main/java/com/skyd/anivu/ext/PaddingValuesExt.kt index 409833c6..aaa5475d 100644 --- a/app/src/main/java/com/skyd/anivu/ext/PaddingValuesExt.kt +++ b/app/src/main/java/com/skyd/anivu/ext/PaddingValuesExt.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.unit.Dp @Composable operator fun PaddingValues.plus(other: PaddingValues): PaddingValues = PaddingValues( @@ -15,3 +16,6 @@ operator fun PaddingValues.plus(other: PaddingValues): PaddingValues = PaddingVa end = calculateEndPadding(LocalLayoutDirection.current) + other.calculateEndPadding(LocalLayoutDirection.current) ) + +@Composable +operator fun PaddingValues.plus(other: Dp): PaddingValues = this + PaddingValues(other) \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ext/PreferenceExt.kt b/app/src/main/java/com/skyd/anivu/ext/PreferenceExt.kt index 270a269b..4268d76e 100644 --- a/app/src/main/java/com/skyd/anivu/ext/PreferenceExt.kt +++ b/app/src/main/java/com/skyd/anivu/ext/PreferenceExt.kt @@ -8,7 +8,14 @@ import com.skyd.anivu.model.preference.appearance.DateStylePreference import com.skyd.anivu.model.preference.appearance.NavigationBarLabelPreference import com.skyd.anivu.model.preference.appearance.TextFieldStylePreference import com.skyd.anivu.model.preference.appearance.ThemePreference +import com.skyd.anivu.model.preference.appearance.article.ArticleItemTonalElevationPreference +import com.skyd.anivu.model.preference.appearance.article.ArticleListTonalElevationPreference +import com.skyd.anivu.model.preference.appearance.article.ArticleTopBarTonalElevationPreference import com.skyd.anivu.model.preference.appearance.feed.FeedGroupExpandPreference +import com.skyd.anivu.model.preference.appearance.feed.FeedListTonalElevationPreference +import com.skyd.anivu.model.preference.appearance.feed.FeedTopBarTonalElevationPreference +import com.skyd.anivu.model.preference.appearance.search.SearchListTonalElevationPreference +import com.skyd.anivu.model.preference.appearance.search.SearchTopBarTonalElevationPreference import com.skyd.anivu.model.preference.behavior.PickImageMethodPreference import com.skyd.anivu.model.preference.behavior.article.ArticleSwipeLeftActionPreference import com.skyd.anivu.model.preference.behavior.article.ArticleTapActionPreference @@ -32,6 +39,13 @@ fun Preferences.toSettings(): Settings { textFieldStyle = TextFieldStylePreference.fromPreferences(this), dateStyle = DateStylePreference.fromPreferences(this), navigationBarLabel = NavigationBarLabelPreference.fromPreferences(this), + feedListTonalElevation = FeedListTonalElevationPreference.fromPreferences(this), + feedTopBarTonalElevation = FeedTopBarTonalElevationPreference.fromPreferences(this), + articleListTonalElevation = ArticleListTonalElevationPreference.fromPreferences(this), + articleTopBarTonalElevation = ArticleTopBarTonalElevationPreference.fromPreferences(this), + articleItemTonalElevation = ArticleItemTonalElevationPreference.fromPreferences(this), + searchListTonalElevation = SearchListTonalElevationPreference.fromPreferences(this), + searchTopBarTonalElevation = SearchTopBarTonalElevationPreference.fromPreferences(this), // Update ignoreUpdateVersion = IgnoreUpdateVersionPreference.fromPreferences(this), diff --git a/app/src/main/java/com/skyd/anivu/model/bean/ArticleBean.kt b/app/src/main/java/com/skyd/anivu/model/bean/ArticleBean.kt index d7d5a309..11726009 100644 --- a/app/src/main/java/com/skyd/anivu/model/bean/ArticleBean.kt +++ b/app/src/main/java/com/skyd/anivu/model/bean/ArticleBean.kt @@ -49,6 +49,8 @@ data class ArticleBean( val image: String? = null, @ColumnInfo(name = LINK_COLUMN) var link: String? = null, + @ColumnInfo(name = GUID_COLUMN) + var guid: String? = null, @ColumnInfo(name = UPDATE_AT_COLUMN) var updateAt: Long? = null, @ColumnInfo(name = IS_READ_COLUMN) @@ -66,6 +68,7 @@ data class ArticleBean( const val CONTENT_COLUMN = "content" const val IMAGE_COLUMN = "image" const val LINK_COLUMN = "link" + const val GUID_COLUMN = "guid" const val UPDATE_AT_COLUMN = "updateAt" const val IS_READ_COLUMN = "isRead" const val IS_FAVORITE_COLUMN = "isFavorite" diff --git a/app/src/main/java/com/skyd/anivu/model/db/AppDatabase.kt b/app/src/main/java/com/skyd/anivu/model/db/AppDatabase.kt index 19de3828..344b7a08 100644 --- a/app/src/main/java/com/skyd/anivu/model/db/AppDatabase.kt +++ b/app/src/main/java/com/skyd/anivu/model/db/AppDatabase.kt @@ -26,6 +26,7 @@ import com.skyd.anivu.model.db.migration.Migration2To3 import com.skyd.anivu.model.db.migration.Migration3To4 import com.skyd.anivu.model.db.migration.Migration4To5 import com.skyd.anivu.model.db.migration.Migration5To6 +import com.skyd.anivu.model.db.migration.Migration6To7 const val APP_DATA_BASE_FILE_NAME = "app.db" @@ -41,7 +42,7 @@ const val APP_DATA_BASE_FILE_NAME = "app.db" GroupBean::class, ], views = [FeedViewBean::class], - version = 6, + version = 7, ) @TypeConverters( value = [] @@ -61,7 +62,7 @@ abstract class AppDatabase : RoomDatabase() { private val migrations = arrayOf( Migration1To2(), Migration2To3(), Migration3To4(), Migration4To5(), - Migration5To6() + Migration5To6(), Migration6To7() ) fun getInstance(context: Context): AppDatabase { diff --git a/app/src/main/java/com/skyd/anivu/model/db/dao/ArticleDao.kt b/app/src/main/java/com/skyd/anivu/model/db/dao/ArticleDao.kt index 65f7336e..13f5d870 100644 --- a/app/src/main/java/com/skyd/anivu/model/db/dao/ArticleDao.kt +++ b/app/src/main/java/com/skyd/anivu/model/db/dao/ArticleDao.kt @@ -31,11 +31,25 @@ interface ArticleDao { val enclosureDao: EnclosureDao } + // null always compares false in '=' @Query( """ SELECT * from $ARTICLE_TABLE_NAME - WHERE ${ArticleBean.LINK_COLUMN} = :link - AND ${ArticleBean.FEED_URL_COLUMN} = :feedUrl + WHERE ${ArticleBean.GUID_COLUMN} = :guid AND + ${ArticleBean.FEED_URL_COLUMN} = :feedUrl + """ + ) + suspend fun queryArticleByGuid( + guid: String?, + feedUrl: String, + ): ArticleBean? + + // null always compares false in '=' + @Query( + """ + SELECT * from $ARTICLE_TABLE_NAME + WHERE ${ArticleBean.LINK_COLUMN} = :link AND + ${ArticleBean.FEED_URL_COLUMN} = :feedUrl """ ) suspend fun queryArticleByLink( @@ -56,16 +70,26 @@ interface ArticleDao { val hiltEntryPoint = EntryPointAccessors.fromApplication(appContext, ArticleDaoEntryPoint::class.java) articleWithEnclosureList.forEach { - // 可能会出现link、feedUrl都一样,但是uuid不一样的情况 - var newArticle = queryArticleByLink( - link = it.article.link, - feedUrl = it.article.feedUrl, - ) + // Duplicate article by guid or link + val guid = it.article.guid + val link = it.article.link + var newArticle: ArticleBean? = null + if (guid != null) { + newArticle = queryArticleByGuid( + guid = guid, + feedUrl = it.article.feedUrl, + ) + } else if (link != null) { + newArticle = queryArticleByLink( + link = link, + feedUrl = it.article.feedUrl, + ) + } if (newArticle == null) { innerUpdateArticle(it.article) newArticle = it.article } else { - // 除了uuid,其他的字段都更新 + // Update all fields except articleId newArticle = it.article.copy(articleId = newArticle.articleId) innerUpdateArticle(newArticle) } @@ -143,7 +167,7 @@ interface ArticleDao { @Query( """ UPDATE $ARTICLE_TABLE_NAME SET ${ArticleBean.IS_FAVORITE_COLUMN} = :favorite - WHERE ${ArticleBean.ARTICLE_ID_COLUMN} LIKE :articleId + WHERE ${ArticleBean.ARTICLE_ID_COLUMN} = :articleId """ ) fun favoriteArticle(articleId: String, favorite: Boolean) @@ -152,7 +176,7 @@ interface ArticleDao { @Query( """ UPDATE $ARTICLE_TABLE_NAME SET ${ArticleBean.IS_READ_COLUMN} = :read - WHERE ${ArticleBean.ARTICLE_ID_COLUMN} LIKE :articleId + WHERE ${ArticleBean.ARTICLE_ID_COLUMN} = :articleId """ ) fun readArticle(articleId: String, read: Boolean) diff --git a/app/src/main/java/com/skyd/anivu/model/db/migration/Migration6To7.kt b/app/src/main/java/com/skyd/anivu/model/db/migration/Migration6To7.kt new file mode 100644 index 00000000..54f4a588 --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/model/db/migration/Migration6To7.kt @@ -0,0 +1,12 @@ +package com.skyd.anivu.model.db.migration + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase +import com.skyd.anivu.model.bean.ARTICLE_TABLE_NAME +import com.skyd.anivu.model.bean.ArticleBean + +class Migration6To7 : Migration(6, 7) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE $ARTICLE_TABLE_NAME ADD ${ArticleBean.GUID_COLUMN} TEXT") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/model/preference/Settings.kt b/app/src/main/java/com/skyd/anivu/model/preference/Settings.kt index 779c4b12..4bdd30e6 100644 --- a/app/src/main/java/com/skyd/anivu/model/preference/Settings.kt +++ b/app/src/main/java/com/skyd/anivu/model/preference/Settings.kt @@ -13,7 +13,14 @@ import com.skyd.anivu.model.preference.appearance.DateStylePreference import com.skyd.anivu.model.preference.appearance.NavigationBarLabelPreference import com.skyd.anivu.model.preference.appearance.TextFieldStylePreference import com.skyd.anivu.model.preference.appearance.ThemePreference +import com.skyd.anivu.model.preference.appearance.article.ArticleItemTonalElevationPreference +import com.skyd.anivu.model.preference.appearance.article.ArticleListTonalElevationPreference +import com.skyd.anivu.model.preference.appearance.article.ArticleTopBarTonalElevationPreference import com.skyd.anivu.model.preference.appearance.feed.FeedGroupExpandPreference +import com.skyd.anivu.model.preference.appearance.feed.FeedListTonalElevationPreference +import com.skyd.anivu.model.preference.appearance.feed.FeedTopBarTonalElevationPreference +import com.skyd.anivu.model.preference.appearance.search.SearchListTonalElevationPreference +import com.skyd.anivu.model.preference.appearance.search.SearchTopBarTonalElevationPreference import com.skyd.anivu.model.preference.behavior.PickImageMethodPreference import com.skyd.anivu.model.preference.behavior.article.ArticleSwipeLeftActionPreference import com.skyd.anivu.model.preference.behavior.article.ArticleTapActionPreference @@ -27,14 +34,19 @@ import com.skyd.anivu.model.preference.player.HardwareDecodePreference import com.skyd.anivu.model.preference.player.PlayerDoubleTapPreference import com.skyd.anivu.model.preference.player.PlayerShow85sButtonPreference import com.skyd.anivu.model.preference.player.PlayerShowScreenshotButtonPreference +import com.skyd.anivu.ui.local.LocalArticleItemTonalElevation +import com.skyd.anivu.ui.local.LocalArticleListTonalElevation import com.skyd.anivu.ui.local.LocalArticleSwipeLeftAction import com.skyd.anivu.ui.local.LocalArticleTapAction +import com.skyd.anivu.ui.local.LocalArticleTopBarTonalElevation import com.skyd.anivu.ui.local.LocalAutoDeleteArticleBefore import com.skyd.anivu.ui.local.LocalAutoDeleteArticleFrequency import com.skyd.anivu.ui.local.LocalDarkMode import com.skyd.anivu.ui.local.LocalDateStyle import com.skyd.anivu.ui.local.LocalDeduplicateTitleInDesc import com.skyd.anivu.ui.local.LocalFeedGroupExpand +import com.skyd.anivu.ui.local.LocalFeedListTonalElevation +import com.skyd.anivu.ui.local.LocalFeedTopBarTonalElevation import com.skyd.anivu.ui.local.LocalHardwareDecode import com.skyd.anivu.ui.local.LocalHideEmptyDefault import com.skyd.anivu.ui.local.LocalIgnoreUpdateVersion @@ -44,6 +56,8 @@ import com.skyd.anivu.ui.local.LocalPickImageMethod import com.skyd.anivu.ui.local.LocalPlayerDoubleTap import com.skyd.anivu.ui.local.LocalPlayerShow85sButton import com.skyd.anivu.ui.local.LocalPlayerShowScreenshotButton +import com.skyd.anivu.ui.local.LocalSearchListTonalElevation +import com.skyd.anivu.ui.local.LocalSearchTopBarTonalElevation import com.skyd.anivu.ui.local.LocalTextFieldStyle import com.skyd.anivu.ui.local.LocalTheme import com.skyd.anivu.ui.local.LocalUseAutoDelete @@ -58,6 +72,13 @@ data class Settings( val textFieldStyle: String = TextFieldStylePreference.default, val dateStyle: String = DateStylePreference.default, val navigationBarLabel: String = NavigationBarLabelPreference.default, + val feedListTonalElevation: Float = FeedListTonalElevationPreference.default, + val feedTopBarTonalElevation: Float = FeedTopBarTonalElevationPreference.default, + val articleListTonalElevation: Float = ArticleListTonalElevationPreference.default, + val articleTopBarTonalElevation: Float = ArticleTopBarTonalElevationPreference.default, + val articleItemTonalElevation: Float = ArticleItemTonalElevationPreference.default, + val searchListTonalElevation: Float = SearchListTonalElevationPreference.default, + val searchTopBarTonalElevation: Float = SearchTopBarTonalElevationPreference.default, // Update val ignoreUpdateVersion: Long = IgnoreUpdateVersionPreference.default, // Behavior @@ -94,6 +115,13 @@ fun SettingsProvider( LocalTextFieldStyle provides settings.textFieldStyle, LocalDateStyle provides settings.dateStyle, LocalNavigationBarLabel provides settings.navigationBarLabel, + LocalFeedListTonalElevation provides settings.feedListTonalElevation, + LocalFeedTopBarTonalElevation provides settings.feedTopBarTonalElevation, + LocalArticleListTonalElevation provides settings.articleListTonalElevation, + LocalArticleTopBarTonalElevation provides settings.articleTopBarTonalElevation, + LocalArticleItemTonalElevation provides settings.articleItemTonalElevation, + LocalSearchListTonalElevation provides settings.searchListTonalElevation, + LocalSearchTopBarTonalElevation provides settings.searchTopBarTonalElevation, // Update LocalIgnoreUpdateVersion provides settings.ignoreUpdateVersion, // Behavior diff --git a/app/src/main/java/com/skyd/anivu/model/preference/appearance/article/ArticleItemTonalElevationPreference.kt b/app/src/main/java/com/skyd/anivu/model/preference/appearance/article/ArticleItemTonalElevationPreference.kt new file mode 100644 index 00000000..3e4bced2 --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/model/preference/appearance/article/ArticleItemTonalElevationPreference.kt @@ -0,0 +1,26 @@ +package com.skyd.anivu.model.preference.appearance.article + +import android.content.Context +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.floatPreferencesKey +import com.skyd.anivu.base.BasePreference +import com.skyd.anivu.ext.dataStore +import com.skyd.anivu.ext.put +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +object ArticleItemTonalElevationPreference : BasePreference { + private const val ARTICLE_ITEM_TONAL_ELEVATION = "articleItemTonalElevation" + override val default = -2f + + val key = floatPreferencesKey(ARTICLE_ITEM_TONAL_ELEVATION) + + fun put(context: Context, scope: CoroutineScope, value: Float) { + scope.launch(Dispatchers.IO) { + context.dataStore.put(key, value) + } + } + + override fun fromPreferences(preferences: Preferences): Float = preferences[key] ?: default +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/model/preference/appearance/article/ArticleListTonalElevationPreference.kt b/app/src/main/java/com/skyd/anivu/model/preference/appearance/article/ArticleListTonalElevationPreference.kt new file mode 100644 index 00000000..3dae50de --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/model/preference/appearance/article/ArticleListTonalElevationPreference.kt @@ -0,0 +1,26 @@ +package com.skyd.anivu.model.preference.appearance.article + +import android.content.Context +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.floatPreferencesKey +import com.skyd.anivu.base.BasePreference +import com.skyd.anivu.ext.dataStore +import com.skyd.anivu.ext.put +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +object ArticleListTonalElevationPreference : BasePreference { + private const val ARTICLE_LIST_TONAL_ELEVATION = "articleListTonalElevation" + override val default = 2f + + val key = floatPreferencesKey(ARTICLE_LIST_TONAL_ELEVATION) + + fun put(context: Context, scope: CoroutineScope, value: Float) { + scope.launch(Dispatchers.IO) { + context.dataStore.put(key, value) + } + } + + override fun fromPreferences(preferences: Preferences): Float = preferences[key] ?: default +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/model/preference/appearance/article/ArticleTopBarTonalElevationPreference.kt b/app/src/main/java/com/skyd/anivu/model/preference/appearance/article/ArticleTopBarTonalElevationPreference.kt new file mode 100644 index 00000000..9c22f13e --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/model/preference/appearance/article/ArticleTopBarTonalElevationPreference.kt @@ -0,0 +1,26 @@ +package com.skyd.anivu.model.preference.appearance.article + +import android.content.Context +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.floatPreferencesKey +import com.skyd.anivu.base.BasePreference +import com.skyd.anivu.ext.dataStore +import com.skyd.anivu.ext.put +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +object ArticleTopBarTonalElevationPreference : BasePreference { + private const val ARTICLE_TOP_BAR_TONAL_ELEVATION = "articleTopBarTonalElevation" + override val default = 2f + + val key = floatPreferencesKey(ARTICLE_TOP_BAR_TONAL_ELEVATION) + + fun put(context: Context, scope: CoroutineScope, value: Float) { + scope.launch(Dispatchers.IO) { + context.dataStore.put(key, value) + } + } + + override fun fromPreferences(preferences: Preferences): Float = preferences[key] ?: default +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/model/preference/appearance/feed/FeedListTonalElevationPreference.kt b/app/src/main/java/com/skyd/anivu/model/preference/appearance/feed/FeedListTonalElevationPreference.kt new file mode 100644 index 00000000..1d2a4f3f --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/model/preference/appearance/feed/FeedListTonalElevationPreference.kt @@ -0,0 +1,32 @@ +package com.skyd.anivu.model.preference.appearance.feed + +import android.content.Context +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.floatPreferencesKey +import com.skyd.anivu.base.BasePreference +import com.skyd.anivu.ext.dataStore +import com.skyd.anivu.ext.put +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +object FeedListTonalElevationPreference : BasePreference { + private const val FEED_LIST_TONAL_ELEVATION = "feedListTonalElevation" + override val default = 0f + + val key = floatPreferencesKey(FEED_LIST_TONAL_ELEVATION) + + fun put(context: Context, scope: CoroutineScope, value: Float) { + scope.launch(Dispatchers.IO) { + context.dataStore.put(key, value) + } + } + + override fun fromPreferences(preferences: Preferences): Float = preferences[key] ?: default +} + +object TonalElevationPreferenceUtil { + fun toDisplay(value: Float): String { + return "%.2fdp".format(value) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/model/preference/appearance/feed/FeedTopBarTonalElevationPreference.kt b/app/src/main/java/com/skyd/anivu/model/preference/appearance/feed/FeedTopBarTonalElevationPreference.kt new file mode 100644 index 00000000..0069ce66 --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/model/preference/appearance/feed/FeedTopBarTonalElevationPreference.kt @@ -0,0 +1,26 @@ +package com.skyd.anivu.model.preference.appearance.feed + +import android.content.Context +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.floatPreferencesKey +import com.skyd.anivu.base.BasePreference +import com.skyd.anivu.ext.dataStore +import com.skyd.anivu.ext.put +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +object FeedTopBarTonalElevationPreference : BasePreference { + private const val FEED_TOP_BAR_TONAL_ELEVATION = "feedTopBarTonalElevation" + override val default = 0f + + val key = floatPreferencesKey(FEED_TOP_BAR_TONAL_ELEVATION) + + fun put(context: Context, scope: CoroutineScope, value: Float) { + scope.launch(Dispatchers.IO) { + context.dataStore.put(key, value) + } + } + + override fun fromPreferences(preferences: Preferences): Float = preferences[key] ?: default +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/model/preference/appearance/search/SearchListTonalElevationPreference.kt b/app/src/main/java/com/skyd/anivu/model/preference/appearance/search/SearchListTonalElevationPreference.kt new file mode 100644 index 00000000..f6a35575 --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/model/preference/appearance/search/SearchListTonalElevationPreference.kt @@ -0,0 +1,26 @@ +package com.skyd.anivu.model.preference.appearance.search + +import android.content.Context +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.floatPreferencesKey +import com.skyd.anivu.base.BasePreference +import com.skyd.anivu.ext.dataStore +import com.skyd.anivu.ext.put +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +object SearchListTonalElevationPreference : BasePreference { + private const val SEARCH_LIST_TONAL_ELEVATION = "searchListTonalElevation" + override val default = 2f + + val key = floatPreferencesKey(SEARCH_LIST_TONAL_ELEVATION) + + fun put(context: Context, scope: CoroutineScope, value: Float) { + scope.launch(Dispatchers.IO) { + context.dataStore.put(key, value) + } + } + + override fun fromPreferences(preferences: Preferences): Float = preferences[key] ?: default +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/model/preference/appearance/search/SearchTopBarTonalElevationPreference.kt b/app/src/main/java/com/skyd/anivu/model/preference/appearance/search/SearchTopBarTonalElevationPreference.kt new file mode 100644 index 00000000..57748065 --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/model/preference/appearance/search/SearchTopBarTonalElevationPreference.kt @@ -0,0 +1,26 @@ +package com.skyd.anivu.model.preference.appearance.search + +import android.content.Context +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.floatPreferencesKey +import com.skyd.anivu.base.BasePreference +import com.skyd.anivu.ext.dataStore +import com.skyd.anivu.ext.put +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +object SearchTopBarTonalElevationPreference : BasePreference { + private const val SEARCH_TOP_BAR_TONAL_ELEVATION = "searchTopBarTonalElevation" + override val default = 2f + + val key = floatPreferencesKey(SEARCH_TOP_BAR_TONAL_ELEVATION) + + fun put(context: Context, scope: CoroutineScope, value: Float) { + scope.launch(Dispatchers.IO) { + context.dataStore.put(key, value) + } + } + + override fun fromPreferences(preferences: Preferences): Float = preferences[key] ?: default +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/model/repository/ArticleRepository.kt b/app/src/main/java/com/skyd/anivu/model/repository/ArticleRepository.kt index 5bd3ba20..293b87f6 100644 --- a/app/src/main/java/com/skyd/anivu/model/repository/ArticleRepository.kt +++ b/app/src/main/java/com/skyd/anivu/model/repository/ArticleRepository.kt @@ -16,6 +16,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapConcat +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn import javax.inject.Inject @@ -40,7 +41,7 @@ class ArticleRepository @Inject constructor( fun requestArticleList(feedUrls: List): Flow> { return combine(filterFavorite, filterRead) { favorite, read -> favorite to read - }.flatMapConcat { (favorite, read) -> + }.flatMapLatest { (favorite, read) -> Pager(pagingConfig) { articleDao.getArticlePagingSource( feedUrls = feedUrls, diff --git a/app/src/main/java/com/skyd/anivu/model/repository/RssHelper.kt b/app/src/main/java/com/skyd/anivu/model/repository/RssHelper.kt index 6e5f8da2..00946134 100644 --- a/app/src/main/java/com/skyd/anivu/model/repository/RssHelper.kt +++ b/app/src/main/java/com/skyd/anivu/model/repository/RssHelper.kt @@ -107,7 +107,8 @@ class RssHelper @Inject constructor( description = content ?: desc, content = content, image = findImg((content ?: desc) ?: ""), - link = syndEntry.link ?: "", + link = syndEntry.link, + guid = syndEntry.uri, updateAt = Date().time, ), enclosures = syndEntry.enclosures.map { @@ -123,7 +124,7 @@ class RssHelper @Inject constructor( fun getRssIcon(url: String): String? { return runCatching { - faviconExtractor.extractFavicon(url).apply { Log.e("TAG", "getRssIcon: $this", ) } + faviconExtractor.extractFavicon(url).apply { Log.e("TAG", "getRssIcon: $this") } }.onFailure { it.printStackTrace() }.getOrNull() } diff --git a/app/src/main/java/com/skyd/anivu/ui/component/AniVuTopBar.kt b/app/src/main/java/com/skyd/anivu/ui/component/AniVuTopBar.kt index 8b179dda..ac7a87d6 100644 --- a/app/src/main/java/com/skyd/anivu/ui/component/AniVuTopBar.kt +++ b/app/src/main/java/com/skyd/anivu/ui/component/AniVuTopBar.kt @@ -10,6 +10,7 @@ import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.LargeTopAppBar import androidx.compose.material3.LocalContentColor import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarColors import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable @@ -33,14 +34,14 @@ fun AniVuTopBar( contentPadding: @Composable () -> PaddingValues = { PaddingValues() }, navigationIcon: @Composable () -> Unit = { BackIcon() }, windowInsets: WindowInsets = TopAppBarDefaults.windowInsets, - actions: @Composable RowScope.() -> Unit = {}, - scrollBehavior: TopAppBarScrollBehavior? = null, -) { - val colors = when (style) { + colors: TopAppBarColors = when (style) { AniVuTopBarStyle.Small -> TopAppBarDefaults.topAppBarColors() AniVuTopBarStyle.Large -> TopAppBarDefaults.largeTopAppBarColors() AniVuTopBarStyle.CenterAligned -> TopAppBarDefaults.centerAlignedTopAppBarColors() - } + }, + actions: @Composable RowScope.() -> Unit = {}, + scrollBehavior: TopAppBarScrollBehavior? = null, +) { val topBarModifier = Modifier.padding(contentPadding()) when (style) { AniVuTopBarStyle.Small -> { diff --git a/app/src/main/java/com/skyd/anivu/ui/component/lazyverticalgrid/adapter/proxy/Article1Proxy.kt b/app/src/main/java/com/skyd/anivu/ui/component/lazyverticalgrid/adapter/proxy/Article1Proxy.kt index daf3d3ef..099e1ca0 100644 --- a/app/src/main/java/com/skyd/anivu/ui/component/lazyverticalgrid/adapter/proxy/Article1Proxy.kt +++ b/app/src/main/java/com/skyd/anivu/ui/component/lazyverticalgrid/adapter/proxy/Article1Proxy.kt @@ -4,6 +4,7 @@ import android.content.Context import android.os.Bundle import androidx.compose.animation.Animatable import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -14,11 +15,14 @@ import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Drafts import androidx.compose.material.icons.outlined.Favorite import androidx.compose.material.icons.outlined.FavoriteBorder import androidx.compose.material.icons.outlined.ImportContacts @@ -28,6 +32,7 @@ import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon +import androidx.compose.material3.LocalAbsoluteTonalElevation import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedCard @@ -35,6 +40,7 @@ import androidx.compose.material3.SwipeToDismissBox import androidx.compose.material3.SwipeToDismissBoxValue import androidx.compose.material3.Text import androidx.compose.material3.rememberSwipeToDismissBoxState +import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect @@ -75,6 +81,7 @@ import com.skyd.anivu.ui.component.lazyverticalgrid.adapter.LazyGridAdapter import com.skyd.anivu.ui.component.rememberAniVuImageLoader import com.skyd.anivu.ui.fragment.read.EnclosureBottomSheet import com.skyd.anivu.ui.fragment.read.ReadFragment +import com.skyd.anivu.ui.local.LocalArticleItemTonalElevation import com.skyd.anivu.ui.local.LocalArticleSwipeLeftAction import com.skyd.anivu.ui.local.LocalArticleTapAction import com.skyd.anivu.ui.local.LocalDeduplicateTitleInDesc @@ -98,10 +105,8 @@ fun Article1Item( ) { val navController = LocalNavController.current val context = LocalContext.current - val articleTapAction = LocalArticleTapAction.current val articleSwipeLeftAction = LocalArticleSwipeLeftAction.current val articleWithEnclosure = data.articleWithEnclosure - val article = articleWithEnclosure.article var expandMenu by rememberSaveable { mutableStateOf(false) } val swipeToDismissBoxState = rememberSwipeToDismissBoxState( @@ -127,25 +132,65 @@ fun Article1Item( swipeToDismissBoxState.targetValue != SwipeToDismissBoxValue.Settled } - SwipeToDismissBox( - state = swipeToDismissBoxState, - backgroundContent = { - SwipeBackgroundContent( - direction = swipeToDismissBoxState.dismissDirection, - isActive = isSwipeToDismissActive, - articleSwipeLeftAction = articleSwipeLeftAction, - context = context, + Box(modifier = Modifier.clip(RoundedCornerShape(12.dp))) { + SwipeToDismissBox( + state = swipeToDismissBoxState, + backgroundContent = { + SwipeBackgroundContent( + direction = swipeToDismissBoxState.dismissDirection, + isActive = isSwipeToDismissActive, + articleSwipeLeftAction = articleSwipeLeftAction, + context = context, + ) + }, + enableDismissFromStartToEnd = false, + ) { + Article1ItemContent( + data = data, + onLongClick = { expandMenu = true }, + onFavorite = onFavorite, + onRead = onRead, ) - }, - enableDismissFromStartToEnd = false, + ArticleMenu( + expanded = expandMenu, + onDismissRequest = { expandMenu = false }, + data = data, + onFavorite = onFavorite, + onRead = onRead, + ) + } + } +} + +@Composable +private fun Article1ItemContent( + data: ArticleWithFeed, + onLongClick: () -> Unit, + onFavorite: (ArticleWithFeed, Boolean) -> Unit, + onRead: (ArticleWithFeed, Boolean) -> Unit, +) { + val context = LocalContext.current + val navController = LocalNavController.current + val articleTapAction = LocalArticleTapAction.current + val articleWithEnclosure = data.articleWithEnclosure + val article = articleWithEnclosure.article + val colorAlpha = if (data.articleWithEnclosure.article.isRead) 0.5f else 1f + + CompositionLocalProvider( + LocalContentColor provides LocalContentColor.current.copy(alpha = colorAlpha) ) { Column( modifier = Modifier - .background(MaterialTheme.colorScheme.background) + .background( + MaterialTheme.colorScheme.surfaceColorAtElevation( + LocalAbsoluteTonalElevation.current + + LocalArticleItemTonalElevation.current.dp + ) + ) .fillMaxWidth() .run { if (article.image.isNullOrBlank()) this else height(IntrinsicSize.Max) } .combinedClickable( - onLongClick = { expandMenu = true }, + onLongClick = onLongClick, onClick = { tapAction( articleTapAction, @@ -154,53 +199,26 @@ fun Article1Item( articleWithEnclosure, ) }, - ) - .padding(horizontal = 16.dp, vertical = 12.dp), + ), ) { - val colorAlpha = if (data.articleWithEnclosure.article.isRead) 0.5f else 1f - CompositionLocalProvider( - LocalContentColor provides LocalContentColor.current.copy(alpha = colorAlpha) - ) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, + val title = article.title?.readable().orEmpty() + + Row(modifier = Modifier.height(IntrinsicSize.Max)) { + Column( + modifier = Modifier + .weight(1f) + .padding(top = 12.dp) + .padding(horizontal = 15.dp), ) { - FeedIcon(data = data.feed) - val feedName = - data.feed.nickname.orEmpty().ifBlank { data.feed.title.orEmpty() } - if (feedName.isNotBlank()) { - Text( - modifier = Modifier - .padding(horizontal = 10.dp) - .weight(1f), - text = feedName, - style = MaterialTheme.typography.labelLarge, - color = MaterialTheme.colorScheme.tertiary.copy(alpha = colorAlpha), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } - val date = article.date?.toDateTimeString(context = context) - if (!date.isNullOrBlank()) { - Text( - text = date, - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.outline.copy(alpha = colorAlpha), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } - } - Spacer(modifier = Modifier.height(3.dp)) - Row { - Column(modifier = Modifier.weight(1f)) { - val title = article.title?.readable().orEmpty() - Text( - text = title, - style = MaterialTheme.typography.titleSmall, - maxLines = 3, - overflow = TextOverflow.Ellipsis, - ) + Text( + text = title, + style = MaterialTheme.typography.titleSmall, + maxLines = 3, + overflow = TextOverflow.Ellipsis, + ) + Spacer(modifier = Modifier.height(2.dp)) + + Row { val author = article.author if (!author.isNullOrBlank()) { Text( @@ -210,47 +228,123 @@ fun Article1Item( overflow = TextOverflow.Ellipsis, ) } - val description = article.description?.readable()?.let { desc -> - if (LocalDeduplicateTitleInDesc.current) desc.replace( - title, - "" - ) else desc - } - if (!description.isNullOrBlank()) { - Spacer(modifier = Modifier.height(3.dp)) + val date = article.date?.toDateTimeString(context = context) + if (!date.isNullOrBlank()) { + if (!author.isNullOrBlank()) { + Text( + modifier = Modifier.padding(horizontal = 3.dp), + text = "·", + style = MaterialTheme.typography.labelMedium, + ) + } Text( - text = description, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), - maxLines = 3, + text = date, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.outline.copy(alpha = colorAlpha), + maxLines = 1, overflow = TextOverflow.Ellipsis, ) } } - if (!article.image.isNullOrBlank()) { - Spacer(modifier = Modifier.width(15.dp)) - OutlinedCard( + Spacer(modifier = Modifier.height(3.dp)) + + val description = article.description?.readable()?.let { desc -> + if (LocalDeduplicateTitleInDesc.current) desc.replace(title, "") else desc + }?.trim() + if (!description.isNullOrBlank()) { + Text( + text = description, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), + maxLines = 3, + overflow = TextOverflow.Ellipsis, + ) + } + } + + if (!article.image.isNullOrBlank()) { + OutlinedCard( + modifier = Modifier + .padding(top = 12.dp, end = 12.dp) + .align(Alignment.CenterVertically), + ) { + AniVuImage( modifier = Modifier - .width(90.dp) - .fillMaxHeight(), - ) { - AniVuImage( - modifier = Modifier.fillMaxSize(), - model = article.image, - contentScale = ContentScale.Crop, - ) - } + .width(100.dp) + .fillMaxHeight() + .heightIn(min = 70.dp, max = 120.dp), + model = article.image, + contentScale = ContentScale.Crop, + ) } } } - ArticleMenu( - expanded = expandMenu, - onDismissRequest = { expandMenu = false }, - data = data, - onFavorite = onFavorite, - onRead = onRead, - ) + // Bottom row + Row( + modifier = Modifier.padding(start = 15.dp, end = 9.dp, top = 3.dp, bottom = 6.dp), + verticalAlignment = Alignment.CenterVertically + ) { + FeedIcon( + data = data.feed, + size = 22.dp + ) + + val feedName = + data.feed.nickname.orEmpty().ifBlank { data.feed.title.orEmpty() } + if (feedName.isNotBlank()) { + Text( + modifier = Modifier + .padding(horizontal = 6.dp) + .weight(1f), + text = feedName, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.tertiary.copy(alpha = colorAlpha), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } else { + Spacer(modifier = Modifier.weight(1f)) + } + + val isFavorite = articleWithEnclosure.article.isFavorite + val isRead = articleWithEnclosure.article.isRead + Icon( + modifier = Modifier + .size(36.dp) + .clip(CircleShape) + .clickable { onFavorite(data, !isFavorite) } + .padding(6.dp), + imageVector = if (isFavorite) { + Icons.Outlined.Favorite + } else { + Icons.Outlined.FavoriteBorder + }, + contentDescription = if (isFavorite) { + stringResource(id = R.string.article_screen_favorite) + } else { + stringResource(id = R.string.article_screen_unfavorite) + }, + ) + Spacer(modifier = Modifier.width(3.dp)) + Icon( + modifier = Modifier + .size(36.dp) + .clip(CircleShape) + .clickable { onRead(data, !isRead) } + .padding(6.dp), + imageVector = if (isRead) { + Icons.Outlined.Drafts + } else { + Icons.Outlined.MarkEmailUnread + }, + contentDescription = if (isRead) { + stringResource(id = R.string.article_screen_mark_as_unread) + } else { + stringResource(id = R.string.article_screen_mark_as_read) + }, + ) + } } } } diff --git a/app/src/main/java/com/skyd/anivu/ui/component/lazyverticalgrid/adapter/proxy/Feed1Proxy.kt b/app/src/main/java/com/skyd/anivu/ui/component/lazyverticalgrid/adapter/proxy/Feed1Proxy.kt index afa651a2..e47180bf 100644 --- a/app/src/main/java/com/skyd/anivu/ui/component/lazyverticalgrid/adapter/proxy/Feed1Proxy.kt +++ b/app/src/main/java/com/skyd/anivu/ui/component/lazyverticalgrid/adapter/proxy/Feed1Proxy.kt @@ -38,7 +38,7 @@ class Feed1Proxy( private val visible: (groupId: String) -> Boolean = { true }, private val selected: (FeedBean) -> Boolean = { false }, private val isEnded: (index: Int) -> Boolean = { false }, - private val useCardLayout: () -> Boolean = { false }, + private val inGroup: () -> Boolean = { false }, private val onClick: ((FeedBean) -> Unit)? = null, private val onEdit: ((FeedBean) -> Unit)? = null, ) : LazyGridAdapter.Proxy() { @@ -50,7 +50,7 @@ class Feed1Proxy( visible = visible, selected = selected, isEnded = isEnded, - useCardLayout = useCardLayout, + inGroup = inGroup, onClick = onClick, onEdit = onEdit, ) @@ -63,7 +63,7 @@ fun Feed1Item( data: FeedViewBean, visible: (groupId: String) -> Boolean, selected: (FeedBean) -> Boolean, - useCardLayout: () -> Boolean, + inGroup: () -> Boolean, onClick: ((FeedBean) -> Unit)? = null, isEnded: (index: Int) -> Boolean, onEdit: ((FeedBean) -> Unit)? = null, @@ -79,20 +79,19 @@ fun Feed1Item( val isEnd = isEnded(index) Row( modifier = Modifier - .padding(horizontal = if (useCardLayout()) 16.dp else 0.dp) .clip( - if (useCardLayout() && isEnd) { - RoundedCornerShape(0.dp, 0.dp, SHAPE_CORNER_DP, SHAPE_CORNER_DP) - } else RectangleShape + if (inGroup()) { + if (isEnd) RoundedCornerShape(0.dp, 0.dp, SHAPE_CORNER_DP, SHAPE_CORNER_DP) + else RectangleShape + } else { + RoundedCornerShape(12.dp) + } + ) + .background( + MaterialTheme.colorScheme.secondary.copy( + alpha = if (selected(feed)) 0.15f else 0.1f + ) ) - .run { - if (useCardLayout()) { - background( - if (selected(feed)) MaterialTheme.colorScheme.surfaceContainerHighest - else MaterialTheme.colorScheme.surfaceContainer - ) - } else this - } .combinedClickable( onLongClick = if (onEdit != null) { { onEdit(feed) } @@ -108,8 +107,8 @@ fun Feed1Item( } else onClick(feed) }, ) - .padding(horizontal = if (useCardLayout()) 20.dp else 16.dp, vertical = 10.dp) - .padding(bottom = if (useCardLayout() && isEnd) 6.dp else 0.dp) + .padding(horizontal = 20.dp, vertical = 10.dp) + .padding(bottom = if (inGroup() && isEnd) 6.dp else 0.dp) ) { FeedIcon(modifier = Modifier.padding(vertical = 3.dp), data = feed, size = 36.dp) Spacer(modifier = Modifier.width(12.dp)) diff --git a/app/src/main/java/com/skyd/anivu/ui/component/lazyverticalgrid/adapter/proxy/Group1Proxy.kt b/app/src/main/java/com/skyd/anivu/ui/component/lazyverticalgrid/adapter/proxy/Group1Proxy.kt index 34b6d7f2..cb1e476f 100644 --- a/app/src/main/java/com/skyd/anivu/ui/component/lazyverticalgrid/adapter/proxy/Group1Proxy.kt +++ b/app/src/main/java/com/skyd/anivu/ui/component/lazyverticalgrid/adapter/proxy/Group1Proxy.kt @@ -9,8 +9,10 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.KeyboardArrowUp +import androidx.compose.material3.LocalAbsoluteTonalElevation import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text +import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -24,6 +26,7 @@ import androidx.compose.ui.unit.dp import com.skyd.anivu.model.bean.GroupBean import com.skyd.anivu.ui.component.AniVuIconButton import com.skyd.anivu.ui.component.lazyverticalgrid.adapter.LazyGridAdapter +import com.skyd.anivu.ui.local.LocalFeedListTonalElevation open class Group1Proxy( val isExpand: (GroupBean) -> Boolean = { false }, @@ -67,7 +70,6 @@ fun Group1Item( Row( modifier = Modifier - .padding(horizontal = 16.dp) .padding(top = 16.dp) .clip( RoundedCornerShape( @@ -77,7 +79,7 @@ fun Group1Item( backgroundShapeCorner, ) ) - .background(color = MaterialTheme.colorScheme.surfaceContainer) + .background(color = MaterialTheme.colorScheme.secondary.copy(alpha = 0.1f)) .combinedClickable( onLongClick = if (onEdit == null) null else { { onEdit(data) } diff --git a/app/src/main/java/com/skyd/anivu/ui/fragment/article/ArticleFragment.kt b/app/src/main/java/com/skyd/anivu/ui/fragment/article/ArticleFragment.kt index 2358a676..3af4e767 100644 --- a/app/src/main/java/com/skyd/anivu/ui/fragment/article/ArticleFragment.kt +++ b/app/src/main/java/com/skyd/anivu/ui/fragment/article/ArticleFragment.kt @@ -4,28 +4,29 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.compose.foundation.horizontalScroll +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.lazy.grid.LazyGridState +import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Done +import androidx.compose.material.icons.outlined.ArrowUpward import androidx.compose.material.icons.outlined.Search import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState -import androidx.compose.material3.FilterChip -import androidx.compose.material3.FilterChipDefaults +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon +import androidx.compose.material3.LocalAbsoluteTonalElevation +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState @@ -33,30 +34,34 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.fragment.app.viewModels import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.paging.PagingData import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems import com.skyd.anivu.R import com.skyd.anivu.base.BaseComposeFragment import com.skyd.anivu.base.mvi.getDispatcher +import com.skyd.anivu.ext.plus import com.skyd.anivu.ext.popBackStackWithLifecycle import com.skyd.anivu.ext.showSnackbar import com.skyd.anivu.model.bean.ArticleWithFeed +import com.skyd.anivu.ui.component.AniVuFloatingActionButton import com.skyd.anivu.ui.component.AniVuIconButton import com.skyd.anivu.ui.component.AniVuTopBar import com.skyd.anivu.ui.component.AniVuTopBarStyle @@ -67,8 +72,12 @@ import com.skyd.anivu.ui.component.lazyverticalgrid.AniVuLazyVerticalGrid import com.skyd.anivu.ui.component.lazyverticalgrid.adapter.LazyGridAdapter import com.skyd.anivu.ui.component.lazyverticalgrid.adapter.proxy.Article1Proxy import com.skyd.anivu.ui.fragment.search.SearchFragment +import com.skyd.anivu.ui.local.LocalArticleListTonalElevation +import com.skyd.anivu.ui.local.LocalArticleTopBarTonalElevation import com.skyd.anivu.ui.local.LocalNavController import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.launch @AndroidEntryPoint class ArticleFragment : BaseComposeFragment() { @@ -76,14 +85,13 @@ class ArticleFragment : BaseComposeFragment() { const val FEED_URLS_KEY = "feedUrls" } - private val feedViewModel by viewModels() private val feedUrls by lazy { arguments?.getStringArrayList(FEED_URLS_KEY).orEmpty() } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View = setContentBase { ArticleScreen(feedUrls = feedUrls, viewModel = feedViewModel) } + ): View = setContentBase { ArticleScreen(feedUrls = feedUrls) } } private val DefaultBackClick = { } @@ -92,6 +100,30 @@ private val DefaultBackClick = { } fun ArticleScreen( feedUrls: List, onBackClick: () -> Unit = DefaultBackClick, +) { + val navController = LocalNavController.current + if (feedUrls.isEmpty()) { + AniVuDialog( + visible = true, + text = { Text(text = stringResource(id = R.string.article_fragment_feed_url_illegal)) }, + confirmButton = { + TextButton(onClick = { navController.popBackStackWithLifecycle() }) { + Text(text = stringResource(id = R.string.exit)) + } + } + ) + } else { + ArticleContentScreen( + feedUrls = feedUrls, + onBackClick = onBackClick, + ) + } +} + +@Composable +private fun ArticleContentScreen( + feedUrls: List, + onBackClick: () -> Unit = DefaultBackClick, viewModel: ArticleViewModel = hiltViewModel(), ) { val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) @@ -99,11 +131,14 @@ fun ArticleScreen( val navController = LocalNavController.current val scope = rememberCoroutineScope() + val listState: LazyGridState = rememberLazyGridState() + var fabHeight by remember { mutableStateOf(0.dp) } + + val dispatch = viewModel.getDispatcher(feedUrls, startWith = ArticleIntent.Init(feedUrls)) val uiState by viewModel.viewState.collectAsStateWithLifecycle() val uiEvent by viewModel.singleEvent.collectAsStateWithLifecycle(initialValue = null) Scaffold( - modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, topBar = { AniVuTopBar( @@ -113,6 +148,14 @@ fun ArticleScreen( if (onBackClick == DefaultBackClick) BackIcon() else BackIcon(onClick = onBackClick) }, + colors = TopAppBarDefaults.centerAlignedTopAppBarColors().copy( + containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation( + LocalArticleTopBarTonalElevation.current.dp + ), + scrolledContainerColor = MaterialTheme.colorScheme.surfaceColorAtElevation( + LocalArticleTopBarTonalElevation.current.dp + 4.dp + ), + ), actions = { AniVuIconButton( onClick = { @@ -132,72 +175,85 @@ fun ArticleScreen( }, scrollBehavior = scrollBehavior, ) - } - ) { paddingValues -> - if (feedUrls.isEmpty()) { - AniVuDialog( - visible = true, - text = { Text(text = stringResource(id = R.string.article_fragment_feed_url_illegal)) }, - confirmButton = { - TextButton(onClick = { navController.popBackStackWithLifecycle() }) { - Text(text = stringResource(id = R.string.exit)) - } + }, + floatingActionButton = { + AnimatedVisibility( + visible = remember { derivedStateOf { listState.firstVisibleItemIndex > 2 } }.value, + enter = fadeIn(), + exit = fadeOut(), + ) { + AniVuFloatingActionButton( + onClick = { scope.launch { listState.animateScrollToItem(0) } }, + onSizeWithSinglePaddingChanged = { _, height -> fabHeight = height }, + contentDescription = stringResource(R.string.to_top), + ) { + Icon( + imageVector = Icons.Outlined.ArrowUpward, + contentDescription = null, + ) } - ) - } else { - val dispatch = - viewModel.getDispatcher(feedUrls, startWith = ArticleIntent.Init(feedUrls)) - Content( - uiState = uiState, - onRefresh = { dispatch(ArticleIntent.Refresh(feedUrls)) }, - onFilterFavorite = { dispatch(ArticleIntent.FilterFavorite(it)) }, - onFilterRead = { dispatch(ArticleIntent.FilterRead(it)) }, - onFavorite = { articleWithFeed, favorite -> - dispatch( - ArticleIntent.Favorite( - articleId = articleWithFeed.articleWithEnclosure.article.articleId, - favorite = favorite, - ) + } + }, + containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation( + LocalAbsoluteTonalElevation.current + + LocalArticleListTonalElevation.current.dp + ), + contentColor = MaterialTheme.colorScheme.onSurface, + ) { paddingValues -> + Content( + uiState = uiState, + listState = listState, + nestedScrollConnection = scrollBehavior.nestedScrollConnection, + onRefresh = { dispatch(ArticleIntent.Refresh(feedUrls)) }, + onFilterFavorite = { dispatch(ArticleIntent.FilterFavorite(it)) }, + onFilterRead = { dispatch(ArticleIntent.FilterRead(it)) }, + onFavorite = { articleWithFeed, favorite -> + dispatch( + ArticleIntent.Favorite( + articleId = articleWithFeed.articleWithEnclosure.article.articleId, + favorite = favorite, ) - }, - onRead = { articleWithFeed, read -> - dispatch( - ArticleIntent.Read( - articleId = articleWithFeed.articleWithEnclosure.article.articleId, - read = read, - ) + ) + }, + onRead = { articleWithFeed, read -> + dispatch( + ArticleIntent.Read( + articleId = articleWithFeed.articleWithEnclosure.article.articleId, + read = read, ) - }, - contentPadding = paddingValues, - ) - } - } + ) + }, + contentPadding = paddingValues + PaddingValues(bottom = fabHeight), + ) - WaitingDialog(visible = uiState.loadingDialog) + WaitingDialog(visible = uiState.loadingDialog) - when (val event = uiEvent) { - is ArticleEvent.InitArticleListResultEvent.Failed -> - snackbarHostState.showSnackbar(message = event.msg, scope = scope) + when (val event = uiEvent) { + is ArticleEvent.InitArticleListResultEvent.Failed -> + snackbarHostState.showSnackbar(message = event.msg, scope = scope) - is ArticleEvent.RefreshArticleListResultEvent.Failed -> - snackbarHostState.showSnackbar(message = event.msg, scope = scope) + is ArticleEvent.RefreshArticleListResultEvent.Failed -> + snackbarHostState.showSnackbar(message = event.msg, scope = scope) - is ArticleEvent.FavoriteArticleResultEvent.Failed -> - snackbarHostState.showSnackbar(message = event.msg, scope = scope) + is ArticleEvent.FavoriteArticleResultEvent.Failed -> + snackbarHostState.showSnackbar(message = event.msg, scope = scope) - is ArticleEvent.ReadArticleResultEvent.Failed -> - snackbarHostState.showSnackbar(message = event.msg, scope = scope) + is ArticleEvent.ReadArticleResultEvent.Failed -> + snackbarHostState.showSnackbar(message = event.msg, scope = scope) - null -> Unit + null -> Unit + } } } @Composable private fun Content( uiState: ArticleState, + listState: LazyGridState, + nestedScrollConnection: NestedScrollConnection, onRefresh: () -> Unit, - onFilterFavorite: (Boolean) -> Unit, - onFilterRead: (Boolean) -> Unit, + onFilterFavorite: (Boolean?) -> Unit, + onFilterRead: (Boolean?) -> Unit, onFavorite: (ArticleWithFeed, Boolean) -> Unit, onRead: (ArticleWithFeed, Boolean) -> Unit, contentPadding: PaddingValues, @@ -206,36 +262,39 @@ private fun Content( refreshing = uiState.articleListState.loading, onRefresh = onRefresh, ) - Box(modifier = Modifier.pullRefresh(state)) { - when (val articleListState = uiState.articleListState) { - is ArticleListState.Failed -> {} - is ArticleListState.Init -> {} - is ArticleListState.Success -> { - Column { - FilterRow( - modifier = Modifier.padding(top = contentPadding.calculateTopPadding()), - onFilterFavorite = onFilterFavorite, - onFilterRead = onFilterRead, - ) - ArticleList( - articles = articleListState.articlePagingDataFlow.collectAsLazyPagingItems(), - onFavorite = onFavorite, - onRead = onRead, - contentPadding = PaddingValues( - start = contentPadding.calculateStartPadding(LocalLayoutDirection.current), - end = contentPadding.calculateStartPadding(LocalLayoutDirection.current), - bottom = contentPadding.calculateBottomPadding(), - ), - ) - } - } + Box( + modifier = Modifier + .pullRefresh(state) + .padding(top = contentPadding.calculateTopPadding()), + ) { + Column { + FilterRow( + onFilterFavorite = onFilterFavorite, + onFilterRead = onFilterRead, + ) + HorizontalDivider() + + val articleListState = uiState.articleListState + ArticleList( + modifier = Modifier.nestedScroll(nestedScrollConnection), + articles = ((articleListState as? ArticleListState.Success) + ?.articlePagingDataFlow + ?: flowOf(PagingData.empty())).collectAsLazyPagingItems(), + listState = listState, + onFavorite = onFavorite, + onRead = onRead, + contentPadding = PaddingValues( + start = contentPadding.calculateStartPadding(LocalLayoutDirection.current), + end = contentPadding.calculateStartPadding(LocalLayoutDirection.current), + bottom = contentPadding.calculateBottomPadding(), + ) + PaddingValues(vertical = 4.dp), + ) } + PullRefreshIndicator( refreshing = uiState.articleListState.loading, state = state, - modifier = Modifier - .padding(contentPadding) - .align(Alignment.TopCenter), + modifier = Modifier.align(Alignment.TopCenter), ) } } @@ -244,6 +303,7 @@ private fun Content( private fun ArticleList( modifier: Modifier = Modifier, articles: LazyPagingItems, + listState: LazyGridState, onFavorite: (ArticleWithFeed, Boolean) -> Unit, onRead: (ArticleWithFeed, Boolean) -> Unit, contentPadding: PaddingValues, @@ -255,65 +315,11 @@ private fun ArticleList( modifier = modifier.fillMaxSize(), columns = GridCells.Adaptive(360.dp), dataList = articles, + listState = listState, adapter = adapter, - contentPadding = contentPadding, + contentPadding = contentPadding + PaddingValues(horizontal = 12.dp, vertical = 6.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), key = { _, item -> (item as ArticleWithFeed).articleWithEnclosure.article.articleId }, ) -} - -@Composable -private fun FilterRow( - modifier: Modifier, - onFilterFavorite: (Boolean) -> Unit, - onFilterRead: (Boolean) -> Unit, -) { - var favoriteSelected by rememberSaveable { mutableStateOf(false) } - var readSelected by rememberSaveable { mutableStateOf(false) } - - Row( - modifier = modifier - .fillMaxWidth() - .horizontalScroll(rememberScrollState()) - .padding(horizontal = 16.dp), - horizontalArrangement = Arrangement.spacedBy(6.dp), - ) { -// FilterChip( -// onClick = { -// favoriteSelected = !favoriteSelected -// onFilterFavorite(favoriteSelected) -// }, -// label = { Text(stringResource(id = R.string.article_screen_favorite_filter)) }, -// selected = favoriteSelected, -// leadingIcon = if (favoriteSelected) { -// { -// Icon( -// imageVector = Icons.Filled.Done, -// contentDescription = stringResource(id = R.string.item_selected), -// modifier = Modifier.size(FilterChipDefaults.IconSize) -// ) -// } -// } else { -// null -// }, -// ) -// FilterChip( -// onClick = { -// readSelected = !readSelected -// onFilterRead(readSelected) -// }, -// label = { Text(stringResource(id = R.string.article_screen_read_filter)) }, -// selected = readSelected, -// leadingIcon = if (readSelected) { -// { -// Icon( -// imageVector = Icons.Filled.Done, -// contentDescription = stringResource(id = R.string.item_selected), -// modifier = Modifier.size(FilterChipDefaults.IconSize) -// ) -// } -// } else { -// null -// }, -// ) - } } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/fragment/article/Filter.kt b/app/src/main/java/com/skyd/anivu/ui/fragment/article/Filter.kt new file mode 100644 index 00000000..e90ee61a --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/ui/fragment/article/Filter.kt @@ -0,0 +1,256 @@ +package com.skyd.anivu.ui.fragment.article + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.expandHorizontally +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkHorizontally +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowDropDown +import androidx.compose.material.icons.filled.ArrowDropUp +import androidx.compose.material.icons.outlined.ClearAll +import androidx.compose.material.icons.outlined.Drafts +import androidx.compose.material.icons.outlined.Favorite +import androidx.compose.material.icons.outlined.FavoriteBorder +import androidx.compose.material.icons.outlined.MarkEmailUnread +import androidx.compose.material.icons.outlined.Markunread +import androidx.compose.material.icons.outlined.Tune +import androidx.compose.material3.AssistChip +import androidx.compose.material3.Badge +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.FilterChip +import androidx.compose.material3.FilterChipDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.skyd.anivu.R + + +@Composable +internal fun FilterRow( + modifier: Modifier = Modifier, + onFilterFavorite: (Boolean?) -> Unit, + onFilterRead: (Boolean?) -> Unit, +) { + var favoriteFilterValue by rememberSaveable { mutableStateOf(null) } + var readFilterValue by rememberSaveable { mutableStateOf(null) } + + Row( + modifier = modifier + .fillMaxWidth() + .horizontalScroll(rememberScrollState()) + .padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + AnimatedVisibility( + visible = favoriteFilterValue != null || readFilterValue != null, + enter = fadeIn() + expandHorizontally(), + exit = fadeOut() + shrinkHorizontally(), + ) { + FilterSetting( + filterCount = { + listOf( + favoriteFilterValue, + readFilterValue + ).map { if (it != null) 1 else 0 }.sum() + }, + onClearAllFilters = { + onFilterFavorite(null) + favoriteFilterValue = null + onFilterRead(null) + readFilterValue = null + }, + ) + } + FavoriteFilter( + current = favoriteFilterValue, + onFilterFavorite = { + onFilterFavorite(it) + favoriteFilterValue = it + } + ) + ReadFilter( + current = readFilterValue, + onFilterRead = { + onFilterRead(it) + readFilterValue = it + }, + ) + } +} + +@Composable +internal fun FilterSetting( + filterCount: () -> Int, + onClearAllFilters: () -> Unit, +) { + var expandMenu by rememberSaveable { mutableStateOf(false) } + Box { + AssistChip( + onClick = { expandMenu = !expandMenu }, + label = { + Badge(containerColor = MaterialTheme.colorScheme.primary) { + Text( + modifier = Modifier.animateContentSize(), + text = filterCount().toString(), + ) + } + }, + leadingIcon = { + Icon( + imageVector = Icons.Outlined.Tune, + contentDescription = stringResource(id = R.string.article_screen_filter_setting), + ) + }, + ) + DropdownMenu( + expanded = expandMenu, + onDismissRequest = { expandMenu = false }, + ) { + DropdownMenuItem( + text = { Text(text = stringResource(id = R.string.article_screen_filter_clear_all_filter)) }, + leadingIcon = { + Icon( + imageVector = Icons.Outlined.ClearAll, + contentDescription = null + ) + }, + onClick = { + onClearAllFilters() + expandMenu = false + }, + ) + } + } +} + +@Composable +internal fun FavoriteFilter( + current: Boolean?, + onFilterFavorite: (Boolean?) -> Unit, +) { + val context = LocalContext.current + val items = remember { + mapOf( + null to Pair( + context.getString(R.string.article_screen_filter_all), + Icons.Outlined.FavoriteBorder, + ), + true to Pair( + context.getString(R.string.article_screen_filter_favorite), + Icons.Outlined.Favorite, + ), + false to Pair( + context.getString(R.string.article_screen_filter_unfavorite), + Icons.Outlined.FavoriteBorder, + ), + ) + } + FavoriteReadFilter( + current = current, + items = items, + onFilter = onFilterFavorite, + ) +} + +@Composable +internal fun ReadFilter( + current: Boolean?, + onFilterRead: (Boolean?) -> Unit, +) { + val context = LocalContext.current + val items = remember { + mapOf( + null to Pair( + context.getString(R.string.article_screen_filter_all), + Icons.Outlined.Markunread, + ), + true to Pair( + context.getString(R.string.article_screen_filter_read), + Icons.Outlined.Drafts, + ), + false to Pair( + context.getString(R.string.article_screen_filter_unread), + Icons.Outlined.MarkEmailUnread, + ), + ) + } + FavoriteReadFilter( + current = current, + items = items, + onFilter = onFilterRead, + ) +} + +@Composable +private fun FavoriteReadFilter( + current: Boolean?, + items: Map>, + onFilter: (Boolean?) -> Unit, +) { + var expandMenu by rememberSaveable { mutableStateOf(false) } + + Box { + FilterChip( + onClick = { expandMenu = !expandMenu }, + label = { + Text( + modifier = Modifier.animateContentSize(), + text = items[current]!!.first, + ) + }, + selected = current != null, + leadingIcon = { + Icon( + imageVector = items[current]!!.second, + contentDescription = null, + ) + }, + trailingIcon = { + Icon( + imageVector = if (expandMenu) Icons.Default.ArrowDropUp + else Icons.Default.ArrowDropDown, + contentDescription = null, + modifier = Modifier.size(FilterChipDefaults.IconSize), + ) + } + ) + DropdownMenu( + expanded = expandMenu, + onDismissRequest = { expandMenu = false }, + ) { + items.forEach { (t, u) -> + DropdownMenuItem( + text = { Text(text = u.first) }, + leadingIcon = { Icon(imageVector = u.second, contentDescription = null) }, + onClick = { + onFilter(t) + expandMenu = false + }, + ) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/fragment/feed/FeedScreen.kt b/app/src/main/java/com/skyd/anivu/ui/fragment/feed/FeedScreen.kt index 7c2c67ef..164457a4 100644 --- a/app/src/main/java/com/skyd/anivu/ui/fragment/feed/FeedScreen.kt +++ b/app/src/main/java/com/skyd/anivu/ui/fragment/feed/FeedScreen.kt @@ -19,6 +19,7 @@ import androidx.compose.material.icons.outlined.RssFeed import androidx.compose.material.icons.outlined.Search import androidx.compose.material.icons.outlined.Workspaces import androidx.compose.material3.Icon +import androidx.compose.material3.LocalAbsoluteTonalElevation import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost @@ -34,6 +35,7 @@ import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirective import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf @@ -90,6 +92,8 @@ import com.skyd.anivu.ui.fragment.article.ArticleFragment import com.skyd.anivu.ui.fragment.article.ArticleScreen import com.skyd.anivu.ui.fragment.search.SearchFragment import com.skyd.anivu.ui.local.LocalFeedGroupExpand +import com.skyd.anivu.ui.local.LocalFeedListTonalElevation +import com.skyd.anivu.ui.local.LocalFeedTopBarTonalElevation import com.skyd.anivu.ui.local.LocalHideEmptyDefault import com.skyd.anivu.ui.local.LocalNavController import com.skyd.anivu.ui.local.LocalWindowSizeClass @@ -190,7 +194,6 @@ private fun FeedList( val dispatch = viewModel.getDispatcher(startWith = FeedIntent.Init) Scaffold( - modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, topBar = { AniVuTopBar( @@ -232,6 +235,14 @@ private fun FeedList( if (windowSizeClass.isCompact) plus(WindowInsetsSides.Left) else this } ), + colors = TopAppBarDefaults.centerAlignedTopAppBarColors().copy( + containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation( + LocalFeedTopBarTonalElevation.current.dp + ), + scrolledContainerColor = MaterialTheme.colorScheme.surfaceColorAtElevation( + LocalFeedTopBarTonalElevation.current.dp + 4.dp + ), + ), scrollBehavior = scrollBehavior, ) }, @@ -247,11 +258,17 @@ private fun FeedList( Icon(imageVector = Icons.Outlined.Add, contentDescription = null) } }, + containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation( + LocalAbsoluteTonalElevation.current + + LocalFeedListTonalElevation.current.dp + ), + contentColor = MaterialTheme.colorScheme.onSurface, ) { innerPadding -> when (val groupListState = uiState.groupListState) { is GroupListState.Failed, GroupListState.Init, GroupListState.Loading -> {} is GroupListState.Success -> { FeedList( + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), result = groupListState.dataList, contentPadding = innerPadding + PaddingValues(bottom = fabHeight + 16.dp), selectedFeedUrls = listPaneSelectedFeedUrls, @@ -517,6 +534,7 @@ private fun CreateGroupDialog( @Composable private fun FeedList( + modifier: Modifier = Modifier, result: List, contentPadding: PaddingValues = PaddingValues(), selectedFeedUrls: List? = null, @@ -575,7 +593,7 @@ private fun FeedList( Feed1Proxy( visible = { feedVisible[it] ?: false }, selected = { selectedFeedUrls != null && it.url in selectedFeedUrls }, - useCardLayout = { true }, + inGroup = { true }, onClick = { onShowArticleList(listOf(it.url)) }, isEnded = { it == result.lastIndex || result[it + 1] is GroupBean }, onEdit = onEditFeed @@ -584,11 +602,11 @@ private fun FeedList( ) } AniVuLazyVerticalGrid( - modifier = Modifier.fillMaxSize(), + modifier = modifier.fillMaxSize(), columns = GridCells.Fixed(1), dataList = result, adapter = adapter, - contentPadding = contentPadding, + contentPadding = contentPadding + PaddingValues(horizontal = 16.dp), key = { _, item -> when (item) { is GroupBean.DefaultGroup -> item.groupId diff --git a/app/src/main/java/com/skyd/anivu/ui/fragment/media/MediaFragment.kt b/app/src/main/java/com/skyd/anivu/ui/fragment/media/MediaFragment.kt index fa0f9a4a..cc8e292c 100644 --- a/app/src/main/java/com/skyd/anivu/ui/fragment/media/MediaFragment.kt +++ b/app/src/main/java/com/skyd/anivu/ui/fragment/media/MediaFragment.kt @@ -105,7 +105,6 @@ fun MediaScreen(path: String, hasParentDir: Boolean, viewModel: MediaViewModel = val uiEvent by viewModel.singleEvent.collectAsStateWithLifecycle(initialValue = null) Scaffold( - modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, topBar = { AniVuTopBar( @@ -173,6 +172,7 @@ fun MediaScreen(path: String, hasParentDir: Boolean, viewModel: MediaViewModel = is MediaListState.Init -> Unit is MediaListState.Success -> { MediaList( + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), data = mediaListState.list, contentPadding = innerPadding, onPlay = { diff --git a/app/src/main/java/com/skyd/anivu/ui/fragment/search/SearchFragment.kt b/app/src/main/java/com/skyd/anivu/ui/fragment/search/SearchFragment.kt index 8280432d..67c642fa 100644 --- a/app/src/main/java/com/skyd/anivu/ui/fragment/search/SearchFragment.kt +++ b/app/src/main/java/com/skyd/anivu/ui/fragment/search/SearchFragment.kt @@ -5,8 +5,11 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.background import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.WindowInsets @@ -19,7 +22,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState +import androidx.compose.foundation.lazy.grid.LazyGridState +import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons @@ -28,6 +32,7 @@ import androidx.compose.material.icons.outlined.Clear import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon +import androidx.compose.material3.LocalAbsoluteTonalElevation import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost @@ -35,8 +40,10 @@ import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.material3.TextFieldDefaults +import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -74,6 +81,8 @@ import com.skyd.anivu.ui.component.lazyverticalgrid.AniVuLazyVerticalGrid import com.skyd.anivu.ui.component.lazyverticalgrid.adapter.LazyGridAdapter import com.skyd.anivu.ui.component.lazyverticalgrid.adapter.proxy.Article1Proxy import com.skyd.anivu.ui.component.lazyverticalgrid.adapter.proxy.Feed1Proxy +import com.skyd.anivu.ui.local.LocalSearchListTonalElevation +import com.skyd.anivu.ui.local.LocalSearchTopBarTonalElevation import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch import java.io.Serializable @@ -117,7 +126,7 @@ fun SearchScreen( val uiEvent by viewModel.singleEvent.collectAsStateWithLifecycle(initialValue = null) val keyboardController = LocalSoftwareKeyboardController.current - val searchResultListState = rememberLazyStaggeredGridState() + val searchResultListState = rememberLazyGridState() var fabHeight by remember { mutableStateOf(0.dp) } var fabWidth by remember { mutableStateOf(0.dp) } @@ -137,14 +146,20 @@ fun SearchScreen( modifier = Modifier.imePadding(), snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, floatingActionButton = { - AnimatedVisibility(visible = searchResultListState.firstVisibleItemIndex > 2) { + AnimatedVisibility( + visible = remember { + derivedStateOf { searchResultListState.firstVisibleItemIndex > 2 } + }.value, + enter = fadeIn(), + exit = fadeOut(), + ) { AniVuFloatingActionButton( onClick = { scope.launch { searchResultListState.animateScrollToItem(0) } }, onSizeWithSinglePaddingChanged = { width, height -> fabWidth = width fabHeight = height }, - contentDescription = stringResource(R.string.search_screen_list_to_top), + contentDescription = stringResource(R.string.to_top), ) { Icon( imageVector = Icons.Outlined.ArrowUpward, @@ -156,7 +171,11 @@ fun SearchScreen( topBar = { Column( modifier = Modifier - .background(MaterialTheme.colorScheme.background) + .background( + MaterialTheme.colorScheme.surfaceColorAtElevation( + LocalSearchTopBarTonalElevation.current.dp + ) + ) .windowInsetsPadding( WindowInsets.systemBars .only(WindowInsetsSides.Horizontal + WindowInsetsSides.Top) @@ -181,7 +200,12 @@ fun SearchScreen( ) HorizontalDivider() } - } + }, + containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation( + LocalAbsoluteTonalElevation.current + + LocalSearchListTonalElevation.current.dp + ), + contentColor = MaterialTheme.colorScheme.onSurface, ) { innerPaddings -> when (val searchResultState = uiState.searchResultState) { is SearchResultState.Failed -> Unit @@ -189,6 +213,7 @@ fun SearchScreen( SearchResultState.Loading -> CircularProgressIndicator() is SearchResultState.Success -> SearchResultList( result = searchResultState.result.collectAsLazyPagingItems(), + listState = searchResultListState, onFavorite = { articleWithFeed, favorite -> dispatch( SearchIntent.Favorite( @@ -205,7 +230,10 @@ fun SearchScreen( ) ) }, - contentPadding = innerPaddings + PaddingValues(bottom = fabHeight), + contentPadding = innerPaddings + PaddingValues( + top = 4.dp, + bottom = 4.dp + fabHeight, + ), ) } @@ -227,6 +255,7 @@ fun SearchScreen( private fun SearchResultList( modifier: Modifier = Modifier, result: LazyPagingItems, + listState: LazyGridState, onFavorite: (ArticleWithFeed, Boolean) -> Unit, onRead: (ArticleWithFeed, Boolean) -> Unit, contentPadding: PaddingValues, @@ -241,10 +270,13 @@ private fun SearchResultList( } AniVuLazyVerticalGrid( modifier = modifier, - columns = GridCells.Adaptive(250.dp), + columns = GridCells.Adaptive(360.dp), dataList = result, + listState = listState, adapter = adapter, - contentPadding = contentPadding, + contentPadding = contentPadding + PaddingValues(horizontal = 12.dp, vertical = 6.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), key = { _, item -> when (item) { is ArticleWithFeed -> item.articleWithEnclosure.article.articleId @@ -294,8 +326,8 @@ private fun SearchBarInputField( unfocusedIndicatorColor = Color.Transparent, errorIndicatorColor = Color.Transparent, disabledIndicatorColor = Color.Transparent, - focusedContainerColor = MaterialTheme.colorScheme.surface, - unfocusedContainerColor = MaterialTheme.colorScheme.surface, + focusedContainerColor = Color.Transparent, + unfocusedContainerColor = Color.Transparent, ), leadingIcon = leadingIcon, trailingIcon = trailingIcon, diff --git a/app/src/main/java/com/skyd/anivu/ui/fragment/settings/appearance/AppearanceFragment.kt b/app/src/main/java/com/skyd/anivu/ui/fragment/settings/appearance/AppearanceFragment.kt index 97171046..fbfe3289 100644 --- a/app/src/main/java/com/skyd/anivu/ui/fragment/settings/appearance/AppearanceFragment.kt +++ b/app/src/main/java/com/skyd/anivu/ui/fragment/settings/appearance/AppearanceFragment.kt @@ -156,6 +156,12 @@ class AppearanceFragment : BasePreferenceFragmentCompat() { styleCategory.addPreference(this) } + val screenStyleCategory = PreferenceCategory(this).apply { + key = "screenStyleCategory" + title = getString(R.string.appearance_fragment_screen_style_category) + screen.addPreference(this) + } + Preference(this).apply { key = "feedScreenStyle" title = getString(R.string.feed_style_screen_name) @@ -163,7 +169,27 @@ class AppearanceFragment : BasePreferenceFragmentCompat() { findMainNavController().navigate(R.id.action_to_feed_style_fragment) true } - styleCategory.addPreference(this) + screenStyleCategory.addPreference(this) + } + + Preference(this).apply { + key = "articleScreenStyle" + title = getString(R.string.article_style_screen_name) + setOnPreferenceClickListener { + findMainNavController().navigate(R.id.action_to_article_style_fragment) + true + } + screenStyleCategory.addPreference(this) + } + + Preference(this).apply { + key = "searchScreenStyle" + title = getString(R.string.search_style_screen_name) + setOnPreferenceClickListener { + findMainNavController().navigate(R.id.action_to_search_style_fragment) + true + } + screenStyleCategory.addPreference(this) } } diff --git a/app/src/main/java/com/skyd/anivu/ui/fragment/settings/appearance/article/ArticleStyleFragment.kt b/app/src/main/java/com/skyd/anivu/ui/fragment/settings/appearance/article/ArticleStyleFragment.kt new file mode 100644 index 00000000..7f284adc --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/ui/fragment/settings/appearance/article/ArticleStyleFragment.kt @@ -0,0 +1,163 @@ +package com.skyd.anivu.ui.fragment.settings.appearance.article + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Tonality +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import com.skyd.anivu.R +import com.skyd.anivu.base.BaseComposeFragment +import com.skyd.anivu.model.preference.appearance.article.ArticleItemTonalElevationPreference +import com.skyd.anivu.model.preference.appearance.article.ArticleListTonalElevationPreference +import com.skyd.anivu.model.preference.appearance.article.ArticleTopBarTonalElevationPreference +import com.skyd.anivu.model.preference.appearance.feed.TonalElevationPreferenceUtil +import com.skyd.anivu.ui.component.AniVuTopBar +import com.skyd.anivu.ui.component.AniVuTopBarStyle +import com.skyd.anivu.ui.component.BaseSettingsItem +import com.skyd.anivu.ui.component.CategorySettingsItem +import com.skyd.anivu.ui.fragment.settings.appearance.feed.TonalElevationDialog +import com.skyd.anivu.ui.local.LocalArticleItemTonalElevation +import com.skyd.anivu.ui.local.LocalArticleListTonalElevation +import com.skyd.anivu.ui.local.LocalArticleTopBarTonalElevation +import dagger.hilt.android.AndroidEntryPoint + + +@AndroidEntryPoint +class ArticleStyleFragment : BaseComposeFragment() { + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = setContentBase { ArticleStyleScreen() } +} + +@Composable +fun ArticleStyleScreen() { + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() + val context = LocalContext.current + val scope = rememberCoroutineScope() + + Scaffold( + topBar = { + AniVuTopBar( + style = AniVuTopBarStyle.Large, + scrollBehavior = scrollBehavior, + title = { Text(text = stringResource(R.string.article_style_screen_name)) }, + ) + } + ) { paddingValues -> + var openTopBarTonalElevationDialog by rememberSaveable { mutableStateOf(false) } + var openArticleListTonalElevationDialog by rememberSaveable { mutableStateOf(false) } + var openArticleItemTonalElevationDialog by rememberSaveable { mutableStateOf(false) } + + LazyColumn( + modifier = Modifier + .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection), + contentPadding = paddingValues, + ) { + item { + CategorySettingsItem(text = stringResource(id = R.string.article_style_screen_top_bar_category)) + } + item { + BaseSettingsItem( + icon = rememberVectorPainter(Icons.Outlined.Tonality), + text = stringResource(id = R.string.tonal_elevation), + descriptionText = TonalElevationPreferenceUtil.toDisplay( + LocalArticleTopBarTonalElevation.current + ), + onClick = { openTopBarTonalElevationDialog = true } + ) + } + item { + CategorySettingsItem(text = stringResource(id = R.string.article_style_screen_article_list_category)) + } + item { + BaseSettingsItem( + icon = rememberVectorPainter(Icons.Outlined.Tonality), + text = stringResource(id = R.string.tonal_elevation), + descriptionText = TonalElevationPreferenceUtil.toDisplay( + LocalArticleListTonalElevation.current + ), + onClick = { openArticleListTonalElevationDialog = true } + ) + } + item { + CategorySettingsItem(text = stringResource(id = R.string.article_style_screen_article_item_category)) + } + item { + BaseSettingsItem( + icon = rememberVectorPainter(Icons.Outlined.Tonality), + text = stringResource(id = R.string.tonal_elevation), + descriptionText = TonalElevationPreferenceUtil.toDisplay( + LocalArticleItemTonalElevation.current + ), + onClick = { openArticleItemTonalElevationDialog = true } + ) + } + } + + if (openTopBarTonalElevationDialog) { + TonalElevationDialog( + onDismissRequest = { openTopBarTonalElevationDialog = false }, + initValue = LocalArticleTopBarTonalElevation.current, + defaultValue = { ArticleTopBarTonalElevationPreference.default }, + onConfirm = { + ArticleTopBarTonalElevationPreference.put( + context = context, + scope = scope, + value = it, + ) + openTopBarTonalElevationDialog = false + } + ) + } + if (openArticleListTonalElevationDialog) { + TonalElevationDialog( + onDismissRequest = { openArticleListTonalElevationDialog = false }, + initValue = LocalArticleListTonalElevation.current, + defaultValue = { ArticleListTonalElevationPreference.default }, + onConfirm = { + ArticleListTonalElevationPreference.put( + context = context, + scope = scope, + value = it, + ) + openArticleListTonalElevationDialog = false + } + ) + } + if (openArticleItemTonalElevationDialog) { + TonalElevationDialog( + onDismissRequest = { openArticleItemTonalElevationDialog = false }, + initValue = LocalArticleItemTonalElevation.current, + defaultValue = { ArticleItemTonalElevationPreference.default }, + onConfirm = { + ArticleItemTonalElevationPreference.put( + context = context, + scope = scope, + value = it, + ) + openArticleItemTonalElevationDialog = false + } + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/fragment/settings/appearance/feed/FeedStyleFragment.kt b/app/src/main/java/com/skyd/anivu/ui/fragment/settings/appearance/feed/FeedStyleFragment.kt index c29d8458..bce0d992 100644 --- a/app/src/main/java/com/skyd/anivu/ui/fragment/settings/appearance/feed/FeedStyleFragment.kt +++ b/app/src/main/java/com/skyd/anivu/ui/fragment/settings/appearance/feed/FeedStyleFragment.kt @@ -4,32 +4,55 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Expand +import androidx.compose.material.icons.outlined.Restore +import androidx.compose.material.icons.outlined.Tonality +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import com.skyd.anivu.R import com.skyd.anivu.base.BaseComposeFragment import com.skyd.anivu.model.preference.appearance.feed.FeedGroupExpandPreference +import com.skyd.anivu.model.preference.appearance.feed.FeedListTonalElevationPreference +import com.skyd.anivu.model.preference.appearance.feed.FeedTopBarTonalElevationPreference +import com.skyd.anivu.model.preference.appearance.feed.TonalElevationPreferenceUtil +import com.skyd.anivu.ui.component.AniVuIconButton import com.skyd.anivu.ui.component.AniVuTopBar import com.skyd.anivu.ui.component.AniVuTopBarStyle +import com.skyd.anivu.ui.component.BaseSettingsItem import com.skyd.anivu.ui.component.CategorySettingsItem import com.skyd.anivu.ui.component.SwitchSettingsItem +import com.skyd.anivu.ui.component.dialog.SliderDialog import com.skyd.anivu.ui.local.LocalFeedGroupExpand +import com.skyd.anivu.ui.local.LocalFeedListTonalElevation +import com.skyd.anivu.ui.local.LocalFeedTopBarTonalElevation import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint -class FeedAppearanceFragment : BaseComposeFragment() { +class FeedStyleFragment : BaseComposeFragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -52,12 +75,28 @@ fun FeedStyleScreen() { ) } ) { paddingValues -> + var openTopBarTonalElevationDialog by rememberSaveable { mutableStateOf(false) } + var openGroupListTonalElevationDialog by rememberSaveable { mutableStateOf(false) } + LazyColumn( modifier = Modifier .fillMaxSize() .nestedScroll(scrollBehavior.nestedScrollConnection), contentPadding = paddingValues, ) { + item { + CategorySettingsItem(text = stringResource(id = R.string.feed_style_screen_top_bar_category)) + } + item { + BaseSettingsItem( + icon = rememberVectorPainter(Icons.Outlined.Tonality), + text = stringResource(id = R.string.tonal_elevation), + descriptionText = TonalElevationPreferenceUtil.toDisplay( + LocalFeedTopBarTonalElevation.current + ), + onClick = { openTopBarTonalElevationDialog = true } + ) + } item { CategorySettingsItem(text = stringResource(id = R.string.feed_style_screen_group_list_category)) } @@ -65,7 +104,7 @@ fun FeedStyleScreen() { val feedGroupExpand = LocalFeedGroupExpand.current SwitchSettingsItem( imageVector = Icons.Outlined.Expand, - text = stringResource(id = R.string.feed_style_screen_feed_group_expand), + text = stringResource(id = R.string.feed_style_screen_group_expand), checked = feedGroupExpand, onCheckedChange = { FeedGroupExpandPreference.put( @@ -76,6 +115,87 @@ fun FeedStyleScreen() { } ) } + item { + BaseSettingsItem( + icon = rememberVectorPainter(Icons.Outlined.Tonality), + text = stringResource(id = R.string.tonal_elevation), + descriptionText = TonalElevationPreferenceUtil.toDisplay( + LocalFeedListTonalElevation.current + ), + onClick = { openGroupListTonalElevationDialog = true } + ) + } + } + + if (openTopBarTonalElevationDialog) { + TonalElevationDialog( + onDismissRequest = { openTopBarTonalElevationDialog = false }, + initValue = LocalFeedTopBarTonalElevation.current, + defaultValue = { FeedTopBarTonalElevationPreference.default }, + onConfirm = { + FeedTopBarTonalElevationPreference.put( + context = context, + scope = scope, + value = it, + ) + openTopBarTonalElevationDialog = false + } + ) + } + if (openGroupListTonalElevationDialog) { + TonalElevationDialog( + onDismissRequest = { openGroupListTonalElevationDialog = false }, + initValue = LocalFeedListTonalElevation.current, + defaultValue = { FeedListTonalElevationPreference.default }, + onConfirm = { + FeedListTonalElevationPreference.put( + context = context, + scope = scope, + value = it, + ) + openGroupListTonalElevationDialog = false + } + ) } } } + +@Composable +internal fun TonalElevationDialog( + onDismissRequest: () -> Unit, + initValue: Float, + defaultValue: () -> Float, + onConfirm: (Float) -> Unit, +) { + var value by rememberSaveable { mutableFloatStateOf(initValue) } + + SliderDialog( + onDismissRequest = onDismissRequest, + value = value, + onValueChange = { value = it }, + valueRange = -6f..12f, + valueLabel = { + Box(modifier = Modifier.fillMaxWidth()) { + Text( + modifier = Modifier + .align(Alignment.Center) + .animateContentSize(), + text = TonalElevationPreferenceUtil.toDisplay(value), + style = MaterialTheme.typography.titleMedium, + ) + AniVuIconButton( + modifier = Modifier.align(Alignment.CenterEnd), + onClick = { value = defaultValue() }, + imageVector = Icons.Outlined.Restore, + ) + } + }, + icon = { Icon(imageVector = Icons.Outlined.Tonality, contentDescription = null) }, + title = { Text(text = stringResource(id = R.string.tonal_elevation)) }, + confirmButton = { + TextButton(onClick = { onConfirm(value) }) { + Text(text = stringResource(id = R.string.ok)) + } + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/fragment/settings/appearance/search/SearchStyleFragment.kt b/app/src/main/java/com/skyd/anivu/ui/fragment/settings/appearance/search/SearchStyleFragment.kt new file mode 100644 index 00000000..2cfa04f9 --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/ui/fragment/settings/appearance/search/SearchStyleFragment.kt @@ -0,0 +1,132 @@ +package com.skyd.anivu.ui.fragment.settings.appearance.search + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Tonality +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import com.skyd.anivu.R +import com.skyd.anivu.base.BaseComposeFragment +import com.skyd.anivu.model.preference.appearance.feed.TonalElevationPreferenceUtil +import com.skyd.anivu.model.preference.appearance.search.SearchListTonalElevationPreference +import com.skyd.anivu.model.preference.appearance.search.SearchTopBarTonalElevationPreference +import com.skyd.anivu.ui.component.AniVuTopBar +import com.skyd.anivu.ui.component.AniVuTopBarStyle +import com.skyd.anivu.ui.component.BaseSettingsItem +import com.skyd.anivu.ui.component.CategorySettingsItem +import com.skyd.anivu.ui.fragment.settings.appearance.feed.TonalElevationDialog +import com.skyd.anivu.ui.local.LocalSearchListTonalElevation +import com.skyd.anivu.ui.local.LocalSearchTopBarTonalElevation +import dagger.hilt.android.AndroidEntryPoint + + +@AndroidEntryPoint +class SearchStyleFragment : BaseComposeFragment() { + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = setContentBase { SearchStyleScreen() } +} + +@Composable +fun SearchStyleScreen() { + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() + val context = LocalContext.current + val scope = rememberCoroutineScope() + + Scaffold( + topBar = { + AniVuTopBar( + style = AniVuTopBarStyle.Large, + scrollBehavior = scrollBehavior, + title = { Text(text = stringResource(R.string.search_style_screen_name)) }, + ) + } + ) { paddingValues -> + var openTopBarTonalElevationDialog by rememberSaveable { mutableStateOf(false) } + var openSearchListTonalElevationDialog by rememberSaveable { mutableStateOf(false) } + + LazyColumn( + modifier = Modifier + .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection), + contentPadding = paddingValues, + ) { + item { + CategorySettingsItem(text = stringResource(id = R.string.search_style_screen_top_bar_category)) + } + item { + BaseSettingsItem( + icon = rememberVectorPainter(Icons.Outlined.Tonality), + text = stringResource(id = R.string.tonal_elevation), + descriptionText = TonalElevationPreferenceUtil.toDisplay( + LocalSearchTopBarTonalElevation.current + ), + onClick = { openTopBarTonalElevationDialog = true } + ) + } + item { + CategorySettingsItem(text = stringResource(id = R.string.search_style_screen_search_list_category)) + } + item { + BaseSettingsItem( + icon = rememberVectorPainter(Icons.Outlined.Tonality), + text = stringResource(id = R.string.tonal_elevation), + descriptionText = TonalElevationPreferenceUtil.toDisplay( + LocalSearchListTonalElevation.current + ), + onClick = { openSearchListTonalElevationDialog = true } + ) + } + } + + if (openTopBarTonalElevationDialog) { + TonalElevationDialog( + onDismissRequest = { openTopBarTonalElevationDialog = false }, + initValue = LocalSearchTopBarTonalElevation.current, + defaultValue = { SearchTopBarTonalElevationPreference.default }, + onConfirm = { + SearchTopBarTonalElevationPreference.put( + context = context, + scope = scope, + value = it, + ) + openTopBarTonalElevationDialog = false + } + ) + } + if (openSearchListTonalElevationDialog) { + TonalElevationDialog( + onDismissRequest = { openSearchListTonalElevationDialog = false }, + initValue = LocalSearchListTonalElevation.current, + defaultValue = { SearchListTonalElevationPreference.default }, + onConfirm = { + SearchListTonalElevationPreference.put( + context = context, + scope = scope, + value = it, + ) + openSearchListTonalElevationDialog = false + } + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/local/LocalValue.kt b/app/src/main/java/com/skyd/anivu/ui/local/LocalValue.kt index 82ad0de4..f6a7acef 100644 --- a/app/src/main/java/com/skyd/anivu/ui/local/LocalValue.kt +++ b/app/src/main/java/com/skyd/anivu/ui/local/LocalValue.kt @@ -9,7 +9,14 @@ import com.skyd.anivu.model.preference.appearance.DateStylePreference import com.skyd.anivu.model.preference.appearance.NavigationBarLabelPreference import com.skyd.anivu.model.preference.appearance.TextFieldStylePreference import com.skyd.anivu.model.preference.appearance.ThemePreference +import com.skyd.anivu.model.preference.appearance.article.ArticleItemTonalElevationPreference +import com.skyd.anivu.model.preference.appearance.article.ArticleListTonalElevationPreference +import com.skyd.anivu.model.preference.appearance.article.ArticleTopBarTonalElevationPreference import com.skyd.anivu.model.preference.appearance.feed.FeedGroupExpandPreference +import com.skyd.anivu.model.preference.appearance.feed.FeedListTonalElevationPreference +import com.skyd.anivu.model.preference.appearance.feed.FeedTopBarTonalElevationPreference +import com.skyd.anivu.model.preference.appearance.search.SearchListTonalElevationPreference +import com.skyd.anivu.model.preference.appearance.search.SearchTopBarTonalElevationPreference import com.skyd.anivu.model.preference.behavior.PickImageMethodPreference import com.skyd.anivu.model.preference.behavior.article.ArticleSwipeLeftActionPreference import com.skyd.anivu.model.preference.behavior.article.ArticleTapActionPreference @@ -39,6 +46,19 @@ val LocalFeedGroupExpand = compositionLocalOf { FeedGroupExpandPreference.defaul val LocalTextFieldStyle = compositionLocalOf { TextFieldStylePreference.default } val LocalDateStyle = compositionLocalOf { DateStylePreference.default } val LocalNavigationBarLabel = compositionLocalOf { NavigationBarLabelPreference.default } +val LocalFeedListTonalElevation = compositionLocalOf { FeedListTonalElevationPreference.default } +val LocalFeedTopBarTonalElevation = + compositionLocalOf { FeedTopBarTonalElevationPreference.default } +val LocalArticleListTonalElevation = + compositionLocalOf { ArticleListTonalElevationPreference.default } +val LocalArticleTopBarTonalElevation = + compositionLocalOf { ArticleTopBarTonalElevationPreference.default } +val LocalArticleItemTonalElevation = + compositionLocalOf { ArticleItemTonalElevationPreference.default } +val LocalSearchListTonalElevation = + compositionLocalOf { SearchListTonalElevationPreference.default } +val LocalSearchTopBarTonalElevation = + compositionLocalOf { SearchTopBarTonalElevationPreference.default } // Update val LocalIgnoreUpdateVersion = compositionLocalOf { IgnoreUpdateVersionPreference.default } diff --git a/app/src/main/java/com/skyd/anivu/ui/theme/Theme.kt b/app/src/main/java/com/skyd/anivu/ui/theme/Theme.kt index 5f57a1bd..e026e051 100644 --- a/app/src/main/java/com/skyd/anivu/ui/theme/Theme.kt +++ b/app/src/main/java/com/skyd/anivu/ui/theme/Theme.kt @@ -34,7 +34,7 @@ fun AniVuTheme( @Composable fun AniVuTheme( darkTheme: Boolean, - wallpaperColors: Map = extractAllColors(darkTheme), + colors: Map = extractAllColors(darkTheme), content: @Composable () -> Unit ) { val view = LocalView.current @@ -49,7 +49,7 @@ fun AniVuTheme( MaterialTheme( colorScheme = remember(themeName) { - wallpaperColors.getOrElse(themeName) { + colors.getOrElse(themeName) { dynamicColorScheme( seedColor = ThemePreference.toSeedColor( context = context, diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index 6ea78bbf..73e47302 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -165,7 +165,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 626a743a..25d4821d 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -168,7 +168,7 @@ 附件 点击项 左滑 - 到顶部 + 到顶部 默认 分组 添加分组 @@ -177,9 +177,10 @@ 删除当前订阅及其所有文章 标题 订阅页面 - 总是展开 + 总是展开 分组列表 - 样式 + 通用样式 + 页面样式 正常 轮廓 文本框样式 @@ -250,8 +251,22 @@ 取消收藏 标为未读 标为已读 - 已收藏 - 已读 + 收藏 + 未收藏 + 已读 + 未读 + 全部 + 设置 + 清除所有过滤 + 色调高程 + 标题栏 + 文章页面 + 标题栏 + 文章列表 + 文章项 + 搜索页面 + 标题栏 + 搜索列表 导入了 %d 项,花费 %.2f 秒 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index ce494921..d3f072c1 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -167,14 +167,14 @@ 附件 點選項 左滑 - 到頂部 + 到頂部 預設 分組 新增分組 移動到組 刪除“%s”分組及其所有訂閱和文章 訂閱頁面 - 總是展開 + 總是展開 分組列表 樣式 相片挑選工具(%s) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 28d23ccc..39d13398 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -176,7 +176,7 @@ Show enclosures Tap item Swipe left - To top + To top Default Group Add a group @@ -185,9 +185,10 @@ Delete the feed and articles it contains Title Feed screen - Always expand + Always expand Group list - Style + Common style + Screen style Normal Outlined Text field style @@ -258,8 +259,22 @@ Unfavorite Unread Mark as read - Favorite - Read + Favorite + Favorite + Read + Unread + All + Settings + Clear all filters + Tonal elevation + Top bar + Article screen + Top bar + Article list + Article item + Search screen + Top bar + Search list Imported %d item, takes %.2f seconds Imported %d items, takes %.2f seconds