git.iko.soy/opencode-android
on 06fd41b help sign in
152 lines 7.3 kB history raw
1package soy.iko.opencode.data.repo
3import android.content.Context
4import androidx.datastore.preferences.core.booleanPreferencesKey
5import androidx.datastore.preferences.core.edit
6import androidx.datastore.preferences.core.floatPreferencesKey
7import androidx.datastore.preferences.core.intPreferencesKey
8import androidx.datastore.preferences.core.stringPreferencesKey
9import androidx.datastore.preferences.preferencesDataStore
10import kotlinx.coroutines.flow.Flow
11import kotlinx.coroutines.flow.map
13enum class ThemeMode { SYSTEM, LIGHT, DARK, AMOLED }
15private val Context.settingsDataStore by preferencesDataStore(name = "settings")
17/** App-preferences store: theme mode, dynamic color (Material You), input behavior. */
18class SettingsStore(context: Context) {
20 private val appContext = context.applicationContext
21 private val themeKey = stringPreferencesKey("theme_mode")
22 private val dynamicColorKey = booleanPreferencesKey("dynamic_color")
23 private val sendOnEnterKey = booleanPreferencesKey("send_on_enter")
24 private val appLockKey = booleanPreferencesKey("app_lock")
25 private val appLockReLockSecondsKey = intPreferencesKey("app_lock_relock_seconds")
26 private val chatTextScaleKey = floatPreferencesKey("chat_text_scale")
27 private val codeWrapKey = booleanPreferencesKey("code_wrap")
28 private val sessionSortModeKey = stringPreferencesKey("session_sort_mode")
29 private val sessionSortDescendingKey = booleanPreferencesKey("session_sort_descending")
30 private val sessionShowArchivedKey = booleanPreferencesKey("session_show_archived")
32 companion object {
33 /** Bounds for the chat text-size multiplier so the UI can't be scaled into
34 * unreadability or an unusable micro-font. 1.0 is the design default. */
35 const val MIN_CHAT_TEXT_SCALE = 0.8f
36 const val MAX_CHAT_TEXT_SCALE = 1.6f
37 const val DEFAULT_CHAT_TEXT_SCALE = 1.0f
38 /** 0 means re-lock the instant the app backgrounds. Other entries are the grace
39 * period (seconds) during which a quick app-switch does NOT re-prompt for auth. */
40 val APP_LOCK_RELOCK_OPTIONS_SECONDS: List<Int> = listOf(0, 30, 60, 300, 1800, 3600)
41 const val DEFAULT_APP_LOCK_RELOCK_SECONDS = 0
42 }
44 val themeMode: Flow<ThemeMode> = appContext.settingsDataStore.data.map { prefs ->
45 runCatching { ThemeMode.valueOf(prefs[themeKey] ?: ThemeMode.SYSTEM.name) }
46 .getOrDefault(ThemeMode.SYSTEM)
47 }
49 // Default OFF so the app ships with its hand-built "Tokyo Night" brand palette rather than
50 // the wallpaper-derived Material You scheme, which fully replaces the brand colours. Users
51 // on Android 12+ can still opt into dynamic color from Settings.
52 val dynamicColor: Flow<Boolean> = appContext.settingsDataStore.data.map { prefs ->
53 prefs[dynamicColorKey] ?: false
54 }
56 /** When true, the hardware Enter key sends the prompt; when false, Enter inserts a
57 * newline and Ctrl+Enter sends. Soft-keyboard IME action always sends. Defaults to
58 * true to match the prior hardcoded behavior. */
59 val sendOnEnter: Flow<Boolean> = appContext.settingsDataStore.data.map { prefs ->
60 prefs[sendOnEnterKey] ?: true
61 }
63 /** When true, the app requires device biometric/credential authentication on launch
64 * (and when returning from the background) before the UI is shown, protecting the
65 * stored server credentials. Defaults to false so the app is usable out of the box. */
66 val appLock: Flow<Boolean> = appContext.settingsDataStore.data.map { prefs ->
67 prefs[appLockKey] ?: false
68 }
70 /** Grace period (seconds) before re-locking after the app backgrounds. 0 = re-lock
71 * immediately (the prior behavior). Lets a quick app-switch not re-prompt, which is the
72 * standard reason users disable biometric locks in messaging/banking apps. */
73 val appLockReLockSeconds: Flow<Int> = appContext.settingsDataStore.data.map { prefs ->
74 prefs[appLockReLockSecondsKey] ?: DEFAULT_APP_LOCK_RELOCK_SECONDS
75 }
77 /** Multiplier applied to chat message (markdown) and code text so users can size the
78 * conversation for readability independently of the system font scale. Clamped to
79 * [[MIN_CHAT_TEXT_SCALE], [MAX_CHAT_TEXT_SCALE]] on read so a corrupt/out-of-range
80 * stored value can never produce an unreadable UI. Defaults to 1.0 (design size). */
81 val chatTextScale: Flow<Float> = appContext.settingsDataStore.data.map { prefs ->
82 (prefs[chatTextScaleKey] ?: DEFAULT_CHAT_TEXT_SCALE)
83 .coerceIn(MIN_CHAT_TEXT_SCALE, MAX_CHAT_TEXT_SCALE)
84 }
86 /** When true, fenced/indented code blocks soft-wrap long lines instead of scrolling
87 * horizontally. Defaults to false (horizontal scroll) to preserve code formatting. */
88 val codeWrap: Flow<Boolean> = appContext.settingsDataStore.data.map { prefs ->
89 prefs[codeWrapKey] ?: false
90 }
92 /** Persisted session-list sort mode so a chosen ordering survives process death (not just
93 * rotation). Stored as the enum name; an unknown value falls back to RECENT. */
94 val sessionSortMode: Flow<String> = appContext.settingsDataStore.data.map { prefs ->
95 prefs[sessionSortModeKey] ?: "RECENT"
96 }
98 /** Persisted session-list sort direction. Defaults to descending (newest first). */
99 val sessionSortDescending: Flow<Boolean> = appContext.settingsDataStore.data.map { prefs ->
100 prefs[sessionSortDescendingKey] ?: true
101 }
103 /** Persisted "show archived sessions" toggle so the choice survives process death. */
104 val sessionShowArchived: Flow<Boolean> = appContext.settingsDataStore.data.map { prefs ->
105 prefs[sessionShowArchivedKey] ?: false
106 }
108 suspend fun setThemeMode(mode: ThemeMode) {
109 appContext.settingsDataStore.edit { it[themeKey] = mode.name }
110 }
112 suspend fun setDynamicColor(enabled: Boolean) {
113 appContext.settingsDataStore.edit { it[dynamicColorKey] = enabled }
114 }
116 suspend fun setSendOnEnter(enabled: Boolean) {
117 appContext.settingsDataStore.edit { it[sendOnEnterKey] = enabled }
118 }
120 suspend fun setAppLock(enabled: Boolean) {
121 appContext.settingsDataStore.edit { it[appLockKey] = enabled }
122 }
124 suspend fun setAppLockReLockSeconds(seconds: Int) {
125 appContext.settingsDataStore.edit {
126 it[appLockReLockSecondsKey] = APP_LOCK_RELOCK_OPTIONS_SECONDS.minBy { k -> Math.abs(k - seconds) }
127 }
128 }
130 suspend fun setChatTextScale(scale: Float) {
131 appContext.settingsDataStore.edit {
132 it[chatTextScaleKey] = scale.coerceIn(MIN_CHAT_TEXT_SCALE, MAX_CHAT_TEXT_SCALE)
133 }
134 }
135 suspend fun setCodeWrap(enabled: Boolean) {
136 appContext.settingsDataStore.edit {
137 it[codeWrapKey] = enabled
138 }
139 }
141 suspend fun setSessionSortMode(mode: String) {
142 appContext.settingsDataStore.edit { it[sessionSortModeKey] = mode }
143 }
145 suspend fun setSessionSortDescending(descending: Boolean) {
146 appContext.settingsDataStore.edit { it[sessionSortDescendingKey] = descending }
147 }
149 suspend fun setSessionShowArchived(show: Boolean) {
150 appContext.settingsDataStore.edit { it[sessionShowArchivedKey] = show }
151 }