Custom themes (#269)

* Update SettingsAppearanceScreen.kt

* Update SettingsAppearanceScreen.kt

* fix spotless

* improve

- Put custom theme in front
- Set default color for custom theme
- Using pre-defined padding values
- Make the color picker scrollable for phone in landscape mode

* fixes and new name

* Update ThemeColorPickerWidget.kt
This commit is contained in:
kana-shii 2024-08-23 10:39:28 -03:00 committed by Cuong-Tran
parent 840829a127
commit 2266447745
No known key found for this signature in database
GPG key ID: 733AA7624B9315C2
13 changed files with 429 additions and 4 deletions

View file

@ -273,9 +273,13 @@ dependencies {
implementation(libs.swipe)
implementation(libs.compose.webview)
implementation(libs.compose.grid)
// KMK -->
implementation(libs.palette.ktx)
implementation(libs.material.kolor)
implementation(libs.haze)
implementation(compose.colorpicker)
// KMK <--
// Logging
implementation(libs.timber)

View file

@ -30,6 +30,8 @@ class UiPreferences(
fun themeDarkAmoled() = preferenceStore.getBoolean("pref_theme_dark_amoled_key", false)
// KMK -->
fun colorTheme() = preferenceStore.getInt("pref_color_theme", 0xFFDF0090.toInt())
fun themeCoverBased() = preferenceStore.getBoolean("pref_theme_cover_based_key", true)
fun themeCoverBasedStyle() = preferenceStore.getEnum("pref_theme_cover_based_style_key", PaletteStyle.Vibrant)

View file

@ -10,6 +10,9 @@ enum class AppTheme(val titleRes: StringResource?) {
DEFAULT(MR.strings.label_default),
MONET(MR.strings.theme_monet),
// Kuukiyomi themes
CUSTOM(KMR.strings.theme_custom),
// Aniyomi themes
COTTONCANDY(KMR.strings.theme_cottoncandy),
MOCHA(KMR.strings.theme_mocha),

View file

@ -15,10 +15,12 @@ import com.materialkolor.PaletteStyle
import eu.kanade.core.preference.asState
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.domain.ui.model.AppTheme
import eu.kanade.domain.ui.model.TabletUiMode
import eu.kanade.domain.ui.model.ThemeMode
import eu.kanade.domain.ui.model.setAppCompatDelegateThemeMode
import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.more.settings.screen.appearance.AppCustomThemeColorPickerScreen
import eu.kanade.presentation.more.settings.screen.appearance.AppLanguageScreen
import eu.kanade.presentation.more.settings.widget.AppThemeModePreferenceWidget
import eu.kanade.presentation.more.settings.widget.AppThemePreferenceWidget
@ -36,6 +38,7 @@ import uy.kohesive.injekt.api.get
import java.time.LocalDate
object SettingsAppearanceScreen : SearchableSettings {
private fun readResolve(): Any = SettingsAppearanceScreen
@ReadOnlyComposable
@Composable
@ -63,6 +66,7 @@ object SettingsAppearanceScreen : SearchableSettings {
uiPreferences: UiPreferences,
): Preference.PreferenceGroup {
val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow
val themeModePref = uiPreferences.themeMode()
val themeMode by themeModePref.collectAsState()
@ -95,6 +99,14 @@ object SettingsAppearanceScreen : SearchableSettings {
)
}
},
// KMK -->
Preference.PreferenceItem.TextPreference(
title = stringResource(KMR.strings.pref_custom_color),
enabled = appTheme == AppTheme.CUSTOM,
subtitle = stringResource(KMR.strings.custom_color_description),
onClick = { navigator.push(AppCustomThemeColorPickerScreen()) },
),
// KMK <--
Preference.PreferenceItem.SwitchPreference(
pref = amoledPref,
title = stringResource(MR.strings.pref_dark_theme_pure_black),

View file

@ -0,0 +1,77 @@
package eu.kanade.presentation.more.settings.screen.appearance
import android.app.Activity
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.core.app.ActivityCompat
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import com.github.skydoves.colorpicker.compose.rememberColorPickerController
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.more.settings.widget.ThemeColorPickerWidget
import eu.kanade.presentation.util.Screen
import tachiyomi.i18n.kmk.KMR
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.collectAsState
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class AppCustomThemeColorPickerScreen : Screen() {
@Composable
override fun Content() {
val uiPreferences: UiPreferences = Injekt.get()
val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow
val controller = rememberColorPickerController()
val customColorPref = uiPreferences.colorTheme()
val customColor by customColorPref.collectAsState()
val appThemePref = uiPreferences.appTheme()
val currentColor by remember {
mutableIntStateOf(customColor)
}
LaunchedEffect(customColorPref) {
customColorPref.set(currentColor)
}
Scaffold(
topBar = { scrollBehavior ->
AppBar(
title = stringResource(KMR.strings.pref_custom_color),
navigateUp = navigator::pop,
scrollBehavior = scrollBehavior,
)
},
) { contentPadding ->
Column(
modifier = Modifier.padding(contentPadding),
) {
ThemeColorPickerWidget(
controller = controller,
initialColor = Color(currentColor),
onItemClick = { color, appTheme ->
customColorPref.set(color.toArgb())
appThemePref.set(appTheme)
(context as? Activity)?.let { ActivityCompat.recreate(it) }
},
)
}
}
}
}

View file

@ -0,0 +1,100 @@
package eu.kanade.presentation.more.settings.widget
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.ImageBitmapConfig
import androidx.compose.ui.graphics.LinearGradientShader
import androidx.compose.ui.graphics.Paint
import androidx.compose.ui.graphics.TileMode
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import com.github.skydoves.colorpicker.compose.BrightnessSlider
import com.github.skydoves.colorpicker.compose.ColorPickerController
@Composable
fun CustomBrightnessSlider(
initialColor: Color,
controller: ColorPickerController,
modifier: Modifier = Modifier,
) {
// Define your colors and sizes directly
val borderColor = Color.LightGray // Color for the slider border
val thumbRadius = 12.dp // Example thumb radius
val trackHeight = 4.dp // Example track height
val borderSize = 1.dp // Example border size for the slider
// Set up the paint for the thumb (wheel)
val wheelPaint = Paint().apply {
color = Color.White
alpha = 1.0f
}
// This function creates the ImageBitmap for the gradient background of the slider
@Composable
fun rememberSliderGradientBitmap(
width: Dp,
height: Dp,
startColor: Color,
endColor: Color,
): ImageBitmap {
val sizePx = with(LocalDensity.current) { IntSize(width.roundToPx(), height.roundToPx()) }
return remember(sizePx, startColor, endColor) {
ImageBitmap(sizePx.width, sizePx.height, ImageBitmapConfig.Argb8888).apply {
val canvas = Canvas(this)
val shader = LinearGradientShader(
colors = listOf(startColor, endColor),
from = Offset(0f, 0f),
to = Offset(sizePx.width.toFloat(), 0f),
tileMode = TileMode.Clamp,
)
val paint = Paint().apply {
this.shader = shader
}
canvas.drawRect(
0f,
0f,
sizePx.width.toFloat(),
sizePx.height.toFloat(),
paint,
)
}
}
}
// Obtain the Composable's size for the gradient background
val sliderWidth = 20.dp // Example width, adjust to your needs
val sliderHeight = thumbRadius * 2 // The height is double the thumb radius
val gradientBitmap = rememberSliderGradientBitmap(
width = sliderWidth, // Subtract the thumb radii from the total width
height = trackHeight,
startColor = Color.White,
endColor = Color.White,
)
BrightnessSlider(
modifier = modifier
.height(sliderHeight)
.fillMaxWidth()
.padding(horizontal = thumbRadius), // Padding equals thumb radius
controller = controller,
initialColor = initialColor,
borderRadius = thumbRadius, // Use thumbRadius for the rounded corners
borderSize = borderSize,
borderColor = borderColor, // Use borderColor for the slider border
wheelRadius = thumbRadius,
wheelColor = Color.White, // Thumb (wheel) color
wheelImageBitmap = gradientBitmap, // Use the generated gradient bitmap as the background
wheelAlpha = 1.0f, // Full opacity for the thumb
wheelPaint = wheelPaint, // Use the defined wheel paint
)
}

View file

@ -0,0 +1,124 @@
package eu.kanade.presentation.more.settings.widget
import android.graphics.Bitmap
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import com.github.skydoves.colorpicker.compose.ColorEnvelope
import com.github.skydoves.colorpicker.compose.ColorPickerController
import com.github.skydoves.colorpicker.compose.HsvColorPicker
import eu.kanade.domain.ui.model.AppTheme
import tachiyomi.i18n.kmk.KMR
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
import kotlin.math.roundToInt
@Composable
internal fun ThemeColorPickerWidget(
initialColor: Color,
controller: ColorPickerController,
onItemClick: (Color, AppTheme) -> Unit,
) {
var selectedColor by remember { mutableStateOf(initialColor) }
var showConfirmButton by remember { mutableStateOf(false) }
val wheelSize = with(LocalDensity.current) { 20.dp.toPx().roundToInt() }
val wheelStrokeWidth = with(LocalDensity.current) { 2.dp.toPx() }
// Remember a wheel bitmap
val wheelBitmap = remember(wheelSize, wheelStrokeWidth) {
val bitmap = Bitmap.createBitmap(wheelSize, wheelSize, Bitmap.Config.ARGB_8888)
val canvas = android.graphics.Canvas(bitmap)
val paint = android.graphics.Paint().apply {
color = android.graphics.Color.WHITE
style = android.graphics.Paint.Style.STROKE
strokeWidth = wheelStrokeWidth
isAntiAlias = true
}
// Draw the circle for wheel indicator
canvas.drawCircle(
wheelSize / 2f,
wheelSize / 2f,
wheelSize / 2f - wheelStrokeWidth,
paint,
)
bitmap.asImageBitmap()
}
BasePreferenceWidget(
subcomponent = {
Column(
modifier = Modifier
.padding(horizontal = MaterialTheme.padding.large)
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Box(
modifier = Modifier
.padding(
vertical = MaterialTheme.padding.medium,
),
) {
HsvColorPicker(
modifier = Modifier
.size(300.dp),
controller = controller,
wheelImageBitmap = wheelBitmap,
initialColor = initialColor,
onColorChanged = { colorEnvelope: ColorEnvelope ->
selectedColor = colorEnvelope.color
showConfirmButton = true
},
)
}
CustomBrightnessSlider(
modifier = Modifier
.fillMaxWidth(),
controller = controller,
initialColor = initialColor,
)
AnimatedVisibility(
visible = showConfirmButton,
enter = fadeIn() + expandVertically(),
modifier = Modifier
.padding(top = MaterialTheme.padding.large),
) {
Button(
onClick = {
onItemClick(selectedColor, AppTheme.CUSTOM)
},
modifier = Modifier
.fillMaxWidth()
.height(48.dp),
content = {
Text(text = stringResource(KMR.strings.action_confirm_color))
},
)
}
}
},
)
}

View file

@ -13,6 +13,7 @@ import eu.kanade.domain.ui.model.AppTheme
import eu.kanade.presentation.theme.colorscheme.BaseColorScheme
import eu.kanade.presentation.theme.colorscheme.CloudflareColorScheme
import eu.kanade.presentation.theme.colorscheme.CottoncandyColorScheme
import eu.kanade.presentation.theme.colorscheme.CustomColorScheme
import eu.kanade.presentation.theme.colorscheme.DoomColorScheme
import eu.kanade.presentation.theme.colorscheme.GreenAppleColorScheme
import eu.kanade.presentation.theme.colorscheme.LavenderColorScheme
@ -102,10 +103,19 @@ private fun getThemeColorScheme(
appTheme: AppTheme,
isAmoled: Boolean,
): ColorScheme {
val colorScheme = if (appTheme == AppTheme.MONET) {
MonetColorScheme(LocalContext.current)
} else {
colorSchemes.getOrDefault(appTheme, TachiyomiColorScheme)
val uiPreferences = Injekt.get<UiPreferences>()
val colorScheme = when (appTheme) {
AppTheme.MONET -> {
MonetColorScheme(LocalContext.current)
}
// KMK -->
AppTheme.CUSTOM -> {
CustomColorScheme(uiPreferences)
}
// KMK <--
else -> {
colorSchemes.getOrDefault(appTheme, TachiyomiColorScheme)
}
}
return colorScheme.getColorScheme(
isSystemInDarkTheme(),

View file

@ -0,0 +1,80 @@
package eu.kanade.presentation.theme.colorscheme
import android.annotation.SuppressLint
import androidx.compose.material3.ColorScheme
import androidx.compose.ui.graphics.Color
import com.google.android.material.color.utilities.Hct
import com.google.android.material.color.utilities.MaterialDynamicColors
import com.google.android.material.color.utilities.SchemeContent
import eu.kanade.domain.ui.UiPreferences
internal class CustomColorScheme(uiPreferences: UiPreferences) : BaseColorScheme() {
private val seed = uiPreferences.colorTheme().get()
private val custom = CustomCompatColorScheme(seed)
override val darkScheme
get() = custom.darkScheme
override val lightScheme
get() = custom.lightScheme
}
private class CustomCompatColorScheme(seed: Int) : BaseColorScheme() {
override val lightScheme = generateColorSchemeFromSeed(seed = seed, dark = false)
override val darkScheme = generateColorSchemeFromSeed(seed = seed, dark = true)
companion object {
private fun Int.toComposeColor(): Color = Color(this)
@SuppressLint("RestrictedApi")
private fun generateColorSchemeFromSeed(seed: Int, dark: Boolean): ColorScheme {
val scheme = SchemeContent(
Hct.fromInt(seed),
dark,
0.0,
)
val dynamicColors = MaterialDynamicColors()
return ColorScheme(
primary = dynamicColors.primary().getArgb(scheme).toComposeColor(),
onPrimary = dynamicColors.onPrimary().getArgb(scheme).toComposeColor(),
primaryContainer = dynamicColors.primaryContainer().getArgb(scheme).toComposeColor(),
onPrimaryContainer = dynamicColors.onPrimaryContainer().getArgb(scheme).toComposeColor(),
inversePrimary = dynamicColors.inversePrimary().getArgb(scheme).toComposeColor(),
secondary = dynamicColors.secondary().getArgb(scheme).toComposeColor(),
onSecondary = dynamicColors.onSecondary().getArgb(scheme).toComposeColor(),
secondaryContainer = dynamicColors.secondaryContainer().getArgb(scheme).toComposeColor(),
onSecondaryContainer = dynamicColors.onSecondaryContainer().getArgb(scheme).toComposeColor(),
tertiary = dynamicColors.tertiary().getArgb(scheme).toComposeColor(),
onTertiary = dynamicColors.onTertiary().getArgb(scheme).toComposeColor(),
tertiaryContainer = dynamicColors.tertiary().getArgb(scheme).toComposeColor(),
onTertiaryContainer = dynamicColors.onTertiaryContainer().getArgb(scheme).toComposeColor(),
background = dynamicColors.background().getArgb(scheme).toComposeColor(),
onBackground = dynamicColors.onBackground().getArgb(scheme).toComposeColor(),
surface = dynamicColors.surface().getArgb(scheme).toComposeColor(),
onSurface = dynamicColors.onSurface().getArgb(scheme).toComposeColor(),
surfaceVariant = dynamicColors.surfaceVariant().getArgb(scheme).toComposeColor(),
onSurfaceVariant = dynamicColors.onSurfaceVariant().getArgb(scheme).toComposeColor(),
surfaceTint = dynamicColors.surfaceTint().getArgb(scheme).toComposeColor(),
inverseSurface = dynamicColors.inverseSurface().getArgb(scheme).toComposeColor(),
inverseOnSurface = dynamicColors.inverseOnSurface().getArgb(scheme).toComposeColor(),
error = dynamicColors.error().getArgb(scheme).toComposeColor(),
onError = dynamicColors.onError().getArgb(scheme).toComposeColor(),
errorContainer = dynamicColors.errorContainer().getArgb(scheme).toComposeColor(),
onErrorContainer = dynamicColors.onErrorContainer().getArgb(scheme).toComposeColor(),
outline = dynamicColors.outline().getArgb(scheme).toComposeColor(),
outlineVariant = dynamicColors.outlineVariant().getArgb(scheme).toComposeColor(),
scrim = Color.Black,
surfaceBright = dynamicColors.surfaceBright().getArgb(scheme).toComposeColor(),
surfaceDim = dynamicColors.surfaceDim().getArgb(scheme).toComposeColor(),
surfaceContainer = dynamicColors.surfaceContainer().getArgb(scheme).toComposeColor(),
surfaceContainerHigh = dynamicColors.surfaceContainerHigh().getArgb(scheme).toComposeColor(),
surfaceContainerHighest = dynamicColors.surfaceContainerHighest().getArgb(scheme).toComposeColor(),
surfaceContainerLow = dynamicColors.surfaceContainerLow().getArgb(scheme).toComposeColor(),
surfaceContainerLowest = dynamicColors.surfaceContainerLowest().getArgb(scheme).toComposeColor(),
)
}
}
}

View file

@ -30,6 +30,7 @@ class ThemingDelegateImpl : ThemingDelegate {
private val themeResources: Map<AppTheme, Int> = mapOf(
AppTheme.MONET to R.style.Theme_Tachiyomi_Monet,
AppTheme.CUSTOM to R.style.Theme_Tachiyomi_Custom,
AppTheme.GREEN_APPLE to R.style.Theme_Tachiyomi_GreenApple,
AppTheme.LAVENDER to R.style.Theme_Tachiyomi_Lavender,
AppTheme.MIDNIGHT_DUSK to R.style.Theme_Tachiyomi_MidnightDusk,

View file

@ -96,6 +96,10 @@
<!-- Monet theme only support S+ -->
<style name="Theme.Tachiyomi.Monet" />
<!-- Custom Theme -->
<style name="Theme.Tachiyomi.Custom" />
<!--== Cotton Candy Theme ==-->
<style name="Theme.Tachiyomi.CottonCandy">
<!-- Theme Colors -->

View file

@ -18,5 +18,6 @@ material3-core = { module = "androidx.compose.material3:material3" }
material-icons = { module = "androidx.compose.material:material-icons-extended" }
glance = "androidx.glance:glance-appwidget:1.1.0"
colorpicker = {module = "com.github.skydoves:colorpicker-compose", version = "1.1.2"}
accompanist-systemuicontroller = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanist" }

View file

@ -10,6 +10,8 @@
<string name="action_move_up">Move up</string>
<string name="action_move_down">Move down</string>
<string name="action_move_bottom">Move bottom</string>
<string name="action_confirm_color">Confirm Color</string>
<!-- Preferences -->
<!-- Appearance section -->
@ -38,6 +40,11 @@
<string name="theme_mocha">Mocha</string>
<string name="theme_sapphire">Sapphire</string>
<!-- Custom Color Picker -->
<string name="pref_custom_color">Palette Personalizer</string>
<string name="custom_color_description">Pick a color for the custom Theme</string>
<string name="theme_custom">Custom Theme</string>
<!-- Library section -->
<string name="pref_show_updating_progress_banner">Show updating progress banner</string>
<string name="action_display_source_badge">Source icon</string>