Merge branch 'dev'
This commit is contained in:
12
.github/ISSUE_TEMPLATE/bug_report.md
vendored
12
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -2,7 +2,7 @@
|
||||
name: Bug report
|
||||
about: Create a report of a bug or crash
|
||||
title: "[BUG] <enter your title here>"
|
||||
labels: bug, priority-unassigned
|
||||
labels: bug, needs-triage
|
||||
assignees: nsh07
|
||||
|
||||
---
|
||||
@@ -12,6 +12,7 @@ A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
@@ -24,10 +25,11 @@ A clear and concise description of what you expected to happen.
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Device (please complete the following information):**
|
||||
- Device model: [e.g. Google Pixel 10]
|
||||
- Android version: [e.g. Android 16]
|
||||
- App version: [e.g. v1.4.3]
|
||||
- Installed from: [e.g. Play Store]
|
||||
|
||||
- Device model: [e.g. Google Pixel 10]
|
||||
- Android version: [e.g. Android 16]
|
||||
- App version: [e.g. v1.4.3]
|
||||
- Installed from: [e.g. Play Store]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -2,7 +2,7 @@
|
||||
name: Feature request
|
||||
about: Suggest a new feature
|
||||
title: "[FEATURE] <enter your title here>"
|
||||
labels: enhancement, priority-unassigned
|
||||
labels: enhancement, needs-triage
|
||||
assignees: nsh07
|
||||
|
||||
---
|
||||
|
||||
@@ -43,8 +43,8 @@ android {
|
||||
applicationId = "org.nsh07.pomodoro"
|
||||
minSdk = 27
|
||||
targetSdk = 36
|
||||
versionCode = 22
|
||||
versionName = "1.7.0"
|
||||
versionCode = 23
|
||||
versionName = "1.7.1"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ package org.nsh07.pomodoro.data
|
||||
import androidx.compose.material3.ColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import org.nsh07.pomodoro.service.TimerStateSnapshot
|
||||
import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsState
|
||||
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState
|
||||
|
||||
@@ -28,4 +29,6 @@ class StateRepository {
|
||||
val settingsState = MutableStateFlow(SettingsState())
|
||||
var timerFrequency: Float = 60f
|
||||
var colorScheme: ColorScheme = lightColorScheme()
|
||||
var timerStateSnapshot: TimerStateSnapshot =
|
||||
TimerStateSnapshot(time = 0, timerState = TimerState())
|
||||
}
|
||||
|
||||
@@ -35,6 +35,12 @@ class ServiceHelper(private val context: Context) {
|
||||
context.startService(it)
|
||||
}
|
||||
|
||||
TimerAction.UndoReset ->
|
||||
Intent(context, TimerService::class.java).also {
|
||||
it.action = TimerService.Actions.UNDO_RESET.toString()
|
||||
context.startService(it)
|
||||
}
|
||||
|
||||
is TimerAction.SkipTimer ->
|
||||
Intent(context, TimerService::class.java).also {
|
||||
it.action = TimerService.Actions.SKIP.toString()
|
||||
|
||||
@@ -75,6 +75,8 @@ class TimerService : Service() {
|
||||
|
||||
private var lastSavedDuration = 0L
|
||||
|
||||
private val timerStateSnapshot by lazy { stateRepository.timerStateSnapshot }
|
||||
|
||||
private val saveLock = Mutex()
|
||||
private var job = SupervisorJob()
|
||||
private val timerScope = CoroutineScope(Dispatchers.IO + job)
|
||||
@@ -134,6 +136,8 @@ class TimerService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
Actions.UNDO_RESET.toString() -> undoReset()
|
||||
|
||||
Actions.SKIP.toString() -> skipScope.launch { skipTimer(true) }
|
||||
|
||||
Actions.STOP_ALARM.toString() -> stopAlarm()
|
||||
@@ -326,6 +330,16 @@ class TimerService : Service() {
|
||||
private suspend fun resetTimer() {
|
||||
val settingsState = _settingsState.value
|
||||
|
||||
timerStateSnapshot.save(
|
||||
lastSavedDuration,
|
||||
time,
|
||||
cycles,
|
||||
startTime,
|
||||
pauseTime,
|
||||
pauseDuration,
|
||||
_timerState.value
|
||||
)
|
||||
|
||||
saveTimeToDb()
|
||||
lastSavedDuration = 0
|
||||
time = settingsState.focusTime
|
||||
@@ -349,6 +363,16 @@ class TimerService : Service() {
|
||||
updateProgressSegments()
|
||||
}
|
||||
|
||||
private fun undoReset() {
|
||||
lastSavedDuration = timerStateSnapshot.lastSavedDuration
|
||||
time = timerStateSnapshot.time
|
||||
cycles = timerStateSnapshot.cycles
|
||||
startTime = timerStateSnapshot.startTime
|
||||
pauseTime = timerStateSnapshot.pauseTime
|
||||
pauseDuration = timerStateSnapshot.pauseDuration
|
||||
_timerState.update { timerStateSnapshot.timerState }
|
||||
}
|
||||
|
||||
private suspend fun skipTimer(fromButton: Boolean = false) {
|
||||
val settingsState = _settingsState.value
|
||||
saveTimeToDb()
|
||||
@@ -406,7 +430,7 @@ class TimerService : Service() {
|
||||
|
||||
autoAlarmStopScope = CoroutineScope(Dispatchers.IO).launch {
|
||||
delay(1 * 60 * 1000)
|
||||
stopAlarm()
|
||||
stopAlarm(fromAutoStop = true)
|
||||
}
|
||||
|
||||
if (settingsState.vibrateEnabled) {
|
||||
@@ -420,7 +444,13 @@ class TimerService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
fun stopAlarm() {
|
||||
/**
|
||||
* Stops ringing the alarm and vibration, and performs related necessary actions
|
||||
*
|
||||
* @param fromAutoStop Whether the function was triggered automatically by the program instead of
|
||||
* intentionally by the user
|
||||
*/
|
||||
fun stopAlarm(fromAutoStop: Boolean = false) {
|
||||
val settingsState = _settingsState.value
|
||||
autoAlarmStopScope?.cancel()
|
||||
|
||||
@@ -449,6 +479,9 @@ class TimerService : Service() {
|
||||
else -> settingsState.longBreakTime.toInt()
|
||||
}, paused = true, complete = false
|
||||
)
|
||||
|
||||
if (settingsState.autostartNextSession && !fromAutoStop) // auto start next session
|
||||
toggleTimer()
|
||||
}
|
||||
|
||||
private fun initializeMediaPlayer(): MediaPlayer? {
|
||||
@@ -515,6 +548,6 @@ class TimerService : Service() {
|
||||
}
|
||||
|
||||
enum class Actions {
|
||||
TOGGLE, SKIP, RESET, STOP_ALARM, UPDATE_ALARM_TONE
|
||||
TOGGLE, SKIP, RESET, UNDO_RESET, STOP_ALARM, UPDATE_ALARM_TONE
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
package org.nsh07.pomodoro.service
|
||||
|
||||
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState
|
||||
|
||||
data class TimerStateSnapshot(
|
||||
var lastSavedDuration: Long = 0L,
|
||||
var time: Long,
|
||||
var cycles: Int = 0,
|
||||
var startTime: Long = 0L,
|
||||
var pauseTime: Long = 0L,
|
||||
var pauseDuration: Long = 0L,
|
||||
var timerState: TimerState
|
||||
) {
|
||||
fun save(
|
||||
lastSavedDuration: Long,
|
||||
time: Long,
|
||||
cycles: Int,
|
||||
startTime: Long,
|
||||
pauseTime: Long,
|
||||
pauseDuration: Long,
|
||||
timerState: TimerState
|
||||
) {
|
||||
this.lastSavedDuration = lastSavedDuration
|
||||
this.time = time
|
||||
this.cycles = cycles
|
||||
this.startTime = startTime
|
||||
this.pauseTime = pauseTime
|
||||
this.pauseDuration = pauseDuration
|
||||
this.timerState = timerState
|
||||
}
|
||||
}
|
||||
@@ -63,7 +63,7 @@ import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.navigation3.ui.LocalNavAnimatedContentScope
|
||||
import kotlinx.coroutines.delay
|
||||
import org.nsh07.pomodoro.ui.theme.AppFonts.googleFlex600
|
||||
import org.nsh07.pomodoro.ui.theme.AppFonts.googleFlex400
|
||||
import org.nsh07.pomodoro.ui.theme.TomatoTheme
|
||||
import org.nsh07.pomodoro.ui.timerScreen.TimerScreen
|
||||
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerMode
|
||||
@@ -83,6 +83,7 @@ import kotlin.random.Random
|
||||
@Composable
|
||||
fun SharedTransitionScope.AlwaysOnDisplay(
|
||||
timerState: TimerState,
|
||||
secureAod: Boolean,
|
||||
progress: () -> Float,
|
||||
setTimerFrequency: (Float) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
@@ -100,8 +101,10 @@ fun SharedTransitionScope.AlwaysOnDisplay(
|
||||
DisposableEffect(Unit) {
|
||||
setTimerFrequency(1f)
|
||||
window.addFlags(
|
||||
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or
|
||||
WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON
|
||||
if (secureAod) {
|
||||
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or
|
||||
WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON
|
||||
} else WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
|
||||
)
|
||||
activity?.setShowWhenLocked(true)
|
||||
insetsController.apply {
|
||||
@@ -246,7 +249,7 @@ fun SharedTransitionScope.AlwaysOnDisplay(
|
||||
Text(
|
||||
text = timerState.timeStr,
|
||||
style = TextStyle(
|
||||
fontFamily = googleFlex600,
|
||||
fontFamily = googleFlex400,
|
||||
fontSize = 56.sp,
|
||||
letterSpacing = (-2).sp,
|
||||
fontFeatureSettings = "tnum"
|
||||
@@ -273,6 +276,7 @@ private fun AlwaysOnDisplayPreview() {
|
||||
SharedTransitionLayout {
|
||||
AlwaysOnDisplay(
|
||||
timerState = timerState,
|
||||
secureAod = true,
|
||||
progress = progress,
|
||||
setTimerFrequency = {}
|
||||
)
|
||||
|
||||
@@ -88,6 +88,7 @@ import androidx.window.core.layout.WindowSizeClass
|
||||
import org.nsh07.pomodoro.billing.TomatoPlusPaywallDialog
|
||||
import org.nsh07.pomodoro.service.TimerService
|
||||
import org.nsh07.pomodoro.ui.settingsScreen.SettingsScreenRoot
|
||||
import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsViewModel
|
||||
import org.nsh07.pomodoro.ui.statsScreen.StatsScreenRoot
|
||||
import org.nsh07.pomodoro.ui.timerScreen.AlarmDialog
|
||||
import org.nsh07.pomodoro.ui.timerScreen.TimerScreen
|
||||
@@ -101,11 +102,13 @@ fun AppScreen(
|
||||
isPlus: Boolean,
|
||||
setTimerFrequency: (Float) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
timerViewModel: TimerViewModel = viewModel(factory = TimerViewModel.Factory)
|
||||
timerViewModel: TimerViewModel = viewModel(factory = TimerViewModel.Factory),
|
||||
settingsViewModel: SettingsViewModel = viewModel(factory = SettingsViewModel.Factory)
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val uiState by timerViewModel.timerState.collectAsStateWithLifecycle()
|
||||
val settingsState by settingsViewModel.settingsState.collectAsStateWithLifecycle()
|
||||
val progress by timerViewModel.progress.collectAsStateWithLifecycle()
|
||||
|
||||
val layoutDirection = LocalLayoutDirection.current
|
||||
@@ -212,7 +215,7 @@ fun AppScreen(
|
||||
),
|
||||
modifier = Modifier.height(56.dp)
|
||||
) {
|
||||
Row {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Crossfade(selected) {
|
||||
if (it) Icon(
|
||||
painterResource(item.selectedIcon),
|
||||
@@ -278,6 +281,7 @@ fun AppScreen(
|
||||
entry<Screen.AOD> {
|
||||
AlwaysOnDisplay(
|
||||
timerState = uiState,
|
||||
secureAod = settingsState.secureAod,
|
||||
progress = { progress },
|
||||
setTimerFrequency = setTimerFrequency,
|
||||
modifier = if (isAODEnabled) Modifier.clickable {
|
||||
|
||||
@@ -35,7 +35,6 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.IconButtonDefaults
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme.colorScheme
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.SwitchDefaults
|
||||
import androidx.compose.material3.Text
|
||||
@@ -128,8 +127,7 @@ fun ColorSchemePickerListItem(
|
||||
leadingContent = {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.palette),
|
||||
contentDescription = null,
|
||||
tint = colorScheme.primary
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
headlineContent = { Text(stringResource(R.string.color_scheme)) },
|
||||
|
||||
@@ -24,7 +24,9 @@ import android.os.Build
|
||||
import android.provider.Settings
|
||||
import android.widget.Toast
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -38,7 +40,6 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
@@ -54,6 +55,7 @@ import androidx.compose.material3.LargeFlexibleTopAppBar
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme.colorScheme
|
||||
import androidx.compose.material3.MaterialTheme.motionScheme
|
||||
import androidx.compose.material3.MaterialTheme.typography
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Slider
|
||||
@@ -74,9 +76,11 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.nsh07.pomodoro.R
|
||||
@@ -114,46 +118,72 @@ fun TimerSettings(
|
||||
) {
|
||||
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
|
||||
val context = LocalContext.current
|
||||
val inspectionMode = LocalInspectionMode.current
|
||||
val appName = stringResource(R.string.app_name)
|
||||
val notificationManagerService =
|
||||
remember { context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager }
|
||||
val notificationManagerService = remember {
|
||||
if (!inspectionMode)
|
||||
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
else
|
||||
null
|
||||
}
|
||||
|
||||
val switchItems = remember(
|
||||
settingsState.dndEnabled,
|
||||
settingsState.aodEnabled,
|
||||
settingsState.autostartNextSession,
|
||||
settingsState.secureAod,
|
||||
isPlus,
|
||||
serviceRunning
|
||||
) {
|
||||
listOf(
|
||||
SettingsSwitchItem(
|
||||
checked = settingsState.dndEnabled,
|
||||
enabled = !serviceRunning,
|
||||
icon = R.drawable.dnd,
|
||||
label = R.string.dnd,
|
||||
description = R.string.dnd_desc,
|
||||
onClick = {
|
||||
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)
|
||||
listOf(
|
||||
SettingsSwitchItem(
|
||||
checked = settingsState.autostartNextSession,
|
||||
icon = R.drawable.autoplay,
|
||||
label = R.string.auto_start_next_timer,
|
||||
description = R.string.auto_start_next_timer_desc,
|
||||
onClick = { onAction(SettingsAction.SaveAutostartNextSession(it)) }
|
||||
),
|
||||
SettingsSwitchItem(
|
||||
checked = settingsState.dndEnabled,
|
||||
enabled = !serviceRunning,
|
||||
icon = R.drawable.dnd,
|
||||
label = R.string.dnd,
|
||||
description = R.string.dnd_desc,
|
||||
onClick = {
|
||||
if (it && notificationManagerService?.isNotificationPolicyAccessGranted() == false) {
|
||||
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() == true) {
|
||||
notificationManagerService.setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_ALL)
|
||||
}
|
||||
onAction(SettingsAction.SaveDndEnabled(it))
|
||||
}
|
||||
onAction(SettingsAction.SaveDndEnabled(it))
|
||||
}
|
||||
)
|
||||
),
|
||||
SettingsSwitchItem(
|
||||
checked = settingsState.aodEnabled,
|
||||
enabled = isPlus,
|
||||
icon = R.drawable.aod,
|
||||
label = R.string.always_on_display,
|
||||
description = R.string.always_on_display_desc,
|
||||
onClick = { onAction(SettingsAction.SaveAodEnabled(it)) }
|
||||
listOf(
|
||||
SettingsSwitchItem(
|
||||
checked = settingsState.aodEnabled,
|
||||
enabled = isPlus,
|
||||
icon = R.drawable.aod,
|
||||
label = R.string.always_on_display,
|
||||
description = R.string.always_on_display_desc,
|
||||
onClick = { onAction(SettingsAction.SaveAodEnabled(it)) }
|
||||
),
|
||||
SettingsSwitchItem(
|
||||
checked = settingsState.secureAod && isPlus,
|
||||
enabled = isPlus,
|
||||
icon = R.drawable.mobile_lock_portrait,
|
||||
label = R.string.secure_aod,
|
||||
description = R.string.secure_aod_desc,
|
||||
onClick = { onAction(SettingsAction.SaveSecureAod(it)) }
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -310,7 +340,7 @@ fun TimerSettings(
|
||||
}
|
||||
item { Spacer(Modifier.height(12.dp)) }
|
||||
|
||||
itemsIndexed(if (isPlus) switchItems else switchItems.take(1)) { index, item ->
|
||||
itemsIndexed(switchItems[0]) { index, item ->
|
||||
ListItem(
|
||||
leadingContent = {
|
||||
Icon(
|
||||
@@ -346,59 +376,18 @@ fun TimerSettings(
|
||||
},
|
||||
colors = listItemColors,
|
||||
modifier = Modifier.clip(
|
||||
if (isPlus) when (index) {
|
||||
when (index) {
|
||||
0 -> topListItemShape
|
||||
switchItems.size - 1 -> bottomListItemShape
|
||||
switchItems[0].size - 1 -> bottomListItemShape
|
||||
else -> middleListItemShape
|
||||
}
|
||||
else cardShape
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA) {
|
||||
if (isPlus) {
|
||||
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) {
|
||||
item {
|
||||
PlusDivider(setShowPaywall)
|
||||
}
|
||||
items(switchItems.drop(1)) { item ->
|
||||
itemsIndexed(switchItems[1]) { index, item ->
|
||||
ListItem(
|
||||
leadingContent = {
|
||||
Icon(
|
||||
@@ -433,11 +422,115 @@ fun TimerSettings(
|
||||
)
|
||||
},
|
||||
colors = listItemColors,
|
||||
modifier = Modifier.clip(
|
||||
when (index) {
|
||||
0 -> topListItemShape
|
||||
switchItems[1].size - 1 -> bottomListItemShape
|
||||
else -> middleListItemShape
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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 = {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
Text(
|
||||
stringResource(R.string.session_only_progress_desc),
|
||||
maxLines = if (expanded) Int.MAX_VALUE else 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier
|
||||
.clickable { expanded = !expanded }
|
||||
.animateContentSize(motionScheme.defaultSpatialSpec())
|
||||
)
|
||||
},
|
||||
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) {
|
||||
item {
|
||||
PlusDivider(setShowPaywall)
|
||||
}
|
||||
itemsIndexed(switchItems[1]) { index, item ->
|
||||
ListItem(
|
||||
leadingContent = {
|
||||
Icon(
|
||||
painterResource(item.icon),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.padding(top = 4.dp)
|
||||
)
|
||||
},
|
||||
headlineContent = { Text(stringResource(item.label)) },
|
||||
supportingContent = { Text(stringResource(item.description)) },
|
||||
trailingContent = {
|
||||
Switch(
|
||||
checked = item.checked,
|
||||
onCheckedChange = { item.onClick(it) },
|
||||
enabled = item.enabled,
|
||||
thumbContent = {
|
||||
if (item.checked) {
|
||||
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(
|
||||
when (index) {
|
||||
0 -> topListItemShape
|
||||
switchItems[1].size - 1 -> bottomListItemShape
|
||||
else -> middleListItemShape
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
Column(
|
||||
@@ -487,7 +580,7 @@ private fun TimerSettingsPreview() {
|
||||
)
|
||||
TimerSettings(
|
||||
isPlus = false,
|
||||
serviceRunning = true,
|
||||
serviceRunning = false,
|
||||
settingsState = remember { SettingsState() },
|
||||
contentPadding = PaddingValues(),
|
||||
focusTimeInputFieldState = focusTimeInputFieldState,
|
||||
|
||||
@@ -28,6 +28,8 @@ sealed interface SettingsAction {
|
||||
data class SaveDndEnabled(val enabled: Boolean) : SettingsAction
|
||||
data class SaveMediaVolumeForAlarm(val enabled: Boolean) : SettingsAction
|
||||
data class SaveSingleProgressBar(val enabled: Boolean) : SettingsAction
|
||||
data class SaveAutostartNextSession(val enabled: Boolean) : SettingsAction
|
||||
data class SaveSecureAod(val enabled: Boolean) : SettingsAction
|
||||
data class SaveAlarmSound(val uri: Uri?) : SettingsAction
|
||||
data class SaveTheme(val theme: String) : SettingsAction
|
||||
data class SaveColorScheme(val color: Color) : SettingsAction
|
||||
|
||||
@@ -33,6 +33,8 @@ data class SettingsState(
|
||||
val dndEnabled: Boolean = false,
|
||||
val mediaVolumeForAlarm: Boolean = false,
|
||||
val singleProgressBar: Boolean = false,
|
||||
val autostartNextSession: Boolean = false,
|
||||
val secureAod: Boolean = true,
|
||||
|
||||
val focusTime: Long = 25 * 60 * 1000L,
|
||||
val shortBreakTime: Long = 5 * 60 * 1000L,
|
||||
|
||||
@@ -39,6 +39,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
@@ -64,7 +65,9 @@ class SettingsViewModel(
|
||||
val backStack = mutableStateListOf<Screen.Settings>(Screen.Settings.Main)
|
||||
|
||||
val isPlus = billingManager.isPlus
|
||||
val serviceRunning = stateRepository.timerState.map { it.serviceRunning }
|
||||
val serviceRunning = stateRepository.timerState
|
||||
.map { it.serviceRunning }
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
viewModelScope,
|
||||
SharingStarted.WhileSubscribed(5000),
|
||||
@@ -111,6 +114,8 @@ class SettingsViewModel(
|
||||
is SettingsAction.SaveDndEnabled -> saveDndEnabled(action.enabled)
|
||||
is SettingsAction.SaveMediaVolumeForAlarm -> saveMediaVolumeForAlarm(action.enabled)
|
||||
is SettingsAction.SaveSingleProgressBar -> saveSingleProgressBar(action.enabled)
|
||||
is SettingsAction.SaveAutostartNextSession -> saveAutostartNextSession(action.enabled)
|
||||
is SettingsAction.SaveSecureAod -> saveSecureAod(action.enabled)
|
||||
is SettingsAction.SaveColorScheme -> saveColorScheme(action.color)
|
||||
is SettingsAction.SaveTheme -> saveTheme(action.theme)
|
||||
is SettingsAction.SaveBlackTheme -> saveBlackTheme(action.enabled)
|
||||
@@ -286,6 +291,30 @@ class SettingsViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveAutostartNextSession(autostartNextSession: Boolean) {
|
||||
viewModelScope.launch {
|
||||
_settingsState.update { currentState ->
|
||||
currentState.copy(autostartNextSession = autostartNextSession)
|
||||
}
|
||||
preferenceRepository.saveBooleanPreference(
|
||||
"autostart_next_session",
|
||||
autostartNextSession
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveSecureAod(secureAod: Boolean) {
|
||||
viewModelScope.launch {
|
||||
_settingsState.update { currentState ->
|
||||
currentState.copy(secureAod = secureAod)
|
||||
}
|
||||
preferenceRepository.saveBooleanPreference(
|
||||
"secure_aod",
|
||||
secureAod
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun reloadSettings() {
|
||||
var settingsState = _settingsState.value
|
||||
val focusTime =
|
||||
@@ -353,6 +382,14 @@ class SettingsViewModel(
|
||||
"single_progress_bar",
|
||||
settingsState.singleProgressBar
|
||||
)
|
||||
val autostartNextSession =
|
||||
preferenceRepository.getBooleanPreference("autostart_next_session")
|
||||
?: preferenceRepository.saveBooleanPreference(
|
||||
"autostart_next_session",
|
||||
settingsState.autostartNextSession
|
||||
)
|
||||
val secureAod = preferenceRepository.getBooleanPreference("secure_aod")
|
||||
?: preferenceRepository.saveBooleanPreference("secure_aod", true)
|
||||
|
||||
_settingsState.update { currentState ->
|
||||
currentState.copy(
|
||||
@@ -369,7 +406,9 @@ class SettingsViewModel(
|
||||
vibrateEnabled = vibrateEnabled,
|
||||
dndEnabled = dndEnabled,
|
||||
mediaVolumeForAlarm = mediaVolumeForAlarm,
|
||||
singleProgressBar = singleProgressBar
|
||||
singleProgressBar = singleProgressBar,
|
||||
autostartNextSession = autostartNextSession,
|
||||
secureAod = secureAod
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -27,9 +27,10 @@ import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModelProducer
|
||||
import com.patrykandpatrick.vico.core.cartesian.data.columnSeries
|
||||
import com.patrykandpatrick.vico.core.cartesian.data.lineSeries
|
||||
import com.patrykandpatrick.vico.core.common.data.ExtraStore
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -46,7 +47,13 @@ class StatsViewModel(
|
||||
private val statRepository: StatRepository
|
||||
) : ViewModel() {
|
||||
|
||||
val todayStat = statRepository.getTodayStat().distinctUntilChanged()
|
||||
val todayStat = statRepository
|
||||
.getTodayStat()
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5000),
|
||||
initialValue = null
|
||||
)
|
||||
|
||||
private val lastWeekSummary =
|
||||
Pair(CartesianChartModelProducer(), ExtraStore.Key<List<String>>())
|
||||
@@ -75,6 +82,7 @@ class StatsViewModel(
|
||||
}
|
||||
lastWeekSummary
|
||||
}
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5000),
|
||||
@@ -91,6 +99,7 @@ class StatsViewModel(
|
||||
it?.focusTimeQ4?.toInt() ?: 0
|
||||
)
|
||||
}
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5000),
|
||||
@@ -109,6 +118,7 @@ class StatsViewModel(
|
||||
}
|
||||
lastMonthSummary
|
||||
}
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5000),
|
||||
@@ -125,6 +135,7 @@ class StatsViewModel(
|
||||
it?.focusTimeQ4?.toInt() ?: 0
|
||||
)
|
||||
}
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5000),
|
||||
@@ -143,6 +154,7 @@ class StatsViewModel(
|
||||
}
|
||||
lastYearSummary
|
||||
}
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5000),
|
||||
@@ -159,6 +171,7 @@ class StatsViewModel(
|
||||
it?.focusTimeQ4?.toInt() ?: 0
|
||||
)
|
||||
}
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5000),
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
package org.nsh07.pomodoro.ui.timerScreen
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Build
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
@@ -65,6 +66,10 @@ import androidx.compose.material3.MaterialTheme.motionScheme
|
||||
import androidx.compose.material3.MaterialTheme.shapes
|
||||
import androidx.compose.material3.MaterialTheme.typography
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.SnackbarResult
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
@@ -74,6 +79,7 @@ import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
|
||||
@@ -83,6 +89,7 @@ import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.res.painterResource
|
||||
@@ -94,8 +101,8 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation3.ui.LocalNavAnimatedContentScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.nsh07.pomodoro.R
|
||||
import org.nsh07.pomodoro.ui.mergePaddingValues
|
||||
import org.nsh07.pomodoro.ui.theme.AppFonts.googleFlex600
|
||||
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
|
||||
import org.nsh07.pomodoro.ui.theme.TomatoTheme
|
||||
@@ -103,7 +110,6 @@ import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerAction
|
||||
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerMode
|
||||
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState
|
||||
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun SharedTransitionScope.TimerScreen(
|
||||
@@ -115,7 +121,9 @@ fun SharedTransitionScope.TimerScreen(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val motionScheme = motionScheme
|
||||
val scope = rememberCoroutineScope()
|
||||
val haptic = LocalHapticFeedback.current
|
||||
val context = LocalContext.current
|
||||
|
||||
val color by animateColorAsState(
|
||||
if (timerState.timerMode == TimerMode.FOCUS) colorScheme.primary
|
||||
@@ -139,6 +147,7 @@ fun SharedTransitionScope.TimerScreen(
|
||||
)
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
@@ -215,16 +224,16 @@ fun SharedTransitionScope.TimerScreen(
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
},
|
||||
bottomBar = { Spacer(Modifier.height(contentPadding.calculateBottomPadding())) },
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||
modifier = modifier
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||
) { innerPadding ->
|
||||
val insets = mergePaddingValues(innerPadding, contentPadding)
|
||||
LazyColumn(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = CenterHorizontally,
|
||||
contentPadding = insets,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
contentPadding = innerPadding,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
item {
|
||||
Column(horizontalAlignment = CenterHorizontally) {
|
||||
@@ -411,6 +420,19 @@ fun SharedTransitionScope.TimerScreen(
|
||||
onClick = {
|
||||
onAction(TimerAction.ResetTimer)
|
||||
haptic.performHapticFeedback(HapticFeedbackType.VirtualKey)
|
||||
|
||||
@SuppressLint("LocalContextGetResourceValueCall")
|
||||
scope.launch {
|
||||
val result = snackbarHostState.showSnackbar(
|
||||
context.getString(R.string.timer_reset_message),
|
||||
actionLabel = context.getString(R.string.undo),
|
||||
withDismissAction = true,
|
||||
duration = SnackbarDuration.Long
|
||||
)
|
||||
if (result == SnackbarResult.ActionPerformed) {
|
||||
onAction(TimerAction.UndoReset)
|
||||
}
|
||||
}
|
||||
},
|
||||
colors = IconButtonDefaults.filledTonalIconButtonColors(
|
||||
containerColor = colorContainer
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Nishant Mishra
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
* 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.ui.timerScreen.viewModel
|
||||
@@ -11,6 +21,7 @@ sealed interface TimerAction {
|
||||
data class SkipTimer(val fromButton: Boolean) : TimerAction
|
||||
|
||||
data object ResetTimer : TimerAction
|
||||
data object UndoReset : TimerAction
|
||||
data object StopAlarm : TimerAction
|
||||
data object ToggleTimer : TimerAction
|
||||
}
|
||||
26
app/src/main/res/drawable/autoplay.xml
Normal file
26
app/src/main/res/drawable/autoplay.xml
Normal 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="M380,623v-286q0,-12 10.5,-18t20.5,1l223,143q9,6 9,17t-9,17L411,640q-10,7 -20.5,1T380,623ZM120,732v68q0,17 -11.5,28.5T80,840q-17,0 -28.5,-11.5T40,800v-160q0,-17 11.5,-28.5T80,600h160q17,0 28.5,11.5T280,640q0,17 -11.5,28.5T240,680h-58q51,75 129.5,117.5T480,840q104,0 190,-54t132,-145q9,-17 23.5,-27t33.5,-6q18,4 23.5,20.5T879,664q-54,116 -161,186T480,920q-108,0 -202.5,-49.5T120,732ZM83,440q-17,0 -27.5,-12.5T48,398q10,-47 26,-86.5t43,-79.5q10,-15 26,-17t29,11q14,14 14,30.5T175,290q-17,26 -27,52t-18,57q-4,18 -16.5,29.5T83,440ZM440,82q0,19 -11.5,31T398,129q-30,7 -55.5,18T291,175q-16,11 -32.5,10T228,170q-12,-12 -10.5,-27.5T233,116q39,-26 77.5,-42.5T396,48q18,-3 31,7t13,27ZM734,170q-14,14 -31,14.5T670,174q-26,-17 -52,-27t-57,-18q-18,-4 -29.5,-16.5T520,82q0,-17 12.5,-27t29.5,-7q48,9 87,25t79,43q14,10 16,26t-10,28ZM878,440q-19,0 -31,-11.5T831,398q-8,-31 -18.5,-56.5T785,289q-11,-16 -10,-32.5t15,-30.5q12,-12 27.5,-10t26.5,16q27,40 43,79t25,87q3,17 -7,29.5T878,440Z" />
|
||||
</vector>
|
||||
26
app/src/main/res/drawable/mobile_lock_portrait.xml
Normal file
26
app/src/main/res/drawable/mobile_lock_portrait.xml
Normal 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="M280,920q-33,0 -56.5,-23.5T200,840v-720q0,-33 23.5,-56.5T280,40h400q33,0 56.5,23.5T760,120v124q18,7 29,22t11,34v80q0,19 -11,34t-29,22v404q0,33 -23.5,56.5T680,920L280,920ZM394,640h172q14,0 24,-10t10,-24v-132q0,-14 -10,-24t-24,-10h-6v-40q0,-33 -23.5,-56.5T480,320q-33,0 -56.5,23.5T400,400v40h-6q-14,0 -24,10t-10,24v132q0,14 10,24t24,10ZM440,440v-40q0,-17 11.5,-28.5T480,360q17,0 28.5,11.5T520,400v40h-80Z" />
|
||||
</vector>
|
||||
@@ -74,5 +74,24 @@
|
||||
<string name="rate_on_google_play">قيم التطبيق علي جوجل بلاي</string>
|
||||
<string name="selected">مُختار</string>
|
||||
<string name="timer_settings_reset_info">أعد المؤقت لتغير الاعدادات</string>
|
||||
<string name="help_with_translation">المساعدة في الترجمة</string>
|
||||
<string name="help_with_translation">المساعدة في ترجمة Tomato</string>
|
||||
<string name="hours_and_minutes_format">%1$dس %2$dد</string>
|
||||
<string name="hours_format">%1$dس</string>
|
||||
<string name="minutes_format">%1$dد</string>
|
||||
<string name="about">عن</string>
|
||||
<string name="help_with_translation_desc">ترجم Tomato إلى لغتك</string>
|
||||
<string name="rate_on_google_play_desc">هل أعجبك التطبيق؟ اكتب مراجعة!</string>
|
||||
<string name="bmc_desc">ادعمني بتبرع صغير</string>
|
||||
<string name="tomato_plus_desc">التخصيص أكثر باستخدام Tomato+</string>
|
||||
<string name="license">الترخيص</string>
|
||||
<string name="media_volume_for_alarm">وضع سماعات الرأس</string>
|
||||
<string name="media_volume_for_alarm_desc">يعمل فقط على سماعات الرأس. إذا كانت سماعات الرأس مفصولة، يصدر المنبه عبر سماعة الهاتف عند مستوى صوت الوسائط.</string>
|
||||
<string name="session_only_progress">التقدم في الجلسة فقط</string>
|
||||
<string name="session_only_progress_desc">عرض التقدم للجلسة الحالية بدلا من سلسلة الجلسات كلها</string>
|
||||
<string name="auto_start_next_timer_desc">ابدأ المؤقت التالي بعد إيقاف المنبه</string>
|
||||
<string name="auto_start_next_timer">التشغيل التلقائي في المؤقت القادم</string>
|
||||
<string name="secure_aod">AOD الآمن</string>
|
||||
<string name="secure_aod_desc">قم بقفل جهازك تلقائيا بعد انتهاء الوقت، مع إبقاء AOD مرئياً</string>
|
||||
<string name="timer_reset_message">إعادة ضبط المؤقت</string>
|
||||
<string name="undo">تراجع</string>
|
||||
</resources>
|
||||
|
||||
@@ -73,6 +73,19 @@
|
||||
<string name="choose_language">Sprache wählen</string>
|
||||
<string name="rate_on_google_play">Im Play Store bewerten</string>
|
||||
<string name="selected">Ausgewählt</string>
|
||||
<string name="help_with_translation">Hilf beim Übersetzen</string>
|
||||
<string name="help_with_translation">Hilf beim Übersetzen von Tomato</string>
|
||||
<string name="timer_settings_reset_info">Den Timer zurücksetzen um Einstellungen zu ändern</string>
|
||||
<string name="hours_and_minutes_format">%1$dh%2$ddm</string>
|
||||
<string name="hours_format">%1$dh</string>
|
||||
<string name="minutes_format">%1$dm</string>
|
||||
<string name="about">Über</string>
|
||||
<string name="help_with_translation_desc">Übersetze Tomato in deine Sprache</string>
|
||||
<string name="rate_on_google_play_desc">Dir gefällt die App? Schreibe eine Bewertung!</string>
|
||||
<string name="bmc_desc">Unterstütze mich mit einer kleinen Spende</string>
|
||||
<string name="tomato_plus_desc">Weiter anpassen mit Tomato+</string>
|
||||
<string name="license">Lizenz</string>
|
||||
<string name="media_volume_for_alarm">Kopfhörer Modus</string>
|
||||
<string name="media_volume_for_alarm_desc">Spielt nur auf Kopfhörern. Wenn die Kopfhörer getrennt sind, wieder Alarm in Medienlautstärke abgespielt.</string>
|
||||
<string name="session_only_progress">Sitzungs-Fortschritt</string>
|
||||
<string name="session_only_progress_desc">Zeige in den Benachrichtigungen nur den Fortschritt der aktuellen Sitzung an, nicht die gesamte Sequenz.</string>
|
||||
</resources>
|
||||
|
||||
@@ -80,5 +80,10 @@
|
||||
<string name="tomato_plus_desc">Personnalisez plus avec Tomato+</string>
|
||||
<string name="about">A propos</string>
|
||||
<string name="help_with_translation_desc">Traduire Tomato dans votre langue</string>
|
||||
<string name="rate_on_google_play_desc">Vous aimez l\'appli? Écrivez un avis!</string>
|
||||
<string name="rate_on_google_play_desc">Vous avez aimé l\'appli? Écrivez un avis!</string>
|
||||
<string name="license">Licence</string>
|
||||
<string name="media_volume_for_alarm">Mode casque</string>
|
||||
<string name="session_only_progress">Progression de la session uniquement</string>
|
||||
<string name="session_only_progress_desc">Montre uniquement la progression de la session actuelle, au lieu de la séquence entière.</string>
|
||||
<string name="media_volume_for_alarm_desc">Fonctionne uniquement avec un casque. Si le casque est déconnecté, l\'alarme retentit via le haut-parleur au volume des médias.</string>
|
||||
</resources>
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
<string name="start_next">अगला शुरू करें</string>
|
||||
<string name="stats">आँकड़े</string>
|
||||
<string name="stop_alarm">अलार्म बंद करें</string>
|
||||
<string name="stop_alarm_dialog_text">वर्तमान टाइमर सत्र पूरा हो गया है। अलार्म बंद करने के लिए कहीं भी टैप करें।</string>
|
||||
<string name="stop_alarm_dialog_text">वर्तमान टाइमर पूरा हो गया है। अलार्म बंद करने के लिए कहीं भी टैप करें।</string>
|
||||
<string name="stop_alarm_question">अलार्म बंद करें?</string>
|
||||
<string name="system_default">सिस्टम</string>
|
||||
<string name="theme">थीम</string>
|
||||
@@ -72,6 +72,25 @@
|
||||
<string name="choose_language">भाषा चुनें</string>
|
||||
<string name="rate_on_google_play">Google Play पर रेटिंग दें</string>
|
||||
<string name="selected">चयनित</string>
|
||||
<string name="help_with_translation">अनुवाद में सहायता करें</string>
|
||||
<string name="help_with_translation">Tomato के अनुवाद में मदद करें</string>
|
||||
<string name="timer_settings_reset_info">सेटिंग्स बदलने के लिए टाइमर रीसेट करें</string>
|
||||
<string name="hours_and_minutes_format">%1$dघं %2$dमि</string>
|
||||
<string name="hours_format">%1$dघं</string>
|
||||
<string name="minutes_format">%1$dमि</string>
|
||||
<string name="about">ऐप के बारे में</string>
|
||||
<string name="help_with_translation_desc">Tomato का अपनी भाषा में अनुवाद करें</string>
|
||||
<string name="rate_on_google_play_desc">ऐप पसंद आया? रिव्यू लिखें!</string>
|
||||
<string name="bmc_desc">एक छोटे से दान से मेरी मदद करें</string>
|
||||
<string name="tomato_plus_desc">Tomato+ के साथ और कस्टमाइज़ करें</string>
|
||||
<string name="license">लाइसेंस</string>
|
||||
<string name="media_volume_for_alarm">हेडफ़ोन मोड</string>
|
||||
<string name="media_volume_for_alarm_desc">अलार्म सिर्फ़ हेडफ़ोन पर बजता है. अगर हेडफ़ोन कनेक्ट नहीं हैं, तो अलार्म मीडिया वॉल्यूम पर स्पीकर से बजता है।</string>
|
||||
<string name="session_only_progress">सरलीकृत प्रगति</string>
|
||||
<string name="session_only_progress_desc">पूरे सत्र के बजाय, नोटिफ़िकेशन में केवल मौजूदा टाइमर की प्रगति दिखाएँ</string>
|
||||
<string name="auto_start_next_timer_desc">अलार्म बंद करने के बाद अगला टाइमर शुरू करें</string>
|
||||
<string name="auto_start_next_timer">टाइमर स्वयं आरंभ</string>
|
||||
<string name="secure_aod">सुरक्षित AOD</string>
|
||||
<string name="secure_aod_desc">तय समय के बाद डिवाइस को स्वयं लॉक कर दें, जबकि AOD दिखता रहे</string>
|
||||
<string name="timer_reset_message">टाइमर रीसेट किया गया</string>
|
||||
<string name="undo">पूर्ववत् करें</string>
|
||||
</resources>
|
||||
|
||||
@@ -74,13 +74,15 @@
|
||||
<string name="always_on_display_desc">タイマー表示中に常時表示ディスプレイに切り替えるには任意の場所をタップしてください</string>
|
||||
<string name="dnd_desc">タイマーの実行時にサイレントモードをオンにします</string>
|
||||
<string name="tomato_foss_desc">全機能がこのバージョンでは解放されています。私のアプリが生活に変化をもたらしたのであれば、%1$s で寄付することで私を支援することをご検討ください。</string>
|
||||
<string name="hours_and_minutes_format">%d時%d分</string>
|
||||
<string name="hours_format">%d時</string>
|
||||
<string name="minutes_format">%d分</string>
|
||||
<string name="about">について</string>
|
||||
<string name="hours_and_minutes_format">%1$d時%2$d分</string>
|
||||
<string name="hours_format">%1$d時</string>
|
||||
<string name="minutes_format">%1$d分</string>
|
||||
<string name="about">このアプリについて</string>
|
||||
<string name="help_with_translation_desc">Tomato をあなたの言語に翻訳してください</string>
|
||||
<string name="rate_on_google_play_desc">アプリが気に入りましたか?レビューを書いてください!</string>
|
||||
<string name="bmc_desc">小さな寄付でご支援してもよろしいでしょうか</string>
|
||||
<string name="tomato_plus_desc">Tomato+ を使用してさらにカスタマイズする</string>
|
||||
<string name="license">ライセンス</string>
|
||||
<string name="media_volume_for_alarm">ヘッドホンモード</string>
|
||||
<string name="media_volume_for_alarm_desc">ヘッドホンで再生します。ヘッドホンが接続されていない場合はスピーカーからメディアボリュームで再生されます。</string>
|
||||
</resources>
|
||||
|
||||
3
app/src/main/res/values-te/strings.xml
Normal file
3
app/src/main/res/values-te/strings.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
</resources>
|
||||
@@ -75,13 +75,17 @@
|
||||
<string name="selected">Seçilen</string>
|
||||
<string name="help_with_translation">Tomato\'nun çevirisine yardım edin</string>
|
||||
<string name="timer_settings_reset_info">Ayarları değiştirmek için zamanlayıcıyı sıfırlayın</string>
|
||||
<string name="hours_and_minutes_format">%dsa %ddk</string>
|
||||
<string name="hours_format">%dsa</string>
|
||||
<string name="minutes_format">%ddk</string>
|
||||
<string name="hours_and_minutes_format">%1$dsa %2$ddk</string>
|
||||
<string name="hours_format">%1$dsa</string>
|
||||
<string name="minutes_format">%1$ddk</string>
|
||||
<string name="about">Hakkında</string>
|
||||
<string name="help_with_translation_desc">Tomato\'yu kendi dilinize çevirin</string>
|
||||
<string name="rate_on_google_play_desc">Uygulamayı beğendinizmi? Bir yorum bırakın!</string>
|
||||
<string name="bmc_desc">Beni küçük bir bağışla destekleyin</string>
|
||||
<string name="tomato_plus_desc">Tomato+ ile daha fazla özelleştirin</string>
|
||||
<string name="license">Lisans</string>
|
||||
<string name="media_volume_for_alarm">Kulaklık modu</string>
|
||||
<string name="media_volume_for_alarm_desc">Sadece kulaklıktan oynatır. Eğer kulaklık devre dışı ise alarmı direkt medya sesi düzeyinde hoparlörden oynatır.</string>
|
||||
<string name="session_only_progress">Yalnızca bu oturumun ilerlemesi</string>
|
||||
<string name="session_only_progress_desc">Bildirimlerde tüm ilerlemeyi göstermektense yalnızca bu oturuma ait ilerlemeyi gösterir.</string>
|
||||
</resources>
|
||||
|
||||
@@ -76,12 +76,17 @@
|
||||
<string name="selected">Обрано</string>
|
||||
<string name="timer_settings_reset_info">Перезапустіть таймер, щоб змінити налаштування</string>
|
||||
<string name="help_with_translation">Допомогти з перекладом Tomato</string>
|
||||
<string name="hours_and_minutes_format">%dгод %dхв</string>
|
||||
<string name="hours_format">%dгод</string>
|
||||
<string name="minutes_format">%dхв</string>
|
||||
<string name="hours_and_minutes_format">%1$dгод %2$dхв</string>
|
||||
<string name="hours_format">%1$dгод</string>
|
||||
<string name="minutes_format">%1$dхв</string>
|
||||
<string name="about">Відомості</string>
|
||||
<string name="help_with_translation_desc">Перекладіть Tomato на Вашу мову</string>
|
||||
<string name="rate_on_google_play_desc">Сподобався додаток? Напишіть відгук!</string>
|
||||
<string name="bmc_desc">Підтримайте мене невеликим пожертвуванням</string>
|
||||
<string name="tomato_plus_desc">Налаштуйте ще більше з Tomato+</string>
|
||||
<string name="license">Ліцензія</string>
|
||||
<string name="media_volume_for_alarm">Режим навушників</string>
|
||||
<string name="media_volume_for_alarm_desc">Сигнал грає лише в навушниках. Якщо навушники від\'єднані, сигнал грає через динамік на гучності звуку медіа.</string>
|
||||
<string name="session_only_progress">Прогрес лише поточної сесії</string>
|
||||
<string name="session_only_progress_desc">Показувати прогрес у сповіщенні лише для поточної сесії, а не для усього порядку.</string>
|
||||
</resources>
|
||||
|
||||
@@ -84,4 +84,8 @@
|
||||
<string name="bmc_desc">小额捐赠支持</string>
|
||||
<string name="tomato_plus_desc">用 Tomato+ 进一步定制</string>
|
||||
<string name="license">许可证</string>
|
||||
<string name="media_volume_for_alarm">耳机模式</string>
|
||||
<string name="media_volume_for_alarm_desc">仅在插入耳机时播放。如果耳机连接断开,闹铃以媒体音量通过扬声器播放。</string>
|
||||
<string name="session_only_progress">仅会话进度</string>
|
||||
<string name="session_only_progress_desc">在通知中只显示当前会话的进度,而非完整序列的进度。</string>
|
||||
</resources>
|
||||
|
||||
@@ -75,6 +75,19 @@
|
||||
<string name="choose_language">選擇語言</string>
|
||||
<string name="rate_on_google_play">在 Google Play 上評分</string>
|
||||
<string name="selected">已選擇</string>
|
||||
<string name="help_with_translation">協助翻譯</string>
|
||||
<string name="help_with_translation">協助翻譯 Tomato</string>
|
||||
<string name="timer_settings_reset_info">重置計時器以變更設定</string>
|
||||
<string name="hours_and_minutes_format">%1$d小時 %2$d分鐘</string>
|
||||
<string name="hours_format">%1$d小時</string>
|
||||
<string name="minutes_format">%1$d分鐘</string>
|
||||
<string name="about">關於</string>
|
||||
<string name="help_with_translation_desc">將 Tomato 翻譯成你的語言</string>
|
||||
<string name="rate_on_google_play_desc">喜歡這個 App 嗎?寫個評論吧!</string>
|
||||
<string name="bmc_desc">以小額捐款支持我</string>
|
||||
<string name="tomato_plus_desc">使用 Tomato+ 進一步自訂</string>
|
||||
<string name="license">授權條款</string>
|
||||
<string name="media_volume_for_alarm">耳機模式</string>
|
||||
<string name="media_volume_for_alarm_desc">僅透過耳機播放。若耳機斷線,鬧鐘將以媒體音量從喇叭播放。</string>
|
||||
<string name="session_only_progress">僅顯示工作階段進度</string>
|
||||
<string name="session_only_progress_desc">在通知中僅顯示目前工作階段的進度,而非完整流程。</string>
|
||||
</resources>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
~ If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<resources>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<string name="alarm">Alarm</string>
|
||||
<string name="alarm_desc">Ring alarm when a timer completes</string>
|
||||
<string name="alarm_sound">Alarm sound</string>
|
||||
@@ -68,7 +68,7 @@
|
||||
<string name="productivity_analysis_desc">Focus durations at different times of the day</string>
|
||||
<string name="rate_on_google_play">Rate on Google Play</string>
|
||||
<string name="restart">Restart</string>
|
||||
<string name="selected">Selected</string>
|
||||
<string name="selected" tools:override="true">Selected</string>
|
||||
<string name="session_length">Session length</string>
|
||||
<string name="session_length_desc">Focus intervals in one session: %1$d</string>
|
||||
<string name="settings">Settings</string>
|
||||
@@ -81,7 +81,7 @@
|
||||
<string name="stats">Stats</string>
|
||||
<string name="stop">Stop</string>
|
||||
<string name="stop_alarm">Stop alarm</string>
|
||||
<string name="stop_alarm_dialog_text">Current timer session is complete. Tap anywhere to stop the alarm.</string>
|
||||
<string name="stop_alarm_dialog_text">Current timer is complete. Tap anywhere to stop the alarm.</string>
|
||||
<string name="stop_alarm_question">Stop Alarm?</string>
|
||||
<string name="system_default">System</string>
|
||||
<string name="theme">Theme</string>
|
||||
@@ -105,6 +105,12 @@
|
||||
<string name="license">License</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="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>
|
||||
<string name="session_only_progress">Simplified progress</string>
|
||||
<string name="session_only_progress_desc">Show progress for the current timer only in notifications, rather than the entire session</string>
|
||||
<string name="auto_start_next_timer_desc">Start next timer after stopping an alarm</string>
|
||||
<string name="auto_start_next_timer">Auto start next timer</string>
|
||||
<string name="secure_aod">Secure AOD</string>
|
||||
<string name="secure_aod_desc">Automatically lock your device after a timeout, while keeping the AOD visible</string>
|
||||
<string name="timer_reset_message">Timer reset</string>
|
||||
<string name="undo">Undo</string>
|
||||
</resources>
|
||||
9
fastlane/metadata/android/en-US/changelogs/23.txt
Normal file
9
fastlane/metadata/android/en-US/changelogs/23.txt
Normal file
@@ -0,0 +1,9 @@
|
||||
New features:
|
||||
- AOD mode now uses a lighter font
|
||||
- New option to auto start next session after stopping an alarm
|
||||
- New option to disable locking screen while in AOD mode
|
||||
- Accidentally reset the timer? You can now undo and correct your mistake ;)
|
||||
|
||||
Fixes:
|
||||
- Improved stats screen performance and fixed lag while opening stats screen
|
||||
- Fixed incorrect alignment of text in navigation toolbar
|
||||
@@ -12,7 +12,7 @@ ksp = "2.3.3"
|
||||
lifecycleRuntimeKtx = "2.10.0"
|
||||
materialKolor = "4.0.5"
|
||||
navigation3 = "1.0.0"
|
||||
revenuecat = "9.15.1"
|
||||
revenuecat = "9.15.2"
|
||||
room = "2.8.4"
|
||||
vico = "2.3.6"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user