Merge branch 'dev'

This commit is contained in:
Nishant Mishra
2025-09-29 00:27:50 +05:30
20 changed files with 220 additions and 117 deletions

View File

@@ -33,8 +33,8 @@ android {
applicationId = "org.nsh07.pomodoro" applicationId = "org.nsh07.pomodoro"
minSdk = 26 minSdk = 26
targetSdk = 36 targetSdk = 36
versionCode = 9 versionCode = 10
versionName = "1.4.1" versionName = "1.4.2"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }

View File

@@ -74,19 +74,19 @@ class MainActivity : ComponentActivity() {
Screen.Timer, Screen.Timer,
R.drawable.timer_outlined, R.drawable.timer_outlined,
R.drawable.timer_filled, R.drawable.timer_filled,
"Timer" R.string.timer
), ),
NavItem( NavItem(
Screen.Stats, Screen.Stats,
R.drawable.monitoring, R.drawable.monitoring,
R.drawable.monitoring_filled, R.drawable.monitoring_filled,
"Stats" R.string.stats
), ),
NavItem( NavItem(
Screen.Settings, Screen.Settings,
R.drawable.settings, R.drawable.settings,
R.drawable.settings_filled, R.drawable.settings_filled,
"Settings" R.string.settings
) )
) )
} }

View File

@@ -14,7 +14,7 @@ class TomatoApplication : Application() {
val notificationChannel = NotificationChannel( val notificationChannel = NotificationChannel(
"timer", "timer",
"Timer progress", getString(R.string.timer_progress),
NotificationManager.IMPORTANCE_HIGH NotificationManager.IMPORTANCE_HIGH
) )

View File

@@ -57,7 +57,7 @@ class DefaultAppContainer(context: Context) : AppContainer {
PendingIntent.FLAG_IMMUTABLE PendingIntent.FLAG_IMMUTABLE
) )
) )
.addTimerActions(context, R.drawable.play, "Start") .addTimerActions(context, R.drawable.play, context.getString(R.string.start))
.setShowWhen(true) .setShowWhen(true)
.setSilent(true) .setSilent(true)
.setOngoing(true) .setOngoing(true)

View File

@@ -34,7 +34,7 @@ fun NotificationCompat.Builder.addTimerActions(
) )
.addAction( .addAction(
R.drawable.restart, R.drawable.restart,
"Exit", context.getString(R.string.exit),
PendingIntent.getService( PendingIntent.getService(
context, context,
0, 0,
@@ -46,7 +46,7 @@ fun NotificationCompat.Builder.addTimerActions(
) )
.addAction( .addAction(
R.drawable.skip_next, R.drawable.skip_next,
"Skip", context.getString(R.string.skip),
PendingIntent.getService( PendingIntent.getService(
context, context,
0, 0,
@@ -62,7 +62,7 @@ fun NotificationCompat.Builder.addStopAlarmAction(
): NotificationCompat.Builder = this ): NotificationCompat.Builder = this
.addAction( .addAction(
R.drawable.alarm, R.drawable.alarm,
"Stop alarm", context.getString(R.string.stop_alarm),
PendingIntent.getService( PendingIntent.getService(
context, context,
0, 0,

View File

@@ -119,7 +119,7 @@ class TimerService : Service() {
if (timerState.value.timerRunning) { if (timerState.value.timerRunning) {
notificationBuilder.clearActions().addTimerActions( notificationBuilder.clearActions().addTimerActions(
this, R.drawable.play, "Start" this, R.drawable.play, getString(R.string.start)
) )
showTimerNotification(time.toInt(), paused = true) showTimerNotification(time.toInt(), paused = true)
_timerState.update { currentState -> _timerState.update { currentState ->
@@ -128,7 +128,7 @@ class TimerService : Service() {
pauseTime = SystemClock.elapsedRealtime() pauseTime = SystemClock.elapsedRealtime()
} else { } else {
notificationBuilder.clearActions().addTimerActions( notificationBuilder.clearActions().addTimerActions(
this, R.drawable.pause, "Stop" this, R.drawable.pause, getString(R.string.stop)
) )
_timerState.update { it.copy(timerRunning = true) } _timerState.update { it.copy(timerRunning = true) }
if (pauseTime != 0L) pauseDuration += SystemClock.elapsedRealtime() - pauseTime if (pauseTime != 0L) pauseDuration += SystemClock.elapsedRealtime() - pauseTime
@@ -186,15 +186,15 @@ class TimerService : Service() {
} }
val currentTimer = when (timerState.value.timerMode) { val currentTimer = when (timerState.value.timerMode) {
TimerMode.FOCUS -> "Focus" TimerMode.FOCUS -> getString(R.string.focus)
TimerMode.SHORT_BREAK -> "Short break" TimerMode.SHORT_BREAK -> getString(R.string.short_break)
else -> "Long break" else -> getString(R.string.long_break)
} }
val nextTimer = when (timerState.value.nextTimerMode) { val nextTimer = when (timerState.value.nextTimerMode) {
TimerMode.FOCUS -> "Focus" TimerMode.FOCUS -> getString(R.string.focus)
TimerMode.SHORT_BREAK -> "Short break" TimerMode.SHORT_BREAK -> getString(R.string.short_break)
else -> "Long break" else -> getString(R.string.long_break)
} }
val remainingTimeString = if ((remainingTime.toFloat() / 60000f) < 1.0f) "< 1" val remainingTimeString = if ((remainingTime.toFloat() / 60000f) < 1.0f) "< 1"
@@ -205,10 +205,18 @@ class TimerService : Service() {
notificationBuilder notificationBuilder
.setContentTitle( .setContentTitle(
if (!complete) { if (!complete) {
"$currentTimer $middleDot $remainingTimeString min remaining" + if (paused) " $middleDot Paused" else "" "$currentTimer $middleDot ${
} else "$currentTimer $middleDot Completed" getString(R.string.min_remaining_notification, remainingTimeString)
}" + if (paused) " $middleDot ${getString(R.string.paused)}" else ""
} else "$currentTimer $middleDot ${getString(R.string.completed)}"
)
.setContentText(
getString(
R.string.up_next_notification,
nextTimer,
timerState.value.nextTimeStr
)
) )
.setContentText("Up next: $nextTimer (${timerState.value.nextTimeStr})")
.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
@@ -364,7 +372,10 @@ class TimerService : Service() {
_timerState.update { currentState -> _timerState.update { currentState ->
currentState.copy(alarmRinging = false) currentState.copy(alarmRinging = false)
} }
notificationBuilder.clearActions().addTimerActions(this, R.drawable.play, "Start next") notificationBuilder.clearActions().addTimerActions(
this, R.drawable.play,
getString(R.string.start_next)
)
showTimerNotification( showTimerNotification(
when (timerState.value.timerMode) { when (timerState.value.timerMode) {
TimerMode.FOCUS -> timerRepository.focusTime.toInt() TimerMode.FOCUS -> timerRepository.focusTime.toInt()

View File

@@ -35,6 +35,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation3.runtime.entryProvider import androidx.navigation3.runtime.entryProvider
@@ -110,7 +111,7 @@ fun AppScreen(
iconPosition = iconPosition =
if (wide) NavigationItemIconPosition.Start if (wide) NavigationItemIconPosition.Start
else NavigationItemIconPosition.Top, else NavigationItemIconPosition.Top,
label = { Text(it.label) } label = { Text(stringResource(it.label)) }
) )
} }
} }

View File

@@ -1,23 +1,24 @@
package org.nsh07.pomodoro.ui package org.nsh07.pomodoro.ui
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.NavKey
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
sealed class Screen: NavKey { sealed class Screen : NavKey {
@Serializable @Serializable
object Timer : Screen() object Timer : Screen()
@Serializable @Serializable
object Settings : Screen() object Settings : Screen()
@Serializable @Serializable
object Stats : Screen() object Stats : Screen()
} }
data class NavItem( data class NavItem(
val route: Screen, val route: Screen,
@param:DrawableRes @param:DrawableRes val unselectedIcon: Int,
val unselectedIcon: Int, @param:DrawableRes val selectedIcon: Int,
@param:DrawableRes @param:StringRes val label: Int
val selectedIcon: Int,
val label: String
) )

View File

@@ -32,6 +32,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach import androidx.compose.ui.util.fastForEach
@@ -100,7 +101,7 @@ fun ColorSchemePickerDialog(
) { ) {
Column(modifier = Modifier.padding(24.dp)) { Column(modifier = Modifier.padding(24.dp)) {
Text( Text(
text = "Choose color scheme", text = stringResource(R.string.choose_color_scheme),
style = MaterialTheme.typography.headlineSmall style = MaterialTheme.typography.headlineSmall
) )
@@ -136,7 +137,7 @@ fun ColorSchemePickerDialog(
onClick = { setShowDialog(false) }, onClick = { setShowDialog(false) },
modifier = Modifier.align(Alignment.End) modifier = Modifier.align(Alignment.End)
) { ) {
Text("Ok") Text(stringResource(R.string.ok))
} }
} }
} }

View File

@@ -19,6 +19,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import org.nsh07.pomodoro.R import org.nsh07.pomodoro.R
import org.nsh07.pomodoro.ui.ClickableListItem import org.nsh07.pomodoro.ui.ClickableListItem
import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
@@ -49,11 +50,11 @@ fun ColorSchemePickerListItem(
tint = colorScheme.primary tint = colorScheme.primary
) )
}, },
headlineContent = { Text("Color scheme") }, headlineContent = { Text(stringResource(R.string.color_scheme)) },
supportingContent = { supportingContent = {
Text( Text(
if (color == Color.White) "Dynamic" if (color == Color.White) stringResource(R.string.dynamic)
else "Color" else stringResource(R.string.color)
) )
}, },
colors = listItemColors, colors = listItemColors,

View File

@@ -12,7 +12,6 @@ import android.content.Intent
import android.media.RingtoneManager import android.media.RingtoneManager
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.provider.MediaStore
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
@@ -68,15 +67,17 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.ui.util.fastCoerceAtLeast
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.nsh07.pomodoro.R import org.nsh07.pomodoro.R
import org.nsh07.pomodoro.service.TimerService import org.nsh07.pomodoro.service.TimerService
import org.nsh07.pomodoro.ui.settingsScreen.viewModel.PreferencesState import org.nsh07.pomodoro.ui.settingsScreen.viewModel.PreferencesState
@@ -174,6 +175,7 @@ private fun SettingsScreen(
onColorSchemeChange: (Color) -> Unit, onColorSchemeChange: (Color) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val context = LocalContext.current
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
val switchColors = SwitchDefaults.colors( val switchColors = SwitchDefaults.colors(
checkedIconColor = colorScheme.primary, checkedIconColor = colorScheme.primary,
@@ -183,34 +185,27 @@ private fun SettingsScreen(
mapOf( mapOf(
"auto" to Pair( "auto" to Pair(
R.drawable.brightness_auto, R.drawable.brightness_auto,
"System default" context.getString(R.string.system_default)
), ),
"light" to Pair(R.drawable.light_mode, "Light"), "light" to Pair(R.drawable.light_mode, context.getString(R.string.light)),
"dark" to Pair(R.drawable.dark_mode, "Dark") "dark" to Pair(R.drawable.dark_mode, context.getString(R.string.dark))
) )
} }
val reverseThemeMap: Map<String, String> = remember { val reverseThemeMap: Map<String, String> = remember {
mapOf( mapOf(
"System default" to "auto", context.getString(R.string.system_default) to "auto",
"Light" to "light", context.getString(R.string.light) to "light",
"Dark" to "dark" context.getString(R.string.dark) to "dark"
) )
} }
val context = LocalContext.current var alarmName by remember { mutableStateOf("...") }
var alarmName by remember { mutableStateOf("") }
LaunchedEffect(alarmSound) { LaunchedEffect(alarmSound) {
val returnCursor = context.contentResolver.query(alarmSound.toUri(), null, null, null, null) withContext(Dispatchers.IO) {
returnCursor?.moveToFirst() alarmName =
alarmName = RingtoneManager.getRingtone(context, alarmSound.toUri())?.getTitle(context) ?: ""
returnCursor }
?.getString(
returnCursor
.getColumnIndex(MediaStore.MediaColumns.TITLE)
.fastCoerceAtLeast(0)
) ?: ""
returnCursor?.close()
} }
val ringtonePickerLauncher = rememberLauncherForActivityResult( val ringtonePickerLauncher = rememberLauncherForActivityResult(
@@ -233,7 +228,7 @@ private fun SettingsScreen(
val intent = Intent(RingtoneManager.ACTION_RINGTONE_PICKER).apply { val intent = Intent(RingtoneManager.ACTION_RINGTONE_PICKER).apply {
putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_ALARM) putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_ALARM)
putExtra(RingtoneManager.EXTRA_RINGTONE_TITLE, "Alarm sound") putExtra(RingtoneManager.EXTRA_RINGTONE_TITLE, stringResource(R.string.alarm_sound))
putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, alarmSound.toUri()) putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, alarmSound.toUri())
} }
@@ -242,22 +237,22 @@ private fun SettingsScreen(
SettingsSwitchItem( SettingsSwitchItem(
checked = preferencesState.blackTheme, checked = preferencesState.blackTheme,
icon = R.drawable.contrast, icon = R.drawable.contrast,
label = "Black theme", label = context.getString(R.string.black_theme),
description = "Use a pure black dark theme", description = context.getString(R.string.black_theme_desc),
onClick = onBlackThemeChange onClick = onBlackThemeChange
), ),
SettingsSwitchItem( SettingsSwitchItem(
checked = alarmEnabled, checked = alarmEnabled,
icon = R.drawable.alarm_on, icon = R.drawable.alarm_on,
label = "Alarm", label = context.getString(R.string.alarm),
description = "Ring alarm when a timer completes", description = context.getString(R.string.alarm_desc),
onClick = onAlarmEnabledChange onClick = onAlarmEnabledChange
), ),
SettingsSwitchItem( SettingsSwitchItem(
checked = vibrateEnabled, checked = vibrateEnabled,
icon = R.drawable.mobile_vibrate, icon = R.drawable.mobile_vibrate,
label = "Vibrate", label = context.getString(R.string.vibrate),
description = "Vibrate when a timer completes", description = context.getString(R.string.vibrate_desc),
onClick = onVibrateEnabledChange onClick = onVibrateEnabledChange
) )
) )
@@ -267,7 +262,7 @@ private fun SettingsScreen(
TopAppBar( TopAppBar(
title = { title = {
Text( Text(
"Settings", stringResource(R.string.settings),
style = LocalTextStyle.current.copy( style = LocalTextStyle.current.copy(
fontFamily = robotoFlexTopBar, fontFamily = robotoFlexTopBar,
fontSize = 32.sp, fontSize = 32.sp,
@@ -303,7 +298,7 @@ private fun SettingsScreen(
verticalArrangement = Arrangement.spacedBy(2.dp) verticalArrangement = Arrangement.spacedBy(2.dp)
) { ) {
Text( Text(
"Focus", stringResource(R.string.focus),
style = typography.titleSmallEmphasized style = typography.titleSmallEmphasized
) )
MinuteInputField( MinuteInputField(
@@ -323,7 +318,7 @@ private fun SettingsScreen(
verticalArrangement = Arrangement.spacedBy(2.dp) verticalArrangement = Arrangement.spacedBy(2.dp)
) { ) {
Text( Text(
"Short break", stringResource(R.string.short_break),
style = typography.titleSmallEmphasized style = typography.titleSmallEmphasized
) )
MinuteInputField( MinuteInputField(
@@ -338,7 +333,7 @@ private fun SettingsScreen(
verticalArrangement = Arrangement.spacedBy(2.dp) verticalArrangement = Arrangement.spacedBy(2.dp)
) { ) {
Text( Text(
"Long break", stringResource(R.string.long_break),
style = typography.titleSmallEmphasized style = typography.titleSmallEmphasized
) )
MinuteInputField( MinuteInputField(
@@ -366,11 +361,16 @@ private fun SettingsScreen(
) )
}, },
headlineContent = { headlineContent = {
Text("Session length") Text(stringResource(R.string.session_length))
}, },
supportingContent = { supportingContent = {
Column { Column {
Text("Focus intervals in one session: ${sessionsSliderState.value.toInt()}") Text(
stringResource(
R.string.session_length_desc,
sessionsSliderState.value.toInt()
)
)
Slider( Slider(
state = sessionsSliderState, state = sessionsSliderState,
modifier = Modifier.padding(vertical = 4.dp) modifier = Modifier.padding(vertical = 4.dp)
@@ -446,7 +446,7 @@ private fun SettingsScreen(
leadingContent = { leadingContent = {
Icon(painterResource(R.drawable.alarm), null) Icon(painterResource(R.drawable.alarm), null)
}, },
headlineContent = { Text("Alarm sound") }, headlineContent = { Text(stringResource(R.string.alarm_sound)) },
supportingContent = { Text(alarmName) }, supportingContent = { Text(alarmName) },
colors = listItemColors, colors = listItemColors,
modifier = Modifier modifier = Modifier
@@ -514,9 +514,7 @@ private fun SettingsScreen(
} }
AnimatedVisibility(expanded) { AnimatedVisibility(expanded) {
Text( Text(
"A \"session\" is a sequence of pomodoro intervals that contain focus" + stringResource(R.string.pomodoro_info),
" intervals, short break intervals, and a long break interval. The " +
"last break of a session is always a long break.",
style = typography.bodyMedium, style = typography.bodyMedium,
color = colorScheme.onSurfaceVariant, color = colorScheme.onSurfaceVariant,
modifier = Modifier.padding(8.dp) modifier = Modifier.padding(8.dp)
@@ -558,7 +556,7 @@ fun SettingsScreenPreview() {
data class SettingsSwitchItem( data class SettingsSwitchItem(
val checked: Boolean, val checked: Boolean,
@DrawableRes val icon: Int, @param:DrawableRes val icon: Int,
val label: String, val label: String,
val description: String, val description: String,
val onClick: (Boolean) -> Unit val onClick: (Boolean) -> Unit

View File

@@ -37,6 +37,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import org.nsh07.pomodoro.R import org.nsh07.pomodoro.R
@@ -71,7 +72,7 @@ fun ThemeDialog(
) { ) {
Column(modifier = Modifier.padding(24.dp)) { Column(modifier = Modifier.padding(24.dp)) {
Text( Text(
text = "Choose theme", text = stringResource(R.string.choose_theme),
style = MaterialTheme.typography.headlineSmall style = MaterialTheme.typography.headlineSmall
) )
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
@@ -122,7 +123,7 @@ fun ThemeDialog(
onClick = { setShowThemeDialog(false) }, onClick = { setShowThemeDialog(false) },
modifier = Modifier.align(Alignment.End) modifier = Modifier.align(Alignment.End)
) { ) {
Text("Ok") Text(stringResource(R.string.ok))
} }
} }
} }

View File

@@ -17,6 +17,8 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import org.nsh07.pomodoro.R
import org.nsh07.pomodoro.ui.ClickableListItem import org.nsh07.pomodoro.ui.ClickableListItem
import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
@@ -49,7 +51,7 @@ fun ThemePickerListItem(
contentDescription = null contentDescription = null
) )
}, },
headlineContent = { Text("Theme") }, headlineContent = { Text(stringResource(R.string.theme)) },
supportingContent = { supportingContent = {
Text(themeMap[theme]!!.second) Text(themeMap[theme]!!.second)
}, },

View File

@@ -16,9 +16,11 @@ import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModelProducer import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModelProducer
import com.patrykandpatrick.vico.core.cartesian.data.CartesianValueFormatter import com.patrykandpatrick.vico.core.cartesian.data.CartesianValueFormatter
import org.nsh07.pomodoro.R
import org.nsh07.pomodoro.utils.millisecondsToHoursMinutes import org.nsh07.pomodoro.utils.millisecondsToHoursMinutes
@Composable @Composable
@@ -26,12 +28,15 @@ fun ColumnScope.ProductivityGraph(
expanded: Boolean, expanded: Boolean,
modelProducer: CartesianChartModelProducer, modelProducer: CartesianChartModelProducer,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
label: String = "Productivity analysis" label: String = stringResource(R.string.productivity_analysis)
) { ) {
AnimatedVisibility(expanded) { AnimatedVisibility(expanded) {
Column(modifier = modifier) { Column(modifier = modifier) {
Text(label, style = typography.titleMedium) Text(label, style = typography.titleMedium)
Text("Focus durations at different times of the day", style = typography.bodySmall) Text(
stringResource(R.string.productivity_analysis_desc),
style = typography.bodySmall
)
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
TimeColumnChart( TimeColumnChart(
modelProducer, modelProducer,

View File

@@ -45,6 +45,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.rotate
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -114,7 +115,7 @@ fun StatsScreen(
TopAppBar( TopAppBar(
title = { title = {
Text( Text(
"Stats", stringResource(R.string.stats),
style = LocalTextStyle.current.copy( style = LocalTextStyle.current.copy(
fontFamily = robotoFlexTopBar, fontFamily = robotoFlexTopBar,
fontSize = 32.sp, fontSize = 32.sp,
@@ -138,7 +139,7 @@ fun StatsScreen(
item { Spacer(Modifier) } item { Spacer(Modifier) }
item { item {
Text( Text(
"Today", stringResource(R.string.today),
style = typography.headlineSmall, style = typography.headlineSmall,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -157,7 +158,7 @@ fun StatsScreen(
) { ) {
Column(Modifier.padding(16.dp)) { Column(Modifier.padding(16.dp)) {
Text( Text(
"Focus", stringResource(R.string.focus),
style = typography.titleMedium, style = typography.titleMedium,
color = colorScheme.onPrimaryContainer color = colorScheme.onPrimaryContainer
) )
@@ -182,7 +183,7 @@ fun StatsScreen(
) { ) {
Column(Modifier.padding(16.dp)) { Column(Modifier.padding(16.dp)) {
Text( Text(
"Break", stringResource(R.string.break_),
style = typography.titleMedium, style = typography.titleMedium,
color = colorScheme.onTertiaryContainer color = colorScheme.onTertiaryContainer
) )
@@ -201,7 +202,7 @@ fun StatsScreen(
item { Spacer(Modifier) } item { Spacer(Modifier) }
item { item {
Text( Text(
"Last week", stringResource(R.string.last_week),
style = typography.headlineSmall, style = typography.headlineSmall,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -222,7 +223,7 @@ fun StatsScreen(
fontFamily = openRundeClock fontFamily = openRundeClock
) )
Text( Text(
"focus per day (avg)", stringResource(R.string.focus_per_day_avg),
style = typography.titleSmall, style = typography.titleSmall,
modifier = Modifier.padding(bottom = 6.3.dp) modifier = Modifier.padding(bottom = 6.3.dp)
) )
@@ -258,14 +259,14 @@ fun StatsScreen(
) { ) {
Icon( Icon(
painterResource(R.drawable.arrow_down), painterResource(R.drawable.arrow_down),
"More info", stringResource(R.string.more_info),
modifier = Modifier.rotate(iconRotation) modifier = Modifier.rotate(iconRotation)
) )
} }
ProductivityGraph( ProductivityGraph(
lastWeekStatExpanded, lastWeekStatExpanded,
lastWeekSummaryAnalysisModelProducer, lastWeekSummaryAnalysisModelProducer,
label = "Weekly productivity analysis", label = stringResource(R.string.weekly_productivity_analysis),
modifier = Modifier.padding(horizontal = 32.dp) modifier = Modifier.padding(horizontal = 32.dp)
) )
} }
@@ -273,7 +274,7 @@ fun StatsScreen(
item { Spacer(Modifier) } item { Spacer(Modifier) }
item { item {
Text( Text(
"Last month", stringResource(R.string.last_month),
style = typography.headlineSmall, style = typography.headlineSmall,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -294,7 +295,7 @@ fun StatsScreen(
fontFamily = openRundeClock fontFamily = openRundeClock
) )
Text( Text(
"focus per day (avg)", text = stringResource(R.string.focus_per_day_avg),
style = typography.titleSmall, style = typography.titleSmall,
modifier = Modifier.padding(bottom = 6.3.dp) modifier = Modifier.padding(bottom = 6.3.dp)
) )
@@ -331,14 +332,14 @@ fun StatsScreen(
) { ) {
Icon( Icon(
painterResource(R.drawable.arrow_down), painterResource(R.drawable.arrow_down),
"More info", stringResource(R.string.more_info),
modifier = Modifier.rotate(iconRotation) modifier = Modifier.rotate(iconRotation)
) )
} }
ProductivityGraph( ProductivityGraph(
lastMonthStatExpanded, lastMonthStatExpanded,
lastMonthSummaryAnalysisModelProducer, lastMonthSummaryAnalysisModelProducer,
label = "Monthly productivity analysis", label = stringResource(R.string.monthly_productivity_analysis),
modifier = Modifier.padding(horizontal = 32.dp) modifier = Modifier.padding(horizontal = 32.dp)
) )
} }

View File

@@ -16,17 +16,18 @@ import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.material3.AlertDialogDefaults import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.BasicAlertDialog import androidx.compose.material3.BasicAlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MaterialTheme.typography import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import org.nsh07.pomodoro.R import org.nsh07.pomodoro.R
@@ -51,25 +52,25 @@ fun AlarmDialog(
Column(modifier = Modifier.padding(24.dp)) { Column(modifier = Modifier.padding(24.dp)) {
Icon( Icon(
painter = painterResource(R.drawable.alarm), painter = painterResource(R.drawable.alarm),
contentDescription = "Alarm", contentDescription = stringResource(R.string.alarm),
modifier = Modifier.align(Alignment.CenterHorizontally) modifier = Modifier.align(Alignment.CenterHorizontally)
) )
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(16.dp))
Text( Text(
text = "Stop Alarm?", text = stringResource(R.string.stop_alarm_question),
style = typography.headlineSmall, style = typography.headlineSmall,
modifier = Modifier.align(Alignment.CenterHorizontally) modifier = Modifier.align(Alignment.CenterHorizontally)
) )
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(16.dp))
Text( Text(
text = "Current timer session is complete. Tap anywhere to stop the alarm." text = stringResource(R.string.stop_alarm_dialog_text)
) )
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))
Button( TextButton(
onClick = stopAlarm, onClick = stopAlarm,
modifier = Modifier.align(Alignment.End), modifier = Modifier.align(Alignment.End),
) { ) {
Text("Dismiss") Text(stringResource(R.string.stop_alarm))
} }
} }
} }

View File

@@ -67,6 +67,7 @@ import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
@@ -134,7 +135,7 @@ fun TimerScreen(
when (it) { when (it) {
TimerMode.BRAND -> TimerMode.BRAND ->
Text( Text(
"Tomato", stringResource(R.string.app_name),
style = TextStyle( style = TextStyle(
fontFamily = robotoFlexTopBar, fontFamily = robotoFlexTopBar,
fontSize = 32.sp, fontSize = 32.sp,
@@ -147,7 +148,7 @@ fun TimerScreen(
TimerMode.FOCUS -> TimerMode.FOCUS ->
Text( Text(
"Focus", stringResource(R.string.focus),
style = TextStyle( style = TextStyle(
fontFamily = robotoFlexTopBar, fontFamily = robotoFlexTopBar,
fontSize = 32.sp, fontSize = 32.sp,
@@ -159,7 +160,7 @@ fun TimerScreen(
) )
TimerMode.SHORT_BREAK -> Text( TimerMode.SHORT_BREAK -> Text(
"Short break", stringResource(R.string.short_break),
style = TextStyle( style = TextStyle(
fontFamily = robotoFlexTopBar, fontFamily = robotoFlexTopBar,
fontSize = 32.sp, fontSize = 32.sp,
@@ -171,7 +172,7 @@ fun TimerScreen(
) )
TimerMode.LONG_BREAK -> Text( TimerMode.LONG_BREAK -> Text(
"Long Break", stringResource(R.string.long_break),
style = TextStyle( style = TextStyle(
fontFamily = robotoFlexTopBar, fontFamily = robotoFlexTopBar,
fontSize = 32.sp, fontSize = 32.sp,
@@ -261,7 +262,11 @@ fun TimerScreen(
shrinkVertically(motionScheme.defaultSpatialSpec()) shrinkVertically(motionScheme.defaultSpatialSpec())
) { ) {
Text( Text(
"${timerState.currentFocusCount} of ${timerState.totalFocusCount}", stringResource(
R.string.timer_session_count,
timerState.currentFocusCount,
timerState.totalFocusCount
),
fontFamily = openRundeClock, fontFamily = openRundeClock,
style = typography.titleLarge, style = typography.titleLarge,
color = colorScheme.outline color = colorScheme.outline
@@ -289,7 +294,7 @@ fun TimerScreen(
) { ) {
Icon( Icon(
painterResource(R.drawable.more_vert_large), painterResource(R.drawable.more_vert_large),
contentDescription = "More", contentDescription = stringResource(R.string.more),
modifier = Modifier.size(32.dp) modifier = Modifier.size(32.dp)
) )
} }
@@ -323,13 +328,13 @@ fun TimerScreen(
if (timerState.timerRunning) { if (timerState.timerRunning) {
Icon( Icon(
painterResource(R.drawable.pause_large), painterResource(R.drawable.pause_large),
contentDescription = "Pause", contentDescription = stringResource(R.string.pause),
modifier = Modifier.size(32.dp) modifier = Modifier.size(32.dp)
) )
} else { } else {
Icon( Icon(
painterResource(R.drawable.play_large), painterResource(R.drawable.play_large),
contentDescription = "Play", contentDescription = stringResource(R.string.play),
modifier = Modifier.size(32.dp) modifier = Modifier.size(32.dp)
) )
} }
@@ -341,16 +346,22 @@ fun TimerScreen(
if (timerState.timerRunning) { if (timerState.timerRunning) {
Icon( Icon(
painterResource(R.drawable.pause), painterResource(R.drawable.pause),
contentDescription = "Pause" contentDescription = stringResource(R.string.pause)
) )
} else { } else {
Icon( Icon(
painterResource(R.drawable.play), painterResource(R.drawable.play),
contentDescription = "Play" contentDescription = stringResource(R.string.play)
) )
} }
}, },
text = { Text(if (timerState.timerRunning) "Pause" else "Play") }, text = {
Text(
if (timerState.timerRunning) stringResource(R.string.pause) else stringResource(
R.string.play
)
)
},
onClick = { onClick = {
onAction(TimerAction.ToggleTimer) onAction(TimerAction.ToggleTimer)
state.dismiss() state.dismiss()
@@ -377,7 +388,7 @@ fun TimerScreen(
) { ) {
Icon( Icon(
painterResource(R.drawable.restart_large), painterResource(R.drawable.restart_large),
contentDescription = "Restart", contentDescription = stringResource(R.string.restart),
modifier = Modifier.size(32.dp) modifier = Modifier.size(32.dp)
) )
} }
@@ -387,10 +398,10 @@ fun TimerScreen(
leadingIcon = { leadingIcon = {
Icon( Icon(
painterResource(R.drawable.restart), painterResource(R.drawable.restart),
"Restart" stringResource(R.string.restart)
) )
}, },
text = { Text("Restart") }, text = { Text(stringResource(R.string.restart)) },
onClick = { onClick = {
onAction(TimerAction.ResetTimer) onAction(TimerAction.ResetTimer)
state.dismiss() state.dismiss()
@@ -417,7 +428,7 @@ fun TimerScreen(
) { ) {
Icon( Icon(
painterResource(R.drawable.skip_next_large), painterResource(R.drawable.skip_next_large),
contentDescription = "Skip to next", contentDescription = stringResource(R.string.skip_to_next),
modifier = Modifier.size(32.dp) modifier = Modifier.size(32.dp)
) )
} }
@@ -427,10 +438,10 @@ fun TimerScreen(
leadingIcon = { leadingIcon = {
Icon( Icon(
painterResource(R.drawable.skip_next), painterResource(R.drawable.skip_next),
"Skip to next" stringResource(R.string.skip_to_next)
) )
}, },
text = { Text("Skip to next") }, text = { Text(stringResource(R.string.skip_to_next)) },
onClick = { onClick = {
onAction(TimerAction.SkipTimer(fromButton = true)) onAction(TimerAction.SkipTimer(fromButton = true))
state.dismiss() state.dismiss()
@@ -444,7 +455,7 @@ fun TimerScreen(
Spacer(Modifier.height(32.dp)) Spacer(Modifier.height(32.dp))
Column(horizontalAlignment = CenterHorizontally) { Column(horizontalAlignment = CenterHorizontally) {
Text("Up next", style = typography.titleSmall) Text(stringResource(R.string.up_next), style = typography.titleSmall)
Text( Text(
timerState.nextTimeStr, timerState.nextTimeStr,
style = TextStyle( style = TextStyle(
@@ -457,9 +468,9 @@ fun TimerScreen(
) )
Text( Text(
when (timerState.nextTimerMode) { when (timerState.nextTimerMode) {
TimerMode.FOCUS -> "Focus" TimerMode.FOCUS -> stringResource(R.string.focus)
TimerMode.SHORT_BREAK -> "Short break" TimerMode.SHORT_BREAK -> stringResource(R.string.short_break)
else -> "Long Break" else -> stringResource(R.string.long_break)
}, },
style = typography.titleMediumEmphasized style = typography.titleMediumEmphasized
) )

View File

@@ -117,11 +117,14 @@ class TimerViewModel(
val today = LocalDate.now() val today = LocalDate.now()
// Fills dates between today and lastDate with 0s to ensure continuous history // Fills dates between today and lastDate with 0s to ensure continuous history
if (lastDate != null) if (lastDate != null) {
while (ChronoUnit.DAYS.between(lastDate, today) > 0) { while (ChronoUnit.DAYS.between(lastDate, today) > 0) {
lastDate = lastDate?.plusDays(1) lastDate = lastDate?.plusDays(1)
statRepository.insertStat(Stat(lastDate!!, 0, 0, 0, 0, 0)) statRepository.insertStat(Stat(lastDate!!, 0, 0, 0, 0, 0))
} }
} else {
statRepository.insertStat(Stat(today, 0, 0, 0, 0, 0))
}
delay(1500) delay(1500)

View File

@@ -1,3 +1,59 @@
<resources> <resources>
<string name="app_name">Tomato</string> <string name="app_name">Tomato</string>
<string name="start">Start</string>
<string name="stop">Stop</string>
<string name="focus">Focus</string>
<string name="short_break">Short break</string>
<string name="long_break">Long break</string>
<string name="exit">Exit</string>
<string name="skip">Skip</string>
<string name="stop_alarm">Stop alarm</string>
<string name="min_remaining_notification">%1$s min remaining</string>
<string name="paused">Paused</string>
<string name="completed">Completed</string>
<string name="up_next_notification">Up next: %1$s (%2$s)</string>
<string name="start_next">Start next</string>
<string name="choose_color_scheme">Choose color scheme</string>
<string name="ok">OK</string>
<string name="color_scheme">Color scheme</string>
<string name="dynamic">Dynamic</string>
<string name="color">Color</string>
<string name="system_default">System default</string>
<string name="alarm">Alarm</string>
<string name="light">Light</string>
<string name="dark">Dark</string>
<string name="choose_theme">Choose theme</string>
<string name="productivity_analysis">Productivity analysis</string>
<string name="productivity_analysis_desc">Focus durations at different times of the day</string>
<string name="alarm_sound">Alarm sound</string>
<string name="black_theme">Black theme</string>
<string name="black_theme_desc">Use a pure black dark theme</string>
<string name="alarm_desc">Ring alarm when a timer completes</string>
<string name="vibrate">Vibrate</string>
<string name="vibrate_desc">Vibrate when a timer completes</string>
<string name="theme">Theme</string>
<string name="settings">Settings</string>
<string name="session_length">Session length</string>
<string name="session_length_desc">Focus intervals in one session: %1$d</string>
<string name="pomodoro_info">A \"session\" is a sequence of pomodoro intervals that contain focus intervals, short break intervals, and a long break interval. The last break of a session is always a long break.</string>
<string name="stats">Stats</string>
<string name="today">Today</string>
<string name="break_">Break</string>
<string name="last_week">Last week</string>
<string name="focus_per_day_avg">focus per day (avg)</string>
<string name="more_info">More info</string>
<string name="weekly_productivity_analysis">Weekly productivity analysis</string>
<string name="last_month">Last month</string>
<string name="monthly_productivity_analysis">Monthly productivity analysis</string>
<string name="stop_alarm_question">Stop Alarm?</string>
<string name="stop_alarm_dialog_text">Current timer session is complete. Tap anywhere to stop the alarm.</string>
<string name="timer_session_count">%1$d of %2$d</string>
<string name="more">More</string>
<string name="pause">Pause</string>
<string name="play">Play</string>
<string name="restart">Restart</string>
<string name="skip_to_next">Skip to next</string>
<string name="up_next">Up next</string>
<string name="timer">Timer</string>
<string name="timer_progress">Timer progress</string>
</resources> </resources>

View File

@@ -0,0 +1,10 @@
This release contains bug fixes on top of the existing new features of 1.4.0:
New features:
- You can now choose a custom theme and color scheme for the app's UI
- New pure black dark theme mode
Fixes:
- Average focus durations now do not include days with no activity
- Fix a critical bug that caused the app's timer state to reset to Focus whenever the app was closed from recents and then opened
- Replace the word "Reset" with "Exit" in the notification to make its purpose less ambiguous