Add colors switch feature
This commit is contained in:
parent
f903238a59
commit
00231970cd
|
@ -68,9 +68,9 @@ class App : Application(), Configuration.Provider {
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
CrashHandler(this)
|
CrashHandler(this)
|
||||||
|
dataStoreInit()
|
||||||
applicationScope.launch(dispatcherDefault) {
|
applicationScope.launch(dispatcherDefault) {
|
||||||
accountInit()
|
accountInit()
|
||||||
dataStoreInit()
|
|
||||||
workerInit()
|
workerInit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package me.ash.reader
|
package me.ash.reader
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import java.lang.Thread.UncaughtExceptionHandler
|
import java.lang.Thread.UncaughtExceptionHandler
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
@ -13,6 +14,7 @@ class CrashHandler(private val context: Context) : UncaughtExceptionHandler {
|
||||||
override fun uncaughtException(p0: Thread, p1: Throwable) {
|
override fun uncaughtException(p0: Thread, p1: Throwable) {
|
||||||
Toast.makeText(context, p1.message, Toast.LENGTH_LONG).show()
|
Toast.makeText(context, p1.message, Toast.LENGTH_LONG).show()
|
||||||
p1.printStackTrace()
|
p1.printStackTrace()
|
||||||
|
Log.e("RLog", "uncaughtException: ${p1.message}" )
|
||||||
android.os.Process.killProcess(android.os.Process.myPid());
|
android.os.Process.killProcess(android.os.Process.myPid());
|
||||||
exitProcess(1)
|
exitProcess(1)
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,10 +19,12 @@ import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import me.ash.reader.ui.theme.palette.onDark
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Banner(
|
fun Banner(
|
||||||
|
@ -33,20 +35,18 @@ fun Banner(
|
||||||
action: (@Composable () -> Unit)? = null,
|
action: (@Composable () -> Unit)? = null,
|
||||||
onClick: () -> Unit = {},
|
onClick: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val lightThemeColors = MaterialTheme.colorScheme
|
|
||||||
val lightPrimaryContainer = lightThemeColors.primaryContainer
|
|
||||||
val lightOnSurface = lightThemeColors.onSurface
|
|
||||||
|
|
||||||
Surface(
|
Surface(
|
||||||
modifier = modifier.fillMaxWidth().height(88.dp),
|
modifier = modifier
|
||||||
color = MaterialTheme.colorScheme.surface,
|
.fillMaxWidth()
|
||||||
|
.height(88.dp),
|
||||||
|
color = Color.Unspecified,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
.clip(RoundedCornerShape(32.dp))
|
.clip(RoundedCornerShape(32.dp))
|
||||||
.background(lightPrimaryContainer)
|
.background(MaterialTheme.colorScheme.primaryContainer onDark MaterialTheme.colorScheme.onPrimaryContainer)
|
||||||
.clickable { onClick() }
|
.clickable { onClick() }
|
||||||
.padding(16.dp, 20.dp),
|
.padding(16.dp, 20.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
@ -57,26 +57,29 @@ fun Banner(
|
||||||
imageVector = it,
|
imageVector = it,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.padding(end = 16.dp),
|
modifier = Modifier.padding(end = 16.dp),
|
||||||
tint = lightOnSurface,
|
tint = MaterialTheme.colorScheme.onSurface onDark MaterialTheme.colorScheme.surface,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.weight(1f).fillMaxHeight(),
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.fillMaxHeight(),
|
||||||
verticalArrangement = Arrangement.SpaceBetween,
|
verticalArrangement = Arrangement.SpaceBetween,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = title,
|
text = title,
|
||||||
maxLines = if (desc == null) 2 else 1,
|
maxLines = if (desc == null) 2 else 1,
|
||||||
style = MaterialTheme.typography.titleLarge.copy(fontSize = 20.sp),
|
style = MaterialTheme.typography.titleLarge.copy(fontSize = 20.sp),
|
||||||
color = lightOnSurface,
|
color = MaterialTheme.colorScheme.onSurface onDark MaterialTheme.colorScheme.surface,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
desc?.let {
|
desc?.let {
|
||||||
Text(
|
Text(
|
||||||
text = it,
|
text = it,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = lightOnSurface.copy(alpha = 0.7f),
|
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
|
||||||
|
onDark MaterialTheme.colorScheme.surface.copy(alpha = 0.7f),
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
|
@ -84,9 +87,12 @@ fun Banner(
|
||||||
}
|
}
|
||||||
action?.let {
|
action?.let {
|
||||||
Box(Modifier.padding(start = 16.dp)) {
|
Box(Modifier.padding(start = 16.dp)) {
|
||||||
CompositionLocalProvider(LocalContentColor provides lightOnSurface) {
|
CompositionLocalProvider(
|
||||||
it()
|
LocalContentColor provides (
|
||||||
}
|
MaterialTheme.colorScheme.onSurface
|
||||||
|
onDark MaterialTheme.colorScheme.surface
|
||||||
|
)
|
||||||
|
) { it() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
45
app/src/main/java/me/ash/reader/ui/component/BlockButton.kt
Normal file
45
app/src/main/java/me/ash/reader/ui/component/BlockButton.kt
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
package me.ash.reader.ui.component
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import me.ash.reader.ui.theme.palette.onDark
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BlockButton(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
text: String = "",
|
||||||
|
selected: Boolean = false,
|
||||||
|
containerColor: Color = MaterialTheme.colorScheme.surface.copy(0.7f) onDark MaterialTheme.colorScheme.inverseOnSurface,
|
||||||
|
selectedContainerColor: Color = MaterialTheme.colorScheme.primaryContainer onDark MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
|
contentColor: Color = MaterialTheme.colorScheme.inverseSurface,
|
||||||
|
selectedContentColor: Color = MaterialTheme.colorScheme.onSurface onDark MaterialTheme.colorScheme.surface,
|
||||||
|
onClick: () -> Unit = {},
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.height(56.dp)
|
||||||
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
.background(if (selected) selectedContainerColor else containerColor)
|
||||||
|
.clickable(onClick = onClick),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
color = if (selected) selectedContentColor else contentColor,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
package me.ash.reader.ui.component
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BlockButtonRadios(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
selected: Int = 0,
|
||||||
|
onSelected: (Int) -> Unit,
|
||||||
|
items: List<BlockButtonRadiosItem> = listOf(),
|
||||||
|
) {
|
||||||
|
|
||||||
|
Column {
|
||||||
|
Row(
|
||||||
|
modifier = modifier.padding(horizontal = 24.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
items.forEachIndexed { index, item ->
|
||||||
|
BlockButton(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(end = if (item == items.last()) 0.dp else 8.dp),
|
||||||
|
text = item.text,
|
||||||
|
selected = selected == index,
|
||||||
|
) {
|
||||||
|
onSelected(index)
|
||||||
|
item.onClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
items[selected].content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class BlockButtonRadiosItem(
|
||||||
|
val text: String,
|
||||||
|
val onClick: () -> Unit = {},
|
||||||
|
val content: @Composable () -> Unit,
|
||||||
|
)
|
106
app/src/main/java/me/ash/reader/ui/component/Switch.kt
Normal file
106
app/src/main/java/me/ash/reader/ui/component/Switch.kt
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2021 Kyant0
|
||||||
|
*
|
||||||
|
* @link https://github.com/Kyant0/MusicYou
|
||||||
|
* @author Kyant0
|
||||||
|
* @modifier Ashinch
|
||||||
|
*/
|
||||||
|
|
||||||
|
package me.ash.reader.ui.component
|
||||||
|
|
||||||
|
import androidx.compose.animation.animateColorAsState
|
||||||
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import me.ash.reader.ui.theme.palette.onDark
|
||||||
|
import me.ash.reader.ui.theme.palette.tonalPalettes
|
||||||
|
|
||||||
|
// TODO: ripple & swipe
|
||||||
|
@Composable
|
||||||
|
fun Switch(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
activated: Boolean,
|
||||||
|
enable: Boolean = true,
|
||||||
|
onClick: (() -> Unit)? = null
|
||||||
|
) {
|
||||||
|
val tonalPalettes = MaterialTheme.colorScheme.tonalPalettes()
|
||||||
|
|
||||||
|
Surface(
|
||||||
|
modifier = modifier.size(56.dp, 28.dp).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)
|
||||||
|
).value
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
then if (onClick != null) Modifier.clickable { onClick() } else Modifier
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(20.dp)
|
||||||
|
.align(Alignment.CenterStart)
|
||||||
|
.offset(x = animateDpAsState(if (activated) 32.dp else 4.dp).value),
|
||||||
|
shape = CircleShape,
|
||||||
|
color = animateColorAsState(
|
||||||
|
if (activated) tonalPalettes primary 90
|
||||||
|
else (tonalPalettes neutralVariant 70) onDark (tonalPalettes neutral 30)
|
||||||
|
).value
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: inactivated colors
|
||||||
|
@Composable
|
||||||
|
fun SwitchHeadline(
|
||||||
|
activated: Boolean,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
title: String,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val tonalPalettes = MaterialTheme.colorScheme.tonalPalettes()
|
||||||
|
|
||||||
|
Surface(
|
||||||
|
modifier = modifier,
|
||||||
|
color = Color.Unspecified,
|
||||||
|
contentColor = tonalPalettes neutral 10,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.clip(RoundedCornerShape(24.dp))
|
||||||
|
.background(tonalPalettes primary 90)
|
||||||
|
.clickable { onClick() }
|
||||||
|
.padding(20.dp, 24.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
maxLines = 2,
|
||||||
|
style = MaterialTheme.typography.titleLarge.copy(fontSize = 20.sp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Box(Modifier.padding(start = 20.dp)) {
|
||||||
|
Switch(activated = activated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,6 +18,10 @@ val Context.currentAccountId: Int
|
||||||
get() = this.dataStore.get(DataStoreKeys.CurrentAccountId)!!
|
get() = this.dataStore.get(DataStoreKeys.CurrentAccountId)!!
|
||||||
val Context.currentAccountType: Int
|
val Context.currentAccountType: Int
|
||||||
get() = this.dataStore.get(DataStoreKeys.CurrentAccountType)!!
|
get() = this.dataStore.get(DataStoreKeys.CurrentAccountType)!!
|
||||||
|
val Context.themeIndex: Int
|
||||||
|
get() = this.dataStore.get(DataStoreKeys.ThemeIndex) ?: 0
|
||||||
|
val Context.customPrimaryColor: String
|
||||||
|
get() = this.dataStore.get(DataStoreKeys.CustomPrimaryColor) ?: ""
|
||||||
|
|
||||||
suspend fun <T> DataStore<Preferences>.put(dataStoreKeys: DataStoreKeys<T>, value: T) {
|
suspend fun <T> DataStore<Preferences>.put(dataStoreKeys: DataStoreKeys<T>, value: T) {
|
||||||
this.edit {
|
this.edit {
|
||||||
|
@ -25,6 +29,14 @@ suspend fun <T> DataStore<Preferences>.put(dataStoreKeys: DataStoreKeys<T>, valu
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun <T> DataStore<Preferences>.putBlocking(dataStoreKeys: DataStoreKeys<T>, value: T) {
|
||||||
|
runBlocking {
|
||||||
|
this@putBlocking.edit {
|
||||||
|
it[dataStoreKeys.key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
fun <T> DataStore<Preferences>.get(dataStoreKeys: DataStoreKeys<T>): T? {
|
fun <T> DataStore<Preferences>.get(dataStoreKeys: DataStoreKeys<T>): T? {
|
||||||
return runBlocking {
|
return runBlocking {
|
||||||
|
@ -59,4 +71,14 @@ sealed class DataStoreKeys<T> {
|
||||||
override val key: Preferences.Key<Int>
|
override val key: Preferences.Key<Int>
|
||||||
get() = intPreferencesKey("currentAccountType")
|
get() = intPreferencesKey("currentAccountType")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
object ThemeIndex : DataStoreKeys<Int>() {
|
||||||
|
override val key: Preferences.Key<Int>
|
||||||
|
get() = intPreferencesKey("themeIndex")
|
||||||
|
}
|
||||||
|
|
||||||
|
object CustomPrimaryColor : DataStoreKeys<String>() {
|
||||||
|
override val key: Preferences.Key<String>
|
||||||
|
get() = stringPreferencesKey("customPrimaryColor")
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -14,13 +14,13 @@ import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import com.google.accompanist.insets.ProvideWindowInsets
|
import com.google.accompanist.insets.ProvideWindowInsets
|
||||||
import com.google.accompanist.insets.navigationBarsHeight
|
import com.google.accompanist.insets.navigationBarsHeight
|
||||||
import com.google.accompanist.insets.statusBarsPadding
|
|
||||||
import com.google.accompanist.navigation.animation.AnimatedNavHost
|
import com.google.accompanist.navigation.animation.AnimatedNavHost
|
||||||
import com.google.accompanist.navigation.animation.rememberAnimatedNavController
|
import com.google.accompanist.navigation.animation.rememberAnimatedNavController
|
||||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||||
import me.ash.reader.ui.ext.animatedComposable
|
import me.ash.reader.ui.ext.animatedComposable
|
||||||
import me.ash.reader.ui.ext.isFirstLaunch
|
import me.ash.reader.ui.ext.isFirstLaunch
|
||||||
import me.ash.reader.ui.page.home.HomePage
|
import me.ash.reader.ui.page.home.HomePage
|
||||||
|
import me.ash.reader.ui.page.settings.ColorAndStyle
|
||||||
import me.ash.reader.ui.page.settings.SettingsPage
|
import me.ash.reader.ui.page.settings.SettingsPage
|
||||||
import me.ash.reader.ui.page.startup.StartupPage
|
import me.ash.reader.ui.page.startup.StartupPage
|
||||||
import me.ash.reader.ui.theme.AppTheme
|
import me.ash.reader.ui.theme.AppTheme
|
||||||
|
@ -38,11 +38,11 @@ fun HomeEntry() {
|
||||||
setSystemBarsColor(Color.Transparent, !isSystemInDarkTheme())
|
setSystemBarsColor(Color.Transparent, !isSystemInDarkTheme())
|
||||||
setNavigationBarColor(MaterialTheme.colorScheme.surface, !isSystemInDarkTheme())
|
setNavigationBarColor(MaterialTheme.colorScheme.surface, !isSystemInDarkTheme())
|
||||||
}
|
}
|
||||||
Column(modifier = Modifier.background(MaterialTheme.colorScheme.surface)) {
|
Column {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.statusBarsPadding()
|
.background(MaterialTheme.colorScheme.surface),
|
||||||
) {
|
) {
|
||||||
AnimatedNavHost(
|
AnimatedNavHost(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
|
@ -57,12 +57,16 @@ fun HomeEntry() {
|
||||||
animatedComposable(route = RouteName.SETTINGS) {
|
animatedComposable(route = RouteName.SETTINGS) {
|
||||||
SettingsPage(navController)
|
SettingsPage(navController)
|
||||||
}
|
}
|
||||||
|
animatedComposable(route = RouteName.COLOR_AND_STYLE) {
|
||||||
|
ColorAndStyle(navController)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer(
|
Spacer(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.navigationBarsHeight()
|
.navigationBarsHeight()
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
.background(MaterialTheme.colorScheme.surface)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,4 +7,5 @@ object RouteName {
|
||||||
const val ARTICLE = "article"
|
const val ARTICLE = "article"
|
||||||
const val READ = "read"
|
const val READ = "read"
|
||||||
const val SETTINGS = "settings"
|
const val SETTINGS = "settings"
|
||||||
|
const val COLOR_AND_STYLE = "color_and_style"
|
||||||
}
|
}
|
|
@ -54,13 +54,13 @@ fun FilterBar(
|
||||||
view.playSoundEffect(SoundEffectConstants.CLICK)
|
view.playSoundEffect(SoundEffectConstants.CLICK)
|
||||||
filterOnClick(item)
|
filterOnClick(item)
|
||||||
},
|
},
|
||||||
// colors = NavigationBarItemDefaults.colors(
|
colors = NavigationBarItemDefaults.colors(
|
||||||
// selectedIconColor = MaterialTheme.colorScheme.onSecondaryContainer,
|
// selectedIconColor = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||||
// unselectedIconColor = MaterialTheme.colorScheme.outline,
|
// unselectedIconColor = MaterialTheme.colorScheme.outline,
|
||||||
// selectedTextColor = MaterialTheme.colorScheme.onSurface,
|
// selectedTextColor = MaterialTheme.colorScheme.onSurface,
|
||||||
// unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
// unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
// indicatorColor = MaterialTheme.colorScheme.secondaryContainer,
|
indicatorColor = MaterialTheme.colorScheme.primaryContainer,
|
||||||
// )
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.width(60.dp))
|
Spacer(modifier = Modifier.width(60.dp))
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
package me.ash.reader.ui.page.home
|
package me.ash.reader.ui.page.home
|
||||||
|
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
|
import com.google.accompanist.insets.statusBarsPadding
|
||||||
import com.google.accompanist.pager.ExperimentalPagerApi
|
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import me.ash.reader.ui.component.ViewPager
|
import me.ash.reader.ui.component.ViewPager
|
||||||
|
@ -91,7 +94,11 @@ fun HomePage(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(MaterialTheme.colorScheme.surface)
|
||||||
|
.statusBarsPadding(),
|
||||||
|
) {
|
||||||
ViewPager(
|
ViewPager(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
state = viewState.pagerState,
|
state = viewState.pagerState,
|
||||||
|
|
|
@ -0,0 +1,340 @@
|
||||||
|
package me.ash.reader.ui.page.settings
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.compose.animation.*
|
||||||
|
import androidx.compose.foundation.*
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Check
|
||||||
|
import androidx.compose.material.icons.outlined.Palette
|
||||||
|
import androidx.compose.material.icons.rounded.ArrowBack
|
||||||
|
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.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
|
import com.google.accompanist.insets.statusBarsPadding
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import me.ash.reader.R
|
||||||
|
import me.ash.reader.ui.component.*
|
||||||
|
import me.ash.reader.ui.ext.*
|
||||||
|
import me.ash.reader.ui.theme.palette.*
|
||||||
|
import me.ash.reader.ui.theme.palette.TonalPalettes.Companion.toTonalPalettes
|
||||||
|
import me.ash.reader.ui.theme.palette.dynamic.extractTonalPalettesFromUserWallpaper
|
||||||
|
|
||||||
|
@SuppressLint("FlowOperatorInvokedInComposition")
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun ColorAndStyle(
|
||||||
|
navController: NavHostController,
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val wallpaperTonalPalettes = extractTonalPalettesFromUserWallpaper()
|
||||||
|
var radioButtonSelected by remember { mutableStateOf(if (context.themeIndex > 4) 0 else 1) }
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(MaterialTheme.colorScheme.surface onLight MaterialTheme.colorScheme.inverseOnSurface)
|
||||||
|
.statusBarsPadding(),
|
||||||
|
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.color_and_style), desc = "")
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 24.dp)
|
||||||
|
.aspectRatio(1.38f)
|
||||||
|
.clip(RoundedCornerShape(24.dp))
|
||||||
|
.background(
|
||||||
|
MaterialTheme.colorScheme.inverseOnSurface
|
||||||
|
onLight MaterialTheme.colorScheme.surface.copy(0.7f)
|
||||||
|
)
|
||||||
|
.clickable { },
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
modifier = Modifier.padding(60.dp),
|
||||||
|
painter = painterResource(id = R.drawable.palettie),
|
||||||
|
contentDescription = stringResource(R.string.welcome),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
BlockButtonRadios(
|
||||||
|
selected = radioButtonSelected,
|
||||||
|
onSelected = { radioButtonSelected = it },
|
||||||
|
items = listOf(
|
||||||
|
BlockButtonRadiosItem(
|
||||||
|
text = stringResource(R.string.wallpaper_colors),
|
||||||
|
onClick = {},
|
||||||
|
) {
|
||||||
|
Palettes(
|
||||||
|
palettes = wallpaperTonalPalettes.run {
|
||||||
|
if (this.size > 5) {
|
||||||
|
this.subList(5, wallpaperTonalPalettes.size)
|
||||||
|
} else {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
themeIndexPrefix = 5,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
BlockButtonRadiosItem(
|
||||||
|
text = stringResource(R.string.basic_colors),
|
||||||
|
onClick = {},
|
||||||
|
) {
|
||||||
|
Palettes(
|
||||||
|
palettes = wallpaperTonalPalettes.subList(0, 5)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
Subtitle(
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp),
|
||||||
|
text = stringResource(R.string.style)
|
||||||
|
)
|
||||||
|
SettingItem(
|
||||||
|
title = stringResource(R.string.dark_theme),
|
||||||
|
desc = stringResource(R.string.use_device_theme),
|
||||||
|
enable = false,
|
||||||
|
separatedActions = true,
|
||||||
|
onClick = {},
|
||||||
|
) {
|
||||||
|
Switch(activated = isSystemInDarkTheme(), enable = false)
|
||||||
|
}
|
||||||
|
SettingItem(
|
||||||
|
title = stringResource(R.string.tonal_elevation),
|
||||||
|
enable = false,
|
||||||
|
onClick = {},
|
||||||
|
) {}
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
Subtitle(
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp),
|
||||||
|
text = stringResource(R.string.fonts)
|
||||||
|
)
|
||||||
|
SettingItem(
|
||||||
|
title = stringResource(R.string.basic_fonts),
|
||||||
|
desc = "Google Sans",
|
||||||
|
enable = false,
|
||||||
|
onClick = {},
|
||||||
|
) {}
|
||||||
|
SettingItem(
|
||||||
|
title = stringResource(R.string.reading_fonts),
|
||||||
|
desc = "Google Sans",
|
||||||
|
enable = false,
|
||||||
|
onClick = {},
|
||||||
|
) {}
|
||||||
|
SettingItem(
|
||||||
|
title = stringResource(R.string.reading_fonts_size),
|
||||||
|
desc = "16sp",
|
||||||
|
enable = false,
|
||||||
|
onClick = {},
|
||||||
|
) {}
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("FlowOperatorInvokedInComposition")
|
||||||
|
@Composable
|
||||||
|
fun Palettes(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
palettes: List<TonalPalettes>,
|
||||||
|
themeIndexPrefix: Int = 0,
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val themeIndex = context.dataStore.data
|
||||||
|
.map { it[DataStoreKeys.ThemeIndex.key] ?: 0 }
|
||||||
|
.collectAsState(initial = 0).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) }
|
||||||
|
|
||||||
|
if (palettes.isEmpty()) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 24.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(80.dp)
|
||||||
|
.clip(RoundedCornerShape(16.dp))
|
||||||
|
.background(
|
||||||
|
MaterialTheme.colorScheme.inverseOnSurface
|
||||||
|
onLight MaterialTheme.colorScheme.surface.copy(0.7f),
|
||||||
|
)
|
||||||
|
.clickable {},
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1)
|
||||||
|
stringResource(R.string.no_palettes)
|
||||||
|
else stringResource(R.string.only_android_8_1_plus),
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
color = MaterialTheme.colorScheme.inverseSurface,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.horizontalScroll(rememberScrollState())
|
||||||
|
.padding(horizontal = 24.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
palettes.forEachIndexed { index, palette ->
|
||||||
|
val isCustom = index == palettes.lastIndex && themeIndexPrefix == 0
|
||||||
|
val i = themeIndex - themeIndexPrefix
|
||||||
|
SelectableMiniPalette(
|
||||||
|
selected = if (i >= palettes.size) i == 0 else i == index,
|
||||||
|
isCustom = isCustom,
|
||||||
|
onClick = {
|
||||||
|
if (isCustom) {
|
||||||
|
addDialogVisible = true
|
||||||
|
} else {
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
context.dataStore.put(
|
||||||
|
DataStoreKeys.ThemeIndex,
|
||||||
|
themeIndexPrefix + index
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
palette = if (isCustom) tonalPalettes else palette
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TextFieldDialog(
|
||||||
|
visible = addDialogVisible,
|
||||||
|
title = "强调色",
|
||||||
|
icon = Icons.Outlined.Palette,
|
||||||
|
value = customColorValue,
|
||||||
|
placeholder = "#123456",
|
||||||
|
onValueChange = {
|
||||||
|
customColorValue = it
|
||||||
|
},
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
addDialogVisible = false
|
||||||
|
customColorValue = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SelectableMiniPalette(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
selected: Boolean,
|
||||||
|
isCustom: Boolean = false,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
palette: TonalPalettes,
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
modifier = modifier,
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
color = if (isCustom) {
|
||||||
|
MaterialTheme.colorScheme.primaryContainer
|
||||||
|
.copy(0.5f) onDark MaterialTheme.colorScheme.onPrimaryContainer.copy(0.3f)
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme
|
||||||
|
.inverseOnSurface onLight MaterialTheme.colorScheme.surface.copy(0.7f)
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable { onClick() }
|
||||||
|
.padding(16.dp)
|
||||||
|
.size(48.dp),
|
||||||
|
shape = CircleShape,
|
||||||
|
color = palette primary 90,
|
||||||
|
) {
|
||||||
|
Box {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(48.dp)
|
||||||
|
.offset((-24).dp, 24.dp),
|
||||||
|
color = palette tertiary 90,
|
||||||
|
) {}
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(48.dp)
|
||||||
|
.offset(24.dp, 24.dp),
|
||||||
|
color = palette secondary 50,
|
||||||
|
) {}
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = selected,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.Center)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(MaterialTheme.colorScheme.primary),
|
||||||
|
enter = fadeIn() + expandIn(expandFrom = Alignment.Center),
|
||||||
|
exit = shrinkOut(shrinkTowards = Alignment.Center) + fadeOut()
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Check,
|
||||||
|
contentDescription = "Checked",
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(8.dp)
|
||||||
|
.size(16.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.surface
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,7 +6,7 @@
|
||||||
* @modifier Ashinch
|
* @modifier Ashinch
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.ash.reader.ui.component
|
package me.ash.reader.ui.page.settings
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
@ -22,6 +22,7 @@ import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
@ -30,6 +31,7 @@ import androidx.compose.ui.unit.sp
|
||||||
@Composable
|
@Composable
|
||||||
fun SelectableSettingGroupItem(
|
fun SelectableSettingGroupItem(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
enable: Boolean = true,
|
||||||
selected: Boolean = false,
|
selected: Boolean = false,
|
||||||
title: String,
|
title: String,
|
||||||
desc: String? = null,
|
desc: String? = null,
|
||||||
|
@ -37,7 +39,7 @@ fun SelectableSettingGroupItem(
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
modifier = modifier.clickable { onClick() },
|
modifier = modifier.clickable { onClick() }.alpha(if (enable) 1f else 0.5f),
|
||||||
color = Color.Unspecified,
|
color = Color.Unspecified,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
|
@ -45,7 +47,7 @@ fun SelectableSettingGroupItem(
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
.background(
|
.background(
|
||||||
color = if (selected) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.surface,
|
color = if (selected) MaterialTheme.colorScheme.onSurface else Color.Unspecified,
|
||||||
shape = RoundedCornerShape(24.dp)
|
shape = RoundedCornerShape(24.dp)
|
||||||
)
|
)
|
||||||
.padding(8.dp, 16.dp),
|
.padding(8.dp, 16.dp),
|
|
@ -0,0 +1,81 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2021 Kyant0
|
||||||
|
*
|
||||||
|
* @link https://github.com/Kyant0/MusicYou
|
||||||
|
* @author Kyant0
|
||||||
|
* @modifier Ashinch
|
||||||
|
*/
|
||||||
|
|
||||||
|
package me.ash.reader.ui.page.settings
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SettingItem(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
enable: Boolean = true,
|
||||||
|
title: String,
|
||||||
|
desc: String? = null,
|
||||||
|
icon: ImageVector? = null,
|
||||||
|
separatedActions: Boolean = false,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
action: (@Composable () -> Unit)? = null
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
modifier = modifier.clickable { onClick() }.alpha(if (enable) 1f else 0.5f),
|
||||||
|
color = Color.Unspecified
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(24.dp, 16.dp, 16.dp, 16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
icon?.let {
|
||||||
|
Icon(
|
||||||
|
imageVector = it,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.padding(end = 24.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
maxLines = if (desc == null) 2 else 1,
|
||||||
|
style = MaterialTheme.typography.titleLarge.copy(fontSize = 20.sp)
|
||||||
|
)
|
||||||
|
desc?.let {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
maxLines = 1,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
action?.let {
|
||||||
|
if (separatedActions) {
|
||||||
|
Divider(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(start = 16.dp)
|
||||||
|
.size(1.dp, 32.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Box(Modifier.padding(start = 16.dp)) {
|
||||||
|
it()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,12 +14,13 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
|
import com.google.accompanist.insets.statusBarsPadding
|
||||||
import me.ash.reader.R
|
import me.ash.reader.R
|
||||||
import me.ash.reader.ui.component.Banner
|
import me.ash.reader.ui.component.Banner
|
||||||
import me.ash.reader.ui.component.DisplayText
|
import me.ash.reader.ui.component.DisplayText
|
||||||
import me.ash.reader.ui.component.FeedbackIconButton
|
import me.ash.reader.ui.component.FeedbackIconButton
|
||||||
import me.ash.reader.ui.component.SelectableSettingGroupItem
|
|
||||||
import me.ash.reader.ui.page.common.RouteName
|
import me.ash.reader.ui.page.common.RouteName
|
||||||
|
import me.ash.reader.ui.theme.palette.onLight
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -27,9 +28,13 @@ fun SettingsPage(
|
||||||
navController: NavHostController,
|
navController: NavHostController,
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = Modifier.background(MaterialTheme.colorScheme.surface),
|
modifier = Modifier
|
||||||
|
.background(MaterialTheme.colorScheme.surface onLight MaterialTheme.colorScheme.inverseOnSurface)
|
||||||
|
.statusBarsPadding(),
|
||||||
|
containerColor = MaterialTheme.colorScheme.surface onLight MaterialTheme.colorScheme.inverseOnSurface,
|
||||||
topBar = {
|
topBar = {
|
||||||
SmallTopAppBar(
|
SmallTopAppBar(
|
||||||
|
colors = TopAppBarDefaults.smallTopAppBarColors(containerColor = MaterialTheme.colorScheme.surface onLight MaterialTheme.colorScheme.inverseOnSurface),
|
||||||
title = {},
|
title = {},
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
FeedbackIconButton(
|
FeedbackIconButton(
|
||||||
|
@ -67,6 +72,7 @@ fun SettingsPage(
|
||||||
title = stringResource(R.string.accounts),
|
title = stringResource(R.string.accounts),
|
||||||
desc = stringResource(R.string.accounts_desc),
|
desc = stringResource(R.string.accounts_desc),
|
||||||
icon = Icons.Outlined.AccountCircle,
|
icon = Icons.Outlined.AccountCircle,
|
||||||
|
enable = false,
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
|
@ -74,13 +80,16 @@ fun SettingsPage(
|
||||||
title = stringResource(R.string.color_and_style),
|
title = stringResource(R.string.color_and_style),
|
||||||
desc = stringResource(R.string.color_and_style_desc),
|
desc = stringResource(R.string.color_and_style_desc),
|
||||||
icon = Icons.Outlined.Palette,
|
icon = Icons.Outlined.Palette,
|
||||||
) {}
|
) {
|
||||||
|
navController.navigate(RouteName.COLOR_AND_STYLE)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
SelectableSettingGroupItem(
|
SelectableSettingGroupItem(
|
||||||
title = stringResource(R.string.interaction),
|
title = stringResource(R.string.interaction),
|
||||||
desc = stringResource(R.string.interaction_desc),
|
desc = stringResource(R.string.interaction_desc),
|
||||||
icon = Icons.Outlined.TouchApp,
|
icon = Icons.Outlined.TouchApp,
|
||||||
|
enable = false,
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
|
@ -88,6 +97,7 @@ fun SettingsPage(
|
||||||
title = stringResource(R.string.languages),
|
title = stringResource(R.string.languages),
|
||||||
desc = stringResource(R.string.languages_desc),
|
desc = stringResource(R.string.languages_desc),
|
||||||
icon = Icons.Outlined.Language,
|
icon = Icons.Outlined.Language,
|
||||||
|
enable = false,
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
|
@ -95,6 +105,7 @@ fun SettingsPage(
|
||||||
title = stringResource(R.string.tips_and_support),
|
title = stringResource(R.string.tips_and_support),
|
||||||
desc = stringResource(R.string.tips_and_support_desc),
|
desc = stringResource(R.string.tips_and_support_desc),
|
||||||
icon = Icons.Outlined.TipsAndUpdates,
|
icon = Icons.Outlined.TipsAndUpdates,
|
||||||
|
enable = false,
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,13 +12,13 @@ import androidx.compose.material.icons.rounded.CheckCircleOutline
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
|
import com.google.accompanist.insets.statusBarsPadding
|
||||||
import com.ireward.htmlcompose.HtmlText
|
import com.ireward.htmlcompose.HtmlText
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import me.ash.reader.R
|
import me.ash.reader.R
|
||||||
|
@ -37,7 +37,7 @@ fun StartupPage(
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = Modifier.background(MaterialTheme.colorScheme.surface),
|
modifier = Modifier.statusBarsPadding().background(MaterialTheme.colorScheme.surface),
|
||||||
topBar = {},
|
topBar = {},
|
||||||
content = {
|
content = {
|
||||||
LazyColumn {
|
LazyColumn {
|
||||||
|
@ -87,13 +87,16 @@ fun StartupPage(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
Row(
|
// Row(
|
||||||
modifier = Modifier
|
// modifier = Modifier
|
||||||
.fillMaxWidth()
|
// .fillMaxWidth()
|
||||||
.padding(24.dp),
|
// .padding(24.dp),
|
||||||
horizontalArrangement = Arrangement.End,
|
// horizontalArrangement = Arrangement.End,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
// verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
// ) {
|
||||||
|
// }
|
||||||
|
},
|
||||||
|
floatingActionButton = {
|
||||||
ExtendedFloatingActionButton(
|
ExtendedFloatingActionButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
navController.navigate(route = RouteName.HOME)
|
navController.navigate(route = RouteName.HOME)
|
||||||
|
@ -110,7 +113,6 @@ fun StartupPage(
|
||||||
text = { Text(text = stringResource(R.string.agree_and_continue)) },
|
text = { Text(text = stringResource(R.string.agree_and_continue)) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,51 +1,54 @@
|
||||||
package me.ash.reader.ui.theme
|
package me.ash.reader.ui.theme
|
||||||
|
|
||||||
import android.os.Build
|
import android.annotation.SuppressLint
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.dynamicDarkColorScheme
|
|
||||||
import androidx.compose.material3.dynamicLightColorScheme
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.staticCompositionLocalOf
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import me.ash.reader.ui.theme.color.PurpleColor
|
import kotlinx.coroutines.flow.map
|
||||||
|
import me.ash.reader.ui.ext.DataStoreKeys
|
||||||
private val LightThemeColors = PurpleColor.lightColorScheme
|
import me.ash.reader.ui.ext.dataStore
|
||||||
private val DarkThemeColors = PurpleColor.darkColorScheme
|
import me.ash.reader.ui.theme.palette.LocalTonalPalettes
|
||||||
|
import me.ash.reader.ui.theme.palette.TonalPalettes
|
||||||
val LocalLightThemeColors = staticCompositionLocalOf { LightThemeColors }
|
import me.ash.reader.ui.theme.palette.core.ProvideZcamViewingConditions
|
||||||
val LocalDarkThemeColors = staticCompositionLocalOf { DarkThemeColors }
|
import me.ash.reader.ui.theme.palette.dynamic.extractTonalPalettesFromUserWallpaper
|
||||||
|
import me.ash.reader.ui.theme.palette.dynamicDarkColorScheme
|
||||||
|
import me.ash.reader.ui.theme.palette.dynamicLightColorScheme
|
||||||
|
|
||||||
|
@SuppressLint("FlowOperatorInvokedInComposition")
|
||||||
@Composable
|
@Composable
|
||||||
fun AppTheme(
|
fun AppTheme(
|
||||||
useDarkTheme: Boolean = isSystemInDarkTheme(),
|
useDarkTheme: Boolean = isSystemInDarkTheme(),
|
||||||
|
wallpaperPalettes: List<TonalPalettes> = extractTonalPalettesFromUserWallpaper(),
|
||||||
content: @Composable () -> Unit
|
content: @Composable () -> Unit
|
||||||
) {
|
) {
|
||||||
// Dynamic color is available on Android 12+
|
val context = LocalContext.current
|
||||||
val dynamicColor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
val themeIndex = context.dataStore.data.map { it[DataStoreKeys.ThemeIndex.key] ?: 0 }
|
||||||
|
.collectAsState(initial = 0).value
|
||||||
val light = when {
|
|
||||||
dynamicColor -> dynamicLightColorScheme(LocalContext.current)
|
|
||||||
else -> LightThemeColors
|
|
||||||
}
|
|
||||||
val dark = when {
|
|
||||||
dynamicColor -> dynamicDarkColorScheme(LocalContext.current)
|
|
||||||
else -> DarkThemeColors
|
|
||||||
}
|
|
||||||
val colorScheme = when {
|
|
||||||
useDarkTheme -> dark
|
|
||||||
else -> light
|
|
||||||
}
|
|
||||||
|
|
||||||
|
ProvideZcamViewingConditions {
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalLightThemeColors provides light,
|
LocalTonalPalettes provides wallpaperPalettes[
|
||||||
LocalDarkThemeColors provides dark,
|
if (themeIndex >= wallpaperPalettes.size) {
|
||||||
|
when {
|
||||||
|
wallpaperPalettes.size == 5 -> 0
|
||||||
|
wallpaperPalettes.size > 5 -> 5
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
themeIndex
|
||||||
|
}
|
||||||
|
]
|
||||||
) {
|
) {
|
||||||
MaterialTheme(
|
MaterialTheme(
|
||||||
colorScheme = colorScheme,
|
colorScheme =
|
||||||
|
if (useDarkTheme) dynamicDarkColorScheme()
|
||||||
|
else dynamicLightColorScheme(),
|
||||||
typography = AppTypography,
|
typography = AppTypography,
|
||||||
content = content
|
content = content
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
|
@ -0,0 +1,94 @@
|
||||||
|
package me.ash.reader.ui.theme.palette
|
||||||
|
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.material3.ColorScheme
|
||||||
|
import androidx.compose.material3.darkColorScheme
|
||||||
|
import androidx.compose.material3.lightColorScheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun dynamicLightColorScheme(): ColorScheme {
|
||||||
|
val palettes = LocalTonalPalettes.current
|
||||||
|
return lightColorScheme(
|
||||||
|
primary = palettes primary 40,
|
||||||
|
onPrimary = palettes primary 100,
|
||||||
|
primaryContainer = palettes primary 90,
|
||||||
|
onPrimaryContainer = palettes primary 10,
|
||||||
|
inversePrimary = palettes primary 80,
|
||||||
|
secondary = palettes secondary 40,
|
||||||
|
onSecondary = palettes secondary 100,
|
||||||
|
secondaryContainer = palettes secondary 90,
|
||||||
|
onSecondaryContainer = palettes secondary 10,
|
||||||
|
tertiary = palettes tertiary 40,
|
||||||
|
onTertiary = palettes tertiary 100,
|
||||||
|
tertiaryContainer = palettes tertiary 90,
|
||||||
|
onTertiaryContainer = palettes tertiary 10,
|
||||||
|
background = palettes neutral 99,
|
||||||
|
onBackground = palettes neutral 10,
|
||||||
|
surface = palettes neutral 99,
|
||||||
|
onSurface = palettes neutral 10,
|
||||||
|
surfaceVariant = palettes neutralVariant 90,
|
||||||
|
onSurfaceVariant = palettes neutralVariant 30,
|
||||||
|
inverseSurface = palettes neutral 20,
|
||||||
|
inverseOnSurface = palettes neutral 95,
|
||||||
|
outline = palettes neutralVariant 50,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun dynamicDarkColorScheme(): ColorScheme {
|
||||||
|
val palettes = LocalTonalPalettes.current
|
||||||
|
return darkColorScheme(
|
||||||
|
primary = palettes primary 80,
|
||||||
|
onPrimary = palettes primary 20,
|
||||||
|
primaryContainer = palettes primary 30,
|
||||||
|
onPrimaryContainer = palettes primary 90,
|
||||||
|
inversePrimary = palettes primary 40,
|
||||||
|
secondary = palettes secondary 80,
|
||||||
|
onSecondary = palettes secondary 20,
|
||||||
|
secondaryContainer = palettes secondary 30,
|
||||||
|
onSecondaryContainer = palettes secondary 90,
|
||||||
|
tertiary = palettes tertiary 80,
|
||||||
|
onTertiary = palettes tertiary 20,
|
||||||
|
tertiaryContainer = palettes tertiary 30,
|
||||||
|
onTertiaryContainer = palettes tertiary 90,
|
||||||
|
background = palettes neutral 10,
|
||||||
|
onBackground = palettes neutral 90,
|
||||||
|
surface = palettes neutral 10,
|
||||||
|
onSurface = palettes neutral 90,
|
||||||
|
surfaceVariant = palettes neutralVariant 30,
|
||||||
|
onSurfaceVariant = palettes neutralVariant 80,
|
||||||
|
inverseSurface = palettes neutral 90,
|
||||||
|
inverseOnSurface = palettes neutral 20,
|
||||||
|
outline = palettes neutralVariant 60,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ColorScheme.tonalPalettes() = LocalTonalPalettes.current
|
||||||
|
|
||||||
|
@Suppress("NOTHING_TO_INLINE")
|
||||||
|
@Composable
|
||||||
|
inline infix fun Color.onLight(lightColor: Color): Color =
|
||||||
|
if (!isSystemInDarkTheme()) lightColor else this
|
||||||
|
|
||||||
|
@Suppress("NOTHING_TO_INLINE")
|
||||||
|
@Composable
|
||||||
|
inline infix fun Color.onDark(darkColor: Color): Color =
|
||||||
|
if (isSystemInDarkTheme()) darkColor else this
|
||||||
|
|
||||||
|
fun String.checkColorHex(): String? {
|
||||||
|
var s = this.trim()
|
||||||
|
if (s.length > 6) {
|
||||||
|
s = s.substring(s.length - 6)
|
||||||
|
}
|
||||||
|
return "[0-9a-fA-F]{6}".toRegex().find(s)?.value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun String.safeHexToColor(): Color =
|
||||||
|
try {
|
||||||
|
Color(java.lang.Long.parseLong(this, 16))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Color.Transparent
|
||||||
|
}
|
|
@ -0,0 +1,116 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2021 Kyant0
|
||||||
|
*
|
||||||
|
* @link https://github.com/Kyant0/MusicYou
|
||||||
|
* @author Kyant0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package me.ash.reader.ui.theme.palette
|
||||||
|
|
||||||
|
object MaterialYouStandard {
|
||||||
|
// TODO: support RGB color spaces
|
||||||
|
// calculated with error = 0.001, chroma = 100, hue in 0 until 360, hue step = 1
|
||||||
|
val sRGBLightnessChromaMap = mapOf(
|
||||||
|
0 to 7.62939453125E-4,
|
||||||
|
1 to 3.5961087544759116,
|
||||||
|
2 to 4.780750274658203,
|
||||||
|
3 to 5.583519405788845,
|
||||||
|
4 to 6.202197604709202,
|
||||||
|
5 to 6.709673139784071,
|
||||||
|
6 to 7.141865624321832,
|
||||||
|
7 to 7.5197558932834205,
|
||||||
|
8 to 7.855508592393663,
|
||||||
|
9 to 8.170108795166016,
|
||||||
|
10 to 8.478755950927734,
|
||||||
|
11 to 8.78151363796658,
|
||||||
|
12 to 9.078428480360243,
|
||||||
|
13 to 9.368563758002388,
|
||||||
|
14 to 9.65309566921658,
|
||||||
|
15 to 9.932153489854601,
|
||||||
|
16 to 10.205938551161024,
|
||||||
|
17 to 10.474622514512804,
|
||||||
|
18 to 10.737465752495659,
|
||||||
|
19 to 10.994769202338325,
|
||||||
|
20 to 11.247151692708334,
|
||||||
|
21 to 11.494865417480469,
|
||||||
|
22 to 11.737997266981337,
|
||||||
|
23 to 11.97671890258789,
|
||||||
|
24 to 12.211305830213758,
|
||||||
|
25 to 12.440338134765625,
|
||||||
|
26 to 12.664968702528212,
|
||||||
|
27 to 12.885534498426649,
|
||||||
|
28 to 13.102118174235025,
|
||||||
|
29 to 13.314840528700087,
|
||||||
|
30 to 13.523866865370008,
|
||||||
|
31 to 13.729370964898003,
|
||||||
|
32 to 13.93154568142361,
|
||||||
|
33 to 14.129519992404514,
|
||||||
|
34 to 14.323438008626303,
|
||||||
|
35 to 14.513916439480251,
|
||||||
|
36 to 14.701114230685764,
|
||||||
|
37 to 14.885203043619791,
|
||||||
|
38 to 15.066161685519749,
|
||||||
|
39 to 15.244210561116537,
|
||||||
|
40 to 15.419385698106554,
|
||||||
|
41 to 15.591801537407768,
|
||||||
|
42 to 15.761661529541016,
|
||||||
|
43 to 15.928798251681858,
|
||||||
|
44 to 16.091954973008896,
|
||||||
|
45 to 16.25248803032769,
|
||||||
|
46 to 16.410503387451172,
|
||||||
|
47 to 16.566007402208115,
|
||||||
|
48 to 16.71794679429796,
|
||||||
|
49 to 16.8612183464898,
|
||||||
|
50 to 16.995349460177952,
|
||||||
|
51 to 17.11943096584744,
|
||||||
|
52 to 17.233810424804688,
|
||||||
|
53 to 17.34021716647678,
|
||||||
|
54 to 17.43742412990994,
|
||||||
|
55 to 17.525511847601997,
|
||||||
|
56 to 17.586015065511067,
|
||||||
|
57 to 17.615225050184463,
|
||||||
|
58 to 17.616180843777126,
|
||||||
|
59 to 17.59176466200087,
|
||||||
|
60 to 17.543212042914497,
|
||||||
|
61 to 17.472801208496094,
|
||||||
|
62 to 17.381820678710938,
|
||||||
|
63 to 17.271283467610676,
|
||||||
|
64 to 17.14224073621962,
|
||||||
|
65 to 16.99536853366428,
|
||||||
|
66 to 16.831576029459637,
|
||||||
|
67 to 16.651276482476128,
|
||||||
|
68 to 16.45496368408203,
|
||||||
|
69 to 16.24362309773763,
|
||||||
|
70 to 16.024080912272137,
|
||||||
|
71 to 15.799829694959852,
|
||||||
|
72 to 15.570498572455513,
|
||||||
|
73 to 15.336426628960503,
|
||||||
|
74 to 15.097156100802952,
|
||||||
|
75 to 14.85240724351671,
|
||||||
|
76 to 14.601781633165148,
|
||||||
|
77 to 14.345453050401476,
|
||||||
|
78 to 14.08291286892361,
|
||||||
|
79 to 13.814093271891275,
|
||||||
|
80 to 13.538227081298828,
|
||||||
|
81 to 13.255418141682943,
|
||||||
|
82 to 12.964892917209202,
|
||||||
|
83 to 12.657786475287544,
|
||||||
|
84 to 12.30661392211914,
|
||||||
|
85 to 11.908126407199436,
|
||||||
|
86 to 11.460111406114367,
|
||||||
|
87 to 10.960358513726128,
|
||||||
|
88 to 10.406063927544487,
|
||||||
|
89 to 9.794313642713758,
|
||||||
|
90 to 9.12274890475803,
|
||||||
|
91 to 8.394618564181858,
|
||||||
|
92 to 7.635277642144097,
|
||||||
|
93 to 6.844251420762804,
|
||||||
|
94 to 6.025793287489149,
|
||||||
|
95 to 5.196952819824219,
|
||||||
|
96 to 4.350443945990668,
|
||||||
|
97 to 3.472722371419271,
|
||||||
|
98 to 2.5394566853841147,
|
||||||
|
99 to 1.4974000718858507,
|
||||||
|
100 to 7.62939453125E-4,
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,105 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2021 Kyant0
|
||||||
|
*
|
||||||
|
* @link https://github.com/Kyant0/MusicYou
|
||||||
|
* @author Kyant0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package me.ash.reader.ui.theme.palette
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.compositionLocalOf
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import me.ash.reader.ui.theme.palette.colorspace.cielab.CieLab
|
||||||
|
import me.ash.reader.ui.theme.palette.colorspace.zcam.Izazbz.Companion.toIzazbz
|
||||||
|
import me.ash.reader.ui.theme.palette.core.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The L from CIELab
|
||||||
|
*/
|
||||||
|
typealias TonalValue = Int
|
||||||
|
|
||||||
|
typealias TonalPalette = MutableMap<TonalValue, Color>
|
||||||
|
|
||||||
|
val LocalTonalPalettes = compositionLocalOf {
|
||||||
|
TonalPalettes(238.36, 15.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun TonalValue.toZcamLightness(): Double =
|
||||||
|
CieLab(L = if (this != 50) toDouble() else 49.6, a = 0.0, b = 0.0).toXyz().toIzazbz()
|
||||||
|
.toZcam().Jz
|
||||||
|
|
||||||
|
data class TonalPalettes(
|
||||||
|
val hue: Double,
|
||||||
|
val primaryChroma: Double,
|
||||||
|
val primary: TonalPalette = mutableMapOf(),
|
||||||
|
val secondary: TonalPalette = mutableMapOf(),
|
||||||
|
val tertiary: TonalPalette = mutableMapOf(),
|
||||||
|
val neutral: TonalPalette = mutableMapOf(),
|
||||||
|
val neutralVariant: TonalPalette = mutableMapOf(),
|
||||||
|
val error: TonalPalette = mutableMapOf(),
|
||||||
|
) {
|
||||||
|
@Composable
|
||||||
|
infix fun primary(tone: TonalValue): Color = primary.getOrPut(tone) {
|
||||||
|
zcamLch(
|
||||||
|
L = tone.toZcamLightness(),
|
||||||
|
C = (1.2 * primaryChroma / MaterialYouStandard.sRGBLightnessChromaMap.maxOf { it.value })
|
||||||
|
.coerceAtLeast(1.0) * MaterialYouStandard.sRGBLightnessChromaMap.getValue(tone),
|
||||||
|
h = hue,
|
||||||
|
).clampToRgb().toColor()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
infix fun secondary(tone: TonalValue): Color = secondary.getOrPut(tone) {
|
||||||
|
zcamLch(
|
||||||
|
L = tone.toZcamLightness(),
|
||||||
|
C = MaterialYouStandard.sRGBLightnessChromaMap.getValue(tone) / 3.0,
|
||||||
|
h = hue,
|
||||||
|
).clampToRgb().toColor()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
infix fun tertiary(tone: TonalValue): Color = tertiary.getOrPut(tone) {
|
||||||
|
zcamLch(
|
||||||
|
L = tone.toZcamLightness(),
|
||||||
|
C = MaterialYouStandard.sRGBLightnessChromaMap.getValue(tone) * 2.0 / 3.0,
|
||||||
|
h = hue + 60.0,
|
||||||
|
).clampToRgb().toColor()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
infix fun neutral(tone: TonalValue): Color = neutral.getOrPut(tone) {
|
||||||
|
zcamLch(
|
||||||
|
L = tone.toZcamLightness(),
|
||||||
|
C = MaterialYouStandard.sRGBLightnessChromaMap.getValue(tone) / 12.0,
|
||||||
|
h = hue,
|
||||||
|
).clampToRgb().toColor()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
infix fun neutralVariant(tone: TonalValue): Color = neutralVariant.getOrPut(tone) {
|
||||||
|
zcamLch(
|
||||||
|
L = tone.toZcamLightness(),
|
||||||
|
C = MaterialYouStandard.sRGBLightnessChromaMap.getValue(tone) / 6.0,
|
||||||
|
h = hue,
|
||||||
|
).clampToRgb().toColor()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
infix fun error(tone: TonalValue): Color = error.getOrPut(tone) {
|
||||||
|
zcamLch(
|
||||||
|
L = tone.toZcamLightness(),
|
||||||
|
C = MaterialYouStandard.sRGBLightnessChromaMap.getValue(tone),
|
||||||
|
h = 33.44,
|
||||||
|
).clampToRgb().toColor()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@Composable
|
||||||
|
fun Color.toTonalPalettes(): TonalPalettes {
|
||||||
|
val zcam = toRgb().toZcam()
|
||||||
|
return TonalPalettes(hue = zcam.hz, primaryChroma = zcam.Cz)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2021 Kyant0
|
||||||
|
*
|
||||||
|
* @link https://github.com/Kyant0/MusicYou
|
||||||
|
* @author Kyant0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package me.ash.reader.ui.theme.palette.colorspace.cielab
|
||||||
|
|
||||||
|
import me.ash.reader.ui.theme.palette.colorspace.ciexyz.CieXyz
|
||||||
|
import kotlin.math.pow
|
||||||
|
|
||||||
|
// TODO: test
|
||||||
|
data class CieLab(
|
||||||
|
val L: Double,
|
||||||
|
val a: Double,
|
||||||
|
val b: Double,
|
||||||
|
) {
|
||||||
|
fun toXyz(
|
||||||
|
whitePoint: CieXyz,
|
||||||
|
luminance: Double
|
||||||
|
): CieXyz {
|
||||||
|
val lp = (L + 16.0) / 116.0
|
||||||
|
val absoluteWhitePoint = whitePoint * luminance
|
||||||
|
return CieXyz(
|
||||||
|
x = absoluteWhitePoint.x * fInv(lp + (a / 500.0)),
|
||||||
|
y = absoluteWhitePoint.y * fInv(lp),
|
||||||
|
z = absoluteWhitePoint.z * fInv(lp - (b / 200.0)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private fun f(x: Double) = when {
|
||||||
|
x > 216.0 / 24389.0 -> x.pow(1.0 / 3.0)
|
||||||
|
else -> x / (108.0 / 841.0) + 4.0 / 29.0
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fInv(x: Double): Double = when {
|
||||||
|
x > 6.0 / 29.0 -> x.pow(3.0)
|
||||||
|
else -> 108.0 / 841.0 * (x - 4.0 / 29.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun CieXyz.toCieLab(
|
||||||
|
whitePoint: CieXyz,
|
||||||
|
luminance: Double
|
||||||
|
): CieLab {
|
||||||
|
val relativeWhitePoint = whitePoint / luminance
|
||||||
|
return CieLab(
|
||||||
|
L = 116.0 * f(y / relativeWhitePoint.y) - 16.0,
|
||||||
|
a = 500.0 * (f(x / relativeWhitePoint.x) - f(y / relativeWhitePoint.y)),
|
||||||
|
b = 200.0 * (f(y / relativeWhitePoint.y) - f(z / relativeWhitePoint.z)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2021 Kyant0
|
||||||
|
*
|
||||||
|
* @link https://github.com/Kyant0/MusicYou
|
||||||
|
* @author Kyant0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package me.ash.reader.ui.theme.palette.colorspace.cielab
|
||||||
|
|
||||||
|
import me.ash.reader.ui.theme.palette.util.square
|
||||||
|
import me.ash.reader.ui.theme.palette.util.toDegrees
|
||||||
|
import me.ash.reader.ui.theme.palette.util.toRadians
|
||||||
|
import kotlin.math.atan2
|
||||||
|
import kotlin.math.cos
|
||||||
|
import kotlin.math.sin
|
||||||
|
import kotlin.math.sqrt
|
||||||
|
|
||||||
|
data class CieLch(
|
||||||
|
val L: Double,
|
||||||
|
val C: Double,
|
||||||
|
val h: Double,
|
||||||
|
) {
|
||||||
|
fun toCieLab(): CieLab {
|
||||||
|
val hRad = h.toRadians()
|
||||||
|
return CieLab(
|
||||||
|
L = L,
|
||||||
|
a = C * cos(hRad),
|
||||||
|
b = C * sin(hRad),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun CieLab.toCieLch(): CieLch = CieLch(
|
||||||
|
L = L,
|
||||||
|
C = sqrt(square(a) + square(b)),
|
||||||
|
h = atan2(b, a).toDegrees().mod(360.0),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2021 Kyant0
|
||||||
|
*
|
||||||
|
* @link https://github.com/Kyant0/MusicYou
|
||||||
|
* @author Kyant0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package me.ash.reader.ui.theme.palette.colorspace.ciexyz
|
||||||
|
|
||||||
|
import me.ash.reader.ui.theme.palette.util.div
|
||||||
|
import me.ash.reader.ui.theme.palette.util.times
|
||||||
|
|
||||||
|
data class CieXyz(
|
||||||
|
val x: Double,
|
||||||
|
val y: Double,
|
||||||
|
val z: Double,
|
||||||
|
) {
|
||||||
|
inline val xyz: DoubleArray
|
||||||
|
get() = doubleArrayOf(x, y, z)
|
||||||
|
|
||||||
|
inline val luminance: Double
|
||||||
|
get() = y
|
||||||
|
|
||||||
|
operator fun times(luminance: Double): CieXyz = (xyz * luminance).asXyz()
|
||||||
|
|
||||||
|
operator fun div(luminance: Double): CieXyz = (xyz / luminance).asXyz()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
internal fun DoubleArray.asXyz(): CieXyz = CieXyz(this[0], this[1], this[2])
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2021 Kyant0
|
||||||
|
*
|
||||||
|
* @link https://github.com/Kyant0/MusicYou
|
||||||
|
* @author Kyant0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package me.ash.reader.ui.theme.palette.colorspace.jzazbz
|
||||||
|
|
||||||
|
import me.ash.reader.ui.theme.palette.colorspace.ciexyz.CieXyz
|
||||||
|
import me.ash.reader.ui.theme.palette.util.Matrix3
|
||||||
|
import kotlin.math.pow
|
||||||
|
|
||||||
|
data class Jzazbz(
|
||||||
|
val Jz: Double,
|
||||||
|
val az: Double,
|
||||||
|
val bz: Double,
|
||||||
|
) {
|
||||||
|
fun toXyz(): CieXyz {
|
||||||
|
val (x_, y_, z) = lmsToXyz * (
|
||||||
|
IzazbzToLms * doubleArrayOf(
|
||||||
|
(Jz + d_0) / (1.0 + d - d * (Jz + d_0)),
|
||||||
|
az,
|
||||||
|
bz,
|
||||||
|
)
|
||||||
|
).map {
|
||||||
|
10000.0 * ((c_1 - it.pow(1.0 / p)) / (c_3 * it.pow(1.0 / p) - c_2)).pow(1.0 / n)
|
||||||
|
}.toDoubleArray()
|
||||||
|
val x = (x_ + (b - 1.0) * z) / b
|
||||||
|
val y = (y_ + (g - 1.0) * x) / g
|
||||||
|
return CieXyz(
|
||||||
|
x = x,
|
||||||
|
y = y,
|
||||||
|
z = z,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val b = 1.15
|
||||||
|
private const val g = 0.66
|
||||||
|
private const val c_1 = 3424.0 / 4096.0
|
||||||
|
private const val c_2 = 2413.0 / 128.0
|
||||||
|
private const val c_3 = 2392.0 / 128.0
|
||||||
|
private const val n = 2610.0 / 16384.0
|
||||||
|
private const val p = 1.7 * 2523.0 / 32.0
|
||||||
|
private const val d = -0.56
|
||||||
|
private const val d_0 = 1.6295499532821566E-11
|
||||||
|
|
||||||
|
private val xyzToLms: Matrix3 = Matrix3(
|
||||||
|
doubleArrayOf(0.41478972, 0.579999, 0.01464800),
|
||||||
|
doubleArrayOf(-0.2015100, 1.120649, 0.05310080),
|
||||||
|
doubleArrayOf(-0.0166008, 0.264800, 0.66847990),
|
||||||
|
)
|
||||||
|
private val lmsToXyz: Matrix3 = xyzToLms.inverse()
|
||||||
|
private val lmsToIzazbz: Matrix3 = Matrix3(
|
||||||
|
doubleArrayOf(0.5, 0.5, 0.0),
|
||||||
|
doubleArrayOf(3.524000, -4.066708, 0.542708),
|
||||||
|
doubleArrayOf(0.199076, 1.096799, -1.295875),
|
||||||
|
)
|
||||||
|
private val IzazbzToLms: Matrix3 = lmsToIzazbz.inverse()
|
||||||
|
|
||||||
|
fun CieXyz.toJzazbz(): Jzazbz {
|
||||||
|
val (Iz, az, bz) = lmsToIzazbz * (
|
||||||
|
xyzToLms * doubleArrayOf(
|
||||||
|
b * x - (b - 1.0) * z,
|
||||||
|
g * y - (g - 1.0) * x,
|
||||||
|
z,
|
||||||
|
)
|
||||||
|
).map {
|
||||||
|
((c_1 + c_2 * (it / 10000.0).pow(n)) / (1.0 + c_3 * (it / 10000.0).pow(n))).pow(p)
|
||||||
|
}.toDoubleArray()
|
||||||
|
return Jzazbz(
|
||||||
|
Jz = (1.0 + d) * Iz / (1.0 + d * Iz) - d_0,
|
||||||
|
az = az,
|
||||||
|
bz = bz,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2021 Kyant0
|
||||||
|
*
|
||||||
|
* @link https://github.com/Kyant0/MusicYou
|
||||||
|
* @author Kyant0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package me.ash.reader.ui.theme.palette.colorspace.jzazbz
|
||||||
|
|
||||||
|
import me.ash.reader.ui.theme.palette.util.square
|
||||||
|
import me.ash.reader.ui.theme.palette.util.toDegrees
|
||||||
|
import me.ash.reader.ui.theme.palette.util.toRadians
|
||||||
|
import kotlin.math.atan2
|
||||||
|
import kotlin.math.cos
|
||||||
|
import kotlin.math.sin
|
||||||
|
import kotlin.math.sqrt
|
||||||
|
|
||||||
|
data class Jzczhz(
|
||||||
|
val Jz: Double,
|
||||||
|
val Cz: Double,
|
||||||
|
val hz: Double,
|
||||||
|
) {
|
||||||
|
fun toJzazbz(): Jzazbz {
|
||||||
|
val hRad = hz.toRadians()
|
||||||
|
return Jzazbz(
|
||||||
|
Jz = Jz,
|
||||||
|
az = Cz * cos(hRad),
|
||||||
|
bz = Cz * sin(hRad),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dE(other: Jzczhz): Double =
|
||||||
|
sqrt(square(Jz - other.Jz) + square(Cz - other.Cz) + 4.0 * Cz * other.Cz * square(sin((hz - other.hz) / 2.0)))
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun Jzazbz.toJzczhz(): Jzczhz = Jzczhz(
|
||||||
|
Jz = Jz,
|
||||||
|
Cz = sqrt(square(az) + square(bz)),
|
||||||
|
hz = atan2(bz, az).toDegrees().mod(360.0),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2021 Kyant0
|
||||||
|
*
|
||||||
|
* @link https://github.com/Kyant0/MusicYou
|
||||||
|
* @author Kyant0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package me.ash.reader.ui.theme.palette.colorspace.oklab
|
||||||
|
|
||||||
|
import me.ash.reader.ui.theme.palette.colorspace.ciexyz.CieXyz
|
||||||
|
import me.ash.reader.ui.theme.palette.colorspace.ciexyz.CieXyz.Companion.asXyz
|
||||||
|
import me.ash.reader.ui.theme.palette.util.Matrix3
|
||||||
|
import kotlin.math.pow
|
||||||
|
|
||||||
|
data class Oklab(
|
||||||
|
val L: Double,
|
||||||
|
val a: Double,
|
||||||
|
val b: Double,
|
||||||
|
) {
|
||||||
|
fun toXyz(): CieXyz = (lmsToXyz * (oklabToLms * doubleArrayOf(L, a, b)).map { it.pow(3.0) }
|
||||||
|
.toDoubleArray()).asXyz()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val xyzToLms: Matrix3 = Matrix3(
|
||||||
|
doubleArrayOf(0.8189330101, 0.3618667424, -0.1288597137),
|
||||||
|
doubleArrayOf(0.0329845436, 0.9293118715, 0.0361456387),
|
||||||
|
doubleArrayOf(0.0482003018, 0.2643662691, 0.6338517070),
|
||||||
|
)
|
||||||
|
private val lmsToXyz: Matrix3 = xyzToLms.inverse()
|
||||||
|
private val lmsToOklab: Matrix3 = Matrix3(
|
||||||
|
doubleArrayOf(0.2104542553, 0.7936177850, -0.0040720468),
|
||||||
|
doubleArrayOf(1.9779984951, -2.4285922050, 0.4505937099),
|
||||||
|
doubleArrayOf(0.0259040371, 0.7827717662, -0.8086757660),
|
||||||
|
)
|
||||||
|
private val oklabToLms: Matrix3 = lmsToOklab.inverse()
|
||||||
|
|
||||||
|
fun CieXyz.toOklab(): Oklab =
|
||||||
|
(lmsToOklab * (xyzToLms * xyz).map { it.pow(1.0 / 3.0) }.toDoubleArray()).asOklab()
|
||||||
|
|
||||||
|
internal fun DoubleArray.asOklab(): Oklab = Oklab(this[0], this[1], this[2])
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2021 Kyant0
|
||||||
|
*
|
||||||
|
* @link https://github.com/Kyant0/MusicYou
|
||||||
|
* @author Kyant0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package me.ash.reader.ui.theme.palette.colorspace.oklab
|
||||||
|
|
||||||
|
import me.ash.reader.ui.theme.palette.colorspace.rgb.Rgb
|
||||||
|
import me.ash.reader.ui.theme.palette.colorspace.rgb.Rgb.Companion.toRgb
|
||||||
|
import me.ash.reader.ui.theme.palette.colorspace.rgb.RgbColorSpace
|
||||||
|
import me.ash.reader.ui.theme.palette.util.square
|
||||||
|
import me.ash.reader.ui.theme.palette.util.toDegrees
|
||||||
|
import me.ash.reader.ui.theme.palette.util.toRadians
|
||||||
|
import kotlin.math.atan2
|
||||||
|
import kotlin.math.cos
|
||||||
|
import kotlin.math.sin
|
||||||
|
import kotlin.math.sqrt
|
||||||
|
|
||||||
|
data class Oklch(
|
||||||
|
val L: Double,
|
||||||
|
val C: Double,
|
||||||
|
val h: Double,
|
||||||
|
) {
|
||||||
|
fun toOklab(): Oklab {
|
||||||
|
val hRad = h.toRadians()
|
||||||
|
return Oklab(
|
||||||
|
L = L,
|
||||||
|
a = C * cos(hRad),
|
||||||
|
b = C * sin(hRad),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clampToRgb(colorSpace: RgbColorSpace): Rgb =
|
||||||
|
toOklab().toXyz().toRgb(1.0, colorSpace).takeIf { it.isInGamut() } ?: copy(
|
||||||
|
C = findChromaBoundaryInRgb(
|
||||||
|
colorSpace,
|
||||||
|
0.001
|
||||||
|
)
|
||||||
|
).toOklab().toXyz().toRgb(1.0, colorSpace).clamp()
|
||||||
|
|
||||||
|
private fun findChromaBoundaryInRgb(
|
||||||
|
colorSpace: RgbColorSpace,
|
||||||
|
error: Double
|
||||||
|
): Double = chromaBoundary.getOrPut(Triple(colorSpace.hashCode(), h, L)) {
|
||||||
|
var low = 0.0
|
||||||
|
var high = C
|
||||||
|
var current = this
|
||||||
|
while (high - low >= error) {
|
||||||
|
val mid = (low + high) / 2.0
|
||||||
|
current = copy(C = mid)
|
||||||
|
if (!current.toOklab().toXyz().toRgb(1.0, colorSpace).isInGamut()) {
|
||||||
|
high = mid
|
||||||
|
} else {
|
||||||
|
val next = current.copy(C = mid + error).toOklab().toXyz().toRgb(1.0, colorSpace)
|
||||||
|
if (next.isInGamut()) {
|
||||||
|
low = mid
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
current.C
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun Oklab.toOklch(): Oklch = Oklch(
|
||||||
|
L = L,
|
||||||
|
C = sqrt(square(a) + square(b)),
|
||||||
|
h = atan2(b, a).toDegrees().mod(360.0),
|
||||||
|
)
|
||||||
|
|
||||||
|
private val chromaBoundary: MutableMap<Triple<Int, Double, Double>, Double> = mutableMapOf()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2021 Kyant0
|
||||||
|
*
|
||||||
|
* @link https://github.com/Kyant0/MusicYou
|
||||||
|
* @author Kyant0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package me.ash.reader.ui.theme.palette.colorspace.rgb
|
||||||
|
|
||||||
|
import me.ash.reader.ui.theme.palette.colorspace.ciexyz.CieXyz
|
||||||
|
import me.ash.reader.ui.theme.palette.colorspace.ciexyz.CieXyz.Companion.asXyz
|
||||||
|
import me.ash.reader.ui.theme.palette.util.div
|
||||||
|
|
||||||
|
data class Rgb(
|
||||||
|
val r: Double,
|
||||||
|
val g: Double,
|
||||||
|
val b: Double,
|
||||||
|
val colorSpace: RgbColorSpace,
|
||||||
|
) {
|
||||||
|
inline val rgb: DoubleArray
|
||||||
|
get() = doubleArrayOf(r, g, b)
|
||||||
|
|
||||||
|
fun isInGamut(): Boolean = rgb.map { it in colorSpace.componentRange }.all { it }
|
||||||
|
|
||||||
|
fun clamp(): Rgb = rgb.map { it.coerceIn(colorSpace.componentRange) }.toDoubleArray().asRgb(colorSpace)
|
||||||
|
|
||||||
|
fun toXyz(luminance: Double): CieXyz = (
|
||||||
|
colorSpace.rgbToXyzMatrix * rgb.map {
|
||||||
|
colorSpace.transferFunction.EOTF(it)
|
||||||
|
}.toDoubleArray()
|
||||||
|
).asXyz() * luminance
|
||||||
|
|
||||||
|
override fun toString(): String = "Rgb(r=$r, g=$g, b=$b, colorSpace=${colorSpace.name})"
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun CieXyz.toRgb(
|
||||||
|
luminance: Double,
|
||||||
|
colorSpace: RgbColorSpace
|
||||||
|
): Rgb = (colorSpace.rgbToXyzMatrix.inverse() * (xyz / luminance))
|
||||||
|
.map { colorSpace.transferFunction.OETF(it) }
|
||||||
|
.toDoubleArray().asRgb(colorSpace)
|
||||||
|
|
||||||
|
internal fun DoubleArray.asRgb(colorSpace: RgbColorSpace): Rgb = Rgb(this[0], this[1], this[2], colorSpace)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,129 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2021 Kyant0
|
||||||
|
*
|
||||||
|
* @link https://github.com/Kyant0/MusicYou
|
||||||
|
* @author Kyant0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package me.ash.reader.ui.theme.palette.colorspace.rgb
|
||||||
|
|
||||||
|
import me.ash.reader.ui.theme.palette.colorspace.ciexyz.CieXyz
|
||||||
|
import me.ash.reader.ui.theme.palette.colorspace.rgb.transferfunction.GammaTransferFunction
|
||||||
|
import me.ash.reader.ui.theme.palette.colorspace.rgb.transferfunction.HLGTransferFunction
|
||||||
|
import me.ash.reader.ui.theme.palette.colorspace.rgb.transferfunction.PQTransferFunction
|
||||||
|
import me.ash.reader.ui.theme.palette.colorspace.rgb.transferfunction.TransferFunction
|
||||||
|
import me.ash.reader.ui.theme.palette.data.Illuminant
|
||||||
|
import me.ash.reader.ui.theme.palette.util.Matrix3
|
||||||
|
|
||||||
|
data class RgbColorSpace(
|
||||||
|
val name: String,
|
||||||
|
val componentRange: ClosedRange<Double>,
|
||||||
|
val whitePoint: CieXyz,
|
||||||
|
internal val primaries: Matrix3,
|
||||||
|
internal val transferFunction: TransferFunction,
|
||||||
|
) {
|
||||||
|
internal val rgbToXyzMatrix: Matrix3
|
||||||
|
get() {
|
||||||
|
val M1 = Matrix3(
|
||||||
|
doubleArrayOf(
|
||||||
|
primaries[0][0] / primaries[0][1],
|
||||||
|
primaries[1][0] / primaries[1][1],
|
||||||
|
primaries[2][0] / primaries[2][1],
|
||||||
|
),
|
||||||
|
doubleArrayOf(1.0, 1.0, 1.0),
|
||||||
|
doubleArrayOf(
|
||||||
|
primaries[0][2] / primaries[0][1],
|
||||||
|
primaries[1][2] / primaries[1][1],
|
||||||
|
primaries[2][2] / primaries[2][1],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val M2 = M1.inverse() * whitePoint.xyz
|
||||||
|
return Matrix3(
|
||||||
|
doubleArrayOf(M1[0][0] * M2[0], M1[0][1] * M2[1], M1[0][2] * M2[2]),
|
||||||
|
doubleArrayOf(M1[1][0] * M2[0], M1[1][1] * M2[1], M1[1][2] * M2[2]),
|
||||||
|
doubleArrayOf(M1[2][0] * M2[0], M1[2][1] * M2[1], M1[2][2] * M2[2]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Standard: IEC 61966-2-1
|
||||||
|
* - [Wikipedia: sRGB](https://en.wikipedia.org/wiki/SRGB)
|
||||||
|
*/
|
||||||
|
val Srgb: RgbColorSpace = RgbColorSpace(
|
||||||
|
name = "sRGB",
|
||||||
|
componentRange = 0.0..1.0,
|
||||||
|
whitePoint = Illuminant.D65,
|
||||||
|
primaries = Matrix3(
|
||||||
|
doubleArrayOf(0.64, 0.33, 0.03),
|
||||||
|
doubleArrayOf(0.30, 0.60, 0.10),
|
||||||
|
doubleArrayOf(0.15, 0.06, 0.79),
|
||||||
|
),
|
||||||
|
transferFunction = GammaTransferFunction.sRGB,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standard: SMPTE EG 432-1
|
||||||
|
* - [Wikipedia: DCI-P3](https://en.wikipedia.org/wiki/DCI-P3)
|
||||||
|
*/
|
||||||
|
val DisplayP3: RgbColorSpace = RgbColorSpace(
|
||||||
|
name = "Display P3",
|
||||||
|
componentRange = 0.0..1.0,
|
||||||
|
whitePoint = Illuminant.D65,
|
||||||
|
primaries = Matrix3(
|
||||||
|
doubleArrayOf(0.68, 0.32, 0.00),
|
||||||
|
doubleArrayOf(0.265, 0.690, 0.045),
|
||||||
|
doubleArrayOf(0.15, 0.06, 0.79),
|
||||||
|
),
|
||||||
|
transferFunction = GammaTransferFunction.sRGB,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standard: ITU-R BT.2020
|
||||||
|
* - [Wikipedia: Rec. 2020](https://en.wikipedia.org/wiki/Rec._2020)
|
||||||
|
*/
|
||||||
|
val BT2020: RgbColorSpace = RgbColorSpace(
|
||||||
|
name = "BT.2020",
|
||||||
|
componentRange = 0.0..1.0,
|
||||||
|
whitePoint = Illuminant.D65,
|
||||||
|
primaries = Matrix3(
|
||||||
|
doubleArrayOf(0.708, 0.292, 0.000),
|
||||||
|
doubleArrayOf(0.170, 0.797, 0.033),
|
||||||
|
doubleArrayOf(0.131, 0.046, 0.823),
|
||||||
|
),
|
||||||
|
transferFunction = GammaTransferFunction.Rec709,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standard: ITU-R BT.2100
|
||||||
|
* - [Wikipedia: Rec. 2100](https://en.wikipedia.org/wiki/Rec._2100)
|
||||||
|
*/
|
||||||
|
val BT2100PQ: RgbColorSpace = RgbColorSpace(
|
||||||
|
name = "BT.2100 (PQ)",
|
||||||
|
componentRange = 0.0..1.0,
|
||||||
|
whitePoint = Illuminant.D65,
|
||||||
|
primaries = Matrix3(
|
||||||
|
doubleArrayOf(0.708, 0.292, 0.000),
|
||||||
|
doubleArrayOf(0.170, 0.797, 0.033),
|
||||||
|
doubleArrayOf(0.131, 0.046, 0.823),
|
||||||
|
),
|
||||||
|
transferFunction = PQTransferFunction(),
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standard: ITU-R BT.2100
|
||||||
|
* - [Wikipedia: Rec. 2100](https://en.wikipedia.org/wiki/Rec._2100)
|
||||||
|
*/
|
||||||
|
val BT2100HLG: RgbColorSpace = RgbColorSpace(
|
||||||
|
name = "BT.2100 (HLG)",
|
||||||
|
componentRange = 0.0..1.0,
|
||||||
|
whitePoint = Illuminant.D65,
|
||||||
|
primaries = Matrix3(
|
||||||
|
doubleArrayOf(0.708, 0.292, 0.000),
|
||||||
|
doubleArrayOf(0.170, 0.797, 0.033),
|
||||||
|
doubleArrayOf(0.131, 0.046, 0.823),
|
||||||
|
),
|
||||||
|
transferFunction = HLGTransferFunction(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2021 Kyant0
|
||||||
|
*
|
||||||
|
* @link https://github.com/Kyant0/MusicYou
|
||||||
|
* @author Kyant0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package me.ash.reader.ui.theme.palette.colorspace.rgb.transferfunction
|
||||||
|
|
||||||
|
import kotlin.math.pow
|
||||||
|
|
||||||
|
class GammaTransferFunction(
|
||||||
|
val gamma: Double, // decoding gamma γ
|
||||||
|
val alpha: Double, // offset a, α = a + 1
|
||||||
|
val beta: Double, // linear-domain threshold β = K_0 / φ = E_t
|
||||||
|
val delta: Double, // linear gain δ = φ
|
||||||
|
) : TransferFunction {
|
||||||
|
override fun EOTF(x: Double): Double = when {
|
||||||
|
x >= beta * delta -> ((x + alpha - 1.0) / alpha).pow(gamma) // transition point βδ = K_0
|
||||||
|
else -> x / delta
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun OETF(x: Double): Double = when {
|
||||||
|
x >= beta -> alpha * (x.pow(1.0 / gamma) - 1.0) + 1.0
|
||||||
|
else -> x * delta
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* [Wikipedia: sRGB - Computing the transfer function](https://en.wikipedia.org/wiki/SRGB#Computing_the_transfer_function)
|
||||||
|
*/
|
||||||
|
val sRGB = GammaTransferFunction(
|
||||||
|
gamma = 2.4,
|
||||||
|
alpha = 1.055,
|
||||||
|
beta = 0.055 / 1.4 / ((1.055 / 2.4).pow(2.4) * (1.4 / 0.055).pow(1.4)),
|
||||||
|
// ~0.03928571428571429 / ~12.923210180787857 = ~0.0030399346397784314 -> 0.003130804935
|
||||||
|
delta = (1.055 / 2.4).pow(2.4) * (1.4 / 0.055).pow(1.4),
|
||||||
|
// ~12.923210180787857 -> 12.920020442059
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [Rec. 709](https://www.itu.int/rec/R-REC-BT.709)
|
||||||
|
*/
|
||||||
|
val Rec709 = GammaTransferFunction(
|
||||||
|
gamma = 2.4,
|
||||||
|
alpha = 1.0 + 5.5 * 0.018053968510807, // ~1.09929682680944
|
||||||
|
beta = 0.018053968510807,
|
||||||
|
delta = 4.5,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2021 Kyant0
|
||||||
|
*
|
||||||
|
* @link https://github.com/Kyant0/MusicYou
|
||||||
|
* @author Kyant0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package me.ash.reader.ui.theme.palette.colorspace.rgb.transferfunction
|
||||||
|
|
||||||
|
import me.ash.reader.ui.theme.palette.util.square
|
||||||
|
import kotlin.math.exp
|
||||||
|
import kotlin.math.ln
|
||||||
|
import kotlin.math.sqrt
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [Rec. 2100](https://www.itu.int/rec/R-REC-BT.2100)
|
||||||
|
*/
|
||||||
|
class HLGTransferFunction : TransferFunction {
|
||||||
|
companion object {
|
||||||
|
private val a = 0.17883277
|
||||||
|
private val b = 1.0 - 4.0 * a // 0.28466892
|
||||||
|
private val c = 0.5 - a * ln(4.0 * a) // 0.55991073
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun EOTF(x: Double): Double = when (x) {
|
||||||
|
in 0.0..1.0 / 2.0 -> 3.0 * square(x)
|
||||||
|
in 1.0 / 2.0..1.0 -> (exp((x - c) / a) + b) / 12.0
|
||||||
|
else -> Double.NaN
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun OETF(x: Double): Double = when (x) {
|
||||||
|
in 0.0..1.0 / 12.0 -> sqrt(3.0 * x)
|
||||||
|
in 1.0 / 12.0..1.0 -> a * ln(12.0 * x - b) + c
|
||||||
|
else -> Double.NaN
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2021 Kyant0
|
||||||
|
*
|
||||||
|
* @link https://github.com/Kyant0/MusicYou
|
||||||
|
* @author Kyant0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package me.ash.reader.ui.theme.palette.colorspace.rgb.transferfunction
|
||||||
|
|
||||||
|
import kotlin.math.pow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [Rec. 2100](https://www.itu.int/rec/R-REC-BT.2100)
|
||||||
|
*/
|
||||||
|
class PQTransferFunction : TransferFunction {
|
||||||
|
companion object {
|
||||||
|
private val m_1 = 2610.0 / 16384.0 // 0.1593017578125
|
||||||
|
private val m_2 = 2523.0 / 4096.0 * 128.0 // 78.84375
|
||||||
|
private val c_1 = 3424.0 / 4096.0 // 0.8359375 = c_3 − c_2 + 1
|
||||||
|
private val c_2 = 2413.0 / 4096.0 * 32.0 // 18.8515625
|
||||||
|
private val c_3 = 2392.0 / 4096.0 * 32.0 // 18.6875
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun EOTF(x: Double): Double =
|
||||||
|
10000.0 * ((x.pow(1.0 / m_2).coerceAtLeast(0.0)) / (c_2 - c_3 * x.pow(1.0 / m_2))).pow(1.0 / m_1)
|
||||||
|
|
||||||
|
override fun OETF(x: Double): Double = ((c_1 + c_2 * x / 10000.0) / (1 + c_3 * x / 10000.0)).pow(
|
||||||
|
m_2
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2021 Kyant0
|
||||||
|
*
|
||||||
|
* @link https://github.com/Kyant0/MusicYou
|
||||||
|
* @author Kyant0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package me.ash.reader.ui.theme.palette.colorspace.rgb.transferfunction
|
||||||
|
|
||||||
|
interface TransferFunction {
|
||||||
|
// nonlinear -> linear
|
||||||
|
fun EOTF(x: Double): Double
|
||||||
|
|
||||||
|
// linear -> nonlinear
|
||||||
|
fun OETF(x: Double): Double
|
||||||
|
}
|
|
@ -0,0 +1,74 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2021 Kyant0
|
||||||
|
*
|
||||||
|
* @link https://github.com/Kyant0/MusicYou
|
||||||
|
* @author Kyant0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package me.ash.reader.ui.theme.palette.colorspace.zcam
|
||||||
|
|
||||||
|
import me.ash.reader.ui.theme.palette.colorspace.ciexyz.CieXyz
|
||||||
|
import me.ash.reader.ui.theme.palette.util.Matrix3
|
||||||
|
import kotlin.math.pow
|
||||||
|
|
||||||
|
data class Izazbz(
|
||||||
|
val Iz: Double,
|
||||||
|
val az: Double,
|
||||||
|
val bz: Double,
|
||||||
|
) {
|
||||||
|
fun toXyz(): CieXyz {
|
||||||
|
val (x_, y_, z) = lmsToXyz * (IzazbzToLms * doubleArrayOf(Iz + epsilon, az, bz)).map {
|
||||||
|
10000.0 * ((c_1 - it.pow(1.0 / rho)) / (c_3 * it.pow(1.0 / rho) - c_2)).pow(1.0 / eta)
|
||||||
|
}.toDoubleArray()
|
||||||
|
val x = (x_ + (b - 1.0) * z) / b
|
||||||
|
val y = (y_ + (g - 1.0) * x) / g
|
||||||
|
return CieXyz(
|
||||||
|
x = x,
|
||||||
|
y = y,
|
||||||
|
z = z,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val b = 1.15
|
||||||
|
private const val g = 0.66
|
||||||
|
private const val c_1 = 3424.0 / 4096.0
|
||||||
|
private const val c_2 = 2413.0 / 128.0
|
||||||
|
private const val c_3 = 2392.0 / 128.0
|
||||||
|
private const val eta = 2610.0 / 16384.0
|
||||||
|
private const val rho = 1.7 * 2523.0 / 32.0
|
||||||
|
private const val epsilon = 3.7035226210190005E-11
|
||||||
|
|
||||||
|
private val xyzToLms: Matrix3 = Matrix3(
|
||||||
|
doubleArrayOf(0.41478972, 0.579999, 0.01464800),
|
||||||
|
doubleArrayOf(-0.2015100, 1.120649, 0.05310080),
|
||||||
|
doubleArrayOf(-0.0166008, 0.264800, 0.66847990),
|
||||||
|
)
|
||||||
|
private val lmsToXyz: Matrix3 = xyzToLms.inverse()
|
||||||
|
private val lmsToIzazbz: Matrix3 = Matrix3(
|
||||||
|
doubleArrayOf(0.0, 1.0, 0.0),
|
||||||
|
doubleArrayOf(3.524000, -4.066708, 0.542708),
|
||||||
|
doubleArrayOf(0.199076, 1.096799, -1.295875),
|
||||||
|
)
|
||||||
|
private val IzazbzToLms: Matrix3 = lmsToIzazbz.inverse()
|
||||||
|
|
||||||
|
fun CieXyz.toIzazbz(): Izazbz {
|
||||||
|
val (I, az, bz) = lmsToIzazbz * (
|
||||||
|
xyzToLms * doubleArrayOf(
|
||||||
|
b * x - (b - 1.0) * z,
|
||||||
|
g * y - (g - 1.0) * x,
|
||||||
|
z,
|
||||||
|
)
|
||||||
|
).map {
|
||||||
|
((c_1 + c_2 * (it / 10000.0).pow(eta)) / (1.0 + c_3 * (it / 10000.0).pow(eta))).pow(
|
||||||
|
rho
|
||||||
|
)
|
||||||
|
}.toDoubleArray()
|
||||||
|
return Izazbz(
|
||||||
|
Iz = I - epsilon,
|
||||||
|
az = az,
|
||||||
|
bz = bz,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,162 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2021 Kyant0
|
||||||
|
*
|
||||||
|
* @link https://github.com/Kyant0/MusicYou
|
||||||
|
* @author Kyant0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package me.ash.reader.ui.theme.palette.colorspace.zcam
|
||||||
|
|
||||||
|
import me.ash.reader.ui.theme.palette.colorspace.ciexyz.CieXyz
|
||||||
|
import me.ash.reader.ui.theme.palette.colorspace.rgb.Rgb
|
||||||
|
import me.ash.reader.ui.theme.palette.colorspace.rgb.Rgb.Companion.toRgb
|
||||||
|
import me.ash.reader.ui.theme.palette.colorspace.rgb.RgbColorSpace
|
||||||
|
import me.ash.reader.ui.theme.palette.colorspace.zcam.Izazbz.Companion.toIzazbz
|
||||||
|
import me.ash.reader.ui.theme.palette.util.square
|
||||||
|
import me.ash.reader.ui.theme.palette.util.toDegrees
|
||||||
|
import me.ash.reader.ui.theme.palette.util.toRadians
|
||||||
|
import kotlin.math.*
|
||||||
|
|
||||||
|
data class Zcam(
|
||||||
|
val hz: Double = Double.NaN,
|
||||||
|
val Qz: Double = Double.NaN,
|
||||||
|
val Jz: Double = Double.NaN,
|
||||||
|
val Mz: Double = Double.NaN,
|
||||||
|
val Cz: Double = Double.NaN,
|
||||||
|
val Sz: Double = Double.NaN,
|
||||||
|
val Vz: Double = Double.NaN,
|
||||||
|
val Kz: Double = Double.NaN,
|
||||||
|
val Wz: Double = Double.NaN,
|
||||||
|
val cond: ViewingConditions,
|
||||||
|
) {
|
||||||
|
fun toIzazbz(): Izazbz {
|
||||||
|
require(!hz.isNaN()) { "Must provide hz." }
|
||||||
|
require(!Qz.isNaN() || !Jz.isNaN()) { "Must provide Qz or Jz." }
|
||||||
|
require(!Mz.isNaN() || !Cz.isNaN() || !Sz.isNaN() || !Vz.isNaN() || !Kz.isNaN() || !Wz.isNaN()) {
|
||||||
|
"Must provide Mz, Cz, Sz, Vz, Kz or Wz."
|
||||||
|
}
|
||||||
|
with(cond) {
|
||||||
|
val Iz = (
|
||||||
|
when {
|
||||||
|
!Qz.isNaN() -> Qz
|
||||||
|
!Jz.isNaN() -> Jz * Qzw / 100.0
|
||||||
|
else -> Double.NaN
|
||||||
|
} / (2700.0 * F_s.pow(2.2) * sqrt(F_b) * F_L.pow(0.2))
|
||||||
|
).pow(F_b.pow(0.12) / (1.6 * F_s))
|
||||||
|
val Jz = Jz.takeUnless { it.isNaN() } ?: when {
|
||||||
|
!Qz.isNaN() -> 100.0 * Qz / Qzw
|
||||||
|
else -> Double.NaN
|
||||||
|
}
|
||||||
|
val Qz = Qz.takeUnless { it.isNaN() } ?: when {
|
||||||
|
!Jz.isNaN() -> Jz * Qzw / 100.0
|
||||||
|
else -> Double.NaN
|
||||||
|
}
|
||||||
|
val Cz = Cz.takeUnless { it.isNaN() } ?: when {
|
||||||
|
!Sz.isNaN() -> Qz * square(Sz) / (100.0 * Qzw * F_L.pow(1.2))
|
||||||
|
!Vz.isNaN() -> sqrt((square(Vz) - square(Jz - 58.0)) / 3.4)
|
||||||
|
!Kz.isNaN() -> sqrt((square((100.0 - Kz) / 0.8) - square(Jz)) / 8.0)
|
||||||
|
!Wz.isNaN() -> sqrt(square(100.0 - Wz) - square(100.0 - Jz))
|
||||||
|
else -> Double.NaN
|
||||||
|
}
|
||||||
|
val Mz = Mz.takeUnless { it.isNaN() } ?: (Cz * Qzw / 100.0)
|
||||||
|
|
||||||
|
val ez = 1.015 + cos(89.038 + hz).toRadians()
|
||||||
|
val Cz_ =
|
||||||
|
(Mz * Izw.pow(0.78) * F_b.pow(0.1) / (100.0 * ez.pow(0.068) * F_L.pow(0.2))).pow(1.0 / (0.37 * 2.0))
|
||||||
|
val hzRad = hz.toRadians()
|
||||||
|
val az = Cz_ * cos(hzRad)
|
||||||
|
val bz = Cz_ * sin(hzRad)
|
||||||
|
|
||||||
|
return Izazbz(
|
||||||
|
Iz = Iz,
|
||||||
|
az = az,
|
||||||
|
bz = bz,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clampToRgb(colorSpace: RgbColorSpace): Rgb = toIzazbz()
|
||||||
|
.toXyz()
|
||||||
|
.toRgb(cond.luminance, colorSpace)
|
||||||
|
.takeIf { it.isInGamut() }
|
||||||
|
?: copy(Cz = findChromaBoundaryInRgb(colorSpace, 0.001))
|
||||||
|
.toIzazbz()
|
||||||
|
.toXyz()
|
||||||
|
.toRgb(cond.luminance, colorSpace)
|
||||||
|
.clamp()
|
||||||
|
|
||||||
|
private fun findChromaBoundaryInRgb(
|
||||||
|
colorSpace: RgbColorSpace,
|
||||||
|
error: Double
|
||||||
|
): Double = chromaBoundary.getOrPut(Triple(colorSpace.hashCode(), hz, Jz)) {
|
||||||
|
var low = 0.0
|
||||||
|
var high = Cz
|
||||||
|
var current = this
|
||||||
|
while (high - low >= error) {
|
||||||
|
val mid = (low + high) / 2.0
|
||||||
|
current = copy(Cz = mid)
|
||||||
|
if (!current.toIzazbz().toXyz().toRgb(cond.luminance, colorSpace).isInGamut()) {
|
||||||
|
high = mid
|
||||||
|
} else {
|
||||||
|
val next = current.copy(Cz = mid + error).toIzazbz().toXyz().toRgb(cond.luminance, colorSpace)
|
||||||
|
if (next.isInGamut()) {
|
||||||
|
low = mid
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
current.Cz
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val chromaBoundary: MutableMap<Triple<Int, Double, Double>, Double> = mutableMapOf()
|
||||||
|
|
||||||
|
data class ViewingConditions(
|
||||||
|
val whitePoint: CieXyz,
|
||||||
|
val luminance: Double,
|
||||||
|
val F_s: Double,
|
||||||
|
val L_a: Double,
|
||||||
|
val Y_b: Double,
|
||||||
|
) {
|
||||||
|
private val absoluteWhitePoint = whitePoint * luminance
|
||||||
|
private val Y_w = absoluteWhitePoint.luminance
|
||||||
|
val F_b = sqrt(Y_b / Y_w)
|
||||||
|
val F_L = 0.171 * L_a.pow(1.0 / 3.0) * (1 - exp(-48.0 / 9.0 * L_a))
|
||||||
|
val Izw = absoluteWhitePoint.toIzazbz().Iz
|
||||||
|
val Qzw = 2700.0 * Izw.pow(1.6 * F_s / F_b.pow(0.12)) * F_s.pow(2.2) * sqrt(F_b) * F_L.pow(0.2)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Izazbz.toZcam(cond: ViewingConditions): Zcam {
|
||||||
|
with(cond) {
|
||||||
|
val hz = atan2(bz, az).toDegrees().mod(360.0) // hue angle
|
||||||
|
val Qz =
|
||||||
|
2700.0 * Iz.pow(1.6 * F_s / F_b.pow(0.12)) * F_s.pow(2.2) * sqrt(F_b) * F_L.pow(0.2) // brightness
|
||||||
|
val Jz = 100.0 * Qz / Qzw // lightness
|
||||||
|
val ez = 1.015 + cos(89.038 + hz).toRadians() // ~ eccentricity factor
|
||||||
|
val Mz =
|
||||||
|
100.0 * (square(az) + square(bz)).pow(0.37) * ez.pow(0.068) * F_L.pow(0.2) /
|
||||||
|
(F_b.pow(0.1) * Izw.pow(0.78)) // colorfulness
|
||||||
|
val Cz = 100.0 * Mz / Qzw // chroma
|
||||||
|
|
||||||
|
val Sz = 100.0 * F_L.pow(0.6) * sqrt(Mz / Qz) // saturation
|
||||||
|
val Vz = sqrt(square(Jz - 58.0) + 3.4 * square(Cz)) // vividness
|
||||||
|
val Kz = 100.0 - 0.8 * sqrt(square(Jz) + 8.0 * square(Cz)) // blackness
|
||||||
|
val Wz = 100.0 - sqrt(square(100.0 - Jz) + square(Cz)) // blackness
|
||||||
|
|
||||||
|
return Zcam(
|
||||||
|
hz = hz,
|
||||||
|
Qz = Qz,
|
||||||
|
Jz = Jz,
|
||||||
|
Mz = Mz,
|
||||||
|
Cz = Cz,
|
||||||
|
Sz = Sz,
|
||||||
|
Vz = Vz,
|
||||||
|
Kz = Kz,
|
||||||
|
Wz = Wz,
|
||||||
|
cond = cond,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,97 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2021 Kyant0
|
||||||
|
*
|
||||||
|
* @link https://github.com/Kyant0/MusicYou
|
||||||
|
* @author Kyant0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package me.ash.reader.ui.theme.palette.core
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import me.ash.reader.ui.theme.palette.colorspace.cielab.CieLab
|
||||||
|
import me.ash.reader.ui.theme.palette.colorspace.cielab.CieLab.Companion.toCieLab
|
||||||
|
import me.ash.reader.ui.theme.palette.colorspace.ciexyz.CieXyz
|
||||||
|
import me.ash.reader.ui.theme.palette.colorspace.oklab.Oklch
|
||||||
|
import me.ash.reader.ui.theme.palette.colorspace.rgb.Rgb
|
||||||
|
import me.ash.reader.ui.theme.palette.colorspace.rgb.Rgb.Companion.toRgb
|
||||||
|
import me.ash.reader.ui.theme.palette.colorspace.zcam.Izazbz
|
||||||
|
import me.ash.reader.ui.theme.palette.colorspace.zcam.Izazbz.Companion.toIzazbz
|
||||||
|
import me.ash.reader.ui.theme.palette.colorspace.zcam.Zcam
|
||||||
|
import me.ash.reader.ui.theme.palette.colorspace.zcam.Zcam.Companion.toZcam
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun rgb(
|
||||||
|
r: Double,
|
||||||
|
g: Double,
|
||||||
|
b: Double,
|
||||||
|
): Rgb = Rgb(
|
||||||
|
r = r,
|
||||||
|
g = g,
|
||||||
|
b = b,
|
||||||
|
colorSpace = LocalRgbColorSpace.current,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun zcamLch(
|
||||||
|
L: Double,
|
||||||
|
C: Double,
|
||||||
|
h: Double,
|
||||||
|
): Zcam = Zcam(
|
||||||
|
hz = h,
|
||||||
|
Jz = L,
|
||||||
|
Cz = C,
|
||||||
|
cond = LocalZcamViewingConditions.current,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun zcam(
|
||||||
|
hue: Double = Double.NaN,
|
||||||
|
brightness: Double = Double.NaN,
|
||||||
|
lightness: Double = Double.NaN,
|
||||||
|
colorfulness: Double = Double.NaN,
|
||||||
|
chroma: Double = Double.NaN,
|
||||||
|
saturation: Double = Double.NaN,
|
||||||
|
vividness: Double = Double.NaN,
|
||||||
|
blackness: Double = Double.NaN,
|
||||||
|
whiteness: Double = Double.NaN,
|
||||||
|
): Zcam = Zcam(
|
||||||
|
hz = hue,
|
||||||
|
Qz = brightness,
|
||||||
|
Jz = lightness,
|
||||||
|
Mz = colorfulness,
|
||||||
|
Cz = chroma,
|
||||||
|
Sz = saturation,
|
||||||
|
Vz = vividness,
|
||||||
|
Kz = blackness,
|
||||||
|
Wz = whiteness,
|
||||||
|
cond = LocalZcamViewingConditions.current,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CieXyz.toRgb(): Rgb = toRgb(LocalLuminance.current, LocalRgbColorSpace.current)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CieLab.toXyz(): CieXyz = toXyz(LocalWhitePoint.current, LocalLuminance.current)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CieXyz.toCieLab(): CieLab = toCieLab(LocalWhitePoint.current, LocalLuminance.current)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Rgb.toXyz(): CieXyz = toXyz(LocalLuminance.current)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Rgb.toZcam(): Zcam = toXyz().toIzazbz().toZcam()
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Oklch.clampToRgb(): Rgb = clampToRgb(LocalRgbColorSpace.current)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Izazbz.toZcam(): Zcam = toZcam(LocalZcamViewingConditions.current)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Zcam.toRgb(): Rgb =
|
||||||
|
toIzazbz().toXyz()
|
||||||
|
.toRgb(LocalZcamViewingConditions.current.luminance, LocalRgbColorSpace.current)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Zcam.clampToRgb(): Rgb = clampToRgb(LocalRgbColorSpace.current)
|
|
@ -0,0 +1,67 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2021 Kyant0
|
||||||
|
*
|
||||||
|
* @link https://github.com/Kyant0/MusicYou
|
||||||
|
* @author Kyant0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package me.ash.reader.ui.theme.palette.core
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.State
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.colorspace.ColorSpaces
|
||||||
|
import me.ash.reader.ui.theme.palette.colorspace.rgb.Rgb
|
||||||
|
import me.ash.reader.ui.theme.palette.colorspace.rgb.RgbColorSpace
|
||||||
|
|
||||||
|
fun Rgb.toColor(): Color = if (!r.isNaN() && !g.isNaN() && !b.isNaN())
|
||||||
|
Color(
|
||||||
|
red = r.toFloat(),
|
||||||
|
green = g.toFloat(),
|
||||||
|
blue = b.toFloat(),
|
||||||
|
colorSpace = when (colorSpace) {
|
||||||
|
RgbColorSpace.Srgb -> ColorSpaces.Srgb
|
||||||
|
RgbColorSpace.DisplayP3 -> ColorSpaces.DisplayP3
|
||||||
|
RgbColorSpace.BT2020 -> ColorSpaces.Bt2020
|
||||||
|
else -> ColorSpaces.Srgb
|
||||||
|
}
|
||||||
|
) else Color.Black
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Color.toRgb(): Rgb {
|
||||||
|
val color = convert(
|
||||||
|
when (LocalRgbColorSpace.current) {
|
||||||
|
RgbColorSpace.Srgb -> ColorSpaces.Srgb
|
||||||
|
RgbColorSpace.DisplayP3 -> ColorSpaces.DisplayP3
|
||||||
|
RgbColorSpace.BT2020 -> ColorSpaces.Bt2020
|
||||||
|
else -> ColorSpaces.Srgb
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return Rgb(
|
||||||
|
r = color.red.toDouble(),
|
||||||
|
g = color.green.toDouble(),
|
||||||
|
b = color.blue.toDouble(),
|
||||||
|
colorSpace = LocalRgbColorSpace.current
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun animateZcamLchAsState(
|
||||||
|
targetValue: ZcamLch,
|
||||||
|
animationSpec: AnimationSpec<ZcamLch> = spring(),
|
||||||
|
finishedListener: ((ZcamLch) -> Unit)? = null
|
||||||
|
): State<ZcamLch> {
|
||||||
|
val converter = remember {
|
||||||
|
TwoWayConverter<ZcamLch, AnimationVector3D>(
|
||||||
|
convertToVector = {
|
||||||
|
AnimationVector3D(it.L.toFloat(), it.C.toFloat(), it.h.toFloat())
|
||||||
|
},
|
||||||
|
convertFromVector = {
|
||||||
|
ZcamLch(L = it.v1.toDouble(), C = it.v2.toDouble(), h = it.v3.toDouble())
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return animateValueAsState(targetValue, converter, animationSpec, finishedListener = finishedListener)
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2021 Kyant0
|
||||||
|
*
|
||||||
|
* @link https://github.com/Kyant0/MusicYou
|
||||||
|
* @author Kyant0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package me.ash.reader.ui.theme.palette.core
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.staticCompositionLocalOf
|
||||||
|
import me.ash.reader.ui.theme.palette.colorspace.cielab.CieLab
|
||||||
|
import me.ash.reader.ui.theme.palette.colorspace.ciexyz.CieXyz
|
||||||
|
import me.ash.reader.ui.theme.palette.colorspace.rgb.RgbColorSpace
|
||||||
|
import me.ash.reader.ui.theme.palette.colorspace.zcam.Zcam
|
||||||
|
import me.ash.reader.ui.theme.palette.data.Illuminant
|
||||||
|
|
||||||
|
val LocalWhitePoint = staticCompositionLocalOf {
|
||||||
|
Illuminant.D65
|
||||||
|
}
|
||||||
|
|
||||||
|
val LocalLuminance = staticCompositionLocalOf {
|
||||||
|
1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
val LocalRgbColorSpace = staticCompositionLocalOf {
|
||||||
|
RgbColorSpace.Srgb
|
||||||
|
}
|
||||||
|
|
||||||
|
val LocalZcamViewingConditions = staticCompositionLocalOf {
|
||||||
|
createZcamViewingConditions()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ProvideZcamViewingConditions(
|
||||||
|
whitePoint: CieXyz = Illuminant.D65,
|
||||||
|
luminance: Double = 203.0, // BT.2408-4, HDR white luminance
|
||||||
|
surroundFactor: Double = 0.69, // average surround
|
||||||
|
content: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalWhitePoint provides whitePoint,
|
||||||
|
LocalLuminance provides luminance,
|
||||||
|
LocalZcamViewingConditions provides createZcamViewingConditions(
|
||||||
|
whitePoint = whitePoint,
|
||||||
|
luminance = luminance,
|
||||||
|
surroundFactor = surroundFactor,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createZcamViewingConditions(
|
||||||
|
whitePoint: CieXyz = Illuminant.D65,
|
||||||
|
luminance: Double = 203.0,
|
||||||
|
surroundFactor: Double = 0.69,
|
||||||
|
): Zcam.Companion.ViewingConditions = Zcam.Companion.ViewingConditions(
|
||||||
|
whitePoint = whitePoint,
|
||||||
|
luminance = luminance,
|
||||||
|
F_s = surroundFactor,
|
||||||
|
L_a = 0.4 * luminance,
|
||||||
|
Y_b = CieLab(50.0, 0.0, 0.0).toXyz(whitePoint, luminance).luminance,
|
||||||
|
)
|
|
@ -0,0 +1,28 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2021 Kyant0
|
||||||
|
*
|
||||||
|
* @link https://github.com/Kyant0/MusicYou
|
||||||
|
* @author Kyant0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package me.ash.reader.ui.theme.palette.core
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import me.ash.reader.ui.theme.palette.colorspace.zcam.Zcam
|
||||||
|
|
||||||
|
data class ZcamLch(
|
||||||
|
val L: Double,
|
||||||
|
val C: Double,
|
||||||
|
val h: Double,
|
||||||
|
) {
|
||||||
|
@Composable
|
||||||
|
fun toZcam(): Zcam = zcamLch(L = L, C = C, h = h)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@Composable
|
||||||
|
fun Color.toZcamLch(): ZcamLch = toRgb().toZcam().toZcamLch()
|
||||||
|
|
||||||
|
fun Zcam.toZcamLch(): ZcamLch = ZcamLch(L = Jz, C = Cz, h = hz)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2021 Kyant0
|
||||||
|
*
|
||||||
|
* @link https://github.com/Kyant0/MusicYou
|
||||||
|
* @author Kyant0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package me.ash.reader.ui.theme.palette.data
|
||||||
|
|
||||||
|
import me.ash.reader.ui.theme.palette.colorspace.ciexyz.CieXyz
|
||||||
|
|
||||||
|
object Illuminant {
|
||||||
|
/** CIE Illuminant D65 - standard 2º observer. 6504 K color temperature.
|
||||||
|
* Values are calculated from [this table](https://github.com/gpmarques/colorimetry/blob/master/all_1nm_data.xls).
|
||||||
|
*/
|
||||||
|
val D65: CieXyz by lazy {
|
||||||
|
CieXyz(
|
||||||
|
x = 10043.7000153676 / 10567.0816669881,
|
||||||
|
y = 1.0,
|
||||||
|
z = 11505.7421788588 / 10567.0816669881,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2021 Kyant0
|
||||||
|
*
|
||||||
|
* @link https://github.com/Kyant0/MusicYou
|
||||||
|
* @author Kyant0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package me.ash.reader.ui.theme.palette.data
|
||||||
|
|
||||||
|
import me.ash.reader.ui.theme.palette.TonalPalettes
|
||||||
|
|
||||||
|
data class Theme(
|
||||||
|
val hue: Double,
|
||||||
|
val primaryChroma: Double,
|
||||||
|
) {
|
||||||
|
fun toTonalPalettes(): TonalPalettes = TonalPalettes(
|
||||||
|
hue = hue,
|
||||||
|
primaryChroma = primaryChroma,
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun TonalPalettes.toTheme(): Theme = Theme(
|
||||||
|
hue = hue,
|
||||||
|
primaryChroma = primaryChroma,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2021 Kyant0
|
||||||
|
*
|
||||||
|
* @link https://github.com/Kyant0/MusicYou
|
||||||
|
* @author Kyant0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package me.ash.reader.ui.theme.palette.dynamic
|
||||||
|
|
||||||
|
import me.ash.reader.ui.theme.palette.colorspace.zcam.Zcam
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.absoluteValue
|
||||||
|
import kotlin.math.sign
|
||||||
|
|
||||||
|
fun Zcam.harmonizeTowards(
|
||||||
|
target: Zcam,
|
||||||
|
factor: Double = 0.5,
|
||||||
|
maxHueShift: Double = 15.0
|
||||||
|
): Zcam = copy(
|
||||||
|
hz = hz + (
|
||||||
|
((180.0 - abs(abs(hz - target.hz) - 180.0)) * factor).coerceAtMost(maxHueShift)
|
||||||
|
) * (
|
||||||
|
listOf(
|
||||||
|
target.hz - hz,
|
||||||
|
target.hz - hz + 360.0,
|
||||||
|
target.hz - hz - 360.0
|
||||||
|
).minOf {
|
||||||
|
it.absoluteValue
|
||||||
|
}.sign.takeIf { it != 0.0 } ?: 1.0
|
||||||
|
)
|
||||||
|
)
|
|
@ -0,0 +1,52 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2021 Kyant0
|
||||||
|
*
|
||||||
|
* @link https://github.com/Kyant0/MusicYou
|
||||||
|
* @author Kyant0
|
||||||
|
* @modifier Ashinch
|
||||||
|
*/
|
||||||
|
|
||||||
|
package me.ash.reader.ui.theme.palette.dynamic
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.WallpaperManager
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalView
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import me.ash.reader.ui.ext.DataStoreKeys
|
||||||
|
import me.ash.reader.ui.ext.dataStore
|
||||||
|
import me.ash.reader.ui.theme.palette.TonalPalettes
|
||||||
|
import me.ash.reader.ui.theme.palette.TonalPalettes.Companion.toTonalPalettes
|
||||||
|
import me.ash.reader.ui.theme.palette.safeHexToColor
|
||||||
|
|
||||||
|
@SuppressLint("FlowOperatorInvokedInComposition")
|
||||||
|
@Composable
|
||||||
|
fun extractTonalPalettesFromUserWallpaper(): List<TonalPalettes> {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val customPrimaryColor =
|
||||||
|
context.dataStore.data.map { it[DataStoreKeys.CustomPrimaryColor.key] ?: "" }
|
||||||
|
.collectAsState(initial = "").value
|
||||||
|
|
||||||
|
val preset = mutableListOf(
|
||||||
|
Color(0xFF80BBFF).toTonalPalettes(),
|
||||||
|
Color(0xFFFFD8E4).toTonalPalettes(),
|
||||||
|
Color(0xFF62539f).toTonalPalettes(),
|
||||||
|
Color(0xFFE9B666).toTonalPalettes(),
|
||||||
|
customPrimaryColor.safeHexToColor().toTonalPalettes()
|
||||||
|
)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1 && !LocalView.current.isInEditMode) {
|
||||||
|
val colors = WallpaperManager.getInstance(LocalContext.current)
|
||||||
|
.getWallpaperColors(WallpaperManager.FLAG_SYSTEM)
|
||||||
|
val primary = colors?.primaryColor?.toArgb()
|
||||||
|
val secondary = colors?.secondaryColor?.toArgb()
|
||||||
|
val tertiary = colors?.tertiaryColor?.toArgb()
|
||||||
|
if (primary != null) preset.add(Color(primary).toTonalPalettes())
|
||||||
|
if (secondary != null) preset.add(Color(secondary).toTonalPalettes())
|
||||||
|
if (tertiary != null) preset.add(Color(tertiary).toTonalPalettes())
|
||||||
|
}
|
||||||
|
return preset
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package me.ash.reader.ui.theme.color
|
package me.ash.reader.ui.theme.palette.preset
|
||||||
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package me.ash.reader.ui.theme.color
|
package me.ash.reader.ui.theme.palette.preset
|
||||||
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package me.ash.reader.ui.theme.color
|
package me.ash.reader.ui.theme.palette.preset
|
||||||
|
|
||||||
import androidx.compose.material3.ColorScheme
|
import androidx.compose.material3.ColorScheme
|
||||||
import androidx.compose.material3.darkColorScheme
|
import androidx.compose.material3.darkColorScheme
|
|
@ -1,4 +1,4 @@
|
||||||
package me.ash.reader.ui.theme.color
|
package me.ash.reader.ui.theme.palette.preset
|
||||||
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
package me.ash.reader.ui.theme.palette.util
|
||||||
|
|
||||||
|
import kotlin.math.PI
|
||||||
|
|
||||||
|
internal fun square(x: Double): Double = x * x
|
||||||
|
|
||||||
|
internal fun Double.toRadians(): Double = this * PI / 180.0
|
||||||
|
internal fun Double.toDegrees(): Double = this * 180.0 / PI
|
||||||
|
|
||||||
|
operator fun DoubleArray.times(x: Double): DoubleArray = map { it * x }.toDoubleArray()
|
||||||
|
operator fun DoubleArray.div(x: Double): DoubleArray = map { it / x }.toDoubleArray()
|
||||||
|
|
||||||
|
class Matrix3(
|
||||||
|
private val x: DoubleArray,
|
||||||
|
private val y: DoubleArray,
|
||||||
|
private val z: DoubleArray,
|
||||||
|
) {
|
||||||
|
fun inverse(): Matrix3 {
|
||||||
|
val det = determinant()
|
||||||
|
return Matrix3(
|
||||||
|
doubleArrayOf(
|
||||||
|
(y[1] * z[2] - y[2] * z[1]) / det,
|
||||||
|
(y[2] * z[0] - y[0] * z[2]) / det,
|
||||||
|
(y[0] * z[1] - y[1] * z[0]) / det,
|
||||||
|
),
|
||||||
|
doubleArrayOf(
|
||||||
|
(x[2] * z[1] - x[1] * z[2]) / det,
|
||||||
|
(x[0] * z[2] - x[2] * z[0]) / det,
|
||||||
|
(x[1] * z[0] - x[0] * z[1]) / det,
|
||||||
|
),
|
||||||
|
doubleArrayOf(
|
||||||
|
(x[1] * y[2] - x[2] * y[1]) / det,
|
||||||
|
(x[2] * y[0] - x[0] * y[2]) / det,
|
||||||
|
(x[0] * y[1] - x[1] * y[0]) / det,
|
||||||
|
),
|
||||||
|
).transpose()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun determinant(): Double =
|
||||||
|
x[0] * (y[1] * z[2] - y[2] * z[1]) -
|
||||||
|
x[1] * (y[0] * z[2] - y[2] * z[0]) +
|
||||||
|
x[2] * (y[0] * z[1] - y[1] * z[0])
|
||||||
|
|
||||||
|
private fun transpose() = Matrix3(
|
||||||
|
doubleArrayOf(x[0], y[0], z[0]),
|
||||||
|
doubleArrayOf(x[1], y[1], z[1]),
|
||||||
|
doubleArrayOf(x[2], y[2], z[2]),
|
||||||
|
)
|
||||||
|
|
||||||
|
operator fun get(i: Int): DoubleArray = when (i) {
|
||||||
|
0 -> x
|
||||||
|
1 -> y
|
||||||
|
2 -> z
|
||||||
|
else -> throw IndexOutOfBoundsException("Index must be 0, 1 or 2")
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun times(vec: DoubleArray): DoubleArray = doubleArrayOf(
|
||||||
|
x[0] * vec[0] + x[1] * vec[1] + x[2] * vec[2],
|
||||||
|
y[0] * vec[0] + y[1] * vec[1] + y[2] * vec[2],
|
||||||
|
z[0] * vec[0] + z[1] * vec[1] + z[2] * vec[2],
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun toString(): String = "{" + arrayOf(x, y, z).joinToString { "[" + it.joinToString() + "]" } + "}"
|
||||||
|
}
|
159
app/src/main/res/drawable/palettie.xml
Normal file
159
app/src/main/res/drawable/palettie.xml
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="888dp"
|
||||||
|
android:height="677.20703dp"
|
||||||
|
android:viewportWidth="888"
|
||||||
|
android:viewportHeight="677.20703">
|
||||||
|
<path
|
||||||
|
android:pathData="M307.693,659.535l8.724,2.613l14.231,-32.408l-12.876,-3.856l-10.079,33.651z"
|
||||||
|
android:fillColor="#ffb8b8"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M306.32,656.02l17.182,5.146 0.001,0a11.431,11.431 0,0 1,7.669 14.229l-0.107,0.356 -28.131,-8.426Z"
|
||||||
|
android:fillColor="#2f2e41"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M390.602,666.663l8.551,-3.135l-8.023,-34.473l-12.62,4.627l12.092,32.981z"
|
||||||
|
android:fillColor="#ffb8b8"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M387.398,664.671l16.839,-6.174 0.001,-0a11.431,11.431 0,0 1,14.666 6.797l0.128,0.349L391.46,675.751Z"
|
||||||
|
android:fillColor="#2f2e41"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M322.321,600.868l-14.857,49.029l17.828,5.2l17.086,-45.315l-20.057,-8.914z"
|
||||||
|
android:fillColor="#2f2e41"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M369.121,612.011l13.372,46.057l18.571,-8.914l-13.371,-43.086l-18.572,5.943z"
|
||||||
|
android:fillColor="#2f2e41"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M362.162,620.318a203.98,203.98 0,0 1,-35.458 -3.27l-0.298,-0.06v-11.33l-6.79,-0.754 5.3,-18.928c-2.47,-29.16 1.04,-66.089 2.176,-76.719 0.26,-2.493 0.431,-3.891 0.431,-3.891l5.959,-50.65 9.97,-9.203 4.529,2.948 8.402,8.401c9.766,24.034 17.515,46.661 17.564,48.115l32.751,104.953 -0.221,0.156C395.106,618.113 377.77,620.318 362.162,620.318Z"
|
||||||
|
android:fillColor="#3f3d56"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M337.708,477.181l-2.29,14.294l15.503,6.395l-13.213,-20.689z"
|
||||||
|
android:strokeAlpha="0.2"
|
||||||
|
android:fillAlpha="0.2"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M368.128,445.438L332.284,445.438a2.784,2.784 0,0 1,-2.781 -2.781v-15.45a20.703,20.703 0,0 1,41.406 0v15.45A2.784,2.784 0,0 1,368.128 445.438Z"
|
||||||
|
android:fillColor="#2f2e41"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M354.164,429.328m-15.179,0a15.179,15.179 0,1 1,30.357 0a15.179,15.179 0,1 1,-30.357 0"
|
||||||
|
android:fillColor="#ffb8b8"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M375.807,428.752L353.889,428.752l-0.225,-3.147 -1.124,3.147h-3.375l-0.445,-6.237 -2.227,6.237h-6.53v-0.309a16.395,16.395 0,0 1,16.377 -16.377L359.43,412.065a16.395,16.395 0,0 1,16.377 16.377Z"
|
||||||
|
android:fillColor="#2f2e41"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M353.71,448.322a2.841,2.841 0,0 1,-0.492 -0.043l-16.049,-2.832L337.169,418.922h17.667l-0.437,0.51c-6.086,7.097 -1.501,18.606 1.774,24.834a2.74,2.74 0,0 1,-0.218 2.909A2.77,2.77 0,0 1,353.71 448.322Z"
|
||||||
|
android:fillColor="#2f2e41"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M382.029,512.433h-8.797a1.131,1.131 0,0 1,-1.13 -1.025l-1.761,-18.049h14.579l-1.761,18.049A1.131,1.131 0,0 1,382.029 512.433Z"
|
||||||
|
android:fillColor="#6c63ff"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M384.896,495.63L370.364,495.63a1.137,1.137 0,0 1,-1.135 -1.135v-2.725a1.137,1.137 0,0 1,1.135 -1.135h14.532a1.137,1.137 0,0 1,1.135 1.135v2.725A1.137,1.137 0,0 1,384.896 495.63Z"
|
||||||
|
android:fillColor="#2f2e41"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M327.892,504.667l0,0a27.881,27.881 0,0 0,33.468 6.765l3.304,-1.635Z"
|
||||||
|
android:strokeAlpha="0.2"
|
||||||
|
android:fillAlpha="0.2"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M380.711,501.173a6.966,6.966 0,0 0,-10.676 0.322l-15.326,-4.302 -4.886,8.676 21.728,5.772a7.004,7.004 0,0 0,9.16 -10.467Z"
|
||||||
|
android:fillColor="#ffb8b8"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M344.139,510.801c-7.286,0.001 -17.145,-4.271 -28.849,-12.546a5.731,5.731 0,0 1,-2.413 -3.928c-0.863,-5.469 4.471,-12.863 4.995,-13.572l5.608,-15.401c0.065,-0.25 1.872,-6.914 6.409,-9.284a7.438,7.438 0,0 1,6.214 -0.265c8.642,3.147 1.894,27.448 0.968,30.635l11.45,5.389 7.271,4.635 9.958,1.042 -2.704,12.512 -15.123,0.34A15.434,15.434 0,0 1,344.139 510.801Z"
|
||||||
|
android:fillColor="#3f3d56"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M887.675,396.659A98.58,98.58 0,1 0,698.454 435.469c-0.096,-0.107 -0.196,-0.211 -0.292,-0.319a98.666,98.666 0,0 0,17.954 27.783c0.022,0.025 0.045,0.049 0.068,0.073 0.606,0.66 1.215,1.317 1.838,1.96a98.28,98.28 0,0 0,69.529 30.254l-3.331,180.929h10.291l-2.083,-119.415 14.887,-7.838 -2.271,-4.314 -12.711,6.692 -0.978,-56.064A98.578,98.578 0,0 0,887.675 396.659Z"
|
||||||
|
android:fillColor="#e6e6e6"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M595.087,348.612a115.526,115.526 0,1 0,-221.75 45.481c-0.113,-0.126 -0.23,-0.248 -0.342,-0.374a115.628,115.628 0,0 0,21.041 32.559c0.026,0.029 0.053,0.057 0.08,0.086 0.71,0.774 1.423,1.543 2.154,2.297a115.176,115.176 0,0 0,81.482 35.454l-3.904,212.033h12.06l-2.441,-139.944 17.446,-9.185 -2.661,-5.055 -14.896,7.842 -1.146,-65.702A115.525,115.525 0,0 0,595.087 348.612Z"
|
||||||
|
android:fillColor="#e6e6e6"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M263.259,303.418A131.467,131.467 0,1 0,10.912 355.175c-0.129,-0.143 -0.261,-0.282 -0.389,-0.426a131.582,131.582 0,0 0,23.944 37.051c0.03,0.033 0.061,0.065 0.091,0.098 0.808,0.88 1.62,1.756 2.451,2.614a131.068,131.068 0,0 0,92.725 40.347L125.291,676.148h13.725L136.238,516.895 156.091,506.442l-3.029,-5.753 -16.952,8.925 -1.304,-74.767A131.465,131.465 0,0 0,263.259 303.418Z"
|
||||||
|
android:fillColor="#e6e6e6"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M756.685,85.976m-85.976,0a85.976,85.976 0,1 1,171.952 0a85.976,85.976 0,1 1,-171.952 0"
|
||||||
|
android:fillColor="#ff6584"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M245.559,187.616m-172.312,0a172.312,172.312 0,1 1,344.623 0a172.312,172.312 0,1 1,-344.623 0"
|
||||||
|
android:fillColor="#6c63ff"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M118.329,72.526A172.325,172.325 0,0 0,405.452 254.895,172.327 172.327,0 1,1 118.329,72.526Z"
|
||||||
|
android:strokeAlpha="0.2"
|
||||||
|
android:fillAlpha="0.2"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M246.032,187.616l0.474,0l8.521,488.532l-17.989,0l8.994,-488.532z"
|
||||||
|
android:fillColor="#3f3d56"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M244.954,461.173l28.484,-14.996l3.97,7.54l-28.484,14.996z"
|
||||||
|
android:fillColor="#3f3d56"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M509.115,671.578s0.622,-13.027 13.366,-11.513"
|
||||||
|
android:fillColor="#3f3d56"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M505.514,652.803m-6.379,0a6.379,6.379 0,1 1,12.757 0a6.379,6.379 0,1 1,-12.757 0"
|
||||||
|
android:fillColor="#6c63ff"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M504.476,663.545h1.801v12.604h-1.801z"
|
||||||
|
android:fillColor="#3f3d56"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M67.083,669.778s0.622,-13.027 13.366,-11.513"
|
||||||
|
android:fillColor="#3f3d56"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M63.482,651.003m-6.379,0a6.379,6.379 0,1 1,12.757 0a6.379,6.379 0,1 1,-12.757 0"
|
||||||
|
android:fillColor="#6c63ff"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M62.444,661.744h1.801v12.604h-1.801z"
|
||||||
|
android:fillColor="#3f3d56"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M171.514,670.678s0.622,-13.027 13.366,-11.513"
|
||||||
|
android:fillColor="#3f3d56"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M167.913,651.903m-6.379,0a6.379,6.379 0,1 1,12.757 0a6.379,6.379 0,1 1,-12.757 0"
|
||||||
|
android:fillColor="#6c63ff"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M166.875,662.644h1.801v12.604h-1.801z"
|
||||||
|
android:fillColor="#3f3d56"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M449.243,83.299l12.795,-10.233c-9.94,-1.097 -14.024,4.324 -15.695,8.615 -7.765,-3.224 -16.219,1.001 -16.219,1.001l25.6,9.294A19.372,19.372 0,0 0,449.243 83.299Z"
|
||||||
|
android:fillColor="#3f3d56"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M643.827,187.54l12.795,-10.233c-9.94,-1.097 -14.024,4.324 -15.695,8.615 -7.765,-3.224 -16.219,1.001 -16.219,1.001l25.6,9.294A19.372,19.372 0,0 0,643.827 187.54Z"
|
||||||
|
android:fillColor="#3f3d56"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M433.955,276.492l12.795,-10.233c-9.94,-1.097 -14.024,4.324 -15.695,8.615 -7.765,-3.224 -16.219,1.001 -16.219,1.001l25.6,9.294A19.372,19.372 0,0 0,433.955 276.492Z"
|
||||||
|
android:fillColor="#3f3d56"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M683.655,676.307s0.622,-13.027 13.366,-11.513"
|
||||||
|
android:fillColor="#3f3d56"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M563.919,676.307s0.622,-13.027 13.366,-11.513"
|
||||||
|
android:fillColor="#3f3d56"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M127.289,676.307s0.622,-13.027 13.366,-11.513"
|
||||||
|
android:fillColor="#3f3d56"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M737.671,676.307s0.622,-13.027 13.366,-11.513"
|
||||||
|
android:fillColor="#3f3d56"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M712.464,676.307s0.622,-13.027 13.366,-11.513"
|
||||||
|
android:fillColor="#3f3d56"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M660.465,676.307s-0.622,-13.027 -13.366,-11.513"
|
||||||
|
android:fillColor="#3f3d56"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M453.403,676.307s-0.622,-13.027 -13.366,-11.513"
|
||||||
|
android:fillColor="#3f3d56"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M281.452,676.307s-0.622,-13.027 -13.366,-11.513"
|
||||||
|
android:fillColor="#3f3d56"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M98.697,676.307s-0.622,-13.027 -13.366,-11.513"
|
||||||
|
android:fillColor="#3f3d56"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M791.904,676.307s-0.622,-13.027 -13.366,-11.513"
|
||||||
|
android:fillColor="#3f3d56"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M714.481,677.207s-0.622,-13.027 -13.366,-11.513"
|
||||||
|
android:fillColor="#3f3d56"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M0,674.604h888v2h-888z"
|
||||||
|
android:fillColor="#3f3d56"/>
|
||||||
|
</vector>
|
|
@ -79,7 +79,7 @@
|
||||||
<string name="color_and_style">颜色和样式</string>
|
<string name="color_and_style">颜色和样式</string>
|
||||||
<string name="color_and_style_desc">主题、色彩系统、字体大小</string>
|
<string name="color_and_style_desc">主题、色彩系统、字体大小</string>
|
||||||
<string name="interaction">交互</string>
|
<string name="interaction">交互</string>
|
||||||
<string name="interaction_desc">布局、触觉反馈</string>
|
<string name="interaction_desc">布局、触感反馈</string>
|
||||||
<string name="languages">语言</string>
|
<string name="languages">语言</string>
|
||||||
<string name="languages_desc">英语、中文</string>
|
<string name="languages_desc">英语、中文</string>
|
||||||
<string name="tips_and_support">提示和支持</string>
|
<string name="tips_and_support">提示和支持</string>
|
||||||
|
@ -89,4 +89,16 @@
|
||||||
<string name="view_terms">查看《<u>服务条款与隐私政策</u>》</string>
|
<string name="view_terms">查看《<u>服务条款与隐私政策</u>》</string>
|
||||||
<string name="terms_link">https://gitee.com/Ashinch/ReadYou/blob/main/TERMS_OF_SERVICE_AND_PRIVACY_POLICY-zh.md</string>
|
<string name="terms_link">https://gitee.com/Ashinch/ReadYou/blob/main/TERMS_OF_SERVICE_AND_PRIVACY_POLICY-zh.md</string>
|
||||||
<string name="agree_and_continue">同意并继续</string>
|
<string name="agree_and_continue">同意并继续</string>
|
||||||
|
<string name="wallpaper_colors">壁纸颜色</string>
|
||||||
|
<string name="no_palettes">暂无色板</string>
|
||||||
|
<string name="only_android_8.1_plus">仅限 Android 8.1+</string>
|
||||||
|
<string name="basic_colors">基本颜色</string>
|
||||||
|
<string name="style">样式</string>
|
||||||
|
<string name="dark_theme">深色模式</string>
|
||||||
|
<string name="use_device_theme">跟随系统设置</string>
|
||||||
|
<string name="tonal_elevation">色调海拔</string>
|
||||||
|
<string name="fonts">字体</string>
|
||||||
|
<string name="basic_fonts">基本字体</string>
|
||||||
|
<string name="reading_fonts">阅读字体</string>
|
||||||
|
<string name="reading_fonts_size">阅读字体大小</string>
|
||||||
</resources>
|
</resources>
|
|
@ -89,4 +89,16 @@
|
||||||
<string name="view_terms">View the <i><u>Terms of Service and Privacy Policy</u></i></string>
|
<string name="view_terms">View the <i><u>Terms of Service and Privacy Policy</u></i></string>
|
||||||
<string name="terms_link">https://github.com/Ashinch/ReadYou/blob/main/TERMS_OF_SERVICE_AND_PRIVACY_POLICY.md</string>
|
<string name="terms_link">https://github.com/Ashinch/ReadYou/blob/main/TERMS_OF_SERVICE_AND_PRIVACY_POLICY.md</string>
|
||||||
<string name="agree_and_continue">Agree and Continue</string>
|
<string name="agree_and_continue">Agree and Continue</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>
|
||||||
|
<string name="basic_colors">Basic Colors</string>
|
||||||
|
<string name="style">Style</string>
|
||||||
|
<string name="dark_theme">Dark Theme</string>
|
||||||
|
<string name="use_device_theme">Use Device Theme</string>
|
||||||
|
<string name="tonal_elevation">Tonal Elevation</string>
|
||||||
|
<string name="fonts">Fonts</string>
|
||||||
|
<string name="basic_fonts">Basic Fonts</string>
|
||||||
|
<string name="reading_fonts">Reading Fonts</string>
|
||||||
|
<string name="reading_fonts_size">Reading Fonts Size</string>
|
||||||
</resources>
|
</resources>
|
Loading…
Reference in New Issue
Block a user