@@ -7,6 +7,15 @@ import eu.kanade.domain.ui.model.TabletUiMode
77import eu.kanade.domain.ui.model.ThemeMode
88import eu.kanade.tachiyomi.util.system.DeviceUtil
99import eu.kanade.tachiyomi.util.system.isDynamicColorAvailable
10+ import kotlinx.coroutines.CoroutineScope
11+ import kotlinx.coroutines.flow.Flow
12+ import kotlinx.coroutines.flow.SharingStarted
13+ import kotlinx.coroutines.flow.StateFlow
14+ import kotlinx.coroutines.flow.distinctUntilChanged
15+ import kotlinx.coroutines.flow.map
16+ import kotlinx.coroutines.flow.merge
17+ import kotlinx.coroutines.flow.stateIn
18+ import tachiyomi.core.common.preference.Preference
1019import tachiyomi.core.common.preference.PreferenceStore
1120import tachiyomi.core.common.preference.getEnum
1221import java.time.format.DateTimeFormatter
@@ -19,9 +28,12 @@ class UiPreferences(
1928
2029 companion object {
2130 const val CUSTOM_THEME_ACCENT_SEED_UNSET = Int .MIN_VALUE
31+ private const val PREF_CUSTOM_THEME_ACCENT_SEED = " pref_custom_theme_accent_seed"
32+ private const val PREF_CUSTOM_THEME_ACCENT_SCHEMA = " pref_custom_theme_accent_schema"
2233 private const val PREF_CUSTOM_THEME_RECENT_ACCENT_SEEDS = " pref_custom_theme_recent_accent_seeds"
2334 private const val MAX_RECENT_CUSTOM_THEME_ACCENT_SEEDS = 5
2435 private const val OPAQUE_ALPHA_MASK = 0xFF000000 .toInt()
36+ private const val CUSTOM_THEME_ACCENT_SCHEMA_VERSION = 1
2537 fun dateFormat (format : String ): DateTimeFormatter = when (format) {
2638 " " -> DateTimeFormatter .ofLocalizedDate(FormatStyle .SHORT )
2739 else -> DateTimeFormatter .ofPattern(format, Locale .getDefault())
@@ -37,6 +49,14 @@ class UiPreferences(
3749 .toList()
3850 }
3951
52+ private fun normalizeAccentSeed (seed : Int ): Int {
53+ return if (seed == CUSTOM_THEME_ACCENT_SEED_UNSET ) {
54+ CUSTOM_THEME_ACCENT_SEED_UNSET
55+ } else {
56+ seed or OPAQUE_ALPHA_MASK
57+ }
58+ }
59+
4060 internal fun upsertRecentAccentSeed (
4161 existing : List <Int >,
4262 appliedSeed : Int ,
@@ -47,6 +67,8 @@ class UiPreferences(
4767 }
4868 }
4969
70+ private val customThemeAccentSeedPreference by lazy { CustomThemeAccentSeedPreference () }
71+
5072 fun themeMode () = preferenceStore.getEnum(" pref_theme_mode_key" , ThemeMode .SYSTEM )
5173
5274 fun appTheme () = preferenceStore.getEnum(
@@ -60,10 +82,7 @@ class UiPreferences(
6082
6183 fun themeDarkAmoled () = preferenceStore.getBoolean(" pref_theme_dark_amoled_key" , false )
6284 fun dynamicEntryCoverTheming () = preferenceStore.getBoolean(" pref_dynamic_entry_cover_theming" , false )
63- fun customThemeAccentSeed () = preferenceStore.getInt(
64- " pref_custom_theme_accent_seed" ,
65- CUSTOM_THEME_ACCENT_SEED_UNSET ,
66- )
85+ fun customThemeAccentSeed (): Preference <Int > = customThemeAccentSeedPreference
6786
6887 fun customThemeRecentAccentSeeds () = preferenceStore.getObject(
6988 key = PREF_CUSTOM_THEME_RECENT_ACCENT_SEEDS ,
@@ -109,4 +128,158 @@ class UiPreferences(
109128 fun startScreen () = preferenceStore.getEnum(" start_screen" , StartScreen .ANIME )
110129
111130 fun navStyle () = preferenceStore.getEnum(" bottom_rail_nav_style" , NavStyle .MOVE_HISTORY_TO_MORE )
131+
132+ private enum class AccentSchemaState {
133+ VALID ,
134+ UNKNOWN_VERSION ,
135+ MALFORMED ,
136+ }
137+
138+ private data class CustomAccentSchemaPayload (
139+ val state : AccentSchemaState ,
140+ val seed : Int? ,
141+ ) {
142+ companion object {
143+ fun fromPersisted (raw : String ): CustomAccentSchemaPayload {
144+ val trimmed = raw.trim()
145+ val separator = trimmed.indexOf(' :' )
146+ if (! trimmed.startsWith(" v" ) || separator <= 1 || separator >= trimmed.lastIndex) {
147+ return CustomAccentSchemaPayload (
148+ state = AccentSchemaState .MALFORMED ,
149+ seed = null ,
150+ )
151+ }
152+
153+ val version = trimmed.substring(1 , separator).toIntOrNull()
154+ ? : return CustomAccentSchemaPayload (
155+ state = AccentSchemaState .MALFORMED ,
156+ seed = null ,
157+ )
158+ val token = trimmed.substring(separator + 1 ).trim()
159+ if (version != CUSTOM_THEME_ACCENT_SCHEMA_VERSION ) {
160+ return CustomAccentSchemaPayload (
161+ state = AccentSchemaState .UNKNOWN_VERSION ,
162+ seed = null ,
163+ )
164+ }
165+
166+ if (token.equals(" unset" , ignoreCase = true )) {
167+ return CustomAccentSchemaPayload (
168+ state = AccentSchemaState .VALID ,
169+ seed = null ,
170+ )
171+ }
172+
173+ val parsed = token.toUIntOrNull(16 )?.toInt()
174+ ? : return CustomAccentSchemaPayload (
175+ state = AccentSchemaState .MALFORMED ,
176+ seed = null ,
177+ )
178+ val normalized = normalizeAccentSeed(parsed)
179+ return CustomAccentSchemaPayload (
180+ state = AccentSchemaState .VALID ,
181+ seed = normalized.takeUnless { it == CUSTOM_THEME_ACCENT_SEED_UNSET },
182+ )
183+ }
184+
185+ fun forSeed (seed : Int ): CustomAccentSchemaPayload {
186+ val normalized = normalizeAccentSeed(seed)
187+ return CustomAccentSchemaPayload (
188+ state = AccentSchemaState .VALID ,
189+ seed = normalized.takeUnless { it == CUSTOM_THEME_ACCENT_SEED_UNSET },
190+ )
191+ }
192+ }
193+ }
194+
195+ private inner class CustomThemeAccentSeedPreference : Preference <Int > {
196+ private val legacyPreference = preferenceStore.getInt(
197+ PREF_CUSTOM_THEME_ACCENT_SEED ,
198+ CUSTOM_THEME_ACCENT_SEED_UNSET ,
199+ )
200+ private val schemaPreference = preferenceStore.getObject(
201+ key = PREF_CUSTOM_THEME_ACCENT_SCHEMA ,
202+ defaultValue = CustomAccentSchemaPayload (
203+ state = AccentSchemaState .MALFORMED ,
204+ seed = null ,
205+ ),
206+ serializer = { payload ->
207+ val version = if (payload.state == AccentSchemaState .VALID ) {
208+ CUSTOM_THEME_ACCENT_SCHEMA_VERSION
209+ } else {
210+ CUSTOM_THEME_ACCENT_SCHEMA_VERSION
211+ }
212+ val seedToken = payload.seed
213+ ?.let { normalized ->
214+ normalizeAccentSeed(normalized).toUInt().toString(16 ).padStart(8 , ' 0' )
215+ }
216+ ? : " unset"
217+ " v$version :$seedToken "
218+ },
219+ deserializer = { encoded ->
220+ CustomAccentSchemaPayload .fromPersisted(encoded)
221+ },
222+ )
223+ private val derivedChanges = merge(
224+ legacyPreference.changes().map { Unit },
225+ schemaPreference.changes().map { Unit },
226+ )
227+ .map { resolveCurrentSeed(migrate = false ) }
228+ .distinctUntilChanged()
229+
230+ override fun key (): String = PREF_CUSTOM_THEME_ACCENT_SEED
231+
232+ override fun get (): Int {
233+ return resolveCurrentSeed(migrate = true )
234+ }
235+
236+ override fun set (value : Int ) {
237+ val normalized = normalizeAccentSeed(value)
238+ legacyPreference.set(normalized)
239+ schemaPreference.set(CustomAccentSchemaPayload .forSeed(normalized))
240+ }
241+
242+ override fun isSet (): Boolean = legacyPreference.isSet() || schemaPreference.isSet()
243+
244+ override fun delete () {
245+ legacyPreference.delete()
246+ schemaPreference.delete()
247+ }
248+
249+ override fun defaultValue (): Int = CUSTOM_THEME_ACCENT_SEED_UNSET
250+
251+ override fun changes (): Flow <Int > = derivedChanges
252+
253+ override fun stateIn (scope : CoroutineScope ): StateFlow <Int > {
254+ return changes().stateIn(
255+ scope = scope,
256+ started = SharingStarted .Eagerly ,
257+ initialValue = get(),
258+ )
259+ }
260+
261+ private fun resolveCurrentSeed (migrate : Boolean ): Int {
262+ val legacySeed = normalizeAccentSeed(legacyPreference.get())
263+ val schemaSet = schemaPreference.isSet()
264+ val schemaPayload = schemaPreference.get()
265+ return when (schemaPayload.state) {
266+ AccentSchemaState .VALID -> {
267+ val resolved = schemaPayload.seed ? : CUSTOM_THEME_ACCENT_SEED_UNSET
268+ if (migrate && legacySeed != resolved) {
269+ legacyPreference.set(resolved)
270+ }
271+ resolved
272+ }
273+ AccentSchemaState .UNKNOWN_VERSION -> legacySeed
274+ AccentSchemaState .MALFORMED -> {
275+ if (migrate && schemaSet) {
276+ schemaPreference.set(CustomAccentSchemaPayload .forSeed(legacySeed))
277+ } else if (migrate && legacySeed != CUSTOM_THEME_ACCENT_SEED_UNSET ) {
278+ schemaPreference.set(CustomAccentSchemaPayload .forSeed(legacySeed))
279+ }
280+ legacySeed
281+ }
282+ }
283+ }
284+ }
112285}
0 commit comments