Merge pull request #35 from Feature/CustomStyles

Feature/CustomStyles
This commit is contained in:
Ashinch 2022-05-02 21:09:25 +08:00 committed by GitHub
commit c1a4c8ec5b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
51 changed files with 2393 additions and 245 deletions

View File

@ -18,6 +18,7 @@ import coil.memory.MemoryCache
import coil.request.* import coil.request.*
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import me.ash.reader.data.preference.SettingsProvider
import me.ash.reader.ui.page.common.HomeEntry import me.ash.reader.ui.page.common.HomeEntry
@AndroidEntryPoint @AndroidEntryPoint
@ -28,9 +29,11 @@ class MainActivity : ComponentActivity(), ImageLoader {
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)
Log.i("RLog", "onCreate: ${ProfileInstallerInitializer().create(this)}") Log.i("RLog", "onCreate: ${ProfileInstallerInitializer().create(this)}")
setContent { setContent {
SettingsProvider {
HomeEntry() HomeEntry()
} }
} }
}
override val components: ComponentRegistry override val components: ComponentRegistry
get() = ComponentRegistry.Builder().add(SvgDecoder.Factory()).build() get() = ComponentRegistry.Builder().add(SvgDecoder.Factory()).build()

View File

@ -2,13 +2,16 @@ package me.ash.reader.data.entity
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.FiberManualRecord import androidx.compose.material.icons.outlined.FiberManualRecord
import androidx.compose.material.icons.rounded.FiberManualRecord
import androidx.compose.material.icons.rounded.Star
import androidx.compose.material.icons.rounded.StarOutline import androidx.compose.material.icons.rounded.StarOutline
import androidx.compose.material.icons.rounded.Subject import androidx.compose.material.icons.rounded.Subject
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
class Filter( class Filter(
var index: Int, val index: Int,
var icon: ImageVector, val iconOutline: ImageVector,
val iconFilled: ImageVector,
) { ) {
fun isStarred(): Boolean = this == Starred fun isStarred(): Boolean = this == Starred
fun isUnread(): Boolean = this == Unread fun isUnread(): Boolean = this == Unread
@ -17,15 +20,18 @@ class Filter(
companion object { companion object {
val Starred = Filter( val Starred = Filter(
index = 0, index = 0,
icon = Icons.Rounded.StarOutline, iconOutline = Icons.Rounded.StarOutline,
iconFilled = Icons.Rounded.Star,
) )
val Unread = Filter( val Unread = Filter(
index = 1, index = 1,
icon = Icons.Outlined.FiberManualRecord, iconOutline = Icons.Outlined.FiberManualRecord,
iconFilled = Icons.Rounded.FiberManualRecord,
) )
val All = Filter( val All = Filter(
index = 2, index = 2,
icon = Icons.Rounded.Subject, iconOutline = Icons.Rounded.Subject,
iconFilled = Icons.Rounded.Subject,
) )
} }
} }

View File

@ -0,0 +1,22 @@
package me.ash.reader.data.preference
import android.content.Context
import androidx.datastore.preferences.core.Preferences
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import me.ash.reader.ui.ext.DataStoreKeys
import me.ash.reader.ui.ext.dataStore
import me.ash.reader.ui.ext.put
object CustomPrimaryColorPreference {
const val default = ""
fun put(context: Context, scope: CoroutineScope, value: String) {
scope.launch {
context.dataStore.put(DataStoreKeys.CustomPrimaryColor, value)
}
}
fun fromPreferences(preferences: Preferences) =
preferences[DataStoreKeys.CustomPrimaryColor.key] ?: default
}

View File

@ -0,0 +1,41 @@
package me.ash.reader.data.preference
import android.content.Context
import androidx.datastore.preferences.core.Preferences
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import me.ash.reader.ui.ext.DataStoreKeys
import me.ash.reader.ui.ext.dataStore
import me.ash.reader.ui.ext.put
sealed class FeedsFilterBarFilledPreference(val value: Boolean) : Preference() {
object ON : FeedsFilterBarFilledPreference(true)
object OFF : FeedsFilterBarFilledPreference(false)
override fun put(context: Context, scope: CoroutineScope) {
scope.launch {
context.dataStore.put(
DataStoreKeys.FeedsFilterBarFilled,
value
)
}
}
companion object {
val default = OFF
val values = listOf(ON, OFF)
fun fromPreferences(preferences: Preferences) =
when (preferences[DataStoreKeys.FeedsFilterBarFilled.key]) {
true -> ON
false -> OFF
else -> default
}
}
}
operator fun FeedsFilterBarFilledPreference.not(): FeedsFilterBarFilledPreference =
when (value) {
true -> FeedsFilterBarFilledPreference.OFF
false -> FeedsFilterBarFilledPreference.ON
}

View File

@ -0,0 +1,22 @@
package me.ash.reader.data.preference
import android.content.Context
import androidx.datastore.preferences.core.Preferences
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import me.ash.reader.ui.ext.DataStoreKeys
import me.ash.reader.ui.ext.dataStore
import me.ash.reader.ui.ext.put
object FeedsFilterBarPaddingPreference {
const val default = 60
fun put(context: Context, scope: CoroutineScope, value: Int) {
scope.launch {
context.dataStore.put(DataStoreKeys.FeedsFilterBarPadding, value)
}
}
fun fromPreferences(preferences: Preferences) =
preferences[DataStoreKeys.FeedsFilterBarPadding.key] ?: default
}

View File

@ -0,0 +1,45 @@
package me.ash.reader.data.preference
import android.content.Context
import androidx.datastore.preferences.core.Preferences
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import me.ash.reader.R
import me.ash.reader.ui.ext.DataStoreKeys
import me.ash.reader.ui.ext.dataStore
import me.ash.reader.ui.ext.put
sealed class FeedsFilterBarStylePreference(val value: Int) : Preference() {
object Icon : FeedsFilterBarStylePreference(0)
object IconLabel : FeedsFilterBarStylePreference(1)
object IconLabelOnlySelected : FeedsFilterBarStylePreference(2)
override fun put(context: Context, scope: CoroutineScope) {
scope.launch {
context.dataStore.put(
DataStoreKeys.FeedsFilterBarStyle,
value
)
}
}
fun getDesc(context: Context): String =
when (this) {
Icon -> context.getString(R.string.icons)
IconLabel -> context.getString(R.string.icons_and_labels)
IconLabelOnlySelected -> context.getString(R.string.icons_and_label_only_selected)
}
companion object {
val default = Icon
val values = listOf(Icon, IconLabel, IconLabelOnlySelected)
fun fromPreferences(preferences: Preferences): FeedsFilterBarStylePreference =
when (preferences[DataStoreKeys.FeedsFilterBarStyle.key]) {
0 -> Icon
1 -> IconLabel
2 -> IconLabelOnlySelected
else -> default
}
}
}

View File

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

View File

@ -0,0 +1,41 @@
package me.ash.reader.data.preference
import android.content.Context
import androidx.datastore.preferences.core.Preferences
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import me.ash.reader.ui.ext.DataStoreKeys
import me.ash.reader.ui.ext.dataStore
import me.ash.reader.ui.ext.put
sealed class FeedsGroupListExpandPreference(val value: Boolean) : Preference() {
object ON : FeedsGroupListExpandPreference(true)
object OFF : FeedsGroupListExpandPreference(false)
override fun put(context: Context, scope: CoroutineScope) {
scope.launch {
context.dataStore.put(
DataStoreKeys.FeedsGroupListExpand,
value
)
}
}
companion object {
val default = ON
val values = listOf(ON, OFF)
fun fromPreferences(preferences: Preferences) =
when (preferences[DataStoreKeys.FeedsGroupListExpand.key]) {
true -> ON
false -> OFF
else -> default
}
}
}
operator fun FeedsGroupListExpandPreference.not(): FeedsGroupListExpandPreference =
when (value) {
true -> FeedsGroupListExpandPreference.OFF
false -> FeedsGroupListExpandPreference.ON
}

View File

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

View File

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

View File

@ -0,0 +1,41 @@
package me.ash.reader.data.preference
import android.content.Context
import androidx.datastore.preferences.core.Preferences
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import me.ash.reader.ui.ext.DataStoreKeys
import me.ash.reader.ui.ext.dataStore
import me.ash.reader.ui.ext.put
sealed class FlowArticleListDateStickyHeaderPreference(val value: Boolean) : Preference() {
object ON : FlowArticleListDateStickyHeaderPreference(true)
object OFF : FlowArticleListDateStickyHeaderPreference(false)
override fun put(context: Context, scope: CoroutineScope) {
scope.launch {
context.dataStore.put(
DataStoreKeys.FlowArticleListDateStickyHeader,
value
)
}
}
companion object {
val default = ON
val values = listOf(ON, OFF)
fun fromPreferences(preferences: Preferences) =
when (preferences[DataStoreKeys.FlowArticleListDateStickyHeader.key]) {
true -> ON
false -> OFF
else -> default
}
}
}
operator fun FlowArticleListDateStickyHeaderPreference.not(): FlowArticleListDateStickyHeaderPreference =
when (value) {
true -> FlowArticleListDateStickyHeaderPreference.OFF
false -> FlowArticleListDateStickyHeaderPreference.ON
}

View File

@ -0,0 +1,41 @@
package me.ash.reader.data.preference
import android.content.Context
import androidx.datastore.preferences.core.Preferences
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import me.ash.reader.ui.ext.DataStoreKeys
import me.ash.reader.ui.ext.dataStore
import me.ash.reader.ui.ext.put
sealed class FlowArticleListDescPreference(val value: Boolean) : Preference() {
object ON : FlowArticleListDescPreference(true)
object OFF : FlowArticleListDescPreference(false)
override fun put(context: Context, scope: CoroutineScope) {
scope.launch {
context.dataStore.put(
DataStoreKeys.FlowArticleListDesc,
value
)
}
}
companion object {
val default = ON
val values = listOf(ON, OFF)
fun fromPreferences(preferences: Preferences) =
when (preferences[DataStoreKeys.FlowArticleListDesc.key]) {
true -> ON
false -> OFF
else -> default
}
}
}
operator fun FlowArticleListDescPreference.not(): FlowArticleListDescPreference =
when (value) {
true -> FlowArticleListDescPreference.OFF
false -> FlowArticleListDescPreference.ON
}

View File

@ -0,0 +1,41 @@
package me.ash.reader.data.preference
import android.content.Context
import androidx.datastore.preferences.core.Preferences
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import me.ash.reader.ui.ext.DataStoreKeys
import me.ash.reader.ui.ext.dataStore
import me.ash.reader.ui.ext.put
sealed class FlowArticleListFeedIconPreference(val value: Boolean) : Preference() {
object ON : FlowArticleListFeedIconPreference(true)
object OFF : FlowArticleListFeedIconPreference(false)
override fun put(context: Context, scope: CoroutineScope) {
scope.launch {
context.dataStore.put(
DataStoreKeys.FlowArticleListFeedIcon,
value
)
}
}
companion object {
val default = ON
val values = listOf(ON, OFF)
fun fromPreferences(preferences: Preferences) =
when (preferences[DataStoreKeys.FlowArticleListFeedIcon.key]) {
true -> ON
false -> OFF
else -> default
}
}
}
operator fun FlowArticleListFeedIconPreference.not(): FlowArticleListFeedIconPreference =
when (value) {
true -> FlowArticleListFeedIconPreference.OFF
false -> FlowArticleListFeedIconPreference.ON
}

View File

@ -0,0 +1,41 @@
package me.ash.reader.data.preference
import android.content.Context
import androidx.datastore.preferences.core.Preferences
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import me.ash.reader.ui.ext.DataStoreKeys
import me.ash.reader.ui.ext.dataStore
import me.ash.reader.ui.ext.put
sealed class FlowArticleListFeedNamePreference(val value: Boolean) : Preference() {
object ON : FlowArticleListFeedNamePreference(true)
object OFF : FlowArticleListFeedNamePreference(false)
override fun put(context: Context, scope: CoroutineScope) {
scope.launch {
context.dataStore.put(
DataStoreKeys.FlowArticleListFeedName,
value
)
}
}
companion object {
val default = ON
val values = listOf(ON, OFF)
fun fromPreferences(preferences: Preferences) =
when (preferences[DataStoreKeys.FlowArticleListFeedName.key]) {
true -> ON
false -> OFF
else -> default
}
}
}
operator fun FlowArticleListFeedNamePreference.not(): FlowArticleListFeedNamePreference =
when (value) {
true -> FlowArticleListFeedNamePreference.OFF
false -> FlowArticleListFeedNamePreference.ON
}

View File

@ -0,0 +1,41 @@
package me.ash.reader.data.preference
import android.content.Context
import androidx.datastore.preferences.core.Preferences
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import me.ash.reader.ui.ext.DataStoreKeys
import me.ash.reader.ui.ext.dataStore
import me.ash.reader.ui.ext.put
sealed class FlowArticleListImagePreference(val value: Boolean) : Preference() {
object ON : FlowArticleListImagePreference(true)
object OFF : FlowArticleListImagePreference(false)
override fun put(context: Context, scope: CoroutineScope) {
scope.launch {
context.dataStore.put(
DataStoreKeys.FlowArticleListImage,
value
)
}
}
companion object {
val default = ON
val values = listOf(ON, OFF)
fun fromPreferences(preferences: Preferences) =
when (preferences[DataStoreKeys.FlowArticleListImage.key]) {
true -> ON
false -> OFF
else -> default
}
}
}
operator fun FlowArticleListImagePreference.not(): FlowArticleListImagePreference =
when (value) {
true -> FlowArticleListImagePreference.OFF
false -> FlowArticleListImagePreference.ON
}

View File

@ -0,0 +1,41 @@
package me.ash.reader.data.preference
import android.content.Context
import androidx.datastore.preferences.core.Preferences
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import me.ash.reader.ui.ext.DataStoreKeys
import me.ash.reader.ui.ext.dataStore
import me.ash.reader.ui.ext.put
sealed class FlowArticleListTimePreference(val value: Boolean) : Preference() {
object ON : FlowArticleListTimePreference(true)
object OFF : FlowArticleListTimePreference(false)
override fun put(context: Context, scope: CoroutineScope) {
scope.launch {
context.dataStore.put(
DataStoreKeys.FlowArticleListTime,
value
)
}
}
companion object {
val default = ON
val values = listOf(ON, OFF)
fun fromPreferences(preferences: Preferences) =
when (preferences[DataStoreKeys.FlowArticleListTime.key]) {
true -> ON
false -> OFF
else -> default
}
}
}
operator fun FlowArticleListTimePreference.not(): FlowArticleListTimePreference =
when (value) {
true -> FlowArticleListTimePreference.OFF
false -> FlowArticleListTimePreference.ON
}

View File

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

View File

@ -0,0 +1,41 @@
package me.ash.reader.data.preference
import android.content.Context
import androidx.datastore.preferences.core.Preferences
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import me.ash.reader.ui.ext.DataStoreKeys
import me.ash.reader.ui.ext.dataStore
import me.ash.reader.ui.ext.put
sealed class FlowFilterBarFilledPreference(val value: Boolean) : Preference() {
object ON : FlowFilterBarFilledPreference(true)
object OFF : FlowFilterBarFilledPreference(false)
override fun put(context: Context, scope: CoroutineScope) {
scope.launch {
context.dataStore.put(
DataStoreKeys.FlowFilterBarFilled,
value
)
}
}
companion object {
val default = OFF
val values = listOf(ON, OFF)
fun fromPreferences(preferences: Preferences) =
when (preferences[DataStoreKeys.FlowFilterBarFilled.key]) {
true -> ON
false -> OFF
else -> default
}
}
}
operator fun FlowFilterBarFilledPreference.not(): FlowFilterBarFilledPreference =
when (value) {
true -> FlowFilterBarFilledPreference.OFF
false -> FlowFilterBarFilledPreference.ON
}

View File

@ -0,0 +1,22 @@
package me.ash.reader.data.preference
import android.content.Context
import androidx.datastore.preferences.core.Preferences
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import me.ash.reader.ui.ext.DataStoreKeys
import me.ash.reader.ui.ext.dataStore
import me.ash.reader.ui.ext.put
object FlowFilterBarPaddingPreference {
const val default = 60
fun put(context: Context, scope: CoroutineScope, value: Int) {
scope.launch {
context.dataStore.put(DataStoreKeys.FlowFilterBarPadding, value)
}
}
fun fromPreferences(preferences: Preferences) =
preferences[DataStoreKeys.FlowFilterBarPadding.key] ?: default
}

View File

@ -0,0 +1,45 @@
package me.ash.reader.data.preference
import android.content.Context
import androidx.datastore.preferences.core.Preferences
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import me.ash.reader.R
import me.ash.reader.ui.ext.DataStoreKeys
import me.ash.reader.ui.ext.dataStore
import me.ash.reader.ui.ext.put
sealed class FlowFilterBarStylePreference(val value: Int) : Preference() {
object Icon : FlowFilterBarStylePreference(0)
object IconLabel : FlowFilterBarStylePreference(1)
object IconLabelOnlySelected : FlowFilterBarStylePreference(2)
override fun put(context: Context, scope: CoroutineScope) {
scope.launch {
context.dataStore.put(
DataStoreKeys.FlowFilterBarStyle,
value
)
}
}
fun getDesc(context: Context): String =
when (this) {
Icon -> context.getString(R.string.icons)
IconLabel -> context.getString(R.string.icons_and_labels)
IconLabelOnlySelected -> context.getString(R.string.icons_and_label_only_selected)
}
companion object {
val default = Icon
val values = listOf(Icon, IconLabel, IconLabelOnlySelected)
fun fromPreferences(preferences: Preferences) =
when (preferences[DataStoreKeys.FlowFilterBarStyle.key]) {
0 -> Icon
1 -> IconLabel
2 -> IconLabelOnlySelected
else -> default
}
}
}

View File

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

View File

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

View File

@ -0,0 +1,8 @@
package me.ash.reader.data.preference
import android.content.Context
import kotlinx.coroutines.CoroutineScope
sealed class Preference {
abstract fun put(context: Context, scope: CoroutineScope)
}

View File

@ -0,0 +1,152 @@
package me.ash.reader.data.preference
import android.util.Log
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.datastore.preferences.core.Preferences
import kotlinx.coroutines.flow.map
import me.ash.reader.ui.ext.collectAsStateValue
import me.ash.reader.ui.ext.dataStore
data class Settings(
val themeIndex: Int = ThemeIndexPreference.default,
val customPrimaryColor: String = CustomPrimaryColorPreference.default,
val feedsFilterBarStyle: FeedsFilterBarStylePreference = FeedsFilterBarStylePreference.default,
val feedsFilterBarFilled: FeedsFilterBarFilledPreference = FeedsFilterBarFilledPreference.default,
val feedsFilterBarPadding: Int = FeedsFilterBarPaddingPreference.default,
val feedsFilterBarTonalElevation: FeedsFilterBarTonalElevationPreference = FeedsFilterBarTonalElevationPreference.default,
val feedsTopBarTonalElevation: FeedsTopBarTonalElevationPreference = FeedsTopBarTonalElevationPreference.default,
val feedsGroupListExpand: FeedsGroupListExpandPreference = FeedsGroupListExpandPreference.default,
val feedsGroupListTonalElevation: FeedsGroupListTonalElevationPreference = FeedsGroupListTonalElevationPreference.default,
val flowFilterBarStyle: FlowFilterBarStylePreference = FlowFilterBarStylePreference.default,
val flowFilterBarFilled: FlowFilterBarFilledPreference = FlowFilterBarFilledPreference.default,
val flowFilterBarPadding: Int = FlowFilterBarPaddingPreference.default,
val flowFilterBarTonalElevation: FlowFilterBarTonalElevationPreference = FlowFilterBarTonalElevationPreference.default,
val flowTopBarTonalElevation: FlowTopBarTonalElevationPreference = FlowTopBarTonalElevationPreference.default,
val flowArticleListFeedIcon: FlowArticleListFeedIconPreference = FlowArticleListFeedIconPreference.default,
val flowArticleListFeedName: FlowArticleListFeedNamePreference = FlowArticleListFeedNamePreference.default,
val flowArticleListImage: FlowArticleListImagePreference = FlowArticleListImagePreference.default,
val flowArticleListDesc: FlowArticleListDescPreference = FlowArticleListDescPreference.default,
val flowArticleListTime: FlowArticleListTimePreference = FlowArticleListTimePreference.default,
val flowArticleListDateStickyHeader: FlowArticleListDateStickyHeaderPreference = FlowArticleListDateStickyHeaderPreference.default,
val flowArticleListTonalElevation: FlowArticleListTonalElevationPreference = FlowArticleListTonalElevationPreference.default,
)
fun Preferences.toSettings(): Settings {
return Settings(
themeIndex = ThemeIndexPreference.fromPreferences(this),
customPrimaryColor = CustomPrimaryColorPreference.fromPreferences(this),
feedsFilterBarStyle = FeedsFilterBarStylePreference.fromPreferences(this),
feedsFilterBarFilled = FeedsFilterBarFilledPreference.fromPreferences(this),
feedsFilterBarPadding = FeedsFilterBarPaddingPreference.fromPreferences(this),
feedsFilterBarTonalElevation = FeedsFilterBarTonalElevationPreference.fromPreferences(this),
feedsTopBarTonalElevation = FeedsTopBarTonalElevationPreference.fromPreferences(this),
feedsGroupListExpand = FeedsGroupListExpandPreference.fromPreferences(this),
feedsGroupListTonalElevation = FeedsGroupListTonalElevationPreference.fromPreferences(this),
flowFilterBarStyle = FlowFilterBarStylePreference.fromPreferences(this),
flowFilterBarFilled = FlowFilterBarFilledPreference.fromPreferences(this),
flowFilterBarPadding = FlowFilterBarPaddingPreference.fromPreferences(this),
flowFilterBarTonalElevation = FlowFilterBarTonalElevationPreference.fromPreferences(this),
flowTopBarTonalElevation = FlowTopBarTonalElevationPreference.fromPreferences(this),
flowArticleListFeedIcon = FlowArticleListFeedIconPreference.fromPreferences(this),
flowArticleListFeedName = FlowArticleListFeedNamePreference.fromPreferences(this),
flowArticleListImage = FlowArticleListImagePreference.fromPreferences(this),
flowArticleListDesc = FlowArticleListDescPreference.fromPreferences(this),
flowArticleListTime = FlowArticleListTimePreference.fromPreferences(this),
flowArticleListDateStickyHeader = FlowArticleListDateStickyHeaderPreference.fromPreferences(this),
flowArticleListTonalElevation = FlowArticleListTonalElevationPreference.fromPreferences(this),
)
}
@Composable
fun SettingsProvider(
content: @Composable () -> Unit,
) {
val context = LocalContext.current
val settings = remember {
context.dataStore.data.map {
Log.i("RLog", "AppTheme: ${it}")
it.toSettings()
}
}.collectAsStateValue(initial = Settings())
CompositionLocalProvider(
LocalThemeIndex provides settings.themeIndex,
LocalCustomPrimaryColor provides settings.customPrimaryColor,
LocalFeedsTopBarTonalElevation provides settings.feedsTopBarTonalElevation,
LocalFeedsGroupListExpand provides settings.feedsGroupListExpand,
LocalFeedsGroupListTonalElevation provides settings.feedsGroupListTonalElevation,
LocalFeedsFilterBarStyle provides settings.feedsFilterBarStyle,
LocalFeedsFilterBarFilled provides settings.feedsFilterBarFilled,
LocalFeedsFilterBarPadding provides settings.feedsFilterBarPadding,
LocalFeedsFilterBarTonalElevation provides settings.feedsFilterBarTonalElevation,
LocalFlowTopBarTonalElevation provides settings.flowTopBarTonalElevation,
LocalFlowArticleListFeedIcon provides settings.flowArticleListFeedIcon,
LocalFlowArticleListFeedName provides settings.flowArticleListFeedName,
LocalFlowArticleListImage provides settings.flowArticleListImage,
LocalFlowArticleListDesc provides settings.flowArticleListDesc,
LocalFlowArticleListTime provides settings.flowArticleListTime,
LocalFlowArticleListDateStickyHeader provides settings.flowArticleListDateStickyHeader,
LocalFlowArticleListTonalElevation provides settings.flowArticleListTonalElevation,
LocalFlowFilterBarStyle provides settings.flowFilterBarStyle,
LocalFlowFilterBarFilled provides settings.flowFilterBarFilled,
LocalFlowFilterBarPadding provides settings.flowFilterBarPadding,
LocalFlowFilterBarTonalElevation provides settings.flowFilterBarTonalElevation,
) {
content()
}
}
val LocalThemeIndex =
compositionLocalOf { ThemeIndexPreference.default }
val LocalCustomPrimaryColor =
compositionLocalOf { CustomPrimaryColorPreference.default }
val LocalFeedsFilterBarStyle =
compositionLocalOf<FeedsFilterBarStylePreference> { FeedsFilterBarStylePreference.default }
val LocalFeedsFilterBarFilled =
compositionLocalOf<FeedsFilterBarFilledPreference> { FeedsFilterBarFilledPreference.default }
val LocalFeedsFilterBarPadding =
compositionLocalOf { FeedsFilterBarPaddingPreference.default }
val LocalFeedsFilterBarTonalElevation =
compositionLocalOf<FeedsFilterBarTonalElevationPreference> { FeedsFilterBarTonalElevationPreference.default }
val LocalFeedsTopBarTonalElevation =
compositionLocalOf<FeedsTopBarTonalElevationPreference> { FeedsTopBarTonalElevationPreference.default }
val LocalFeedsGroupListExpand =
compositionLocalOf<FeedsGroupListExpandPreference> { FeedsGroupListExpandPreference.default }
val LocalFeedsGroupListTonalElevation =
compositionLocalOf<FeedsGroupListTonalElevationPreference> { FeedsGroupListTonalElevationPreference.default }
val LocalFlowFilterBarStyle =
compositionLocalOf<FlowFilterBarStylePreference> { FlowFilterBarStylePreference.default }
val LocalFlowFilterBarFilled =
compositionLocalOf<FlowFilterBarFilledPreference> { FlowFilterBarFilledPreference.default }
val LocalFlowFilterBarPadding =
compositionLocalOf { FlowFilterBarPaddingPreference.default }
val LocalFlowFilterBarTonalElevation =
compositionLocalOf<FlowFilterBarTonalElevationPreference> { FlowFilterBarTonalElevationPreference.default }
val LocalFlowTopBarTonalElevation =
compositionLocalOf<FlowTopBarTonalElevationPreference> { FlowTopBarTonalElevationPreference.default }
val LocalFlowArticleListFeedIcon =
compositionLocalOf<FlowArticleListFeedIconPreference> { FlowArticleListFeedIconPreference.default }
val LocalFlowArticleListFeedName =
compositionLocalOf<FlowArticleListFeedNamePreference> { FlowArticleListFeedNamePreference.default }
val LocalFlowArticleListImage =
compositionLocalOf<FlowArticleListImagePreference> { FlowArticleListImagePreference.default }
val LocalFlowArticleListDesc =
compositionLocalOf<FlowArticleListDescPreference> { FlowArticleListDescPreference.default }
val LocalFlowArticleListTime =
compositionLocalOf<FlowArticleListTimePreference> { FlowArticleListTimePreference.default }
val LocalFlowArticleListDateStickyHeader =
compositionLocalOf<FlowArticleListDateStickyHeaderPreference> { FlowArticleListDateStickyHeaderPreference.default }
val LocalFlowArticleListTonalElevation =
compositionLocalOf<FlowArticleListTonalElevationPreference> { FlowArticleListTonalElevationPreference.default }

View File

@ -0,0 +1,23 @@
package me.ash.reader.data.preference
import android.content.Context
import androidx.datastore.preferences.core.Preferences
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import me.ash.reader.ui.ext.DataStoreKeys
import me.ash.reader.ui.ext.dataStore
import me.ash.reader.ui.ext.put
object ThemeIndexPreference {
const val default = 5
fun put(context: Context, scope: CoroutineScope, value: Int) {
scope.launch(Dispatchers.IO) {
context.dataStore.put(DataStoreKeys.ThemeIndex, value)
}
}
fun fromPreferences(preferences: Preferences) =
preferences[DataStoreKeys.ThemeIndex.key] ?: default
}

View File

@ -45,8 +45,8 @@ fun Switch(
.alpha(if (enable) 1f else 0.5f), .alpha(if (enable) 1f else 0.5f),
shape = CircleShape, shape = CircleShape,
color = animateColorAsState( color = animateColorAsState(
if (activated) (tonalPalettes primary 40) onDark (tonalPalettes neutralVariant 50) if (activated) (tonalPalettes primary 40) onDark (tonalPalettes secondary 50)
else (tonalPalettes neutralVariant 50) onDark (tonalPalettes neutral 60) else (tonalPalettes neutralVariant 50) onDark (tonalPalettes neutral 30)
).value ).value
) { ) {
Box( Box(
@ -61,7 +61,7 @@ fun Switch(
shape = CircleShape, shape = CircleShape,
color = animateColorAsState( color = animateColorAsState(
if (activated) tonalPalettes primary 90 if (activated) tonalPalettes primary 90
else (tonalPalettes neutralVariant 70) onDark (tonalPalettes neutral 30) else (tonalPalettes neutralVariant 70) onDark (tonalPalettes neutral 60)
).value ).value
) {} ) {}
} }

View File

@ -0,0 +1,38 @@
package me.ash.reader.ui.component
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import me.ash.reader.R
@Composable
fun Tips(
modifier: Modifier = Modifier,
text: String,
) {
Column(
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 16.dp),
) {
Icon(
imageVector = Icons.Outlined.Info,
contentDescription = stringResource(R.string.tips_and_support),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = text,
style = MaterialTheme.typography.labelLarge.copy(fontWeight = FontWeight.Light),
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}

View File

@ -9,8 +9,16 @@ import kotlin.math.ln
fun ColorScheme.surfaceColorAtElevation( fun ColorScheme.surfaceColorAtElevation(
elevation: Dp, elevation: Dp,
color: Color = surface,
): Color = color.atElevation(surfaceTint, elevation)
fun Color.atElevation(
sourceColor: Color,
elevation: Dp,
): Color { ): Color {
if (elevation == 0.dp) return surface if (elevation == 0.dp) return this
val alpha = ((4.5f * ln(elevation.value + 1)) + 2f) / 100f return sourceColor.copy(alpha = elevation.alphaLN(constant = 4.5f)).compositeOver(this)
return primary.copy(alpha = alpha).compositeOver(surface)
} }
fun Dp.alphaLN(constant: Float = 1f, weight: Float = 0f): Float =
((constant * ln(value + 1) + weight) + 2f) / 100f

View File

@ -5,10 +5,12 @@ import android.util.Log
import androidx.datastore.core.DataStore import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.* import androidx.datastore.preferences.core.*
import androidx.datastore.preferences.preferencesDataStore import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.io.IOException import java.io.IOException
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings") val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
@ -31,10 +33,7 @@ val Context.currentAccountId: Int
get() = this.dataStore.get(DataStoreKeys.CurrentAccountId)!! get() = this.dataStore.get(DataStoreKeys.CurrentAccountId)!!
val Context.currentAccountType: Int val Context.currentAccountType: Int
get() = this.dataStore.get(DataStoreKeys.CurrentAccountType)!! get() = this.dataStore.get(DataStoreKeys.CurrentAccountType)!!
val Context.themeIndex: Int
get() = this.dataStore.get(DataStoreKeys.ThemeIndex) ?: 5
val Context.customPrimaryColor: String
get() = this.dataStore.get(DataStoreKeys.CustomPrimaryColor) ?: ""
val Context.initialPage: Int val Context.initialPage: Int
get() = this.dataStore.get(DataStoreKeys.InitialPage) ?: 0 get() = this.dataStore.get(DataStoreKeys.InitialPage) ?: 0
val Context.initialFilter: Int val Context.initialFilter: Int
@ -42,9 +41,11 @@ val Context.initialFilter: Int
suspend fun <T> DataStore<Preferences>.put(dataStoreKeys: DataStoreKeys<T>, value: T) { suspend fun <T> DataStore<Preferences>.put(dataStoreKeys: DataStoreKeys<T>, value: T) {
this.edit { this.edit {
withContext(Dispatchers.IO) {
it[dataStoreKeys.key] = value it[dataStoreKeys.key] = value
} }
} }
}
fun <T> DataStore<Preferences>.putBlocking(dataStoreKeys: DataStoreKeys<T>, value: T) { fun <T> DataStore<Preferences>.putBlocking(dataStoreKeys: DataStoreKeys<T>, value: T) {
runBlocking { runBlocking {
@ -129,6 +130,101 @@ sealed class DataStoreKeys<T> {
get() = stringPreferencesKey("customPrimaryColor") get() = stringPreferencesKey("customPrimaryColor")
} }
object FeedsFilterBarStyle : DataStoreKeys<Int>() {
override val key: Preferences.Key<Int>
get() = intPreferencesKey("feedsFilterBarStyle")
}
object FeedsFilterBarFilled : DataStoreKeys<Boolean>() {
override val key: Preferences.Key<Boolean>
get() = booleanPreferencesKey("feedsFilterBarFilled")
}
object FeedsFilterBarPadding : DataStoreKeys<Int>() {
override val key: Preferences.Key<Int>
get() = intPreferencesKey("feedsFilterBarPadding")
}
object FeedsFilterBarTonalElevation : DataStoreKeys<Int>() {
override val key: Preferences.Key<Int>
get() = intPreferencesKey("feedsFilterBarTonalElevation")
}
object FeedsTopBarTonalElevation : DataStoreKeys<Int>() {
override val key: Preferences.Key<Int>
get() = intPreferencesKey("feedsTopBarTonalElevation")
}
object FeedsGroupListExpand : DataStoreKeys<Boolean>() {
override val key: Preferences.Key<Boolean>
get() = booleanPreferencesKey("feedsGroupListExpand")
}
object FeedsGroupListTonalElevation : DataStoreKeys<Int>() {
override val key: Preferences.Key<Int>
get() = intPreferencesKey("feedsGroupListTonalElevation")
}
object FlowFilterBarStyle : DataStoreKeys<Int>() {
override val key: Preferences.Key<Int>
get() = intPreferencesKey("flowFilterBarStyle")
}
object FlowFilterBarFilled : DataStoreKeys<Boolean>() {
override val key: Preferences.Key<Boolean>
get() = booleanPreferencesKey("flowFilterBarFilled")
}
object FlowFilterBarPadding : DataStoreKeys<Int>() {
override val key: Preferences.Key<Int>
get() = intPreferencesKey("flowFilterBarPadding")
}
object FlowFilterBarTonalElevation : DataStoreKeys<Int>() {
override val key: Preferences.Key<Int>
get() = intPreferencesKey("flowFilterBarTonalElevation")
}
object FlowTopBarTonalElevation : DataStoreKeys<Int>() {
override val key: Preferences.Key<Int>
get() = intPreferencesKey("flowTopBarTonalElevation")
}
object FlowArticleListFeedIcon : DataStoreKeys<Boolean>() {
override val key: Preferences.Key<Boolean>
get() = booleanPreferencesKey("flowArticleListFeedIcon")
}
object FlowArticleListFeedName : DataStoreKeys<Boolean>() {
override val key: Preferences.Key<Boolean>
get() = booleanPreferencesKey("flowArticleListFeedName")
}
object FlowArticleListImage : DataStoreKeys<Boolean>() {
override val key: Preferences.Key<Boolean>
get() = booleanPreferencesKey("flowArticleListImage")
}
object FlowArticleListDesc : DataStoreKeys<Boolean>() {
override val key: Preferences.Key<Boolean>
get() = booleanPreferencesKey("flowArticleListDesc")
}
object FlowArticleListTime : DataStoreKeys<Boolean>() {
override val key: Preferences.Key<Boolean>
get() = booleanPreferencesKey("flowArticleListTime")
}
object FlowArticleListDateStickyHeader : DataStoreKeys<Boolean>() {
override val key: Preferences.Key<Boolean>
get() = booleanPreferencesKey("flowArticleListDateStickyHeader")
}
object FlowArticleListTonalElevation : DataStoreKeys<Int>() {
override val key: Preferences.Key<Int>
get() = intPreferencesKey("flowArticleListTonalElevation")
}
object InitialPage : DataStoreKeys<Int>() { object InitialPage : DataStoreKeys<Int>() {
override val key: Preferences.Key<Int> override val key: Preferences.Key<Int>
get() = intPreferencesKey("initialPage") get() = intPreferencesKey("initialPage")

View File

@ -3,6 +3,7 @@ package me.ash.reader.ui.ext
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
@ -10,3 +11,9 @@ import kotlin.coroutines.CoroutineContext
fun <T> StateFlow<T>.collectAsStateValue( fun <T> StateFlow<T>.collectAsStateValue(
context: CoroutineContext = Dispatchers.Default context: CoroutineContext = Dispatchers.Default
): T = collectAsState(context).value ): T = collectAsState(context).value
@Composable
fun <T : R, R> Flow<T>.collectAsStateValue(
initial: R,
context: CoroutineContext = Dispatchers.Default
): R = collectAsState(initial, context).value

View File

@ -22,6 +22,8 @@ import me.ash.reader.ui.page.home.flow.FlowPage
import me.ash.reader.ui.page.home.read.ReadPage import me.ash.reader.ui.page.home.read.ReadPage
import me.ash.reader.ui.page.settings.SettingsPage import me.ash.reader.ui.page.settings.SettingsPage
import me.ash.reader.ui.page.settings.color.ColorAndStyle import me.ash.reader.ui.page.settings.color.ColorAndStyle
import me.ash.reader.ui.page.settings.color.feeds.FeedsPageStyle
import me.ash.reader.ui.page.settings.color.flow.FlowPageStyle
import me.ash.reader.ui.page.settings.interaction.Interaction import me.ash.reader.ui.page.settings.interaction.Interaction
import me.ash.reader.ui.page.settings.tips.TipsAndSupport import me.ash.reader.ui.page.settings.tips.TipsAndSupport
import me.ash.reader.ui.page.startup.StartupPage import me.ash.reader.ui.page.startup.StartupPage
@ -34,6 +36,7 @@ fun HomeEntry(
homeViewModel: HomeViewModel = hiltViewModel(), homeViewModel: HomeViewModel = hiltViewModel(),
) { ) {
val context = LocalContext.current val context = LocalContext.current
val viewState = homeViewModel.viewState.collectAsStateValue() val viewState = homeViewModel.viewState.collectAsStateValue()
val filterState = homeViewModel.filterState.collectAsStateValue() val filterState = homeViewModel.filterState.collectAsStateValue()
val pagingItems = viewState.pagingData.collectAsLazyPagingItems() val pagingItems = viewState.pagingData.collectAsLazyPagingItems()
@ -97,9 +100,12 @@ fun HomeEntry(
navController = navController, navController = navController,
startDestination = if (context.isFirstLaunch) RouteName.STARTUP else RouteName.FEEDS, startDestination = if (context.isFirstLaunch) RouteName.STARTUP else RouteName.FEEDS,
) { ) {
// Startup
animatedComposable(route = RouteName.STARTUP) { animatedComposable(route = RouteName.STARTUP) {
StartupPage(navController) StartupPage(navController)
} }
// Home
animatedComposable(route = RouteName.FEEDS) { animatedComposable(route = RouteName.FEEDS) {
FeedsPage(navController = navController, homeViewModel = homeViewModel) FeedsPage(navController = navController, homeViewModel = homeViewModel)
} }
@ -113,15 +119,29 @@ fun HomeEntry(
animatedComposable(route = "${RouteName.READING}/{articleId}") { animatedComposable(route = "${RouteName.READING}/{articleId}") {
ReadPage(navController = navController) ReadPage(navController = navController)
} }
// Settings
animatedComposable(route = RouteName.SETTINGS) { animatedComposable(route = RouteName.SETTINGS) {
SettingsPage(navController) SettingsPage(navController)
} }
// Color & Style
animatedComposable(route = RouteName.COLOR_AND_STYLE) { animatedComposable(route = RouteName.COLOR_AND_STYLE) {
ColorAndStyle(navController) ColorAndStyle(navController)
} }
animatedComposable(route = RouteName.FEEDS_PAGE_STYLE) {
FeedsPageStyle(navController)
}
animatedComposable(route = RouteName.FLOW_PAGE_STYLE) {
FlowPageStyle(navController)
}
// Interaction
animatedComposable(route = RouteName.INTERACTION) { animatedComposable(route = RouteName.INTERACTION) {
Interaction(navController) Interaction(navController)
} }
// Tips & Support
animatedComposable(route = RouteName.TIPS_AND_SUPPORT) { animatedComposable(route = RouteName.TIPS_AND_SUPPORT) {
TipsAndSupport(navController) TipsAndSupport(navController)
} }

View File

@ -1,12 +1,25 @@
package me.ash.reader.ui.page.common package me.ash.reader.ui.page.common
object RouteName { object RouteName {
// Startup
const val STARTUP = "startup" const val STARTUP = "startup"
// Home
const val FEEDS = "feeds" const val FEEDS = "feeds"
const val FLOW = "flow" const val FLOW = "flow"
const val READING = "reading" const val READING = "reading"
// Settings
const val SETTINGS = "settings" const val SETTINGS = "settings"
// Color & Style
const val COLOR_AND_STYLE = "color_and_style" const val COLOR_AND_STYLE = "color_and_style"
const val FEEDS_PAGE_STYLE = "feeds_page_style"
const val FLOW_PAGE_STYLE = "flow_page_style"
// Interaction
const val INTERACTION = "interaction" const val INTERACTION = "interaction"
// Tips & Support
const val TIPS_AND_SUPPORT = "tips_and_support" const val TIPS_AND_SUPPORT = "tips_and_support"
} }

View File

@ -1,54 +1,69 @@
package me.ash.reader.ui.page.home package me.ash.reader.ui.page.home
import android.view.SoundEffectConstants import android.view.SoundEffectConstants
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.width
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.zIndex
import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.ExperimentalPagerApi
import me.ash.reader.data.entity.Filter import me.ash.reader.data.entity.Filter
import me.ash.reader.data.preference.FlowFilterBarStylePreference
import me.ash.reader.ui.ext.getName import me.ash.reader.ui.ext.getName
import me.ash.reader.ui.theme.palette.alwaysLight import me.ash.reader.ui.theme.palette.onDark
@OptIn(ExperimentalPagerApi::class) @OptIn(ExperimentalPagerApi::class)
@Composable @Composable
fun FilterBar( fun FilterBar(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
filter: Filter, filter: Filter,
filterBarStyle: Int,
filterBarFilled: Boolean,
filterBarPadding: Dp,
filterBarTonalElevation: Dp,
filterOnClick: (Filter) -> Unit = {}, filterOnClick: (Filter) -> Unit = {},
) { ) {
val view = LocalView.current val view = LocalView.current
Box(
modifier = Modifier.height(60.dp)
) {
Divider(
modifier = Modifier
.fillMaxWidth()
.height(1.dp)
.zIndex(1f),
color = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.24f)
)
NavigationBar( NavigationBar(
modifier = Modifier.fillMaxSize(), tonalElevation = filterBarTonalElevation,
tonalElevation = 0.dp,
) { ) {
Spacer(modifier = Modifier.width(60.dp)) Spacer(modifier = Modifier.width(filterBarPadding))
listOf( listOf(
Filter.Starred, Filter.Starred,
Filter.Unread, Filter.Unread,
Filter.All, Filter.All,
).forEach { item -> ).forEach { item ->
NavigationBarItem( NavigationBarItem(
// modifier = Modifier.height(60.dp),
alwaysShowLabel = when (filterBarStyle) {
FlowFilterBarStylePreference.Icon.value -> false
FlowFilterBarStylePreference.IconLabel.value -> true
FlowFilterBarStylePreference.IconLabelOnlySelected.value -> false
else -> false
},
icon = { icon = {
Icon( Icon(
imageVector = item.icon, imageVector = if (filter == item && filterBarFilled) {
item.iconFilled
} else {
item.iconOutline
},
contentDescription = item.getName() contentDescription = item.getName()
) )
}, },
label = if (filterBarStyle == FlowFilterBarStylePreference.Icon.value) {
null
} else {
{
Text(
text = item.getName(),
style = MaterialTheme.typography.labelLarge
)
}
},
selected = filter == item, selected = filter == item,
onClick = { onClick = {
// view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) // view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
@ -56,15 +71,14 @@ fun FilterBar(
filterOnClick(item) filterOnClick(item)
}, },
colors = NavigationBarItemDefaults.colors( colors = NavigationBarItemDefaults.colors(
selectedIconColor = MaterialTheme.colorScheme.onSecondaryContainer alwaysLight true, // selectedIconColor = MaterialTheme.colorScheme.onSecondaryContainer alwaysLight true,
// unselectedIconColor = MaterialTheme.colorScheme.outline, // unselectedIconColor = MaterialTheme.colorScheme.outline,
selectedTextColor = MaterialTheme.colorScheme.onSurface alwaysLight true, // selectedTextColor = MaterialTheme.colorScheme.onSurface alwaysLight true,
// unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant, // unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant,
indicatorColor = MaterialTheme.colorScheme.primaryContainer alwaysLight true, indicatorColor = MaterialTheme.colorScheme.primaryContainer onDark MaterialTheme.colorScheme.secondaryContainer,
) )
) )
} }
Spacer(modifier = Modifier.width(60.dp)) Spacer(modifier = Modifier.width(filterBarPadding))
}
} }
} }

View File

@ -62,7 +62,7 @@ class HomeViewModel @Inject constructor(
private fun fetchArticles() { private fun fetchArticles() {
_viewState.update { _viewState.update {
it.copy( it.copy(
pagingData = Pager(PagingConfig(pageSize = 10)) { pagingData = Pager(PagingConfig(pageSize = 15)) {
if (_viewState.value.searchContent.isNotBlank()) { if (_viewState.value.searchContent.isNotBlank()) {
rssRepository.get().searchArticles( rssRepository.get().searchArticles(
content = _viewState.value.searchContent.trim(), content = _viewState.value.searchContent.trim(),

View File

@ -16,11 +16,13 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import me.ash.reader.data.entity.Feed import me.ash.reader.data.entity.Feed
import me.ash.reader.ui.page.home.feeds.option.feed.FeedOptionViewAction import me.ash.reader.ui.page.home.feeds.option.feed.FeedOptionViewAction
import me.ash.reader.ui.page.home.feeds.option.feed.FeedOptionViewModel import me.ash.reader.ui.page.home.feeds.option.feed.FeedOptionViewModel
import kotlin.math.ln
@OptIn( @OptIn(
androidx.compose.foundation.ExperimentalFoundationApi::class, androidx.compose.foundation.ExperimentalFoundationApi::class,
@ -31,6 +33,7 @@ fun FeedItem(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
feed: Feed, feed: Feed,
feedOptionViewModel: FeedOptionViewModel = hiltViewModel(), feedOptionViewModel: FeedOptionViewModel = hiltViewModel(),
tonalElevation: Dp,
onClick: () -> Unit = {}, onClick: () -> Unit = {},
) { ) {
val view = LocalView.current val view = LocalView.current
@ -76,9 +79,10 @@ fun FeedItem(
) )
} }
if (feed.important ?: 0 != 0) { if (feed.important ?: 0 != 0) {
Row() {
Badge( Badge(
containerColor = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.24f), containerColor = MaterialTheme.colorScheme.surfaceTint.copy(
alpha = (ln(tonalElevation.value + 1.4f) + 2f) / 100f
),
contentColor = MaterialTheme.colorScheme.outline, contentColor = MaterialTheme.colorScheme.outline,
content = { content = {
Text( Text(
@ -91,4 +95,3 @@ fun FeedItem(
} }
} }
} }
}

View File

@ -29,6 +29,7 @@ import androidx.navigation.NavHostController
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import me.ash.reader.R import me.ash.reader.R
import me.ash.reader.data.entity.toVersion import me.ash.reader.data.entity.toVersion
import me.ash.reader.data.preference.*
import me.ash.reader.data.repository.SyncWorker.Companion.getIsSyncing import me.ash.reader.data.repository.SyncWorker.Companion.getIsSyncing
import me.ash.reader.ui.component.Banner import me.ash.reader.ui.component.Banner
import me.ash.reader.ui.component.DisplayText import me.ash.reader.ui.component.DisplayText
@ -45,6 +46,7 @@ import me.ash.reader.ui.page.home.feeds.option.group.GroupOptionDrawer
import me.ash.reader.ui.page.home.feeds.subscribe.SubscribeDialog import me.ash.reader.ui.page.home.feeds.subscribe.SubscribeDialog
import me.ash.reader.ui.page.home.feeds.subscribe.SubscribeViewAction import me.ash.reader.ui.page.home.feeds.subscribe.SubscribeViewAction
import me.ash.reader.ui.page.home.feeds.subscribe.SubscribeViewModel import me.ash.reader.ui.page.home.feeds.subscribe.SubscribeViewModel
import me.ash.reader.ui.theme.palette.onDark
@SuppressLint("FlowOperatorInvokedInComposition") @SuppressLint("FlowOperatorInvokedInComposition")
@OptIn( @OptIn(
@ -59,6 +61,14 @@ fun FeedsPage(
homeViewModel: HomeViewModel, homeViewModel: HomeViewModel,
) { ) {
val context = LocalContext.current val context = LocalContext.current
val topBarTonalElevation = LocalFeedsTopBarTonalElevation.current
val groupListTonalElevation = LocalFeedsGroupListTonalElevation.current
val groupListExpand = LocalFeedsGroupListExpand.current
val filterBarStyle = LocalFeedsFilterBarStyle.current
val filterBarFilled = LocalFeedsFilterBarFilled.current
val filterBarPadding = LocalFeedsFilterBarPadding.current
val filterBarTonalElevation = LocalFeedsFilterBarTonalElevation.current
val feedsViewState = feedsViewModel.viewState.collectAsStateValue() val feedsViewState = feedsViewModel.viewState.collectAsStateValue()
val filterState = homeViewModel.filterState.collectAsStateValue() val filterState = homeViewModel.filterState.collectAsStateValue()
@ -117,11 +127,19 @@ fun FeedsPage(
Scaffold( Scaffold(
modifier = Modifier modifier = Modifier
.background(MaterialTheme.colorScheme.surface) .background(MaterialTheme.colorScheme.surfaceColorAtElevation(topBarTonalElevation.value.dp))
.statusBarsPadding() .statusBarsPadding()
.navigationBarsPadding(), .navigationBarsPadding(),
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(
groupListTonalElevation.value.dp
) onDark MaterialTheme.colorScheme.surface,
topBar = { topBar = {
SmallTopAppBar( SmallTopAppBar(
colors = TopAppBarDefaults.smallTopAppBarColors(
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(
topBarTonalElevation.value.dp
),
),
title = {}, title = {},
navigationIcon = { navigationIcon = {
FeedbackIconButton( FeedbackIconButton(
@ -174,7 +192,7 @@ fun FeedsPage(
Banner( Banner(
title = filterState.filter.getName(), title = filterState.filter.getName(),
desc = feedsViewState.importantCount, desc = feedsViewState.importantCount,
icon = filterState.filter.icon, icon = filterState.filter.iconOutline,
action = { action = {
Icon( Icon(
imageVector = Icons.Outlined.KeyboardArrowRight, imageVector = Icons.Outlined.KeyboardArrowRight,
@ -204,6 +222,8 @@ fun FeedsPage(
// Crossfade(targetState = groupWithFeed) { groupWithFeed -> // Crossfade(targetState = groupWithFeed) { groupWithFeed ->
Column { Column {
GroupItem( GroupItem(
isExpanded = groupListExpand.value,
tonalElevation = groupListTonalElevation.value.dp,
group = groupWithFeed.group, group = groupWithFeed.group,
feeds = groupWithFeed.feeds, feeds = groupWithFeed.feeds,
groupOnClick = { groupOnClick = {
@ -241,19 +261,19 @@ fun FeedsPage(
}, },
bottomBar = { bottomBar = {
FilterBar( FilterBar(
modifier = Modifier
.height(60.dp)
.fillMaxWidth(),
filter = filterState.filter, filter = filterState.filter,
filterOnClick = { filterBarStyle = filterBarStyle.value,
filterBarFilled = filterBarFilled.value,
filterBarPadding = filterBarPadding.dp,
filterBarTonalElevation = filterBarTonalElevation.value.dp,
) {
filterChange( filterChange(
navController = navController, navController = navController,
homeViewModel = homeViewModel, homeViewModel = homeViewModel,
filterState = filterState.copy(filter = it), filterState = filterState.copy(filter = it),
isNavigate = false, isNavigate = false,
) )
}, }
)
} }
) )

View File

@ -22,11 +22,13 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import me.ash.reader.R import me.ash.reader.R
import me.ash.reader.data.entity.Feed import me.ash.reader.data.entity.Feed
import me.ash.reader.data.entity.Group import me.ash.reader.data.entity.Group
import me.ash.reader.ui.ext.alphaLN
import me.ash.reader.ui.page.home.feeds.option.group.GroupOptionViewAction import me.ash.reader.ui.page.home.feeds.option.group.GroupOptionViewAction
import me.ash.reader.ui.page.home.feeds.option.group.GroupOptionViewModel import me.ash.reader.ui.page.home.feeds.option.group.GroupOptionViewModel
@ -34,6 +36,7 @@ import me.ash.reader.ui.page.home.feeds.option.group.GroupOptionViewModel
@Composable @Composable
fun GroupItem( fun GroupItem(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
tonalElevation: Dp,
group: Group, group: Group,
feeds: List<Feed>, feeds: List<Feed>,
isExpanded: Boolean = true, isExpanded: Boolean = true,
@ -50,7 +53,9 @@ fun GroupItem(
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
.clip(RoundedCornerShape(32.dp)) .clip(RoundedCornerShape(32.dp))
.background(MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.14f)) .background(
MaterialTheme.colorScheme.secondary.copy(alpha = tonalElevation.alphaLN(weight = 1.2f))
)
.combinedClickable( .combinedClickable(
onClick = { onClick = {
groupOnClick() groupOnClick()
@ -82,7 +87,11 @@ fun GroupItem(
.padding(end = 20.dp) .padding(end = 20.dp)
.size(24.dp) .size(24.dp)
.clip(CircleShape) .clip(CircleShape)
.background(MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.24f)) .background(
MaterialTheme.colorScheme.surfaceTint.copy(
alpha = tonalElevation.alphaLN(weight = 1.4f)
)
)
.clickable { .clickable {
expanded = !expanded expanded = !expanded
}, },
@ -107,6 +116,7 @@ fun GroupItem(
FeedItem( FeedItem(
modifier = Modifier.padding(horizontal = 20.dp), modifier = Modifier.padding(horizontal = 20.dp),
feed = feed, feed = feed,
tonalElevation = tonalElevation,
) { ) {
feedOnClick(feed) feedOnClick(feed)
} }

View File

@ -21,6 +21,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import me.ash.reader.R import me.ash.reader.R
import me.ash.reader.data.entity.ArticleWithFeed import me.ash.reader.data.entity.ArticleWithFeed
import me.ash.reader.data.preference.*
import me.ash.reader.ui.ext.formatAsString import me.ash.reader.ui.ext.formatAsString
@Composable @Composable
@ -30,6 +31,11 @@ fun ArticleItem(
onClick: (ArticleWithFeed) -> Unit = {}, onClick: (ArticleWithFeed) -> Unit = {},
) { ) {
val context = LocalContext.current val context = LocalContext.current
val articleListFeedIcon = LocalFlowArticleListFeedIcon.current
val articleListFeedName = LocalFlowArticleListFeedName.current
val articleListImage = LocalFlowArticleListImage.current
val articleListDesc = LocalFlowArticleListDesc.current
val articleListDate = LocalFlowArticleListTime.current
Column( Column(
modifier = Modifier modifier = Modifier
@ -44,20 +50,28 @@ fun ArticleItem(
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
// Feed name
if (articleListFeedName.value) {
Text( Text(
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.padding(start = 30.dp), .padding(start = if (articleListFeedIcon.value) 30.dp else 0.dp),
text = articleWithFeed.feed.name, text = articleWithFeed.feed.name,
color = MaterialTheme.colorScheme.tertiary, color = MaterialTheme.colorScheme.tertiary,
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
) )
}
if (articleListDate.value) {
Row( Row(
modifier = Modifier.padding(start = 6.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
if (!articleListFeedName.value) {
Spacer(Modifier.width(if (articleListFeedIcon.value) 30.dp else 0.dp))
}
// Starred
if (articleWithFeed.article.isStarred) { if (articleWithFeed.article.isStarred) {
Icon( Icon(
modifier = Modifier modifier = Modifier
@ -68,6 +82,8 @@ fun ArticleItem(
tint = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f), tint = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
) )
} }
// Date
Text( Text(
text = articleWithFeed.article.date.formatAsString( text = articleWithFeed.article.date.formatAsString(
context, context,
@ -78,9 +94,12 @@ fun ArticleItem(
) )
} }
} }
}
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) { ) {
// Feed icon
if (articleListFeedIcon.value) {
Row( Row(
modifier = Modifier modifier = Modifier
.size(20.dp) .size(20.dp)
@ -88,16 +107,21 @@ fun ArticleItem(
.background(MaterialTheme.colorScheme.outline.copy(alpha = 0.2f)) .background(MaterialTheme.colorScheme.outline.copy(alpha = 0.2f))
) {} ) {}
Spacer(modifier = Modifier.width(10.dp)) Spacer(modifier = Modifier.width(10.dp))
}
// Article
Column( Column(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) { ) {
// Title
Text( Text(
text = articleWithFeed.article.title, text = articleWithFeed.article.title,
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
maxLines = 2, maxLines = if (articleListDesc.value) 2 else 4,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
) )
// Description
if (articleListDesc.value) {
Text( Text(
text = articleWithFeed.article.shortDescription, text = articleWithFeed.article.shortDescription,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
@ -109,3 +133,4 @@ fun ArticleItem(
} }
} }
} }
}

View File

@ -13,6 +13,9 @@ import me.ash.reader.data.entity.ArticleWithFeed
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
fun LazyListScope.ArticleList( fun LazyListScope.ArticleList(
pagingItems: LazyPagingItems<FlowItemView>, pagingItems: LazyPagingItems<FlowItemView>,
articleListFeedIcon: Boolean,
articleListDateStickyHeader: Boolean,
articleListTonalElevation: Int,
onClick: (ArticleWithFeed) -> Unit = {}, onClick: (ArticleWithFeed) -> Unit = {},
) { ) {
for (index in 0 until pagingItems.itemCount) { for (index in 0 until pagingItems.itemCount) {
@ -29,8 +32,14 @@ fun LazyListScope.ArticleList(
is FlowItemView.Date -> { is FlowItemView.Date -> {
val separator = pagingItems[index] as FlowItemView.Date val separator = pagingItems[index] as FlowItemView.Date
if (separator.showSpacer) item { Spacer(modifier = Modifier.height(40.dp)) } if (separator.showSpacer) item { Spacer(modifier = Modifier.height(40.dp)) }
stickyHeader { if (articleListDateStickyHeader) {
StickyHeader(separator.date) stickyHeader(key = separator.date) {
StickyHeader(separator.date, articleListFeedIcon, articleListTonalElevation)
}
} else {
item(key = separator.date) {
StickyHeader(separator.date, articleListFeedIcon, articleListTonalElevation)
}
} }
} }
else -> {} else -> {}

View File

@ -11,10 +11,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.ArrowBack import androidx.compose.material.icons.rounded.ArrowBack
import androidx.compose.material.icons.rounded.DoneAll import androidx.compose.material.icons.rounded.DoneAll
import androidx.compose.material.icons.rounded.Search import androidx.compose.material.icons.rounded.Search
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.*
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SmallTopAppBar
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
@ -29,17 +26,20 @@ import androidx.paging.compose.LazyPagingItems
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.ash.reader.R import me.ash.reader.R
import me.ash.reader.data.preference.*
import me.ash.reader.data.repository.SyncWorker.Companion.getIsSyncing import me.ash.reader.data.repository.SyncWorker.Companion.getIsSyncing
import me.ash.reader.ui.component.DisplayText import me.ash.reader.ui.component.DisplayText
import me.ash.reader.ui.component.FeedbackIconButton import me.ash.reader.ui.component.FeedbackIconButton
import me.ash.reader.ui.component.SwipeRefresh import me.ash.reader.ui.component.SwipeRefresh
import me.ash.reader.ui.ext.collectAsStateValue import me.ash.reader.ui.ext.collectAsStateValue
import me.ash.reader.ui.ext.getName import me.ash.reader.ui.ext.getName
import me.ash.reader.ui.ext.surfaceColorAtElevation
import me.ash.reader.ui.page.common.RouteName import me.ash.reader.ui.page.common.RouteName
import me.ash.reader.ui.page.home.FilterBar import me.ash.reader.ui.page.home.FilterBar
import me.ash.reader.ui.page.home.FilterState import me.ash.reader.ui.page.home.FilterState
import me.ash.reader.ui.page.home.HomeViewAction import me.ash.reader.ui.page.home.HomeViewAction
import me.ash.reader.ui.page.home.HomeViewModel import me.ash.reader.ui.page.home.HomeViewModel
import me.ash.reader.ui.theme.palette.onDark
@OptIn( @OptIn(
ExperimentalMaterial3Api::class, ExperimentalMaterial3Api::class,
@ -55,6 +55,14 @@ fun FlowPage(
pagingItems: LazyPagingItems<FlowItemView>, pagingItems: LazyPagingItems<FlowItemView>,
) { ) {
val keyboardController = LocalSoftwareKeyboardController.current val keyboardController = LocalSoftwareKeyboardController.current
val topBarTonalElevation = LocalFlowTopBarTonalElevation.current
val articleListTonalElevation = LocalFlowArticleListTonalElevation.current
val articleListFeedIcon = LocalFlowArticleListFeedIcon.current
val articleListDateStickyHeader = LocalFlowArticleListDateStickyHeader.current
val filterBarStyle = LocalFlowFilterBarStyle.current
val filterBarFilled = LocalFlowFilterBarFilled.current
val filterBarPadding = LocalFlowFilterBarPadding.current
val filterBarTonalElevation = LocalFlowFilterBarTonalElevation.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
@ -100,12 +108,20 @@ fun FlowPage(
Scaffold( Scaffold(
modifier = Modifier modifier = Modifier
.background(MaterialTheme.colorScheme.surface) .background(MaterialTheme.colorScheme.surfaceColorAtElevation(topBarTonalElevation.value.dp))
.statusBarsPadding() .statusBarsPadding()
.navigationBarsPadding(), .navigationBarsPadding(),
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(
articleListTonalElevation.value.dp
) onDark MaterialTheme.colorScheme.surface,
topBar = { topBar = {
SmallTopAppBar( SmallTopAppBar(
title = {}, title = {},
colors = TopAppBarDefaults.smallTopAppBarColors(
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(
topBarTonalElevation.value.dp
),
),
navigationIcon = { navigationIcon = {
FeedbackIconButton( FeedbackIconButton(
imageVector = Icons.Rounded.ArrowBack, imageVector = Icons.Rounded.ArrowBack,
@ -182,7 +198,7 @@ fun FlowPage(
state = listState, state = listState,
) { ) {
item { item {
DisplayTextHeader(filterState, isSyncing) DisplayTextHeader(filterState, isSyncing, articleListFeedIcon.value)
AnimatedVisibility( AnimatedVisibility(
visible = markAsRead, visible = markAsRead,
enter = fadeIn() + expandVertically(), enter = fadeIn() + expandVertically(),
@ -244,6 +260,9 @@ fun FlowPage(
} }
ArticleList( ArticleList(
pagingItems = pagingItems, pagingItems = pagingItems,
articleListFeedIcon = articleListFeedIcon.value,
articleListDateStickyHeader = articleListDateStickyHeader.value,
articleListTonalElevation = articleListTonalElevation.value,
) { ) {
onSearch = false onSearch = false
navController.navigate("${RouteName.READING}/${it.article.id}") { navController.navigate("${RouteName.READING}/${it.article.id}") {
@ -261,16 +280,16 @@ fun FlowPage(
}, },
bottomBar = { bottomBar = {
FilterBar( FilterBar(
modifier = Modifier
.height(60.dp)
.fillMaxWidth(),
filter = filterState.filter, filter = filterState.filter,
filterOnClick = { filterBarStyle = filterBarStyle.value,
filterBarFilled = filterBarFilled.value,
filterBarPadding = filterBarPadding.dp,
filterBarTonalElevation = filterBarTonalElevation.value.dp,
) {
flowViewModel.dispatch(FlowViewAction.ScrollToItem(0)) flowViewModel.dispatch(FlowViewAction.ScrollToItem(0))
homeViewModel.dispatch(HomeViewAction.ChangeFilter(filterState.copy(filter = it))) homeViewModel.dispatch(HomeViewAction.ChangeFilter(filterState.copy(filter = it)))
homeViewModel.dispatch(HomeViewAction.FetchArticles) homeViewModel.dispatch(HomeViewAction.FetchArticles)
}, }
)
} }
) )
} }
@ -278,10 +297,11 @@ fun FlowPage(
@Composable @Composable
private fun DisplayTextHeader( private fun DisplayTextHeader(
filterState: FilterState, filterState: FilterState,
isSyncing: Boolean isSyncing: Boolean,
articleListFeedIcon: Boolean,
) { ) {
DisplayText( DisplayText(
modifier = Modifier.padding(start = 30.dp), modifier = Modifier.padding(start = if (articleListFeedIcon) 30.dp else 0.dp),
text = when { text = when {
filterState.group != null -> filterState.group.name filterState.group != null -> filterState.group.name
filterState.feed != null -> filterState.feed.name filterState.feed != null -> filterState.feed.name

View File

@ -10,18 +10,29 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import me.ash.reader.ui.ext.surfaceColorAtElevation
import me.ash.reader.ui.theme.palette.onDark
@Composable @Composable
fun StickyHeader(currentItemDay: String) { fun StickyHeader(
currentItemDay: String,
articleListFeedIcon: Boolean,
articleListTonalElevation: Int,
) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.background(MaterialTheme.colorScheme.surface), .background(
MaterialTheme.colorScheme.surfaceColorAtElevation(articleListTonalElevation.dp)
onDark MaterialTheme.colorScheme.surface
),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Text( Text(
modifier = Modifier modifier = Modifier.padding(
.padding(start = if (true) 54.dp else 24.dp, bottom = 4.dp), start = if (articleListFeedIcon) 54.dp else 24.dp,
bottom = 4.dp
),
text = currentItemDay, text = currentItemDay,
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
style = MaterialTheme.typography.labelLarge, style = MaterialTheme.typography.labelLarge,

View File

@ -1,6 +1,7 @@
package me.ash.reader.ui.page.settings.color package me.ash.reader.ui.page.settings.color
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context
import android.os.Build import android.os.Build
import androidx.compose.animation.* import androidx.compose.animation.*
import androidx.compose.foundation.background import androidx.compose.foundation.background
@ -24,12 +25,13 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import me.ash.reader.R import me.ash.reader.R
import me.ash.reader.data.preference.CustomPrimaryColorPreference
import me.ash.reader.data.preference.LocalCustomPrimaryColor
import me.ash.reader.data.preference.LocalThemeIndex
import me.ash.reader.data.preference.ThemeIndexPreference
import me.ash.reader.ui.component.* import me.ash.reader.ui.component.*
import me.ash.reader.ui.ext.* import me.ash.reader.ui.page.common.RouteName
import me.ash.reader.ui.page.settings.SettingItem import me.ash.reader.ui.page.settings.SettingItem
import me.ash.reader.ui.svg.PALETTE import me.ash.reader.ui.svg.PALETTE
import me.ash.reader.ui.svg.SVGString import me.ash.reader.ui.svg.SVGString
@ -46,8 +48,11 @@ fun ColorAndStyle(
) { ) {
val context = LocalContext.current val context = LocalContext.current
val useDarkTheme = LocalUseDarkTheme.current val useDarkTheme = LocalUseDarkTheme.current
val themeIndex = LocalThemeIndex.current
val customPrimaryColor = LocalCustomPrimaryColor.current
val wallpaperTonalPalettes = extractTonalPalettesFromUserWallpaper() val wallpaperTonalPalettes = extractTonalPalettesFromUserWallpaper()
var radioButtonSelected by remember { mutableStateOf(if (context.themeIndex > 4) 0 else 1) } var radioButtonSelected by remember { mutableStateOf(if (themeIndex > 4) 0 else 1) }
Scaffold( Scaffold(
modifier = Modifier modifier = Modifier
@ -111,6 +116,7 @@ fun ColorAndStyle(
onClick = {}, onClick = {},
) { ) {
Palettes( Palettes(
context = context,
palettes = wallpaperTonalPalettes.run { palettes = wallpaperTonalPalettes.run {
if (this.size > 5) { if (this.size > 5) {
this.subList(5, wallpaperTonalPalettes.size) this.subList(5, wallpaperTonalPalettes.size)
@ -118,7 +124,9 @@ fun ColorAndStyle(
emptyList() emptyList()
} }
}, },
themeIndex = themeIndex,
themeIndexPrefix = 5, themeIndexPrefix = 5,
customPrimaryColor = customPrimaryColor,
) )
}, },
BlockRadioGroupButtonItem( BlockRadioGroupButtonItem(
@ -126,7 +134,10 @@ fun ColorAndStyle(
onClick = {}, onClick = {},
) { ) {
Palettes( Palettes(
palettes = wallpaperTonalPalettes.subList(0, 5) context = context,
themeIndex = themeIndex,
palettes = wallpaperTonalPalettes.subList(0, 5),
customPrimaryColor = customPrimaryColor,
) )
}, },
), ),
@ -162,13 +173,19 @@ fun ColorAndStyle(
) )
SettingItem( SettingItem(
title = stringResource(R.string.feeds_page), title = stringResource(R.string.feeds_page),
enable = false, onClick = {
onClick = {}, navController.navigate(RouteName.FEEDS_PAGE_STYLE) {
launchSingleTop = true
}
},
) {} ) {}
SettingItem( SettingItem(
title = stringResource(R.string.flow_page), title = stringResource(R.string.flow_page),
enable = false, onClick = {
onClick = {}, navController.navigate(RouteName.FLOW_PAGE_STYLE) {
launchSingleTop = true
}
},
) {} ) {}
SettingItem( SettingItem(
title = stringResource(R.string.reading_page), title = stringResource(R.string.reading_page),
@ -186,20 +203,16 @@ fun ColorAndStyle(
@Composable @Composable
fun Palettes( fun Palettes(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
context: Context,
palettes: List<TonalPalettes>, palettes: List<TonalPalettes>,
themeIndex: Int = 0,
themeIndexPrefix: Int = 0, themeIndexPrefix: Int = 0,
customPrimaryColor: String = "",
) { ) {
val context = LocalContext.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val themeIndex = context.dataStore.data
.map { it[DataStoreKeys.ThemeIndex.key] ?: 5 }
.collectAsState(initial = 5).value
val customPrimaryColor = context.dataStore.data
.map { it[DataStoreKeys.CustomPrimaryColor.key] ?: "" }
.collectAsState(initial = "").value
val tonalPalettes = customPrimaryColor.safeHexToColor().toTonalPalettes() val tonalPalettes = customPrimaryColor.safeHexToColor().toTonalPalettes()
var addDialogVisible by remember { mutableStateOf(false) } var addDialogVisible by remember { mutableStateOf(false) }
var customColorValue by remember { mutableStateOf(context.customPrimaryColor) } var customColorValue by remember { mutableStateOf(customPrimaryColor) }
if (palettes.isEmpty()) { if (palettes.isEmpty()) {
Row( Row(
@ -239,14 +252,10 @@ fun Palettes(
isCustom = isCustom, isCustom = isCustom,
onClick = { onClick = {
if (isCustom) { if (isCustom) {
customColorValue = customPrimaryColor
addDialogVisible = true addDialogVisible = true
} else { } else {
scope.launch(Dispatchers.IO) { ThemeIndexPreference.put(context, scope, themeIndexPrefix + index)
context.dataStore.put(
DataStoreKeys.ThemeIndex,
themeIndexPrefix + index
)
}
} }
}, },
palette = if (isCustom) tonalPalettes else palette palette = if (isCustom) tonalPalettes else palette
@ -266,16 +275,12 @@ fun Palettes(
}, },
onDismissRequest = { onDismissRequest = {
addDialogVisible = false addDialogVisible = false
customColorValue = context.customPrimaryColor
}, },
onConfirm = { onConfirm = {
it.checkColorHex()?.let { it.checkColorHex()?.let {
scope.launch(Dispatchers.IO) { CustomPrimaryColorPreference.put(context, scope, it)
context.dataStore.put(DataStoreKeys.CustomPrimaryColor, it) ThemeIndexPreference.put(context, scope, 4)
context.dataStore.put(DataStoreKeys.ThemeIndex, 4)
}
addDialogVisible = false addDialogVisible = false
customColorValue = it
} }
} }
) )

View File

@ -0,0 +1,367 @@
package me.ash.reader.ui.page.settings.color.feeds
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Add
import androidx.compose.material.icons.rounded.ArrowBack
import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import me.ash.reader.R
import me.ash.reader.data.entity.Feed
import me.ash.reader.data.entity.Filter
import me.ash.reader.data.entity.Group
import me.ash.reader.data.preference.*
import me.ash.reader.ui.component.*
import me.ash.reader.ui.ext.surfaceColorAtElevation
import me.ash.reader.ui.page.home.FilterBar
import me.ash.reader.ui.page.home.feeds.GroupItem
import me.ash.reader.ui.page.settings.SettingItem
import me.ash.reader.ui.theme.palette.onDark
import me.ash.reader.ui.theme.palette.onLight
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun FeedsPageStyle(
navController: NavHostController,
) {
val context = LocalContext.current
val filterBarStyle = LocalFeedsFilterBarStyle.current
val filterBarFilled = LocalFeedsFilterBarFilled.current
val filterBarPadding = LocalFeedsFilterBarPadding.current
val filterBarTonalElevation = LocalFeedsFilterBarTonalElevation.current
val topBarTonalElevation = LocalFeedsTopBarTonalElevation.current
val groupListExpand = LocalFeedsGroupListExpand.current
val groupListTonalElevation = LocalFeedsGroupListTonalElevation.current
val scope = rememberCoroutineScope()
var filterBarStyleDialogVisible by remember { mutableStateOf(false) }
var filterBarPaddingDialogVisible by remember { mutableStateOf(false) }
var filterBarTonalElevationDialogVisible by remember { mutableStateOf(false) }
var topBarTonalElevationDialogVisible by remember { mutableStateOf(false) }
var groupListTonalElevationDialogVisible by remember { mutableStateOf(false) }
var filterBarPaddingValue: Int? by remember { mutableStateOf(filterBarPadding) }
Scaffold(
modifier = Modifier
.background(MaterialTheme.colorScheme.surface onLight MaterialTheme.colorScheme.inverseOnSurface)
.statusBarsPadding()
.navigationBarsPadding(),
containerColor = MaterialTheme.colorScheme.surface onLight MaterialTheme.colorScheme.inverseOnSurface,
topBar = {
SmallTopAppBar(
colors = TopAppBarDefaults.smallTopAppBarColors(
containerColor = MaterialTheme.colorScheme.surface onLight MaterialTheme.colorScheme.inverseOnSurface
),
title = {},
navigationIcon = {
FeedbackIconButton(
imageVector = Icons.Rounded.ArrowBack,
contentDescription = stringResource(R.string.back),
tint = MaterialTheme.colorScheme.onSurface
) {
navController.popBackStack()
}
},
actions = {}
)
},
content = {
LazyColumn {
item {
DisplayText(text = stringResource(R.string.feeds_page), desc = "")
}
// Preview
item {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp)
.clip(RoundedCornerShape(24.dp))
.background(
MaterialTheme.colorScheme.inverseOnSurface
onLight MaterialTheme.colorScheme.surface.copy(0.7f)
)
.clickable { },
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
FeedsPagePreview(
topBarTonalElevation = topBarTonalElevation,
groupListExpand = groupListExpand,
groupListTonalElevation = groupListTonalElevation,
filterBarStyle = filterBarStyle.value,
filterBarFilled = filterBarFilled.value,
filterBarPadding = filterBarPadding.dp,
filterBarTonalElevation = filterBarTonalElevation.value.dp,
)
}
Spacer(modifier = Modifier.height(24.dp))
}
// Top Bar
item {
Subtitle(
modifier = Modifier.padding(horizontal = 24.dp),
text = stringResource(R.string.top_bar)
)
SettingItem(
title = stringResource(R.string.tonal_elevation),
desc = "${topBarTonalElevation.value}dp",
onClick = {
topBarTonalElevationDialogVisible = true
},
) {}
// Tips(text = stringResource(R.string.tips_top_bar_tonal_elevation))
Spacer(modifier = Modifier.height(24.dp))
}
// Group List
item {
Subtitle(
modifier = Modifier.padding(horizontal = 24.dp),
text = stringResource(R.string.group_list)
)
SettingItem(
title = stringResource(R.string.always_expand),
onClick = {
(!groupListExpand).put(context, scope)
},
) {
Switch(activated = groupListExpand.value) {
(!groupListExpand).put(context, scope)
}
}
SettingItem(
title = stringResource(R.string.tonal_elevation),
desc = "${groupListTonalElevation.value}dp",
onClick = {
groupListTonalElevationDialogVisible = true
},
) {}
Tips(text = stringResource(R.string.tips_group_list_tonal_elevation))
Spacer(modifier = Modifier.height(24.dp))
}
// Filter Bar
item {
Subtitle(
modifier = Modifier.padding(horizontal = 24.dp),
text = stringResource(R.string.filter_bar),
)
SettingItem(
title = stringResource(R.string.style),
desc = filterBarStyle.getDesc(context),
onClick = {
filterBarStyleDialogVisible = true
},
) {}
SettingItem(
title = stringResource(R.string.fill_selected_icon),
onClick = {
(!filterBarFilled).put(context, scope)
},
) {
Switch(activated = filterBarFilled.value) {
(!filterBarFilled).put(context, scope)
}
}
SettingItem(
title = stringResource(R.string.padding_on_both_ends),
desc = "${filterBarPadding}dp",
onClick = {
filterBarPaddingValue = filterBarPadding
filterBarPaddingDialogVisible = true
},
) {}
SettingItem(
title = stringResource(R.string.tonal_elevation),
desc = "${filterBarTonalElevation.value}dp",
onClick = {
filterBarTonalElevationDialogVisible = true
},
) {}
Spacer(modifier = Modifier.height(24.dp))
}
}
}
)
RadioDialog(
visible = filterBarStyleDialogVisible,
title = stringResource(R.string.style),
options = FeedsFilterBarStylePreference.values.map {
RadioDialogOption(
text = it.getDesc(context),
selected = filterBarStyle == it,
) {
it.put(context, scope)
}
}
) {
filterBarStyleDialogVisible = false
}
TextFieldDialog(
visible = filterBarPaddingDialogVisible,
title = stringResource(R.string.padding_on_both_ends),
value = (filterBarPaddingValue ?: "").toString(),
placeholder = stringResource(R.string.value),
onValueChange = {
filterBarPaddingValue = it.filter { it.isDigit() }.toIntOrNull()
},
onDismissRequest = {
filterBarPaddingDialogVisible = false
},
onConfirm = {
FeedsFilterBarPaddingPreference.put(context, scope, filterBarPaddingValue ?: 0)
filterBarPaddingDialogVisible = false
}
)
RadioDialog(
visible = filterBarTonalElevationDialogVisible,
title = stringResource(R.string.tonal_elevation),
options = FeedsFilterBarTonalElevationPreference.values.map {
RadioDialogOption(
text = it.getDesc(context),
selected = it == filterBarTonalElevation,
) {
it.put(context, scope)
}
}
) {
filterBarTonalElevationDialogVisible = false
}
RadioDialog(
visible = topBarTonalElevationDialogVisible,
title = stringResource(R.string.tonal_elevation),
options = FeedsTopBarTonalElevationPreference.values.map {
RadioDialogOption(
text = it.getDesc(context),
selected = it == topBarTonalElevation,
) {
it.put(context, scope)
}
}
) {
topBarTonalElevationDialogVisible = false
}
RadioDialog(
visible = groupListTonalElevationDialogVisible,
title = stringResource(R.string.tonal_elevation),
options = FeedsGroupListTonalElevationPreference.values.map {
RadioDialogOption(
text = it.getDesc(context),
selected = it == groupListTonalElevation,
) {
it.put(context, scope)
}
}
) {
groupListTonalElevationDialogVisible = false
}
}
@Composable
fun FeedsPagePreview(
topBarTonalElevation: FeedsTopBarTonalElevationPreference,
groupListExpand: FeedsGroupListExpandPreference,
groupListTonalElevation: FeedsGroupListTonalElevationPreference,
filterBarStyle: Int,
filterBarFilled: Boolean,
filterBarPadding: Dp,
filterBarTonalElevation: Dp,
) {
var filter by remember { mutableStateOf(Filter.Unread) }
Column(
modifier = Modifier
.animateContentSize()
.background(
color = MaterialTheme.colorScheme.surfaceColorAtElevation(
groupListTonalElevation.value.dp
) onDark MaterialTheme.colorScheme.surface,
shape = RoundedCornerShape(24.dp)
)
) {
SmallTopAppBar(
title = {},
colors = TopAppBarDefaults.smallTopAppBarColors(
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(
topBarTonalElevation.value.dp
),
),
navigationIcon = {
FeedbackIconButton(
imageVector = Icons.Rounded.ArrowBack,
contentDescription = stringResource(R.string.back),
tint = MaterialTheme.colorScheme.onSurface
) {}
},
actions = {
FeedbackIconButton(
imageVector = Icons.Rounded.Refresh,
contentDescription = stringResource(R.string.refresh),
tint = MaterialTheme.colorScheme.onSurface,
) {}
FeedbackIconButton(
imageVector = Icons.Rounded.Add,
contentDescription = stringResource(R.string.subscribe),
tint = MaterialTheme.colorScheme.onSurface,
) {}
}
)
Spacer(modifier = Modifier.height(12.dp))
GroupItem(
isExpanded = groupListExpand.value,
tonalElevation = groupListTonalElevation.value.dp,
group = Group(
id = "",
name = stringResource(R.string.defaults),
accountId = 0,
),
feeds = listOf(
Feed(
id = "",
name = stringResource(R.string.preview_feed_name),
icon = "",
accountId = 0,
groupId = "",
url = "",
).apply {
important = 100
}
),
)
Spacer(modifier = Modifier.height(12.dp))
FilterBar(
modifier = Modifier.padding(horizontal = 12.dp),
filter = filter,
filterBarStyle = filterBarStyle,
filterBarFilled = filterBarFilled,
filterBarPadding = filterBarPadding,
filterBarTonalElevation = filterBarTonalElevation,
) {
filter = it
}
}
}

View File

@ -0,0 +1,429 @@
package me.ash.reader.ui.page.settings.color.flow
import android.annotation.SuppressLint
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.ArrowBack
import androidx.compose.material.icons.rounded.DoneAll
import androidx.compose.material.icons.rounded.Search
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import me.ash.reader.R
import me.ash.reader.data.entity.Article
import me.ash.reader.data.entity.ArticleWithFeed
import me.ash.reader.data.entity.Feed
import me.ash.reader.data.entity.Filter
import me.ash.reader.data.preference.*
import me.ash.reader.ui.component.*
import me.ash.reader.ui.ext.surfaceColorAtElevation
import me.ash.reader.ui.page.home.FilterBar
import me.ash.reader.ui.page.home.flow.ArticleItem
import me.ash.reader.ui.page.settings.SettingItem
import me.ash.reader.ui.theme.palette.onDark
import me.ash.reader.ui.theme.palette.onLight
import java.util.*
@SuppressLint("FlowOperatorInvokedInComposition")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun FlowPageStyle(
navController: NavHostController,
) {
val context = LocalContext.current
val filterBarStyle = LocalFlowFilterBarStyle.current
val filterBarFilled = LocalFlowFilterBarFilled.current
val filterBarPadding = LocalFlowFilterBarPadding.current
val filterBarTonalElevation = LocalFlowFilterBarTonalElevation.current
val topBarTonalElevation = LocalFlowTopBarTonalElevation.current
val articleListFeedIcon = LocalFlowArticleListFeedIcon.current
val articleListFeedName = LocalFlowArticleListFeedName.current
val articleListImage = LocalFlowArticleListImage.current
val articleListDesc = LocalFlowArticleListDesc.current
val articleListTime = LocalFlowArticleListTime.current
val articleListStickyDate = LocalFlowArticleListDateStickyHeader.current
val articleListTonalElevation = LocalFlowArticleListTonalElevation.current
val scope = rememberCoroutineScope()
var filterBarStyleDialogVisible by remember { mutableStateOf(false) }
var filterBarPaddingDialogVisible by remember { mutableStateOf(false) }
var filterBarTonalElevationDialogVisible by remember { mutableStateOf(false) }
var topBarTonalElevationDialogVisible by remember { mutableStateOf(false) }
var articleListTonalElevationDialogVisible by remember { mutableStateOf(false) }
var filterBarPaddingValue: Int? by remember { mutableStateOf(filterBarPadding) }
Scaffold(
modifier = Modifier
.background(MaterialTheme.colorScheme.surface onLight MaterialTheme.colorScheme.inverseOnSurface)
.statusBarsPadding()
.navigationBarsPadding(),
containerColor = MaterialTheme.colorScheme.surface onLight MaterialTheme.colorScheme.inverseOnSurface,
topBar = {
SmallTopAppBar(
colors = TopAppBarDefaults.smallTopAppBarColors(
containerColor = MaterialTheme.colorScheme.surface onLight MaterialTheme.colorScheme.inverseOnSurface
),
title = {},
navigationIcon = {
FeedbackIconButton(
imageVector = Icons.Rounded.ArrowBack,
contentDescription = stringResource(R.string.back),
tint = MaterialTheme.colorScheme.onSurface
) {
navController.popBackStack()
}
},
actions = {}
)
},
content = {
LazyColumn {
item {
DisplayText(text = stringResource(R.string.flow_page), desc = "")
}
// Preview
item {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp)
.clip(RoundedCornerShape(24.dp))
.background(
MaterialTheme.colorScheme.inverseOnSurface
onLight MaterialTheme.colorScheme.surface.copy(0.7f)
)
.clickable { },
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
FlowPagePreview(
topBarTonalElevation = topBarTonalElevation,
articleListTonalElevation = articleListTonalElevation,
filterBarStyle = filterBarStyle.value,
filterBarFilled = filterBarFilled.value,
filterBarPadding = filterBarPadding.dp,
filterBarTonalElevation = filterBarTonalElevation.value.dp,
)
}
Spacer(modifier = Modifier.height(24.dp))
}
// Top Bar
item {
Subtitle(
modifier = Modifier.padding(horizontal = 24.dp),
text = stringResource(R.string.top_bar)
)
SettingItem(
title = stringResource(R.string.mark_as_read_button_position),
desc = stringResource(R.string.top),
enable = false,
onClick = {},
) {}
SettingItem(
title = stringResource(R.string.tonal_elevation),
desc = "${topBarTonalElevation.value}dp",
onClick = {
topBarTonalElevationDialogVisible = true
},
) {}
// Tips(text = stringResource(R.string.tips_top_bar_tonal_elevation))
Spacer(modifier = Modifier.height(24.dp))
}
// Article List
item {
Subtitle(
modifier = Modifier.padding(horizontal = 24.dp),
text = stringResource(R.string.article_list)
)
SettingItem(
title = stringResource(R.string.display_feed_favicon),
onClick = {
(!articleListFeedIcon).put(context, scope)
},
) {
Switch(activated = articleListFeedIcon.value) {
(!articleListFeedIcon).put(context, scope)
}
}
SettingItem(
title = stringResource(R.string.display_feed_name),
onClick = {
(!articleListFeedName).put(context, scope)
},
) {
Switch(activated = articleListFeedName.value) {
(!articleListFeedName).put(context, scope)
}
}
SettingItem(
title = stringResource(R.string.display_article_image),
enable = false,
onClick = {},
) {
Switch(activated = false, enable = false)
}
SettingItem(
title = stringResource(R.string.display_article_desc),
onClick = {
(!articleListDesc).put(context, scope)
},
) {
Switch(activated = articleListDesc.value) {
(!articleListDesc).put(context, scope)
}
}
SettingItem(
title = stringResource(R.string.display_article_date),
onClick = {
(!articleListTime).put(context, scope)
},
) {
Switch(activated = articleListTime.value) {
(!articleListTime).put(context, scope)
}
}
SettingItem(
title = stringResource(R.string.article_date_sticky_header),
onClick = {
(!articleListStickyDate).put(context, scope)
},
) {
Switch(activated = articleListStickyDate.value) {
(!articleListStickyDate).put(context, scope)
}
}
SettingItem(
title = stringResource(R.string.tonal_elevation),
desc = "${articleListTonalElevation.value}dp",
onClick = {
articleListTonalElevationDialogVisible = true
},
) {}
Tips(text = stringResource(R.string.tips_article_list_tonal_elevation))
Spacer(modifier = Modifier.height(24.dp))
}
// Filter Bar
item {
Subtitle(
modifier = Modifier.padding(horizontal = 24.dp),
text = stringResource(R.string.filter_bar),
)
SettingItem(
title = stringResource(R.string.style),
desc = filterBarStyle.getDesc(context),
onClick = {
filterBarStyleDialogVisible = true
},
) {}
SettingItem(
title = stringResource(R.string.fill_selected_icon),
onClick = {
(!filterBarFilled).put(context, scope)
},
) {
Switch(activated = filterBarFilled.value) {
(!filterBarFilled).put(context, scope)
}
}
SettingItem(
title = stringResource(R.string.padding_on_both_ends),
desc = "${filterBarPadding}dp",
onClick = {
filterBarPaddingValue = filterBarPadding
filterBarPaddingDialogVisible = true
},
) {}
SettingItem(
title = stringResource(R.string.tonal_elevation),
desc = "${filterBarTonalElevation.value}dp",
onClick = {
filterBarTonalElevationDialogVisible = true
},
) {}
Spacer(modifier = Modifier.height(24.dp))
}
}
}
)
RadioDialog(
visible = filterBarStyleDialogVisible,
title = stringResource(R.string.style),
options = FlowFilterBarStylePreference.values.map {
RadioDialogOption(
text = it.getDesc(context),
selected = it == filterBarStyle,
) {
it.put(context, scope)
}
}
) {
filterBarStyleDialogVisible = false
}
TextFieldDialog(
visible = filterBarPaddingDialogVisible,
title = stringResource(R.string.padding_on_both_ends),
value = (filterBarPaddingValue ?: "").toString(),
placeholder = stringResource(R.string.value),
onValueChange = {
filterBarPaddingValue = it.filter { it.isDigit() }.toIntOrNull()
},
onDismissRequest = {
filterBarPaddingDialogVisible = false
},
onConfirm = {
FlowFilterBarPaddingPreference.put(context, scope, filterBarPaddingValue ?: 0)
filterBarPaddingDialogVisible = false
}
)
RadioDialog(
visible = filterBarTonalElevationDialogVisible,
title = stringResource(R.string.tonal_elevation),
options = FlowFilterBarTonalElevationPreference.values.map {
RadioDialogOption(
text = it.getDesc(context),
selected = it == filterBarTonalElevation,
) {
it.put(context, scope)
}
}
) {
filterBarTonalElevationDialogVisible = false
}
RadioDialog(
visible = topBarTonalElevationDialogVisible,
title = stringResource(R.string.tonal_elevation),
options = FlowTopBarTonalElevationPreference.values.map {
RadioDialogOption(
text = it.getDesc(context),
selected = it == topBarTonalElevation,
) {
it.put(context, scope)
}
}
) {
topBarTonalElevationDialogVisible = false
}
RadioDialog(
visible = articleListTonalElevationDialogVisible,
title = stringResource(R.string.tonal_elevation),
options = FlowArticleListTonalElevationPreference.values.map {
RadioDialogOption(
text = it.getDesc(context),
selected = it == articleListTonalElevation,
) {
it.put(context, scope)
}
}
) {
articleListTonalElevationDialogVisible = false
}
}
@Composable
fun FlowPagePreview(
topBarTonalElevation: FlowTopBarTonalElevationPreference,
articleListTonalElevation: FlowArticleListTonalElevationPreference,
filterBarStyle: Int,
filterBarFilled: Boolean,
filterBarPadding: Dp,
filterBarTonalElevation: Dp,
) {
var filter by remember { mutableStateOf(Filter.Unread) }
Column(
modifier = Modifier
.animateContentSize()
.background(
color = MaterialTheme.colorScheme.surfaceColorAtElevation(
articleListTonalElevation.value.dp
) onDark MaterialTheme.colorScheme.surface,
shape = RoundedCornerShape(24.dp)
)
) {
SmallTopAppBar(
title = {},
colors = TopAppBarDefaults.smallTopAppBarColors(
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(
topBarTonalElevation.value.dp
),
),
navigationIcon = {
FeedbackIconButton(
imageVector = Icons.Rounded.ArrowBack,
contentDescription = stringResource(R.string.back),
tint = MaterialTheme.colorScheme.onSurface
) {}
},
actions = {
FeedbackIconButton(
imageVector = Icons.Rounded.DoneAll,
contentDescription = stringResource(R.string.mark_all_as_read),
tint = MaterialTheme.colorScheme.onSurface,
) {}
FeedbackIconButton(
imageVector = Icons.Rounded.Search,
contentDescription = stringResource(R.string.search),
tint = MaterialTheme.colorScheme.onSurface,
) {}
}
)
Spacer(modifier = Modifier.height(12.dp))
ArticleItem(
articleWithFeed = ArticleWithFeed(
Article(
id = "",
title = stringResource(R.string.preview_article_title),
shortDescription = stringResource(R.string.preview_article_desc),
rawDescription = stringResource(R.string.preview_article_desc),
link = "",
feedId = "",
accountId = 0,
date = Date(),
isStarred = true,
),
feed = Feed(
id = "",
name = stringResource(R.string.preview_feed_name),
icon = "",
accountId = 0,
groupId = "",
url = "",
),
)
)
Spacer(modifier = Modifier.height(12.dp))
FilterBar(
modifier = Modifier.padding(horizontal = 12.dp),
filter = filter,
filterBarStyle = filterBarStyle,
filterBarFilled = filterBarFilled,
filterBarPadding = filterBarPadding,
filterBarTonalElevation = filterBarTonalElevation,
) {
filter = it
}
}
}

View File

@ -10,11 +10,9 @@ import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.ash.reader.R import me.ash.reader.R
@ -32,7 +30,6 @@ fun Interaction(
navController: NavHostController, navController: NavHostController,
) { ) {
val context = LocalContext.current val context = LocalContext.current
val view = LocalView.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
var initialPageDialogVisible by remember { mutableStateOf(false) } var initialPageDialogVisible by remember { mutableStateOf(false) }
var initialFilterDialogVisible by remember { mutableStateOf(false) } var initialFilterDialogVisible by remember { mutableStateOf(false) }
@ -116,7 +113,7 @@ fun Interaction(
text = stringResource(R.string.feeds_page), text = stringResource(R.string.feeds_page),
selected = initialPage == 0, selected = initialPage == 0,
) { ) {
scope.launch(Dispatchers.IO) { scope.launch {
context.dataStore.put(DataStoreKeys.InitialPage, 0) context.dataStore.put(DataStoreKeys.InitialPage, 0)
} }
}, },
@ -124,7 +121,7 @@ fun Interaction(
text = stringResource(R.string.flow_page), text = stringResource(R.string.flow_page),
selected = initialPage == 1, selected = initialPage == 1,
) { ) {
scope.launch(Dispatchers.IO) { scope.launch {
context.dataStore.put(DataStoreKeys.InitialPage, 1) context.dataStore.put(DataStoreKeys.InitialPage, 1)
} }
}, },
@ -141,7 +138,7 @@ fun Interaction(
text = stringResource(R.string.starred), text = stringResource(R.string.starred),
selected = initialFilter == 0, selected = initialFilter == 0,
) { ) {
scope.launch(Dispatchers.IO) { scope.launch {
context.dataStore.put(DataStoreKeys.InitialFilter, 0) context.dataStore.put(DataStoreKeys.InitialFilter, 0)
} }
}, },
@ -149,7 +146,7 @@ fun Interaction(
text = stringResource(R.string.unread), text = stringResource(R.string.unread),
selected = initialFilter == 1, selected = initialFilter == 1,
) { ) {
scope.launch(Dispatchers.IO) { scope.launch {
context.dataStore.put(DataStoreKeys.InitialFilter, 1) context.dataStore.put(DataStoreKeys.InitialFilter, 1)
} }
}, },
@ -157,7 +154,7 @@ fun Interaction(
text = stringResource(R.string.all), text = stringResource(R.string.all),
selected = initialFilter == 2, selected = initialFilter == 2,
) { ) {
scope.launch(Dispatchers.IO) { scope.launch {
context.dataStore.put(DataStoreKeys.InitialFilter, 2) context.dataStore.put(DataStoreKeys.InitialFilter, 2)
} }
}, },

View File

@ -6,7 +6,6 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material.icons.rounded.CheckCircleOutline import androidx.compose.material.icons.rounded.CheckCircleOutline
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -21,6 +20,7 @@ import kotlinx.coroutines.launch
import me.ash.reader.R import me.ash.reader.R
import me.ash.reader.ui.component.DisplayText import me.ash.reader.ui.component.DisplayText
import me.ash.reader.ui.component.DynamicSVGImage import me.ash.reader.ui.component.DynamicSVGImage
import me.ash.reader.ui.component.Tips
import me.ash.reader.ui.ext.DataStoreKeys import me.ash.reader.ui.ext.DataStoreKeys
import me.ash.reader.ui.ext.dataStore import me.ash.reader.ui.ext.dataStore
import me.ash.reader.ui.ext.put import me.ash.reader.ui.ext.put
@ -57,14 +57,12 @@ fun StartupPage(
) )
} }
item { item {
TipsItem( Tips(
modifier = Modifier modifier = Modifier.padding(top = 40.dp),
.padding(horizontal = 24.dp) text = stringResource(R.string.agree_terms),
.padding(top = 40.dp)
) )
} }
item { item {
Spacer(modifier = Modifier.height(10.dp))
TextButton( TextButton(
modifier = Modifier.padding(horizontal = 12.dp), modifier = Modifier.padding(horizontal = 12.dp),
onClick = { onClick = {
@ -120,25 +118,3 @@ fun StartupPage(
} }
) )
} }
@Composable
private fun TipsItem(
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.fillMaxWidth()
) {
Icon(
imageVector = Icons.Outlined.Info,
contentDescription = stringResource(R.string.tips_and_support),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
Spacer(modifier = Modifier.height(22.dp))
Text(
text = stringResource(R.string.agree_terms),
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}

View File

@ -10,6 +10,7 @@ object SVGString
fun String.parseDynamicColor(tonalPalettes: TonalPalettes, isDarkTheme: Boolean): String = fun String.parseDynamicColor(tonalPalettes: TonalPalettes, isDarkTheme: Boolean): String =
replace("fill=\"(.+?)\"".toRegex()) { replace("fill=\"(.+?)\"".toRegex()) {
val value = it.groupValues[1] val value = it.groupValues[1]
Log.i("RLog", "parseDynamicColor: $value")
if (value.startsWith("#")) return@replace it.value if (value.startsWith("#")) return@replace it.value
try { try {
val (scheme, tone) = value.split("(?<=\\d)(?=\\D)|(?=\\d)(?<=\\D)".toRegex()) val (scheme, tone) = value.split("(?<=\\d)(?=\\D)|(?=\\d)(?<=\\D)".toRegex())
@ -24,6 +25,7 @@ fun String.parseDynamicColor(tonalPalettes: TonalPalettes, isDarkTheme: Boolean)
}?.toArgb() ?: 0xFFFFFF }?.toArgb() ?: 0xFFFFFF
"fill=\"${String.format("#%06X", 0xFFFFFF and argb)}\"" "fill=\"${String.format("#%06X", 0xFFFFFF and argb)}\""
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace()
Log.e("RLog", "parseDynamicColor: ${e.message}") Log.e("RLog", "parseDynamicColor: ${e.message}")
it.value it.value
} }

View File

@ -1,16 +1,11 @@
package me.ash.reader.ui.theme package me.ash.reader.ui.theme
import android.annotation.SuppressLint
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.platform.LocalContext import me.ash.reader.data.preference.LocalThemeIndex
import kotlinx.coroutines.flow.map
import me.ash.reader.ui.ext.DataStoreKeys
import me.ash.reader.ui.ext.dataStore
import me.ash.reader.ui.theme.palette.LocalTonalPalettes import me.ash.reader.ui.theme.palette.LocalTonalPalettes
import me.ash.reader.ui.theme.palette.TonalPalettes import me.ash.reader.ui.theme.palette.TonalPalettes
import me.ash.reader.ui.theme.palette.core.ProvideZcamViewingConditions import me.ash.reader.ui.theme.palette.core.ProvideZcamViewingConditions
@ -20,17 +15,13 @@ import me.ash.reader.ui.theme.palette.dynamicLightColorScheme
val LocalUseDarkTheme = compositionLocalOf { false } val LocalUseDarkTheme = compositionLocalOf { false }
@SuppressLint("FlowOperatorInvokedInComposition")
@Composable @Composable
fun AppTheme( fun AppTheme(
useDarkTheme: Boolean = isSystemInDarkTheme(), useDarkTheme: Boolean = isSystemInDarkTheme(),
wallpaperPalettes: List<TonalPalettes> = extractTonalPalettesFromUserWallpaper(), wallpaperPalettes: List<TonalPalettes> = extractTonalPalettesFromUserWallpaper(),
content: @Composable () -> Unit content: @Composable () -> Unit
) { ) {
val context = LocalContext.current val themeIndex = LocalThemeIndex.current
val themeIndex = context.dataStore.data
.map { it[DataStoreKeys.ThemeIndex.key] ?: 5 }
.collectAsState(initial = 5).value
val tonalPalettes = wallpaperPalettes[ val tonalPalettes = wallpaperPalettes[
if (themeIndex >= wallpaperPalettes.size) { if (themeIndex >= wallpaperPalettes.size) {
@ -47,7 +38,7 @@ fun AppTheme(
ProvideZcamViewingConditions { ProvideZcamViewingConditions {
CompositionLocalProvider( CompositionLocalProvider(
LocalTonalPalettes provides tonalPalettes.also { it.Preheating() }, LocalTonalPalettes provides tonalPalettes.also { it.Preheating() },
LocalUseDarkTheme provides useDarkTheme LocalUseDarkTheme provides useDarkTheme,
) { ) {
MaterialTheme( MaterialTheme(
colorScheme = colorScheme =

View File

@ -31,6 +31,7 @@ fun dynamicLightColorScheme(): ColorScheme {
onSurface = palettes neutral 10, onSurface = palettes neutral 10,
surfaceVariant = palettes neutralVariant 90, surfaceVariant = palettes neutralVariant 90,
onSurfaceVariant = palettes neutralVariant 30, onSurfaceVariant = palettes neutralVariant 30,
surfaceTint = palettes primary 40,
inverseSurface = palettes neutral 20, inverseSurface = palettes neutral 20,
inverseOnSurface = palettes neutral 95, inverseOnSurface = palettes neutral 95,
outline = palettes neutralVariant 50, outline = palettes neutralVariant 50,
@ -60,6 +61,7 @@ fun dynamicDarkColorScheme(): ColorScheme {
onSurface = palettes neutral 90, onSurface = palettes neutral 90,
surfaceVariant = palettes neutralVariant 30, surfaceVariant = palettes neutralVariant 30,
onSurfaceVariant = palettes neutralVariant 80, onSurfaceVariant = palettes neutralVariant 80,
surfaceTint = palettes primary 80,
inverseSurface = palettes neutral 90, inverseSurface = palettes neutral 90,
inverseOnSurface = palettes neutral 20, inverseOnSurface = palettes neutral 20,
outline = palettes neutralVariant 60, outline = palettes neutralVariant 60,

View File

@ -79,7 +79,7 @@
<string name="accounts">账户</string> <string name="accounts">账户</string>
<string name="accounts_desc">本地、FreshRSS</string> <string name="accounts_desc">本地、FreshRSS</string>
<string name="color_and_style">颜色和样式</string> <string name="color_and_style">颜色和样式</string>
<string name="color_and_style_desc">主题、色彩系统、字体大小</string> <string name="color_and_style_desc">主题、色调样式、字体大小</string>
<string name="interaction">交互</string> <string name="interaction">交互</string>
<string name="interaction_desc">启动时、触感反馈</string> <string name="interaction_desc">启动时、触感反馈</string>
<string name="languages">语言</string> <string name="languages">语言</string>
@ -99,7 +99,7 @@
<string name="primary_color_hint">例如 #666666 或 666666</string> <string name="primary_color_hint">例如 #666666 或 666666</string>
<string name="appearance">外观</string> <string name="appearance">外观</string>
<string name="style">样式</string> <string name="style">样式</string>
<string name="dark_theme">深色模式</string> <string name="dark_theme">深色主题</string>
<string name="use_device_theme">跟随系统设置</string> <string name="use_device_theme">跟随系统设置</string>
<string name="tonal_elevation">色调海拔</string> <string name="tonal_elevation">色调海拔</string>
<string name="fonts">字体</string> <string name="fonts">字体</string>
@ -124,4 +124,29 @@
<string name="on_start">启动时</string> <string name="on_start">启动时</string>
<string name="initial_page">起始页面</string> <string name="initial_page">起始页面</string>
<string name="initial_filter">起始过滤条件</string> <string name="initial_filter">起始过滤条件</string>
<string name="preview_article_title">呜呜呜,黎明之剑完结了</string>
<string name="preview_article_desc">是宴席,就有结束的时候,但这本书真的陪了我好久啊,好舍不得,而且主线结束了坑却没填完,不知道啥时候才有番外啊</string>
<string name="preview_feed_name">漩涡书院</string>
<string name="value"></string>
<string name="padding_on_both_ends">两端边距</string>
<string name="display_article_date">显示文章发布时间</string>
<string name="display_article_desc">显示文章描述</string>
<string name="display_article_image">显示文章插图</string>
<string name="display_feed_name">显示订阅源名称</string>
<string name="display_feed_favicon">显示订阅源图标</string>
<string name="article_date_sticky_header">文章发布日期粘性标签(实验性)</string>
<string name="article_list">文章列表</string>
<string name="group_list">分组列表</string>
<string name="always_expand">始终展开</string>
<string name="top">顶部</string>
<string name="mark_as_read_button_position">“标记为已读”按钮的位置</string>
<string name="top_bar">标题栏</string>
<string name="fill_selected_icon">填充已选中的图标</string>
<string name="filter_bar">过滤栏</string>
<string name="icons">图标</string>
<string name="icons_and_labels">图标和文字</string>
<string name="icons_and_label_only_selected">图标和文字(仅选中时)</string>
<string name="tips_top_bar_tonal_elevation">标题栏的色调海拔仅在滚动时可用。</string>
<string name="tips_article_list_tonal_elevation">文章列表的色调海拔仅在亮色主题时可用。</string>
<string name="tips_group_list_tonal_elevation">分组列表的色调海拔仅在亮色主题时可用。</string>
</resources> </resources>

View File

@ -79,7 +79,7 @@
<string name="accounts">Accounts</string> <string name="accounts">Accounts</string>
<string name="accounts_desc">Local, FreshRSS</string> <string name="accounts_desc">Local, FreshRSS</string>
<string name="color_and_style">Color &amp; style</string> <string name="color_and_style">Color &amp; style</string>
<string name="color_and_style_desc">Theme, color system, font size</string> <string name="color_and_style_desc">Theme, color style, font size</string>
<string name="interaction">Interaction</string> <string name="interaction">Interaction</string>
<string name="interaction_desc">On start, haptic feedback</string> <string name="interaction_desc">On start, haptic feedback</string>
<string name="languages">Languages</string> <string name="languages">Languages</string>
@ -88,9 +88,9 @@
<string name="tips_and_support_desc">About, open source</string> <string name="tips_and_support_desc">About, open source</string>
<string name="welcome">Welcome</string> <string name="welcome">Welcome</string>
<string name="agree_terms">Before you can continue, you need to agree to Read You\'s Terms of Service and Privacy Policy.</string> <string name="agree_terms">Before you can continue, you need to agree to Read You\'s Terms of Service and Privacy Policy.</string>
<string name="view_terms">View the &lt;i&gt;&lt;u&gt;Terms of Service and Privacy Policy&lt;/u&gt;&lt;/i&gt;</string> <string name="view_terms">View the &lt;i&gt;&lt;u&gt;Terms of Service &amp; Privacy Policy&lt;/u&gt;&lt;/i&gt;</string>
<string name="terms_link">https://github.com/Ashinch/ReadYou/blob/main/TERMS_OF_SERVICE_AND_PRIVACY_POLICY.md</string> <string name="terms_link">https://github.com/Ashinch/ReadYou/blob/main/TERMS_OF_SERVICE_AND_PRIVACY_POLICY.md</string>
<string name="agree_and_continue">Agree and Continue</string> <string name="agree_and_continue">Agree</string>
<string name="wallpaper_colors">Wallpaper Colors</string> <string name="wallpaper_colors">Wallpaper Colors</string>
<string name="no_palettes">No Palettes</string> <string name="no_palettes">No Palettes</string>
<string name="only_android_8.1_plus">Only Android 8.1+</string> <string name="only_android_8.1_plus">Only Android 8.1+</string>
@ -116,7 +116,7 @@
<string name="update">Update</string> <string name="update">Update</string>
<string name="skip_this_version">Skip This Version</string> <string name="skip_this_version">Skip This Version</string>
<string name="checking_updates">Checking for updates…</string> <string name="checking_updates">Checking for updates…</string>
<string name="is_latest_version">This is the latest version</string> <string name="is_latest_version">This is latest version</string>
<string name="check_failure">Check failure</string> <string name="check_failure">Check failure</string>
<string name="download_failure">Download failure</string> <string name="download_failure">Download failure</string>
<string name="rate_limit">The request rate is limited</string> <string name="rate_limit">The request rate is limited</string>
@ -124,4 +124,29 @@
<string name="on_start">On Start</string> <string name="on_start">On Start</string>
<string name="initial_page">Initial Page</string> <string name="initial_page">Initial Page</string>
<string name="initial_filter">Initial Filter</string> <string name="initial_filter">Initial Filter</string>
<string name="preview_article_title">The novel "Lord of the Mysteries" has finally come to an end</string>
<string name="preview_article_desc">The Fool is the eighth and final volume of the Lord of the Mysteries series written by Cuttlefish That Loves Diving.</string>
<string name="preview_feed_name">Reddit</string>
<string name="value">value</string>
<string name="padding_on_both_ends">Padding on Both Ends</string>
<string name="display_article_date">Display Article Publish Time</string>
<string name="display_article_desc">Display Article Descriptions</string>
<string name="display_article_image">Display Article Images</string>
<string name="display_feed_name">Display Feed Names</string>
<string name="display_feed_favicon">Display Feed Favicons</string>
<string name="article_date_sticky_header">Article Publish Date Sticky Header (Experimental)</string>
<string name="article_list">Article List</string>
<string name="group_list">Group List</string>
<string name="always_expand">Always Expand</string>
<string name="top">Top</string>
<string name="mark_as_read_button_position">\"Mark as Read\" Button Position</string>
<string name="top_bar">Top Bar</string>
<string name="fill_selected_icon">Fill The Selected Icon</string>
<string name="filter_bar">Filter Bar</string>
<string name="icons">Icons</string>
<string name="icons_and_labels">Icons &amp; Labels</string>
<string name="icons_and_label_only_selected">Icons &amp; Label (Only Selected)</string>
<string name="tips_top_bar_tonal_elevation">Tone elevation of the top bar is only available when scrolling.</string>
<string name="tips_article_list_tonal_elevation">Tone elevation of the article list is only available for light theme.</string>
<string name="tips_group_list_tonal_elevation">Tone elevation of the group list is only available for light theme.</string>
</resources> </resources>