SettingsStore.kt
Each line, with the release that last changed it.
| 0.5.0 | 1 | package dev.stave.data |
| 2 | ||
| 3 | import android.content.Context | |
| 4 | import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler | |
| 5 | import androidx.datastore.preferences.core.booleanPreferencesKey | |
| 6 | import androidx.datastore.preferences.core.edit | |
| 7 | import androidx.datastore.preferences.core.emptyPreferences | |
| 8 | import androidx.datastore.preferences.core.floatPreferencesKey | |
| 9 | import androidx.datastore.preferences.core.intPreferencesKey | |
| 10 | import androidx.datastore.preferences.core.stringPreferencesKey | |
| 11 | import androidx.datastore.preferences.core.stringSetPreferencesKey | |
| 12 | import androidx.datastore.preferences.preferencesDataStore | |
| 13 | import dagger.hilt.android.qualifiers.ApplicationContext | |
| 14 | import dev.stave.domain.model.MetadataField | |
| 15 | import kotlinx.coroutines.flow.Flow | |
| 16 | import kotlinx.coroutines.flow.map | |
| 17 | import javax.inject.Inject | |
| 18 | import javax.inject.Singleton | |
| 19 | ||
| 20 | /** Replay Gain operating mode. */ | |
| 21 | enum class ReplayGainMode { OFF, TRACK, ALBUM } | |
| 22 | ||
| 23 | /** Preset color choices for the current-lyric highlight. AMBER matches the Earhorn | |
| 24 | * accent; WHITE is high-contrast/no-tint; SAGE is the secondary accent for folks | |
| 25 | * who want a less-warm look. Adding more is cheap — enum + a one-line entry in the | |
| 26 | * settings radio row. */ | |
| 27 | enum class LyricsHighlightColor { AMBER, WHITE, SAGE } | |
| 28 | ||
| 29 | /** Legacy preset — retained so existing settings blobs still deserialize. New code | |
| 30 | * should read [SettingsStore.lyricsHighlightSize] / Bold / Color directly. */ | |
| 31 | @Deprecated("Use the three independent settings (size, bold, color) instead.") | |
| 32 | enum class LyricsHighlightStyle { SIZE_ONLY, COLOR_ONLY, SIZE_AND_COLOR, BOLD } | |
| 33 | ||
| 34 | // Maintainer, 2026-10-29: a corrupt settings file crashed the app at launch; | |
| 35 | // replace it with empty (defaults) instead. | |
| 36 | private val Context.dataStore by preferencesDataStore( | |
| 37 | name = "stave_settings", | |
| 38 | corruptionHandler = ReplaceFileCorruptionHandler { emptyPreferences() }, | |
| 39 | ) | |
| 40 | ||
| 41 | @Singleton | |
| 42 | class SettingsStore @Inject constructor( | |
| 43 | @ApplicationContext private val context: Context, | |
| 44 | ) { | |
| 45 | val enabledFields: Flow<Set<MetadataField>> = context.dataStore.data.map { prefs -> | |
| 46 | val stored = prefs[KEY_FIELDS] | |
| 47 | if (stored == null) MetadataField.defaults | |
| 48 | else stored.mapNotNull(MetadataField::fromKey).toSet() | |
| 49 | } | |
| 50 | ||
| 51 | /** UDN of the last server the user selected. Empty string = none chosen. Auto-navigate | |
| 52 | * logic in EarhornNavHost reads this against the live discovery list. Cleared when the | |
| 53 | * user presses Rescan. */ | |
| 54 | val preferredServerUdn: Flow<String?> = context.dataStore.data.map { prefs -> | |
| 55 | prefs[KEY_PREFERRED_SERVER]?.takeIf { it.isNotBlank() } | |
| 56 | } | |
| 57 | ||
| 58 | /** Whether long titles/artists should marquee-scroll when ellipsized. Default off — | |
| 59 | * marquee is stylish on the Now Playing screen but visually noisy in dense list | |
| 60 | * views, so the user opts in. */ | |
| 61 | val marqueeEnabled: Flow<Boolean> = context.dataStore.data.map { prefs -> | |
| 62 | prefs[KEY_MARQUEE] ?: false | |
| 63 | } | |
| 64 | ||
| 65 | /** Whether Fraunces (serif) renders app-wide. Default true — Fraunces carries | |
| 66 | * the brand identity and is the design intent. Disabling falls through to Inter | |
| 67 | * (sans-serif) for everything that would otherwise render in Fraunces; users who | |
| 68 | * find serif type harder to read can opt out without losing the rest of the | |
| 69 | * type-system structure (sizes, weights, letter-spacing all stay the same). */ | |
| 70 | val useFraunces: Flow<Boolean> = context.dataStore.data.map { prefs -> | |
| 71 | prefs[KEY_USE_FRAUNCES] ?: true | |
| 72 | } | |
| 73 | ||
| 74 | /** ReplayGain mode. OFF = no volume correction; TRACK = per-track normalization; | |
| 75 | * ALBUM = per-album normalization (preserves the album's internal dynamics). */ | |
| 76 | val replayGainMode: Flow<ReplayGainMode> = context.dataStore.data.map { prefs -> | |
| 77 | val ordinal = prefs[KEY_RG_MODE] ?: ReplayGainMode.OFF.ordinal | |
| 78 | ReplayGainMode.entries.getOrElse(ordinal) { ReplayGainMode.OFF } | |
| 79 | } | |
| 80 | ||
| 81 | /** ReplayGain pre-amp in dB. Added to every track's RG-corrected volume. Default +3dB | |
| 82 | * — conventional because RG's -6dBFS target leaves headroom that most listeners want | |
| 83 | * to reclaim. Range is clamped to [-15, +15] in setter. */ | |
| 84 | val replayGainPreAmpDb: Flow<Float> = context.dataStore.data.map { prefs -> | |
| 85 | prefs[KEY_RG_PREAMP] ?: 3f | |
| 86 | } | |
| 87 | ||
| 88 | /** Genre names to exclude from random-track and random-album picks. Case-insensitive | |
| 89 | * comparison at filter time. Users typically add "Podcast", "Comedy", "Audiobook" | |
| 90 | * here so their party-shuffle doesn't pull spoken-word items. */ | |
| 91 | val randomExcludedGenres: Flow<Set<String>> = context.dataStore.data.map { prefs -> | |
| 92 | prefs[KEY_RANDOM_EXCLUDED_GENRES] ?: emptySet() | |
| 93 | } | |
| 94 | ||
| 95 | /** Size multiplier applied to the current lyric line. 1.0 = same as others; | |
| 96 | * 1.5 = 50% larger; 2.0 = double. Clamped [1.0, 2.5] by the setter. Default 1.5 | |
| 97 | * matches the prior SIZE_AND_COLOR preset's visual feel. */ | |
| 98 | val lyricsHighlightSize: Flow<Float> = context.dataStore.data.map { prefs -> | |
| 99 | prefs[KEY_LYRICS_HIGHLIGHT_SIZE] ?: 1.5f | |
| 100 | } | |
| 101 | ||
| 102 | /** Whether the current lyric line renders bold. */ | |
| 103 | val lyricsHighlightBold: Flow<Boolean> = context.dataStore.data.map { prefs -> | |
| 104 | prefs[KEY_LYRICS_HIGHLIGHT_BOLD] ?: false | |
| 105 | } | |
| 106 | ||
| 107 | /** Accent color for the current lyric line. */ | |
| 108 | val lyricsHighlightColor: Flow<LyricsHighlightColor> = context.dataStore.data.map { prefs -> | |
| 109 | val ordinal = prefs[KEY_LYRICS_HIGHLIGHT_COLOR] ?: LyricsHighlightColor.AMBER.ordinal | |
| 110 | LyricsHighlightColor.entries.getOrElse(ordinal) { LyricsHighlightColor.AMBER } | |
| 111 | } | |
| 112 | ||
| 113 | /** Whether the lyrics screen shows a metadata chip grid (artist, album, format | |
| 114 | * etc.) in the upper-right corner. Default on; the user can toggle it off if | |
| 115 | * they prefer a clean reading view. */ | |
| 116 | val lyricsMetadataEnabled: Flow<Boolean> = context.dataStore.data.map { prefs -> | |
| 117 | prefs[KEY_LYRICS_METADATA] ?: true | |
| 118 | } | |
| 119 | ||
| 120 | /** Master privacy switch. When FALSE, Earhorn will not contact any third-party service | |
| 121 | * (lyrics lookup etc.). The lyrics screen shows a "disabled in privacy settings" | |
| 122 | * state instead of pinging LRCLIB. Default TRUE — opt-out, matching typical app | |
| 123 | * defaults; users who care about it can toggle off. */ | |
| 124 | val allowExternalCalls: Flow<Boolean> = context.dataStore.data.map { prefs -> | |
| 125 | prefs[KEY_ALLOW_EXTERNAL_CALLS] ?: true | |
| 126 | } | |
| 127 | ||
| 128 | // ── Renderer mesh ──────────────────────────────────────────────── | |
| 129 | // Settings backing the UPnP MediaRenderer broadcast feature and the | |
| 130 | // user's currently-selected playback target. See [EarhornRendererService] | |
| 131 | // for the broadcast side, [RendererCoordinator] for the routing side. | |
| 132 | ||
| 133 | /** Master switch for advertising this device as a UPnP MediaRenderer on the LAN. | |
| 134 | * Default FALSE — opt-in, since broadcasting holds the multicast lock for | |
| 135 | * longer and answers SSDP M-SEARCH probes from anything on the network. | |
| 136 | * When TRUE, [EarhornRendererService] generates a UDN on first enable | |
| 137 | * (stored in [rendererUdn]) and registers a LocalDevice via the holder. */ | |
| 138 | val rendererBroadcastEnabled: Flow<Boolean> = context.dataStore.data.map { prefs -> | |
| 139 | prefs[KEY_RENDERER_BROADCAST_ENABLED] ?: false | |
| 140 | } | |
| 141 | ||
| 142 | /** User-supplied friendly name for the broadcast renderer. Null = autocompute | |
| 143 | * ("Earhorn on $deviceName"). Empty strings normalize to null on write so the | |
| 144 | * picker downstream doesn't have to special-case blank labels. */ | |
| 145 | val rendererFriendlyName: Flow<String?> = context.dataStore.data.map { prefs -> | |
| 146 | prefs[KEY_RENDERER_FRIENDLY_NAME]?.takeIf { it.isNotBlank() } | |
| 147 | } | |
| 148 | ||
| 149 | /** Stable UDN this device advertises when broadcasting. Generated once on first | |
| 150 | * broadcast-enable ([EarhornRendererService] writes it via [setRendererUdn]) and | |
| 151 | * never changed afterward — re-using the same UDN across sessions lets other | |
| 152 | * control points re-attach to the renderer they remember. Format is | |
| 153 | * "uuid:" + standard UUID, matching jUPnP's UDN.identifierString. */ | |
| 154 | val rendererUdn: Flow<String?> = context.dataStore.data.map { prefs -> | |
| 155 | prefs[KEY_RENDERER_UDN]?.takeIf { it.isNotBlank() } | |
| 156 | } | |
| 157 | ||
| 158 | /** UDN of the renderer the user wants to drive. Null = this device's local | |
| 159 | * playback (Media3 / Cast as the existing flow decides). Non-null = a remote | |
| 160 | * UPnP MediaRenderer discovered via [UpnpServiceHolder.renderers]. The | |
| 161 | * RendererCoordinator combines this with the discovery list and switches the | |
| 162 | * active backend whenever either side changes. */ | |
| 163 | val leftyMode: Flow<Boolean> = context.dataStore.data.map { prefs -> | |
| 164 | prefs[KEY_LEFTY_MODE] ?: false | |
| 165 | } | |
| 166 | ||
| 167 | val activeRendererUdn: Flow<String?> = context.dataStore.data.map { prefs -> | |
| 168 | prefs[KEY_ACTIVE_RENDERER_UDN]?.takeIf { it.isNotBlank() } | |
| 169 | } | |
| 170 | ||
| 171 | /** Serialized descriptor of the Cast device the user was last OBSERVING | |
| 172 | * (raw-cast follow mode), or null when not observing / after an explicit | |
| 173 | * disconnect. Written by RawCastObserver on successful connect, cleared | |
| 174 | * on user-initiated disconnect. The value is an opaque string owned by | |
| 175 | * RawCastObserver (it serializes/parses its own CastDevice) — the store | |
| 176 | * deliberately doesn't know the format, to avoid a data→cast dependency. | |
| 177 | * | |
| 178 | * Purpose: survive process death. Observing on a phone dies with the | |
| 179 | * process (the TLS socket can't outlive it); persisting the target lets | |
| 180 | * the PlaybackService auto-reattach on next launch if the CCA session | |
| 181 | * is still active, instead of silently dumping the user into local mode | |
| 182 | * with a stale queue on screen. */ | |
| 183 | val observedCastDevice: Flow<String?> = context.dataStore.data.map { prefs -> | |
| 184 | prefs[KEY_OBSERVED_CAST_DEVICE]?.takeIf { it.isNotBlank() } | |
| 185 | } | |
| 186 | ||
| 187 | suspend fun setObservedCastDevice(serialized: String?) { | |
| 188 | context.dataStore.edit { prefs -> | |
| 189 | if (serialized.isNullOrBlank()) prefs.remove(KEY_OBSERVED_CAST_DEVICE) | |
| 190 | else prefs[KEY_OBSERVED_CAST_DEVICE] = serialized | |
| 191 | } | |
| 192 | } | |
| 193 | ||
| 194 | /** Kiosk hand-back target: package name of the app to bring forward when | |
| 195 | * an auto-observed CCA session genuinely ends (voice-cast pipeline — | |
| 196 | * FaveatHaus on other screens). Null = disarmed, which is the state | |
| 197 | * on every normal install: it is ONLY ever set by an OBSERVE launch that | |
| 198 | * carries a `returnTo` extra, and it is cleared the moment a hand-back | |
| 199 | * fires, so a user who opens Earhorn by hand is never yanked away. | |
| 200 | * Persisted (not in-memory) so process death mid-observation doesn't | |
| 201 | * strand a kiosk screen in Earhorn after the music ends. */ | |
| 202 | val kioskReturnPackage: Flow<String?> = context.dataStore.data.map { prefs -> | |
| 203 | prefs[KEY_KIOSK_RETURN_PACKAGE]?.takeIf { it.isNotBlank() } | |
| 204 | } | |
| 205 | ||
| 206 | /** group name -> "host:port" of the last successful attach. Cold | |
| 207 | * starts use it when mDNS is dead (fresh-rebooted routers often drop | |
| 208 | * multicast for minutes — 2026-09-01, the one Android 14 screen). */ | |
| 209 | val lastKnownHosts: Flow<Map<String, String>> = context.dataStore.data.map { prefs -> | |
| 210 | (prefs[KEY_LAST_HOSTS] ?: "").split('\n').filter { it.contains('\t') } | |
| 211 | .associate { line -> line.substringBefore('\t') to line.substringAfter('\t') } | |
| 212 | } | |
| 213 | ||
| 214 | suspend fun setLastKnownHost(group: String, hostPort: String) { | |
| 215 | context.dataStore.edit { prefs -> | |
| 216 | val cur = (prefs[KEY_LAST_HOSTS] ?: "").split('\n') | |
| 217 | .filter { it.contains('\t') } | |
| 218 | .associate { it.substringBefore('\t') to it.substringAfter('\t') } | |
| 219 | .toMutableMap() | |
| 220 | cur[group] = hostPort | |
| 221 | // Audit 2026-09-02: this wrote the LITERAL text "${it.key}\t${it.value}" | |
| 222 | // for every entry (a ${'$'} escape that was never a template), so the | |
| 223 | // map on disk never held a real host and the cold-start rescue in | |
| 224 | // 0.4.15 never dialled anything. Now a real template. | |
| 225 | prefs[KEY_LAST_HOSTS] = cur.entries.take(24) | |
| 226 | .joinToString("\n") { "${it.key}\t${it.value}" } | |
| 227 | } | |
| 228 | } | |
| 229 | ||
| 230 | /** Whether OBSERVE events flip this device to Now Playing and arm the | |
| 231 | * kiosk hand-back. ON for other screens (the pipeline's purpose); OFF | |
| 232 | * turns this install into a manual instrument — OBSERVE is ignored | |
| 233 | * entirely, so browsing is never hijacked (decision, 2026-08-30: | |
| 234 | * mandatory on phones). Per-device. */ | |
| 235 | val kioskFlipEnabled: Flow<Boolean> = context.dataStore.data.map { prefs -> | |
| 236 | prefs[KEY_KIOSK_FLIP_ENABLED] ?: true | |
| 237 | } | |
| 238 | ||
| 239 | suspend fun setKioskFlipEnabled(enabled: Boolean) { | |
| 240 | context.dataStore.edit { prefs -> prefs[KEY_KIOSK_FLIP_ENABLED] = enabled } | |
| 241 | } | |
| 242 | ||
| 243 | /** URL of the Dom-Etha PWA (house CCA control panel). Null = feature | |
| 244 | * off. Not hardcoded — other installs point it wherever theirs lives. */ | |
| 245 | val domEthaUrl: Flow<String?> = context.dataStore.data.map { prefs -> | |
| 246 | prefs[KEY_DOM_ETHA_URL]?.takeIf { it.isNotBlank() } | |
| 247 | } | |
| 248 | ||
| 249 | suspend fun setDomEthaUrl(url: String?) { | |
| 250 | context.dataStore.edit { prefs -> | |
| 251 | if (url.isNullOrBlank()) prefs.remove(KEY_DOM_ETHA_URL) | |
| 252 | else prefs[KEY_DOM_ETHA_URL] = url.trim() | |
| 253 | } | |
| 254 | } | |
| 255 | ||
| 256 | /** Which button occupies the Now Playing corner slot: false = sleep | |
| 257 | * timer (default), true = Dom-Etha panel (only honored when a URL is | |
| 258 | * set — a Dom-Etha choice with no URL falls back to the timer). */ | |
| 259 | val cornerButtonDomEtha: Flow<Boolean> = context.dataStore.data.map { prefs -> | |
| 260 | prefs[KEY_CORNER_BUTTON_DOM_ETHA] ?: false | |
| 261 | } | |
| 262 | ||
| 263 | suspend fun setCornerButtonDomEtha(enabled: Boolean) { | |
| 264 | context.dataStore.edit { prefs -> prefs[KEY_CORNER_BUTTON_DOM_ETHA] = enabled } | |
| 265 | } | |
| 266 | ||
| 267 | suspend fun setKioskReturnPackage(pkg: String?) { | |
| 268 | context.dataStore.edit { prefs -> | |
| 269 | if (pkg.isNullOrBlank()) prefs.remove(KEY_KIOSK_RETURN_PACKAGE) | |
| 270 | else prefs[KEY_KIOSK_RETURN_PACKAGE] = pkg | |
| 271 | } | |
| 272 | } | |
| 273 | ||
| 274 | suspend fun setPreferredServer(udn: String?) { | |
| 275 | context.dataStore.edit { prefs -> | |
| 276 | if (udn.isNullOrBlank()) prefs.remove(KEY_PREFERRED_SERVER) | |
| 277 | else prefs[KEY_PREFERRED_SERVER] = udn | |
| 278 | } | |
| 279 | } | |
| 280 | ||
| 281 | suspend fun setMarqueeEnabled(enabled: Boolean) { | |
| 282 | context.dataStore.edit { prefs -> prefs[KEY_MARQUEE] = enabled } | |
| 283 | } | |
| 284 | ||
| 285 | suspend fun setUseFraunces(enabled: Boolean) { | |
| 286 | context.dataStore.edit { prefs -> prefs[KEY_USE_FRAUNCES] = enabled } | |
| 287 | } | |
| 288 | ||
| 289 | suspend fun setLyricsHighlightSize(multiplier: Float) { | |
| 290 | context.dataStore.edit { prefs -> | |
| 291 | prefs[KEY_LYRICS_HIGHLIGHT_SIZE] = multiplier.coerceIn(1.0f, 2.5f) | |
| 292 | } | |
| 293 | } | |
| 294 | ||
| 295 | suspend fun setLyricsHighlightBold(bold: Boolean) { | |
| 296 | context.dataStore.edit { prefs -> prefs[KEY_LYRICS_HIGHLIGHT_BOLD] = bold } | |
| 297 | } | |
| 298 | ||
| 299 | suspend fun setLyricsHighlightColor(color: LyricsHighlightColor) { | |
| 300 | context.dataStore.edit { prefs -> prefs[KEY_LYRICS_HIGHLIGHT_COLOR] = color.ordinal } | |
| 301 | } | |
| 302 | ||
| 303 | suspend fun setAllowExternalCalls(allow: Boolean) { | |
| 304 | context.dataStore.edit { prefs -> prefs[KEY_ALLOW_EXTERNAL_CALLS] = allow } | |
| 305 | } | |
| 306 | ||
| 307 | suspend fun setReplayGainMode(mode: ReplayGainMode) { | |
| 308 | context.dataStore.edit { prefs -> prefs[KEY_RG_MODE] = mode.ordinal } | |
| 309 | } | |
| 310 | ||
| 311 | suspend fun setReplayGainPreAmpDb(db: Float) { | |
| 312 | context.dataStore.edit { prefs -> | |
| 313 | prefs[KEY_RG_PREAMP] = db.coerceIn(-15f, 15f) | |
| 314 | } | |
| 315 | } | |
| 316 | ||
| 317 | suspend fun addExcludedGenre(genre: String) { | |
| 318 | val trimmed = genre.trim() | |
| 319 | if (trimmed.isEmpty()) return | |
| 320 | context.dataStore.edit { prefs -> | |
| 321 | val current = prefs[KEY_RANDOM_EXCLUDED_GENRES] ?: emptySet() | |
| 322 | prefs[KEY_RANDOM_EXCLUDED_GENRES] = current + trimmed | |
| 323 | } | |
| 324 | } | |
| 325 | ||
| 326 | suspend fun removeExcludedGenre(genre: String) { | |
| 327 | context.dataStore.edit { prefs -> | |
| 328 | val current = prefs[KEY_RANDOM_EXCLUDED_GENRES] ?: emptySet() | |
| 329 | prefs[KEY_RANDOM_EXCLUDED_GENRES] = current - genre | |
| 330 | } | |
| 331 | } | |
| 332 | ||
| 333 | suspend fun setLyricsMetadataEnabled(enabled: Boolean) { | |
| 334 | context.dataStore.edit { prefs -> prefs[KEY_LYRICS_METADATA] = enabled } | |
| 335 | } | |
| 336 | ||
| 337 | suspend fun setEnabled(fields: Set<MetadataField>) { | |
| 338 | context.dataStore.edit { prefs -> | |
| 339 | prefs[KEY_FIELDS] = fields.map { it.key }.toSet() | |
| 340 | } | |
| 341 | } | |
| 342 | ||
| 343 | suspend fun toggle(field: MetadataField) { | |
| 344 | context.dataStore.edit { prefs -> | |
| 345 | val current = (prefs[KEY_FIELDS] ?: MetadataField.defaults.map { it.key }.toSet()) | |
| 346 | .toMutableSet() | |
| 347 | if (current.contains(field.key)) current.remove(field.key) else current.add(field.key) | |
| 348 | prefs[KEY_FIELDS] = current | |
| 349 | } | |
| 350 | } | |
| 351 | ||
| 352 | // ── Renderer mesh setters ──────────────────────────────────────── | |
| 353 | ||
| 354 | suspend fun setRendererBroadcastEnabled(enabled: Boolean) { | |
| 355 | context.dataStore.edit { prefs -> prefs[KEY_RENDERER_BROADCAST_ENABLED] = enabled } | |
| 356 | } | |
| 357 | ||
| 358 | suspend fun setRendererFriendlyName(name: String?) { | |
| 359 | context.dataStore.edit { prefs -> | |
| 360 | if (name.isNullOrBlank()) prefs.remove(KEY_RENDERER_FRIENDLY_NAME) | |
| 361 | else prefs[KEY_RENDERER_FRIENDLY_NAME] = name | |
| 362 | } | |
| 363 | } | |
| 364 | ||
| 365 | suspend fun setRendererUdn(udn: String?) { | |
| 366 | context.dataStore.edit { prefs -> | |
| 367 | if (udn.isNullOrBlank()) prefs.remove(KEY_RENDERER_UDN) | |
| 368 | else prefs[KEY_RENDERER_UDN] = udn | |
| 369 | } | |
| 370 | } | |
| 371 | ||
| 372 | suspend fun setLeftyMode(enabled: Boolean) { | |
| 373 | context.dataStore.edit { it[KEY_LEFTY_MODE] = enabled } | |
| 374 | } | |
| 375 | ||
| 376 | suspend fun setActiveRendererUdn(udn: String?) { | |
| 377 | context.dataStore.edit { prefs -> | |
| 378 | if (udn.isNullOrBlank()) prefs.remove(KEY_ACTIVE_RENDERER_UDN) | |
| 379 | else prefs[KEY_ACTIVE_RENDERER_UDN] = udn | |
| 380 | } | |
| 381 | } | |
| 382 | ||
| 383 | companion object { | |
| 384 | private val KEY_FIELDS = stringSetPreferencesKey("enabled_metadata_fields") | |
| 385 | private val KEY_PREFERRED_SERVER = stringPreferencesKey("preferred_server_udn") | |
| 386 | private val KEY_OBSERVED_CAST_DEVICE = stringPreferencesKey("observed_cast_device") | |
| 387 | private val KEY_KIOSK_RETURN_PACKAGE = stringPreferencesKey("kiosk_return_package") | |
| 388 | private val KEY_KIOSK_FLIP_ENABLED = booleanPreferencesKey("kiosk_flip_enabled") | |
| 389 | private val KEY_LAST_HOSTS = stringPreferencesKey("last_known_group_hosts") | |
| 390 | private val KEY_DOM_ETHA_URL = stringPreferencesKey("dom_etha_url") | |
| 391 | private val KEY_CORNER_BUTTON_DOM_ETHA = booleanPreferencesKey("corner_button_dom_etha") | |
| 392 | private val KEY_MARQUEE = booleanPreferencesKey("marquee_enabled") | |
| 393 | private val KEY_USE_FRAUNCES = booleanPreferencesKey("use_fraunces") | |
| 394 | private val KEY_RG_MODE = intPreferencesKey("replay_gain_mode") | |
| 395 | private val KEY_RG_PREAMP = floatPreferencesKey("replay_gain_preamp_db") | |
| 396 | private val KEY_RANDOM_EXCLUDED_GENRES = stringSetPreferencesKey("random_excluded_genres") | |
| 397 | private val KEY_LYRICS_HIGHLIGHT_SIZE = floatPreferencesKey("lyrics_highlight_size") | |
| 398 | private val KEY_LYRICS_HIGHLIGHT_BOLD = booleanPreferencesKey("lyrics_highlight_bold") | |
| 399 | private val KEY_LYRICS_HIGHLIGHT_COLOR = intPreferencesKey("lyrics_highlight_color") | |
| 400 | private val KEY_ALLOW_EXTERNAL_CALLS = booleanPreferencesKey("allow_external_calls") | |
| 401 | private val KEY_LYRICS_METADATA = booleanPreferencesKey("lyrics_metadata_enabled") | |
| 402 | private val KEY_RENDERER_BROADCAST_ENABLED = | |
| 403 | booleanPreferencesKey("renderer_broadcast_enabled") | |
| 404 | private val KEY_RENDERER_FRIENDLY_NAME = | |
| 405 | stringPreferencesKey("renderer_friendly_name") | |
| 406 | private val KEY_RENDERER_UDN = stringPreferencesKey("renderer_udn") | |
| 407 | private val KEY_ACTIVE_RENDERER_UDN = stringPreferencesKey("active_renderer_udn") | |
| 408 | private val KEY_LEFTY_MODE = booleanPreferencesKey("lefty_mode") | |
| 409 | } | |
| 410 | } |


