From ca66306d05eebd2d0db99b06b8c2dc9787154489 Mon Sep 17 00:00:00 2001 From: SmilingPixel Date: Tue, 21 Apr 2026 02:45:51 +0000 Subject: [PATCH 01/19] feat(theme): add light and dark color schemes with theme mode support --- .../io/github/smiling_pixel/theme/Color.kt | 98 +++++++++++++++++++ .../io/github/smiling_pixel/theme/Theme.kt | 86 ++++++++++++++++ .../github/smiling_pixel/theme/ThemeMode.kt | 26 +++++ 3 files changed, 210 insertions(+) create mode 100644 composeApp/src/commonMain/kotlin/io/github/smiling_pixel/theme/Color.kt create mode 100644 composeApp/src/commonMain/kotlin/io/github/smiling_pixel/theme/Theme.kt create mode 100644 composeApp/src/commonMain/kotlin/io/github/smiling_pixel/theme/ThemeMode.kt diff --git a/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/theme/Color.kt b/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/theme/Color.kt new file mode 100644 index 0000000..277a247 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/theme/Color.kt @@ -0,0 +1,98 @@ +package io.github.smiling_pixel.theme + +import androidx.compose.ui.graphics.Color + +// Light Theme Colors + +/** Primary color for the light theme, representing the main green brand. */ +val primaryLight = Color(0xFF386B36) +/** Color used for text/icons on top of the primary color in the light theme. */ +val onPrimaryLight = Color(0xFFFFFFFF) +/** A lighter tone of the primary color, used as a background for grouped elements in the light theme. */ +val primaryContainerLight = Color(0xFFB9F3B1) +/** Color used for text/icons on top of the primary container in the light theme. */ +val onPrimaryContainerLight = Color(0xFF002203) + +/** Secondary brand color for the light theme, used for less prominent accents. */ +val secondaryLight = Color(0xFF526350) +/** Color used for text/icons on top of the secondary color in the light theme. */ +val onSecondaryLight = Color(0xFFFFFFFF) +/** A lighter tone of the secondary color, used for secondary grouped elements in the light theme. */ +val secondaryContainerLight = Color(0xFFD5E8D0) +/** Color used for text/icons on top of the secondary container in the light theme. */ +val onSecondaryContainerLight = Color(0xFF111F0F) + +/** Tertiary brand color for the light theme, providing a contrasting accent (teal/blue). */ +val tertiaryLight = Color(0xFF39656B) +/** Color used for text/icons on top of the tertiary color in the light theme. */ +val onTertiaryLight = Color(0xFFFFFFFF) +/** A lighter tone of the tertiary color, used for tertiary grouped elements in the light theme. */ +val tertiaryContainerLight = Color(0xFFBCEBF2) +/** Color used for text/icons on top of the tertiary container in the light theme. */ +val onTertiaryContainerLight = Color(0xFF001F23) + +/** Error color for the light theme, indicating destructive actions or errors. */ +val errorLight = Color(0xFFBA1A1A) +/** Color used for text/icons on top of the error color in the light theme. */ +val onErrorLight = Color(0xFFFFFFFF) +/** A lighter tone of the error color, used as a subtle error background in the light theme. */ +val errorContainerLight = Color(0xFFFFDAD6) +/** Color used for text/icons on top of the error container in the light theme. */ +val onErrorContainerLight = Color(0xFF410002) + +/** Background color for the overall application layout in the light theme. */ +val backgroundLight = Color(0xFFF7FBF2) +/** Color used for text/icons on top of the overall background in the light theme. */ +val onBackgroundLight = Color(0xFF181D17) +/** Surface color for components like cards and menus in the light theme. */ +val surfaceLight = Color(0xFFF7FBF2) +/** Color used for text/icons on top of surfaces in the light theme. */ +val onSurfaceLight = Color(0xFF181D17) + + +// Dark Theme Colors + +/** Primary color for the dark theme, optimized for dark backgrounds. */ +val primaryDark = Color(0xFF9ED697) +/** Color used for text/icons on top of the primary color in the dark theme. */ +val onPrimaryDark = Color(0xFF05390C) +/** A darker tone of the primary color, used as a background for grouped elements in the dark theme. */ +val primaryContainerDark = Color(0xFF1F5120) +/** Color used for text/icons on top of the primary container in the dark theme. */ +val onPrimaryContainerDark = Color(0xFFB9F3B1) + +/** Secondary brand color for the dark theme. */ +val secondaryDark = Color(0xFFB9CCB5) +/** Color used for text/icons on top of the secondary color in the dark theme. */ +val onSecondaryDark = Color(0xFF253424) +/** A darker tone of the secondary color for the dark theme. */ +val secondaryContainerDark = Color(0xFF3B4B39) +/** Color used for text/icons on top of the secondary container in the dark theme. */ +val onSecondaryContainerDark = Color(0xFFD5E8D0) + +/** Tertiary brand color for the dark theme. */ +val tertiaryDark = Color(0xFFA0CFD5) +/** Color used for text/icons on top of the tertiary color in the dark theme. */ +val onTertiaryDark = Color(0xFF00363B) +/** A darker tone of the tertiary color for the dark theme. */ +val tertiaryContainerDark = Color(0xFF1E4D53) +/** Color used for text/icons on top of the tertiary container in the dark theme. */ +val onTertiaryContainerDark = Color(0xFFBCEBF2) + +/** Error color for the dark theme. */ +val errorDark = Color(0xFFFFB4AB) +/** Color used for text/icons on top of the error color in the dark theme. */ +val onErrorDark = Color(0xFF690005) +/** A darker tone of the error color for the dark theme. */ +val errorContainerDark = Color(0xFF93000A) +/** Color used for text/icons on top of the error container in the dark theme. */ +val onErrorContainerDark = Color(0xFFFFDAD6) + +/** Background color for the overall application layout in the dark theme. */ +val backgroundDark = Color(0xFF10140F) +/** Color used for text/icons on top of the overall background in the dark theme. */ +val onBackgroundDark = Color(0xFFE0E4DB) +/** Surface color for components like cards and menus in the dark theme. */ +val surfaceDark = Color(0xFF10140F) +/** Color used for text/icons on top of surfaces in the dark theme. */ +val onSurfaceDark = Color(0xFFE0E4DB) diff --git a/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/theme/Theme.kt b/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/theme/Theme.kt new file mode 100644 index 0000000..bab66d0 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/theme/Theme.kt @@ -0,0 +1,86 @@ +package io.github.smiling_pixel.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable + +/** + * Material 3 light color scheme for the application. + */ +private val LightColorScheme = lightColorScheme( + primary = primaryLight, + onPrimary = onPrimaryLight, + primaryContainer = primaryContainerLight, + onPrimaryContainer = onPrimaryContainerLight, + secondary = secondaryLight, + onSecondary = onSecondaryLight, + secondaryContainer = secondaryContainerLight, + onSecondaryContainer = onSecondaryContainerLight, + tertiary = tertiaryLight, + onTertiary = onTertiaryLight, + tertiaryContainer = tertiaryContainerLight, + onTertiaryContainer = onTertiaryContainerLight, + error = errorLight, + onError = onErrorLight, + errorContainer = errorContainerLight, + onErrorContainer = onErrorContainerLight, + background = backgroundLight, + onBackground = onBackgroundLight, + surface = surfaceLight, + onSurface = onSurfaceLight +) + +/** + * Material 3 dark color scheme for the application. + */ +private val DarkColorScheme = darkColorScheme( + primary = primaryDark, + onPrimary = onPrimaryDark, + primaryContainer = primaryContainerDark, + onPrimaryContainer = onPrimaryContainerDark, + secondary = secondaryDark, + onSecondary = onSecondaryDark, + secondaryContainer = secondaryContainerDark, + onSecondaryContainer = onSecondaryContainerDark, + tertiary = tertiaryDark, + onTertiary = onTertiaryDark, + tertiaryContainer = tertiaryContainerDark, + onTertiaryContainer = onTertiaryContainerDark, + error = errorDark, + onError = onErrorDark, + errorContainer = errorContainerDark, + onErrorContainer = onErrorContainerDark, + background = backgroundDark, + onBackground = onBackgroundDark, + surface = surfaceDark, + onSurface = onSurfaceDark +) + +/** + * The main theme for the MarkDay diary app. + * + * This theme applies a green-based Material 3 color system to the application, adjusting + * appropriately for light and dark system settings. It wraps the standard `MaterialTheme` + * composable from the Jetpack Compose Material 3 library. + * + * @param useDarkTheme Whether to use the dark theme color scheme. Defaults to system preference. + * @param content The composable content that will be styled by this theme. + */ +@Composable +fun MarkDayTheme( + useDarkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable () -> Unit +) { + val colorScheme = if (useDarkTheme) { + DarkColorScheme + } else { + LightColorScheme + } + + MaterialTheme( + colorScheme = colorScheme, + content = content + ) +} diff --git a/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/theme/ThemeMode.kt b/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/theme/ThemeMode.kt new file mode 100644 index 0000000..2f5a5ed --- /dev/null +++ b/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/theme/ThemeMode.kt @@ -0,0 +1,26 @@ +package io.github.smiling_pixel.theme + +/** + * Represents the different theme modes available in the application. + * + * This enum is used to determine which color palette (light or dark) + * should be applied to the user interface. + */ +enum class ThemeMode { + /** + * Follows the system's default theme settings. + * If the operating system is set to dark mode, the app will use the dark theme. + * Otherwise, it will default to the light theme. + */ + SYSTEM, + + /** + * Forces the application to use the light theme, regardless of the system settings. + */ + LIGHT, + + /** + * Forces the application to use the dark theme, regardless of the system settings. + */ + DARK +} From a3a652c48c6b861718fbc19370afaab7ffa24bc9 Mon Sep 17 00:00:00 2001 From: SmilingPixel Date: Wed, 22 Apr 2026 01:30:16 +0000 Subject: [PATCH 02/19] feat(theme): add theme mode selection to settings screen --- .../preference/SettingsRepository.kt | 13 ++++++ .../smiling_pixel/screens/SettingsScreen.kt | 42 +++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/preference/SettingsRepository.kt b/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/preference/SettingsRepository.kt index 43aa8a5..eec03b0 100644 --- a/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/preference/SettingsRepository.kt +++ b/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/preference/SettingsRepository.kt @@ -1,8 +1,21 @@ package io.github.smiling_pixel.preference +import io.github.smiling_pixel.theme.ThemeMode import kotlinx.coroutines.flow.Flow interface SettingsRepository { + /** + * A flow emitting the current [ThemeMode] setting of the application. + * Default is [ThemeMode.SYSTEM]. + */ + val themeMode: Flow + + /** + * Updates the application's theme mode setting. + * @param mode The new [ThemeMode] to be applied. + */ + suspend fun setThemeMode(mode: ThemeMode) + val googleWeatherApiKey: Flow suspend fun setGoogleWeatherApiKey(key: String?) diff --git a/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/screens/SettingsScreen.kt b/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/screens/SettingsScreen.kt index 6931a48..c5bd251 100644 --- a/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/screens/SettingsScreen.kt +++ b/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/screens/SettingsScreen.kt @@ -15,8 +15,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.unit.dp +import androidx.compose.ui.Alignment import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.Button +import androidx.compose.material3.RadioButton +import androidx.compose.foundation.layout.Row import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.OutlinedTextField import androidx.compose.runtime.collectAsState @@ -38,6 +41,7 @@ import kotlinx.coroutines.launch fun SettingsScreen() { val scope = rememberCoroutineScope() val settingsRepository = getSettingsRepository() + val themeMode by settingsRepository.themeMode.collectAsState(initial = io.github.smiling_pixel.theme.ThemeMode.SYSTEM) val apiKey by settingsRepository.googleWeatherApiKey.collectAsState(initial = null) val uriHandler = LocalUriHandler.current @@ -94,6 +98,44 @@ fun SettingsScreen() { ) Spacer(modifier = Modifier.height(24.dp)) + /** + * A section to allow the user to select their preferred Theme Mode. + * The user can select from SYSTEM, LIGHT, or DARK modes using a group of radio buttons. + */ + Text( + text = "Appearance", + style = MaterialTheme.typography.titleLarge + ) + Spacer(modifier = Modifier.height(8.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + io.github.smiling_pixel.theme.ThemeMode.entries.forEach { mode -> + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.clickable { + scope.launch { + settingsRepository.setThemeMode(mode) + } + }.padding(end = 16.dp) + ) { + RadioButton( + selected = themeMode == mode, + onClick = { + scope.launch { + settingsRepository.setThemeMode(mode) + } + } + ) + Spacer(modifier = Modifier.width(4.dp)) + Text(text = mode.name.lowercase().replaceFirstChar { it.uppercase() }) + } + } + } + + Spacer(modifier = Modifier.height(24.dp)) + Text( text = "Third-party Services", style = MaterialTheme.typography.titleLarge From daa4ed9e20886bf33c3c47436bdb3a92486baebc Mon Sep 17 00:00:00 2001 From: SmilingPixel Date: Thu, 23 Apr 2026 03:02:47 +0000 Subject: [PATCH 03/19] feat(theme): implement theme mode management in DataStore and WasmJs settings repositories --- .../preference/DataStoreSettingsRepository.kt | 19 +++++++++++++++++++ .../preference/SettingsRepository.wasmJs.kt | 17 +++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/composeApp/src/nonWebMain/kotlin/io/github/smiling_pixel/preference/DataStoreSettingsRepository.kt b/composeApp/src/nonWebMain/kotlin/io/github/smiling_pixel/preference/DataStoreSettingsRepository.kt index 143c658..6723abe 100644 --- a/composeApp/src/nonWebMain/kotlin/io/github/smiling_pixel/preference/DataStoreSettingsRepository.kt +++ b/composeApp/src/nonWebMain/kotlin/io/github/smiling_pixel/preference/DataStoreSettingsRepository.kt @@ -6,6 +6,7 @@ import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey +import io.github.smiling_pixel.theme.ThemeMode import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import okio.Path.Companion.toPath @@ -30,6 +31,24 @@ class DataStoreSettingsRepository(private val dataStore: DataStore) private val IS_AUTO_SYNC_ENABLED = booleanPreferencesKey("is_auto_sync_enabled") private val CLOUD_SYNC_PATH = stringPreferencesKey("cloud_sync_path") private val CLOUD_SYNC_DELETION_TOMBSTONES_JSON = stringPreferencesKey("cloud_sync_deletion_tombstones_json") + private val THEME_MODE = stringPreferencesKey("theme_mode") + + override val themeMode: Flow = dataStore.data + .map { preferences -> + preferences[THEME_MODE]?.let { modeString -> + try { + ThemeMode.valueOf(modeString) + } catch (e: IllegalArgumentException) { + ThemeMode.SYSTEM + } + } ?: ThemeMode.SYSTEM + } + + override suspend fun setThemeMode(mode: ThemeMode) { + dataStore.edit { preferences -> + preferences[THEME_MODE] = mode.name + } + } override val googleWeatherApiKey: Flow = dataStore.data .map { preferences -> diff --git a/composeApp/src/wasmJsMain/kotlin/io/github/smiling_pixel/preference/SettingsRepository.wasmJs.kt b/composeApp/src/wasmJsMain/kotlin/io/github/smiling_pixel/preference/SettingsRepository.wasmJs.kt index b26f99d..f2174a1 100644 --- a/composeApp/src/wasmJsMain/kotlin/io/github/smiling_pixel/preference/SettingsRepository.wasmJs.kt +++ b/composeApp/src/wasmJsMain/kotlin/io/github/smiling_pixel/preference/SettingsRepository.wasmJs.kt @@ -1,5 +1,6 @@ package io.github.smiling_pixel.preference +import io.github.smiling_pixel.theme.ThemeMode import kotlinx.browser.localStorage import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -12,6 +13,22 @@ class WasmJsSettingsRepository : SettingsRepository { private val _cloudSyncPath = MutableStateFlow(localStorage.getItem("cloud_sync_path") ?: "/MarkDay") private val _cloudSyncDeletionTombstonesJson = MutableStateFlow(localStorage.getItem("cloud_sync_deletion_tombstones_json")) + private val _themeMode = MutableStateFlow( + localStorage.getItem("theme_mode")?.let { + try { + ThemeMode.valueOf(it) + } catch (e: IllegalArgumentException) { + ThemeMode.SYSTEM + } + } ?: ThemeMode.SYSTEM + ) + + override val themeMode: Flow = _themeMode.asStateFlow() + + override suspend fun setThemeMode(mode: ThemeMode) { + localStorage.setItem("theme_mode", mode.name) + _themeMode.value = mode + } override val googleWeatherApiKey: Flow = _apiKey.asStateFlow() From e82ff21f530c726aa84d22f89c101afd9a4fcb2b Mon Sep 17 00:00:00 2001 From: SmilingPixel Date: Fri, 24 Apr 2026 03:01:25 +0000 Subject: [PATCH 04/19] feat(ui): update EntryDetailsScreen and MomentsScreen theme --- .../screens/EntryDetailsScreen.kt | 19 ++++++++++++++----- .../smiling_pixel/screens/MomentsScreen.kt | 1 - 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/screens/EntryDetailsScreen.kt b/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/screens/EntryDetailsScreen.kt index e21361f..e48b1a3 100644 --- a/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/screens/EntryDetailsScreen.kt +++ b/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/screens/EntryDetailsScreen.kt @@ -47,7 +47,6 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import kotlin.time.ExperimentalTime @@ -55,6 +54,16 @@ import io.github.smiling_pixel.util.Logger import io.github.smiling_pixel.util.e import io.github.smiling_pixel.util.w +/** + * Screen displaying the details of a diary entry. + * It allows the user to view the entry's content, title, date, and weather, + * or edit these details if they switch to edit mode. + * + * @param entry The [DiaryEntry] to display or edit. If null, creates a new entry. + * @param weatherClient The [WeatherClient] to fetch weather information. + * @param onSave Callback invoked when the user saves the entry. + * @param onCancel Callback invoked when the user cancels editing or goes back. + */ @OptIn(ExperimentalTime::class, ExperimentalMaterial3Api::class) @Composable fun EntryDetailsScreen( @@ -201,7 +210,7 @@ fun EntryDetailsScreen( } Spacer(modifier = Modifier.height(12.dp)) - HorizontalDivider(thickness = 1.dp, color = Color.Gray) + HorizontalDivider(thickness = 1.dp, color = MaterialTheme.colorScheme.outlineVariant) Spacer(modifier = Modifier.height(12.dp)) Row(verticalAlignment = Alignment.CenterVertically) { @@ -296,7 +305,7 @@ fun EntryDetailsScreen( Text( text = "Created: ${createdLocal.date} $createdTimeStr", style = MaterialTheme.typography.bodySmall, - color = Color.Gray + color = MaterialTheme.colorScheme.outlineVariant ) Spacer(modifier = Modifier.height(6.dp)) @@ -305,7 +314,7 @@ fun EntryDetailsScreen( Text( text = "Updated: ${updatedLocal.date} $updatedTimeStr", style = MaterialTheme.typography.bodySmall, - color = Color.Gray + color = MaterialTheme.colorScheme.outlineVariant ) Spacer(modifier = Modifier.height(12.dp)) @@ -318,7 +327,7 @@ fun EntryDetailsScreen( Spacer(modifier = Modifier.height(6.dp)) } - HorizontalDivider(thickness = 1.dp, color = Color.Gray) + HorizontalDivider(thickness = 1.dp, color = MaterialTheme.colorScheme.outlineVariant) Spacer(modifier = Modifier.height(12.dp)) diff --git a/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/screens/MomentsScreen.kt b/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/screens/MomentsScreen.kt index 4f3fe8d..1251893 100644 --- a/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/screens/MomentsScreen.kt +++ b/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/screens/MomentsScreen.kt @@ -36,7 +36,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import io.github.smiling_pixel.filesystem.FileRepository import io.github.smiling_pixel.model.FileMetadata From 6e673a84c97d7a23ce1dd77e3521fecf6a575b03 Mon Sep 17 00:00:00 2001 From: SmilingPixel Date: Sat, 25 Apr 2026 01:54:09 +0000 Subject: [PATCH 05/19] feat(theme): integrate theme mode selection into App component --- .../src/commonMain/kotlin/io/github/smiling_pixel/App.kt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/App.kt b/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/App.kt index 50b8c11..559c97f 100644 --- a/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/App.kt +++ b/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/App.kt @@ -1,5 +1,6 @@ package io.github.smiling_pixel +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding @@ -18,6 +19,7 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -38,7 +40,9 @@ import io.github.smiling_pixel.filesystem.InMemoryFileManager import io.github.smiling_pixel.database.InMemoryFileMetadataDao import io.github.smiling_pixel.client.GoogleWeatherClient import io.github.smiling_pixel.client.WeatherClient +import io.github.smiling_pixel.theme.ThemeMode import io.github.smiling_pixel.preference.getSettingsRepository +import io.github.smiling_pixel.theme.MarkDayTheme import io.github.smiling_pixel.screens.EntriesScreen import io.github.smiling_pixel.screens.EntryDetailsScreen import io.github.smiling_pixel.screens.InsightsScreen @@ -75,7 +79,10 @@ fun App( getAsyncImageLoader(context) } - MaterialTheme { + val themeMode by getSettingsRepository().themeMode.collectAsState(initial = ThemeMode.SYSTEM) + val useDarkTheme = themeMode == ThemeMode.DARK || (themeMode == ThemeMode.SYSTEM && isSystemInDarkTheme()) + + MarkDayTheme(useDarkTheme = useDarkTheme) { val repo = providedRepo ?: remember { DiaryRepository(InMemoryDiaryDao()) } val fileRepo = providedFileRepo ?: remember { FileRepository(InMemoryFileManager(), InMemoryFileMetadataDao()) From 4ff46ef3bf0dcc38c3dcb16a1068f394b5e7b807 Mon Sep 17 00:00:00 2001 From: SmilingPixel Date: Sun, 26 Apr 2026 02:23:22 +0000 Subject: [PATCH 06/19] feat(theme): improve theme mode selection UI in SettingsScreen --- .../smiling_pixel/screens/SettingsScreen.kt | 79 ++++++++++++------- 1 file changed, 50 insertions(+), 29 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/screens/SettingsScreen.kt b/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/screens/SettingsScreen.kt index c5bd251..7d4cf8f 100644 --- a/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/screens/SettingsScreen.kt +++ b/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/screens/SettingsScreen.kt @@ -16,9 +16,17 @@ import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.unit.dp import androidx.compose.ui.Alignment +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button -import androidx.compose.material3.RadioButton +import androidx.compose.material3.HorizontalDivider +import androidx.compose.ui.draw.clip import androidx.compose.foundation.layout.Row import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.OutlinedTextField @@ -90,57 +98,67 @@ fun SettingsScreen() { Column( modifier = Modifier .fillMaxSize() + .verticalScroll(rememberScrollState()) .padding(16.dp) ) { Text( text = "Settings", - style = MaterialTheme.typography.headlineMedium + style = MaterialTheme.typography.headlineMedium, + color = MaterialTheme.colorScheme.primary ) - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(32.dp)) /** * A section to allow the user to select their preferred Theme Mode. - * The user can select from SYSTEM, LIGHT, or DARK modes using a group of radio buttons. + * The user can select from SYSTEM, LIGHT, or DARK modes using a group of rounded rectangles. */ Text( text = "Appearance", - style = MaterialTheme.typography.titleLarge + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.primary ) - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(12.dp)) Row( modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically ) { io.github.smiling_pixel.theme.ThemeMode.entries.forEach { mode -> - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.clickable { - scope.launch { - settingsRepository.setThemeMode(mode) - } - }.padding(end = 16.dp) - ) { - RadioButton( - selected = themeMode == mode, - onClick = { + val isSelected = themeMode == mode + Box( + modifier = Modifier + .weight(1f) + .clip(RoundedCornerShape(12.dp)) + .clickable { scope.launch { settingsRepository.setThemeMode(mode) } } + .background(if (isSelected) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceVariant) + .then( + if (isSelected) Modifier.border(1.dp, MaterialTheme.colorScheme.primary, RoundedCornerShape(12.dp)) + else Modifier + ) + .padding(vertical = 12.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = mode.name.lowercase().replaceFirstChar { it.uppercase() }, + color = if (isSelected) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.labelLarge ) - Spacer(modifier = Modifier.width(4.dp)) - Text(text = mode.name.lowercase().replaceFirstChar { it.uppercase() }) } } } - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(32.dp)) Text( text = "Third-party Services", - style = MaterialTheme.typography.titleLarge + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.primary ) - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(12.dp)) OutlinedTextField( value = apiKey ?: "", onValueChange = { newKey -> @@ -153,13 +171,14 @@ fun SettingsScreen() { singleLine = true ) - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(32.dp)) Text( text = "Cloud Drive Sync", - style = MaterialTheme.typography.titleLarge + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.primary ) - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(12.dp)) errorMessage?.let { msg -> Text( @@ -286,20 +305,22 @@ fun SettingsScreen() { } } - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(32.dp)) Text( text = "About", - style = MaterialTheme.typography.titleLarge + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.primary ) - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(12.dp)) Text( text = "MarkDay Diary App v1.0.0", style = MaterialTheme.typography.bodyLarge ) Text( text = "A cross-platform diary application built with Kotlin Multiplatform and Compose.", - style = MaterialTheme.typography.bodyMedium + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant ) Spacer(modifier = Modifier.height(8.dp)) Text( From 33069a80d2ea4198b51b08c5269b80efbd5c0968 Mon Sep 17 00:00:00 2001 From: SmilingPixel Date: Mon, 27 Apr 2026 00:56:09 +0000 Subject: [PATCH 07/19] feat(theme): add pure black mode support for OLED screens in Settings --- .../preference/SettingsRepository.kt | 3 ++ .../smiling_pixel/screens/SettingsScreen.kt | 31 +++++++++++++++++++ .../io/github/smiling_pixel/theme/Color.kt | 5 +++ .../io/github/smiling_pixel/theme/Theme.kt | 29 ++++++++++++++++- 4 files changed, 67 insertions(+), 1 deletion(-) diff --git a/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/preference/SettingsRepository.kt b/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/preference/SettingsRepository.kt index eec03b0..b19820e 100644 --- a/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/preference/SettingsRepository.kt +++ b/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/preference/SettingsRepository.kt @@ -16,6 +16,9 @@ interface SettingsRepository { */ suspend fun setThemeMode(mode: ThemeMode) + val isPureBlackEnabled: Flow + suspend fun setPureBlackEnabled(enabled: Boolean) + val googleWeatherApiKey: Flow suspend fun setGoogleWeatherApiKey(key: String?) diff --git a/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/screens/SettingsScreen.kt b/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/screens/SettingsScreen.kt index 7d4cf8f..5401d69 100644 --- a/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/screens/SettingsScreen.kt +++ b/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/screens/SettingsScreen.kt @@ -29,6 +29,7 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.ui.draw.clip import androidx.compose.foundation.layout.Row import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Switch import androidx.compose.material3.OutlinedTextField import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -50,6 +51,7 @@ fun SettingsScreen() { val scope = rememberCoroutineScope() val settingsRepository = getSettingsRepository() val themeMode by settingsRepository.themeMode.collectAsState(initial = io.github.smiling_pixel.theme.ThemeMode.SYSTEM) + val isPureBlackEnabled by settingsRepository.isPureBlackEnabled.collectAsState(initial = false) val apiKey by settingsRepository.googleWeatherApiKey.collectAsState(initial = null) val uriHandler = LocalUriHandler.current @@ -151,6 +153,35 @@ fun SettingsScreen() { } } + Spacer(modifier = Modifier.height(16.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Column(modifier = Modifier.weight(1f).padding(end = 16.dp)) { + Text( + text = "Pure Black Mode", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface + ) + Text( + text = "Use pure black backgrounds in dark mode instead of dark gray, optimizing for OLED screens.", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + Switch( + checked = isPureBlackEnabled, + onCheckedChange = { isChecked -> + scope.launch { + settingsRepository.setPureBlackEnabled(isChecked) + } + } + ) + } + Spacer(modifier = Modifier.height(32.dp)) Text( diff --git a/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/theme/Color.kt b/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/theme/Color.kt index 277a247..b15a468 100644 --- a/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/theme/Color.kt +++ b/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/theme/Color.kt @@ -96,3 +96,8 @@ val onBackgroundDark = Color(0xFFE0E4DB) val surfaceDark = Color(0xFF10140F) /** Color used for text/icons on top of surfaces in the dark theme. */ val onSurfaceDark = Color(0xFFE0E4DB) + +/** Pure black background for OLED screens in the dark theme. */ +val backgroundPureBlack = Color(0xFF000000) +/** Pure black surface for OLED screens in the dark theme. */ +val surfacePureBlack = Color(0xFF000000) diff --git a/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/theme/Theme.kt b/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/theme/Theme.kt index bab66d0..c435323 100644 --- a/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/theme/Theme.kt +++ b/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/theme/Theme.kt @@ -58,6 +58,32 @@ private val DarkColorScheme = darkColorScheme( onSurface = onSurfaceDark ) +/** + * Material 3 dark color scheme for the application with pure black backgrounds for OLED screens. + */ +private val PureBlackColorScheme = darkColorScheme( + primary = primaryDark, + onPrimary = onPrimaryDark, + primaryContainer = primaryContainerDark, + onPrimaryContainer = onPrimaryContainerDark, + secondary = secondaryDark, + onSecondary = onSecondaryDark, + secondaryContainer = secondaryContainerDark, + onSecondaryContainer = onSecondaryContainerDark, + tertiary = tertiaryDark, + onTertiary = onTertiaryDark, + tertiaryContainer = tertiaryContainerDark, + onTertiaryContainer = onTertiaryContainerDark, + error = errorDark, + onError = onErrorDark, + errorContainer = errorContainerDark, + onErrorContainer = onErrorContainerDark, + background = backgroundPureBlack, + onBackground = onBackgroundDark, + surface = surfacePureBlack, + onSurface = onSurfaceDark +) + /** * The main theme for the MarkDay diary app. * @@ -71,10 +97,11 @@ private val DarkColorScheme = darkColorScheme( @Composable fun MarkDayTheme( useDarkTheme: Boolean = isSystemInDarkTheme(), + isPureBlack: Boolean = false, content: @Composable () -> Unit ) { val colorScheme = if (useDarkTheme) { - DarkColorScheme + if (isPureBlack) PureBlackColorScheme else DarkColorScheme } else { LightColorScheme } From acfb550e2510d2fe8aae166200f12ef1a11af2ee Mon Sep 17 00:00:00 2001 From: SmilingPixel Date: Tue, 28 Apr 2026 03:12:35 +0000 Subject: [PATCH 08/19] feat: add theme repo impl and apply pure dark theme --- .../commonMain/kotlin/io/github/smiling_pixel/App.kt | 6 +++++- .../preference/DataStoreSettingsRepository.kt | 12 ++++++++++++ .../preference/SettingsRepository.wasmJs.kt | 8 ++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/App.kt b/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/App.kt index 559c97f..dd5cccc 100644 --- a/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/App.kt +++ b/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/App.kt @@ -80,9 +80,13 @@ fun App( } val themeMode by getSettingsRepository().themeMode.collectAsState(initial = ThemeMode.SYSTEM) + val isPureBlackEnabled by getSettingsRepository().isPureBlackEnabled.collectAsState(initial = false) val useDarkTheme = themeMode == ThemeMode.DARK || (themeMode == ThemeMode.SYSTEM && isSystemInDarkTheme()) - MarkDayTheme(useDarkTheme = useDarkTheme) { + MarkDayTheme( + useDarkTheme = useDarkTheme, + isPureBlack = isPureBlackEnabled + ) { val repo = providedRepo ?: remember { DiaryRepository(InMemoryDiaryDao()) } val fileRepo = providedFileRepo ?: remember { FileRepository(InMemoryFileManager(), InMemoryFileMetadataDao()) diff --git a/composeApp/src/nonWebMain/kotlin/io/github/smiling_pixel/preference/DataStoreSettingsRepository.kt b/composeApp/src/nonWebMain/kotlin/io/github/smiling_pixel/preference/DataStoreSettingsRepository.kt index 6723abe..43cca0f 100644 --- a/composeApp/src/nonWebMain/kotlin/io/github/smiling_pixel/preference/DataStoreSettingsRepository.kt +++ b/composeApp/src/nonWebMain/kotlin/io/github/smiling_pixel/preference/DataStoreSettingsRepository.kt @@ -32,6 +32,7 @@ class DataStoreSettingsRepository(private val dataStore: DataStore) private val CLOUD_SYNC_PATH = stringPreferencesKey("cloud_sync_path") private val CLOUD_SYNC_DELETION_TOMBSTONES_JSON = stringPreferencesKey("cloud_sync_deletion_tombstones_json") private val THEME_MODE = stringPreferencesKey("theme_mode") + private val IS_PURE_BLACK_ENABLED = booleanPreferencesKey("is_pure_black_enabled") override val themeMode: Flow = dataStore.data .map { preferences -> @@ -50,6 +51,17 @@ class DataStoreSettingsRepository(private val dataStore: DataStore) } } + override val isPureBlackEnabled: Flow = dataStore.data + .map { preferences -> + preferences[IS_PURE_BLACK_ENABLED] ?: false + } + + override suspend fun setPureBlackEnabled(enabled: Boolean) { + dataStore.edit { preferences -> + preferences[IS_PURE_BLACK_ENABLED] = enabled + } + } + override val googleWeatherApiKey: Flow = dataStore.data .map { preferences -> preferences[WEATHER_API_KEY] diff --git a/composeApp/src/wasmJsMain/kotlin/io/github/smiling_pixel/preference/SettingsRepository.wasmJs.kt b/composeApp/src/wasmJsMain/kotlin/io/github/smiling_pixel/preference/SettingsRepository.wasmJs.kt index f2174a1..186e97f 100644 --- a/composeApp/src/wasmJsMain/kotlin/io/github/smiling_pixel/preference/SettingsRepository.wasmJs.kt +++ b/composeApp/src/wasmJsMain/kotlin/io/github/smiling_pixel/preference/SettingsRepository.wasmJs.kt @@ -22,6 +22,7 @@ class WasmJsSettingsRepository : SettingsRepository { } } ?: ThemeMode.SYSTEM ) + private val _isPureBlackEnabled = MutableStateFlow(localStorage.getItem("is_pure_black_enabled") == "true") override val themeMode: Flow = _themeMode.asStateFlow() @@ -30,6 +31,13 @@ class WasmJsSettingsRepository : SettingsRepository { _themeMode.value = mode } + override val isPureBlackEnabled: Flow = _isPureBlackEnabled.asStateFlow() + + override suspend fun setPureBlackEnabled(enabled: Boolean) { + localStorage.setItem("is_pure_black_enabled", enabled.toString()) + _isPureBlackEnabled.value = enabled + } + override val googleWeatherApiKey: Flow = _apiKey.asStateFlow() override suspend fun setGoogleWeatherApiKey(key: String?) { From f24296292c55f35314fedc0a9a057c15b422212e Mon Sep 17 00:00:00 2001 From: SmilingPixel Date: Thu, 30 Apr 2026 02:05:49 +0000 Subject: [PATCH 09/19] feat(theme): add theme mode and pure black mode settings to InMemorySettingsRepository Co-authored-by: Copilot --- .../github/smiling_pixel/sync/SyncManagerTest.kt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/composeApp/src/commonTest/kotlin/io/github/smiling_pixel/sync/SyncManagerTest.kt b/composeApp/src/commonTest/kotlin/io/github/smiling_pixel/sync/SyncManagerTest.kt index 64b78b3..97f51b5 100644 --- a/composeApp/src/commonTest/kotlin/io/github/smiling_pixel/sync/SyncManagerTest.kt +++ b/composeApp/src/commonTest/kotlin/io/github/smiling_pixel/sync/SyncManagerTest.kt @@ -325,12 +325,26 @@ class SyncManagerTest { } private class InMemorySettingsRepository : SettingsRepository { + private val themeModeState = MutableStateFlow(io.github.smiling_pixel.theme.ThemeMode.SYSTEM) + private val pureBlackEnabledState = MutableStateFlow(false) private val apiKeyState = MutableStateFlow(null) private val cloudSyncEnabledState = MutableStateFlow(false) private val autoSyncEnabledState = MutableStateFlow(false) private val cloudSyncPathState = MutableStateFlow("/MarkDay") private val cloudSyncDeletionTombstonesJsonState = MutableStateFlow(null) + override val themeMode: Flow = themeModeState.asStateFlow() + + override suspend fun setThemeMode(mode: io.github.smiling_pixel.theme.ThemeMode) { + themeModeState.value = mode + } + + override val isPureBlackEnabled: Flow = pureBlackEnabledState.asStateFlow() + + override suspend fun setPureBlackEnabled(enabled: Boolean) { + pureBlackEnabledState.value = enabled + } + override val googleWeatherApiKey: Flow = apiKeyState.asStateFlow() override suspend fun setGoogleWeatherApiKey(key: String?) { From 0de6f4fd634953d8c881793a7df2e09569f70b3c Mon Sep 17 00:00:00 2001 From: SmilingPixel Date: Fri, 1 May 2026 02:06:10 +0000 Subject: [PATCH 10/19] feat(settings): optimize settings repository instantiation with lazy initialization --- .../smiling_pixel/preference/SettingsRepository.wasmJs.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/composeApp/src/wasmJsMain/kotlin/io/github/smiling_pixel/preference/SettingsRepository.wasmJs.kt b/composeApp/src/wasmJsMain/kotlin/io/github/smiling_pixel/preference/SettingsRepository.wasmJs.kt index 186e97f..05ca44d 100644 --- a/composeApp/src/wasmJsMain/kotlin/io/github/smiling_pixel/preference/SettingsRepository.wasmJs.kt +++ b/composeApp/src/wasmJsMain/kotlin/io/github/smiling_pixel/preference/SettingsRepository.wasmJs.kt @@ -83,4 +83,6 @@ class WasmJsSettingsRepository : SettingsRepository { } } -actual fun getSettingsRepository(): SettingsRepository = WasmJsSettingsRepository() +private val repositoryInstance by lazy { WasmJsSettingsRepository() } + +actual fun getSettingsRepository(): SettingsRepository = repositoryInstance From f6950dc55e24855069fab70121b130c94e88fe1a Mon Sep 17 00:00:00 2001 From: SmilingPixel Date: Sat, 2 May 2026 01:18:33 +0000 Subject: [PATCH 11/19] feat(app): optimize settings repository usage with local reference --- .../src/commonMain/kotlin/io/github/smiling_pixel/App.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/App.kt b/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/App.kt index dd5cccc..2ddb784 100644 --- a/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/App.kt +++ b/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/App.kt @@ -79,8 +79,9 @@ fun App( getAsyncImageLoader(context) } - val themeMode by getSettingsRepository().themeMode.collectAsState(initial = ThemeMode.SYSTEM) - val isPureBlackEnabled by getSettingsRepository().isPureBlackEnabled.collectAsState(initial = false) + val settingsRepository = remember { getSettingsRepository() } + val themeMode by settingsRepository.themeMode.collectAsState(initial = ThemeMode.SYSTEM) + val isPureBlackEnabled by settingsRepository.isPureBlackEnabled.collectAsState(initial = false) val useDarkTheme = themeMode == ThemeMode.DARK || (themeMode == ThemeMode.SYSTEM && isSystemInDarkTheme()) MarkDayTheme( @@ -91,7 +92,7 @@ fun App( val fileRepo = providedFileRepo ?: remember { FileRepository(InMemoryFileManager(), InMemoryFileMetadataDao()) } - val weatherClient = remember { GoogleWeatherClient(getSettingsRepository()) } + val weatherClient = remember { GoogleWeatherClient(settingsRepository) } val scope = rememberCoroutineScope() val navController = rememberNavController() var selected by remember { mutableStateOf(EntriesRoute) } From 166b8abae19883fd2e206db8d5ff3b185e342146 Mon Sep 17 00:00:00 2001 From: SmilingPixel Date: Sun, 3 May 2026 01:39:02 +0000 Subject: [PATCH 12/19] feat(settings): enhance selectable group for theme options in SettingsScreen --- .../smiling_pixel/screens/SettingsScreen.kt | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/screens/SettingsScreen.kt b/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/screens/SettingsScreen.kt index 5401d69..8a1b09a 100644 --- a/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/screens/SettingsScreen.kt +++ b/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/screens/SettingsScreen.kt @@ -13,6 +13,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.dp import androidx.compose.ui.Alignment @@ -22,6 +23,8 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.selection.selectableGroup import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button @@ -121,7 +124,7 @@ fun SettingsScreen() { ) Spacer(modifier = Modifier.height(12.dp)) Row( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth().selectableGroup(), horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically ) { @@ -131,11 +134,15 @@ fun SettingsScreen() { modifier = Modifier .weight(1f) .clip(RoundedCornerShape(12.dp)) - .clickable { - scope.launch { - settingsRepository.setThemeMode(mode) + .selectable( + selected = isSelected, + role = Role.RadioButton, + onClick = { + scope.launch { + settingsRepository.setThemeMode(mode) + } } - } + ) .background(if (isSelected) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceVariant) .then( if (isSelected) Modifier.border(1.dp, MaterialTheme.colorScheme.primary, RoundedCornerShape(12.dp)) From 2ce4f180220fb4cc4c8345378902ada278e6cfa4 Mon Sep 17 00:00:00 2001 From: SmilingPixel Date: Mon, 4 May 2026 03:10:37 +0000 Subject: [PATCH 13/19] feat(theme): update app theme init to wait for saved preference values Co-authored-by: Copilot --- .../commonMain/kotlin/io/github/smiling_pixel/App.kt | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/App.kt b/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/App.kt index 2ddb784..7d3bb08 100644 --- a/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/App.kt +++ b/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/App.kt @@ -80,13 +80,18 @@ fun App( } val settingsRepository = remember { getSettingsRepository() } - val themeMode by settingsRepository.themeMode.collectAsState(initial = ThemeMode.SYSTEM) - val isPureBlackEnabled by settingsRepository.isPureBlackEnabled.collectAsState(initial = false) + val themeMode by settingsRepository.themeMode.collectAsState(initial = null) + val isPureBlackEnabled by settingsRepository.isPureBlackEnabled.collectAsState(initial = null) + + if (themeMode == null || isPureBlackEnabled == null) { + return + } + val useDarkTheme = themeMode == ThemeMode.DARK || (themeMode == ThemeMode.SYSTEM && isSystemInDarkTheme()) MarkDayTheme( useDarkTheme = useDarkTheme, - isPureBlack = isPureBlackEnabled + isPureBlack = isPureBlackEnabled!! ) { val repo = providedRepo ?: remember { DiaryRepository(InMemoryDiaryDao()) } val fileRepo = providedFileRepo ?: remember { From 095e6db5bbcfd9a8c5ad02bc9bba651a498eb096 Mon Sep 17 00:00:00 2001 From: SmilingPixel Date: Tue, 5 May 2026 02:17:00 +0000 Subject: [PATCH 14/19] feat(settings): update SettingsScreen to remember settings repository --- .../kotlin/io/github/smiling_pixel/screens/SettingsScreen.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/screens/SettingsScreen.kt b/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/screens/SettingsScreen.kt index 8a1b09a..b717795 100644 --- a/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/screens/SettingsScreen.kt +++ b/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/screens/SettingsScreen.kt @@ -52,7 +52,9 @@ import kotlinx.coroutines.launch @Composable fun SettingsScreen() { val scope = rememberCoroutineScope() - val settingsRepository = getSettingsRepository() + // Remember the settings repository so recomposition does not recreate a new DataStore-backed + // repository instance and resubscribe all mapped flows unnecessarily. + val settingsRepository = remember { getSettingsRepository() } val themeMode by settingsRepository.themeMode.collectAsState(initial = io.github.smiling_pixel.theme.ThemeMode.SYSTEM) val isPureBlackEnabled by settingsRepository.isPureBlackEnabled.collectAsState(initial = false) val apiKey by settingsRepository.googleWeatherApiKey.collectAsState(initial = null) From 64787ee590accceb55906ad599f01df679eba0fb Mon Sep 17 00:00:00 2001 From: SmilingPixel Date: Wed, 6 May 2026 00:55:32 +0000 Subject: [PATCH 15/19] docs(theme): update MarkDayTheme KDoc to document new parameters - Added documentation for the `isPureBlack` parameter. - Rephrased the description to clarify that it provides support for light, dark, and pure black color schemes, rather than only tying to system settings. --- .../commonMain/kotlin/io/github/smiling_pixel/theme/Theme.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/theme/Theme.kt b/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/theme/Theme.kt index c435323..5aafd22 100644 --- a/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/theme/Theme.kt +++ b/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/theme/Theme.kt @@ -87,11 +87,12 @@ private val PureBlackColorScheme = darkColorScheme( /** * The main theme for the MarkDay diary app. * - * This theme applies a green-based Material 3 color system to the application, adjusting - * appropriately for light and dark system settings. It wraps the standard `MaterialTheme` + * This theme applies a green-based Material 3 color system to the application, providing + * support for light, dark, and pure black color schemes. It wraps the standard `MaterialTheme` * composable from the Jetpack Compose Material 3 library. * * @param useDarkTheme Whether to use the dark theme color scheme. Defaults to system preference. + * @param isPureBlack Whether to use pure black backgrounds when in dark mode. Defaults to false. * @param content The composable content that will be styled by this theme. */ @Composable From 3022c2acce2f2e780fb45d537d2401287c85ae3a Mon Sep 17 00:00:00 2001 From: SmilingPixel Date: Thu, 7 May 2026 08:55:28 +0000 Subject: [PATCH 16/19] feat(app): wrap image loader factory setup in LaunchedEffect so that it only executes once Co-authored-by: Copilot --- .../src/commonMain/kotlin/io/github/smiling_pixel/App.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/App.kt b/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/App.kt index 7d3bb08..7562b93 100644 --- a/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/App.kt +++ b/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/App.kt @@ -75,8 +75,10 @@ fun App( providedRepo: io.github.smiling_pixel.database.DiaryRepository? = null, providedFileRepo: FileRepository? = null ) { - setSingletonImageLoaderFactory { context -> - getAsyncImageLoader(context) + LaunchedEffect(Unit) { + setSingletonImageLoaderFactory { context -> + getAsyncImageLoader(context) + } } val settingsRepository = remember { getSettingsRepository() } From ab320cbd7ef7739a5d9a3cec00216b0feec6f923 Mon Sep 17 00:00:00 2001 From: SmilingPixel Date: Thu, 7 May 2026 08:56:33 +0000 Subject: [PATCH 17/19] docs(settings): update comment style for theme mode selection section Co-authored-by: Copilot --- .../kotlin/io/github/smiling_pixel/screens/SettingsScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/screens/SettingsScreen.kt b/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/screens/SettingsScreen.kt index b717795..8c2a1b9 100644 --- a/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/screens/SettingsScreen.kt +++ b/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/screens/SettingsScreen.kt @@ -115,7 +115,7 @@ fun SettingsScreen() { ) Spacer(modifier = Modifier.height(32.dp)) - /** + /* * A section to allow the user to select their preferred Theme Mode. * The user can select from SYSTEM, LIGHT, or DARK modes using a group of rounded rectangles. */ From 2a821e11cf99b1100da8d2b02679ec0fe38678f0 Mon Sep 17 00:00:00 2001 From: SmilingPixel Date: Thu, 7 May 2026 08:59:09 +0000 Subject: [PATCH 18/19] refactor: clean import --- composeApp/src/commonMain/kotlin/io/github/smiling_pixel/App.kt | 2 -- .../kotlin/io/github/smiling_pixel/screens/SettingsScreen.kt | 1 - 2 files changed, 3 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/App.kt b/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/App.kt index 7562b93..275bfa9 100644 --- a/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/App.kt +++ b/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/App.kt @@ -10,7 +10,6 @@ import androidx.compose.material.icons.filled.AccountCircle import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Delete import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.CenterAlignedTopAppBar @@ -39,7 +38,6 @@ import io.github.smiling_pixel.filesystem.FileRepository import io.github.smiling_pixel.filesystem.InMemoryFileManager import io.github.smiling_pixel.database.InMemoryFileMetadataDao import io.github.smiling_pixel.client.GoogleWeatherClient -import io.github.smiling_pixel.client.WeatherClient import io.github.smiling_pixel.theme.ThemeMode import io.github.smiling_pixel.preference.getSettingsRepository import io.github.smiling_pixel.theme.MarkDayTheme diff --git a/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/screens/SettingsScreen.kt b/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/screens/SettingsScreen.kt index 8c2a1b9..84e1122 100644 --- a/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/screens/SettingsScreen.kt +++ b/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/screens/SettingsScreen.kt @@ -28,7 +28,6 @@ import androidx.compose.foundation.selection.selectableGroup import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button -import androidx.compose.material3.HorizontalDivider import androidx.compose.ui.draw.clip import androidx.compose.foundation.layout.Row import androidx.compose.material3.CircularProgressIndicator From fa1a13588bb3fbccaeea7d86162cef51c7913589 Mon Sep 17 00:00:00 2001 From: SmilingPixel Date: Fri, 8 May 2026 02:16:05 +0000 Subject: [PATCH 19/19] fix: optimize side effects and improve comment style - Wrap Coil image loader factory in `remember` to prevent re-initialization during recompositions (fixed compilation error from `LaunchedEffect`). - Convert KDoc-style block comment in `SettingsScreen` to a regular block comment to avoid incorrect tooling attribution. - Add documentation explaining the memoization of the image loader factory. --- .../commonMain/kotlin/io/github/smiling_pixel/App.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/App.kt b/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/App.kt index 275bfa9..ca71d59 100644 --- a/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/App.kt +++ b/composeApp/src/commonMain/kotlin/io/github/smiling_pixel/App.kt @@ -73,11 +73,13 @@ fun App( providedRepo: io.github.smiling_pixel.database.DiaryRepository? = null, providedFileRepo: FileRepository? = null ) { - LaunchedEffect(Unit) { - setSingletonImageLoaderFactory { context -> - getAsyncImageLoader(context) - } + // Memoize the image loader factory to prevent it from being re-created on every recomposition. + // While setSingletonImageLoaderFactory is a @Composable, we use remember here to ensure the + // factory lambda remains stable across theme changes and other UI updates. + val imageLoaderFactory = remember { + { context: coil3.PlatformContext -> getAsyncImageLoader(context) } } + setSingletonImageLoaderFactory(imageLoaderFactory) val settingsRepository = remember { getSettingsRepository() } val themeMode by settingsRepository.themeMode.collectAsState(initial = null)