Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
34b7148
feat: add option to make widgets background transparent
saberr26 May 1, 2026
1fe1825
fix: add missing imports and fix type mismatches in widgets
saberr26 May 1, 2026
7122bbb
fix: ensures timer widget is fully transparent when enabled
saberr26 May 1, 2026
5a44751
fix: update timer widget button colors to accent1 and accent3 roles
saberr26 May 2, 2026
b22b488
fix: use static debug keystore for consistent signing
saberr26 May 2, 2026
826479a
fix: rename custom signing config to avoid collision
saberr26 May 2, 2026
3b40a24
fix: splash screen follows background color
saberr26 May 2, 2026
d2ddd49
feat: splash screen follows system theme and monet
saberr26 May 2, 2026
f553e8a
fix: ignore launcher corner radius for timer widget to prevent cropping
saberr26 May 2, 2026
e626424
Merge branch 'nsh07:dev' into dev
saberr26 May 2, 2026
7e19eb7
revert: remove signing config changes and widget color updates
saberr26 May 3, 2026
1880122
Merge remote-tracking branch 'upstream/dev' into dev
saberr26 May 9, 2026
f475c51
feat: add dedicated widgets settings with opacity and background roles
saberr26 May 9, 2026
1dcef58
fix: resolve compilation errors in Navigation.kt and WidgetsSettings.kt
saberr26 May 9, 2026
d3d9475
fix: improve widget responsiveness and instant updates, add more roles
saberr26 May 9, 2026
140e3aa
fix: resolve compilation errors in AndroidTimerHelper and fix widget …
saberr26 May 9, 2026
b2a8886
fix: resolve multiplatform compilation and hide widgets settings on d…
saberr26 May 9, 2026
f30ca90
feat(widgets): migrate configuration to Room and add per-widget custo…
saberr26 May 11, 2026
254a22c
fix: cleanup
nsh07 May 12, 2026
4ec2a6a
revert: revert color system changes
nsh07 May 12, 2026
64d1ee8
refactor: move android-specific code to androidApp
nsh07 May 13, 2026
22bc02d
feat(widget): make previews in config screen more accurate
nsh07 May 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions androidApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)

Expand Down
9 changes: 9 additions & 0 deletions androidApp/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,15 @@
</intent-filter>
</activity>

<activity
android:name=".widget.config.WidgetConfigurationActivity"
android:exported="true"
android:theme="@style/Theme.Tomato.WidgetConfig">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter>
</activity>

<service
android:name=".service.TimerService"
android:foregroundServiceType="specialUse">
Expand Down
2 changes: 2 additions & 0 deletions androidApp/src/main/java/org/nsh07/pomodoro/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -42,6 +43,7 @@ class MainActivity : ComponentActivity() {
private val activityCallbacks: ActivityCallbacks by inject()

override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
super.onCreate(savedInstanceState)
enableEdgeToEdge()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<CoroutineDispatcher> { Dispatchers.IO }
Expand All @@ -60,6 +62,7 @@ val servicesModule = module {
single { create(::createNotificationCompatBuilder) }

single<ActivityCallbacks>()
viewModel<WidgetConfigurationViewModel>()
}

private fun createAppInfo(): AppInfo = AppInfo(BuildConfig.DEBUG)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -71,4 +80,40 @@ class AndroidTimerHelper(private val context: Context) : TimerHelper {
e.printStackTrace()
}
}
}

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}")
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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()
)
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -51,6 +52,7 @@ fun HorizontalStackedBarGlance(
values: List<Long>,
width: Dp,
modifier: GlanceModifier = GlanceModifier,
color: ColorProvider = colors.primary,
rankList: List<Int> = remember(values) {
val sortedIndices = values.indices.sortedByDescending { values[it] }
val ranks = MutableList(values.size) { 0 }
Expand Down Expand Up @@ -89,7 +91,7 @@ fun HorizontalStackedBarGlance(
GlanceModifier
.cornerRadius(4.dp)
.background(
colors.primary
color
.getColor(context)
.copy(
(1f - (rankList.getOrNull(index) ?: 0) * 0.1f)
Expand All @@ -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)
)
Expand Down Expand Up @@ -132,4 +134,4 @@ fun HorizontalStackedBarGlance(
)
)
) {}
}
}
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
*/

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)
}
}
Loading
Loading