Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 0 additions & 1 deletion desktopApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ compose.desktop {
description = "Desktop client for Tempo Timesheets"
copyright = "Copyright (c) 2025 Matej Semančík"
vendor = "matsem.dev"
licenseFile.set(rootProject.file("LICENSE"))

// https://github.com/JetBrains/compose-multiplatform/issues/2686
modules("jdk.unsupported")
Expand Down
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ androidx-room = "2.7.0-rc01"
androidx-sqlite = "2.5.0-alpha13"
appDirs = "1.3.0"
markdownRenderer = "0.31.0"
platformTools = "0.2.9"

[libraries]
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
Expand Down Expand Up @@ -51,6 +52,7 @@ coil-svg = { module = "io.coil-kt.coil3:coil-svg", version.ref = "coil" }

appDirs = { module = "net.harawata:appdirs", version.ref = "appDirs" }
markdownRenderer = { module = "com.mikepenz:multiplatform-markdown-renderer-m3", version.ref = "markdownRenderer" }
platformTools-darkModeDetector = { module = "io.github.kdroidfilter:platformtools.darkmodedetector", version.ref = "platformTools" }

[plugins]
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" }
Expand Down
3 changes: 3 additions & 0 deletions shared/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ kotlin {
// database
implementation(libs.androidx.room.runtime)
implementation(libs.androidx.sqlite.bundled)

// other
implementation(libs.platformTools.darkModeDetector)
}

commonTest.dependencies {
Expand Down
5 changes: 4 additions & 1 deletion shared/src/commonMain/composeResources/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@
<string name="log_time">Log Time</string>

<!-- Settings -->
<string name="settings_app_section_title">⚙️ App</string>
<string name="settings_theme_section_title">🎨 Theme</string>
<string name="app_theme_system">System</string>
<string name="app_theme_light">Light</string>
<string name="app_theme_dark">Dark</string>
<string name="settings_account_section_title">👤 Account</string>
<string name="settings_credentials_section_title">🔐 Sign In</string>
<string name="credentials_description">Sign In by providing necessary credentials.</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ interface ApplicationPersistence {

suspend fun deleteCredentials()

suspend fun saveDarkMode(darkMode: Boolean)

fun observeDarkMode(): Flow<Boolean?>

suspend fun clearDarkMode()

suspend fun clear()
}

Expand All @@ -23,6 +29,7 @@ internal class ApplicationPersistenceImpl(

companion object {
private val CredentialsKey = stringPreferencesKey("credentials")
private val DarkModeKey = stringPreferencesKey("dark_mode")
}

override suspend fun saveCredentials(credentials: Credentials) = handler.save(CredentialsKey, credentials)
Expand All @@ -33,5 +40,11 @@ internal class ApplicationPersistenceImpl(

override suspend fun deleteCredentials() = handler.delete(CredentialsKey)

override suspend fun saveDarkMode(darkMode: Boolean) = handler.save(DarkModeKey, darkMode)

override suspend fun clearDarkMode() = handler.delete(DarkModeKey)

override fun observeDarkMode(): Flow<Boolean?> = handler.observe(DarkModeKey)

override suspend fun clear() = handler.clear()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package dev.matsem.bpm.data.repo

import dev.matsem.bpm.data.persistence.ApplicationPersistence
import dev.matsem.bpm.data.repo.model.AppThemeMode
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map

interface PreferenceRepo {
suspend fun saveAppThemeMode(mode: AppThemeMode)
fun observeAppThemeMode(): Flow<AppThemeMode>
}

internal class PreferenceRepoImpl(
private val applicationPersistence: ApplicationPersistence,
) : PreferenceRepo {

override suspend fun saveAppThemeMode(mode: AppThemeMode) = when (mode) {
AppThemeMode.SYSTEM -> applicationPersistence.clearDarkMode()
AppThemeMode.DARK -> applicationPersistence.saveDarkMode(true)
AppThemeMode.LIGHT -> applicationPersistence.saveDarkMode(false)
}

override fun observeAppThemeMode(): Flow<AppThemeMode> = applicationPersistence.observeDarkMode().map { isDark ->
when (isDark) {
null -> AppThemeMode.SYSTEM
true -> AppThemeMode.DARK
false -> AppThemeMode.LIGHT
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package dev.matsem.bpm.data.repo.model

enum class AppThemeMode {
SYSTEM,
LIGHT,
DARK
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package dev.matsem.bpm.design.chip

import androidx.compose.material3.FilterChip
import androidx.compose.material3.FilterChipDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import dev.matsem.bpm.design.theme.BpmTheme

@Composable
fun AppFilterChip(
selected: Boolean,
label: String,
leadingIcon: (@Composable () -> Unit)? = null,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
FilterChip(
selected = selected,
modifier = modifier,
onClick = onClick,
label = { Text(text = label) },
shape = BpmTheme.shapes.small,
colors = FilterChipDefaults.filterChipColors(
containerColor = BpmTheme.colorScheme.surfaceContainer
),
border = null,
leadingIcon = leadingIcon,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package dev.matsem.bpm.feature.app.presentation
import dev.matsem.bpm.arch.BaseModel
import dev.matsem.bpm.data.operation.UndoStack
import dev.matsem.bpm.data.repo.GitHubRepo
import dev.matsem.bpm.data.repo.PreferenceRepo
import dev.matsem.bpm.data.repo.model.Timer
import dev.matsem.bpm.design.navigation.NavigationBarItem
import dev.matsem.bpm.tooling.Platform
Expand All @@ -14,12 +15,14 @@ internal class AppWindowModel(
private val gitHubRepo: GitHubRepo,
private val platform: Platform,
private val undoStack: UndoStack,
private val preferenceRepo: PreferenceRepo,
) : BaseModel<AppWindowState, Nothing>(
defaultState = AppWindowState(newVersionBannerVisible = false)
), AppWindow, KoinComponent {

init {
override suspend fun onStart() {
checkForUpdates()
observeAppThemeMode()
}

private fun checkForUpdates() {
Expand All @@ -39,6 +42,14 @@ internal class AppWindowModel(
}
}

private fun observeAppThemeMode() = coroutineScope.launch {
preferenceRepo.observeAppThemeMode().collect { mode ->
updateState { state ->
state.copy(themeMode = mode)
}
}
}

override val actions: AppWindowActions = object : AppWindowActions {

override fun onNavigationBarClick(item: NavigationBarItem) = updateState { state ->
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dev.matsem.bpm.feature.app.presentation

import dev.matsem.bpm.data.repo.model.AppThemeMode
import dev.matsem.bpm.data.repo.model.AppVersion
import dev.matsem.bpm.data.repo.model.Timer
import dev.matsem.bpm.design.navigation.NavigationBarItem
Expand All @@ -13,6 +14,7 @@ data class AppWindowState(
content = AppWindowContent.Timer,
sheet = null
),
val themeMode: AppThemeMode = AppThemeMode.SYSTEM,
) {
val navigationItems: ImmutableList<NavigationBarItem>
get() = persistentListOf(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,37 +8,27 @@ import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Add
import androidx.compose.material.icons.rounded.DarkMode
import androidx.compose.material.icons.rounded.LightMode
import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.BottomAppBarDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.FloatingActionButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.input.key.Key
Expand All @@ -53,7 +43,7 @@ import bpm_tracker.shared.generated.resources.app_name
import bpm_tracker.shared.generated.resources.new_timer
import bpm_tracker.shared.generated.resources.pick_issue
import bpm_tracker.shared.generated.resources.timer
import bpm_tracker.shared.generated.resources.toggle_dark_mode
import dev.matsem.bpm.data.repo.model.AppThemeMode
import dev.matsem.bpm.design.navigation.BottomNavigationBar
import dev.matsem.bpm.design.sheet.GenericModalBottomSheet
import dev.matsem.bpm.design.sheet.SheetHeader
Expand All @@ -71,6 +61,7 @@ import dev.matsem.bpm.feature.settings.ui.SettingsScreenUi
import dev.matsem.bpm.feature.tracker.presentation.TrackerScreen
import dev.matsem.bpm.feature.tracker.ui.TrackerScreenUi
import dev.matsem.bpm.tooling.Platform
import io.github.kdroidfilter.platformtools.darkmodedetector.isSystemInDarkMode
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.stringResource
import org.jetbrains.compose.ui.tooling.preview.Preview
Expand All @@ -86,14 +77,18 @@ fun AppWindowUi(
val state by window.state.collectAsStateWithLifecycle()
val actions = window.actions

val isSystemInDarkTheme = isSystemInDarkTheme() // Stores initial state of dark mode and stores in [darkMode] state.
var darkMode by remember { mutableStateOf(isSystemInDarkTheme) }
val focusManager = LocalFocusManager.current

val trackerScreen: TrackerScreen = koinInject()
val platform: Platform = koinInject()

BpmTheme(isDark = darkMode) {
val isDarkTheme = when (state.themeMode) {
AppThemeMode.SYSTEM -> isSystemInDarkMode()
AppThemeMode.LIGHT -> false
AppThemeMode.DARK -> true
}

BpmTheme(isDark = isDarkTheme) {
Scaffold(
modifier = Modifier.onPreviewKeyEvent { keyEvent ->
// Let user use arrow keys on the main screen in addition to TAB key for focusing elements
Expand Down Expand Up @@ -145,14 +140,6 @@ fun AppWindowUi(
items = state.navigationItems,
onClick = actions::onNavigationBarClick
)
IconButton(
onClick = { darkMode = !darkMode })
{
Icon(
if (darkMode) Icons.Rounded.LightMode else Icons.Rounded.DarkMode,
contentDescription = stringResource(Res.string.toggle_dark_mode),
)
}
Text(
"${stringResource(Res.string.app_name)} (${platform.getVersionString()})",
style = BpmTheme.typography.labelMedium,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package dev.matsem.bpm.feature.settings.presentation

import androidx.compose.ui.text.input.TextFieldValue
import dev.matsem.bpm.data.repo.model.AppThemeMode

interface SettingsActions {
fun onAppThemeModeClick(mode: AppThemeMode)

fun onJiraCloudName(input: TextFieldValue)
fun onJiraEmailInput(input: TextFieldValue)
fun onJiraApiKeyInput(input: TextFieldValue)
Expand All @@ -13,6 +16,7 @@ interface SettingsActions {

companion object {
fun noOp() = object : SettingsActions {
override fun onAppThemeModeClick(mode: AppThemeMode) = Unit
override fun onJiraCloudName(input: TextFieldValue) = Unit
override fun onJiraEmailInput(input: TextFieldValue) = Unit
override fun onJiraApiKeyInput(input: TextFieldValue) = Unit
Expand All @@ -21,4 +25,4 @@ interface SettingsActions {
override fun onLogoutClick() = Unit
}
}
}
}
Loading
Loading