Skip to content
19 changes: 16 additions & 3 deletions app/src/main/kotlin/moe/rukamori/archivetune/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -174,15 +174,28 @@ class App : Application(), SingletonImageLoader.Factory {
}

// Apply random theme on startup if enabled
if (prefs[RandomThemeOnStartupKey] == true) {
val randomPalette = ThemePalettes.generateRandomPalette()
if (prefs[RandomThemeOnStartupKey] ?: (Build.VERSION.SDK_INT < Build.VERSION_CODES.S)) {
val styleName = prefs[RandomThemeStyleKey] ?: RandomThemeStyle.TONAL_SPOT.name
val style = try { RandomThemeStyle.valueOf(styleName) } catch (_: Exception) { RandomThemeStyle.TONAL_SPOT }
val randomPalette = when (style) {
RandomThemeStyle.TONAL_SPOT -> ThemePalettes.generateTonalSpotPalette()
RandomThemeStyle.NEUTRAL -> ThemePalettes.generateNeutralPalette()
RandomThemeStyle.VIBRANT -> ThemePalettes.generateVibrantPalette()
RandomThemeStyle.EXPRESSIVE -> ThemePalettes.generateExpressivePalette()
RandomThemeStyle.RAINBOW -> ThemePalettes.generateRainbowPalette()
RandomThemeStyle.FRUIT_SALAD -> ThemePalettes.generateFruitSaladPalette()
RandomThemeStyle.MONOCHROME -> ThemePalettes.generateMonochromePalette()
RandomThemeStyle.FIDELITY -> ThemePalettes.generateFidelityPalette()
RandomThemeStyle.CONTENT -> ThemePalettes.generateContentPalette()
RandomThemeStyle.CHAOS -> ThemePalettes.generateChaosPalette()
}
val seedPalette = ThemeSeedPalette(
primary = randomPalette.primary,
secondary = randomPalette.secondary,
tertiary = randomPalette.tertiary,
neutral = randomPalette.neutral
)
val encodedPalette = ThemeSeedPaletteCodec.encodeForPreference(seedPalette, "Random")
val encodedPalette = ThemeSeedPaletteCodec.encodeForPreference(seedPalette, style.name)
dataStore.edit { settings ->
settings[CustomThemeColorKey] = encodedPalette
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,21 @@ import java.time.ZoneOffset
val DynamicThemeKey = booleanPreferencesKey("dynamicTheme")
val CustomThemeColorKey = stringPreferencesKey("customThemeColor")
val RandomThemeOnStartupKey = booleanPreferencesKey("randomThemeOnStartup")

enum class RandomThemeStyle {
TONAL_SPOT,
NEUTRAL,
VIBRANT,
EXPRESSIVE,
RAINBOW,
FRUIT_SALAD,
MONOCHROME,
FIDELITY,
CONTENT,
CHAOS,
}

val RandomThemeStyleKey = stringPreferencesKey("randomThemeStyle")
val DarkModeKey = stringPreferencesKey("darkMode")
val PureBlackKey = booleanPreferencesKey("pureBlack")
val DisableAnimationsKey = booleanPreferencesKey("disableAnimations")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,10 @@ import moe.rukamori.archivetune.constants.PlayerBackgroundStyleKey
import moe.rukamori.archivetune.constants.MiniPlayerBackgroundStyle
import moe.rukamori.archivetune.constants.MiniPlayerBackgroundStyleKey
import moe.rukamori.archivetune.constants.PureBlackKey
import moe.rukamori.archivetune.constants.CustomThemeColorKey
import moe.rukamori.archivetune.constants.RandomThemeOnStartupKey
import moe.rukamori.archivetune.constants.RandomThemeStyle
import moe.rukamori.archivetune.constants.RandomThemeStyleKey
import moe.rukamori.archivetune.constants.PlayerButtonsStyle
import moe.rukamori.archivetune.constants.PlayerButtonsStyleKey
import moe.rukamori.archivetune.constants.SliderStyle
Expand Down Expand Up @@ -122,7 +125,11 @@ import moe.rukamori.archivetune.ui.component.SwitchPreference
import moe.rukamori.archivetune.ui.component.ThumbnailCornerRadiusSelectorButton
import moe.rukamori.archivetune.ui.player.StyledPlaybackSlider
import moe.rukamori.archivetune.ui.theme.CustomFontLoader
import moe.rukamori.archivetune.ui.theme.ThemeSeedPalette
import moe.rukamori.archivetune.ui.theme.ThemeSeedPaletteCodec
import moe.rukamori.archivetune.ui.utils.backToMain
import moe.rukamori.archivetune.utils.PreferenceStore
import moe.rukamori.archivetune.utils.dataStore
import moe.rukamori.archivetune.utils.isLowRamDevice
import moe.rukamori.archivetune.utils.rememberEnumPreference
import moe.rukamori.archivetune.utils.rememberPreference
Expand All @@ -142,7 +149,11 @@ fun AppearanceSettings(
)
val (randomThemeOnStartup, onRandomThemeOnStartupChange) = rememberPreference(
RandomThemeOnStartupKey,
defaultValue = false
defaultValue = Build.VERSION.SDK_INT < Build.VERSION_CODES.S
)
val (randomThemeStyle, onRandomThemeStyleChange) = rememberEnumPreference(
RandomThemeStyleKey,
defaultValue = RandomThemeStyle.TONAL_SPOT
)
val (darkMode, onDarkModeChange) = rememberEnumPreference(
DarkModeKey,
Expand Down Expand Up @@ -399,6 +410,64 @@ fun AppearanceSettings(
)
}

item(visible = randomThemeOnStartup && (!dynamicTheme || Build.VERSION.SDK_INT < Build.VERSION_CODES.S)) {
EnumListPreference(
title = { Text(stringResource(R.string.random_theme_style)) },
description = stringResource(R.string.random_theme_style_desc),
icon = { Icon(painterResource(R.drawable.shuffle), null) },
selectedValue = randomThemeStyle,
valueText = {
stringResource(
when (it) {
RandomThemeStyle.TONAL_SPOT -> R.string.random_style_tonal_spot
RandomThemeStyle.NEUTRAL -> R.string.random_style_neutral
RandomThemeStyle.VIBRANT -> R.string.random_style_vibrant
RandomThemeStyle.EXPRESSIVE -> R.string.random_style_expressive
RandomThemeStyle.RAINBOW -> R.string.random_style_rainbow
RandomThemeStyle.FRUIT_SALAD -> R.string.random_style_fruit_salad
RandomThemeStyle.MONOCHROME -> R.string.random_style_monochrome
RandomThemeStyle.FIDELITY -> R.string.random_style_fidelity
RandomThemeStyle.CONTENT -> R.string.random_style_content
RandomThemeStyle.CHAOS -> R.string.random_style_chaos
}
)
},
onValueSelected = onRandomThemeStyleChange,
)
}

item(visible = randomThemeOnStartup && (!dynamicTheme || Build.VERSION.SDK_INT < Build.VERSION_CODES.S)) {
PreferenceEntry(
title = { Text(stringResource(R.string.reload_random_theme)) },
description = stringResource(R.string.reload_random_theme_desc),
icon = { Icon(painterResource(R.drawable.casino), null) },
onClick = {
val palette = when (randomThemeStyle) {
RandomThemeStyle.TONAL_SPOT -> ThemePalettes.generateTonalSpotPalette()
RandomThemeStyle.NEUTRAL -> ThemePalettes.generateNeutralPalette()
RandomThemeStyle.VIBRANT -> ThemePalettes.generateVibrantPalette()
RandomThemeStyle.EXPRESSIVE -> ThemePalettes.generateExpressivePalette()
RandomThemeStyle.RAINBOW -> ThemePalettes.generateRainbowPalette()
RandomThemeStyle.FRUIT_SALAD -> ThemePalettes.generateFruitSaladPalette()
RandomThemeStyle.MONOCHROME -> ThemePalettes.generateMonochromePalette()
RandomThemeStyle.FIDELITY -> ThemePalettes.generateFidelityPalette()
RandomThemeStyle.CONTENT -> ThemePalettes.generateContentPalette()
RandomThemeStyle.CHAOS -> ThemePalettes.generateChaosPalette()
}
val seedPalette = ThemeSeedPalette(
primary = palette.primary,
secondary = palette.secondary,
tertiary = palette.tertiary,
neutral = palette.neutral,
)
val encoded = ThemeSeedPaletteCodec.encodeForPreference(seedPalette, randomThemeStyle.name)
PreferenceStore.launchEdit(context.dataStore) {
this[CustomThemeColorKey] = encoded
}
},
)
}

item(visible = !dynamicTheme || Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
PreferenceEntry(
title = { Text(stringResource(R.string.color_palette)) },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -773,25 +773,138 @@ object ThemePalettes {

fun getRandomPalette(): ThemePalette = allPalettes.random()

fun generateRandomPalette(): ThemePalette {
fun generateTonalSpotPalette(): ThemePalette {
val random = java.util.Random()
val primaryHue = random.nextFloat() * 360f
val primarySaturation = 0.5f + random.nextFloat() * 0.4f
val primaryLightness = 0.4f + random.nextFloat() * 0.25f
val primary = hctToColor(primaryHue, primarySaturation, primaryLightness)
val secondaryHue = (primaryHue + 30f + random.nextFloat() * 60f) % 360f
val secondary = hctToColor(secondaryHue, primarySaturation * 0.9f, primaryLightness * 1.1f)
val tertiaryHue = (primaryHue - 30f - random.nextFloat() * 60f + 360f) % 360f
val tertiary = hctToColor(tertiaryHue, primarySaturation * 0.8f, primaryLightness * 0.95f)
val neutralHue = (primaryHue + random.nextFloat() * 20f - 10f) % 360f
val neutral = hctToColor(neutralHue, 0.1f, primaryLightness * 0.8f)
val primary = hctToColor(
random.nextFloat() * 360f,
0.5f + random.nextFloat() * 0.4f,
0.4f + random.nextFloat() * 0.25f,
)
return ThemePalette(
id = "tonalspot_" + System.currentTimeMillis(),
nameResId = R.string.palette_custom,
primary = primary, secondary = primary, tertiary = primary, neutral = primary,
)
}

fun generateNeutralPalette(): ThemePalette {
val random = java.util.Random()
val primary = hctToColor(
random.nextFloat() * 360f,
0.1f + random.nextFloat() * 0.2f,
0.4f + random.nextFloat() * 0.25f,
)
return ThemePalette(
id = "neutral_" + System.currentTimeMillis(),
nameResId = R.string.palette_custom,
primary = primary, secondary = primary, tertiary = primary, neutral = primary,
)
}

fun generateVibrantPalette(): ThemePalette {
val random = java.util.Random()
val primary = hctToColor(
random.nextFloat() * 360f,
0.8f + random.nextFloat() * 0.2f,
0.45f + random.nextFloat() * 0.15f,
)
return ThemePalette(
id = "vibrant_" + System.currentTimeMillis(),
nameResId = R.string.palette_custom,
primary = primary, secondary = primary, tertiary = primary, neutral = primary,
)
}

fun generateExpressivePalette(): ThemePalette {
val random = java.util.Random()
val primary = hctToColor(
random.nextFloat() * 360f,
0.6f + random.nextFloat() * 0.35f,
0.35f + random.nextFloat() * 0.35f,
)
return ThemePalette(
id = "expressive_" + System.currentTimeMillis(),
nameResId = R.string.palette_custom,
primary = primary, secondary = primary, tertiary = primary, neutral = primary,
)
}

fun generateRainbowPalette(): ThemePalette {
val random = java.util.Random()
val primary = hctToColor(
random.nextFloat() * 360f,
0.5f + random.nextFloat() * 0.4f,
0.4f + random.nextFloat() * 0.25f,
)
return ThemePalette(
id = "rainbow_" + System.currentTimeMillis(),
nameResId = R.string.palette_custom,
primary = primary, secondary = primary, tertiary = primary, neutral = primary,
)
}

fun generateFruitSaladPalette(): ThemePalette {
val random = java.util.Random()
val primary = hctToColor(
random.nextFloat() * 360f,
0.6f + random.nextFloat() * 0.3f,
0.4f + random.nextFloat() * 0.3f,
)
return ThemePalette(
id = "fruitsalad_" + System.currentTimeMillis(),
nameResId = R.string.palette_custom,
primary = primary, secondary = primary, tertiary = primary, neutral = primary,
)
}

fun generateMonochromePalette(): ThemePalette {
val random = java.util.Random()
val gray = hctToColor(0f, 0f, random.nextFloat())
return ThemePalette(
id = "monochrome_" + System.currentTimeMillis(),
nameResId = R.string.palette_custom,
primary = gray, secondary = gray, tertiary = gray, neutral = gray,
)
}

fun generateFidelityPalette(): ThemePalette {
val random = java.util.Random()
val primary = hctToColor(
random.nextFloat() * 360f,
0.5f + random.nextFloat() * 0.4f,
0.4f + random.nextFloat() * 0.25f,
)
return ThemePalette(
id = "fidelity_" + System.currentTimeMillis(),
nameResId = R.string.palette_custom,
primary = primary, secondary = primary, tertiary = primary, neutral = primary,
)
}

fun generateContentPalette(): ThemePalette {
val random = java.util.Random()
val primary = hctToColor(
random.nextFloat() * 360f,
0.5f + random.nextFloat() * 0.35f,
0.4f + random.nextFloat() * 0.25f,
)
return ThemePalette(
id = "content_" + System.currentTimeMillis(),
nameResId = R.string.palette_custom,
primary = primary, secondary = primary, tertiary = primary, neutral = primary,
)
}

fun generateChaosPalette(): ThemePalette {
val random = java.util.Random()
val primary = hctToColor(random.nextFloat() * 360f, random.nextFloat(), random.nextFloat())
val secondary = hctToColor(random.nextFloat() * 360f, random.nextFloat(), random.nextFloat())
val tertiary = hctToColor(random.nextFloat() * 360f, random.nextFloat(), random.nextFloat())
val neutral = hctToColor(random.nextFloat() * 360f, random.nextFloat(), random.nextFloat())
return ThemePalette(
id = "random_" + System.currentTimeMillis(),
id = "chaos_" + System.currentTimeMillis(),
nameResId = R.string.palette_custom,
primary = primary,
secondary = secondary,
tertiary = tertiary,
neutral = neutral,
primary = primary, secondary = secondary, tertiary = tertiary, neutral = neutral,
)
}

Expand Down
32 changes: 29 additions & 3 deletions app/src/main/kotlin/moe/rukamori/archivetune/ui/theme/Theme.kt
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import moe.rukamori.archivetune.constants.AppFontPreference
import moe.rukamori.archivetune.constants.RandomThemeStyle
import moe.rukamori.archivetune.constants.RandomThemeStyleKey
import moe.rukamori.archivetune.utils.rememberEnumPreference
import kotlin.math.abs
import kotlin.math.min

Expand Down Expand Up @@ -113,22 +116,43 @@ fun ArchiveTuneTheme(
}
}
val expressiveMotionScheme = remember { MotionScheme.expressive() }
val paletteStyle = remember(themeColor, seedPalette) {
val (randomThemeStyle, _) = rememberEnumPreference(
RandomThemeStyleKey,
defaultValue = RandomThemeStyle.TONAL_SPOT,
)

val paletteStyle = remember(themeColor, seedPalette, randomThemeStyle) {
paletteStyleFor(seedPalette?.primary ?: themeColor)
}

val forcedPaletteStyle = remember(randomThemeStyle) {
when (randomThemeStyle) {
RandomThemeStyle.TONAL_SPOT -> PaletteStyle.TonalSpot
RandomThemeStyle.NEUTRAL -> PaletteStyle.Neutral
RandomThemeStyle.VIBRANT -> PaletteStyle.Vibrant
RandomThemeStyle.EXPRESSIVE -> PaletteStyle.Expressive
RandomThemeStyle.RAINBOW -> PaletteStyle.Rainbow
RandomThemeStyle.FRUIT_SALAD -> PaletteStyle.FruitSalad
RandomThemeStyle.MONOCHROME -> PaletteStyle.Monochrome
RandomThemeStyle.FIDELITY -> PaletteStyle.Fidelity
RandomThemeStyle.CONTENT -> PaletteStyle.Content
RandomThemeStyle.CHAOS -> null
}
}

val appColorScheme =
remember(seedPalette, themeColor, darkTheme) {
remember(seedPalette, themeColor, darkTheme, forcedPaletteStyle) {
if (seedPalette != null) {
exactPaletteColorScheme(
palette = seedPalette,
isDark = darkTheme,
style = forcedPaletteStyle,
)
} else {
materialKolorDynamicColorScheme(
keyColor = themeColor,
isDark = darkTheme,
style = paletteStyle,
style = forcedPaletteStyle ?: paletteStyle,
)
}
}
Expand Down Expand Up @@ -216,12 +240,14 @@ private fun animateColorScheme(targetColorScheme: ColorScheme): ColorScheme {
private fun exactPaletteColorScheme(
palette: ThemeSeedPalette,
isDark: Boolean,
style: PaletteStyle? = null,
): ColorScheme = mergedSeedColorScheme(
primarySeed = palette.primary,
secondarySeed = palette.secondary,
tertiarySeed = palette.tertiary,
neutralSeed = palette.neutral,
isDark = isDark,
style = style ?: paletteStyleFor(palette.primary),
)

private fun materialKolorDynamicColorScheme(
Expand Down
Loading