diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 51fc1f4a4..a10d94d27 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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) diff --git a/app/src/main/java/eu/kanade/domain/ui/UiPreferences.kt b/app/src/main/java/eu/kanade/domain/ui/UiPreferences.kt index e04543c66..178e9adbe 100644 --- a/app/src/main/java/eu/kanade/domain/ui/UiPreferences.kt +++ b/app/src/main/java/eu/kanade/domain/ui/UiPreferences.kt @@ -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) diff --git a/app/src/main/java/eu/kanade/domain/ui/model/AppTheme.kt b/app/src/main/java/eu/kanade/domain/ui/model/AppTheme.kt index 87e76ef03..61670c85a 100644 --- a/app/src/main/java/eu/kanade/domain/ui/model/AppTheme.kt +++ b/app/src/main/java/eu/kanade/domain/ui/model/AppTheme.kt @@ -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), diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAppearanceScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAppearanceScreen.kt index 2f893886f..1c9b8e501 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAppearanceScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAppearanceScreen.kt @@ -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), diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/appearance/AppCustomThemeColorPickerScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/appearance/AppCustomThemeColorPickerScreen.kt new file mode 100644 index 000000000..13a6966f6 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/appearance/AppCustomThemeColorPickerScreen.kt @@ -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) } + }, + ) + } + } + } +} diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/widget/CustomBrightnessSlider.kt b/app/src/main/java/eu/kanade/presentation/more/settings/widget/CustomBrightnessSlider.kt new file mode 100644 index 000000000..a1da01d20 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/more/settings/widget/CustomBrightnessSlider.kt @@ -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 + ) +} diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/widget/ThemeColorPickerWidget.kt b/app/src/main/java/eu/kanade/presentation/more/settings/widget/ThemeColorPickerWidget.kt new file mode 100644 index 000000000..9f628f540 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/more/settings/widget/ThemeColorPickerWidget.kt @@ -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)) + }, + ) + } + } + }, + ) +} diff --git a/app/src/main/java/eu/kanade/presentation/theme/TachiyomiTheme.kt b/app/src/main/java/eu/kanade/presentation/theme/TachiyomiTheme.kt index a57495679..70a26d1ce 100644 --- a/app/src/main/java/eu/kanade/presentation/theme/TachiyomiTheme.kt +++ b/app/src/main/java/eu/kanade/presentation/theme/TachiyomiTheme.kt @@ -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() + val colorScheme = when (appTheme) { + AppTheme.MONET -> { + MonetColorScheme(LocalContext.current) + } + // KMK --> + AppTheme.CUSTOM -> { + CustomColorScheme(uiPreferences) + } + // KMK <-- + else -> { + colorSchemes.getOrDefault(appTheme, TachiyomiColorScheme) + } } return colorScheme.getColorScheme( isSystemInDarkTheme(), diff --git a/app/src/main/java/eu/kanade/presentation/theme/colorscheme/CustomColorScheme.kt b/app/src/main/java/eu/kanade/presentation/theme/colorscheme/CustomColorScheme.kt new file mode 100644 index 000000000..8a1c32616 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/theme/colorscheme/CustomColorScheme.kt @@ -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(), + ) + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/delegate/ThemingDelegate.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/delegate/ThemingDelegate.kt index 723161115..c9f058006 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/delegate/ThemingDelegate.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/delegate/ThemingDelegate.kt @@ -30,6 +30,7 @@ class ThemingDelegateImpl : ThemingDelegate { private val themeResources: Map = 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, diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 7edad80c0..1638f0ac0 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -96,6 +96,10 @@