Skip to content
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -2286,6 +2286,7 @@
| | | +--- org.jetbrains.androidx.core:core-bundle:1.0.1 (*)
| | | +--- org.jetbrains.androidx.navigation:navigation-compose:2.9.0-beta03 (*)
| | | +--- org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.8 (*)
| | | +--- project :core:common (*)
| | | +--- project :core:model (*)
| | | +--- org.jetbrains.compose.ui:ui:1.8.2 (*)
| | | +--- org.jetbrains.compose.foundation:foundation:1.8.2 (*)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ fun DarkThemeConfig.isDarkMode(
DarkThemeConfig.FOLLOW_SYSTEM -> isSystemDarkMode
DarkThemeConfig.DARK -> true
DarkThemeConfig.LIGHT -> false
DarkThemeConfig.BASED_ON_TIME -> false
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.mifos.core.common.DateHelper
import org.mifos.core.data.repository.UserDataRepository
import org.mifos.core.model.DarkThemeConfig
import org.mifos.core.model.LanguageConfig
import org.mifos.core.model.TimeBasedTheme
import template.core.base.platform.garbage.GarbageCollectionManager
import template.core.base.ui.BaseViewModel

Expand All @@ -32,9 +34,20 @@ class AppViewModel(
isAndroidTheme = false,
isDynamicColorsEnabled = false,
isScreenCaptureAllowed = false,
timeBasedTheme = TimeBasedTheme(
hourStart = 18,
hourEnd = 0,
minStart = 6,
minEnd = 0,
),
Comment thread
revanthkumarJ marked this conversation as resolved.
),
) {
init {
settingsRepository
.observeTimeBasedThemeConfig
.onEach { trySendAction(AppAction.Internal.TimeBasedThemeUpdate(it)) }
.launchIn(viewModelScope)

settingsRepository
.observeDarkThemeConfig
.onEach { trySendAction(AppAction.Internal.ThemeUpdate(it)) }
Expand Down Expand Up @@ -70,6 +83,8 @@ class AppViewModel(
is AppAction.Internal.CurrentUserStateChange -> handleCurrentUserStateChange()

is AppAction.Internal.UserUnlockStateChange -> handleUserUnlockStateChange()

is AppAction.Internal.TimeBasedThemeUpdate -> handleTimeBasedThemeUpdate(action)
}
}

Expand All @@ -84,8 +99,18 @@ class AppViewModel(
}

private fun handleAppThemeUpdated(action: AppAction.Internal.ThemeUpdate) {
val timeBased = state.timeBasedTheme
var darkTheme = action.theme == DarkThemeConfig.DARK
if (action.theme == DarkThemeConfig.BASED_ON_TIME) {
darkTheme = DateHelper.isDarkModeBasedOnTime(
timeBased.hourStart,
timeBased.minStart,
timeBased.hourEnd,
timeBased.minEnd,
)
}
mutableStateFlow.update {
it.copy(darkTheme = action.theme == DarkThemeConfig.DARK)
it.copy(darkTheme = darkTheme)
}
sendEvent(AppEvent.UpdateAppTheme(osValue = action.theme.osValue))
}
Expand All @@ -102,6 +127,14 @@ class AppViewModel(
recreateUiAndGarbageCollect()
}

private fun handleTimeBasedThemeUpdate(action: AppAction.Internal.TimeBasedThemeUpdate) {
mutableStateFlow.update {
it.copy(
timeBasedTheme = action.theme,
)
}
}

private fun recreateUiAndGarbageCollect() {
sendEvent(AppEvent.Recreate)
garbageCollectionManager.tryCollect()
Expand All @@ -113,6 +146,7 @@ data class AppState(
val isAndroidTheme: Boolean,
val isDynamicColorsEnabled: Boolean,
val isScreenCaptureAllowed: Boolean,
val timeBasedTheme: TimeBasedTheme,
)

sealed interface AppEvent {
Expand Down Expand Up @@ -144,6 +178,10 @@ sealed interface AppAction {
val theme: DarkThemeConfig,
) : Internal()

data class TimeBasedThemeUpdate(
val theme: TimeBasedTheme,
) : Internal()

data object UserUnlockStateChange : Internal()

data class DynamicColorsUpdate(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package org.mifos.core.common

import kotlinx.datetime.Clock
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import kotlin.time.ExperimentalTime

object DateHelper {

@OptIn(ExperimentalTime::class)
fun isDarkModeBasedOnTime(
startHour: Int,
startMinute: Int,
endHour: Int,
endMinute: Int,
): Boolean {
val now = Clock.System.now()
.toLocalDateTime(TimeZone.currentSystemDefault())
.time

val currentMinutes = now.hour * 60 + now.minute
val startMinutes = startHour * 60 + startMinute
val endMinutes = endHour * 60 + endMinute

return if (startMinutes < endMinutes) {
// Same-day range (e.g., 06:00 → 18:00)
currentMinutes in startMinutes until endMinutes
} else {
// Cross-midnight range (e.g., 18:00 → 06:00)
currentMinutes !in endMinutes..<startMinutes
}
}

fun formatTimeRange(
startHour: Int,
startMinute: Int,
endHour: Int,
endMinute: Int,
): String {
return "${format(startHour, startMinute)} - ${format(endHour, endMinute)}"
}

fun format(hour: Int, minute: Int): String {
val period = if (hour < 12) "AM" else "PM"
val hour12 = when (hour % 12) {
0 -> 12
else -> hour % 12
}
return "$hour12:${minute.toString().padStart(2, '0')} $period"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import kotlinx.coroutines.flow.StateFlow
import org.mifos.core.model.DarkThemeConfig
import org.mifos.core.model.LanguageConfig
import org.mifos.core.model.ThemeBrand
import org.mifos.core.model.TimeBasedTheme
import org.mifos.core.model.UserData

/**
Expand All @@ -35,6 +36,8 @@ interface UserDataRepository {

val observeDarkThemeConfig: Flow<DarkThemeConfig>

val observeTimeBasedThemeConfig: Flow<TimeBasedTheme>

val observeDynamicColorPreference: Flow<Boolean>

val observeScreenCapturePreference: Flow<Boolean>
Expand All @@ -45,6 +48,8 @@ interface UserDataRepository {

suspend fun setDarkThemeConfig(darkThemeConfig: DarkThemeConfig)

suspend fun setTimeBasedThemeConfig(timeBasedThemeConfig: TimeBasedTheme)

suspend fun setDynamicColorPreference(useDynamicColor: Boolean)

suspend fun setIsAuthenticated(isAuthenticated: Boolean)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import org.mifos.core.datastore.UserPreferencesRepository
import org.mifos.core.model.DarkThemeConfig
import org.mifos.core.model.LanguageConfig
import org.mifos.core.model.ThemeBrand
import org.mifos.core.model.TimeBasedTheme
import org.mifos.core.model.UserData

class UserDataRepositoryImpl(
Expand All @@ -36,6 +37,9 @@ class UserDataRepositoryImpl(
override val observeDarkThemeConfig: Flow<DarkThemeConfig>
get() = preferencesRepository.observeDarkThemeConfig

override val observeTimeBasedThemeConfig: Flow<TimeBasedTheme>
get() = preferencesRepository.observeTimeBasedThemeConfig

override val observeDynamicColorPreference: Flow<Boolean>
get() = preferencesRepository.observeDynamicColorPreference

Expand All @@ -49,6 +53,9 @@ class UserDataRepositoryImpl(
override suspend fun setDarkThemeConfig(darkThemeConfig: DarkThemeConfig) =
preferencesRepository.setDarkThemeConfig(darkThemeConfig)

override suspend fun setTimeBasedThemeConfig(timeBasedThemeConfig: TimeBasedTheme) =
preferencesRepository.setTimeBasedThemeConfig(timeBasedThemeConfig)

override suspend fun setDynamicColorPreference(useDynamicColor: Boolean) =
preferencesRepository.setDynamicColorPreference(useDynamicColor)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import kotlinx.coroutines.flow.StateFlow
import org.mifos.core.model.DarkThemeConfig
import org.mifos.core.model.LanguageConfig
import org.mifos.core.model.ThemeBrand
import org.mifos.core.model.TimeBasedTheme
import org.mifos.core.model.UserData

/**
Expand All @@ -35,6 +36,8 @@ interface UserPreferencesRepository {

val observeDarkThemeConfig: Flow<DarkThemeConfig>

val observeTimeBasedThemeConfig: Flow<TimeBasedTheme>

val observeDynamicColorPreference: Flow<Boolean>

val observeScreenCapturePreference: Flow<Boolean>
Expand All @@ -45,6 +48,10 @@ interface UserPreferencesRepository {

suspend fun setDarkThemeConfig(darkThemeConfig: DarkThemeConfig)

suspend fun setTimeBasedThemeConfig(timeBasedTheme: TimeBasedTheme)

suspend fun updateTimeBasedTheme(theme: TimeBasedTheme)
Comment thread
revanthkumarJ marked this conversation as resolved.
Outdated

suspend fun setDynamicColorPreference(useDynamicColor: Boolean)

suspend fun setIsAuthenticated(isAuthenticated: Boolean)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import kotlinx.serialization.ExperimentalSerializationApi
import org.mifos.core.model.DarkThemeConfig
import org.mifos.core.model.LanguageConfig
import org.mifos.core.model.ThemeBrand
import org.mifos.core.model.TimeBasedTheme
import org.mifos.core.model.UserData
import template.core.base.common.manager.DispatcherManager

Expand Down Expand Up @@ -62,6 +63,9 @@ class UserPreferencesRepositoryImpl(
override val observeDarkThemeConfig: Flow<DarkThemeConfig>
get() = _userData.map { it.darkThemeConfig }

override val observeTimeBasedThemeConfig: Flow<TimeBasedTheme>
get() = _userData.map { it.timeBasedTheme }

override val observeDynamicColorPreference: Flow<Boolean>
get() = _userData.map { it.useDynamicColor }

Expand Down Expand Up @@ -89,6 +93,21 @@ class UserPreferencesRepositoryImpl(
_userData.value = newPreference
}

override suspend fun setTimeBasedThemeConfig(timeBasedTheme: TimeBasedTheme) {
withContext(dispatcher.io) {
val newPreference = settings.getUserPreference().copy(timeBasedTheme = timeBasedTheme)
settings.putUserPreference(newPreference)
_userData.value = newPreference
}
}

override suspend fun updateTimeBasedTheme(theme: TimeBasedTheme) =
withContext(dispatcher.io) {
val newPreference = settings.getUserPreference().copy(timeBasedTheme = theme)
settings.putUserPreference(newPreference)
_userData.value = newPreference
}
Comment thread
revanthkumarJ marked this conversation as resolved.
Outdated

override suspend fun setDynamicColorPreference(useDynamicColor: Boolean) =
withContext(dispatcher.io) {
val newPreference = settings.getUserPreference().copy(useDynamicColor = useDynamicColor)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ enum class DarkThemeConfig(val configName: String, val osValue: Int) {
FOLLOW_SYSTEM("Follow System", -1),
LIGHT("Light", 1),
DARK("Dark", 2),
BASED_ON_TIME("Based on Time", 3),
;

companion object {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package org.mifos.core.model

import kotlinx.serialization.Serializable

@Serializable
data class TimeBasedTheme(
val hourStart: Int,
val hourEnd: Int,
val minStart: Int,
val minEnd: Int,
)
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ data class UserData(
val enableScreenCapture: Boolean,
val isPasscodeEnabled: Boolean,
val isBiometricsEnabled: Boolean,
val timeBasedTheme: TimeBasedTheme,
) {
companion object {
val DEFAULT = UserData(
Expand All @@ -41,7 +42,13 @@ data class UserData(
isBiometricsEnabled = false,
showOnboarding = false,
firstTimeUser = false,
enableScreenCapture = false,
enableScreenCapture = true,
timeBasedTheme = TimeBasedTheme(
hourStart = 18,
hourEnd = 6,
minStart = 0,
minEnd = 0,
),
)
}
}
1 change: 1 addition & 0 deletions feature/settings/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ kotlin {
sourceSets {
commonMain.dependencies {
implementation(projects.core.data)
implementation(projects.core.common)
implementation(projects.core.model)
implementation(projects.coreBase.ui)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,15 @@
<string name="feature_settings_dismiss_dialog_button_text">OK</string>
<string name="feature_settings_change_theme_text">Change Theme</string>
<string name="feature_settings_change_theme_placeholder_text">Change Theme</string>

<string name="feature_settings_dark_mode_based_on_time">Based on Time</string>
<string name="feature_settings_dark_mode_label">Dark mode</string>
<string name="feature_settings_light_mode_label">Light mode</string>

<string name="feature_settings_choose_dark_mode_time">Choose Dark Mode Time</string>
<string name="feature_settings_dark_mode_starts_at">Dark mode starts at</string>
<string name="feature_settings_dark_mode_ends_at">Dark mode ends at</string>
<string name="feature_settings_apply_theme">Apply Theme</string>
<string name="feature_settings_cancel">Cancel</string>
<string name="feature_settings_ok">Ok</string>
</resources>
Loading
Loading