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 dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CompletableDeferred
import me.ash.reader.data.preference.SettingsProvider
import me.ash.reader.ui.page.common.HomeEntry
@AndroidEntryPoint
@ -28,7 +29,9 @@ class MainActivity : ComponentActivity(), ImageLoader {
WindowCompat.setDecorFitsSystemWindows(window, false)
Log.i("RLog", "onCreate: ${ProfileInstallerInitializer().create(this)}")
setContent {
HomeEntry()
SettingsProvider {
HomeEntry()
}
}
}

View File

@ -2,13 +2,16 @@ package me.ash.reader.data.entity
import androidx.compose.material.icons.Icons
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.Subject
import androidx.compose.ui.graphics.vector.ImageVector
class Filter(
var index: Int,
var icon: ImageVector,
val index: Int,
val iconOutline: ImageVector,
val iconFilled: ImageVector,
) {
fun isStarred(): Boolean = this == Starred
fun isUnread(): Boolean = this == Unread
@ -17,15 +20,18 @@ class Filter(
companion object {
val Starred = Filter(
index = 0,
icon = Icons.Rounded.StarOutline,
iconOutline = Icons.Rounded.StarOutline,
iconFilled = Icons.Rounded.Star,
)
val Unread = Filter(
index = 1,
icon = Icons.Outlined.FiberManualRecord,
iconOutline = Icons.Outlined.FiberManualRecord,
iconFilled = Icons.Rounded.FiberManualRecord,
)
val All = Filter(
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),
shape = CircleShape,
color = animateColorAsState(
if (activated) (tonalPalettes primary 40) onDark (tonalPalettes neutralVariant 50)
else (tonalPalettes neutralVariant 50) onDark (tonalPalettes neutral 60)
if (activated) (tonalPalettes primary 40) onDark (tonalPalettes secondary 50)
else (tonalPalettes neutralVariant 50) onDark (tonalPalettes neutral 30)
).value
) {
Box(
@ -61,7 +61,7 @@ fun Switch(
shape = CircleShape,
color = animateColorAsState(
if (activated) tonalPalettes primary 90
else (tonalPalettes neutralVariant 70) onDark (tonalPalettes neutral 30)
else (tonalPalettes neutralVariant 70) onDark (tonalPalettes neutral 60)
).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(
elevation: Dp,
color: Color = surface,
): Color = color.atElevation(surfaceTint, elevation)
fun Color.atElevation(
sourceColor: Color,
elevation: Dp,
): Color {
if (elevation == 0.dp) return surface
val alpha = ((4.5f * ln(elevation.value + 1)) + 2f) / 100f
return primary.copy(alpha = alpha).compositeOver(surface)
if (elevation == 0.dp) return this
return sourceColor.copy(alpha = elevation.alphaLN(constant = 4.5f)).compositeOver(this)
}
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.preferences.core.*
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.io.IOException
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
@ -31,10 +33,7 @@ val Context.currentAccountId: Int
get() = this.dataStore.get(DataStoreKeys.CurrentAccountId)!!
val Context.currentAccountType: Int
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
get() = this.dataStore.get(DataStoreKeys.InitialPage) ?: 0
val Context.initialFilter: Int
@ -42,7 +41,9 @@ val Context.initialFilter: Int
suspend fun <T> DataStore<Preferences>.put(dataStoreKeys: DataStoreKeys<T>, value: T) {
this.edit {
it[dataStoreKeys.key] = value
withContext(Dispatchers.IO) {
it[dataStoreKeys.key] = value
}
}
}
@ -129,6 +130,101 @@ sealed class DataStoreKeys<T> {
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>() {
override val key: Preferences.Key<Int>
get() = intPreferencesKey("initialPage")

View File

@ -3,6 +3,7 @@ package me.ash.reader.ui.ext
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import kotlin.coroutines.CoroutineContext
@ -10,3 +11,9 @@ import kotlin.coroutines.CoroutineContext
fun <T> StateFlow<T>.collectAsStateValue(
context: CoroutineContext = Dispatchers.Default
): 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.settings.SettingsPage
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.tips.TipsAndSupport
import me.ash.reader.ui.page.startup.StartupPage
@ -34,6 +36,7 @@ fun HomeEntry(
homeViewModel: HomeViewModel = hiltViewModel(),
) {
val context = LocalContext.current
val viewState = homeViewModel.viewState.collectAsStateValue()
val filterState = homeViewModel.filterState.collectAsStateValue()
val pagingItems = viewState.pagingData.collectAsLazyPagingItems()
@ -97,9 +100,12 @@ fun HomeEntry(
navController = navController,
startDestination = if (context.isFirstLaunch) RouteName.STARTUP else RouteName.FEEDS,
) {
// Startup
animatedComposable(route = RouteName.STARTUP) {
StartupPage(navController)
}
// Home
animatedComposable(route = RouteName.FEEDS) {
FeedsPage(navController = navController, homeViewModel = homeViewModel)
}
@ -113,15 +119,29 @@ fun HomeEntry(
animatedComposable(route = "${RouteName.READING}/{articleId}") {
ReadPage(navController = navController)
}
// Settings
animatedComposable(route = RouteName.SETTINGS) {
SettingsPage(navController)
}
// Color & Style
animatedComposable(route = RouteName.COLOR_AND_STYLE) {
ColorAndStyle(navController)
}
animatedComposable(route = RouteName.FEEDS_PAGE_STYLE) {
FeedsPageStyle(navController)
}
animatedComposable(route = RouteName.FLOW_PAGE_STYLE) {
FlowPageStyle(navController)
}
// Interaction
animatedComposable(route = RouteName.INTERACTION) {
Interaction(navController)
}
// Tips & Support
animatedComposable(route = RouteName.TIPS_AND_SUPPORT) {
TipsAndSupport(navController)
}

View File

@ -1,12 +1,25 @@
package me.ash.reader.ui.page.common
object RouteName {
// Startup
const val STARTUP = "startup"
// Home
const val FEEDS = "feeds"
const val FLOW = "flow"
const val READING = "reading"
// Settings
const val SETTINGS = "settings"
// Color & 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"
// Tips & Support
const val TIPS_AND_SUPPORT = "tips_and_support"
}

View File

@ -1,70 +1,84 @@
package me.ash.reader.ui.page.home
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.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import androidx.compose.ui.unit.Dp
import com.google.accompanist.pager.ExperimentalPagerApi
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.theme.palette.alwaysLight
import me.ash.reader.ui.theme.palette.onDark
@OptIn(ExperimentalPagerApi::class)
@Composable
fun FilterBar(
modifier: Modifier = Modifier,
filter: Filter,
filterBarStyle: Int,
filterBarFilled: Boolean,
filterBarPadding: Dp,
filterBarTonalElevation: Dp,
filterOnClick: (Filter) -> Unit = {},
) {
val view = LocalView.current
Box(
modifier = Modifier.height(60.dp)
NavigationBar(
tonalElevation = filterBarTonalElevation,
) {
Divider(
modifier = Modifier
.fillMaxWidth()
.height(1.dp)
.zIndex(1f),
color = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.24f)
)
NavigationBar(
modifier = Modifier.fillMaxSize(),
tonalElevation = 0.dp,
) {
Spacer(modifier = Modifier.width(60.dp))
listOf(
Filter.Starred,
Filter.Unread,
Filter.All,
).forEach { item ->
NavigationBarItem(
icon = {
Icon(
imageVector = item.icon,
contentDescription = item.getName()
)
},
selected = filter == item,
onClick = {
// view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
view.playSoundEffect(SoundEffectConstants.CLICK)
filterOnClick(item)
},
colors = NavigationBarItemDefaults.colors(
selectedIconColor = MaterialTheme.colorScheme.onSecondaryContainer alwaysLight true,
// unselectedIconColor = MaterialTheme.colorScheme.outline,
selectedTextColor = MaterialTheme.colorScheme.onSurface alwaysLight true,
// unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant,
indicatorColor = MaterialTheme.colorScheme.primaryContainer alwaysLight true,
Spacer(modifier = Modifier.width(filterBarPadding))
listOf(
Filter.Starred,
Filter.Unread,
Filter.All,
).forEach { item ->
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(
imageVector = if (filter == item && filterBarFilled) {
item.iconFilled
} else {
item.iconOutline
},
contentDescription = item.getName()
)
},
label = if (filterBarStyle == FlowFilterBarStylePreference.Icon.value) {
null
} else {
{
Text(
text = item.getName(),
style = MaterialTheme.typography.labelLarge
)
}
},
selected = filter == item,
onClick = {
// view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
view.playSoundEffect(SoundEffectConstants.CLICK)
filterOnClick(item)
},
colors = NavigationBarItemDefaults.colors(
// selectedIconColor = MaterialTheme.colorScheme.onSecondaryContainer alwaysLight true,
// unselectedIconColor = MaterialTheme.colorScheme.outline,
// selectedTextColor = MaterialTheme.colorScheme.onSurface alwaysLight true,
// unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant,
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() {
_viewState.update {
it.copy(
pagingData = Pager(PagingConfig(pageSize = 10)) {
pagingData = Pager(PagingConfig(pageSize = 15)) {
if (_viewState.value.searchContent.isNotBlank()) {
rssRepository.get().searchArticles(
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.platform.LocalView
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import me.ash.reader.data.entity.Feed
import me.ash.reader.ui.page.home.feeds.option.feed.FeedOptionViewAction
import me.ash.reader.ui.page.home.feeds.option.feed.FeedOptionViewModel
import kotlin.math.ln
@OptIn(
androidx.compose.foundation.ExperimentalFoundationApi::class,
@ -31,6 +33,7 @@ fun FeedItem(
modifier: Modifier = Modifier,
feed: Feed,
feedOptionViewModel: FeedOptionViewModel = hiltViewModel(),
tonalElevation: Dp,
onClick: () -> Unit = {},
) {
val view = LocalView.current
@ -76,18 +79,18 @@ fun FeedItem(
)
}
if (feed.important ?: 0 != 0) {
Row() {
Badge(
containerColor = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.24f),
contentColor = MaterialTheme.colorScheme.outline,
content = {
Text(
text = feed.important.toString(),
style = MaterialTheme.typography.labelSmall
)
},
)
}
Badge(
containerColor = MaterialTheme.colorScheme.surfaceTint.copy(
alpha = (ln(tonalElevation.value + 1.4f) + 2f) / 100f
),
contentColor = MaterialTheme.colorScheme.outline,
content = {
Text(
text = feed.important.toString(),
style = MaterialTheme.typography.labelSmall
)
},
)
}
}
}

View File

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

View File

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

View File

@ -21,6 +21,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import me.ash.reader.R
import me.ash.reader.data.entity.ArticleWithFeed
import me.ash.reader.data.preference.*
import me.ash.reader.ui.ext.formatAsString
@Composable
@ -30,6 +31,11 @@ fun ArticleItem(
onClick: (ArticleWithFeed) -> Unit = {},
) {
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(
modifier = Modifier
@ -44,67 +50,86 @@ fun ArticleItem(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
modifier = Modifier
.weight(1f)
.padding(start = 30.dp),
text = articleWithFeed.feed.name,
color = MaterialTheme.colorScheme.tertiary,
style = MaterialTheme.typography.labelMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Row(
modifier = Modifier.padding(start = 6.dp),
verticalAlignment = Alignment.CenterVertically,
) {
if (articleWithFeed.article.isStarred) {
Icon(
modifier = Modifier
.size(14.dp)
.padding(end = 2.dp),
imageVector = Icons.Rounded.Star,
contentDescription = stringResource(R.string.starred),
tint = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
// Feed name
if (articleListFeedName.value) {
Text(
modifier = Modifier
.weight(1f)
.padding(start = if (articleListFeedIcon.value) 30.dp else 0.dp),
text = articleWithFeed.feed.name,
color = MaterialTheme.colorScheme.tertiary,
style = MaterialTheme.typography.labelMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
if (articleListDate.value) {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
if (!articleListFeedName.value) {
Spacer(Modifier.width(if (articleListFeedIcon.value) 30.dp else 0.dp))
}
// Starred
if (articleWithFeed.article.isStarred) {
Icon(
modifier = Modifier
.size(14.dp)
.padding(end = 2.dp),
imageVector = Icons.Rounded.Star,
contentDescription = stringResource(R.string.starred),
tint = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
)
}
// Date
Text(
text = articleWithFeed.article.date.formatAsString(
context,
onlyHourMinute = true
),
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
style = MaterialTheme.typography.labelMedium,
)
}
Text(
text = articleWithFeed.article.date.formatAsString(
context,
onlyHourMinute = true
),
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
style = MaterialTheme.typography.labelMedium,
)
}
}
Row(
modifier = Modifier.fillMaxWidth(),
) {
Row(
modifier = Modifier
.size(20.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.outline.copy(alpha = 0.2f))
) {}
Spacer(modifier = Modifier.width(10.dp))
// Feed icon
if (articleListFeedIcon.value) {
Row(
modifier = Modifier
.size(20.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.outline.copy(alpha = 0.2f))
) {}
Spacer(modifier = Modifier.width(10.dp))
}
// Article
Column(
modifier = Modifier.fillMaxWidth(),
) {
// Title
Text(
text = articleWithFeed.article.title,
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.titleMedium,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
Text(
text = articleWithFeed.article.shortDescription,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
style = MaterialTheme.typography.bodySmall,
maxLines = 2,
maxLines = if (articleListDesc.value) 2 else 4,
overflow = TextOverflow.Ellipsis,
)
// Description
if (articleListDesc.value) {
Text(
text = articleWithFeed.article.shortDescription,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
style = MaterialTheme.typography.bodySmall,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
}
}
}
}

View File

@ -13,6 +13,9 @@ import me.ash.reader.data.entity.ArticleWithFeed
@OptIn(ExperimentalFoundationApi::class)
fun LazyListScope.ArticleList(
pagingItems: LazyPagingItems<FlowItemView>,
articleListFeedIcon: Boolean,
articleListDateStickyHeader: Boolean,
articleListTonalElevation: Int,
onClick: (ArticleWithFeed) -> Unit = {},
) {
for (index in 0 until pagingItems.itemCount) {
@ -29,8 +32,14 @@ fun LazyListScope.ArticleList(
is FlowItemView.Date -> {
val separator = pagingItems[index] as FlowItemView.Date
if (separator.showSpacer) item { Spacer(modifier = Modifier.height(40.dp)) }
stickyHeader {
StickyHeader(separator.date)
if (articleListDateStickyHeader) {
stickyHeader(key = separator.date) {
StickyHeader(separator.date, articleListFeedIcon, articleListTonalElevation)
}
} else {
item(key = separator.date) {
StickyHeader(separator.date, articleListFeedIcon, articleListTonalElevation)
}
}
}
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.DoneAll
import androidx.compose.material.icons.rounded.Search
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SmallTopAppBar
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
@ -29,17 +26,20 @@ import androidx.paging.compose.LazyPagingItems
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import me.ash.reader.R
import me.ash.reader.data.preference.*
import me.ash.reader.data.repository.SyncWorker.Companion.getIsSyncing
import me.ash.reader.ui.component.DisplayText
import me.ash.reader.ui.component.FeedbackIconButton
import me.ash.reader.ui.component.SwipeRefresh
import me.ash.reader.ui.ext.collectAsStateValue
import me.ash.reader.ui.ext.getName
import me.ash.reader.ui.ext.surfaceColorAtElevation
import me.ash.reader.ui.page.common.RouteName
import me.ash.reader.ui.page.home.FilterBar
import me.ash.reader.ui.page.home.FilterState
import me.ash.reader.ui.page.home.HomeViewAction
import me.ash.reader.ui.page.home.HomeViewModel
import me.ash.reader.ui.theme.palette.onDark
@OptIn(
ExperimentalMaterial3Api::class,
@ -55,6 +55,14 @@ fun FlowPage(
pagingItems: LazyPagingItems<FlowItemView>,
) {
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 focusRequester = remember { FocusRequester() }
@ -100,12 +108,20 @@ fun FlowPage(
Scaffold(
modifier = Modifier
.background(MaterialTheme.colorScheme.surface)
.background(MaterialTheme.colorScheme.surfaceColorAtElevation(topBarTonalElevation.value.dp))
.statusBarsPadding()
.navigationBarsPadding(),
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(
articleListTonalElevation.value.dp
) onDark MaterialTheme.colorScheme.surface,
topBar = {
SmallTopAppBar(
title = {},
colors = TopAppBarDefaults.smallTopAppBarColors(
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(
topBarTonalElevation.value.dp
),
),
navigationIcon = {
FeedbackIconButton(
imageVector = Icons.Rounded.ArrowBack,
@ -113,7 +129,7 @@ fun FlowPage(
tint = MaterialTheme.colorScheme.onSurface
) {
onSearch = false
if(navController.previousBackStackEntry == null) {
if (navController.previousBackStackEntry == null) {
navController.navigate(RouteName.FEEDS) {
launchSingleTop = true
}
@ -182,7 +198,7 @@ fun FlowPage(
state = listState,
) {
item {
DisplayTextHeader(filterState, isSyncing)
DisplayTextHeader(filterState, isSyncing, articleListFeedIcon.value)
AnimatedVisibility(
visible = markAsRead,
enter = fadeIn() + expandVertically(),
@ -244,6 +260,9 @@ fun FlowPage(
}
ArticleList(
pagingItems = pagingItems,
articleListFeedIcon = articleListFeedIcon.value,
articleListDateStickyHeader = articleListDateStickyHeader.value,
articleListTonalElevation = articleListTonalElevation.value,
) {
onSearch = false
navController.navigate("${RouteName.READING}/${it.article.id}") {
@ -261,16 +280,16 @@ fun FlowPage(
},
bottomBar = {
FilterBar(
modifier = Modifier
.height(60.dp)
.fillMaxWidth(),
filter = filterState.filter,
filterOnClick = {
flowViewModel.dispatch(FlowViewAction.ScrollToItem(0))
homeViewModel.dispatch(HomeViewAction.ChangeFilter(filterState.copy(filter = it)))
homeViewModel.dispatch(HomeViewAction.FetchArticles)
},
)
filterBarStyle = filterBarStyle.value,
filterBarFilled = filterBarFilled.value,
filterBarPadding = filterBarPadding.dp,
filterBarTonalElevation = filterBarTonalElevation.value.dp,
) {
flowViewModel.dispatch(FlowViewAction.ScrollToItem(0))
homeViewModel.dispatch(HomeViewAction.ChangeFilter(filterState.copy(filter = it)))
homeViewModel.dispatch(HomeViewAction.FetchArticles)
}
}
)
}
@ -278,10 +297,11 @@ fun FlowPage(
@Composable
private fun DisplayTextHeader(
filterState: FilterState,
isSyncing: Boolean
isSyncing: Boolean,
articleListFeedIcon: Boolean,
) {
DisplayText(
modifier = Modifier.padding(start = 30.dp),
modifier = Modifier.padding(start = if (articleListFeedIcon) 30.dp else 0.dp),
text = when {
filterState.group != null -> filterState.group.name
filterState.feed != null -> filterState.feed.name

View File

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

View File

@ -1,6 +1,7 @@
package me.ash.reader.ui.page.settings.color
import android.annotation.SuppressLint
import android.content.Context
import android.os.Build
import androidx.compose.animation.*
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.unit.dp
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.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.ext.*
import me.ash.reader.ui.page.common.RouteName
import me.ash.reader.ui.page.settings.SettingItem
import me.ash.reader.ui.svg.PALETTE
import me.ash.reader.ui.svg.SVGString
@ -46,8 +48,11 @@ fun ColorAndStyle(
) {
val context = LocalContext.current
val useDarkTheme = LocalUseDarkTheme.current
val themeIndex = LocalThemeIndex.current
val customPrimaryColor = LocalCustomPrimaryColor.current
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(
modifier = Modifier
@ -111,6 +116,7 @@ fun ColorAndStyle(
onClick = {},
) {
Palettes(
context = context,
palettes = wallpaperTonalPalettes.run {
if (this.size > 5) {
this.subList(5, wallpaperTonalPalettes.size)
@ -118,7 +124,9 @@ fun ColorAndStyle(
emptyList()
}
},
themeIndex = themeIndex,
themeIndexPrefix = 5,
customPrimaryColor = customPrimaryColor,
)
},
BlockRadioGroupButtonItem(
@ -126,7 +134,10 @@ fun ColorAndStyle(
onClick = {},
) {
Palettes(
palettes = wallpaperTonalPalettes.subList(0, 5)
context = context,
themeIndex = themeIndex,
palettes = wallpaperTonalPalettes.subList(0, 5),
customPrimaryColor = customPrimaryColor,
)
},
),
@ -162,13 +173,19 @@ fun ColorAndStyle(
)
SettingItem(
title = stringResource(R.string.feeds_page),
enable = false,
onClick = {},
onClick = {
navController.navigate(RouteName.FEEDS_PAGE_STYLE) {
launchSingleTop = true
}
},
) {}
SettingItem(
title = stringResource(R.string.flow_page),
enable = false,
onClick = {},
onClick = {
navController.navigate(RouteName.FLOW_PAGE_STYLE) {
launchSingleTop = true
}
},
) {}
SettingItem(
title = stringResource(R.string.reading_page),
@ -186,20 +203,16 @@ fun ColorAndStyle(
@Composable
fun Palettes(
modifier: Modifier = Modifier,
context: Context,
palettes: List<TonalPalettes>,
themeIndex: Int = 0,
themeIndexPrefix: Int = 0,
customPrimaryColor: String = "",
) {
val context = LocalContext.current
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()
var addDialogVisible by remember { mutableStateOf(false) }
var customColorValue by remember { mutableStateOf(context.customPrimaryColor) }
var customColorValue by remember { mutableStateOf(customPrimaryColor) }
if (palettes.isEmpty()) {
Row(
@ -239,14 +252,10 @@ fun Palettes(
isCustom = isCustom,
onClick = {
if (isCustom) {
customColorValue = customPrimaryColor
addDialogVisible = true
} else {
scope.launch(Dispatchers.IO) {
context.dataStore.put(
DataStoreKeys.ThemeIndex,
themeIndexPrefix + index
)
}
ThemeIndexPreference.put(context, scope, themeIndexPrefix + index)
}
},
palette = if (isCustom) tonalPalettes else palette
@ -266,16 +275,12 @@ fun Palettes(
},
onDismissRequest = {
addDialogVisible = false
customColorValue = context.customPrimaryColor
},
onConfirm = {
it.checkColorHex()?.let {
scope.launch(Dispatchers.IO) {
context.dataStore.put(DataStoreKeys.CustomPrimaryColor, it)
context.dataStore.put(DataStoreKeys.ThemeIndex, 4)
}
CustomPrimaryColorPreference.put(context, scope, it)
ThemeIndexPreference.put(context, scope, 4)
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.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import me.ash.reader.R
@ -32,7 +30,6 @@ fun Interaction(
navController: NavHostController,
) {
val context = LocalContext.current
val view = LocalView.current
val scope = rememberCoroutineScope()
var initialPageDialogVisible by remember { mutableStateOf(false) }
var initialFilterDialogVisible by remember { mutableStateOf(false) }
@ -116,7 +113,7 @@ fun Interaction(
text = stringResource(R.string.feeds_page),
selected = initialPage == 0,
) {
scope.launch(Dispatchers.IO) {
scope.launch {
context.dataStore.put(DataStoreKeys.InitialPage, 0)
}
},
@ -124,7 +121,7 @@ fun Interaction(
text = stringResource(R.string.flow_page),
selected = initialPage == 1,
) {
scope.launch(Dispatchers.IO) {
scope.launch {
context.dataStore.put(DataStoreKeys.InitialPage, 1)
}
},
@ -141,7 +138,7 @@ fun Interaction(
text = stringResource(R.string.starred),
selected = initialFilter == 0,
) {
scope.launch(Dispatchers.IO) {
scope.launch {
context.dataStore.put(DataStoreKeys.InitialFilter, 0)
}
},
@ -149,7 +146,7 @@ fun Interaction(
text = stringResource(R.string.unread),
selected = initialFilter == 1,
) {
scope.launch(Dispatchers.IO) {
scope.launch {
context.dataStore.put(DataStoreKeys.InitialFilter, 1)
}
},
@ -157,7 +154,7 @@ fun Interaction(
text = stringResource(R.string.all),
selected = initialFilter == 2,
) {
scope.launch(Dispatchers.IO) {
scope.launch {
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.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material.icons.rounded.CheckCircleOutline
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
@ -21,6 +20,7 @@ import kotlinx.coroutines.launch
import me.ash.reader.R
import me.ash.reader.ui.component.DisplayText
import me.ash.reader.ui.component.DynamicSVGImage
import me.ash.reader.ui.component.Tips
import me.ash.reader.ui.ext.DataStoreKeys
import me.ash.reader.ui.ext.dataStore
import me.ash.reader.ui.ext.put
@ -57,14 +57,12 @@ fun StartupPage(
)
}
item {
TipsItem(
modifier = Modifier
.padding(horizontal = 24.dp)
.padding(top = 40.dp)
Tips(
modifier = Modifier.padding(top = 40.dp),
text = stringResource(R.string.agree_terms),
)
}
item {
Spacer(modifier = Modifier.height(10.dp))
TextButton(
modifier = Modifier.padding(horizontal = 12.dp),
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 =
replace("fill=\"(.+?)\"".toRegex()) {
val value = it.groupValues[1]
Log.i("RLog", "parseDynamicColor: $value")
if (value.startsWith("#")) return@replace it.value
try {
val (scheme, tone) = value.split("(?<=\\d)(?=\\D)|(?=\\d)(?<=\\D)".toRegex())
@ -24,6 +25,7 @@ fun String.parseDynamicColor(tonalPalettes: TonalPalettes, isDarkTheme: Boolean)
}?.toArgb() ?: 0xFFFFFF
"fill=\"${String.format("#%06X", 0xFFFFFF and argb)}\""
} catch (e: Exception) {
e.printStackTrace()
Log.e("RLog", "parseDynamicColor: ${e.message}")
it.value
}

View File

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

View File

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

View File

@ -79,7 +79,7 @@
<string name="accounts">账户</string>
<string name="accounts_desc">本地、FreshRSS</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_desc">启动时、触感反馈</string>
<string name="languages">语言</string>
@ -99,7 +99,7 @@
<string name="primary_color_hint">例如 #666666 或 666666</string>
<string name="appearance">外观</string>
<string name="style">样式</string>
<string name="dark_theme">深色模式</string>
<string name="dark_theme">深色主题</string>
<string name="use_device_theme">跟随系统设置</string>
<string name="tonal_elevation">色调海拔</string>
<string name="fonts">字体</string>
@ -124,4 +124,29 @@
<string name="on_start">启动时</string>
<string name="initial_page">起始页面</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>

View File

@ -79,7 +79,7 @@
<string name="accounts">Accounts</string>
<string name="accounts_desc">Local, FreshRSS</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_desc">On start, haptic feedback</string>
<string name="languages">Languages</string>
@ -88,9 +88,9 @@
<string name="tips_and_support_desc">About, open source</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="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="agree_and_continue">Agree and Continue</string>
<string name="agree_and_continue">Agree</string>
<string name="wallpaper_colors">Wallpaper Colors</string>
<string name="no_palettes">No Palettes</string>
<string name="only_android_8.1_plus">Only Android 8.1+</string>
@ -116,7 +116,7 @@
<string name="update">Update</string>
<string name="skip_this_version">Skip This Version</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="download_failure">Download failure</string>
<string name="rate_limit">The request rate is limited</string>
@ -124,4 +124,29 @@
<string name="on_start">On Start</string>
<string name="initial_page">Initial Page</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>