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() {
|
||||
super.onCreate()
|
||||
CrashHandler(this)
|
||||
dataStoreInit()
|
||||
applicationScope.launch(dispatcherDefault) {
|
||||
accountInit()
|
||||
dataStoreInit()
|
||||
workerInit()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package me.ash.reader
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import java.lang.Thread.UncaughtExceptionHandler
|
||||
import kotlin.system.exitProcess
|
||||
|
@ -13,6 +14,7 @@ class CrashHandler(private val context: Context) : UncaughtExceptionHandler {
|
|||
override fun uncaughtException(p0: Thread, p1: Throwable) {
|
||||
Toast.makeText(context, p1.message, Toast.LENGTH_LONG).show()
|
||||
p1.printStackTrace()
|
||||
Log.e("RLog", "uncaughtException: ${p1.message}" )
|
||||
android.os.Process.killProcess(android.os.Process.myPid());
|
||||
exitProcess(1)
|
||||
}
|
||||
|
|
|
@ -19,10 +19,12 @@ import androidx.compose.runtime.CompositionLocalProvider
|
|||
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.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import me.ash.reader.ui.theme.palette.onDark
|
||||
|
||||
@Composable
|
||||
fun Banner(
|
||||
|
@ -33,20 +35,18 @@ fun Banner(
|
|||
action: (@Composable () -> Unit)? = null,
|
||||
onClick: () -> Unit = {},
|
||||
) {
|
||||
val lightThemeColors = MaterialTheme.colorScheme
|
||||
val lightPrimaryContainer = lightThemeColors.primaryContainer
|
||||
val lightOnSurface = lightThemeColors.onSurface
|
||||
|
||||
Surface(
|
||||
modifier = modifier.fillMaxWidth().height(88.dp),
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(88.dp),
|
||||
color = Color.Unspecified,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 16.dp)
|
||||
.clip(RoundedCornerShape(32.dp))
|
||||
.background(lightPrimaryContainer)
|
||||
.background(MaterialTheme.colorScheme.primaryContainer onDark MaterialTheme.colorScheme.onPrimaryContainer)
|
||||
.clickable { onClick() }
|
||||
.padding(16.dp, 20.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
|
@ -57,26 +57,29 @@ fun Banner(
|
|||
imageVector = it,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.padding(end = 16.dp),
|
||||
tint = lightOnSurface,
|
||||
tint = MaterialTheme.colorScheme.onSurface onDark MaterialTheme.colorScheme.surface,
|
||||
)
|
||||
}
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier.weight(1f).fillMaxHeight(),
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxHeight(),
|
||||
verticalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
maxLines = if (desc == null) 2 else 1,
|
||||
style = MaterialTheme.typography.titleLarge.copy(fontSize = 20.sp),
|
||||
color = lightOnSurface,
|
||||
color = MaterialTheme.colorScheme.onSurface onDark MaterialTheme.colorScheme.surface,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
desc?.let {
|
||||
Text(
|
||||
text = it,
|
||||
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,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
|
@ -84,9 +87,12 @@ fun Banner(
|
|||
}
|
||||
action?.let {
|
||||
Box(Modifier.padding(start = 16.dp)) {
|
||||
CompositionLocalProvider(LocalContentColor provides lightOnSurface) {
|
||||
it()
|
||||
}
|
||||
CompositionLocalProvider(
|
||||
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)!!
|
||||
val Context.currentAccountType: Int
|
||||
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) {
|
||||
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")
|
||||
fun <T> DataStore<Preferences>.get(dataStoreKeys: DataStoreKeys<T>): T? {
|
||||
return runBlocking {
|
||||
|
@ -59,4 +71,14 @@ sealed class DataStoreKeys<T> {
|
|||
override val key: Preferences.Key<Int>
|
||||
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 com.google.accompanist.insets.ProvideWindowInsets
|
||||
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.rememberAnimatedNavController
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
import me.ash.reader.ui.ext.animatedComposable
|
||||
import me.ash.reader.ui.ext.isFirstLaunch
|
||||
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.startup.StartupPage
|
||||
import me.ash.reader.ui.theme.AppTheme
|
||||
|
@ -38,11 +38,11 @@ fun HomeEntry() {
|
|||
setSystemBarsColor(Color.Transparent, !isSystemInDarkTheme())
|
||||
setNavigationBarColor(MaterialTheme.colorScheme.surface, !isSystemInDarkTheme())
|
||||
}
|
||||
Column(modifier = Modifier.background(MaterialTheme.colorScheme.surface)) {
|
||||
Column {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.statusBarsPadding()
|
||||
.background(MaterialTheme.colorScheme.surface),
|
||||
) {
|
||||
AnimatedNavHost(
|
||||
navController = navController,
|
||||
|
@ -57,12 +57,16 @@ fun HomeEntry() {
|
|||
animatedComposable(route = RouteName.SETTINGS) {
|
||||
SettingsPage(navController)
|
||||
}
|
||||
animatedComposable(route = RouteName.COLOR_AND_STYLE) {
|
||||
ColorAndStyle(navController)
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.navigationBarsHeight()
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.surface)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,4 +7,5 @@ object RouteName {
|
|||
const val ARTICLE = "article"
|
||||
const val READ = "read"
|
||||
const val SETTINGS = "settings"
|
||||
const val COLOR_AND_STYLE = "color_and_style"
|
||||
}
|
|
@ -54,13 +54,13 @@ fun FilterBar(
|
|||
view.playSoundEffect(SoundEffectConstants.CLICK)
|
||||
filterOnClick(item)
|
||||
},
|
||||
// colors = NavigationBarItemDefaults.colors(
|
||||
colors = NavigationBarItemDefaults.colors(
|
||||
// selectedIconColor = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
// unselectedIconColor = MaterialTheme.colorScheme.outline,
|
||||
// selectedTextColor = MaterialTheme.colorScheme.onSurface,
|
||||
// unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
// indicatorColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||
// )
|
||||
indicatorColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
)
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(60.dp))
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
package me.ash.reader.ui.page.home
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.navigation.NavHostController
|
||||
import com.google.accompanist.insets.statusBarsPadding
|
||||
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||
import kotlinx.coroutines.launch
|
||||
import me.ash.reader.ui.component.ViewPager
|
||||
|
@ -91,7 +94,11 @@ fun HomePage(
|
|||
)
|
||||
}
|
||||
|
||||
Column {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(MaterialTheme.colorScheme.surface)
|
||||
.statusBarsPadding(),
|
||||
) {
|
||||
ViewPager(
|
||||
modifier = Modifier.weight(1f),
|
||||
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
|
||||
*/
|
||||
|
||||
package me.ash.reader.ui.component
|
||||
package me.ash.reader.ui.page.settings
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
|
@ -22,6 +22,7 @@ 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.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
@ -30,6 +31,7 @@ import androidx.compose.ui.unit.sp
|
|||
@Composable
|
||||
fun SelectableSettingGroupItem(
|
||||
modifier: Modifier = Modifier,
|
||||
enable: Boolean = true,
|
||||
selected: Boolean = false,
|
||||
title: String,
|
||||
desc: String? = null,
|
||||
|
@ -37,7 +39,7 @@ fun SelectableSettingGroupItem(
|
|||
onClick: () -> Unit,
|
||||
) {
|
||||
Surface(
|
||||
modifier = modifier.clickable { onClick() },
|
||||
modifier = modifier.clickable { onClick() }.alpha(if (enable) 1f else 0.5f),
|
||||
color = Color.Unspecified,
|
||||
) {
|
||||
Row(
|
||||
|
@ -45,7 +47,7 @@ fun SelectableSettingGroupItem(
|
|||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
.background(
|
||||
color = if (selected) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.surface,
|
||||
color = if (selected) MaterialTheme.colorScheme.onSurface else Color.Unspecified,
|
||||
shape = RoundedCornerShape(24.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.unit.dp
|
||||
import androidx.navigation.NavHostController
|
||||
import com.google.accompanist.insets.statusBarsPadding
|
||||
import me.ash.reader.R
|
||||
import me.ash.reader.ui.component.Banner
|
||||
import me.ash.reader.ui.component.DisplayText
|
||||
import me.ash.reader.ui.component.FeedbackIconButton
|
||||
import me.ash.reader.ui.component.SelectableSettingGroupItem
|
||||
import me.ash.reader.ui.page.common.RouteName
|
||||
import me.ash.reader.ui.theme.palette.onLight
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
|
@ -27,9 +28,13 @@ fun SettingsPage(
|
|||
navController: NavHostController,
|
||||
) {
|
||||
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 = {
|
||||
SmallTopAppBar(
|
||||
colors = TopAppBarDefaults.smallTopAppBarColors(containerColor = MaterialTheme.colorScheme.surface onLight MaterialTheme.colorScheme.inverseOnSurface),
|
||||
title = {},
|
||||
navigationIcon = {
|
||||
FeedbackIconButton(
|
||||
|
@ -67,6 +72,7 @@ fun SettingsPage(
|
|||
title = stringResource(R.string.accounts),
|
||||
desc = stringResource(R.string.accounts_desc),
|
||||
icon = Icons.Outlined.AccountCircle,
|
||||
enable = false,
|
||||
) {}
|
||||
}
|
||||
item {
|
||||
|
@ -74,13 +80,16 @@ fun SettingsPage(
|
|||
title = stringResource(R.string.color_and_style),
|
||||
desc = stringResource(R.string.color_and_style_desc),
|
||||
icon = Icons.Outlined.Palette,
|
||||
) {}
|
||||
) {
|
||||
navController.navigate(RouteName.COLOR_AND_STYLE)
|
||||
}
|
||||
}
|
||||
item {
|
||||
SelectableSettingGroupItem(
|
||||
title = stringResource(R.string.interaction),
|
||||
desc = stringResource(R.string.interaction_desc),
|
||||
icon = Icons.Outlined.TouchApp,
|
||||
enable = false,
|
||||
) {}
|
||||
}
|
||||
item {
|
||||
|
@ -88,6 +97,7 @@ fun SettingsPage(
|
|||
title = stringResource(R.string.languages),
|
||||
desc = stringResource(R.string.languages_desc),
|
||||
icon = Icons.Outlined.Language,
|
||||
enable = false,
|
||||
) {}
|
||||
}
|
||||
item {
|
||||
|
@ -95,6 +105,7 @@ fun SettingsPage(
|
|||
title = stringResource(R.string.tips_and_support),
|
||||
desc = stringResource(R.string.tips_and_support_desc),
|
||||
icon = Icons.Outlined.TipsAndUpdates,
|
||||
enable = false,
|
||||
) {}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,13 +12,13 @@ import androidx.compose.material.icons.rounded.CheckCircleOutline
|
|||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.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 com.ireward.htmlcompose.HtmlText
|
||||
import kotlinx.coroutines.launch
|
||||
import me.ash.reader.R
|
||||
|
@ -37,7 +37,7 @@ fun StartupPage(
|
|||
val scope = rememberCoroutineScope()
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier.background(MaterialTheme.colorScheme.surface),
|
||||
modifier = Modifier.statusBarsPadding().background(MaterialTheme.colorScheme.surface),
|
||||
topBar = {},
|
||||
content = {
|
||||
LazyColumn {
|
||||
|
@ -87,29 +87,31 @@ fun StartupPage(
|
|||
}
|
||||
},
|
||||
bottomBar = {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(24.dp),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
ExtendedFloatingActionButton(
|
||||
onClick = {
|
||||
navController.navigate(route = RouteName.HOME)
|
||||
scope.launch {
|
||||
context.dataStore.put(DataStoreKeys.IsFirstLaunch, false)
|
||||
}
|
||||
},
|
||||
icon = {
|
||||
Icon(
|
||||
Icons.Rounded.CheckCircleOutline,
|
||||
stringResource(R.string.agree_and_continue)
|
||||
)
|
||||
},
|
||||
text = { Text(text = stringResource(R.string.agree_and_continue)) },
|
||||
)
|
||||
}
|
||||
// Row(
|
||||
// modifier = Modifier
|
||||
// .fillMaxWidth()
|
||||
// .padding(24.dp),
|
||||
// horizontalArrangement = Arrangement.End,
|
||||
// verticalAlignment = Alignment.CenterVertically,
|
||||
// ) {
|
||||
// }
|
||||
},
|
||||
floatingActionButton = {
|
||||
ExtendedFloatingActionButton(
|
||||
onClick = {
|
||||
navController.navigate(route = RouteName.HOME)
|
||||
scope.launch {
|
||||
context.dataStore.put(DataStoreKeys.IsFirstLaunch, false)
|
||||
}
|
||||
},
|
||||
icon = {
|
||||
Icon(
|
||||
Icons.Rounded.CheckCircleOutline,
|
||||
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
|
||||
|
||||
import android.os.Build
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import me.ash.reader.ui.theme.color.PurpleColor
|
||||
|
||||
private val LightThemeColors = PurpleColor.lightColorScheme
|
||||
private val DarkThemeColors = PurpleColor.darkColorScheme
|
||||
|
||||
val LocalLightThemeColors = staticCompositionLocalOf { LightThemeColors }
|
||||
val LocalDarkThemeColors = staticCompositionLocalOf { DarkThemeColors }
|
||||
import kotlinx.coroutines.flow.map
|
||||
import me.ash.reader.ui.ext.DataStoreKeys
|
||||
import me.ash.reader.ui.ext.dataStore
|
||||
import me.ash.reader.ui.theme.palette.LocalTonalPalettes
|
||||
import me.ash.reader.ui.theme.palette.TonalPalettes
|
||||
import me.ash.reader.ui.theme.palette.core.ProvideZcamViewingConditions
|
||||
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
|
||||
fun AppTheme(
|
||||
useDarkTheme: Boolean = isSystemInDarkTheme(),
|
||||
wallpaperPalettes: List<TonalPalettes> = extractTonalPalettesFromUserWallpaper(),
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
// Dynamic color is available on Android 12+
|
||||
val dynamicColor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||
val context = LocalContext.current
|
||||
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
|
||||
}
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalLightThemeColors provides light,
|
||||
LocalDarkThemeColors provides dark,
|
||||
) {
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = AppTypography,
|
||||
content = content
|
||||
)
|
||||
ProvideZcamViewingConditions {
|
||||
CompositionLocalProvider(
|
||||
LocalTonalPalettes provides wallpaperPalettes[
|
||||
if (themeIndex >= wallpaperPalettes.size) {
|
||||
when {
|
||||
wallpaperPalettes.size == 5 -> 0
|
||||
wallpaperPalettes.size > 5 -> 5
|
||||
else -> 0
|
||||
}
|
||||
} else {
|
||||
themeIndex
|
||||
}
|
||||
]
|
||||
) {
|
||||
MaterialTheme(
|
||||
colorScheme =
|
||||
if (useDarkTheme) dynamicDarkColorScheme()
|
||||
else dynamicLightColorScheme(),
|
||||
typography = AppTypography,
|
||||
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
|
||||
|
|
@ -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
|
||||
|
|
@ -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.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
|
||||
|
|
@ -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_desc">主题、色彩系统、字体大小</string>
|
||||
<string name="interaction">交互</string>
|
||||
<string name="interaction_desc">布局、触觉反馈</string>
|
||||
<string name="interaction_desc">布局、触感反馈</string>
|
||||
<string name="languages">语言</string>
|
||||
<string name="languages_desc">英语、中文</string>
|
||||
<string name="tips_and_support">提示和支持</string>
|
||||
|
@ -89,4 +89,16 @@
|
|||
<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="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>
|
|
@ -89,4 +89,16 @@
|
|||
<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="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>
|
Loading…
Reference in New Issue
Block a user