From 34b7148df36bca83886b7152aa9b99695ffc0266 Mon Sep 17 00:00:00 2001 From: saberr26 Date: Fri, 1 May 2026 23:32:43 +0100 Subject: [PATCH 01/20] feat: add option to make widgets background transparent --- .../nsh07/pomodoro/widget/HistoryAppWidget.kt | 18 +++++-- .../nsh07/pomodoro/widget/TimerAppWidget.kt | 13 +++-- .../nsh07/pomodoro/widget/TodayAppWidget.kt | 16 +++--- .../composeResources/values/strings.xml | 2 + .../nsh07/pomodoro/data/StateRepository.kt | 7 +++ .../components/ColorSchemePickerListItem.kt | 2 +- .../screens/AppearanceSettings.kt | 52 +++++++++++++++++-- .../viewModel/SettingsAction.kt | 1 + .../settingsScreen/viewModel/SettingsState.kt | 1 + .../viewModel/SettingsViewModel.kt | 13 +++++ 10 files changed, 106 insertions(+), 19 deletions(-) diff --git a/androidApp/src/main/java/org/nsh07/pomodoro/widget/HistoryAppWidget.kt b/androidApp/src/main/java/org/nsh07/pomodoro/widget/HistoryAppWidget.kt index fb9612a6..aadb4f77 100644 --- a/androidApp/src/main/java/org/nsh07/pomodoro/widget/HistoryAppWidget.kt +++ b/androidApp/src/main/java/org/nsh07/pomodoro/widget/HistoryAppWidget.kt @@ -81,34 +81,41 @@ class HistoryAppWidget : GlanceAppWidget(), KoinComponent { id: GlanceId ) { val statRepository: StatRepository = get() + val stateRepository: StateRepository = get() val history = statRepository.getLastNDaysStats(30).first().reversed() provideContent { + val settingsState by stateRepository.settingsState.collectAsState() val size = LocalSize.current val history = history.takeLast(((size.width.value - 32) / 24).toInt()) key(size) { GlanceTheme { - Content(history, history.maxBy { it.totalFocusTime() }.totalFocusTime()) + Content( + history, + history.maxBy { it.totalFocusTime() }.totalFocusTime(), + settingsState.transparentWidgets + ) } } } } @Composable - private fun Content(history: List, maxFocus: Long) { + private fun Content(history: List, maxFocus: Long, transparentWidgets: Boolean) { val context = LocalContext.current val size = LocalSize.current val scope = rememberCoroutineScope() val roundedCornersSupported = Build.VERSION.SDK_INT >= 31 + val widgetBackground = if (transparentWidgets) Color.Transparent else colors.widgetBackground Column( modifier = GlanceModifier .fillMaxSize() .then( - if (roundedCornersSupported) GlanceModifier.background(colors.widgetBackground) + if (roundedCornersSupported) GlanceModifier.background(widgetBackground) else GlanceModifier.background( ImageProvider(R.drawable.rounded_24dp), - colorFilter = ColorFilter.tint(colors.widgetBackground) + colorFilter = ColorFilter.tint(widgetBackground) ) ) .clickable(actionStartActivity()) @@ -332,7 +339,8 @@ class HistoryAppWidget : GlanceAppWidget(), KoinComponent { ) { Content( history = history, - maxFocus = history.maxBy { it.totalFocusTime() }.totalFocusTime() + maxFocus = history.maxBy { it.totalFocusTime() }.totalFocusTime(), + transparentWidgets = false ) } } diff --git a/androidApp/src/main/java/org/nsh07/pomodoro/widget/TimerAppWidget.kt b/androidApp/src/main/java/org/nsh07/pomodoro/widget/TimerAppWidget.kt index e36979e1..08271260 100644 --- a/androidApp/src/main/java/org/nsh07/pomodoro/widget/TimerAppWidget.kt +++ b/androidApp/src/main/java/org/nsh07/pomodoro/widget/TimerAppWidget.kt @@ -72,14 +72,15 @@ class TimerAppWidget : GlanceAppWidget(), KoinComponent { val stateRepository: StateRepository = get() provideContent { val timerState by stateRepository.timerState.collectAsState() + val settingsState by stateRepository.settingsState.collectAsState() GlanceTheme { - Content(timerState) + Content(timerState, settingsState.transparentWidgets) } } } @Composable - private fun Content(timerState: TimerState) { + private fun Content(timerState: TimerState, transparentWidgets: Boolean) { val size = LocalSize.current val context = LocalContext.current val circleSize = minOf(256.dp, size.width, size.height) @@ -103,7 +104,10 @@ class TimerAppWidget : GlanceAppWidget(), KoinComponent { .size(circleSize) .background( ImageProvider(R.drawable.rounded_full), - colorFilter = ColorFilter.tint(colors.widgetBackground) + colorFilter = ColorFilter.tint( + if (transparentWidgets) Color.Transparent + else colors.widgetBackground + ) ) ) { val clockHeight = (circleSize.value * 0.25f) @@ -199,7 +203,8 @@ class TimerAppWidget : GlanceAppWidget(), KoinComponent { GlanceTheme(colors = ColorProviders(lightScheme)) { Box(GlanceModifier.background(Color.White)) { Content( - timerState = TimerState() + timerState = TimerState(), + transparentWidgets = false ) } } diff --git a/androidApp/src/main/java/org/nsh07/pomodoro/widget/TodayAppWidget.kt b/androidApp/src/main/java/org/nsh07/pomodoro/widget/TodayAppWidget.kt index d2dff0f7..7bee8161 100644 --- a/androidApp/src/main/java/org/nsh07/pomodoro/widget/TodayAppWidget.kt +++ b/androidApp/src/main/java/org/nsh07/pomodoro/widget/TodayAppWidget.kt @@ -83,31 +83,34 @@ class TodayAppWidget : GlanceAppWidget(), KoinComponent { id: GlanceId ) { val statRepository: StatRepository = get() + val stateRepository: StateRepository = get() val stat = statRepository.getTodayStat().first() ?: Stat(LocalDate.now(), 0, 0, 0, 0, 0) provideContent { + val settingsState by stateRepository.settingsState.collectAsState() key(LocalSize.current) { GlanceTheme { - Content(stat) + Content(stat, settingsState.transparentWidgets) } } } } @Composable - private fun Content(stat: Stat) { + private fun Content(stat: Stat, transparentWidgets: Boolean) { val context = LocalContext.current val size = LocalSize.current val scope = rememberCoroutineScope() + val widgetBackground = if (transparentWidgets) Color.Transparent else colors.widgetBackground Box( contentAlignment = Alignment.TopEnd, modifier = GlanceModifier .then( - if (Build.VERSION.SDK_INT >= 31) GlanceModifier.background(colors.widgetBackground) + if (Build.VERSION.SDK_INT >= 31) GlanceModifier.background(widgetBackground) else GlanceModifier.background( ImageProvider(R.drawable.rounded_24dp), - colorFilter = ColorFilter.tint(colors.widgetBackground) + colorFilter = ColorFilter.tint(widgetBackground) ) ) .padding(16.dp) @@ -203,14 +206,15 @@ class TodayAppWidget : GlanceAppWidget(), KoinComponent { GlanceModifier.cornerRadius(32.dp) ) { Content( - Stat( + stat = Stat( date = LocalDate.of(2026, 3, 12), focusTimeQ1 = 1617943, focusTimeQ2 = 5704591, focusTimeQ3 = 556490, focusTimeQ4 = 1200498, breakTime = 3939448 - ) + ), + transparentWidgets = false ) } } diff --git a/shared/src/commonMain/composeResources/values/strings.xml b/shared/src/commonMain/composeResources/values/strings.xml index 22c43be1..98e0a0ac 100644 --- a/shared/src/commonMain/composeResources/values/strings.xml +++ b/shared/src/commonMain/composeResources/values/strings.xml @@ -118,6 +118,8 @@ Stop Alarm? System Theme + Transparent widgets + Remove background from widgets Timer Timer progress Timer reset diff --git a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/data/StateRepository.kt b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/data/StateRepository.kt index 556af93c..54ebe083 100644 --- a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/data/StateRepository.kt +++ b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/data/StateRepository.kt @@ -129,6 +129,12 @@ class StateRepository(private val preferenceRepository: PreferenceRepository) { val secureAod = preferenceRepository.getBooleanPreference("secure_aod") ?: preferenceRepository.saveBooleanPreference("secure_aod", defaults.secureAod) + val transparentWidgets = preferenceRepository.getBooleanPreference("transparent_widgets") + ?: preferenceRepository.saveBooleanPreference( + "transparent_widgets", + defaults.transparentWidgets + ) + val vibrationOnDuration = (preferenceRepository.getIntPreference("vibration_on_duration") ?: preferenceRepository.saveIntPreference( "vibration_on_duration", @@ -172,6 +178,7 @@ class StateRepository(private val preferenceRepository: PreferenceRepository) { singleProgressBar = singleProgressBar, autostartNextSession = autostartNextSession, secureAod = secureAod, + transparentWidgets = transparentWidgets, vibrationOnDuration = vibrationOnDuration, vibrationOffDuration = vibrationOffDuration, vibrationAmplitude = vibrationAmplitude, diff --git a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/settingsScreen/components/ColorSchemePickerListItem.kt b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/settingsScreen/components/ColorSchemePickerListItem.kt index 46e3414c..71413adb 100644 --- a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/settingsScreen/components/ColorSchemePickerListItem.kt +++ b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/settingsScreen/components/ColorSchemePickerListItem.kt @@ -142,7 +142,7 @@ fun ColorSchemePickerListItem( enabled = isPlus, shapes = ListItemDefaults.segmentedShapes( 1, - 3, + items, ListItemDefaults.shapes( shape = shapes.extraSmall.copy( bottomStart = CornerSize(0), diff --git a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/settingsScreen/screens/AppearanceSettings.kt b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/settingsScreen/screens/AppearanceSettings.kt index a7b64a11..cb959409 100644 --- a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/settingsScreen/screens/AppearanceSettings.kt +++ b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/settingsScreen/screens/AppearanceSettings.kt @@ -77,6 +77,8 @@ import tomato.shared.generated.resources.check import tomato.shared.generated.resources.clear import tomato.shared.generated.resources.contrast import tomato.shared.generated.resources.settings +import tomato.shared.generated.resources.transparent_widgets +import tomato.shared.generated.resources.transparent_widgets_desc @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable @@ -156,7 +158,7 @@ fun AppearanceSettings( ThemePickerListItem( theme = settingsState.theme, onThemeChange = { onAction(SettingsAction.SaveTheme(it)) }, - items = if (isPlus) 3 else 1, + items = if (isPlus) 4 else 1, index = 0 ) } @@ -168,7 +170,7 @@ fun AppearanceSettings( item { ColorSchemePickerListItem( color = settingsState.colorScheme.toColor(), - items = 3, + items = if (isPlus) 4 else 3, index = if (isPlus) 1 else 0, isPlus = isPlus, onColorChange = { onAction(SettingsAction.SaveColorScheme(it)) }, @@ -214,7 +216,51 @@ fun AppearanceSettings( }, colors = listItemColors, enabled = isPlus, - shapes = segmentedListItemShapes(2, 3) + shapes = segmentedListItemShapes(2, 4) + ) + } + + item { + val item = SettingsSwitchItem( + checked = settingsState.transparentWidgets, + icon = Res.drawable.clear, + label = Res.string.transparent_widgets, + description = Res.string.transparent_widgets_desc, + onClick = { onAction(SettingsAction.SaveTransparentWidgets(it)) } + ) + SegmentedListItem( + onClick = { item.onClick(!item.checked) }, + leadingContent = { + Icon(painterResource(item.icon), contentDescription = null) + }, + content = { Text(stringResource(item.label)) }, + supportingContent = { Text(stringResource(item.description)) }, + trailingContent = { + Switch( + checked = item.checked, + onCheckedChange = { item.onClick(it) }, + enabled = isPlus, + thumbContent = { + if (item.checked) { + Icon( + painter = painterResource(Res.drawable.check), + contentDescription = null, + modifier = Modifier.size(SwitchDefaults.IconSize), + ) + } else { + Icon( + painter = painterResource(Res.drawable.clear), + contentDescription = null, + modifier = Modifier.size(SwitchDefaults.IconSize), + ) + } + }, + colors = switchColors + ) + }, + colors = listItemColors, + enabled = isPlus, + shapes = segmentedListItemShapes(3, 4) ) } diff --git a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsAction.kt b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsAction.kt index fe00a7be..61c0177f 100644 --- a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsAction.kt +++ b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsAction.kt @@ -29,6 +29,7 @@ sealed interface SettingsAction { data class SaveSingleProgressBar(val enabled: Boolean) : SettingsAction data class SaveAutostartNextSession(val enabled: Boolean) : SettingsAction data class SaveSecureAod(val enabled: Boolean) : SettingsAction + data class SaveTransparentWidgets(val enabled: Boolean) : SettingsAction data class SaveAlarmSound(val uri: String?) : SettingsAction data class SaveTheme(val theme: String) : SettingsAction data class SaveColorScheme(val color: Color) : SettingsAction diff --git a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsState.kt b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsState.kt index 61b1add7..5bd1b8dd 100644 --- a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsState.kt +++ b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsState.kt @@ -36,6 +36,7 @@ data class SettingsState( val singleProgressBar: Boolean = false, val autostartNextSession: Boolean = false, val secureAod: Boolean = true, + val transparentWidgets: Boolean = false, val isShowingEraseDataDialog: Boolean = false, val vibrationOnDuration: Long = 1000L, diff --git a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt index 24a8a2f8..1f153943 100644 --- a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt +++ b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt @@ -106,6 +106,7 @@ class SettingsViewModel( is SettingsAction.SaveSingleProgressBar -> saveSingleProgressBar(action.enabled) is SettingsAction.SaveAutostartNextSession -> saveAutostartNextSession(action.enabled) is SettingsAction.SaveSecureAod -> saveSecureAod(action.enabled) + is SettingsAction.SaveTransparentWidgets -> saveTransparentWidgets(action.enabled) is SettingsAction.SaveColorScheme -> saveColorScheme(action.color) is SettingsAction.SaveTheme -> saveTheme(action.theme) is SettingsAction.SaveBlackTheme -> saveBlackTheme(action.enabled) @@ -360,6 +361,18 @@ class SettingsViewModel( } } + private fun saveTransparentWidgets(enabled: Boolean) { + viewModelScope.launch { + _settingsState.update { currentState -> + currentState.copy(transparentWidgets = enabled) + } + preferenceRepository.saveBooleanPreference( + "transparent_widgets", + enabled + ) + } + } + private fun saveVibrationOnDuration(vibrationOnDuration: Long) { viewModelScope.launch { _settingsState.update { currentState -> From 1fe1825bd2773b55a02cd930d19098022f2f8cfa Mon Sep 17 00:00:00 2001 From: saberr26 Date: Fri, 1 May 2026 23:50:48 +0100 Subject: [PATCH 02/20] fix: add missing imports and fix type mismatches in widgets --- .../main/java/org/nsh07/pomodoro/widget/HistoryAppWidget.kt | 6 +++++- .../main/java/org/nsh07/pomodoro/widget/TimerAppWidget.kt | 3 ++- .../main/java/org/nsh07/pomodoro/widget/TodayAppWidget.kt | 6 +++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/androidApp/src/main/java/org/nsh07/pomodoro/widget/HistoryAppWidget.kt b/androidApp/src/main/java/org/nsh07/pomodoro/widget/HistoryAppWidget.kt index aadb4f77..4b295cee 100644 --- a/androidApp/src/main/java/org/nsh07/pomodoro/widget/HistoryAppWidget.kt +++ b/androidApp/src/main/java/org/nsh07/pomodoro/widget/HistoryAppWidget.kt @@ -21,6 +21,8 @@ import android.content.Context import android.os.Build import androidx.compose.material3.MaterialTheme.typography import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.runtime.key import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.graphics.Color @@ -59,6 +61,7 @@ import androidx.glance.material3.ColorProviders import androidx.glance.preview.ExperimentalGlancePreviewApi import androidx.glance.preview.Preview import androidx.glance.text.FontWeight +import androidx.glance.unit.ColorProvider import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent @@ -67,6 +70,7 @@ import org.nsh07.pomodoro.MainActivity import org.nsh07.pomodoro.R import org.nsh07.pomodoro.data.Stat import org.nsh07.pomodoro.data.StatRepository +import org.nsh07.pomodoro.data.StateRepository import org.nsh07.pomodoro.ui.theme.lightScheme import org.nsh07.pomodoro.utils.millisecondsToHoursMinutes import org.nsh07.pomodoro.widget.TomatoWidgetSize.Width4 @@ -106,7 +110,7 @@ class HistoryAppWidget : GlanceAppWidget(), KoinComponent { val size = LocalSize.current val scope = rememberCoroutineScope() val roundedCornersSupported = Build.VERSION.SDK_INT >= 31 - val widgetBackground = if (transparentWidgets) Color.Transparent else colors.widgetBackground + val widgetBackground = if (transparentWidgets) ColorProvider(Color.Transparent) else colors.widgetBackground Column( modifier = GlanceModifier diff --git a/androidApp/src/main/java/org/nsh07/pomodoro/widget/TimerAppWidget.kt b/androidApp/src/main/java/org/nsh07/pomodoro/widget/TimerAppWidget.kt index 08271260..204bf99a 100644 --- a/androidApp/src/main/java/org/nsh07/pomodoro/widget/TimerAppWidget.kt +++ b/androidApp/src/main/java/org/nsh07/pomodoro/widget/TimerAppWidget.kt @@ -48,6 +48,7 @@ import androidx.glance.layout.Row import androidx.glance.layout.fillMaxSize import androidx.glance.layout.size import androidx.glance.material3.ColorProviders +import androidx.glance.unit.ColorProvider import androidx.glance.preview.ExperimentalGlancePreviewApi import androidx.glance.preview.Preview import org.koin.core.component.KoinComponent @@ -105,7 +106,7 @@ class TimerAppWidget : GlanceAppWidget(), KoinComponent { .background( ImageProvider(R.drawable.rounded_full), colorFilter = ColorFilter.tint( - if (transparentWidgets) Color.Transparent + if (transparentWidgets) ColorProvider(Color.Transparent) else colors.widgetBackground ) ) diff --git a/androidApp/src/main/java/org/nsh07/pomodoro/widget/TodayAppWidget.kt b/androidApp/src/main/java/org/nsh07/pomodoro/widget/TodayAppWidget.kt index 7bee8161..6a5f78df 100644 --- a/androidApp/src/main/java/org/nsh07/pomodoro/widget/TodayAppWidget.kt +++ b/androidApp/src/main/java/org/nsh07/pomodoro/widget/TodayAppWidget.kt @@ -21,6 +21,8 @@ import android.content.Context import android.os.Build import androidx.compose.material3.MaterialTheme.typography import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.runtime.key import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.graphics.Color @@ -58,6 +60,7 @@ import androidx.glance.preview.Preview import androidx.glance.text.FontWeight import androidx.glance.text.Text import androidx.glance.text.TextStyle +import androidx.glance.unit.ColorProvider import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent @@ -66,6 +69,7 @@ import org.nsh07.pomodoro.MainActivity import org.nsh07.pomodoro.R import org.nsh07.pomodoro.data.Stat import org.nsh07.pomodoro.data.StatRepository +import org.nsh07.pomodoro.data.StateRepository import org.nsh07.pomodoro.ui.theme.lightScheme import org.nsh07.pomodoro.utils.millisecondsToHoursMinutes import org.nsh07.pomodoro.utils.millisecondsToMinutes @@ -102,7 +106,7 @@ class TodayAppWidget : GlanceAppWidget(), KoinComponent { val context = LocalContext.current val size = LocalSize.current val scope = rememberCoroutineScope() - val widgetBackground = if (transparentWidgets) Color.Transparent else colors.widgetBackground + val widgetBackground = if (transparentWidgets) ColorProvider(Color.Transparent) else colors.widgetBackground Box( contentAlignment = Alignment.TopEnd, modifier = GlanceModifier From 7122bbb7b55b41aef346f1040910ee1ce63ce6bc Mon Sep 17 00:00:00 2001 From: saberr26 Date: Sat, 2 May 2026 00:18:37 +0100 Subject: [PATCH 03/20] fix: ensures timer widget is fully transparent when enabled --- .../java/org/nsh07/pomodoro/widget/TimerAppWidget.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/androidApp/src/main/java/org/nsh07/pomodoro/widget/TimerAppWidget.kt b/androidApp/src/main/java/org/nsh07/pomodoro/widget/TimerAppWidget.kt index 204bf99a..51ab28e2 100644 --- a/androidApp/src/main/java/org/nsh07/pomodoro/widget/TimerAppWidget.kt +++ b/androidApp/src/main/java/org/nsh07/pomodoro/widget/TimerAppWidget.kt @@ -94,7 +94,7 @@ class TimerAppWidget : GlanceAppWidget(), KoinComponent { Box( modifier = GlanceModifier .fillMaxSize() - .background(Color.Transparent) + .background(ColorProvider(Color.Transparent)) .clickable(actionStartActivity()), contentAlignment = Alignment.Center ) { @@ -103,11 +103,11 @@ class TimerAppWidget : GlanceAppWidget(), KoinComponent { contentAlignment = Alignment.Center, modifier = GlanceModifier .size(circleSize) - .background( - ImageProvider(R.drawable.rounded_full), - colorFilter = ColorFilter.tint( - if (transparentWidgets) ColorProvider(Color.Transparent) - else colors.widgetBackground + .then( + if (transparentWidgets) GlanceModifier + else GlanceModifier.background( + ImageProvider(R.drawable.rounded_full), + colorFilter = ColorFilter.tint(colors.widgetBackground) ) ) ) { From 5a44751fc2fc827bdd53199c806a128ad4f643aa Mon Sep 17 00:00:00 2001 From: saberr26 Date: Sat, 2 May 2026 01:30:37 +0100 Subject: [PATCH 04/20] fix: update timer widget button colors to accent1 and accent3 roles --- .../java/org/nsh07/pomodoro/widget/TimerAppWidget.kt | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/androidApp/src/main/java/org/nsh07/pomodoro/widget/TimerAppWidget.kt b/androidApp/src/main/java/org/nsh07/pomodoro/widget/TimerAppWidget.kt index 51ab28e2..f983668d 100644 --- a/androidApp/src/main/java/org/nsh07/pomodoro/widget/TimerAppWidget.kt +++ b/androidApp/src/main/java/org/nsh07/pomodoro/widget/TimerAppWidget.kt @@ -183,14 +183,8 @@ class TimerAppWidget : GlanceAppWidget(), KoinComponent { actionParametersOf(key to TimerService.Actions.TOGGLE) ) }, - backgroundColor = - if (breakMode) - colors.tertiary - else colors.primary, - contentColor = - if (breakMode) - colors.onTertiary - else colors.onPrimary + backgroundColor = if (breakMode) colors.tertiary else colors.primary, + contentColor = if (breakMode) colors.onTertiary else colors.onPrimary ) } } From b22b488b92c1578882447c503234ef22a954cbc8 Mon Sep 17 00:00:00 2001 From: saberr26 Date: Sat, 2 May 2026 01:44:55 +0100 Subject: [PATCH 05/20] fix: use static debug keystore for consistent signing --- androidApp/build.gradle.kts | 7 +++++++ androidApp/debug.keystore | Bin 0 -> 2666 bytes 2 files changed, 7 insertions(+) create mode 100644 androidApp/debug.keystore diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts index dba61220..4b2e18a2 100644 --- a/androidApp/build.gradle.kts +++ b/androidApp/build.gradle.kts @@ -62,6 +62,12 @@ android { keyAlias = System.getenv("SIGNING_KEY_ALIAS") keyPassword = System.getenv("SIGNING_KEY_PASSWORD") } + create("debug") { + storeFile = file("debug.keystore") + storePassword = "android" + keyAlias = "androiddebugkey" + keyPassword = "android" + } } buildTypes { @@ -72,6 +78,7 @@ android { } debug { applicationIdSuffix = ".debug" + signingConfig = signingConfigs.getByName("debug") } } diff --git a/androidApp/debug.keystore b/androidApp/debug.keystore new file mode 100644 index 0000000000000000000000000000000000000000..14145a35dac98508d30ae4a718803157fc926af7 GIT binary patch literal 2666 zcma)8X*kpk_nsLuqp>9^StGK`&ya0oO@u)xOGfs6nXxWzy(t}$QMM5SVuKl&#R~$WlAEdK8t)94jBVDaIS$C#S zLbdS#cjm&a(d5qy&^8)MZiHXDoGdNS{QB4_J@x*p*Zfp6pX6UoZ7UGm0Gg@{!;e_x z*_-;rqDxROL0S=B_d8urPApk>8WE>0Hsgu~`;n1rkI&)vsX3T?oxYOT$FL+8pV6eb zNL?`&OP*mF!5Mm=Y=Dha$2#HJDS;RAWcsubPPds1ExdnJCj< zMk`33`&GAlc%gd~-Zh5R6A>=66ImXZSO0KkD~-98Az@h{YvwRO&UqHi+p^cJm-~4c zm7D%+MHyKrUUv6Fl&=iGf!RQ4_2=i^nU~Ydt2c?eZARdaQjZs^}S zDCEb&uwIUP2y1zJD@8#3O;8=0?OTl5%zF%^c59Yr@;d#o&QlK@dQN)51xLy=;l9`W z^{?^G`RiD4&eqtXO_#JnUQ+3EWUGJjc=nRnvdEtkGllC3E?elM?ZAi>NaoRwI< zDcP054N>Auc(0nwQv4!3Ow+G^ah~}4t`nc14wk~7tx8+$uqh_3adMeGD{MejUfFC+ zk)_-#I$mA*BT_gxsE^h%!(xXr_C|R z+8N_or-wfw>&KfA@wLh~$Z3m|uQ5eqGRkwQa}WKbYO^0eubar|U+~v9*b5VowBBR> z<}=oSXuV`I{wC#HB1qi4mjim=_Mq4F$D*TB1gra>G-nr$Kf>xCZ7-R)z`R6-Y927= ze>%lY6F>IYHhh)R+YcEWeFNqQ8QK&*`x5o}11qw^_Qytr{s5WWHPR@%zi@l9_E*L0 zpVyK_!)MZr2pFyH==55bf~&g&_+9^~x8lsqcmB@mMc#hF(PWKw`4^`yW;T2vt)Don zEY6JI*0WatZ+H=r=oIc`CjK>XHx>Td)6ObLb;E9|grKjXjXLr|V!ejFZn4aU@%Cb} zNz9F#CSt`coS|!T6PE~HB~cL(kP9df`^sqk#WgaJMxkC55dR zs>;z?3fQErj<8hT`s;rQZb4fQ)lOmp{OE((y-y|@KpcUE5-waNi#yr)cTTbrBuL|= zfCXRUfp?~z0#WwE70?Z9EW4Xt@>Abpo282BGNU>?1LuQ$?r%!^_n8i^0!(7`s_>UFe{} z(fr5Mve9E{RO{>hiYg+fjtm?O8@2YlNsJzf6p(*I+cD{)V{a*(m5Fsxy; zJ~eC0A7$*+tMF)Tsj|E)Y_i8D%M|yfj&upu%4ROE#UI5AMI-v^470449NnTMLb7k2Qz59 zix>}0+hKBu+BtEf=i%668V@y8?bTS9O(@E|L@}~p@r6XS4Z4qCF3Z>|KcYUX#nOw2 z(UaE5EaQYM%739yhz1x354{84?a8-PI-75bD*2cxPS*xNsdi+PPbS#4a<2ZmF0}q6 zdy}tbYWl@&SFH2kNc(oy`AU_wtL+7Xw`oms_%Dh$NK*UmgC-9k9=Jkz+MaOc?%x(wkYKEN+EmLf@*fM`0kSY$p7MOF>2 ze7cjY)4{VdF*8Q>f@Wj{G@cfpZjU0Dr)rQ-%^T+9+u8zxhTSN>NuoJi+7XgDA9LR8 zyxVDG+Op0KMbd*kXMUdTVA?sM7Pc5t+H%mf&I#o5UwNN-#+9<->xRW7g)F?gR>h^l znSXnCNjMrks4m|6ypwk}A$Lda)`}tq3F&^nUV?HkRKTcu9r%5;Cq& z{5|X0Eyc&C65=RN?HoVh&-a_7*Y1>Q-@(7Z8IXMBWu$6|@4E@&-yq)si8gxDHM+Gx z?5(}niPcd%zlunoP$&QAIx#fsZ}dcWLRLv1fCiKR&OiSb1PlW~MYk^QxcGtuJ;RQO y21aFzZCVy@Ta-oCuaqF&0J>v=5|np-x=pVtBEX_@sh|ZGKJOgOXi)sWi1-&_DBEfP literal 0 HcmV?d00001 From 826479a44e886bf30cd6f1b89edacd68b44eaa8f Mon Sep 17 00:00:00 2001 From: saberr26 Date: Sat, 2 May 2026 01:53:49 +0100 Subject: [PATCH 06/20] fix: rename custom signing config to avoid collision --- androidApp/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts index 4b2e18a2..a1cd19a7 100644 --- a/androidApp/build.gradle.kts +++ b/androidApp/build.gradle.kts @@ -62,7 +62,7 @@ android { keyAlias = System.getenv("SIGNING_KEY_ALIAS") keyPassword = System.getenv("SIGNING_KEY_PASSWORD") } - create("debug") { + create("debugStatic") { storeFile = file("debug.keystore") storePassword = "android" keyAlias = "androiddebugkey" @@ -78,7 +78,7 @@ android { } debug { applicationIdSuffix = ".debug" - signingConfig = signingConfigs.getByName("debug") + signingConfig = signingConfigs.getByName("debugStatic") } } From 3b40a240aebd1bb8f9d4af58fd69bc68b7cbc624 Mon Sep 17 00:00:00 2001 From: saberr26 Date: Sat, 2 May 2026 02:16:04 +0100 Subject: [PATCH 07/20] fix: splash screen follows background color --- androidApp/src/main/res/values/themes.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/androidApp/src/main/res/values/themes.xml b/androidApp/src/main/res/values/themes.xml index ee750f3c..4f4d98e3 100644 --- a/androidApp/src/main/res/values/themes.xml +++ b/androidApp/src/main/res/values/themes.xml @@ -20,6 +20,6 @@ \ No newline at end of file From d2ddd49822e7c18cf1582296e003ef47350bf915 Mon Sep 17 00:00:00 2001 From: saberr26 Date: Sat, 2 May 2026 03:19:43 +0100 Subject: [PATCH 08/20] feat: splash screen follows system theme and monet --- androidApp/build.gradle.kts | 1 + .../src/main/java/org/nsh07/pomodoro/MainActivity.kt | 2 ++ androidApp/src/main/res/values-night-v31/themes.xml | 12 ++++++++++++ androidApp/src/main/res/values-night/themes.xml | 10 ++++++++++ androidApp/src/main/res/values-v31/themes.xml | 12 ++++++++++++ androidApp/src/main/res/values/themes.xml | 6 ++++-- gradle/libs.versions.toml | 2 ++ 7 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 androidApp/src/main/res/values-night-v31/themes.xml create mode 100644 androidApp/src/main/res/values-night/themes.xml create mode 100644 androidApp/src/main/res/values-v31/themes.xml diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts index a1cd19a7..a28ec45b 100644 --- a/androidApp/build.gradle.kts +++ b/androidApp/build.gradle.kts @@ -139,6 +139,7 @@ koinCompiler { dependencies { implementation(libs.androidx.core.ktx) + implementation(libs.androidx.core.splashscreen) implementation(libs.androidx.lifecycle.runtime) implementation(libs.androidx.lifecycle.viewmodel) implementation(libs.androidx.lifecycle.viewmodel.compose) diff --git a/androidApp/src/main/java/org/nsh07/pomodoro/MainActivity.kt b/androidApp/src/main/java/org/nsh07/pomodoro/MainActivity.kt index 2139e9a2..57acaafe 100644 --- a/androidApp/src/main/java/org/nsh07/pomodoro/MainActivity.kt +++ b/androidApp/src/main/java/org/nsh07/pomodoro/MainActivity.kt @@ -26,6 +26,7 @@ import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme.colorScheme import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.koin.android.ext.android.inject import org.nsh07.pomodoro.data.StateRepository @@ -42,6 +43,7 @@ class MainActivity : ComponentActivity() { private val activityCallbacks: ActivityCallbacks by inject() override fun onCreate(savedInstanceState: Bundle?) { + installSplashScreen() super.onCreate(savedInstanceState) enableEdgeToEdge() diff --git a/androidApp/src/main/res/values-night-v31/themes.xml b/androidApp/src/main/res/values-night-v31/themes.xml new file mode 100644 index 00000000..24085937 --- /dev/null +++ b/androidApp/src/main/res/values-night-v31/themes.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/androidApp/src/main/res/values-night/themes.xml b/androidApp/src/main/res/values-night/themes.xml new file mode 100644 index 00000000..c3dc010d --- /dev/null +++ b/androidApp/src/main/res/values-night/themes.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/androidApp/src/main/res/values-v31/themes.xml b/androidApp/src/main/res/values-v31/themes.xml new file mode 100644 index 00000000..cfccbdbe --- /dev/null +++ b/androidApp/src/main/res/values-v31/themes.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/androidApp/src/main/res/values/themes.xml b/androidApp/src/main/res/values/themes.xml index 4f4d98e3..bb97f638 100644 --- a/androidApp/src/main/res/values/themes.xml +++ b/androidApp/src/main/res/values/themes.xml @@ -19,7 +19,9 @@ \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d9ef65c5..102fcd78 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,6 +11,7 @@ buildKonfig = "0.18.0" composeBom = "2026.04.00" composenativetray = "1.2.0" coreKtx = "1.18.0" +coreSplashScreen = "1.0.1" espressoCore = "3.7.0" filekitCore = "0.13.0" filekitDialogsCompose = "0.13.0" @@ -39,6 +40,7 @@ androidx-adaptive = { group = "org.jetbrains.compose.material3.adaptive", name = androidx-compose-adaptive-navigation3 = { module = "org.jetbrains.compose.material3.adaptive:adaptive-navigation3", version.ref = "adaptive" } androidx-compose-bom = { group = "androidx.compose", name = "compose-bom-alpha", version.ref = "composeBom" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } +androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "coreSplashScreen" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } androidx-glance-appwidget = { module = "androidx.glance:glance-appwidget", version.ref = "glance" } androidx-glance-appwidget-preview = { module = "androidx.glance:glance-appwidget-preview", version.ref = "glance" } From f553e8adc7fca91e0275c80ae0a5f66a032b91b8 Mon Sep 17 00:00:00 2001 From: saberr26 Date: Sat, 2 May 2026 03:28:55 +0100 Subject: [PATCH 09/20] fix: ignore launcher corner radius for timer widget to prevent cropping --- .../main/java/org/nsh07/pomodoro/widget/TimerAppWidget.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/androidApp/src/main/java/org/nsh07/pomodoro/widget/TimerAppWidget.kt b/androidApp/src/main/java/org/nsh07/pomodoro/widget/TimerAppWidget.kt index f983668d..4f12dd32 100644 --- a/androidApp/src/main/java/org/nsh07/pomodoro/widget/TimerAppWidget.kt +++ b/androidApp/src/main/java/org/nsh07/pomodoro/widget/TimerAppWidget.kt @@ -38,14 +38,17 @@ import androidx.glance.action.clickable import androidx.glance.appwidget.GlanceAppWidget import androidx.glance.appwidget.SizeMode import androidx.glance.appwidget.action.actionRunCallback +import androidx.glance.appwidget.appWidgetBackground import androidx.glance.appwidget.components.CircleIconButton import androidx.glance.appwidget.components.SquareIconButton +import androidx.glance.appwidget.cornerRadius import androidx.glance.appwidget.provideContent import androidx.glance.background import androidx.glance.layout.Alignment import androidx.glance.layout.Box import androidx.glance.layout.Row import androidx.glance.layout.fillMaxSize +import androidx.glance.layout.padding import androidx.glance.layout.size import androidx.glance.material3.ColorProviders import androidx.glance.unit.ColorProvider @@ -84,7 +87,7 @@ class TimerAppWidget : GlanceAppWidget(), KoinComponent { private fun Content(timerState: TimerState, transparentWidgets: Boolean) { val size = LocalSize.current val context = LocalContext.current - val circleSize = minOf(256.dp, size.width, size.height) + val circleSize = minOf(256.dp, size.width - 8.dp, size.height - 8.dp) val breakMode = timerState.timerMode == TimerMode.SHORT_BREAK || timerState.timerMode == TimerMode.LONG_BREAK @@ -94,7 +97,10 @@ class TimerAppWidget : GlanceAppWidget(), KoinComponent { Box( modifier = GlanceModifier .fillMaxSize() + .padding(4.dp) + .appWidgetBackground() .background(ColorProvider(Color.Transparent)) + .cornerRadius(0.dp) .clickable(actionStartActivity()), contentAlignment = Alignment.Center ) { From 7e19eb712404678bc6b2948f7615ea405140f3fa Mon Sep 17 00:00:00 2001 From: saberr26 Date: Sun, 3 May 2026 05:25:06 +0100 Subject: [PATCH 10/20] revert: remove signing config changes and widget color updates --- androidApp/build.gradle.kts | 7 ------- androidApp/debug.keystore | Bin 2666 -> 0 bytes .../org/nsh07/pomodoro/widget/TimerAppWidget.kt | 10 ++++++++-- 3 files changed, 8 insertions(+), 9 deletions(-) delete mode 100644 androidApp/debug.keystore diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts index a28ec45b..8323bd29 100644 --- a/androidApp/build.gradle.kts +++ b/androidApp/build.gradle.kts @@ -62,12 +62,6 @@ android { keyAlias = System.getenv("SIGNING_KEY_ALIAS") keyPassword = System.getenv("SIGNING_KEY_PASSWORD") } - create("debugStatic") { - storeFile = file("debug.keystore") - storePassword = "android" - keyAlias = "androiddebugkey" - keyPassword = "android" - } } buildTypes { @@ -78,7 +72,6 @@ android { } debug { applicationIdSuffix = ".debug" - signingConfig = signingConfigs.getByName("debugStatic") } } diff --git a/androidApp/debug.keystore b/androidApp/debug.keystore deleted file mode 100644 index 14145a35dac98508d30ae4a718803157fc926af7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2666 zcma)8X*kpk_nsLuqp>9^StGK`&ya0oO@u)xOGfs6nXxWzy(t}$QMM5SVuKl&#R~$WlAEdK8t)94jBVDaIS$C#S zLbdS#cjm&a(d5qy&^8)MZiHXDoGdNS{QB4_J@x*p*Zfp6pX6UoZ7UGm0Gg@{!;e_x z*_-;rqDxROL0S=B_d8urPApk>8WE>0Hsgu~`;n1rkI&)vsX3T?oxYOT$FL+8pV6eb zNL?`&OP*mF!5Mm=Y=Dha$2#HJDS;RAWcsubPPds1ExdnJCj< zMk`33`&GAlc%gd~-Zh5R6A>=66ImXZSO0KkD~-98Az@h{YvwRO&UqHi+p^cJm-~4c zm7D%+MHyKrUUv6Fl&=iGf!RQ4_2=i^nU~Ydt2c?eZARdaQjZs^}S zDCEb&uwIUP2y1zJD@8#3O;8=0?OTl5%zF%^c59Yr@;d#o&QlK@dQN)51xLy=;l9`W z^{?^G`RiD4&eqtXO_#JnUQ+3EWUGJjc=nRnvdEtkGllC3E?elM?ZAi>NaoRwI< zDcP054N>Auc(0nwQv4!3Ow+G^ah~}4t`nc14wk~7tx8+$uqh_3adMeGD{MejUfFC+ zk)_-#I$mA*BT_gxsE^h%!(xXr_C|R z+8N_or-wfw>&KfA@wLh~$Z3m|uQ5eqGRkwQa}WKbYO^0eubar|U+~v9*b5VowBBR> z<}=oSXuV`I{wC#HB1qi4mjim=_Mq4F$D*TB1gra>G-nr$Kf>xCZ7-R)z`R6-Y927= ze>%lY6F>IYHhh)R+YcEWeFNqQ8QK&*`x5o}11qw^_Qytr{s5WWHPR@%zi@l9_E*L0 zpVyK_!)MZr2pFyH==55bf~&g&_+9^~x8lsqcmB@mMc#hF(PWKw`4^`yW;T2vt)Don zEY6JI*0WatZ+H=r=oIc`CjK>XHx>Td)6ObLb;E9|grKjXjXLr|V!ejFZn4aU@%Cb} zNz9F#CSt`coS|!T6PE~HB~cL(kP9df`^sqk#WgaJMxkC55dR zs>;z?3fQErj<8hT`s;rQZb4fQ)lOmp{OE((y-y|@KpcUE5-waNi#yr)cTTbrBuL|= zfCXRUfp?~z0#WwE70?Z9EW4Xt@>Abpo282BGNU>?1LuQ$?r%!^_n8i^0!(7`s_>UFe{} z(fr5Mve9E{RO{>hiYg+fjtm?O8@2YlNsJzf6p(*I+cD{)V{a*(m5Fsxy; zJ~eC0A7$*+tMF)Tsj|E)Y_i8D%M|yfj&upu%4ROE#UI5AMI-v^470449NnTMLb7k2Qz59 zix>}0+hKBu+BtEf=i%668V@y8?bTS9O(@E|L@}~p@r6XS4Z4qCF3Z>|KcYUX#nOw2 z(UaE5EaQYM%739yhz1x354{84?a8-PI-75bD*2cxPS*xNsdi+PPbS#4a<2ZmF0}q6 zdy}tbYWl@&SFH2kNc(oy`AU_wtL+7Xw`oms_%Dh$NK*UmgC-9k9=Jkz+MaOc?%x(wkYKEN+EmLf@*fM`0kSY$p7MOF>2 ze7cjY)4{VdF*8Q>f@Wj{G@cfpZjU0Dr)rQ-%^T+9+u8zxhTSN>NuoJi+7XgDA9LR8 zyxVDG+Op0KMbd*kXMUdTVA?sM7Pc5t+H%mf&I#o5UwNN-#+9<->xRW7g)F?gR>h^l znSXnCNjMrks4m|6ypwk}A$Lda)`}tq3F&^nUV?HkRKTcu9r%5;Cq& z{5|X0Eyc&C65=RN?HoVh&-a_7*Y1>Q-@(7Z8IXMBWu$6|@4E@&-yq)si8gxDHM+Gx z?5(}niPcd%zlunoP$&QAIx#fsZ}dcWLRLv1fCiKR&OiSb1PlW~MYk^QxcGtuJ;RQO y21aFzZCVy@Ta-oCuaqF&0J>v=5|np-x=pVtBEX_@sh|ZGKJOgOXi)sWi1-&_DBEfP diff --git a/androidApp/src/main/java/org/nsh07/pomodoro/widget/TimerAppWidget.kt b/androidApp/src/main/java/org/nsh07/pomodoro/widget/TimerAppWidget.kt index 4f12dd32..8deb482d 100644 --- a/androidApp/src/main/java/org/nsh07/pomodoro/widget/TimerAppWidget.kt +++ b/androidApp/src/main/java/org/nsh07/pomodoro/widget/TimerAppWidget.kt @@ -189,8 +189,14 @@ class TimerAppWidget : GlanceAppWidget(), KoinComponent { actionParametersOf(key to TimerService.Actions.TOGGLE) ) }, - backgroundColor = if (breakMode) colors.tertiary else colors.primary, - contentColor = if (breakMode) colors.onTertiary else colors.onPrimary + backgroundColor = + if (breakMode) + colors.tertiary + else colors.primary, + contentColor = + if (breakMode) + colors.onTertiary + else colors.onPrimary ) } } From f475c51c57757f89a7b992f47b9524ee2d438c56 Mon Sep 17 00:00:00 2001 From: saberr26 Date: Sat, 9 May 2026 17:24:46 +0100 Subject: [PATCH 11/20] feat: add dedicated widgets settings with opacity and background roles --- .../nsh07/pomodoro/widget/HistoryAppWidget.kt | 19 +- .../nsh07/pomodoro/widget/TimerAppWidget.kt | 25 +- .../nsh07/pomodoro/widget/TodayAppWidget.kt | 21 +- .../composeResources/values/strings.xml | 6 +- .../kotlin/org/nsh07/pomodoro/Navigation.kt | 6 + .../nsh07/pomodoro/data/StateRepository.kt | 17 +- .../kotlin/org/nsh07/pomodoro/ui/Screen.kt | 3 + .../ui/settingsScreen/SettingsScreen.kt | 23 ++ .../screens/AppearanceSettings.kt | 48 +-- .../settingsScreen/screens/WidgetsSettings.kt | 314 ++++++++++++++++++ .../viewModel/SettingsAction.kt | 3 +- .../settingsScreen/viewModel/SettingsState.kt | 3 +- .../viewModel/SettingsViewModel.kt | 47 ++- 13 files changed, 454 insertions(+), 81 deletions(-) create mode 100644 shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/settingsScreen/screens/WidgetsSettings.kt diff --git a/androidApp/src/main/java/org/nsh07/pomodoro/widget/HistoryAppWidget.kt b/androidApp/src/main/java/org/nsh07/pomodoro/widget/HistoryAppWidget.kt index 4b295cee..61ea398c 100644 --- a/androidApp/src/main/java/org/nsh07/pomodoro/widget/HistoryAppWidget.kt +++ b/androidApp/src/main/java/org/nsh07/pomodoro/widget/HistoryAppWidget.kt @@ -97,7 +97,8 @@ class HistoryAppWidget : GlanceAppWidget(), KoinComponent { Content( history, history.maxBy { it.totalFocusTime() }.totalFocusTime(), - settingsState.transparentWidgets + settingsState.widgetOpacity, + settingsState.widgetBackgroundRole ) } } @@ -105,12 +106,21 @@ class HistoryAppWidget : GlanceAppWidget(), KoinComponent { } @Composable - private fun Content(history: List, maxFocus: Long, transparentWidgets: Boolean) { + private fun Content(history: List, maxFocus: Long, opacity: Float, backgroundRole: String) { val context = LocalContext.current val size = LocalSize.current val scope = rememberCoroutineScope() val roundedCornersSupported = Build.VERSION.SDK_INT >= 31 - val widgetBackground = if (transparentWidgets) ColorProvider(Color.Transparent) else colors.widgetBackground + val backgroundColor = when (backgroundRole) { + "surface" -> colors.surface + "surfaceVariant" -> colors.surfaceVariant + "primaryContainer" -> colors.primaryContainer + "secondaryContainer" -> colors.secondaryContainer + "tertiaryContainer" -> colors.tertiaryContainer + else -> colors.surface + }.getColor(context).copy(alpha = opacity) + val widgetBackground = ColorProvider(backgroundColor) + Column( modifier = GlanceModifier @@ -344,7 +354,8 @@ class HistoryAppWidget : GlanceAppWidget(), KoinComponent { Content( history = history, maxFocus = history.maxBy { it.totalFocusTime() }.totalFocusTime(), - transparentWidgets = false + opacity = 1.0f, + backgroundRole = "surface" ) } } diff --git a/androidApp/src/main/java/org/nsh07/pomodoro/widget/TimerAppWidget.kt b/androidApp/src/main/java/org/nsh07/pomodoro/widget/TimerAppWidget.kt index 8deb482d..cf6e20d4 100644 --- a/androidApp/src/main/java/org/nsh07/pomodoro/widget/TimerAppWidget.kt +++ b/androidApp/src/main/java/org/nsh07/pomodoro/widget/TimerAppWidget.kt @@ -78,13 +78,13 @@ class TimerAppWidget : GlanceAppWidget(), KoinComponent { val timerState by stateRepository.timerState.collectAsState() val settingsState by stateRepository.settingsState.collectAsState() GlanceTheme { - Content(timerState, settingsState.transparentWidgets) + Content(timerState, settingsState.widgetOpacity, settingsState.widgetBackgroundRole) } } } @Composable - private fun Content(timerState: TimerState, transparentWidgets: Boolean) { + private fun Content(timerState: TimerState, opacity: Float, backgroundRole: String) { val size = LocalSize.current val context = LocalContext.current val circleSize = minOf(256.dp, size.width - 8.dp, size.height - 8.dp) @@ -94,6 +94,15 @@ class TimerAppWidget : GlanceAppWidget(), KoinComponent { val secondaryButtonColor = if (!breakMode) colors.tertiary else colors.primary val onSecondaryButtonColor = if (!breakMode) colors.onTertiary else colors.onPrimary + val backgroundColor = when (backgroundRole) { + "surface" -> colors.surface + "surfaceVariant" -> colors.surfaceVariant + "primaryContainer" -> colors.primaryContainer + "secondaryContainer" -> colors.secondaryContainer + "tertiaryContainer" -> colors.tertiaryContainer + else -> colors.surface + }.getColor(context).copy(alpha = opacity) + Box( modifier = GlanceModifier .fillMaxSize() @@ -109,12 +118,9 @@ class TimerAppWidget : GlanceAppWidget(), KoinComponent { contentAlignment = Alignment.Center, modifier = GlanceModifier .size(circleSize) - .then( - if (transparentWidgets) GlanceModifier - else GlanceModifier.background( - ImageProvider(R.drawable.rounded_full), - colorFilter = ColorFilter.tint(colors.widgetBackground) - ) + .background( + ImageProvider(R.drawable.rounded_full), + colorFilter = ColorFilter.tint(ColorProvider(backgroundColor)) ) ) { val clockHeight = (circleSize.value * 0.25f) @@ -211,7 +217,8 @@ class TimerAppWidget : GlanceAppWidget(), KoinComponent { Box(GlanceModifier.background(Color.White)) { Content( timerState = TimerState(), - transparentWidgets = false + opacity = 1.0f, + backgroundRole = "surface" ) } } diff --git a/androidApp/src/main/java/org/nsh07/pomodoro/widget/TodayAppWidget.kt b/androidApp/src/main/java/org/nsh07/pomodoro/widget/TodayAppWidget.kt index 6a5f78df..046c858e 100644 --- a/androidApp/src/main/java/org/nsh07/pomodoro/widget/TodayAppWidget.kt +++ b/androidApp/src/main/java/org/nsh07/pomodoro/widget/TodayAppWidget.kt @@ -95,26 +95,34 @@ class TodayAppWidget : GlanceAppWidget(), KoinComponent { val settingsState by stateRepository.settingsState.collectAsState() key(LocalSize.current) { GlanceTheme { - Content(stat, settingsState.transparentWidgets) + Content(stat, settingsState.widgetOpacity, settingsState.widgetBackgroundRole) } } } } @Composable - private fun Content(stat: Stat, transparentWidgets: Boolean) { + private fun Content(stat: Stat, opacity: Float, backgroundRole: String) { val context = LocalContext.current val size = LocalSize.current val scope = rememberCoroutineScope() - val widgetBackground = if (transparentWidgets) ColorProvider(Color.Transparent) else colors.widgetBackground + val backgroundColor = when (backgroundRole) { + "surface" -> colors.surface + "surfaceVariant" -> colors.surfaceVariant + "primaryContainer" -> colors.primaryContainer + "secondaryContainer" -> colors.secondaryContainer + "tertiaryContainer" -> colors.tertiaryContainer + else -> colors.surface + }.getColor(context).copy(alpha = opacity) + Box( contentAlignment = Alignment.TopEnd, modifier = GlanceModifier .then( - if (Build.VERSION.SDK_INT >= 31) GlanceModifier.background(widgetBackground) + if (Build.VERSION.SDK_INT >= 31) GlanceModifier.background(ColorProvider(backgroundColor)) else GlanceModifier.background( ImageProvider(R.drawable.rounded_24dp), - colorFilter = ColorFilter.tint(widgetBackground) + colorFilter = ColorFilter.tint(ColorProvider(backgroundColor)) ) ) .padding(16.dp) @@ -218,7 +226,8 @@ class TodayAppWidget : GlanceAppWidget(), KoinComponent { focusTimeQ4 = 1200498, breakTime = 3939448 ), - transparentWidgets = false + opacity = 1.0f, + backgroundRole = "surface" ) } } diff --git a/shared/src/commonMain/composeResources/values/strings.xml b/shared/src/commonMain/composeResources/values/strings.xml index 98e0a0ac..64b7f328 100644 --- a/shared/src/commonMain/composeResources/values/strings.xml +++ b/shared/src/commonMain/composeResources/values/strings.xml @@ -118,8 +118,10 @@ Stop Alarm? System Theme - Transparent widgets - Remove background from widgets + Widgets + Customize the appearance of home screen widgets + Opacity + Background role Timer Timer progress Timer reset diff --git a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/Navigation.kt b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/Navigation.kt index 57a51804..c745e3a7 100644 --- a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/Navigation.kt +++ b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/Navigation.kt @@ -59,5 +59,11 @@ val settingsScreens = listOf( Res.drawable.palette, Res.string.appearance, listOf(Res.string.theme, Res.string.color_scheme, Res.string.black_theme) + ), + SettingsNavItem( + Screen.Settings.Widgets, + Res.drawable.clocks, + Res.string.widgets, + listOf(Res.string.opacity, Res.string.background_role) ) ) diff --git a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/data/StateRepository.kt b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/data/StateRepository.kt index 54ebe083..70e79029 100644 --- a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/data/StateRepository.kt +++ b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/data/StateRepository.kt @@ -129,10 +129,16 @@ class StateRepository(private val preferenceRepository: PreferenceRepository) { val secureAod = preferenceRepository.getBooleanPreference("secure_aod") ?: preferenceRepository.saveBooleanPreference("secure_aod", defaults.secureAod) - val transparentWidgets = preferenceRepository.getBooleanPreference("transparent_widgets") - ?: preferenceRepository.saveBooleanPreference( - "transparent_widgets", - defaults.transparentWidgets + val widgetOpacity = (preferenceRepository.getIntPreference("widget_opacity") + ?: preferenceRepository.saveIntPreference( + "widget_opacity", + (defaults.widgetOpacity * 100).toInt() + )).toFloat() / 100f + + val widgetBackgroundRole = preferenceRepository.getStringPreference("widget_background_role") + ?: preferenceRepository.saveStringPreference( + "widget_background_role", + defaults.widgetBackgroundRole ) val vibrationOnDuration = (preferenceRepository.getIntPreference("vibration_on_duration") @@ -178,7 +184,8 @@ class StateRepository(private val preferenceRepository: PreferenceRepository) { singleProgressBar = singleProgressBar, autostartNextSession = autostartNextSession, secureAod = secureAod, - transparentWidgets = transparentWidgets, + widgetOpacity = widgetOpacity, + widgetBackgroundRole = widgetBackgroundRole, vibrationOnDuration = vibrationOnDuration, vibrationOffDuration = vibrationOffDuration, vibrationAmplitude = vibrationAmplitude, diff --git a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/Screen.kt b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/Screen.kt index 108f9733..3ebdfbb3 100644 --- a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/Screen.kt +++ b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/Screen.kt @@ -48,6 +48,9 @@ sealed class Screen : NavKey { @Serializable object Timer : Settings() + + @Serializable + object Widgets : Settings() } @Serializable diff --git a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt index 7332edea..ec6d5dc4 100644 --- a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt +++ b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt @@ -49,6 +49,7 @@ import org.nsh07.pomodoro.ui.settingsScreen.screens.AboutScreen import org.nsh07.pomodoro.ui.settingsScreen.screens.AlarmSettings import org.nsh07.pomodoro.ui.settingsScreen.screens.AppearanceSettings import org.nsh07.pomodoro.ui.settingsScreen.screens.SettingsMainScreen +import org.nsh07.pomodoro.ui.settingsScreen.screens.WidgetsSettings import org.nsh07.pomodoro.ui.settingsScreen.screens.TimerSettings import org.nsh07.pomodoro.ui.settingsScreen.screens.backupRestore.BackupRestoreScreen import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsViewModel @@ -93,6 +94,13 @@ fun SettingsScreenRoot( ) ) { viewModel.sessionsSliderState } + val widgetOpacitySliderState = rememberSaveable( + saver = SliderState.Saver( + viewModel.widgetOpacitySliderState.onValueChangeFinished, + viewModel.widgetOpacitySliderState.valueRange + ) + ) { viewModel.widgetOpacitySliderState } + val directionMultiplier = if (LocalLayoutDirection.current == LayoutDirection.Ltr) 1 else -1 NavDisplay( @@ -197,6 +205,21 @@ fun SettingsScreenRoot( modifier = modifier, ) } + + entry( + metadata = detailPane() + ) { + WidgetsSettings( + settingsState = settingsState, + contentPadding = contentPadding, + isPlus = isPlus, + opacitySliderState = widgetOpacitySliderState, + onAction = viewModel::onAction, + setShowPaywall = setShowPaywall, + onBack = backStack::onBack, + modifier = modifier, + ) + } }, modifier = Modifier.background(topBarColors.containerColor) ) diff --git a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/settingsScreen/screens/AppearanceSettings.kt b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/settingsScreen/screens/AppearanceSettings.kt index cb959409..c17addcb 100644 --- a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/settingsScreen/screens/AppearanceSettings.kt +++ b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/settingsScreen/screens/AppearanceSettings.kt @@ -77,8 +77,6 @@ import tomato.shared.generated.resources.check import tomato.shared.generated.resources.clear import tomato.shared.generated.resources.contrast import tomato.shared.generated.resources.settings -import tomato.shared.generated.resources.transparent_widgets -import tomato.shared.generated.resources.transparent_widgets_desc @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable @@ -216,51 +214,7 @@ fun AppearanceSettings( }, colors = listItemColors, enabled = isPlus, - shapes = segmentedListItemShapes(2, 4) - ) - } - - item { - val item = SettingsSwitchItem( - checked = settingsState.transparentWidgets, - icon = Res.drawable.clear, - label = Res.string.transparent_widgets, - description = Res.string.transparent_widgets_desc, - onClick = { onAction(SettingsAction.SaveTransparentWidgets(it)) } - ) - SegmentedListItem( - onClick = { item.onClick(!item.checked) }, - leadingContent = { - Icon(painterResource(item.icon), contentDescription = null) - }, - content = { Text(stringResource(item.label)) }, - supportingContent = { Text(stringResource(item.description)) }, - trailingContent = { - Switch( - checked = item.checked, - onCheckedChange = { item.onClick(it) }, - enabled = isPlus, - thumbContent = { - if (item.checked) { - Icon( - painter = painterResource(Res.drawable.check), - contentDescription = null, - modifier = Modifier.size(SwitchDefaults.IconSize), - ) - } else { - Icon( - painter = painterResource(Res.drawable.clear), - contentDescription = null, - modifier = Modifier.size(SwitchDefaults.IconSize), - ) - } - }, - colors = switchColors - ) - }, - colors = listItemColors, - enabled = isPlus, - shapes = segmentedListItemShapes(3, 4) + shapes = segmentedListItemShapes(2, 3) ) } diff --git a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/settingsScreen/screens/WidgetsSettings.kt b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/settingsScreen/screens/WidgetsSettings.kt new file mode 100644 index 00000000..fcb30bc7 --- /dev/null +++ b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/settingsScreen/screens/WidgetsSettings.kt @@ -0,0 +1,314 @@ +/* + * Copyright (c) 2026 Nishant Mishra + * + * This file is part of Tomato - a minimalist pomodoro timer for Android. + * + * Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU + * General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tomato. + * If not, see . + */ + +package org.nsh07.pomodoro.ui.settingsScreen.screens + +import androidx.compose.animation.animateColorAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.FilledTonalIconButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.LargeFlexibleTopAppBar +import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme.colorScheme +import androidx.compose.material3.MaterialTheme.typography +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Slider +import androidx.compose.material3.SliderState +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.WavySlider +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +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.input.nestedscroll.nestedScroll +import androidx.compose.ui.unit.dp +import androidx.window.core.layout.WindowSizeClass.Companion.WIDTH_DP_EXPANDED_LOWER_BOUND +import org.jetbrains.compose.resources.painterResource +import org.jetbrains.compose.resources.stringResource +import org.nsh07.pomodoro.ui.mergePaddingValues +import org.nsh07.pomodoro.ui.settingsScreen.components.PlusDivider +import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsAction +import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsState +import org.nsh07.pomodoro.ui.theme.CustomColors.detailPaneTopBarColors +import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors +import org.nsh07.pomodoro.ui.theme.CustomColors.topBarColors +import org.nsh07.pomodoro.ui.theme.LocalAppFonts +import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.PANE_MAX_WIDTH +import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.bottomListItemShape +import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.cardShape +import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape +import org.nsh07.pomodoro.ui.topBarWindowInsets +import tomato.shared.generated.resources.Res +import tomato.shared.generated.resources.arrow_back +import tomato.shared.generated.resources.back +import tomato.shared.generated.resources.background_role +import tomato.shared.generated.resources.check +import tomato.shared.generated.resources.clear +import tomato.shared.generated.resources.opacity +import tomato.shared.generated.resources.settings +import tomato.shared.generated.resources.widgets + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) +@Composable +fun WidgetsSettings( + settingsState: SettingsState, + contentPadding: PaddingValues, + isPlus: Boolean, + opacitySliderState: SliderState, + onAction: (SettingsAction) -> Unit, + setShowPaywall: (Boolean) -> Unit, + onBack: () -> Unit, + modifier: Modifier = Modifier +) { + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() + + val widthExpanded = currentWindowAdaptiveInfo() + .windowSizeClass + .isWidthAtLeastBreakpoint(WIDTH_DP_EXPANDED_LOWER_BOUND) + + val barColors = if (widthExpanded) detailPaneTopBarColors + else topBarColors + + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .fillMaxSize() + .background(barColors.containerColor) + ) { + Scaffold( + topBar = { + LargeFlexibleTopAppBar( + windowInsets = topBarWindowInsets(), + title = { + Text( + stringResource(Res.string.widgets), + fontFamily = LocalAppFonts.current.topBarTitle + ) + }, + subtitle = { + Text(stringResource(Res.string.settings)) + }, + navigationIcon = { + if (!widthExpanded) + FilledTonalIconButton( + onClick = onBack, + shapes = IconButtonDefaults.shapes(), + colors = IconButtonDefaults.filledTonalIconButtonColors( + containerColor = listItemColors.containerColor + ) + ) { + Icon( + painterResource(Res.drawable.arrow_back), + stringResource(Res.string.back) + ) + } + }, + colors = barColors, + scrollBehavior = scrollBehavior + ) + }, + containerColor = barColors.containerColor, + modifier = modifier + .widthIn(max = PANE_MAX_WIDTH) + .nestedScroll(scrollBehavior.nestedScrollConnection) + ) { innerPadding -> + val insets = mergePaddingValues(innerPadding, contentPadding) + LazyColumn( + verticalArrangement = Arrangement.spacedBy(2.dp), + contentPadding = insets, + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp) + ) { + item { + Spacer(Modifier.height(14.dp)) + } + + item { + WidgetPreviewCard( + opacity = settingsState.widgetOpacity, + backgroundRole = settingsState.widgetBackgroundRole + ) + } + + item { + Spacer(Modifier.height(12.dp)) + } + + item { + Column(Modifier.background(listItemColors.containerColor, topListItemShape)) { + ListItem( + leadingContent = { + Icon(painterResource(Res.drawable.clear), null) + }, + headlineContent = { + Text(stringResource(Res.string.opacity)) + }, + supportingContent = { + Text("${(opacitySliderState.value * 100).toInt()}%") + }, + colors = listItemColors, + modifier = Modifier.clip(cardShape) + ) + WavySlider( + state = opacitySliderState, + enabled = isPlus, + modifier = Modifier + .padding(start = (16 * 2 + 24).dp, end = 16.dp, bottom = 12.dp) + ) + } + } + + if (!isPlus) { + item { PlusDivider(setShowPaywall) } + } + + item { + Column(Modifier.background(listItemColors.containerColor, bottomListItemShape)) { + ListItem( + leadingContent = { + Icon(painterResource(Res.drawable.palette), null) + }, + headlineContent = { + Text(stringResource(Res.string.background_role)) + }, + supportingContent = { + Text(settingsState.widgetBackgroundRole) + }, + colors = listItemColors, + modifier = Modifier.clip(cardShape) + ) + RoleDotsList( + selectedRole = settingsState.widgetBackgroundRole, + enabled = isPlus, + onRoleSelected = { onAction(SettingsAction.SaveWidgetBackgroundRole(it)) }, + modifier = Modifier.padding(start = (16 * 2 + 24).dp, end = 16.dp, bottom = 12.dp) + ) + } + } + + item { Spacer(Modifier.height(12.dp)) } + } + } + } +} + +@Composable +fun WidgetPreviewCard( + opacity: Float, + backgroundRole: String, + modifier: Modifier = Modifier +) { + val backgroundColor = when (backgroundRole) { + "surface" -> colorScheme.surface + "surfaceVariant" -> colorScheme.surfaceVariant + "primaryContainer" -> colorScheme.primaryContainer + "secondaryContainer" -> colorScheme.secondaryContainer + "tertiaryContainer" -> colorScheme.tertiaryContainer + else -> colorScheme.surface + } + + Box( + modifier = modifier + .fillMaxWidth() + .height(160.dp) + .background(colorScheme.surfaceVariant.copy(alpha = 0.5f), RoundedCornerShape(24.dp)) + .padding(16.dp), + contentAlignment = Alignment.Center + ) { + Box( + modifier = Modifier + .size(120.dp) + .background(backgroundColor.copy(alpha = opacity), RoundedCornerShape(24.dp)), + contentAlignment = Alignment.Center + ) { + Text( + "25:00", + style = typography.headlineMedium, + fontFamily = LocalAppFonts.current.topBarTitle, + color = if (opacity < 0.5f && backgroundColor == colorScheme.surface) colorScheme.onSurfaceVariant else colorScheme.onSurface + ) + } + } +} + +@Composable +fun RoleDotsList( + selectedRole: String, + enabled: Boolean, + onRoleSelected: (String) -> Unit, + modifier: Modifier = Modifier +) { + val roles = listOf( + "surface" to colorScheme.surface, + "surfaceVariant" to colorScheme.surfaceVariant, + "primaryContainer" to colorScheme.primaryContainer, + "secondaryContainer" to colorScheme.secondaryContainer, + "tertiaryContainer" to colorScheme.tertiaryContainer + ) + + LazyRow( + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = modifier + ) { + items(roles) { (role, color) -> + val isSelected = role == selectedRole + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .size(32.dp) + .clip(CircleShape) + .background(animateColorAsState(if (enabled) color else color.copy(alpha = 0.3f)).value) + .clickable(enabled = enabled) { onRoleSelected(role) } + ) { + if (isSelected) { + Icon( + painterResource(Res.drawable.check), + contentDescription = null, + tint = if (color == colorScheme.surface || color == colorScheme.surfaceVariant) colorScheme.primary else colorScheme.onPrimaryContainer, + modifier = Modifier.size(16.dp) + ) + } + } + } + } +} diff --git a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsAction.kt b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsAction.kt index 61c0177f..62dfbc76 100644 --- a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsAction.kt +++ b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsAction.kt @@ -29,7 +29,8 @@ sealed interface SettingsAction { data class SaveSingleProgressBar(val enabled: Boolean) : SettingsAction data class SaveAutostartNextSession(val enabled: Boolean) : SettingsAction data class SaveSecureAod(val enabled: Boolean) : SettingsAction - data class SaveTransparentWidgets(val enabled: Boolean) : SettingsAction + data class SaveWidgetOpacity(val opacity: Float) : SettingsAction + data class SaveWidgetBackgroundRole(val role: String) : SettingsAction data class SaveAlarmSound(val uri: String?) : SettingsAction data class SaveTheme(val theme: String) : SettingsAction data class SaveColorScheme(val color: Color) : SettingsAction diff --git a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsState.kt b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsState.kt index 5bd1b8dd..44927cdb 100644 --- a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsState.kt +++ b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsState.kt @@ -36,7 +36,8 @@ data class SettingsState( val singleProgressBar: Boolean = false, val autostartNextSession: Boolean = false, val secureAod: Boolean = true, - val transparentWidgets: Boolean = false, + val widgetOpacity: Float = 1.0f, + val widgetBackgroundRole: String = "surface", val isShowingEraseDataDialog: Boolean = false, val vibrationOnDuration: Long = 1000L, diff --git a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt index 1f153943..6d7e6dd5 100644 --- a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt +++ b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt @@ -92,6 +92,14 @@ class SettingsViewModel( ) } + val widgetOpacitySliderState by lazy { + SliderState( + value = _settingsState.value.widgetOpacity, + valueRange = 0f..1f, + onValueChangeFinished = ::updateWidgetOpacity + ) + } + private var focusFlowCollectionJob: Job? = null private var shortBreakFlowCollectionJob: Job? = null private var longBreakFlowCollectionJob: Job? = null @@ -106,7 +114,8 @@ class SettingsViewModel( is SettingsAction.SaveSingleProgressBar -> saveSingleProgressBar(action.enabled) is SettingsAction.SaveAutostartNextSession -> saveAutostartNextSession(action.enabled) is SettingsAction.SaveSecureAod -> saveSecureAod(action.enabled) - is SettingsAction.SaveTransparentWidgets -> saveTransparentWidgets(action.enabled) + is SettingsAction.SaveWidgetOpacity -> saveWidgetOpacity(action.opacity) + is SettingsAction.SaveWidgetBackgroundRole -> saveWidgetBackgroundRole(action.role) is SettingsAction.SaveColorScheme -> saveColorScheme(action.color) is SettingsAction.SaveTheme -> saveTheme(action.theme) is SettingsAction.SaveBlackTheme -> saveBlackTheme(action.enabled) @@ -361,14 +370,40 @@ class SettingsViewModel( } } - private fun saveTransparentWidgets(enabled: Boolean) { + private fun updateWidgetOpacity() { + viewModelScope.launch(Dispatchers.IO) { + _settingsState.update { currentState -> + currentState.copy( + widgetOpacity = widgetOpacitySliderState.value + ) + } + preferenceRepository.saveIntPreference( + "widget_opacity", + (widgetOpacitySliderState.value * 100).toInt() + ) + } + } + + private fun saveWidgetOpacity(opacity: Float) { viewModelScope.launch { _settingsState.update { currentState -> - currentState.copy(transparentWidgets = enabled) + currentState.copy(widgetOpacity = opacity) } - preferenceRepository.saveBooleanPreference( - "transparent_widgets", - enabled + preferenceRepository.saveIntPreference( + "widget_opacity", + (opacity * 100).toInt() + ) + } + } + + private fun saveWidgetBackgroundRole(role: String) { + viewModelScope.launch { + _settingsState.update { currentState -> + currentState.copy(widgetBackgroundRole = role) + } + preferenceRepository.saveStringPreference( + "widget_background_role", + role ) } } From 1dcef5842008ed7d20cbe760cafba10fd839bc4a Mon Sep 17 00:00:00 2001 From: saberr26 Date: Sat, 9 May 2026 17:34:15 +0100 Subject: [PATCH 12/20] fix: resolve compilation errors in Navigation.kt and WidgetsSettings.kt --- shared/src/commonMain/kotlin/org/nsh07/pomodoro/Navigation.kt | 4 ++++ .../pomodoro/ui/settingsScreen/screens/WidgetsSettings.kt | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/Navigation.kt b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/Navigation.kt index c745e3a7..6fcfdc59 100644 --- a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/Navigation.kt +++ b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/Navigation.kt @@ -24,17 +24,21 @@ import tomato.shared.generated.resources.alarm import tomato.shared.generated.resources.alarm_sound import tomato.shared.generated.resources.always_on_display import tomato.shared.generated.resources.appearance +import tomato.shared.generated.resources.background_role import tomato.shared.generated.resources.black_theme +import tomato.shared.generated.resources.clocks import tomato.shared.generated.resources.color_scheme import tomato.shared.generated.resources.dnd import tomato.shared.generated.resources.durations import tomato.shared.generated.resources.media_volume_for_alarm +import tomato.shared.generated.resources.opacity import tomato.shared.generated.resources.palette import tomato.shared.generated.resources.sound import tomato.shared.generated.resources.theme import tomato.shared.generated.resources.timer import tomato.shared.generated.resources.timer_filled import tomato.shared.generated.resources.vibrate +import tomato.shared.generated.resources.widgets val settingsScreens = listOf( SettingsNavItem( diff --git a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/settingsScreen/screens/WidgetsSettings.kt b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/settingsScreen/screens/WidgetsSettings.kt index fcb30bc7..f7a00862 100644 --- a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/settingsScreen/screens/WidgetsSettings.kt +++ b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/settingsScreen/screens/WidgetsSettings.kt @@ -51,7 +51,6 @@ import androidx.compose.material3.Slider import androidx.compose.material3.SliderState import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.WavySlider import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -84,6 +83,7 @@ import tomato.shared.generated.resources.background_role import tomato.shared.generated.resources.check import tomato.shared.generated.resources.clear import tomato.shared.generated.resources.opacity +import tomato.shared.generated.resources.palette import tomato.shared.generated.resources.settings import tomato.shared.generated.resources.widgets @@ -189,7 +189,7 @@ fun WidgetsSettings( colors = listItemColors, modifier = Modifier.clip(cardShape) ) - WavySlider( + Slider( state = opacitySliderState, enabled = isPlus, modifier = Modifier From d3d9475614f72d6ff8503208e006d1f06203222c Mon Sep 17 00:00:00 2001 From: saberr26 Date: Sat, 9 May 2026 17:54:52 +0100 Subject: [PATCH 13/20] fix: improve widget responsiveness and instant updates, add more roles --- .../org/nsh07/pomodoro/service/AndroidTimerHelper.kt | 6 ++++++ .../org/nsh07/pomodoro/widget/HistoryAppWidget.kt | 11 ++++++----- .../java/org/nsh07/pomodoro/widget/TimerAppWidget.kt | 10 ++++++---- .../java/org/nsh07/pomodoro/widget/TodayAppWidget.kt | 10 ++++++---- .../kotlin/org/nsh07/pomodoro/service/TimerHelper.kt | 1 + .../ui/settingsScreen/screens/WidgetsSettings.kt | 10 ++++++---- .../ui/settingsScreen/viewModel/SettingsViewModel.kt | 3 +++ 7 files changed, 34 insertions(+), 17 deletions(-) diff --git a/androidApp/src/main/java/org/nsh07/pomodoro/service/AndroidTimerHelper.kt b/androidApp/src/main/java/org/nsh07/pomodoro/service/AndroidTimerHelper.kt index bfde2dda..971fa727 100644 --- a/androidApp/src/main/java/org/nsh07/pomodoro/service/AndroidTimerHelper.kt +++ b/androidApp/src/main/java/org/nsh07/pomodoro/service/AndroidTimerHelper.kt @@ -71,4 +71,10 @@ class AndroidTimerHelper(private val context: Context) : TimerHelper { e.printStackTrace() } } + + override fun updateWidgets() { + androidx.glance.appwidget.updateAll(context, org.nsh07.pomodoro.widget.TimerAppWidget()) + androidx.glance.appwidget.updateAll(context, org.nsh07.pomodoro.widget.TodayAppWidget()) + androidx.glance.appwidget.updateAll(context, org.nsh07.pomodoro.widget.HistoryAppWidget()) + } } \ No newline at end of file diff --git a/androidApp/src/main/java/org/nsh07/pomodoro/widget/HistoryAppWidget.kt b/androidApp/src/main/java/org/nsh07/pomodoro/widget/HistoryAppWidget.kt index 61ea398c..df686f81 100644 --- a/androidApp/src/main/java/org/nsh07/pomodoro/widget/HistoryAppWidget.kt +++ b/androidApp/src/main/java/org/nsh07/pomodoro/widget/HistoryAppWidget.kt @@ -111,27 +111,28 @@ class HistoryAppWidget : GlanceAppWidget(), KoinComponent { val size = LocalSize.current val scope = rememberCoroutineScope() val roundedCornersSupported = Build.VERSION.SDK_INT >= 31 - val backgroundColor = when (backgroundRole) { + val backgroundRoleColor = when (backgroundRole) { "surface" -> colors.surface "surfaceVariant" -> colors.surfaceVariant "primaryContainer" -> colors.primaryContainer "secondaryContainer" -> colors.secondaryContainer "tertiaryContainer" -> colors.tertiaryContainer + "accent2_800" -> ColorProvider(Color(0xFF3B4D3C)) else -> colors.surface - }.getColor(context).copy(alpha = opacity) - val widgetBackground = ColorProvider(backgroundColor) + } Column( modifier = GlanceModifier .fillMaxSize() .then( - if (roundedCornersSupported) GlanceModifier.background(widgetBackground) + if (roundedCornersSupported) GlanceModifier.background(backgroundRoleColor) else GlanceModifier.background( ImageProvider(R.drawable.rounded_24dp), - colorFilter = ColorFilter.tint(widgetBackground) + colorFilter = ColorFilter.tint(backgroundRoleColor) ) ) + .background(ColorProvider(Color.Black.copy(alpha = 1f - opacity))) .clickable(actionStartActivity()) ) { TitleBar( diff --git a/androidApp/src/main/java/org/nsh07/pomodoro/widget/TimerAppWidget.kt b/androidApp/src/main/java/org/nsh07/pomodoro/widget/TimerAppWidget.kt index cf6e20d4..51280790 100644 --- a/androidApp/src/main/java/org/nsh07/pomodoro/widget/TimerAppWidget.kt +++ b/androidApp/src/main/java/org/nsh07/pomodoro/widget/TimerAppWidget.kt @@ -94,14 +94,15 @@ class TimerAppWidget : GlanceAppWidget(), KoinComponent { val secondaryButtonColor = if (!breakMode) colors.tertiary else colors.primary val onSecondaryButtonColor = if (!breakMode) colors.onTertiary else colors.onPrimary - val backgroundColor = when (backgroundRole) { + val backgroundRoleColor = when (backgroundRole) { "surface" -> colors.surface "surfaceVariant" -> colors.surfaceVariant "primaryContainer" -> colors.primaryContainer "secondaryContainer" -> colors.secondaryContainer "tertiaryContainer" -> colors.tertiaryContainer + "accent2_800" -> ColorProvider(Color(0xFF3B4D3C)) // Placeholder for accent2_800 dark role else -> colors.surface - }.getColor(context).copy(alpha = opacity) + } Box( modifier = GlanceModifier @@ -120,8 +121,9 @@ class TimerAppWidget : GlanceAppWidget(), KoinComponent { .size(circleSize) .background( ImageProvider(R.drawable.rounded_full), - colorFilter = ColorFilter.tint(ColorProvider(backgroundColor)) + colorFilter = ColorFilter.tint(backgroundRoleColor) ) + .background(ColorProvider(Color.Black.copy(alpha = 1f - opacity))) ) { val clockHeight = (circleSize.value * 0.25f) if (timerState.alarmRinging) { @@ -144,7 +146,7 @@ class TimerAppWidget : GlanceAppWidget(), KoinComponent { if (!timerState.alarmRinging) { Row( - GlanceModifier + modifier = GlanceModifier .background( ImageProvider(R.drawable.rounded_24dp), colorFilter = ColorFilter.tint(secondaryButtonColor) diff --git a/androidApp/src/main/java/org/nsh07/pomodoro/widget/TodayAppWidget.kt b/androidApp/src/main/java/org/nsh07/pomodoro/widget/TodayAppWidget.kt index 046c858e..705ad73d 100644 --- a/androidApp/src/main/java/org/nsh07/pomodoro/widget/TodayAppWidget.kt +++ b/androidApp/src/main/java/org/nsh07/pomodoro/widget/TodayAppWidget.kt @@ -106,25 +106,27 @@ class TodayAppWidget : GlanceAppWidget(), KoinComponent { val context = LocalContext.current val size = LocalSize.current val scope = rememberCoroutineScope() - val backgroundColor = when (backgroundRole) { + val backgroundRoleColor = when (backgroundRole) { "surface" -> colors.surface "surfaceVariant" -> colors.surfaceVariant "primaryContainer" -> colors.primaryContainer "secondaryContainer" -> colors.secondaryContainer "tertiaryContainer" -> colors.tertiaryContainer + "accent2_800" -> ColorProvider(Color(0xFF3B4D3C)) else -> colors.surface - }.getColor(context).copy(alpha = opacity) + } Box( contentAlignment = Alignment.TopEnd, modifier = GlanceModifier .then( - if (Build.VERSION.SDK_INT >= 31) GlanceModifier.background(ColorProvider(backgroundColor)) + if (Build.VERSION.SDK_INT >= 31) GlanceModifier.background(backgroundRoleColor) else GlanceModifier.background( ImageProvider(R.drawable.rounded_24dp), - colorFilter = ColorFilter.tint(ColorProvider(backgroundColor)) + colorFilter = ColorFilter.tint(backgroundRoleColor) ) ) + .background(ColorProvider(Color.Black.copy(alpha = 1f - opacity))) .padding(16.dp) .clickable(actionStartActivity()) ) { diff --git a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/service/TimerHelper.kt b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/service/TimerHelper.kt index c2c9443a..e83f4c1b 100644 --- a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/service/TimerHelper.kt +++ b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/service/TimerHelper.kt @@ -21,4 +21,5 @@ import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerAction interface TimerHelper { fun onAction(action: TimerAction) + fun updateWidgets() } diff --git a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/settingsScreen/screens/WidgetsSettings.kt b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/settingsScreen/screens/WidgetsSettings.kt index f7a00862..71f47881 100644 --- a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/settingsScreen/screens/WidgetsSettings.kt +++ b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/settingsScreen/screens/WidgetsSettings.kt @@ -283,10 +283,12 @@ fun RoleDotsList( "surfaceVariant" to colorScheme.surfaceVariant, "primaryContainer" to colorScheme.primaryContainer, "secondaryContainer" to colorScheme.secondaryContainer, - "tertiaryContainer" to colorScheme.tertiaryContainer + "tertiaryContainer" to colorScheme.tertiaryContainer, + "accent2_800" to Color(0xFF3B4D3C) // Approximation for dark theme accent2_800 ) LazyRow( + contentPadding = PaddingValues(end = 16.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = modifier ) { @@ -295,7 +297,7 @@ fun RoleDotsList( Box( contentAlignment = Alignment.Center, modifier = Modifier - .size(32.dp) + .size(40.dp) .clip(CircleShape) .background(animateColorAsState(if (enabled) color else color.copy(alpha = 0.3f)).value) .clickable(enabled = enabled) { onRoleSelected(role) } @@ -304,8 +306,8 @@ fun RoleDotsList( Icon( painterResource(Res.drawable.check), contentDescription = null, - tint = if (color == colorScheme.surface || color == colorScheme.surfaceVariant) colorScheme.primary else colorScheme.onPrimaryContainer, - modifier = Modifier.size(16.dp) + tint = if (role == "surface" || role == "surfaceVariant") colorScheme.primary else Color.White, + modifier = Modifier.size(20.dp) ) } } diff --git a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt index 6d7e6dd5..f72a6922 100644 --- a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt +++ b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt @@ -381,6 +381,7 @@ class SettingsViewModel( "widget_opacity", (widgetOpacitySliderState.value * 100).toInt() ) + timerHelper.updateWidgets() } } @@ -393,6 +394,7 @@ class SettingsViewModel( "widget_opacity", (opacity * 100).toInt() ) + timerHelper.updateWidgets() } } @@ -405,6 +407,7 @@ class SettingsViewModel( "widget_background_role", role ) + timerHelper.updateWidgets() } } From 140e3aacfb074122c18d9ee8adea244572bfc028 Mon Sep 17 00:00:00 2001 From: saberr26 Date: Sat, 9 May 2026 18:09:15 +0100 Subject: [PATCH 14/20] fix: resolve compilation errors in AndroidTimerHelper and fix widget background transparency --- .../pomodoro/service/AndroidTimerHelper.kt | 19 ++++++++++++++++--- .../nsh07/pomodoro/widget/HistoryAppWidget.kt | 9 +++++---- .../nsh07/pomodoro/widget/TimerAppWidget.kt | 18 +++++++++++------- .../nsh07/pomodoro/widget/TodayAppWidget.kt | 9 +++++---- 4 files changed, 37 insertions(+), 18 deletions(-) diff --git a/androidApp/src/main/java/org/nsh07/pomodoro/service/AndroidTimerHelper.kt b/androidApp/src/main/java/org/nsh07/pomodoro/service/AndroidTimerHelper.kt index 971fa727..d66b48ce 100644 --- a/androidApp/src/main/java/org/nsh07/pomodoro/service/AndroidTimerHelper.kt +++ b/androidApp/src/main/java/org/nsh07/pomodoro/service/AndroidTimerHelper.kt @@ -20,7 +20,14 @@ package org.nsh07.pomodoro.service import android.content.Context import android.content.Intent import android.util.Log +import androidx.glance.appwidget.updateAll +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerAction +import org.nsh07.pomodoro.widget.HistoryAppWidget +import org.nsh07.pomodoro.widget.TimerAppWidget +import org.nsh07.pomodoro.widget.TodayAppWidget /** * Helper class that holds a reference to [Context] and helps call [Context.startService] in @@ -73,8 +80,14 @@ class AndroidTimerHelper(private val context: Context) : TimerHelper { } override fun updateWidgets() { - androidx.glance.appwidget.updateAll(context, org.nsh07.pomodoro.widget.TimerAppWidget()) - androidx.glance.appwidget.updateAll(context, org.nsh07.pomodoro.widget.TodayAppWidget()) - androidx.glance.appwidget.updateAll(context, org.nsh07.pomodoro.widget.HistoryAppWidget()) + CoroutineScope(Dispatchers.IO).launch { + try { + TimerAppWidget().updateAll(context) + TodayAppWidget().updateAll(context) + HistoryAppWidget().updateAll(context) + } catch (e: Exception) { + Log.e("AndroidTimerHelper", "Error updating widgets: ${e.message}") + } + } } } \ No newline at end of file diff --git a/androidApp/src/main/java/org/nsh07/pomodoro/widget/HistoryAppWidget.kt b/androidApp/src/main/java/org/nsh07/pomodoro/widget/HistoryAppWidget.kt index df686f81..3d505cc7 100644 --- a/androidApp/src/main/java/org/nsh07/pomodoro/widget/HistoryAppWidget.kt +++ b/androidApp/src/main/java/org/nsh07/pomodoro/widget/HistoryAppWidget.kt @@ -111,7 +111,7 @@ class HistoryAppWidget : GlanceAppWidget(), KoinComponent { val size = LocalSize.current val scope = rememberCoroutineScope() val roundedCornersSupported = Build.VERSION.SDK_INT >= 31 - val backgroundRoleColor = when (backgroundRole) { + val backgroundRoleColorProvider = when (backgroundRole) { "surface" -> colors.surface "surfaceVariant" -> colors.surfaceVariant "primaryContainer" -> colors.primaryContainer @@ -121,18 +121,19 @@ class HistoryAppWidget : GlanceAppWidget(), KoinComponent { else -> colors.surface } + val finalBackgroundColor = ColorProvider(backgroundRoleColorProvider.getColor(context).copy(alpha = opacity)) + Column( modifier = GlanceModifier .fillMaxSize() .then( - if (roundedCornersSupported) GlanceModifier.background(backgroundRoleColor) + if (roundedCornersSupported) GlanceModifier.background(finalBackgroundColor) else GlanceModifier.background( ImageProvider(R.drawable.rounded_24dp), - colorFilter = ColorFilter.tint(backgroundRoleColor) + colorFilter = ColorFilter.tint(finalBackgroundColor) ) ) - .background(ColorProvider(Color.Black.copy(alpha = 1f - opacity))) .clickable(actionStartActivity()) ) { TitleBar( diff --git a/androidApp/src/main/java/org/nsh07/pomodoro/widget/TimerAppWidget.kt b/androidApp/src/main/java/org/nsh07/pomodoro/widget/TimerAppWidget.kt index 51280790..5028e969 100644 --- a/androidApp/src/main/java/org/nsh07/pomodoro/widget/TimerAppWidget.kt +++ b/androidApp/src/main/java/org/nsh07/pomodoro/widget/TimerAppWidget.kt @@ -94,16 +94,18 @@ class TimerAppWidget : GlanceAppWidget(), KoinComponent { val secondaryButtonColor = if (!breakMode) colors.tertiary else colors.primary val onSecondaryButtonColor = if (!breakMode) colors.onTertiary else colors.onPrimary - val backgroundRoleColor = when (backgroundRole) { + val backgroundRoleColorProvider = when (backgroundRole) { "surface" -> colors.surface "surfaceVariant" -> colors.surfaceVariant "primaryContainer" -> colors.primaryContainer "secondaryContainer" -> colors.secondaryContainer "tertiaryContainer" -> colors.tertiaryContainer - "accent2_800" -> ColorProvider(Color(0xFF3B4D3C)) // Placeholder for accent2_800 dark role + "accent2_800" -> ColorProvider(Color(0xFF3B4D3C)) else -> colors.surface } + val finalBackgroundColor = ColorProvider(backgroundRoleColorProvider.getColor(context).copy(alpha = opacity)) + Box( modifier = GlanceModifier .fillMaxSize() @@ -114,16 +116,18 @@ class TimerAppWidget : GlanceAppWidget(), KoinComponent { .clickable(actionStartActivity()), contentAlignment = Alignment.Center ) { - Box(contentAlignment = Alignment.TopEnd) { + Box( + modifier = GlanceModifier.size(circleSize), + contentAlignment = Alignment.TopEnd + ) { Box( contentAlignment = Alignment.Center, modifier = GlanceModifier - .size(circleSize) + .fillMaxSize() .background( ImageProvider(R.drawable.rounded_full), - colorFilter = ColorFilter.tint(backgroundRoleColor) + colorFilter = ColorFilter.tint(finalBackgroundColor) ) - .background(ColorProvider(Color.Black.copy(alpha = 1f - opacity))) ) { val clockHeight = (circleSize.value * 0.25f) if (timerState.alarmRinging) { @@ -177,7 +181,7 @@ class TimerAppWidget : GlanceAppWidget(), KoinComponent { Box( contentAlignment = Alignment.BottomStart, - modifier = GlanceModifier.size(circleSize) + modifier = GlanceModifier.fillMaxSize() ) { SquareIconButton( imageProvider = diff --git a/androidApp/src/main/java/org/nsh07/pomodoro/widget/TodayAppWidget.kt b/androidApp/src/main/java/org/nsh07/pomodoro/widget/TodayAppWidget.kt index 705ad73d..ce4fcd69 100644 --- a/androidApp/src/main/java/org/nsh07/pomodoro/widget/TodayAppWidget.kt +++ b/androidApp/src/main/java/org/nsh07/pomodoro/widget/TodayAppWidget.kt @@ -106,7 +106,7 @@ class TodayAppWidget : GlanceAppWidget(), KoinComponent { val context = LocalContext.current val size = LocalSize.current val scope = rememberCoroutineScope() - val backgroundRoleColor = when (backgroundRole) { + val backgroundRoleColorProvider = when (backgroundRole) { "surface" -> colors.surface "surfaceVariant" -> colors.surfaceVariant "primaryContainer" -> colors.primaryContainer @@ -116,17 +116,18 @@ class TodayAppWidget : GlanceAppWidget(), KoinComponent { else -> colors.surface } + val finalBackgroundColor = ColorProvider(backgroundRoleColorProvider.getColor(context).copy(alpha = opacity)) + Box( contentAlignment = Alignment.TopEnd, modifier = GlanceModifier .then( - if (Build.VERSION.SDK_INT >= 31) GlanceModifier.background(backgroundRoleColor) + if (Build.VERSION.SDK_INT >= 31) GlanceModifier.background(finalBackgroundColor) else GlanceModifier.background( ImageProvider(R.drawable.rounded_24dp), - colorFilter = ColorFilter.tint(backgroundRoleColor) + colorFilter = ColorFilter.tint(finalBackgroundColor) ) ) - .background(ColorProvider(Color.Black.copy(alpha = 1f - opacity))) .padding(16.dp) .clickable(actionStartActivity()) ) { From b2a8886f17a9c01ae0f4358b45061e496cbc3ce9 Mon Sep 17 00:00:00 2001 From: saberr26 Date: Sat, 9 May 2026 18:18:21 +0100 Subject: [PATCH 15/20] fix: resolve multiplatform compilation and hide widgets settings on desktop --- .../commonMain/kotlin/org/nsh07/pomodoro/Navigation.kt | 8 +++++--- .../kotlin/org/nsh07/pomodoro/timer/DesktopTimerHelper.kt | 6 +++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/Navigation.kt b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/Navigation.kt index 6fcfdc59..86c78647 100644 --- a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/Navigation.kt +++ b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/Navigation.kt @@ -39,8 +39,10 @@ import tomato.shared.generated.resources.timer import tomato.shared.generated.resources.timer_filled import tomato.shared.generated.resources.vibrate import tomato.shared.generated.resources.widgets +import org.nsh07.pomodoro.utils.OS +import org.nsh07.pomodoro.utils.currentOS -val settingsScreens = listOf( +val settingsScreens = listOfNotNull( SettingsNavItem( Screen.Settings.Timer, Res.drawable.timer_filled, @@ -64,10 +66,10 @@ val settingsScreens = listOf( Res.string.appearance, listOf(Res.string.theme, Res.string.color_scheme, Res.string.black_theme) ), - SettingsNavItem( + if (currentOS == OS.ANDROID) SettingsNavItem( Screen.Settings.Widgets, Res.drawable.clocks, Res.string.widgets, listOf(Res.string.opacity, Res.string.background_role) - ) + ) else null ) diff --git a/shared/src/jvmMain/kotlin/org/nsh07/pomodoro/timer/DesktopTimerHelper.kt b/shared/src/jvmMain/kotlin/org/nsh07/pomodoro/timer/DesktopTimerHelper.kt index 1529fad2..eff91bd6 100644 --- a/shared/src/jvmMain/kotlin/org/nsh07/pomodoro/timer/DesktopTimerHelper.kt +++ b/shared/src/jvmMain/kotlin/org/nsh07/pomodoro/timer/DesktopTimerHelper.kt @@ -82,6 +82,10 @@ class DesktopTimerHelper( } } + override fun updateWidgets() { + // No-op for desktop + } + private fun toggleTimer() { timerManager.toggleTimer( scope = timerScope, @@ -145,4 +149,4 @@ class DesktopTimerHelper( if (settingsState.autostartNextSession && !fromAutoStop) // auto start next session toggleTimer() } -} \ No newline at end of file +} From f30ca908d9e5e2151aa1c540b59e8ce521ebefaa Mon Sep 17 00:00:00 2001 From: saberr26 Date: Mon, 11 May 2026 20:32:23 +0100 Subject: [PATCH 16/20] feat(widgets): migrate configuration to Room and add per-widget customization --- androidApp/keystore.jks | Bin 0 -> 2744 bytes androidApp/src/main/AndroidManifest.xml | 9 + .../pomodoro/service/AndroidTimerHelper.kt | 28 +- .../nsh07/pomodoro/widget/HistoryAppWidget.kt | 303 ++++----- .../nsh07/pomodoro/widget/TimerAppWidget.kt | 174 +++-- .../nsh07/pomodoro/widget/TodayAppWidget.kt | 101 ++- .../widget/WidgetConfigurationActivity.kt | 100 +++ .../components/HorizontalStackedBarGlance.kt | 8 +- androidApp/src/main/res/values/themes.xml | 6 + .../src/main/res/xml/history_widget_info.xml | 2 + .../src/main/res/xml/timer_widget_info.xml | 2 + .../src/main/res/xml/today_widget_info.xml | 2 + .../3.json | 175 ++++++ .../4.json | 189 ++++++ .../5.json | 196 ++++++ .../kotlin/org/nsh07/pomodoro/di/modules.kt | 20 +- .../kotlin/org/nsh07/pomodoro/Navigation.kt | 8 +- .../org/nsh07/pomodoro/data/AppDatabase.kt | 12 +- .../nsh07/pomodoro/data/StateRepository.kt | 21 +- .../pomodoro/data/WidgetConfiguration.kt | 41 ++ .../pomodoro/data/WidgetConfigurationDao.kt | 44 ++ .../org/nsh07/pomodoro/service/TimerHelper.kt | 1 + .../ui/settingsScreen/SettingsScreen.kt | 23 - .../screens/WidgetConfigurationScreen.kt | 595 ++++++++++++++++++ .../settingsScreen/screens/WidgetsSettings.kt | 316 ---------- .../viewModel/SettingsAction.kt | 2 - .../settingsScreen/viewModel/SettingsState.kt | 2 - .../viewModel/SettingsViewModel.kt | 51 -- .../viewModel/WidgetConfigurationViewModel.kt | 163 +++++ .../pomodoro/timer/DesktopTimerHelper.kt | 4 + 30 files changed, 1893 insertions(+), 705 deletions(-) create mode 100644 androidApp/keystore.jks create mode 100644 androidApp/src/main/java/org/nsh07/pomodoro/widget/WidgetConfigurationActivity.kt create mode 100644 shared/schemas/org.nsh07.pomodoro.data.AppDatabase/3.json create mode 100644 shared/schemas/org.nsh07.pomodoro.data.AppDatabase/4.json create mode 100644 shared/schemas/org.nsh07.pomodoro.data.AppDatabase/5.json create mode 100644 shared/src/commonMain/kotlin/org/nsh07/pomodoro/data/WidgetConfiguration.kt create mode 100644 shared/src/commonMain/kotlin/org/nsh07/pomodoro/data/WidgetConfigurationDao.kt create mode 100644 shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/settingsScreen/screens/WidgetConfigurationScreen.kt delete mode 100644 shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/settingsScreen/screens/WidgetsSettings.kt create mode 100644 shared/src/commonMain/kotlin/org/nsh07/pomodoro/ui/settingsScreen/viewModel/WidgetConfigurationViewModel.kt diff --git a/androidApp/keystore.jks b/androidApp/keystore.jks new file mode 100644 index 0000000000000000000000000000000000000000..08ff5252549e971f7d0bf9a157f5753dd79e1d43 GIT binary patch literal 2744 zcma);XIK)97RLc`KuBshipqrxvv7pt%~vziOw9q#hNHBsP;+It(Q<1>V&Us3N8(g|I1p#0X@(k^xA(dCspo#W_rrPq&-wq)IUj$|!H}S}ARr%x1id5xLEk%b zZ&Mg32+Soxt-vIx`A^#vL*g(0uZTYfOybY{X=nT_vcR7IY{8&FP%eqz@TYkK%B8T4?Zvu4Hi*DoE!am>_$kexVyV|;z|^l1nyT%9M$U_)}R`aeQ&fTu1SYGh^Aat z?N%j#9WB#}=g1WaaJIX4n6YKu?tp4MlYU6ziO6yr8biHCzrR1Qa`(t`Kn|4)MINnXzWwEUT!Me(8!^Z@;~9 zO}{a|@GMZrNzt<^w6H0}$ zMV-Q`EHSbV8^!i6ezBE#u-co?d?Ukg#J4de=vpnIPUJ|Fa|WPal<(2EdUJzl_`Kvw z>8ekpp~1*=ar@tW_^X zg5+i+)bdmNEf^DWCusj(d$f1`_ld|Py{8octnD`?*4lO)1H$OX`06z5iGlQSDpgEm zs4hv*j$wqpSgve*h&TzW8pKJ25T7-ASkAi~<8FC`3IDM)!`f>xq&t{Hv?DIhnm}I_ zpvf<>VzNt4DHM&plg1(_ib<-WAMi={RhLx3vf&mV>#$uPHWN6aG<`X3%2>XOqJz>= zp1L(lUtBKE@oz4>lI5BY?!31+xwi4xpY8ZP=50x*zOiDT?ykN}();iGFNt%ix~i*B zO}PJQfKF*q?kqBkLB01p8@2#>c))|Z8UgKFl1FU&AK+l&>CwDoG{?@LIKON@0o z6yZugSC=j^H8JrpRx9RiNAbjCb7{x+*G&)3XzGowAc)R=*s07h_DT2X!&9^ALh_;$ z%#bWv1}cf~5;;qdq^ghSU(5N})z=EU@lYm_)Xj*8pH7ROzQvf$U&ffH0D`WyZkTw& zDI!zJ!er$ZRC!J3+-tpZyBAK#5NTo3QAx%p1rE%?$a}x@XA7}Lo+(UB=@3Ptqy((7 zTytv!UktkG(~)R6c}=2LsOL;LZ$Rf5eyMdCaAVmRnD$qVTGbCnfvr;2%+7dC^OLx>KTyonfh6GU0gx4#2RMoO+w%>hEI%;rJ)Gyc`If{mHrD4SI#dj6_aOZK)VI6LRkfR~Da zPjVOc{iG1H^VaH2Uvs30V?Ns`BJ#_t5P<+Z>FFg_hW&K=RVxW!)+=@~haH9uX%H4; z+R|&ztu!W@o6iiZ-l(>Gpc3Hc%E$k=L- z9My;;{c1@cKeVN8h(Z3sB?O@kKMVr|0DJ%hfZIQck^LQtt0TZ-crSv7jJmeAj@EH4 zZB2FE<5&y{oc3!-P#~8CCjGR+K|sJyx&1o<{x3|&ZkKBal+hT0Jtk_+Q)uU7VV`;b z52mY-`!uYqUJLzjHSK3ExVl7=z<|@^j-35U!~Smy?7m#?KX#-L|2a>ns)zTYu{v4q zil?wNu$kO6#+lT07q6>)+?Q64lv1N~_1=c5cv|)>HWd2isP76~j&eHx3hnaG1boseVfk zOW9;yVLH0TJ2)jQY|-$-r!(86$>G72Sl2ZCPKhH$cM}aP0NFdd8!yVL1&o>^~$^$8iK7a^5Vjkc?A0!Lq)awFj( z8CLHc>uFaX`TS9RIPayY2o4`Aymq$_qy1c!Xp%tHyTVmpS4d~d)5^00o`=g!T0HB5 z$2qmAI;~|^q8?`Na_LFTofE@0khP(8Q1k2sEnaquxJV%M?R53`mITT;Vh_;kM{>KT zD$Ziz+U~P>QIgApLG61PI>T~J$fu|5Byl6P$ZvJ2tL6q$-&TfP|6Us9#@rbWb;Fy; zHtewE?}v!@O#pMlXjHpY`(AkEF3&s4c*YM>2_Rv~X52?fPPkT$o zX!2syriNEi#aW2y9JNNxyNR{O;e2E_-JUk@La*QHlnR>Y%L;9A^G;thG}t^pJKFKw zXN>3dd?L9&&^Q2FG3bgeQm9H%5&4`q!rd~oK@i#A1*4msHI8@Hoj$W=DMTG&`SKsl zW%YTRZ0E1Q_KYZF(U)4~+|;1i9y$+EFH8N>LsxKvBo8&GYtI5snpUJ8RXVem~WB6i0u7O Yu*dN@cl?9J&*I{|*oG%IdH*2jUqCJvC;$Ke literal 0 HcmV?d00001 diff --git a/androidApp/src/main/AndroidManifest.xml b/androidApp/src/main/AndroidManifest.xml index c8a5467f..bff1c3ba 100644 --- a/androidApp/src/main/AndroidManifest.xml +++ b/androidApp/src/main/AndroidManifest.xml @@ -48,6 +48,15 @@ + + + + + + diff --git a/androidApp/src/main/java/org/nsh07/pomodoro/service/AndroidTimerHelper.kt b/androidApp/src/main/java/org/nsh07/pomodoro/service/AndroidTimerHelper.kt index d66b48ce..9c1a5071 100644 --- a/androidApp/src/main/java/org/nsh07/pomodoro/service/AndroidTimerHelper.kt +++ b/androidApp/src/main/java/org/nsh07/pomodoro/service/AndroidTimerHelper.kt @@ -17,9 +17,11 @@ package org.nsh07.pomodoro.service +import android.appwidget.AppWidgetManager import android.content.Context import android.content.Intent import android.util.Log +import androidx.glance.appwidget.GlanceAppWidgetManager import androidx.glance.appwidget.updateAll import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -90,4 +92,28 @@ class AndroidTimerHelper(private val context: Context) : TimerHelper { } } } -} \ No newline at end of file + + override fun updateWidget(appWidgetId: Int) { + CoroutineScope(Dispatchers.IO).launch { + try { + val appWidgetManager = AppWidgetManager.getInstance(context) + val info = appWidgetManager.getAppWidgetInfo(appWidgetId) + val className = info?.provider?.className ?: "" + + val widget = when { + className.contains("TimerWidgetReceiver") -> TimerAppWidget() + className.contains("TodayWidgetReceiver") -> TodayAppWidget() + className.contains("HistoryWidgetReceiver") -> HistoryAppWidget() + else -> null + } + + widget?.let { + val glanceId = GlanceAppWidgetManager(context).getGlanceIdBy(appWidgetId) + it.update(context, glanceId) + } + } catch (e: Exception) { + Log.e("AndroidTimerHelper", "Error updating widget $appWidgetId: ${e.message}") + } + } + } +} diff --git a/androidApp/src/main/java/org/nsh07/pomodoro/widget/HistoryAppWidget.kt b/androidApp/src/main/java/org/nsh07/pomodoro/widget/HistoryAppWidget.kt index 3d505cc7..ee1b9d4d 100644 --- a/androidApp/src/main/java/org/nsh07/pomodoro/widget/HistoryAppWidget.kt +++ b/androidApp/src/main/java/org/nsh07/pomodoro/widget/HistoryAppWidget.kt @@ -40,8 +40,8 @@ import androidx.glance.LocalSize import androidx.glance.action.actionStartActivity import androidx.glance.action.clickable import androidx.glance.appwidget.GlanceAppWidget +import androidx.glance.appwidget.GlanceAppWidgetManager import androidx.glance.appwidget.SizeMode -import androidx.glance.appwidget.components.TitleBar import androidx.glance.appwidget.cornerRadius import androidx.glance.appwidget.provideContent import androidx.glance.appwidget.updateAll @@ -56,11 +56,14 @@ import androidx.glance.layout.fillMaxSize import androidx.glance.layout.fillMaxWidth import androidx.glance.layout.height import androidx.glance.layout.padding +import androidx.glance.layout.size import androidx.glance.layout.width import androidx.glance.material3.ColorProviders import androidx.glance.preview.ExperimentalGlancePreviewApi import androidx.glance.preview.Preview import androidx.glance.text.FontWeight +import androidx.glance.text.Text +import androidx.glance.text.TextStyle import androidx.glance.unit.ColorProvider import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch @@ -70,7 +73,7 @@ import org.nsh07.pomodoro.MainActivity import org.nsh07.pomodoro.R import org.nsh07.pomodoro.data.Stat import org.nsh07.pomodoro.data.StatRepository -import org.nsh07.pomodoro.data.StateRepository +import org.nsh07.pomodoro.data.WidgetConfigurationDao import org.nsh07.pomodoro.ui.theme.lightScheme import org.nsh07.pomodoro.utils.millisecondsToHoursMinutes import org.nsh07.pomodoro.widget.TomatoWidgetSize.Width4 @@ -85,11 +88,23 @@ class HistoryAppWidget : GlanceAppWidget(), KoinComponent { id: GlanceId ) { val statRepository: StatRepository = get() - val stateRepository: StateRepository = get() + val widgetConfigurationDao: WidgetConfigurationDao = get() + val appWidgetId = GlanceAppWidgetManager(context).getAppWidgetId(id) + + // Fetch once so the very first frame already has the saved config (no flash of defaults), + // then observe the Flow so any subsequent save recomposes immediately. + val initialConfig = widgetConfigurationDao.getConfiguration(appWidgetId) + val configFlow = widgetConfigurationDao.getConfigurationFlow(appWidgetId) + val history = statRepository.getLastNDaysStats(30).first().reversed() provideContent { - val settingsState by stateRepository.settingsState.collectAsState() + val config by configFlow.collectAsState(initial = initialConfig) + val opacity = config?.opacity ?: 1.0f + val backgroundRole = config?.backgroundRole ?: "onSecondary" + val foregroundRole = config?.foregroundRole ?: "primary" + val headerRole = config?.headerRole ?: "onPrimary" + val barCornerRadius = config?.barCornerRadius ?: 16 val size = LocalSize.current val history = history.takeLast(((size.width.value - 32) / 24).toInt()) key(size) { @@ -97,8 +112,11 @@ class HistoryAppWidget : GlanceAppWidget(), KoinComponent { Content( history, history.maxBy { it.totalFocusTime() }.totalFocusTime(), - settingsState.widgetOpacity, - settingsState.widgetBackgroundRole + opacity, + backgroundRole, + foregroundRole, + headerRole, + barCornerRadius ) } } @@ -106,56 +124,68 @@ class HistoryAppWidget : GlanceAppWidget(), KoinComponent { } @Composable - private fun Content(history: List, maxFocus: Long, opacity: Float, backgroundRole: String) { + private fun Content( + history: List, + maxFocus: Long, + opacity: Float, + backgroundRole: String, + foregroundRole: String, + headerRole: String, + barCornerRadius: Int + ) { val context = LocalContext.current val size = LocalSize.current val scope = rememberCoroutineScope() - val roundedCornersSupported = Build.VERSION.SDK_INT >= 31 - val backgroundRoleColorProvider = when (backgroundRole) { - "surface" -> colors.surface - "surfaceVariant" -> colors.surfaceVariant - "primaryContainer" -> colors.primaryContainer - "secondaryContainer" -> colors.secondaryContainer - "tertiaryContainer" -> colors.tertiaryContainer - "accent2_800" -> ColorProvider(Color(0xFF3B4D3C)) - else -> colors.surface - } - val finalBackgroundColor = ColorProvider(backgroundRoleColorProvider.getColor(context).copy(alpha = opacity)) + val backgroundRoleColorProvider = getWidgetColorProvider(backgroundRole) + val foregroundRoleColorProvider = getWidgetColorProvider(foregroundRole) + val headerRoleColorProvider = getWidgetColorProvider(headerRole) Column( modifier = GlanceModifier .fillMaxSize() - .then( - if (roundedCornersSupported) GlanceModifier.background(finalBackgroundColor) - else GlanceModifier.background( - ImageProvider(R.drawable.rounded_24dp), - colorFilter = ColorFilter.tint(finalBackgroundColor) - ) + .background( + imageProvider = ImageProvider(R.drawable.rounded_24dp), + colorFilter = ColorFilter.tint(backgroundRoleColorProvider), + alpha = opacity ) .clickable(actionStartActivity()) ) { - TitleBar( - startIcon = ImageProvider(R.drawable.tomato_logo_notification), - title = context.getString(R.string.focus_history), - actions = { - if (size.width >= Width4) { - Box(GlanceModifier.padding(horizontal = 16.dp)) { - Image( - provider = ImageProvider(R.drawable.refresh), - contentDescription = null, - colorFilter = ColorFilter.tint(colors.onSurface), - modifier = GlanceModifier - .cornerRadius(24.dp) - .clickable { - scope.launch { this@HistoryAppWidget.updateAll(context) } - } - ) - } - } - }, - ) + // Custom Title Bar implementation to ensure icon tinting + Row( + modifier = GlanceModifier.fillMaxWidth().padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Image( + provider = ImageProvider(R.drawable.tomato_logo_notification), + contentDescription = null, + colorFilter = ColorFilter.tint(headerRoleColorProvider), + modifier = GlanceModifier.size(24.dp) + ) + Spacer(GlanceModifier.width(8.dp)) + Text( + text = context.getString(R.string.focus_history), + style = TextStyle( + color = headerRoleColorProvider, + fontSize = typography.titleSmall.fontSize, + fontWeight = FontWeight.Bold + ) + ) + Spacer(GlanceModifier.defaultWeight()) + if (size.width >= Width4) { + Image( + provider = ImageProvider(R.drawable.refresh), + contentDescription = null, + colorFilter = ColorFilter.tint(headerRoleColorProvider), + modifier = GlanceModifier + .cornerRadius(12.dp) + .clickable { + scope.launch { this@HistoryAppWidget.updateAll(context) } + } + ) + } + } Column( GlanceModifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp) @@ -168,7 +198,7 @@ class HistoryAppWidget : GlanceAppWidget(), KoinComponent { context.getString(R.string.hours_and_minutes_format) ) + " ", typography.headlineSmall.fontSize.value, - colors.onSurface, + headerRoleColorProvider, fontWeight = FontWeight.Bold ) @@ -177,7 +207,7 @@ class HistoryAppWidget : GlanceAppWidget(), KoinComponent { context, context.getString(R.string.focus_per_day_avg), typography.bodyMedium.fontSize.value, - colors.onSurfaceVariant, + headerRoleColorProvider, isClock = false, modifier = GlanceModifier.padding(bottom = 2.8.dp) ) @@ -202,16 +232,8 @@ class HistoryAppWidget : GlanceAppWidget(), KoinComponent { .height( (84 * (it.totalFocusTime().toFloat() / maxFocus)).dp ) - .then( - if (roundedCornersSupported) - GlanceModifier - .background(colors.primary) - .cornerRadius(16.dp) - else GlanceModifier.background( - ImageProvider(R.drawable.rounded_16dp), - colorFilter = ColorFilter.tint(colors.primary) - ) - ) + .background(foregroundRoleColorProvider) + .cornerRadius(barCornerRadius.dp) ) } } @@ -222,145 +244,38 @@ class HistoryAppWidget : GlanceAppWidget(), KoinComponent { } } - @OptIn(ExperimentalGlancePreviewApi::class) - @Preview(widthDp = 400, heightDp = 216) @Composable - private fun ContentPreview() { - val history = listOf( - Stat( - date = LocalDate.of(2026, 3, 12), - focusTimeQ1 = 1617943 + 7200000, - focusTimeQ2 = 5704591, - focusTimeQ3 = 556490, - focusTimeQ4 = 1200498, - breakTime = 3939448 - ), - Stat( - date = LocalDate.of(2026, 3, 13), - focusTimeQ1 = 1128282 + 7200000, - focusTimeQ2 = 4590524, - focusTimeQ3 = 7747202, - focusTimeQ4 = 1119272, - breakTime = 311887 - ), - Stat( - date = LocalDate.of(2026, 3, 14), - focusTimeQ1 = 1418079 + 7200000, - focusTimeQ2 = 8141785, - focusTimeQ3 = 5208864, - focusTimeQ4 = 2793210, - breakTime = 2873581 - ), - Stat( - date = LocalDate.of(2026, 3, 15), - focusTimeQ1 = 38960 + 7200000, - focusTimeQ2 = 9544172, - focusTimeQ3 = 2216626, - focusTimeQ4 = 1424242, - breakTime = 4635775 - ), - Stat( - date = LocalDate.of(2026, 3, 16), - focusTimeQ1 = 948108 + 7200000, - focusTimeQ2 = 7715257, - focusTimeQ3 = 648629, - focusTimeQ4 = 319655, - breakTime = 1710029 - ), - Stat( - date = LocalDate.of(2026, 3, 17), - focusTimeQ1 = 1673932 + 7200000, - focusTimeQ2 = 7368028, - focusTimeQ3 = 6028910, - focusTimeQ4 = 2134210, - breakTime = 2811766 - ), - Stat( - date = LocalDate.of(2026, 3, 18), - focusTimeQ1 = 435688 + 7200000, - focusTimeQ2 = 9487983, - focusTimeQ3 = 248276, - focusTimeQ4 = 913853, - breakTime = 162869 - ), - Stat( - date = LocalDate.of(2026, 3, 19), - focusTimeQ1 = 1579291 + 7200000, - focusTimeQ2 = 3743344, - focusTimeQ3 = 3383617, - focusTimeQ4 = 3424645, - breakTime = 3443552 - ), - Stat( - date = LocalDate.of(2026, 3, 20), - focusTimeQ1 = 522247 + 7200000, - focusTimeQ2 = 7156785, - focusTimeQ3 = 5190730, - focusTimeQ4 = 3086522, - breakTime = 3768831 - ), - Stat( - date = LocalDate.of(2026, 3, 21), - focusTimeQ1 = 310048 + 7200000, - focusTimeQ2 = 5901959, - focusTimeQ3 = 441673, - focusTimeQ4 = 3562958, - breakTime = 5470220 - ), - Stat( - date = LocalDate.of(2026, 3, 22), - focusTimeQ1 = 1200000 + 7200000, - focusTimeQ2 = 4000000, - focusTimeQ3 = 3000000, - focusTimeQ4 = 1000000, - breakTime = 2000000 - ), - Stat( - date = LocalDate.of(2026, 3, 23), - focusTimeQ1 = 500000 + 7200000, - focusTimeQ2 = 8000000, - focusTimeQ3 = 1000000, - focusTimeQ4 = 500000, - breakTime = 1000000 - ), - Stat( - date = LocalDate.of(2026, 3, 24), - focusTimeQ1 = 2000000 + 7200000, - focusTimeQ2 = 2000000, - focusTimeQ3 = 2000000, - focusTimeQ4 = 2000000, - breakTime = 3000000 - ), - Stat( - date = LocalDate.of(2026, 3, 25), - focusTimeQ1 = 0 + 7200000, - focusTimeQ2 = 10000000, - focusTimeQ3 = 0, - focusTimeQ4 = 0, - breakTime = 500000 - ), - Stat( - date = LocalDate.of(2026, 3, 26), - focusTimeQ1 = 3000000 + 7200000, - focusTimeQ2 = 3000000, - focusTimeQ3 = 3000000, - focusTimeQ4 = 3000000, - breakTime = 4000000 - ) - ) - GlanceTheme(colors = ColorProviders(lightScheme)) { - Box(GlanceModifier.background(Color.White)) { - Box( - GlanceModifier.cornerRadius(32.dp) - ) { - Content( - history = history, - maxFocus = history.maxBy { it.totalFocusTime() }.totalFocusTime(), - opacity = 1.0f, - backgroundRole = "surface" - ) - } - } + private fun getWidgetColorProvider(role: String): ColorProvider { + return when (role) { + "black" -> ColorProvider(Color.Black) + "primary" -> colors.primary + "onPrimary" -> colors.onPrimary + "primaryContainer" -> colors.primaryContainer + "onPrimaryContainer" -> colors.onPrimaryContainer + "secondary" -> colors.secondary + "onSecondary" -> colors.onSecondary + "secondaryContainer" -> colors.secondaryContainer + "onSecondaryContainer" -> colors.onSecondaryContainer + "tertiary" -> colors.tertiary + "onTertiary" -> colors.onTertiary + "tertiaryContainer" -> colors.tertiaryContainer + "onTertiaryContainer" -> colors.onTertiaryContainer + "error" -> colors.error + "onError" -> colors.onError + "errorContainer" -> colors.errorContainer + "onErrorContainer" -> colors.onErrorContainer + "surface" -> colors.surface + "onSurface" -> colors.onSurface + "surfaceVariant" -> colors.surfaceVariant + "onSurfaceVariant" -> colors.onSurfaceVariant + "outline" -> colors.outline + "background" -> colors.background + "onBackground" -> colors.onBackground + "inverseSurface" -> colors.inverseSurface + "inverseOnSurface" -> colors.inverseOnSurface + "inversePrimary" -> colors.inversePrimary + "white" -> ColorProvider(Color.White) + else -> colors.surface } } } diff --git a/androidApp/src/main/java/org/nsh07/pomodoro/widget/TimerAppWidget.kt b/androidApp/src/main/java/org/nsh07/pomodoro/widget/TimerAppWidget.kt index 5028e969..cba48ac0 100644 --- a/androidApp/src/main/java/org/nsh07/pomodoro/widget/TimerAppWidget.kt +++ b/androidApp/src/main/java/org/nsh07/pomodoro/widget/TimerAppWidget.kt @@ -36,6 +36,7 @@ import androidx.glance.action.actionParametersOf import androidx.glance.action.actionStartActivity import androidx.glance.action.clickable import androidx.glance.appwidget.GlanceAppWidget +import androidx.glance.appwidget.GlanceAppWidgetManager import androidx.glance.appwidget.SizeMode import androidx.glance.appwidget.action.actionRunCallback import androidx.glance.appwidget.appWidgetBackground @@ -59,6 +60,7 @@ import org.koin.core.component.get import org.nsh07.pomodoro.MainActivity import org.nsh07.pomodoro.R import org.nsh07.pomodoro.data.StateRepository +import org.nsh07.pomodoro.data.WidgetConfigurationDao import org.nsh07.pomodoro.service.TimerService import org.nsh07.pomodoro.ui.theme.lightScheme import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerMode @@ -74,37 +76,57 @@ class TimerAppWidget : GlanceAppWidget(), KoinComponent { id: GlanceId ) { val stateRepository: StateRepository = get() + val widgetConfigurationDao: WidgetConfigurationDao = get() + val appWidgetId = GlanceAppWidgetManager(context).getAppWidgetId(id) + + // Fetch once so the very first frame already has the saved config (no flash of defaults), + // then observe the Flow so any subsequent save recomposes immediately. + val initialConfig = widgetConfigurationDao.getConfiguration(appWidgetId) + val configFlow = widgetConfigurationDao.getConfigurationFlow(appWidgetId) + provideContent { val timerState by stateRepository.timerState.collectAsState() - val settingsState by stateRepository.settingsState.collectAsState() + val config by configFlow.collectAsState(initial = initialConfig) + val opacity = config?.opacity ?: 1.0f + val backgroundRole = config?.backgroundRole ?: "onSecondary" + val foregroundRole = config?.foregroundRole ?: "primary" + val headerRole = config?.headerRole ?: "onPrimary" + val skipButtonRole = config?.skipButtonRole ?: "tertiary" + val onSkipButtonRole = config?.onSkipButtonRole ?: "onTertiary" GlanceTheme { - Content(timerState, settingsState.widgetOpacity, settingsState.widgetBackgroundRole) + Content(timerState, opacity, backgroundRole, foregroundRole, headerRole, skipButtonRole, onSkipButtonRole) } } } @Composable - private fun Content(timerState: TimerState, opacity: Float, backgroundRole: String) { + private fun Content( + timerState: TimerState, + opacity: Float, + backgroundRole: String, + foregroundRole: String, + headerRole: String, + skipButtonRole: String, + onSkipButtonRole: String + ) { val size = LocalSize.current val context = LocalContext.current val circleSize = minOf(256.dp, size.width - 8.dp, size.height - 8.dp) val breakMode = timerState.timerMode == TimerMode.SHORT_BREAK || timerState.timerMode == TimerMode.LONG_BREAK - val secondaryButtonColor = if (!breakMode) colors.tertiary else colors.primary - val onSecondaryButtonColor = if (!breakMode) colors.onTertiary else colors.onPrimary + val backgroundRoleColorProvider = getWidgetColorProvider(backgroundRole) + val foregroundRoleColorProvider = getWidgetColorProvider(foregroundRole) + val headerRoleColorProvider = getWidgetColorProvider(headerRole) + val skipButtonRoleColorProvider = getWidgetColorProvider(skipButtonRole) + val onSkipButtonRoleColorProvider = getWidgetColorProvider(onSkipButtonRole) - val backgroundRoleColorProvider = when (backgroundRole) { - "surface" -> colors.surface - "surfaceVariant" -> colors.surfaceVariant - "primaryContainer" -> colors.primaryContainer - "secondaryContainer" -> colors.secondaryContainer - "tertiaryContainer" -> colors.tertiaryContainer - "accent2_800" -> ColorProvider(Color(0xFF3B4D3C)) - else -> colors.surface - } - - val finalBackgroundColor = ColorProvider(backgroundRoleColorProvider.getColor(context).copy(alpha = opacity)) + // Play/pause button uses foregroundRole for background, headerRole for icons + val buttonColor = foregroundRoleColorProvider + val onButtonColor = headerRoleColorProvider + // Skip/restart button group has independent colors + val skipColor = skipButtonRoleColorProvider + val onSkipColor = onSkipButtonRoleColorProvider Box( modifier = GlanceModifier @@ -118,23 +140,28 @@ class TimerAppWidget : GlanceAppWidget(), KoinComponent { ) { Box( modifier = GlanceModifier.size(circleSize), - contentAlignment = Alignment.TopEnd + contentAlignment = Alignment.Center ) { Box( - contentAlignment = Alignment.Center, modifier = GlanceModifier .fillMaxSize() .background( - ImageProvider(R.drawable.rounded_full), - colorFilter = ColorFilter.tint(finalBackgroundColor) + imageProvider = ImageProvider(R.drawable.rounded_full), + colorFilter = ColorFilter.tint(backgroundRoleColorProvider), + alpha = opacity ) + ) {} + + Box( + contentAlignment = Alignment.Center, + modifier = GlanceModifier.fillMaxSize() ) { val clockHeight = (circleSize.value * 0.25f) if (timerState.alarmRinging) { Image( ImageProvider(R.drawable.alarm), contentDescription = context.getString(R.string.stop_alarm), - colorFilter = ColorFilter.tint(colors.primary), + colorFilter = ColorFilter.tint(foregroundRoleColorProvider), modifier = GlanceModifier.size(clockHeight.dp) ) } else { @@ -142,40 +169,44 @@ class TimerAppWidget : GlanceAppWidget(), KoinComponent { context, timerState.timeStr, clockHeight, - if (!breakMode) colors.primary - else colors.tertiary + foregroundRoleColorProvider ) } } if (!timerState.alarmRinging) { - Row( - modifier = GlanceModifier - .background( - ImageProvider(R.drawable.rounded_24dp), - colorFilter = ColorFilter.tint(secondaryButtonColor) - ) + Box( + contentAlignment = Alignment.TopEnd, + modifier = GlanceModifier.fillMaxSize() ) { - if (timerState.timerRunning) + Row( + modifier = GlanceModifier + .background( + imageProvider = ImageProvider(R.drawable.rounded_24dp), + colorFilter = ColorFilter.tint(skipColor) + ) + ) { + if (timerState.timerRunning) + CircleIconButton( + imageProvider = ImageProvider(R.drawable.restart), + contentDescription = context.getString(R.string.restart), + onClick = actionRunCallback( + actionParametersOf(key to TimerService.Actions.RESET) + ), + backgroundColor = skipColor, + contentColor = onSkipColor + ) + CircleIconButton( - imageProvider = ImageProvider(R.drawable.restart), - contentDescription = context.getString(R.string.restart), + imageProvider = ImageProvider(R.drawable.skip_next), + contentDescription = context.getString(R.string.skip_to_next), onClick = actionRunCallback( - actionParametersOf(key to TimerService.Actions.RESET) + actionParametersOf(key to TimerService.Actions.SKIP) ), - backgroundColor = secondaryButtonColor, - contentColor = onSecondaryButtonColor + backgroundColor = skipColor, + contentColor = onSkipColor ) - - CircleIconButton( - imageProvider = ImageProvider(R.drawable.skip_next), - contentDescription = context.getString(R.string.skip_to_next), - onClick = actionRunCallback( - actionParametersOf(key to TimerService.Actions.SKIP) - ), - backgroundColor = secondaryButtonColor, - contentColor = onSecondaryButtonColor - ) + } } } @@ -191,7 +222,7 @@ class TimerAppWidget : GlanceAppWidget(), KoinComponent { if (!timerState.timerRunning) ImageProvider(R.drawable.play) else ImageProvider(R.drawable.pause) }, - contentDescription = context.getString(R.string.play), + contentDescription = context.getString(R.string.start), onClick = if (timerState.alarmRinging) { actionRunCallback( actionParametersOf(key to TimerService.Actions.STOP_ALARM) @@ -201,20 +232,49 @@ class TimerAppWidget : GlanceAppWidget(), KoinComponent { actionParametersOf(key to TimerService.Actions.TOGGLE) ) }, - backgroundColor = - if (breakMode) - colors.tertiary - else colors.primary, - contentColor = - if (breakMode) - colors.onTertiary - else colors.onPrimary + backgroundColor = buttonColor, + contentColor = onButtonColor ) } } } } + @Composable + private fun getWidgetColorProvider(role: String): ColorProvider { + return when (role) { + "black" -> ColorProvider(Color.Black) + "primary" -> colors.primary + "onPrimary" -> colors.onPrimary + "primaryContainer" -> colors.primaryContainer + "onPrimaryContainer" -> colors.onPrimaryContainer + "secondary" -> colors.secondary + "onSecondary" -> colors.onSecondary + "secondaryContainer" -> colors.secondaryContainer + "onSecondaryContainer" -> colors.onSecondaryContainer + "tertiary" -> colors.tertiary + "onTertiary" -> colors.onTertiary + "tertiaryContainer" -> colors.tertiaryContainer + "onTertiaryContainer" -> colors.onTertiaryContainer + "error" -> colors.error + "onError" -> colors.onError + "errorContainer" -> colors.errorContainer + "onErrorContainer" -> colors.onErrorContainer + "surface" -> colors.surface + "onSurface" -> colors.onSurface + "surfaceVariant" -> colors.surfaceVariant + "onSurfaceVariant" -> colors.onSurfaceVariant + "outline" -> colors.outline + "background" -> colors.background + "onBackground" -> colors.onBackground + "inverseSurface" -> colors.inverseSurface + "inverseOnSurface" -> colors.inverseOnSurface + "inversePrimary" -> colors.inversePrimary + "white" -> ColorProvider(Color.White) + else -> colors.surface + } + } + @OptIn(ExperimentalGlancePreviewApi::class) @Preview(widthDp = 196, heightDp = 196) @Composable @@ -224,7 +284,11 @@ class TimerAppWidget : GlanceAppWidget(), KoinComponent { Content( timerState = TimerState(), opacity = 1.0f, - backgroundRole = "surface" + backgroundRole = "onSecondary", + foregroundRole = "primary", + headerRole = "onPrimary", + skipButtonRole = "tertiary", + onSkipButtonRole = "onTertiary" ) } } diff --git a/androidApp/src/main/java/org/nsh07/pomodoro/widget/TodayAppWidget.kt b/androidApp/src/main/java/org/nsh07/pomodoro/widget/TodayAppWidget.kt index ce4fcd69..33022022 100644 --- a/androidApp/src/main/java/org/nsh07/pomodoro/widget/TodayAppWidget.kt +++ b/androidApp/src/main/java/org/nsh07/pomodoro/widget/TodayAppWidget.kt @@ -40,6 +40,7 @@ import androidx.glance.LocalSize import androidx.glance.action.actionStartActivity import androidx.glance.action.clickable import androidx.glance.appwidget.GlanceAppWidget +import androidx.glance.appwidget.GlanceAppWidgetManager import androidx.glance.appwidget.SizeMode import androidx.glance.appwidget.cornerRadius import androidx.glance.appwidget.provideContent @@ -69,7 +70,7 @@ import org.nsh07.pomodoro.MainActivity import org.nsh07.pomodoro.R import org.nsh07.pomodoro.data.Stat import org.nsh07.pomodoro.data.StatRepository -import org.nsh07.pomodoro.data.StateRepository +import org.nsh07.pomodoro.data.WidgetConfigurationDao import org.nsh07.pomodoro.ui.theme.lightScheme import org.nsh07.pomodoro.utils.millisecondsToHoursMinutes import org.nsh07.pomodoro.utils.millisecondsToMinutes @@ -87,46 +88,54 @@ class TodayAppWidget : GlanceAppWidget(), KoinComponent { id: GlanceId ) { val statRepository: StatRepository = get() - val stateRepository: StateRepository = get() + val widgetConfigurationDao: WidgetConfigurationDao = get() + val appWidgetId = GlanceAppWidgetManager(context).getAppWidgetId(id) + + // Fetch once so the very first frame already has the saved config (no flash of defaults), + // then observe the Flow so any subsequent save recomposes immediately. + val initialConfig = widgetConfigurationDao.getConfiguration(appWidgetId) + val configFlow = widgetConfigurationDao.getConfigurationFlow(appWidgetId) + val stat = statRepository.getTodayStat().first() ?: Stat(LocalDate.now(), 0, 0, 0, 0, 0) provideContent { - val settingsState by stateRepository.settingsState.collectAsState() + val config by configFlow.collectAsState(initial = initialConfig) + val opacity = config?.opacity ?: 1.0f + val backgroundRole = config?.backgroundRole ?: "onSecondary" + val foregroundRole = config?.foregroundRole ?: "primary" + val headerRole = config?.headerRole ?: "onPrimary" key(LocalSize.current) { GlanceTheme { - Content(stat, settingsState.widgetOpacity, settingsState.widgetBackgroundRole) + Content(stat, opacity, backgroundRole, foregroundRole, headerRole) } } } } @Composable - private fun Content(stat: Stat, opacity: Float, backgroundRole: String) { + private fun Content( + stat: Stat, + opacity: Float, + backgroundRole: String, + foregroundRole: String, + headerRole: String + ) { val context = LocalContext.current val size = LocalSize.current val scope = rememberCoroutineScope() - val backgroundRoleColorProvider = when (backgroundRole) { - "surface" -> colors.surface - "surfaceVariant" -> colors.surfaceVariant - "primaryContainer" -> colors.primaryContainer - "secondaryContainer" -> colors.secondaryContainer - "tertiaryContainer" -> colors.tertiaryContainer - "accent2_800" -> ColorProvider(Color(0xFF3B4D3C)) - else -> colors.surface - } - val finalBackgroundColor = ColorProvider(backgroundRoleColorProvider.getColor(context).copy(alpha = opacity)) + val backgroundRoleColorProvider = getWidgetColorProvider(backgroundRole) + val foregroundRoleColorProvider = getWidgetColorProvider(foregroundRole) + val headerRoleColorProvider = getWidgetColorProvider(headerRole) Box( contentAlignment = Alignment.TopEnd, modifier = GlanceModifier - .then( - if (Build.VERSION.SDK_INT >= 31) GlanceModifier.background(finalBackgroundColor) - else GlanceModifier.background( - ImageProvider(R.drawable.rounded_24dp), - colorFilter = ColorFilter.tint(finalBackgroundColor) - ) + .background( + imageProvider = ImageProvider(R.drawable.rounded_24dp), + colorFilter = ColorFilter.tint(backgroundRoleColorProvider), + alpha = opacity ) .padding(16.dp) .clickable(actionStartActivity()) @@ -135,7 +144,7 @@ class TodayAppWidget : GlanceAppWidget(), KoinComponent { Text( context.getString(R.string.focus), style = TextStyle( - color = colors.onSurface, + color = headerRoleColorProvider, fontSize = typography.titleMedium.fontSize ) ) @@ -147,7 +156,7 @@ class TodayAppWidget : GlanceAppWidget(), KoinComponent { context.getString(R.string.hours_and_minutes_format) ), typography.displaySmall.fontSize.value, - colors.onSurface, + foregroundRoleColorProvider, fontWeight = FontWeight.Bold, isClock = true, modifier = GlanceModifier.padding(top = 8.dp) @@ -181,7 +190,7 @@ class TodayAppWidget : GlanceAppWidget(), KoinComponent { context.getString(R.string.hours_and_minutes_format) ), typography.bodyLarge.fontSize.value, - colors.onSurfaceVariant, + foregroundRoleColorProvider, fontWeight = FontWeight.Bold ) } @@ -191,7 +200,8 @@ class TodayAppWidget : GlanceAppWidget(), KoinComponent { Spacer(GlanceModifier.height(8.dp)) HorizontalStackedBarGlance( values = values, - width = size.width - 32.dp + width = size.width - 32.dp, + color = foregroundRoleColorProvider ) } } @@ -200,7 +210,7 @@ class TodayAppWidget : GlanceAppWidget(), KoinComponent { Image( provider = ImageProvider(R.drawable.refresh), contentDescription = null, - colorFilter = ColorFilter.tint(colors.onSurface), + colorFilter = ColorFilter.tint(headerRoleColorProvider), modifier = GlanceModifier .cornerRadius(12.dp) .clickable { @@ -211,6 +221,41 @@ class TodayAppWidget : GlanceAppWidget(), KoinComponent { } } + @Composable + private fun getWidgetColorProvider(role: String): ColorProvider { + return when (role) { + "black" -> ColorProvider(Color.Black) + "primary" -> colors.primary + "onPrimary" -> colors.onPrimary + "primaryContainer" -> colors.primaryContainer + "onPrimaryContainer" -> colors.onPrimaryContainer + "secondary" -> colors.secondary + "onSecondary" -> colors.onSecondary + "secondaryContainer" -> colors.secondaryContainer + "onSecondaryContainer" -> colors.onSecondaryContainer + "tertiary" -> colors.tertiary + "onTertiary" -> colors.onTertiary + "tertiaryContainer" -> colors.tertiaryContainer + "onTertiaryContainer" -> colors.onTertiaryContainer + "error" -> colors.error + "onError" -> colors.onError + "errorContainer" -> colors.errorContainer + "onErrorContainer" -> colors.onErrorContainer + "surface" -> colors.surface + "onSurface" -> colors.onSurface + "surfaceVariant" -> colors.surfaceVariant + "onSurfaceVariant" -> colors.onSurfaceVariant + "outline" -> colors.outline + "background" -> colors.background + "onBackground" -> colors.onBackground + "inverseSurface" -> colors.inverseSurface + "inverseOnSurface" -> colors.inverseOnSurface + "inversePrimary" -> colors.inversePrimary + "white" -> ColorProvider(Color.White) + else -> colors.surface + } + } + @OptIn(ExperimentalGlancePreviewApi::class) @Preview(widthDp = 400, heightDp = 216) @Composable @@ -230,7 +275,9 @@ class TodayAppWidget : GlanceAppWidget(), KoinComponent { breakTime = 3939448 ), opacity = 1.0f, - backgroundRole = "surface" + backgroundRole = "onSecondary", + foregroundRole = "primary", + headerRole = "onPrimary" ) } } diff --git a/androidApp/src/main/java/org/nsh07/pomodoro/widget/WidgetConfigurationActivity.kt b/androidApp/src/main/java/org/nsh07/pomodoro/widget/WidgetConfigurationActivity.kt new file mode 100644 index 00000000..6222baa4 --- /dev/null +++ b/androidApp/src/main/java/org/nsh07/pomodoro/widget/WidgetConfigurationActivity.kt @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2026 Nishant Mishra + * + * This file is part of Tomato - a minimalist pomodoro timer for Android. + * + * Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU + * General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tomato. + * If not, see . + */ + +package org.nsh07.pomodoro.widget + +import android.appwidget.AppWidgetManager +import android.content.Intent +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import org.koin.android.ext.android.inject +import org.koin.androidx.viewmodel.ext.android.viewModel +import org.koin.core.parameter.parametersOf +import org.nsh07.pomodoro.ui.settingsScreen.screens.WidgetConfigurationScreen +import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsViewModel +import org.nsh07.pomodoro.ui.settingsScreen.viewModel.WidgetConfigurationViewModel +import org.nsh07.pomodoro.ui.settingsScreen.viewModel.WidgetType +import org.nsh07.pomodoro.ui.theme.TomatoTheme +import org.nsh07.pomodoro.utils.toColor + +class WidgetConfigurationActivity : ComponentActivity() { + + private var appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID + + private val settingsViewModel: SettingsViewModel by inject() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + + // Find the widget id from the intent. + appWidgetId = intent?.extras?.getInt( + AppWidgetManager.EXTRA_APPWIDGET_ID, + AppWidgetManager.INVALID_APPWIDGET_ID + ) ?: AppWidgetManager.INVALID_APPWIDGET_ID + + // If this activity was started with an invalid widget ID, finish with an error. + if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) { + finish() + return + } + + val viewModel: WidgetConfigurationViewModel by viewModel { parametersOf(appWidgetId) } + + // Detect widget type + val appWidgetManager = AppWidgetManager.getInstance(this) + val info = appWidgetManager.getAppWidgetInfo(appWidgetId) + val className = info?.provider?.className ?: "" + + val type = when { + className.contains("TimerWidgetReceiver") -> WidgetType.TIMER + className.contains("TodayWidgetReceiver") -> WidgetType.TODAY + className.contains("HistoryWidgetReceiver") -> WidgetType.HISTORY + else -> WidgetType.UNKNOWN + } + viewModel.setWidgetType(type) + + setContent { + val settingsState = settingsViewModel.settingsState.value + val seed = settingsState.colorScheme.toColor() + + TomatoTheme( + darkTheme = true, // Force dark theme for widget config as it's usually better for overlays + seedColor = seed, + blackTheme = settingsState.blackTheme + ) { + WidgetConfigurationScreen( + viewModel = viewModel, + onDone = { + viewModel.saveAllSettings() + val resultValue = Intent().apply { + putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) + } + setResult(RESULT_OK, resultValue) + finish() + } + ) + } + } + + // Set the result to CANCELED. This will cause the widget host to cancel + // out of the widget placement if the user presses the back button. + setResult(RESULT_CANCELED) + } +} diff --git a/androidApp/src/main/java/org/nsh07/pomodoro/widget/components/HorizontalStackedBarGlance.kt b/androidApp/src/main/java/org/nsh07/pomodoro/widget/components/HorizontalStackedBarGlance.kt index 04e5ccf8..a4be995d 100644 --- a/androidApp/src/main/java/org/nsh07/pomodoro/widget/components/HorizontalStackedBarGlance.kt +++ b/androidApp/src/main/java/org/nsh07/pomodoro/widget/components/HorizontalStackedBarGlance.kt @@ -38,6 +38,7 @@ import androidx.glance.layout.Spacer import androidx.glance.layout.fillMaxWidth import androidx.glance.layout.height import androidx.glance.layout.width +import androidx.glance.unit.ColorProvider import org.nsh07.pomodoro.R import org.nsh07.pomodoro.ui.statsScreen.components.HORIZONTAL_STACKED_BAR_HEIGHT @@ -51,6 +52,7 @@ fun HorizontalStackedBarGlance( values: List, width: Dp, modifier: GlanceModifier = GlanceModifier, + color: ColorProvider = colors.primary, rankList: List = remember(values) { val sortedIndices = values.indices.sortedByDescending { values[it] } val ranks = MutableList(values.size) { 0 } @@ -89,7 +91,7 @@ fun HorizontalStackedBarGlance( GlanceModifier .cornerRadius(4.dp) .background( - colors.primary + color .getColor(context) .copy( (1f - (rankList.getOrNull(index) ?: 0) * 0.1f) @@ -104,7 +106,7 @@ fun HorizontalStackedBarGlance( else GlanceModifier.background( ImageProvider(R.drawable.rounded_4dp), - colorFilter = ColorFilter.tint(colors.primary), + colorFilter = ColorFilter.tint(color), alpha = (1f - (rankList.getOrNull(index) ?: 0) * 0.1f) .coerceAtLeast(0.1f) ) @@ -132,4 +134,4 @@ fun HorizontalStackedBarGlance( ) ) ) {} -} \ No newline at end of file +} diff --git a/androidApp/src/main/res/values/themes.xml b/androidApp/src/main/res/values/themes.xml index bb97f638..d79c6158 100644 --- a/androidApp/src/main/res/values/themes.xml +++ b/androidApp/src/main/res/values/themes.xml @@ -19,6 +19,12 @@ +