diff --git a/android/app/src/main/java/com/fabledsword/minstrel/theme/ThemeMode.kt b/android/app/src/main/java/com/fabledsword/minstrel/theme/ThemeMode.kt new file mode 100644 index 00000000..b821e67b --- /dev/null +++ b/android/app/src/main/java/com/fabledsword/minstrel/theme/ThemeMode.kt @@ -0,0 +1,36 @@ +package com.fabledsword.minstrel.theme + +/** + * User-selectable theme override. SYSTEM defers to + * `isSystemInDarkTheme()`; LIGHT and DARK pin the app to that mode + * regardless of the device setting. + * + * Stored as the wire string in `AuthSessionEntity.themeMode`. Null + * persistence value means "never set" which we treat as SYSTEM. + */ +enum class ThemeMode { + SYSTEM, LIGHT, DARK; + + /** Stable string identifier used for persistence. */ + val wire: String get() = name.lowercase() + + /** + * Maps to MinstrelTheme's `darkOverride: Boolean?` parameter: + * - SYSTEM → null (let isSystemInDarkTheme decide) + * - LIGHT → false + * - DARK → true + */ + fun toDarkOverride(): Boolean? = when (this) { + SYSTEM -> null + LIGHT -> false + DARK -> true + } + + companion object { + fun fromWire(wire: String?): ThemeMode = when (wire) { + "light" -> LIGHT + "dark" -> DARK + else -> SYSTEM + } + } +} diff --git a/android/app/src/main/java/com/fabledsword/minstrel/theme/ThemePreferenceViewModel.kt b/android/app/src/main/java/com/fabledsword/minstrel/theme/ThemePreferenceViewModel.kt new file mode 100644 index 00000000..7234c90d --- /dev/null +++ b/android/app/src/main/java/com/fabledsword/minstrel/theme/ThemePreferenceViewModel.kt @@ -0,0 +1,45 @@ +package com.fabledsword.minstrel.theme + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.fabledsword.minstrel.auth.AuthStore +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import javax.inject.Inject + +private const val SHARE_STOP_TIMEOUT_MS = 5_000L + +/** + * Surfaces AuthStore.themeMode as a typed [ThemeMode] StateFlow and + * lets callers write through. Used by: + * - MainActivity's App() to pick the darkOverride to wrap the + * NavGraph in. + * - SettingsScreen's Appearance section as the 3-way picker. + * + * Lives in the theme package rather than a UI package because it's + * tightly coupled to the theme tokens; SettingsScreen just consumes it. + */ +@HiltViewModel +class ThemePreferenceViewModel @Inject constructor( + private val authStore: AuthStore, +) : ViewModel() { + + val themeMode: StateFlow = + authStore.themeMode + .map { ThemeMode.fromWire(it) } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(SHARE_STOP_TIMEOUT_MS), + initialValue = ThemeMode.fromWire(authStore.themeMode.value), + ) + + fun setThemeMode(mode: ThemeMode) { + // Persist null for SYSTEM so a "never set" row reads identically + // to an explicit "follow system" pick — keeps the wire/storage + // round-trip clean. + authStore.setThemeMode(if (mode == ThemeMode.SYSTEM) null else mode.wire) + } +}