diff --git a/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt b/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt index 94d8900..dfdf04c 100644 --- a/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt +++ b/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt @@ -5,19 +5,23 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme.colorScheme import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.nsh07.pomodoro.ui.AppScreen import org.nsh07.pomodoro.ui.NavItem import org.nsh07.pomodoro.ui.Screen -import org.nsh07.pomodoro.ui.statsScreen.viewModel.StatsViewModel +import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsViewModel import org.nsh07.pomodoro.ui.theme.TomatoTheme import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerViewModel +import org.nsh07.pomodoro.utils.toColor class MainActivity : ComponentActivity() { private val timerViewModel: TimerViewModel by viewModels(factoryProducer = { TimerViewModel.Factory }) - private val statsViewModel: StatsViewModel by viewModels(factoryProducer = { StatsViewModel.Factory }) + private val settingsViewModel: SettingsViewModel by viewModels(factoryProducer = { SettingsViewModel.Factory }) private val appContainer by lazy { (application as TomatoApplication).container @@ -27,14 +31,27 @@ class MainActivity : ComponentActivity() { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { - TomatoTheme { + val preferencesState by settingsViewModel.preferencesState.collectAsStateWithLifecycle() + + val darkTheme = when (preferencesState.theme) { + "dark" -> true + "light" -> false + else -> isSystemInDarkTheme() + } + + val seed = preferencesState.colorScheme.toColor() + + TomatoTheme( + darkTheme = darkTheme, + seedColor = seed, + blackTheme = preferencesState.blackTheme + ) { val colorScheme = colorScheme LaunchedEffect(colorScheme) { appContainer.appTimerRepository.colorScheme = colorScheme } - timerViewModel.setCompositionLocals(colorScheme) - AppScreen(timerViewModel = timerViewModel, statsViewModel = statsViewModel) + AppScreen(timerViewModel = timerViewModel) } } } diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt b/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt index 0a1e0d9..f2a578d 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt @@ -46,7 +46,6 @@ import org.nsh07.pomodoro.MainActivity.Companion.screens import org.nsh07.pomodoro.service.TimerService import org.nsh07.pomodoro.ui.settingsScreen.SettingsScreenRoot import org.nsh07.pomodoro.ui.statsScreen.StatsScreenRoot -import org.nsh07.pomodoro.ui.statsScreen.viewModel.StatsViewModel import org.nsh07.pomodoro.ui.timerScreen.AlarmDialog import org.nsh07.pomodoro.ui.timerScreen.TimerScreen import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerAction @@ -56,8 +55,7 @@ import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerViewModel @Composable fun AppScreen( modifier: Modifier = Modifier, - timerViewModel: TimerViewModel = viewModel(factory = TimerViewModel.Factory), - statsViewModel: StatsViewModel = viewModel(factory = StatsViewModel.Factory) + timerViewModel: TimerViewModel = viewModel(factory = TimerViewModel.Factory) ) { val context = LocalContext.current @@ -194,7 +192,6 @@ fun AppScreen( entry { StatsScreenRoot( contentPadding = contentPadding, - viewModel = statsViewModel, modifier = modifier.padding( start = contentPadding.calculateStartPadding(layoutDirection), end = contentPadding.calculateEndPadding(layoutDirection), diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/PreferencesState.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/PreferencesState.kt new file mode 100644 index 0000000..25a3374 --- /dev/null +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/PreferencesState.kt @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2025 Nishant Mishra + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.nsh07.pomodoro.ui.settingsScreen.viewModel + +import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.Color + +@Immutable +data class PreferencesState( + val theme: String = "system", + val colorScheme: String = Color.White.toString(), + val blackTheme: Boolean = false +) diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt index dc2439d..25c66ac 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.SliderState import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.graphics.Color import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY @@ -20,8 +21,11 @@ import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.viewModelFactory import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.nsh07.pomodoro.TomatoApplication import org.nsh07.pomodoro.data.AppPreferenceRepository @@ -32,6 +36,9 @@ class SettingsViewModel( private val preferenceRepository: AppPreferenceRepository, private val timerRepository: TimerRepository ) : ViewModel() { + private val _preferencesState = MutableStateFlow(PreferencesState()) + val preferencesState = _preferencesState.asStateFlow() + val focusTimeTextFieldState = TextFieldState((timerRepository.focusTime / 60000).toString()) val shortBreakTimeTextFieldState = @@ -56,6 +63,22 @@ class SettingsViewModel( preferenceRepository.getBooleanPreferenceFlow("vibrate_enabled").distinctUntilChanged() init { + viewModelScope.launch { + val theme = preferenceRepository.getStringPreference("theme") + ?: preferenceRepository.saveStringPreference("theme", "system") + val colorScheme = preferenceRepository.getStringPreference("color_scheme") + ?: preferenceRepository.saveStringPreference("color_scheme", Color.White.toString()) + val blackTheme = preferenceRepository.getBooleanPreference("black_theme") + ?: preferenceRepository.saveBooleanPreference("black_theme", false) + + _preferencesState.update { currentState -> + currentState.copy( + theme = theme, + colorScheme = colorScheme, + blackTheme = blackTheme + ) + } + } viewModelScope.launch(Dispatchers.IO) { snapshotFlow { focusTimeTextFieldState.text } .debounce(500) diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/theme/Theme.kt b/app/src/main/java/org/nsh07/pomodoro/ui/theme/Theme.kt index af90563..97ca9b2 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/theme/Theme.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/theme/Theme.kt @@ -2,13 +2,18 @@ package org.nsh07.pomodoro.ui.theme import android.os.Build import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.MaterialExpressiveTheme +import androidx.compose.material3.MotionScheme import androidx.compose.material3.darkColorScheme import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import com.materialkolor.dynamiccolor.ColorSpec +import com.materialkolor.rememberDynamicColorScheme private val DarkColorScheme = darkColorScheme( primary = Purple80, @@ -32,11 +37,13 @@ private val LightColorScheme = lightColorScheme( */ ) +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun TomatoTheme( darkTheme: Boolean = isSystemInDarkTheme(), - // Dynamic color is available on Android 12+ + seedColor: Color = Color.White, dynamicColor: Boolean = true, + blackTheme: Boolean = false, content: @Composable () -> Unit ) { val colorScheme = when { @@ -49,9 +56,24 @@ fun TomatoTheme( else -> LightColorScheme } - MaterialTheme( - colorScheme = colorScheme, + val dynamicColorScheme = rememberDynamicColorScheme( + seedColor = when (seedColor) { + Color.White -> colorScheme.primary + else -> seedColor + }, + isDark = darkTheme, + specVersion = if (blackTheme && darkTheme) ColorSpec.SpecVersion.SPEC_2021 else ColorSpec.SpecVersion.SPEC_2025, + isAmoled = blackTheme && darkTheme + ) + + val scheme = + if (seedColor == Color.White && !(blackTheme && darkTheme)) colorScheme + else dynamicColorScheme + + MaterialExpressiveTheme( + colorScheme = scheme, typography = Typography, + motionScheme = MotionScheme.expressive(), content = content ) } \ No newline at end of file diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt index 71cb79f..5b88b45 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt @@ -9,7 +9,6 @@ package org.nsh07.pomodoro.ui.timerScreen.viewModel import android.app.Application import android.provider.Settings -import androidx.compose.material3.ColorScheme import androidx.core.net.toUri import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.ViewModelProvider @@ -52,8 +51,6 @@ class TimerViewModel( private var pauseTime = 0L private var pauseDuration = 0L - private lateinit var cs: ColorScheme - init { viewModelScope.launch(Dispatchers.IO) { timerRepository.focusTime = @@ -116,10 +113,6 @@ class TimerViewModel( } } - fun setCompositionLocals(colorScheme: ColorScheme) { - cs = colorScheme - } - private fun resetTimer() { viewModelScope.launch { saveTimeToDb() diff --git a/app/src/main/java/org/nsh07/pomodoro/utils/Utils.kt b/app/src/main/java/org/nsh07/pomodoro/utils/Utils.kt index 2696c04..6406065 100644 --- a/app/src/main/java/org/nsh07/pomodoro/utils/Utils.kt +++ b/app/src/main/java/org/nsh07/pomodoro/utils/Utils.kt @@ -7,6 +7,7 @@ package org.nsh07.pomodoro.utils +import androidx.compose.ui.graphics.Color import java.util.Locale import java.util.concurrent.TimeUnit @@ -36,4 +37,26 @@ fun millisecondsToHoursMinutes(t: Long): String { "%dh %dm", TimeUnit.MILLISECONDS.toHours(t), TimeUnit.MILLISECONDS.toMinutes(t) % TimeUnit.HOURS.toMinutes(1) ) -} \ No newline at end of file +} + +/** + * Extension function for [String] to convert it to a [androidx.compose.ui.graphics.Color] + * + * The base string must be of the format produced by [androidx.compose.ui.graphics.Color.toString], + * i.e, the color black with 100% opacity in sRGB would be represented by: + * + * Color(0.0, 0.0, 0.0, 1.0, sRGB IEC61966-2.1) + */ +fun String.toColor(): Color { + // Sample string: Color(0.0, 0.0, 0.0, 1.0, sRGB IEC61966-2.1) + val comma1 = this.indexOf(',') + val comma2 = this.indexOf(',', comma1 + 1) + val comma3 = this.indexOf(',', comma2 + 1) + val comma4 = this.indexOf(',', comma3 + 1) + + val r = this.substringAfter('(').substringBefore(',').toFloat() + val g = this.slice(comma1 + 1..comma2 - 1).toFloat() + val b = this.slice(comma2 + 1..comma3 - 1).toFloat() + val a = this.slice(comma3 + 1..comma4 - 1).toFloat() + return Color(r, g, b, a) +}