diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts index dba61220..ef8ba0c7 100644 --- a/androidApp/build.gradle.kts +++ b/androidApp/build.gradle.kts @@ -132,6 +132,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) @@ -153,6 +154,7 @@ dependencies { implementation(project.dependencies.platform(libs.koin.bom)) implementation(libs.koin.core) implementation(libs.koin.android) + implementation(libs.koin.compose.viewmodel) testImplementation(libs.junit) diff --git a/androidApp/src/main/AndroidManifest.xml b/androidApp/src/main/AndroidManifest.xml index c8a5467f..997c1d1c 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/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/java/org/nsh07/pomodoro/di/androidModules.kt b/androidApp/src/main/java/org/nsh07/pomodoro/di/androidModules.kt index 0c007939..abfa40a7 100644 --- a/androidApp/src/main/java/org/nsh07/pomodoro/di/androidModules.kt +++ b/androidApp/src/main/java/org/nsh07/pomodoro/di/androidModules.kt @@ -33,6 +33,7 @@ import org.koin.dsl.bind import org.koin.dsl.module import org.koin.plugin.module.dsl.create import org.koin.plugin.module.dsl.single +import org.koin.plugin.module.dsl.viewModel import org.nsh07.pomodoro.BuildConfig import org.nsh07.pomodoro.R import org.nsh07.pomodoro.data.AppPreferenceRepository @@ -44,6 +45,7 @@ import org.nsh07.pomodoro.service.AndroidTimerHelper import org.nsh07.pomodoro.service.TimerHelper import org.nsh07.pomodoro.service.TimerManager import org.nsh07.pomodoro.service.addTimerActions +import org.nsh07.pomodoro.widget.config.WidgetConfigurationViewModel val servicesModule = module { single { Dispatchers.IO } @@ -60,6 +62,7 @@ val servicesModule = module { single { create(::createNotificationCompatBuilder) } single() + viewModel() } private fun createAppInfo(): AppInfo = AppInfo(BuildConfig.DEBUG) 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..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,10 +17,19 @@ 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 +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 @@ -71,4 +80,40 @@ class AndroidTimerHelper(private val context: Context) : TimerHelper { e.printStackTrace() } } -} \ No newline at end of file + + override fun updateWidgets() { + 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}") + } + } + } + + 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 fb9612a6..bdf68712 100644 --- a/androidApp/src/main/java/org/nsh07/pomodoro/widget/HistoryAppWidget.kt +++ b/androidApp/src/main/java/org/nsh07/pomodoro/widget/HistoryAppWidget.kt @@ -203,7 +203,22 @@ class HistoryAppWidget : GlanceAppWidget(), KoinComponent { @Preview(widthDp = 400, heightDp = 216) @Composable private fun ContentPreview() { - val history = listOf( + GlanceTheme(colors = ColorProviders(lightScheme)) { + Box(GlanceModifier.background(Color.White)) { + Box( + GlanceModifier.cornerRadius(32.dp) + ) { + Content( + history = previewHistoryData, + maxFocus = previewHistoryData.maxBy { it.totalFocusTime() }.totalFocusTime() + ) + } + } + } + } + + companion object { + val previewHistoryData = listOf( Stat( date = LocalDate.of(2026, 3, 12), focusTimeQ1 = 1617943 + 7200000, @@ -325,17 +340,5 @@ class HistoryAppWidget : GlanceAppWidget(), KoinComponent { 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() - ) - } - } - } } } 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/java/org/nsh07/pomodoro/widget/config/WidgetConfigurationActivity.kt b/androidApp/src/main/java/org/nsh07/pomodoro/widget/config/WidgetConfigurationActivity.kt new file mode 100644 index 00000000..64190993 --- /dev/null +++ b/androidApp/src/main/java/org/nsh07/pomodoro/widget/config/WidgetConfigurationActivity.kt @@ -0,0 +1,109 @@ +/* + * 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.config + +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 androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import org.koin.android.ext.android.inject +import org.koin.androidx.viewmodel.ext.android.viewModel +import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsViewModel +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 = this.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) { + this.finish() + return + } + + val viewModel: WidgetConfigurationViewModel by viewModel() + + // 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 by settingsViewModel.settingsState.collectAsStateWithLifecycle() + + val darkTheme = when (settingsState.theme) { + "dark" -> true + "light" -> false + else -> isSystemInDarkTheme() + } + + val seed = settingsState.colorScheme.toColor() + + val isPlus by settingsViewModel.isPlus.collectAsStateWithLifecycle() + + TomatoTheme( + darkTheme = darkTheme, + seedColor = seed, + blackTheme = settingsState.blackTheme + ) { + WidgetConfigurationScreen( + viewModel = viewModel, + isPlus = isPlus, + onDone = { + viewModel.saveAllSettings(appWidgetId) + val resultValue = Intent().apply { + putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) + } + this.setResult(RESULT_OK, resultValue) + this.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. + this.setResult(RESULT_CANCELED) + } +} \ No newline at end of file diff --git a/androidApp/src/main/java/org/nsh07/pomodoro/widget/config/WidgetConfigurationScreen.kt b/androidApp/src/main/java/org/nsh07/pomodoro/widget/config/WidgetConfigurationScreen.kt new file mode 100644 index 00000000..cdb65bfd --- /dev/null +++ b/androidApp/src/main/java/org/nsh07/pomodoro/widget/config/WidgetConfigurationScreen.kt @@ -0,0 +1,580 @@ +/* + * 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.config + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +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.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.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.Icon +import androidx.compose.material3.LargeFlexibleTopAppBar +import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme.colorScheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Slider +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +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.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import org.koin.compose.viewmodel.koinViewModel +import org.nsh07.pomodoro.R +import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors +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.middleListItemShape +import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape +import org.nsh07.pomodoro.ui.topBarWindowInsets +import kotlin.math.roundToInt + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) +@Composable +fun WidgetConfigurationScreen( + isPlus: Boolean, + onDone: () -> Unit, + modifier: Modifier = Modifier, + viewModel: WidgetConfigurationViewModel = koinViewModel() +) { + val state by viewModel.state.collectAsState() + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() + + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .fillMaxSize() + .background(Color.Transparent) + ) { + Scaffold( + topBar = { + LargeFlexibleTopAppBar( + windowInsets = topBarWindowInsets(), + title = { + Text( + stringResource(R.string.widgets), + fontFamily = LocalAppFonts.current.topBarTitle + ) + }, + subtitle = { + Text( + "Configure ${ + state.widgetType.name.lowercase() + .replaceFirstChar { it.uppercase() } + } Instance" + ) + }, + scrollBehavior = scrollBehavior + ) + }, + bottomBar = { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + contentAlignment = Alignment.Center + ) { + Button( + onClick = onDone, + modifier = Modifier.fillMaxWidth() + ) { + Text("Done") + } + } + }, + containerColor = Color.Transparent, + modifier = modifier + .widthIn(max = PANE_MAX_WIDTH) + .nestedScroll(scrollBehavior.nestedScrollConnection) + ) { innerPadding -> + LazyColumn( + verticalArrangement = Arrangement.spacedBy(2.dp), + contentPadding = innerPadding, + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp) + ) { + item { + Spacer(Modifier.height(14.dp)) + } + + item { + WidgetPreviewCard( + opacity = state.opacity, +// backgroundRole = state.backgroundRole, +// foregroundRole = state.foregroundRole, +// headerRole = state.headerRole, +// skipButtonRole = state.skipButtonRole, +// onSkipButtonRole = state.onSkipButtonRole, +// barCornerRadius = state.barCornerRadius, + widgetType = state.widgetType + ) + } + + item { + Spacer(Modifier.height(12.dp)) + } + + item { + Column( + Modifier.background( + listItemColors.containerColor.copy(alpha = 0.9f), + topListItemShape + ) + ) { + ListItem( + leadingContent = { + Icon(painterResource(R.drawable.clear), null) + }, + headlineContent = { + Text(stringResource(R.string.opacity)) + }, + supportingContent = { + Text("${(state.opacity * 100).toInt()}%") + }, + colors = listItemColors.copy( + containerColor = Color.Transparent, + selectedContainerColor = Color.Transparent, + draggedContainerColor = Color.Transparent, + ), + modifier = Modifier.clip(cardShape) + ) + Slider( + value = state.opacity, + onValueChange = { viewModel.updateOpacityInstant(it) }, + modifier = Modifier + .padding(start = (16 * 2 + 24).dp, end = 16.dp, bottom = 12.dp) + ) + } + } + + item { + Column( + Modifier.background( + listItemColors.containerColor.copy(alpha = 0.9f), + middleListItemShape + ) + ) { + ListItem( + leadingContent = { + Icon(painterResource(R.drawable.palette), null) + }, + headlineContent = { + Text(stringResource(R.string.background_role)) + }, + supportingContent = { + Text(state.backgroundRole) + }, + colors = listItemColors.copy( + containerColor = Color.Transparent, + selectedContainerColor = Color.Transparent, + draggedContainerColor = Color.Transparent, + ), + modifier = Modifier.clip(cardShape) + ) + RoleDotsList( + selectedRole = state.backgroundRole, + onRoleSelected = { viewModel.setBackgroundRole(it) }, + modifier = Modifier.padding( + start = (16 * 2 + 24).dp, + end = 16.dp, + bottom = 12.dp + ) + ) + } + } + + item { + Column( + Modifier.background( + listItemColors.containerColor.copy(alpha = 0.9f), + middleListItemShape + ) + ) { + ListItem( + leadingContent = { + Icon(painterResource(R.drawable.palette), null) + }, + headlineContent = { + Text("Foreground Role") + }, + supportingContent = { + Text(state.foregroundRole) + }, + colors = listItemColors.copy( + containerColor = Color.Transparent, + selectedContainerColor = Color.Transparent, + draggedContainerColor = Color.Transparent, + ), + modifier = Modifier.clip(cardShape) + ) + RoleDotsList( + selectedRole = state.foregroundRole, + onRoleSelected = { viewModel.setForegroundRole(it) }, + modifier = Modifier.padding( + start = (16 * 2 + 24).dp, + end = 16.dp, + bottom = 12.dp + ) + ) + } + } + + item { + Column( + Modifier.background( + listItemColors.containerColor.copy(alpha = 0.9f), + if (state.widgetType == WidgetType.TIMER || state.widgetType == WidgetType.HISTORY) + middleListItemShape else bottomListItemShape + ) + ) { + ListItem( + leadingContent = { + Icon(painterResource(R.drawable.palette), null) + }, + headlineContent = { + Text(if (state.widgetType == WidgetType.TIMER) "Start Button Foreground" else "Header Role") + }, + supportingContent = { + Text(state.headerRole) + }, + colors = listItemColors.copy( + containerColor = Color.Transparent, + selectedContainerColor = Color.Transparent, + draggedContainerColor = Color.Transparent, + ), + modifier = Modifier.clip(cardShape) + ) + RoleDotsList( + selectedRole = state.headerRole, + onRoleSelected = { viewModel.setHeaderRole(it) }, + modifier = Modifier.padding( + start = (16 * 2 + 24).dp, + end = 16.dp, + bottom = 12.dp + ) + ) + } + } + + if (state.widgetType == WidgetType.TIMER) { + item { + Column( + Modifier.background( + listItemColors.containerColor.copy(alpha = 0.9f), + middleListItemShape + ) + ) { + ListItem( + leadingContent = { + Icon(painterResource(R.drawable.palette), null) + }, + headlineContent = { + Text("Skip Button Background") + }, + supportingContent = { + Text(state.skipButtonRole) + }, + colors = listItemColors.copy( + containerColor = Color.Transparent, + selectedContainerColor = Color.Transparent, + draggedContainerColor = Color.Transparent, + ), + modifier = Modifier.clip(cardShape) + ) + RoleDotsList( + selectedRole = state.skipButtonRole, + onRoleSelected = { viewModel.setSkipButtonRole(it) }, + modifier = Modifier.padding( + start = (16 * 2 + 24).dp, + end = 16.dp, + bottom = 12.dp + ) + ) + } + } + + item { + Column( + Modifier.background( + listItemColors.containerColor.copy(alpha = 0.9f), + bottomListItemShape + ) + ) { + ListItem( + leadingContent = { + Icon(painterResource(R.drawable.palette), null) + }, + headlineContent = { + Text("Skip Button Foreground") + }, + supportingContent = { + Text(state.onSkipButtonRole) + }, + colors = listItemColors.copy( + containerColor = Color.Transparent, + selectedContainerColor = Color.Transparent, + draggedContainerColor = Color.Transparent, + ), + modifier = Modifier.clip(cardShape) + ) + RoleDotsList( + selectedRole = state.onSkipButtonRole, + onRoleSelected = { viewModel.setOnSkipButtonRole(it) }, + modifier = Modifier.padding( + start = (16 * 2 + 24).dp, + end = 16.dp, + bottom = 12.dp + ) + ) + } + } + } + + if (state.widgetType == WidgetType.HISTORY) { + item { + Column( + Modifier.background( + listItemColors.containerColor.copy(alpha = 0.9f), + bottomListItemShape + ) + ) { + ListItem( + leadingContent = { + Icon(painterResource(R.drawable.restart), null) + }, + headlineContent = { + Text("Bar Corner Radius") + }, + supportingContent = { + Text("${state.barCornerRadius}dp") + }, + colors = listItemColors.copy( + containerColor = Color.Transparent, + selectedContainerColor = Color.Transparent, + draggedContainerColor = Color.Transparent, + ), + modifier = Modifier.clip(cardShape) + ) + Slider( + value = state.barCornerRadius.toFloat(), + onValueChange = { viewModel.setBarCornerRadius(it.roundToInt()) }, + valueRange = 0f..16f, + steps = 15, + modifier = Modifier + .padding(start = (16 * 2 + 24).dp, end = 16.dp, bottom = 12.dp) + ) + } + } + } + + item { Spacer(Modifier.height(80.dp)) } + } + } + } +} + +@Composable +fun WidgetPreviewCard( + opacity: Float, + modifier: Modifier = Modifier, + background: Color = colorScheme.surfaceContainer, + onSurface: Color = colorScheme.onSurface, + onSurfaceVariant: Color = colorScheme.onSurfaceVariant, + tertiary: Color = colorScheme.tertiary, + primary: Color = colorScheme.primary, + onTertiary: Color = colorScheme.onTertiary, + onPrimary: Color = colorScheme.onPrimary, + widgetType: WidgetType +) { + Box( + modifier = modifier + .fillMaxWidth() + .padding(8.dp), + contentAlignment = Alignment.Center + ) { + when (widgetType) { + WidgetType.TIMER -> { + TimerWidgetPreview( + opacity = opacity, + background = background, + tertiary = tertiary, + onTertiary = onTertiary, + primary = primary, + onPrimary = onPrimary, + modifier = Modifier.size(200.dp) + ) + } + + WidgetType.TODAY -> { + TodayAppWidgetPreview( + background = background, + onSurface = onSurface, + onSurfaceVariant = onSurfaceVariant, + modifier = Modifier + .fillMaxWidth() + .height(200.dp) + ) + } + + WidgetType.HISTORY -> { + HistoryWidgetPreview( + background = background, + onSurface = onSurface, + onSurfaceVariant = onSurfaceVariant, + primary = primary, + modifier = Modifier + .fillMaxWidth() + .height(200.dp) + ) + } + + else -> { + Text("Select a widget type", color = Color.White) + } + } + } +} + +@Composable +fun getRoleColor(role: String): Color { + return when (role) { + "black" -> Color(0xFF000000) + "primary" -> colorScheme.primary + "onPrimary" -> colorScheme.onPrimary + "primaryContainer" -> colorScheme.primaryContainer + "onPrimaryContainer" -> colorScheme.onPrimaryContainer + "secondary" -> colorScheme.secondary + "onSecondary" -> colorScheme.onSecondary + "secondaryContainer" -> colorScheme.secondaryContainer + "onSecondaryContainer" -> colorScheme.onSecondaryContainer + "tertiary" -> colorScheme.tertiary + "onTertiary" -> colorScheme.onTertiary + "tertiaryContainer" -> colorScheme.tertiaryContainer + "onTertiaryContainer" -> colorScheme.onTertiaryContainer + "error" -> colorScheme.error + "onError" -> colorScheme.onError + "errorContainer" -> colorScheme.errorContainer + "onErrorContainer" -> colorScheme.onErrorContainer + "surface" -> colorScheme.surface + "onSurface" -> colorScheme.onSurface + "surfaceVariant" -> colorScheme.surfaceVariant + "onSurfaceVariant" -> colorScheme.onSurfaceVariant + "outline" -> colorScheme.outline + "background" -> colorScheme.background + "onBackground" -> colorScheme.onBackground + "inverseSurface" -> colorScheme.inverseSurface + "inverseOnSurface" -> colorScheme.inverseOnSurface + "inversePrimary" -> colorScheme.inversePrimary + "white" -> Color.White + else -> colorScheme.surface + } +} + +@Composable +fun RoleDotsList( + selectedRole: String, + onRoleSelected: (String) -> Unit, + modifier: Modifier = Modifier +) { + val roles = listOf( + "black", "white", + "primary", "onPrimary", "primaryContainer", "onPrimaryContainer", + "secondary", "onSecondary", "secondaryContainer", "onSecondaryContainer", + "tertiary", "onTertiary", "tertiaryContainer", "onTertiaryContainer", + "error", "onError", "errorContainer", "onErrorContainer", + "surface", "onSurface", "surfaceVariant", "onSurfaceVariant", + "background", "onBackground", + "outline", + "inverseSurface", "inverseOnSurface", "inversePrimary" + ) + + LazyRow( + contentPadding = PaddingValues(end = 16.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = modifier + ) { + items(roles) { role -> + val color = getRoleColor(role) + val isSelected = role == selectedRole + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .size(40.dp) + .clip(CircleShape) + .background(color) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { onRoleSelected(role) } + ) { + if (isSelected) { + Icon( + painterResource(R.drawable.check), + contentDescription = null, + tint = if (role in listOf( + "surface", + "surfaceVariant", + "background", + "white", + "primaryContainer", + "secondaryContainer", + "tertiaryContainer", + "errorContainer", + "onPrimary", + "onSecondary", + "onTertiary", + "onError", + "inverseSurface", + "inverseOnSurface", + "inversePrimary" + ) + ) colorScheme.primary else Color.White, + modifier = Modifier.size(20.dp) + ) + } + } + } + } +} diff --git a/androidApp/src/main/java/org/nsh07/pomodoro/widget/config/WidgetConfigurationViewModel.kt b/androidApp/src/main/java/org/nsh07/pomodoro/widget/config/WidgetConfigurationViewModel.kt new file mode 100644 index 00000000..fe602c4b --- /dev/null +++ b/androidApp/src/main/java/org/nsh07/pomodoro/widget/config/WidgetConfigurationViewModel.kt @@ -0,0 +1,159 @@ +/* + * 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.config + +import androidx.compose.material3.SliderState +import androidx.compose.runtime.Immutable +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import org.nsh07.pomodoro.service.TimerHelper + +enum class WidgetType { + TIMER, TODAY, HISTORY, UNKNOWN +} + +@Immutable +data class WidgetConfigurationState( + val opacity: Float = 1.0f, + val backgroundRole: String = "onSecondary", + val foregroundRole: String = "primary", + val headerRole: String = "onPrimary", + val skipButtonRole: String = "tertiary", + val onSkipButtonRole: String = "onTertiary", + val barCornerRadius: Int = 16, + val widgetType: WidgetType = WidgetType.UNKNOWN +) + +class WidgetConfigurationViewModel( + private val timerHelper: TimerHelper +) : ViewModel() { + + private val _state = MutableStateFlow(WidgetConfigurationState()) + val state = _state.asStateFlow() + + val opacitySliderState by lazy { + SliderState( + value = _state.value.opacity, + valueRange = 0f..1f + ) + } + + init { + viewModelScope.launch { +// val config = widgetConfigurationDao.getConfiguration(appWidgetId) +// if (config != null) { +// _state.update { +// it.copy( +// opacity = config.opacity, +// backgroundRole = config.backgroundRole, +// foregroundRole = config.foregroundRole, +// headerRole = config.headerRole, +// skipButtonRole = config.skipButtonRole, +// onSkipButtonRole = config.onSkipButtonRole, +// barCornerRadius = config.barCornerRadius +// ) +// } +// opacitySliderState.value = config.opacity +// } + } + } + + fun setWidgetType(type: WidgetType) { + _state.update { it.copy(widgetType = type) } + } + + fun updateOpacityInstant(opacity: Float) { +// opacitySliderState.value = opacity +// _state.update { it.copy(opacity = opacity) } +// saveSettings() + } + + fun setBackgroundRole(role: String) { +// _state.update { it.copy(backgroundRole = role) } +// saveSettings() + } + + fun setForegroundRole(role: String) { +// _state.update { it.copy(foregroundRole = role) } +// saveSettings() + } + + fun setHeaderRole(role: String) { +// _state.update { it.copy(headerRole = role) } +// saveSettings() + } + + fun setSkipButtonRole(role: String) { +// _state.update { it.copy(skipButtonRole = role) } +// saveSettings() + } + + fun setOnSkipButtonRole(role: String) { +// _state.update { it.copy(onSkipButtonRole = role) } +// saveSettings() + } + + fun setBarCornerRadius(radius: Int) { +// _state.update { it.copy(barCornerRadius = radius) } +// saveSettings() + } + + private fun saveSettings(id: Int) { +// viewModelScope.launch { +// val s = _state.value +// val config = WidgetConfiguration( +// id, +// s.opacity, +// s.backgroundRole, +// s.foregroundRole, +// s.headerRole, +// s.skipButtonRole, +// s.onSkipButtonRole, +// s.barCornerRadius +// ) +// widgetConfigurationDao.insertConfiguration(config) +// timerHelper.updateWidget(id) +// } + } + + /** + * Persists the full current state (including the latest opacity slider value) to the + * database. Call this before finishing the configuration activity to guarantee that any + * pending slider value is written. + */ + fun saveAllSettings(id: Int) { +// viewModelScope.launch { +// val s = _state.value +// val config = WidgetConfiguration( +// id, +// opacitySliderState.value, +// s.backgroundRole, +// s.foregroundRole, +// s.headerRole, +// s.skipButtonRole, +// s.onSkipButtonRole, +// s.barCornerRadius +// ) +// widgetConfigurationDao.insertConfiguration(config) +// } + } +} diff --git a/androidApp/src/main/java/org/nsh07/pomodoro/widget/config/WidgetPreview.kt b/androidApp/src/main/java/org/nsh07/pomodoro/widget/config/WidgetPreview.kt new file mode 100644 index 00000000..493b6e5f --- /dev/null +++ b/androidApp/src/main/java/org/nsh07/pomodoro/widget/config/WidgetPreview.kt @@ -0,0 +1,359 @@ +/* + * 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.config + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +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.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme.colorScheme +import androidx.compose.material3.MaterialTheme.shapes +import androidx.compose.material3.MaterialTheme.typography +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.util.fastForEach +import androidx.compose.ui.util.fastForEachIndexed +import androidx.compose.ui.util.fastMaxBy +import org.nsh07.pomodoro.R +import org.nsh07.pomodoro.data.Stat +import org.nsh07.pomodoro.ui.statsScreen.components.HorizontalStackedBar +import org.nsh07.pomodoro.utils.millisecondsToHoursMinutes +import org.nsh07.pomodoro.utils.millisecondsToMinutes +import org.nsh07.pomodoro.widget.HistoryAppWidget.Companion.previewHistoryData +import org.nsh07.pomodoro.widget.TomatoWidgetSize.Height2 +import org.nsh07.pomodoro.widget.TomatoWidgetSize.Width4 +import java.time.LocalDate + +@Composable +fun TodayAppWidgetPreview( + background: Color, + onSurface: Color, + onSurfaceVariant: Color, + modifier: Modifier = Modifier +) { + var size by remember { mutableStateOf(DpSize.Zero) } + val density = LocalDensity.current + + val stat = remember { + Stat( + date = LocalDate.of(2026, 3, 12), + focusTimeQ1 = 1617943, + focusTimeQ2 = 5704591, + focusTimeQ3 = 556490, + focusTimeQ4 = 1200498, + breakTime = 3939448 + ) + } + + Box( + contentAlignment = Alignment.TopEnd, + modifier = modifier + .background(background, RoundedCornerShape(24.dp)) + .onSizeChanged { + with(density) { + size = DpSize(it.width.toDp(), it.height.toDp()) + } + } + ) { + Column( + Modifier + .fillMaxSize() + .padding(16.dp) + ) { + Text( + stringResource(R.string.focus), + style = TextStyle( + color = onSurface, + fontSize = typography.titleMedium.fontSize + ) + ) + + Text( + millisecondsToHoursMinutes( + stat.totalFocusTime(), + stringResource(R.string.hours_and_minutes_format) + ), + style = typography.displaySmall, + color = onSurface, + modifier = Modifier.padding(top = 8.dp) + ) + + Spacer(Modifier.weight(1f)) + + if (size.height >= Height2) { + val values = listOf( + stat.focusTimeQ1, + stat.focusTimeQ2, + stat.focusTimeQ3, + stat.focusTimeQ4 + ) + if (size.width >= Width4) { + Row { + values.fastForEach { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.width(((size.width.value - 32f) / 4).dp) + ) { + Text( + if (it <= 60 * 60 * 1000) + millisecondsToMinutes( + it, + stringResource(R.string.minutes_format) + ) + else millisecondsToHoursMinutes( + it, + stringResource(R.string.hours_and_minutes_format) + ), + style = typography.bodyLarge, + color = onSurfaceVariant + ) + } + } + } + } + Spacer(Modifier.height(8.dp)) + HorizontalStackedBar( + values = values, + minutesFormat = stringResource(R.string.minutes_format), + hoursMinutesFormat = stringResource(R.string.hours_and_minutes_format), + modifier = Modifier.fillMaxWidth() + ) + } + } + + if (size.width >= Width4) { + Icon( + painter = painterResource(R.drawable.refresh), + contentDescription = null, + tint = onSurface, + modifier = Modifier.padding(16.dp) + ) + } + } +} + +@Composable +fun HistoryWidgetPreview( + background: Color, + onSurface: Color, + onSurfaceVariant: Color, + primary: Color, + modifier: Modifier = Modifier +) { + var size by remember { mutableStateOf(DpSize.Zero) } + val density = LocalDensity.current + + Column( + modifier = modifier + .fillMaxSize() + .background(background, RoundedCornerShape(24.dp)) + .onSizeChanged { + with(density) { + size = DpSize(it.width.toDp(), it.height.toDp()) + } + } + ) { + Row( + Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + modifier = Modifier + .size(48.dp) + .padding(start = 2.dp), + contentAlignment = Alignment.Center + ) { + Icon( + modifier = Modifier.size(24.dp), + painter = painterResource(R.drawable.tomato_logo_notification), + contentDescription = "", + tint = onSurface, + ) + } + Text( + text = stringResource(R.string.focus_history), + style = TextStyle( + color = onSurface, + fontWeight = FontWeight.Medium, + fontSize = 16.sp + ), + maxLines = 1, + modifier = Modifier.weight(1f), + ) + if (size.width >= Width4) { + Box(Modifier.padding(horizontal = 16.dp)) { + Icon( + painter = painterResource(R.drawable.refresh), + contentDescription = null, + tint = onSurface + ) + } + } + } + + Column( + Modifier + .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) + .weight(1f) + ) { + Row(verticalAlignment = Alignment.Bottom) { + Text( + millisecondsToHoursMinutes( + if (previewHistoryData.isEmpty()) 0 else previewHistoryData.sumOf { it.totalFocusTime() } / previewHistoryData.size, + stringResource(R.string.hours_and_minutes_format) + ) + " ", + style = typography.headlineSmall, + color = onSurface + ) + + if (size.width >= Width4) { + Text( + stringResource(R.string.focus_per_day_avg), + style = typography.bodyMedium, + color = onSurfaceVariant, + modifier = Modifier.padding(bottom = 2.8.dp) + ) + } + } + + Row( + horizontalArrangement = Arrangement.Center, + modifier = Modifier.fillMaxWidth() + ) { + val maxFocus = remember { + previewHistoryData.fastMaxBy { it.totalFocusTime() }?.totalFocusTime() ?: 0L + } + previewHistoryData.chunked(10).fastForEachIndexed { baseIndex, it -> + Row( + verticalAlignment = Alignment.Bottom, + modifier = Modifier.fillMaxHeight() + ) { + it.fastForEachIndexed { index, it -> + val flatIndex = baseIndex * 10 + index + Box(Modifier.padding(end = if (flatIndex != previewHistoryData.lastIndex) 4.dp else 0.dp)) { + Spacer( + Modifier + .width(20.dp) + .height( + (84 * (it.totalFocusTime().toFloat() / maxFocus)).dp + ) + .background(primary, RoundedCornerShape(16.dp)) + ) + } + } + } + } + } + } + } +} + +@Composable +fun TimerWidgetPreview( + opacity: Float, + background: Color, + tertiary: Color, + onTertiary: Color, + primary: Color, + onPrimary: Color, + modifier: Modifier = Modifier +) { + var size by remember { mutableStateOf(DpSize.Zero) } + val density = LocalDensity.current + + Box( + modifier = modifier + .background(background.copy(alpha = opacity), CircleShape) + .onSizeChanged { + with(density) { + size = DpSize(it.width.toDp(), it.height.toDp()) + } + }, + contentAlignment = Alignment.Center, + ) { + Text( + text = "25:00", + fontFamily = typography.bodyMedium.fontFamily, + fontSize = (minOf(256.dp, size.width, size.height).value * 0.25f).sp, + color = colorScheme.primary + ) + + // Skip / restart button group (top-end) — independent colors + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.TopEnd) { + Box( + Modifier + .size(48.dp) + .background(tertiary, CircleShape), + contentAlignment = Alignment.Center + ) { + Icon( + painterResource(R.drawable.skip_next), + null, + tint = onTertiary, + modifier = Modifier.size(24.dp) + ) + } + } + // Play / pause button (bottom-start) — foregroundRole bg, headerRole fg + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.BottomStart) { + Box( + Modifier + .size(60.dp) + .background(primary, shapes.large), + contentAlignment = Alignment.Center + ) { + Icon( + painterResource(R.drawable.play), + null, + tint = onPrimary, + modifier = Modifier.size(24.dp) + ) + } + } + } +} 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 ee750f3c..d79c6158 100644 --- a/androidApp/src/main/res/values/themes.xml +++ b/androidApp/src/main/res/values/themes.xml @@ -19,7 +19,15 @@ + + \ No newline at end of file diff --git a/androidApp/src/main/res/xml/history_widget_info.xml b/androidApp/src/main/res/xml/history_widget_info.xml index ee2e52b9..4f21a9d0 100644 --- a/androidApp/src/main/res/xml/history_widget_info.xml +++ b/androidApp/src/main/res/xml/history_widget_info.xml @@ -17,6 +17,7 @@ \ No newline at end of file diff --git a/androidApp/src/main/res/xml/timer_widget_info.xml b/androidApp/src/main/res/xml/timer_widget_info.xml index 4d1bed1c..e3dac964 100644 --- a/androidApp/src/main/res/xml/timer_widget_info.xml +++ b/androidApp/src/main/res/xml/timer_widget_info.xml @@ -17,6 +17,7 @@ \ No newline at end of file diff --git a/androidApp/src/main/res/xml/today_widget_info.xml b/androidApp/src/main/res/xml/today_widget_info.xml index cfccdbb4..ed7c4c0e 100644 --- a/androidApp/src/main/res/xml/today_widget_info.xml +++ b/androidApp/src/main/res/xml/today_widget_info.xml @@ -17,6 +17,7 @@ \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d9ef65c5..6dec4477 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.2.0" 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" } diff --git a/shared/src/commonMain/composeResources/values/strings.xml b/shared/src/commonMain/composeResources/values/strings.xml index 22c43be1..64b7f328 100644 --- a/shared/src/commonMain/composeResources/values/strings.xml +++ b/shared/src/commonMain/composeResources/values/strings.xml @@ -118,6 +118,10 @@ Stop Alarm? System Theme + 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..e1f9d2b3 100644 --- a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/Navigation.kt +++ b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/Navigation.kt @@ -36,7 +36,7 @@ import tomato.shared.generated.resources.timer import tomato.shared.generated.resources.timer_filled import tomato.shared.generated.resources.vibrate -val settingsScreens = listOf( +val settingsScreens = listOfNotNull( SettingsNavItem( Screen.Settings.Timer, Res.drawable.timer_filled, diff --git a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/data/AppDatabase.kt b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/data/AppDatabase.kt index ba3da5d6..9afac032 100644 --- a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/data/AppDatabase.kt +++ b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/data/AppDatabase.kt @@ -34,4 +34,4 @@ abstract class AppDatabase : RoomDatabase() { abstract fun preferenceDao(): PreferenceDao abstract fun statDao(): StatDao abstract fun systemDao(): SystemDao -} \ No newline at end of file +} 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..da4f8f63 100644 --- a/shared/src/commonMain/kotlin/org/nsh07/pomodoro/data/StateRepository.kt +++ b/shared/src/commonMain/kotlin/org/nsh07/pomodoro/data/StateRepository.kt @@ -196,4 +196,4 @@ class StateRepository(private val preferenceRepository: PreferenceRepository) { } } } -} \ No newline at end of file +} 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..c7afea28 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,6 @@ import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerAction interface TimerHelper { fun onAction(action: TimerAction) + fun updateWidgets() + fun updateWidget(appWidgetId: Int) } 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..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 @@ -156,7 +156,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 +168,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)) }, 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..2959b75c 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,14 @@ class DesktopTimerHelper( } } + override fun updateWidgets() { + // No-op for desktop + } + + override fun updateWidget(appWidgetId: Int) { + // No-op for desktop + } + private fun toggleTimer() { timerManager.toggleTimer( scope = timerScope, @@ -145,4 +153,4 @@ class DesktopTimerHelper( if (settingsState.autostartNextSession && !fromAutoStop) // auto start next session toggleTimer() } -} \ No newline at end of file +}