feat(notification): add option to show only current session progress

Closes: #162
This commit is contained in:
Nishant Mishra
2025-12-04 11:54:40 +05:30
parent edf0353448
commit e6f01096c2
7 changed files with 133 additions and 32 deletions

View File

@@ -265,7 +265,7 @@ class TimerService : Service() {
.setStyle( .setStyle(
notificationStyle notificationStyle
.setProgress( // Set the current progress by filling the previous intervals and part of the current interval .setProgress( // Set the current progress by filling the previous intervals and part of the current interval
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA && !settingsState.singleProgressBar) {
(totalTime - remainingTime) + ((cycles + 1) / 2) * settingsState.focusTime.toInt() + (cycles / 2) * settingsState.shortBreakTime.toInt() (totalTime - remainingTime) + ((cycles + 1) / 2) * settingsState.focusTime.toInt() + (cycles / 2) * settingsState.shortBreakTime.toInt()
} else (totalTime - remainingTime) } else (totalTime - remainingTime)
) )
@@ -288,7 +288,7 @@ class TimerService : Service() {
notificationStyle = NotificationCompat.ProgressStyle() notificationStyle = NotificationCompat.ProgressStyle()
.also { .also {
// Add all the Focus, Short break and long break intervals in order // Add all the Focus, Short break and long break intervals in order
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA && !settingsState.singleProgressBar) {
// Android 16 and later supports live updates // Android 16 and later supports live updates
// Set progress bar sections if on Baklava or later // Set progress bar sections if on Baklava or later
for (i in 0..<settingsState.sessionLength * 2) { for (i in 0..<settingsState.sessionLength * 2) {
@@ -326,7 +326,6 @@ class TimerService : Service() {
private suspend fun resetTimer() { private suspend fun resetTimer() {
val settingsState = _settingsState.value val settingsState = _settingsState.value
updateProgressSegments()
saveTimeToDb() saveTimeToDb()
lastSavedDuration = 0 lastSavedDuration = 0
time = settingsState.focusTime time = settingsState.focusTime
@@ -346,13 +345,13 @@ class TimerService : Service() {
totalFocusCount = settingsState.sessionLength totalFocusCount = settingsState.sessionLength
) )
} }
updateProgressSegments()
} }
private suspend fun skipTimer(fromButton: Boolean = false) { private suspend fun skipTimer(fromButton: Boolean = false) {
val settingsState = _settingsState.value val settingsState = _settingsState.value
updateProgressSegments()
saveTimeToDb() saveTimeToDb()
updateProgressSegments()
showTimerNotification(0, paused = true, complete = !fromButton) showTimerNotification(0, paused = true, complete = !fromButton)
lastSavedDuration = 0 lastSavedDuration = 0
startTime = 0L startTime = 0L
@@ -394,6 +393,8 @@ class TimerService : Service() {
) )
} }
} }
updateProgressSegments()
} }
fun startAlarm() { fun startAlarm() {

View File

@@ -20,6 +20,7 @@ package org.nsh07.pomodoro.ui.settingsScreen.screens
import android.app.NotificationManager import android.app.NotificationManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build
import android.provider.Settings import android.provider.Settings
import android.widget.Toast import android.widget.Toast
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
@@ -117,33 +118,45 @@ fun TimerSettings(
val notificationManagerService = val notificationManagerService =
remember { context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager } remember { context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager }
val switchItems = listOf( val switchItems = remember(
SettingsSwitchItem( settingsState.dndEnabled,
checked = settingsState.dndEnabled, settingsState.aodEnabled,
enabled = !serviceRunning, isPlus,
icon = R.drawable.dnd, serviceRunning
label = R.string.dnd, ) {
description = R.string.dnd_desc, listOf(
onClick = { SettingsSwitchItem(
if (it && !notificationManagerService.isNotificationPolicyAccessGranted()) { checked = settingsState.dndEnabled,
val intent = Intent(Settings.ACTION_NOTIFICATION_POLICY_ACCESS_SETTINGS) enabled = !serviceRunning,
Toast.makeText(context, "Enable permission for \"$appName\"", Toast.LENGTH_LONG) icon = R.drawable.dnd,
.show() label = R.string.dnd,
context.startActivity(intent) description = R.string.dnd_desc,
} else if (!it && notificationManagerService.isNotificationPolicyAccessGranted()) { onClick = {
notificationManagerService.setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_ALL) if (it && !notificationManagerService.isNotificationPolicyAccessGranted()) {
val intent = Intent(Settings.ACTION_NOTIFICATION_POLICY_ACCESS_SETTINGS)
Toast.makeText(
context,
"Enable permission for \"$appName\"",
Toast.LENGTH_LONG
)
.show()
context.startActivity(intent)
} else if (!it && notificationManagerService.isNotificationPolicyAccessGranted()) {
notificationManagerService.setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_ALL)
}
onAction(SettingsAction.SaveDndEnabled(it))
} }
onAction(SettingsAction.SaveDndEnabled(it)) ),
} SettingsSwitchItem(
), checked = settingsState.aodEnabled,
SettingsSwitchItem( enabled = isPlus,
checked = settingsState.aodEnabled, icon = R.drawable.aod,
icon = R.drawable.aod, label = R.string.always_on_display,
label = R.string.always_on_display, description = R.string.always_on_display_desc,
description = R.string.always_on_display_desc, onClick = { onAction(SettingsAction.SaveAodEnabled(it)) }
onClick = { onAction(SettingsAction.SaveAodEnabled(it)) } )
) )
) }
Scaffold( Scaffold(
topBar = { topBar = {
@@ -343,6 +356,44 @@ fun TimerSettings(
) )
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA) {
item { Spacer(Modifier.height(12.dp)) }
item {
ListItem(
leadingContent = {
Icon(painterResource(R.drawable.view_day), null)
},
headlineContent = { Text(stringResource(R.string.session_only_progress)) },
supportingContent = { Text(stringResource(R.string.session_only_progress_desc)) },
trailingContent = {
Switch(
checked = settingsState.singleProgressBar,
enabled = !serviceRunning,
onCheckedChange = { onAction(SettingsAction.SaveSingleProgressBar(it)) },
thumbContent = {
if (settingsState.singleProgressBar) {
Icon(
painter = painterResource(R.drawable.check),
contentDescription = null,
modifier = Modifier.size(SwitchDefaults.IconSize),
)
} else {
Icon(
painter = painterResource(R.drawable.clear),
contentDescription = null,
modifier = Modifier.size(SwitchDefaults.IconSize),
)
}
},
colors = switchColors
)
},
colors = listItemColors,
modifier = Modifier.clip(cardShape)
)
}
}
if (!isPlus) { if (!isPlus) {
item { item {
PlusDivider(setShowPaywall) PlusDivider(setShowPaywall)
@@ -362,7 +413,7 @@ fun TimerSettings(
Switch( Switch(
checked = item.checked, checked = item.checked,
onCheckedChange = { item.onClick(it) }, onCheckedChange = { item.onClick(it) },
enabled = isPlus, enabled = item.enabled,
thumbContent = { thumbContent = {
if (item.checked) { if (item.checked) {
Icon( Icon(

View File

@@ -27,6 +27,7 @@ sealed interface SettingsAction {
data class SaveAodEnabled(val enabled: Boolean) : SettingsAction data class SaveAodEnabled(val enabled: Boolean) : SettingsAction
data class SaveDndEnabled(val enabled: Boolean) : SettingsAction data class SaveDndEnabled(val enabled: Boolean) : SettingsAction
data class SaveMediaVolumeForAlarm(val enabled: Boolean) : SettingsAction data class SaveMediaVolumeForAlarm(val enabled: Boolean) : SettingsAction
data class SaveSingleProgressBar(val enabled: Boolean) : SettingsAction
data class SaveAlarmSound(val uri: Uri?) : SettingsAction data class SaveAlarmSound(val uri: Uri?) : SettingsAction
data class SaveTheme(val theme: String) : SettingsAction data class SaveTheme(val theme: String) : SettingsAction
data class SaveColorScheme(val color: Color) : SettingsAction data class SaveColorScheme(val color: Color) : SettingsAction

View File

@@ -32,6 +32,7 @@ data class SettingsState(
val vibrateEnabled: Boolean = true, val vibrateEnabled: Boolean = true,
val dndEnabled: Boolean = false, val dndEnabled: Boolean = false,
val mediaVolumeForAlarm: Boolean = false, val mediaVolumeForAlarm: Boolean = false,
val singleProgressBar: Boolean = false,
val focusTime: Long = 25 * 60 * 1000L, val focusTime: Long = 25 * 60 * 1000L,
val shortBreakTime: Long = 5 * 60 * 1000L, val shortBreakTime: Long = 5 * 60 * 1000L,

View File

@@ -110,6 +110,7 @@ class SettingsViewModel(
is SettingsAction.SaveVibrateEnabled -> saveVibrateEnabled(action.enabled) is SettingsAction.SaveVibrateEnabled -> saveVibrateEnabled(action.enabled)
is SettingsAction.SaveDndEnabled -> saveDndEnabled(action.enabled) is SettingsAction.SaveDndEnabled -> saveDndEnabled(action.enabled)
is SettingsAction.SaveMediaVolumeForAlarm -> saveMediaVolumeForAlarm(action.enabled) is SettingsAction.SaveMediaVolumeForAlarm -> saveMediaVolumeForAlarm(action.enabled)
is SettingsAction.SaveSingleProgressBar -> saveSingleProgressBar(action.enabled)
is SettingsAction.SaveColorScheme -> saveColorScheme(action.color) is SettingsAction.SaveColorScheme -> saveColorScheme(action.color)
is SettingsAction.SaveTheme -> saveTheme(action.theme) is SettingsAction.SaveTheme -> saveTheme(action.theme)
is SettingsAction.SaveBlackTheme -> saveBlackTheme(action.enabled) is SettingsAction.SaveBlackTheme -> saveBlackTheme(action.enabled)
@@ -273,6 +274,18 @@ class SettingsViewModel(
} }
} }
private fun saveSingleProgressBar(singleProgressBar: Boolean) {
viewModelScope.launch {
_settingsState.update { currentState ->
currentState.copy(singleProgressBar = singleProgressBar)
}
preferenceRepository.saveBooleanPreference(
"single_progress_bar",
singleProgressBar
)
}
}
suspend fun reloadSettings() { suspend fun reloadSettings() {
var settingsState = _settingsState.value var settingsState = _settingsState.value
val focusTime = val focusTime =
@@ -335,6 +348,11 @@ class SettingsViewModel(
"media_volume_for_alarm", "media_volume_for_alarm",
settingsState.mediaVolumeForAlarm settingsState.mediaVolumeForAlarm
) )
val singleProgressBar = preferenceRepository.getBooleanPreference("single_progress_bar")
?: preferenceRepository.saveBooleanPreference(
"single_progress_bar",
settingsState.singleProgressBar
)
_settingsState.update { currentState -> _settingsState.update { currentState ->
currentState.copy( currentState.copy(
@@ -350,7 +368,8 @@ class SettingsViewModel(
alarmEnabled = alarmEnabled, alarmEnabled = alarmEnabled,
vibrateEnabled = vibrateEnabled, vibrateEnabled = vibrateEnabled,
dndEnabled = dndEnabled, dndEnabled = dndEnabled,
mediaVolumeForAlarm = mediaVolumeForAlarm mediaVolumeForAlarm = mediaVolumeForAlarm,
singleProgressBar = singleProgressBar
) )
} }

View File

@@ -0,0 +1,26 @@
<!--
~ Copyright (c) 2025 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/>.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#e3e3e3"
android:pathData="M159,800q-17,0 -28,-11.5T120,760q0,-17 11.5,-28.5T160,720h641q17,0 28,11.5t11,28.5q0,17 -11.5,28.5T800,800L159,800ZM200,640q-33,0 -56.5,-23.5T120,560v-160q0,-33 23.5,-56.5T200,320h560q33,0 56.5,23.5T840,400v160q0,33 -23.5,56.5T760,640L200,640ZM159,240q-17,0 -28,-11.5T120,200q0,-17 11.5,-28.5T160,160h641q17,0 28,11.5t11,28.5q0,17 -11.5,28.5T800,240L159,240Z" />
</vector>

View File

@@ -105,4 +105,6 @@
<string name="license">License</string> <string name="license">License</string>
<string name="media_volume_for_alarm">Headphone mode</string> <string name="media_volume_for_alarm">Headphone mode</string>
<string name="media_volume_for_alarm_desc">Plays on headphones only. If headphones are disconnected, alarm plays through speaker at media volume.</string> <string name="media_volume_for_alarm_desc">Plays on headphones only. If headphones are disconnected, alarm plays through speaker at media volume.</string>
<string name="session_only_progress">Session-only progress</string>
<string name="session_only_progress_desc">Show progress for the current session only in notifications, rather than the full sequence.</string>
</resources> </resources>