Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
ca66306
feat(theme): add light and dark color schemes with theme mode support
SmilingPixel Apr 21, 2026
a3a652c
feat(theme): add theme mode selection to settings screen
SmilingPixel Apr 22, 2026
daa4ed9
feat(theme): implement theme mode management in DataStore and WasmJs …
SmilingPixel Apr 23, 2026
e82ff21
feat(ui): update EntryDetailsScreen and MomentsScreen theme
SmilingPixel Apr 24, 2026
6e673a8
feat(theme): integrate theme mode selection into App component
SmilingPixel Apr 25, 2026
4ff46ef
feat(theme): improve theme mode selection UI in SettingsScreen
SmilingPixel Apr 26, 2026
33069a8
feat(theme): add pure black mode support for OLED screens in Settings
SmilingPixel Apr 27, 2026
acfb550
feat: add theme repo impl and apply pure dark theme
SmilingPixel Apr 28, 2026
f242962
feat(theme): add theme mode and pure black mode settings to InMemoryS…
SmilingPixel Apr 30, 2026
0de6f4f
feat(settings): optimize settings repository instantiation with lazy …
SmilingPixel May 1, 2026
f6950dc
feat(app): optimize settings repository usage with local reference
SmilingPixel May 2, 2026
166b8ab
feat(settings): enhance selectable group for theme options in Setting…
SmilingPixel May 3, 2026
2ce4f18
feat(theme): update app theme init to wait for saved preference values
SmilingPixel May 4, 2026
095e6db
feat(settings): update SettingsScreen to remember settings repository
SmilingPixel May 5, 2026
64787ee
docs(theme): update MarkDayTheme KDoc to document new parameters
SmilingPixel May 6, 2026
3022c2a
feat(app): wrap image loader factory setup in LaunchedEffect so that …
SmilingPixel May 7, 2026
ab320cb
docs(settings): update comment style for theme mode selection section
SmilingPixel May 7, 2026
2a821e1
refactor: clean import
SmilingPixel May 7, 2026
fa1a135
fix: optimize side effects and improve comment style
SmilingPixel May 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 25 additions & 6 deletions composeApp/src/commonMain/kotlin/io/github/smiling_pixel/App.kt
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -9,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
Expand All @@ -18,6 +18,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
Expand All @@ -37,8 +38,9 @@ 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
Comment thread
SmilingPixel marked this conversation as resolved.
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
Expand Down Expand Up @@ -71,16 +73,33 @@ fun App(
providedRepo: io.github.smiling_pixel.database.DiaryRepository? = null,
providedFileRepo: FileRepository? = null
) {
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)

MaterialTheme {
val settingsRepository = remember { getSettingsRepository() }
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!!
Comment on lines +85 to +96
) {
val repo = providedRepo ?: remember { DiaryRepository(InMemoryDiaryDao()) }
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<AppRoute>(EntriesRoute) }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,24 @@
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<ThemeMode>

/**
* Updates the application's theme mode setting.
* @param mode The new [ThemeMode] to be applied.
*/
suspend fun setThemeMode(mode: ThemeMode)

val isPureBlackEnabled: Flow<Boolean>
suspend fun setPureBlackEnabled(enabled: Boolean)
Comment thread
SmilingPixel marked this conversation as resolved.

val googleWeatherApiKey: Flow<String?>
suspend fun setGoogleWeatherApiKey(key: String?)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,23 @@ 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
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(
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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))
Expand All @@ -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))
Expand All @@ -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))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,25 @@ 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
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.selection.selectable
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.ui.draw.clip
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Switch
Comment thread
SmilingPixel marked this conversation as resolved.
import androidx.compose.material3.OutlinedTextField
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
Expand All @@ -37,7 +51,11 @@ 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)
val uriHandler = LocalUriHandler.current

Expand Down Expand Up @@ -86,19 +104,100 @@ 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 rounded rectangles.
*/
Text(
text = "Appearance",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.height(12.dp))
Row(
modifier = Modifier.fillMaxWidth().selectableGroup(),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
io.github.smiling_pixel.theme.ThemeMode.entries.forEach { mode ->
val isSelected = themeMode == mode
Box(
modifier = Modifier
.weight(1f)
.clip(RoundedCornerShape(12.dp))
.selectable(
selected = isSelected,
role = Role.RadioButton,
onClick = {
scope.launch {
settingsRepository.setThemeMode(mode)
}
}
)
.background(if (isSelected) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceVariant)
Comment thread
SmilingPixel marked this conversation as resolved.
.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.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(
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 ->
Expand All @@ -111,13 +210,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(
Expand Down Expand Up @@ -244,20 +344,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(
Expand Down
Loading
Loading