Merge branch 'dev'

This commit is contained in:
Nishant Mishra
2025-10-28 15:00:48 +05:30
53 changed files with 1421 additions and 247 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

BIN
.github/repo_photos/googleplay.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -19,13 +19,13 @@ jobs:
run: chmod +x gradlew run: chmod +x gradlew
- name: Build debug APK with Gradle - name: Build debug APK with Gradle
run: ./gradlew assembleDebug run: ./gradlew assembleFossDebug
- name: Run tests - name: Run tests
run: ./gradlew testDebugUnitTest run: ./gradlew testFossDebugUnitTest
- name: Upload debug APK artifact - name: Upload debug APK artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v5
with: with:
name: tomato-debug name: tomato-debug
path: ./app/build/outputs/apk/debug/app-debug.apk path: ./app/build/outputs/apk/debug/app-debug.apk

View File

@@ -7,9 +7,6 @@ Tomato is a minimalist Pomodoro timer for Android based on Material 3 Expressive
</div> </div>
> [!NOTE]
> Tomato is available on Google Play in closed testing. Please [use this link to join the testers Google Group](https://groups.google.com/g/nsh07-app-testers) and then install [Tomato from the Play Store with this link](https://play.google.com/store/apps/details?id=org.nsh07.pomodoro). Tomato needs 12 testers for at least 14 days for the app to be available on Google Play publicly.
<div align="center"> <div align="center">
<a href="https://hosted.weblate.org/engage/tomato/?utm_source=widget"> <a href="https://hosted.weblate.org/engage/tomato/?utm_source=widget">
@@ -24,15 +21,18 @@ Tomato is a minimalist Pomodoro timer for Android based on Material 3 Expressive
<a href="https://github.com/nsh07/tomato/blob/main/LICENSE"> <a href="https://github.com/nsh07/tomato/blob/main/LICENSE">
<img src="https://img.shields.io/github/license/nsh07/tomato?logo=gnu&color=blue&labelColor=1a1a1a"> <img src="https://img.shields.io/github/license/nsh07/tomato?logo=gnu&color=blue&labelColor=1a1a1a">
</a> </a>
<img src="https://img.shields.io/badge/API-26+-blue?logo=android&labelColor=1a1a1a"> <img src="https://img.shields.io/badge/API-27+-blue?logo=android&labelColor=1a1a1a">
<p> <p>
<a href="https://apt.izzysoft.de/fdroid/index/apk/org.nsh07.pomodoro"> <a href="https://play.google.com/store/apps/details?id=org.nsh07.pomodoro">
<img src="https://gitlab.com/IzzyOnDroid/repo/-/raw/master/assets/IzzyOnDroid.png" width="200"> <img src=".github/repo_photos/googleplay.png" width="200">
</a> </a>
<a href="https://f-droid.org/packages/org.nsh07.pomodoro"> <a href="https://f-droid.org/packages/org.nsh07.pomodoro">
<img src="https://f-droid.org/badge/get-it-on.png" width="200"> <img src="https://f-droid.org/badge/get-it-on.png" width="200">
</a> </a>
<a href="https://apt.izzysoft.de/fdroid/index/apk/org.nsh07.pomodoro">
<img src="https://gitlab.com/IzzyOnDroid/repo/-/raw/master/assets/IzzyOnDroid.png" width="200">
</a>
</p> </p>
<p> <p>
<a href="https://hosted.weblate.org/engage/tomato/"> <a href="https://hosted.weblate.org/engage/tomato/">
@@ -48,9 +48,11 @@ Tomato is a minimalist Pomodoro timer for Android based on Material 3 Expressive
<br/> <br/>
> *"... an app to support this habit helps me stay focused and get things done. Currently, that app is Tomato."* > *"... an app to support this habit helps me stay focused and get things done. Currently, that app
is Tomato."*
\- [*Android Authority*](https://www.androidauthority.com/best-new-android-apps-october-2025-3602966/) \- [*Android
Authority*](https://www.androidauthority.com/best-new-android-apps-october-2025-3602966/)
<br/> <br/>
@@ -92,23 +94,20 @@ translating this project into languages you know.
## Download ## Download
- **Google Play Store** (recommended): Tomato is available for closed testing on Google Play. Use - **Google Play Store** (recommended): Tomato will soon be available (currently in closed testing)
[this link to join the testers Google Group](https://groups.google.com/g/nsh07-app-testers), then on the Google Play Store.
[install Tomato from the Play Store with this link](https://play.google.com/store/apps/details?id=org.nsh07.pomodoro) and help Tomato to get an official Play Store release. [You can find it through this link](https://play.google.com/store/apps/details?id=org.nsh07.pomodoro).
- **F-Droid** (recommended): Tomato is available on the official F-Droid repository. Simply open - **F-Droid** (recommended): Tomato is available on the official F-Droid repository. Simply open
your preferred F-Droid app and search for Tomato. your preferred F-Droid app and search for Tomato. Updates on F-Droid are generally a week late. To
Updates on F-Droid are generally a week late. To get faster updates, you can install it through get faster updates, you can install it through
the [IzzyOnDroid repository](https://apt.izzysoft.de/fdroid/). the [IzzyOnDroid repository](https://apt.izzysoft.de/fdroid/).
- **Obtainium** (recommended): You can add this GitHub repository
on [Obtainium](https://obtainium.imranr.dev/) to get updates directly from GitHub releases. This
is the fastest way to install and update Tomato.
- **GitHub releases**: Alternatively, you can manually download and install APKs from - **GitHub releases**: Alternatively, you can manually download and install APKs from
the [Releases](https://github.com/nsh07/Tomato/releases/latest) section of this repo (This the [Releases](https://github.com/nsh07/Tomato/releases/latest) section of this repo (This method
method is not recommended, use Obtainium instead). is not recommended, use Google Play/F-Droid instead).
> [!TIP] > [!TIP]
> To [verify](https://developer.android.com/studio/command-line/apksigner#usage-verify) the APK > To [verify](https://developer.android.com/studio/command-line/apksigner#usage-verify) the APK
> downloaded from Obtainium/GitHub, use the following signing certificate fingerprints: > downloaded from GitHub, use the following signing certificate fingerprints:
> ``` > ```
> SHA1: B1:4E:17:93:11:E8:DB:D5:35:EF:8D:E9:FB:8F:FF:08:F8:EC:65:08 > SHA1: B1:4E:17:93:11:E8:DB:D5:35:EF:8D:E9:FB:8F:FF:08:F8:EC:65:08
> SHA256: 07:BE:F3:05:81:BA:EE:8F:45:EC:93:E4:7E:E6:8E:F2:08:74:E5:0E:F5:70:9C:78:B2:EE:67:AC:86:BE:4C:3D > SHA256: 07:BE:F3:05:81:BA:EE:8F:45:EC:93:E4:7E:E6:8E:F2:08:74:E5:0E:F5:70:9C:78:B2:EE:67:AC:86:BE:4C:3D

View File

@@ -43,8 +43,8 @@ android {
applicationId = "org.nsh07.pomodoro" applicationId = "org.nsh07.pomodoro"
minSdk = 27 minSdk = 27
targetSdk = 36 targetSdk = 36
versionCode = 15 versionCode = 17
versionName = "1.6.0" versionName = "1.6.2"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
@@ -58,6 +58,19 @@ android {
) )
} }
} }
flavorDimensions += "version"
productFlavors {
create("foss") {
dimension = "version"
isDefault = true
}
create("play") {
dimension = "version"
versionNameSuffix = "-play"
}
}
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_17 sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17
@@ -103,6 +116,9 @@ dependencies {
implementation(libs.androidx.room.ktx) implementation(libs.androidx.room.ktx)
ksp(libs.androidx.room.compiler) ksp(libs.androidx.room.compiler)
"playImplementation"(libs.revenuecat.purchases)
"playImplementation"(libs.revenuecat.purchases.ui)
testImplementation(libs.junit) testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.junit)

View File

@@ -0,0 +1,33 @@
/*
* 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.billing
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
/**
* Google Play implementation of BillingManager
*/
class FossBillingManager : BillingManager {
override val isPlus = MutableStateFlow(true).asStateFlow()
override val isLoaded = MutableStateFlow(true).asStateFlow()
}
object BillingManagerProvider {
val manager: BillingManager = FossBillingManager()
}

View File

@@ -0,0 +1,85 @@
/*
* 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.billing
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import org.nsh07.pomodoro.R
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
@Composable
fun TomatoPlusPaywallDialog(
isPlus: Boolean,
onDismiss: () -> Unit
) {
val uriHandler = LocalUriHandler.current
BackHandler(enabled = true, onDismiss)
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.fillMaxSize()
.background(colorScheme.surface)
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Icon(
painterResource(R.drawable.bmc),
null,
tint = colorScheme.onSurface
)
Spacer(Modifier.height(16.dp))
Text(
stringResource(R.string.tomato_foss),
style = typography.headlineSmall,
fontFamily = robotoFlexTopBar,
color = colorScheme.onSurface
)
Spacer(Modifier.height(8.dp))
Text(
stringResource(R.string.tomato_foss_desc, "BuyMeACoffee"),
textAlign = TextAlign.Center,
color = colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = 24.dp)
)
Spacer(Modifier.height(16.dp))
Button(onClick = { uriHandler.openUri("https://coff.ee/nsh07") }) {
Text("Buy Me A Coffee")
}
}
}
}

View File

@@ -0,0 +1,22 @@
/*
* 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.billing
import android.content.Context
fun initializePurchases(context: Context) {}

View File

@@ -18,6 +18,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

View File

@@ -30,12 +30,10 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.nsh07.pomodoro.ui.AppScreen import org.nsh07.pomodoro.ui.AppScreen
import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsViewModel import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsViewModel
import org.nsh07.pomodoro.ui.theme.TomatoTheme import org.nsh07.pomodoro.ui.theme.TomatoTheme
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerViewModel
import org.nsh07.pomodoro.utils.toColor import org.nsh07.pomodoro.utils.toColor
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
private val timerViewModel: TimerViewModel by viewModels(factoryProducer = { TimerViewModel.Factory })
private val settingsViewModel: SettingsViewModel by viewModels(factoryProducer = { SettingsViewModel.Factory }) private val settingsViewModel: SettingsViewModel by viewModels(factoryProducer = { SettingsViewModel.Factory })
private val appContainer by lazy { private val appContainer by lazy {
@@ -62,6 +60,20 @@ class MainActivity : ComponentActivity() {
val seed = preferencesState.colorScheme.toColor() val seed = preferencesState.colorScheme.toColor()
val isPlus by settingsViewModel.isPlus.collectAsStateWithLifecycle()
val isPurchaseStateLoaded by settingsViewModel.isPurchaseStateLoaded.collectAsStateWithLifecycle()
val isSettingsLoaded by settingsViewModel.isSettingsLoaded.collectAsStateWithLifecycle()
LaunchedEffect(isPurchaseStateLoaded, isPlus, isSettingsLoaded) {
if (isPurchaseStateLoaded && isSettingsLoaded) {
if (!isPlus) {
settingsViewModel.resetPaywalledSettings()
} else {
settingsViewModel.reloadSettings()
}
}
}
TomatoTheme( TomatoTheme(
darkTheme = darkTheme, darkTheme = darkTheme,
seedColor = seed, seedColor = seed,
@@ -73,7 +85,7 @@ class MainActivity : ComponentActivity() {
} }
AppScreen( AppScreen(
timerViewModel = timerViewModel, isPlus = isPlus,
isAODEnabled = preferencesState.aodEnabled, isAODEnabled = preferencesState.aodEnabled,
setTimerFrequency = { setTimerFrequency = {
appContainer.appTimerRepository.timerFrequency = it appContainer.appTimerRepository.timerFrequency = it

View File

@@ -1,8 +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/>.
*/
package org.nsh07.pomodoro package org.nsh07.pomodoro
import android.app.Application import android.app.Application
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import org.nsh07.pomodoro.billing.initializePurchases
import org.nsh07.pomodoro.data.AppContainer import org.nsh07.pomodoro.data.AppContainer
import org.nsh07.pomodoro.data.DefaultAppContainer import org.nsh07.pomodoro.data.DefaultAppContainer
@@ -12,6 +30,8 @@ class TomatoApplication : Application() {
super.onCreate() super.onCreate()
container = DefaultAppContainer(this) container = DefaultAppContainer(this)
initializePurchases(this)
val notificationChannel = NotificationChannel( val notificationChannel = NotificationChannel(
"timer", "timer",
getString(R.string.timer_progress), getString(R.string.timer_progress),

View File

@@ -0,0 +1,25 @@
/*
* 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.billing
import kotlinx.coroutines.flow.StateFlow
interface BillingManager {
val isPlus: StateFlow<Boolean>
val isLoaded: StateFlow<Boolean>
}

View File

@@ -17,6 +17,7 @@
package org.nsh07.pomodoro.data package org.nsh07.pomodoro.data
import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context import android.content.Context
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@@ -25,6 +26,8 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import org.nsh07.pomodoro.R import org.nsh07.pomodoro.R
import org.nsh07.pomodoro.billing.BillingManager
import org.nsh07.pomodoro.billing.BillingManagerProvider
import org.nsh07.pomodoro.service.addTimerActions import org.nsh07.pomodoro.service.addTimerActions
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState
import org.nsh07.pomodoro.utils.millisecondsToStr import org.nsh07.pomodoro.utils.millisecondsToStr
@@ -33,7 +36,9 @@ interface AppContainer {
val appPreferenceRepository: AppPreferenceRepository val appPreferenceRepository: AppPreferenceRepository
val appStatRepository: AppStatRepository val appStatRepository: AppStatRepository
val appTimerRepository: AppTimerRepository val appTimerRepository: AppTimerRepository
val billingManager: BillingManager
val notificationManager: NotificationManagerCompat val notificationManager: NotificationManagerCompat
val notificationManagerService: NotificationManager
val notificationBuilder: NotificationCompat.Builder val notificationBuilder: NotificationCompat.Builder
val timerState: MutableStateFlow<TimerState> val timerState: MutableStateFlow<TimerState>
val time: MutableStateFlow<Long> val time: MutableStateFlow<Long>
@@ -52,10 +57,15 @@ class DefaultAppContainer(context: Context) : AppContainer {
override val appTimerRepository: AppTimerRepository by lazy { AppTimerRepository() } override val appTimerRepository: AppTimerRepository by lazy { AppTimerRepository() }
override val billingManager: BillingManager by lazy { BillingManagerProvider.manager }
override val notificationManager: NotificationManagerCompat by lazy { override val notificationManager: NotificationManagerCompat by lazy {
NotificationManagerCompat.from(context) NotificationManagerCompat.from(context)
} }
override val notificationManagerService: NotificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
override val notificationBuilder: NotificationCompat.Builder by lazy { override val notificationBuilder: NotificationCompat.Builder by lazy {
NotificationCompat.Builder(context, "timer") NotificationCompat.Builder(context, "timer")
.setSmallIcon(R.drawable.tomato_logo_notification) .setSmallIcon(R.drawable.tomato_logo_notification)

View File

@@ -1,8 +1,18 @@
/* /*
* Copyright (c) 2025 Nishant Mishra * Copyright (c) 2025 Nishant Mishra
* *
* You should have received a copy of the GNU General Public License * This file is part of Tomato - a minimalist pomodoro timer for Android.
* along with this program. If not, see <https://www.gnu.org/licenses/>. *
* 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.data package org.nsh07.pomodoro.data
@@ -27,6 +37,7 @@ interface TimerRepository {
var alarmEnabled: Boolean var alarmEnabled: Boolean
var vibrateEnabled: Boolean var vibrateEnabled: Boolean
var dndEnabled: Boolean
var colorScheme: ColorScheme var colorScheme: ColorScheme
@@ -46,6 +57,7 @@ class AppTimerRepository : TimerRepository {
override var timerFrequency: Float = 10f override var timerFrequency: Float = 10f
override var alarmEnabled = true override var alarmEnabled = true
override var vibrateEnabled = true override var vibrateEnabled = true
override var dndEnabled: Boolean = false
override var colorScheme = lightColorScheme() override var colorScheme = lightColorScheme()
override var alarmSoundUri: Uri? = override var alarmSoundUri: Uri? =
Settings.System.DEFAULT_ALARM_ALERT_URI ?: Settings.System.DEFAULT_RINGTONE_URI Settings.System.DEFAULT_ALARM_ALERT_URI ?: Settings.System.DEFAULT_RINGTONE_URI

View File

@@ -18,6 +18,7 @@
package org.nsh07.pomodoro.service package org.nsh07.pomodoro.service
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.NotificationManager
import android.app.Service import android.app.Service
import android.content.Intent import android.content.Intent
import android.media.AudioAttributes import android.media.AudioAttributes
@@ -30,7 +31,6 @@ import android.os.Vibrator
import android.os.VibratorManager import android.os.VibratorManager
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@@ -53,7 +53,8 @@ class TimerService : Service() {
private val timerRepository by lazy { appContainer.appTimerRepository } private val timerRepository by lazy { appContainer.appTimerRepository }
private val statRepository by lazy { appContainer.appStatRepository } private val statRepository by lazy { appContainer.appStatRepository }
private val notificationManager by lazy { NotificationManagerCompat.from(this) } private val notificationManager by lazy { appContainer.notificationManager }
private val notificationManagerService by lazy { appContainer.notificationManagerService }
private val notificationBuilder by lazy { appContainer.notificationBuilder } private val notificationBuilder by lazy { appContainer.notificationBuilder }
private val _timerState by lazy { appContainer.timerState } private val _timerState by lazy { appContainer.timerState }
private val _time by lazy { appContainer.time } private val _time by lazy { appContainer.time }
@@ -106,6 +107,7 @@ class TimerService : Service() {
runBlocking { runBlocking {
job.cancel() job.cancel()
saveTimeToDb() saveTimeToDb()
setDoNotDisturb(false)
notificationManager.cancel(1) notificationManager.cancel(1)
alarm?.release() alarm?.release()
} }
@@ -127,7 +129,7 @@ class TimerService : Service() {
} }
} }
Actions.SKIP.toString() -> skipTimer(true) Actions.SKIP.toString() -> skipScope.launch { skipTimer(true) }
Actions.STOP_ALARM.toString() -> stopAlarm() Actions.STOP_ALARM.toString() -> stopAlarm()
@@ -140,6 +142,7 @@ class TimerService : Service() {
updateProgressSegments() updateProgressSegments()
if (timerState.value.timerRunning) { if (timerState.value.timerRunning) {
setDoNotDisturb(false)
notificationBuilder.clearActions().addTimerActions( notificationBuilder.clearActions().addTimerActions(
this, R.drawable.play, getString(R.string.start) this, R.drawable.play, getString(R.string.start)
) )
@@ -149,6 +152,8 @@ class TimerService : Service() {
} }
pauseTime = SystemClock.elapsedRealtime() pauseTime = SystemClock.elapsedRealtime()
} else { } else {
if (timerState.value.timerMode == TimerMode.FOCUS) setDoNotDisturb(true)
else setDoNotDisturb(false)
notificationBuilder.clearActions().addTimerActions( notificationBuilder.clearActions().addTimerActions(
this, R.drawable.pause, getString(R.string.stop) this, R.drawable.pause, getString(R.string.stop)
) )
@@ -324,48 +329,48 @@ class TimerService : Service() {
} }
} }
private fun skipTimer(fromButton: Boolean = false) { private suspend fun skipTimer(fromButton: Boolean = false) {
updateProgressSegments() updateProgressSegments()
skipScope.launch { saveTimeToDb()
saveTimeToDb() updateProgressSegments()
updateProgressSegments() showTimerNotification(0, paused = true, complete = !fromButton)
showTimerNotification(0, paused = true, complete = !fromButton) startTime = 0L
startTime = 0L pauseTime = 0L
pauseTime = 0L pauseDuration = 0L
pauseDuration = 0L
cycles = (cycles + 1) % (timerRepository.sessionLength * 2) cycles = (cycles + 1) % (timerRepository.sessionLength * 2)
if (cycles % 2 == 0) { if (cycles % 2 == 0) {
time = timerRepository.focusTime if (timerState.value.timerRunning) setDoNotDisturb(true)
_timerState.update { currentState -> time = timerRepository.focusTime
currentState.copy( _timerState.update { currentState ->
timerMode = TimerMode.FOCUS, currentState.copy(
timeStr = millisecondsToStr(time), timerMode = TimerMode.FOCUS,
totalTime = time, timeStr = millisecondsToStr(time),
nextTimerMode = if (cycles == (timerRepository.sessionLength - 1) * 2) TimerMode.LONG_BREAK else TimerMode.SHORT_BREAK, totalTime = time,
nextTimeStr = if (cycles == (timerRepository.sessionLength - 1) * 2) millisecondsToStr( nextTimerMode = if (cycles == (timerRepository.sessionLength - 1) * 2) TimerMode.LONG_BREAK else TimerMode.SHORT_BREAK,
timerRepository.longBreakTime nextTimeStr = if (cycles == (timerRepository.sessionLength - 1) * 2) millisecondsToStr(
) else millisecondsToStr( timerRepository.longBreakTime
timerRepository.shortBreakTime ) else millisecondsToStr(
), timerRepository.shortBreakTime
currentFocusCount = cycles / 2 + 1, ),
totalFocusCount = timerRepository.sessionLength currentFocusCount = cycles / 2 + 1,
) totalFocusCount = timerRepository.sessionLength
} )
} else { }
val long = cycles == (timerRepository.sessionLength * 2) - 1 } else {
time = if (long) timerRepository.longBreakTime else timerRepository.shortBreakTime if (timerState.value.timerRunning) setDoNotDisturb(false)
val long = cycles == (timerRepository.sessionLength * 2) - 1
time = if (long) timerRepository.longBreakTime else timerRepository.shortBreakTime
_timerState.update { currentState -> _timerState.update { currentState ->
currentState.copy( currentState.copy(
timerMode = if (long) TimerMode.LONG_BREAK else TimerMode.SHORT_BREAK, timerMode = if (long) TimerMode.LONG_BREAK else TimerMode.SHORT_BREAK,
timeStr = millisecondsToStr(time), timeStr = millisecondsToStr(time),
totalTime = time, totalTime = time,
nextTimerMode = TimerMode.FOCUS, nextTimerMode = TimerMode.FOCUS,
nextTimeStr = millisecondsToStr(timerRepository.focusTime) nextTimeStr = millisecondsToStr(timerRepository.focusTime)
) )
}
} }
} }
} }
@@ -441,7 +446,15 @@ class TimerService : Service() {
} }
} }
fun updateAlarmTone() { private fun setDoNotDisturb(doNotDisturb: Boolean) {
if (timerRepository.dndEnabled && notificationManagerService.isNotificationPolicyAccessGranted()) {
if (doNotDisturb) {
notificationManagerService.setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_ALARMS)
} else notificationManagerService.setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_ALL)
}
}
private fun updateAlarmTone() {
alarm?.release() alarm?.release()
alarm = initializeMediaPlayer() alarm = initializeMediaPlayer()
} }

View File

@@ -23,6 +23,8 @@ import androidx.compose.animation.Crossfade
import androidx.compose.animation.SharedTransitionLayout import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.togetherWith import androidx.compose.animation.togetherWith
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateEndPadding
@@ -41,8 +43,10 @@ import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier 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
@@ -54,6 +58,7 @@ import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberNavBackStack import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.ui.NavDisplay import androidx.navigation3.ui.NavDisplay
import androidx.window.core.layout.WindowSizeClass import androidx.window.core.layout.WindowSizeClass
import org.nsh07.pomodoro.billing.TomatoPlusPaywallDialog
import org.nsh07.pomodoro.service.TimerService import org.nsh07.pomodoro.service.TimerService
import org.nsh07.pomodoro.ui.settingsScreen.SettingsScreenRoot import org.nsh07.pomodoro.ui.settingsScreen.SettingsScreenRoot
import org.nsh07.pomodoro.ui.statsScreen.StatsScreenRoot import org.nsh07.pomodoro.ui.statsScreen.StatsScreenRoot
@@ -65,10 +70,11 @@ import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerViewModel
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable @Composable
fun AppScreen( fun AppScreen(
modifier: Modifier = Modifier,
timerViewModel: TimerViewModel = viewModel(factory = TimerViewModel.Factory),
isAODEnabled: Boolean, isAODEnabled: Boolean,
setTimerFrequency: (Float) -> Unit isPlus: Boolean,
setTimerFrequency: (Float) -> Unit,
modifier: Modifier = Modifier,
timerViewModel: TimerViewModel = viewModel(factory = TimerViewModel.Factory)
) { ) {
val context = LocalContext.current val context = LocalContext.current
@@ -91,6 +97,7 @@ fun AppScreen(
} }
} }
var showPaywall by remember { mutableStateOf(false) }
Scaffold( Scaffold(
bottomBar = { bottomBar = {
@@ -157,6 +164,7 @@ fun AppScreen(
entry<Screen.Timer> { entry<Screen.Timer> {
TimerScreen( TimerScreen(
timerState = uiState, timerState = uiState,
isPlus = isPlus,
progress = { progress }, progress = { progress },
onAction = { action -> onAction = { action ->
when (action) { when (action) {
@@ -218,6 +226,7 @@ fun AppScreen(
entry<Screen.Settings.Main> { entry<Screen.Settings.Main> {
SettingsScreenRoot( SettingsScreenRoot(
setShowPaywall = { showPaywall = it },
modifier = modifier.padding( modifier = modifier.padding(
start = contentPadding.calculateStartPadding(layoutDirection), start = contentPadding.calculateStartPadding(layoutDirection),
end = contentPadding.calculateEndPadding(layoutDirection), end = contentPadding.calculateEndPadding(layoutDirection),
@@ -240,4 +249,12 @@ fun AppScreen(
) )
} }
} }
AnimatedVisibility(
showPaywall,
enter = slideInVertically { it },
exit = slideOutVertically { it }
) {
TomatoPlusPaywallDialog(isPlus = isPlus) { showPaywall = false }
}
} }

View File

@@ -17,6 +17,7 @@
package org.nsh07.pomodoro.ui.settingsScreen package org.nsh07.pomodoro.ui.settingsScreen
import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
@@ -67,6 +68,7 @@ import org.nsh07.pomodoro.service.TimerService
import org.nsh07.pomodoro.ui.Screen import org.nsh07.pomodoro.ui.Screen
import org.nsh07.pomodoro.ui.settingsScreen.components.AboutCard import org.nsh07.pomodoro.ui.settingsScreen.components.AboutCard
import org.nsh07.pomodoro.ui.settingsScreen.components.ClickableListItem import org.nsh07.pomodoro.ui.settingsScreen.components.ClickableListItem
import org.nsh07.pomodoro.ui.settingsScreen.components.PlusPromo
import org.nsh07.pomodoro.ui.settingsScreen.screens.AlarmSettings import org.nsh07.pomodoro.ui.settingsScreen.screens.AlarmSettings
import org.nsh07.pomodoro.ui.settingsScreen.screens.AppearanceSettings import org.nsh07.pomodoro.ui.settingsScreen.screens.AppearanceSettings
import org.nsh07.pomodoro.ui.settingsScreen.screens.TimerSettings import org.nsh07.pomodoro.ui.settingsScreen.screens.TimerSettings
@@ -80,6 +82,7 @@ import org.nsh07.pomodoro.ui.theme.CustomColors.topBarColors
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun SettingsScreenRoot( fun SettingsScreenRoot(
setShowPaywall: (Boolean) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: SettingsViewModel = viewModel(factory = SettingsViewModel.Factory) viewModel: SettingsViewModel = viewModel(factory = SettingsViewModel.Factory)
) { ) {
@@ -102,8 +105,10 @@ fun SettingsScreenRoot(
viewModel.longBreakTimeTextFieldState viewModel.longBreakTimeTextFieldState
} }
val isPlus by viewModel.isPlus.collectAsStateWithLifecycle()
val alarmEnabled by viewModel.alarmEnabled.collectAsStateWithLifecycle(true) val alarmEnabled by viewModel.alarmEnabled.collectAsStateWithLifecycle(true)
val vibrateEnabled by viewModel.vibrateEnabled.collectAsStateWithLifecycle(true) val vibrateEnabled by viewModel.vibrateEnabled.collectAsStateWithLifecycle(true)
val dndEnabled by viewModel.dndEnabled.collectAsStateWithLifecycle(false)
val alarmSound by viewModel.alarmSound.collectAsStateWithLifecycle(viewModel.currentAlarmSound) val alarmSound by viewModel.alarmSound.collectAsStateWithLifecycle(viewModel.currentAlarmSound)
val preferencesState by viewModel.preferencesState.collectAsStateWithLifecycle() val preferencesState by viewModel.preferencesState.collectAsStateWithLifecycle()
@@ -118,6 +123,7 @@ fun SettingsScreenRoot(
} }
SettingsScreen( SettingsScreen(
isPlus = isPlus,
preferencesState = preferencesState, preferencesState = preferencesState,
backStack = backStack, backStack = backStack,
focusTimeInputFieldState = focusTimeInputFieldState, focusTimeInputFieldState = focusTimeInputFieldState,
@@ -126,11 +132,13 @@ fun SettingsScreenRoot(
sessionsSliderState = sessionsSliderState, sessionsSliderState = sessionsSliderState,
alarmEnabled = alarmEnabled, alarmEnabled = alarmEnabled,
vibrateEnabled = vibrateEnabled, vibrateEnabled = vibrateEnabled,
dndEnabled = dndEnabled,
alarmSound = alarmSound, alarmSound = alarmSound,
onAlarmEnabledChange = viewModel::saveAlarmEnabled, onAlarmEnabledChange = viewModel::saveAlarmEnabled,
onVibrateEnabledChange = viewModel::saveVibrateEnabled, onVibrateEnabledChange = viewModel::saveVibrateEnabled,
onBlackThemeChange = viewModel::saveBlackTheme, onBlackThemeChange = viewModel::saveBlackTheme,
onAodEnabledChange = viewModel::saveAodEnabled, onAodEnabledChange = viewModel::saveAodEnabled,
onDndEnabledChange = viewModel::saveDndEnabled,
onAlarmSoundChanged = { onAlarmSoundChanged = {
viewModel.saveAlarmSound(it) viewModel.saveAlarmSound(it)
Intent(context, TimerService::class.java).apply { Intent(context, TimerService::class.java).apply {
@@ -140,13 +148,16 @@ fun SettingsScreenRoot(
}, },
onThemeChange = viewModel::saveTheme, onThemeChange = viewModel::saveTheme,
onColorSchemeChange = viewModel::saveColorScheme, onColorSchemeChange = viewModel::saveColorScheme,
setShowPaywall = setShowPaywall,
modifier = modifier modifier = modifier
) )
} }
@SuppressLint("LocalContextGetResourceValueCall")
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable @Composable
private fun SettingsScreen( private fun SettingsScreen(
isPlus: Boolean,
preferencesState: PreferencesState, preferencesState: PreferencesState,
backStack: SnapshotStateList<Screen.Settings>, backStack: SnapshotStateList<Screen.Settings>,
focusTimeInputFieldState: TextFieldState, focusTimeInputFieldState: TextFieldState,
@@ -155,14 +166,17 @@ private fun SettingsScreen(
sessionsSliderState: SliderState, sessionsSliderState: SliderState,
alarmEnabled: Boolean, alarmEnabled: Boolean,
vibrateEnabled: Boolean, vibrateEnabled: Boolean,
dndEnabled: Boolean,
alarmSound: String, alarmSound: String,
onAlarmEnabledChange: (Boolean) -> Unit, onAlarmEnabledChange: (Boolean) -> Unit,
onVibrateEnabledChange: (Boolean) -> Unit, onVibrateEnabledChange: (Boolean) -> Unit,
onBlackThemeChange: (Boolean) -> Unit, onBlackThemeChange: (Boolean) -> Unit,
onAodEnabledChange: (Boolean) -> Unit, onAodEnabledChange: (Boolean) -> Unit,
onDndEnabledChange: (Boolean) -> Unit,
onAlarmSoundChanged: (Uri?) -> Unit, onAlarmSoundChanged: (Uri?) -> Unit,
onThemeChange: (String) -> Unit, onThemeChange: (String) -> Unit,
onColorSchemeChange: (Color) -> Unit, onColorSchemeChange: (Color) -> Unit,
setShowPaywall: (Boolean) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val context = LocalContext.current val context = LocalContext.current
@@ -212,10 +226,20 @@ private fun SettingsScreen(
) { ) {
item { Spacer(Modifier.height(12.dp)) } item { Spacer(Modifier.height(12.dp)) }
item { AboutCard() } if (!isPlus) item {
PlusPromo(isPlus, setShowPaywall)
Spacer(Modifier.height(14.dp))
}
item { AboutCard(isPlus) }
item { Spacer(Modifier.height(12.dp)) } item { Spacer(Modifier.height(12.dp)) }
if (isPlus) item {
PlusPromo(isPlus, setShowPaywall)
Spacer(Modifier.height(14.dp))
}
itemsIndexed(settingsScreens) { index, item -> itemsIndexed(settingsScreens) { index, item ->
ClickableListItem( ClickableListItem(
leadingContent = { leadingContent = {
@@ -261,21 +285,27 @@ private fun SettingsScreen(
entry<Screen.Settings.Appearance> { entry<Screen.Settings.Appearance> {
AppearanceSettings( AppearanceSettings(
preferencesState = preferencesState, preferencesState = preferencesState,
isPlus = isPlus,
onBlackThemeChange = onBlackThemeChange, onBlackThemeChange = onBlackThemeChange,
onThemeChange = onThemeChange, onThemeChange = onThemeChange,
onColorSchemeChange = onColorSchemeChange, onColorSchemeChange = onColorSchemeChange,
setShowPaywall = setShowPaywall,
onBack = backStack::removeLastOrNull onBack = backStack::removeLastOrNull
) )
} }
entry<Screen.Settings.Timer> { entry<Screen.Settings.Timer> {
TimerSettings( TimerSettings(
isPlus = isPlus,
aodEnabled = preferencesState.aodEnabled, aodEnabled = preferencesState.aodEnabled,
dndEnabled = dndEnabled,
focusTimeInputFieldState = focusTimeInputFieldState, focusTimeInputFieldState = focusTimeInputFieldState,
shortBreakTimeInputFieldState = shortBreakTimeInputFieldState, shortBreakTimeInputFieldState = shortBreakTimeInputFieldState,
longBreakTimeInputFieldState = longBreakTimeInputFieldState, longBreakTimeInputFieldState = longBreakTimeInputFieldState,
sessionsSliderState = sessionsSliderState, sessionsSliderState = sessionsSliderState,
onAodEnabledChange = onAodEnabledChange, onAodEnabledChange = onAodEnabledChange,
onBack = backStack::removeLastOrNull onDndEnabledChange = onDndEnabledChange,
setShowPaywall = setShowPaywall,
onBack = backStack::removeLastOrNull,
) )
} }
} }

View File

@@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.material3.Button import androidx.compose.material3.Button
@@ -50,7 +51,10 @@ import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
// Taken from https://github.com/shub39/Grit/blob/master/app/src/main/java/com/shub39/grit/core/presentation/settings/ui/component/AboutApp.kt // Taken from https://github.com/shub39/Grit/blob/master/app/src/main/java/com/shub39/grit/core/presentation/settings/ui/component/AboutApp.kt
@Composable @Composable
fun AboutCard(modifier: Modifier = Modifier) { fun AboutCard(
isPlus: Boolean,
modifier: Modifier = Modifier
) {
val uriHandler = LocalUriHandler.current val uriHandler = LocalUriHandler.current
val context = LocalContext.current val context = LocalContext.current
@@ -76,7 +80,8 @@ fun AboutCard(modifier: Modifier = Modifier) {
) { ) {
Column { Column {
Text( Text(
text = stringResource(R.string.app_name), if (!isPlus) stringResource(R.string.app_name)
else stringResource(R.string.app_name_plus),
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
fontFamily = robotoFlexTopBar fontFamily = robotoFlexTopBar
) )
@@ -123,8 +128,9 @@ fun AboutCard(modifier: Modifier = Modifier) {
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Icon( Icon(
painterResource(R.drawable.coffee), painterResource(R.drawable.bmc),
contentDescription = "Buy me a coffee", contentDescription = "Buy me a coffee",
modifier = Modifier.height(24.dp)
) )
Text(text = "Buy me a coffee") Text(text = "Buy me a coffee")

View File

@@ -17,9 +17,9 @@
package org.nsh07.pomodoro.ui.settingsScreen.components package org.nsh07.pomodoro.ui.settingsScreen.components
import android.os.Build
import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
@@ -28,6 +28,8 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@@ -38,6 +40,7 @@ import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
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.graphics.Color import androidx.compose.ui.graphics.Color
@@ -56,6 +59,7 @@ fun ColorSchemePickerListItem(
color: Color, color: Color,
items: Int, items: Int,
index: Int, index: Int,
isPlus: Boolean,
onColorChange: (Color) -> Unit, onColorChange: (Color) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
@@ -65,6 +69,7 @@ fun ColorSchemePickerListItem(
Color(0xff9fd75c), Color(0xffc1d02d), Color(0xfffabd00), Color(0xffffb86e), Color(0xff9fd75c), Color(0xffc1d02d), Color(0xfffabd00), Color(0xffffb86e),
Color.White Color.White
) )
val zeroCorner = remember { CornerSize(0) }
Column( Column(
modifier modifier
@@ -76,45 +81,49 @@ fun ColorSchemePickerListItem(
} }
) )
) { ) {
ListItem( if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
leadingContent = { ListItem(
Icon( leadingContent = {
painterResource(R.drawable.colors), Icon(
null painterResource(R.drawable.colors),
) null
}, )
headlineContent = { Text("Dynamic color") }, },
supportingContent = { Text("Adapt theme colors from your wallpaper") }, headlineContent = { Text(stringResource(R.string.dynamic_color)) },
trailingContent = { supportingContent = { Text(stringResource(R.string.dynamic_color_desc)) },
val checked = color == colorSchemes.last() trailingContent = {
Switch( val checked = color == colorSchemes.last()
checked = checked, Switch(
onCheckedChange = { checked = checked,
if (it) onColorChange(colorSchemes.last()) onCheckedChange = {
else onColorChange(colorSchemes.first()) if (it) onColorChange(colorSchemes.last())
}, else onColorChange(colorSchemes.first())
thumbContent = { },
if (checked) { enabled = isPlus,
Icon( thumbContent = {
painter = painterResource(R.drawable.check), if (checked) {
contentDescription = null, Icon(
modifier = Modifier.size(SwitchDefaults.IconSize), painter = painterResource(R.drawable.check),
) contentDescription = null,
} else { modifier = Modifier.size(SwitchDefaults.IconSize),
Icon( )
painter = painterResource(R.drawable.clear), } else {
contentDescription = null, Icon(
modifier = Modifier.size(SwitchDefaults.IconSize), painter = painterResource(R.drawable.clear),
) contentDescription = null,
} modifier = Modifier.size(SwitchDefaults.IconSize),
}, )
colors = switchColors }
) },
}, colors = switchColors
colors = listItemColors, )
modifier = Modifier.clip(middleListItemShape) },
) colors = listItemColors,
Spacer(Modifier.height(2.dp)) modifier = Modifier.clip(middleListItemShape)
)
Spacer(Modifier.height(2.dp))
}
ListItem( ListItem(
leadingContent = { leadingContent = {
Icon( Icon(
@@ -131,24 +140,31 @@ fun ColorSchemePickerListItem(
) )
}, },
colors = listItemColors, colors = listItemColors,
modifier = Modifier.clip(middleListItemShape) modifier = Modifier.clip(
RoundedCornerShape(
topStart = middleListItemShape.topStart,
topEnd = middleListItemShape.topEnd,
zeroCorner,
zeroCorner
)
)
) )
Column( LazyRow(
verticalArrangement = Arrangement.spacedBy(8.dp), contentPadding = PaddingValues(horizontal = 48.dp),
userScrollEnabled = isPlus,
modifier = Modifier modifier = Modifier
.background(listItemColors.containerColor) .background(listItemColors.containerColor)
.padding(bottom = 8.dp) .padding(bottom = 8.dp)
) { ) {
LazyRow(contentPadding = PaddingValues(horizontal = 48.dp)) { items(colorSchemes.dropLast(1)) {
items(colorSchemes.dropLast(1)) { ColorPickerButton(
ColorPickerButton( color = it,
it, isSelected = it == color,
it == color, enabled = isPlus,
modifier = Modifier.padding(4.dp) modifier = Modifier.padding(4.dp)
) { ) {
onColorChange(it) onColorChange(it)
}
} }
} }
} }
@@ -160,12 +176,17 @@ fun ColorSchemePickerListItem(
fun ColorPickerButton( fun ColorPickerButton(
color: Color, color: Color,
isSelected: Boolean, isSelected: Boolean,
enabled: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onClick: () -> Unit onClick: () -> Unit
) { ) {
IconButton( IconButton(
shapes = IconButtonDefaults.shapes(), shapes = IconButtonDefaults.shapes(),
colors = IconButtonDefaults.iconButtonColors(containerColor = color), colors = IconButtonDefaults.iconButtonColors(
containerColor = color,
disabledContainerColor = color.copy(0.3f)
),
enabled = enabled,
modifier = modifier.size(48.dp), modifier = modifier.size(48.dp),
onClick = onClick onClick = onClick
) { ) {

View File

@@ -0,0 +1,51 @@
/*
* 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.ui.settingsScreen.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Button
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
@Composable
fun PlusDivider(
setShowPaywall: (Boolean) -> Unit,
modifier: Modifier = Modifier
) {
Box(contentAlignment = Alignment.Center, modifier = modifier.padding(vertical = 14.dp)) {
HorizontalDivider(modifier = Modifier.clip(CircleShape), thickness = 4.dp)
Button(
onClick = { setShowPaywall(true) },
modifier = Modifier
.background(colorScheme.surfaceContainer)
.padding(horizontal = 8.dp)
) {
Text("Customize further with Tomato+", style = typography.titleSmall)
}
}
}

View File

@@ -0,0 +1,82 @@
/*
* 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.ui.settingsScreen.components
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import org.nsh07.pomodoro.R
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
@Composable
fun PlusPromo(
isPlus: Boolean,
setShowPaywall: (Boolean) -> Unit,
modifier: Modifier = Modifier
) {
val container = if (isPlus) colorScheme.surfaceBright else colorScheme.primary
val onContainer = if (isPlus) colorScheme.onSurface else colorScheme.onPrimary
val onContainerVariant = if (isPlus) colorScheme.onSurfaceVariant else colorScheme.onPrimary
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = modifier
.clip(CircleShape)
.background(container)
.padding(16.dp)
.clickable { setShowPaywall(true) }
) {
Icon(
painterResource(R.drawable.tomato_logo_notification),
null,
tint = onContainerVariant,
modifier = Modifier
.size(24.dp)
)
Spacer(Modifier.width(8.dp))
Text(
if (!isPlus) stringResource(R.string.get_plus)
else stringResource(R.string.app_name_plus),
style = typography.titleLarge,
fontFamily = robotoFlexTopBar,
color = onContainer
)
Spacer(Modifier.weight(1f))
Icon(
painterResource(R.drawable.arrow_forward_big),
null,
tint = onContainerVariant
)
}
}

View File

@@ -43,6 +43,7 @@ import androidx.compose.ui.unit.dp
import org.nsh07.pomodoro.R import org.nsh07.pomodoro.R
import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.bottomListItemShape import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.bottomListItemShape
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.cardShape
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.middleListItemShape import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.middleListItemShape
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape
@@ -69,11 +70,13 @@ fun ThemePickerListItem(
Column( Column(
modifier modifier
.clip( .clip(
when (index) { if (items > 1)
0 -> topListItemShape when (index) {
items - 1 -> bottomListItemShape 0 -> topListItemShape
else -> middleListItemShape items - 1 -> bottomListItemShape
}, else -> middleListItemShape
}
else cardShape,
), ),
) { ) {
ListItem( ListItem(

View File

@@ -48,6 +48,7 @@ import androidx.compose.ui.unit.dp
import org.nsh07.pomodoro.R import org.nsh07.pomodoro.R
import org.nsh07.pomodoro.ui.settingsScreen.SettingsSwitchItem import org.nsh07.pomodoro.ui.settingsScreen.SettingsSwitchItem
import org.nsh07.pomodoro.ui.settingsScreen.components.ColorSchemePickerListItem import org.nsh07.pomodoro.ui.settingsScreen.components.ColorSchemePickerListItem
import org.nsh07.pomodoro.ui.settingsScreen.components.PlusDivider
import org.nsh07.pomodoro.ui.settingsScreen.components.ThemePickerListItem import org.nsh07.pomodoro.ui.settingsScreen.components.ThemePickerListItem
import org.nsh07.pomodoro.ui.settingsScreen.viewModel.PreferencesState import org.nsh07.pomodoro.ui.settingsScreen.viewModel.PreferencesState
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
@@ -55,16 +56,18 @@ import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
import org.nsh07.pomodoro.ui.theme.CustomColors.switchColors import org.nsh07.pomodoro.ui.theme.CustomColors.switchColors
import org.nsh07.pomodoro.ui.theme.CustomColors.topBarColors import org.nsh07.pomodoro.ui.theme.CustomColors.topBarColors
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.bottomListItemShape import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.bottomListItemShape
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.middleListItemShape import org.nsh07.pomodoro.ui.theme.TomatoTheme
import org.nsh07.pomodoro.utils.toColor import org.nsh07.pomodoro.utils.toColor
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable @Composable
fun AppearanceSettings( fun AppearanceSettings(
preferencesState: PreferencesState, preferencesState: PreferencesState,
isPlus: Boolean,
onBlackThemeChange: (Boolean) -> Unit, onBlackThemeChange: (Boolean) -> Unit,
onThemeChange: (String) -> Unit, onThemeChange: (String) -> Unit,
onColorSchemeChange: (Color) -> Unit, onColorSchemeChange: (Color) -> Unit,
setShowPaywall: (Boolean) -> Unit,
onBack: () -> Unit, onBack: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
@@ -100,22 +103,26 @@ fun AppearanceSettings(
item { item {
Spacer(Modifier.height(14.dp)) Spacer(Modifier.height(14.dp))
} }
item {
ColorSchemePickerListItem(
color = preferencesState.colorScheme.toColor(),
items = 3,
index = 0,
onColorChange = onColorSchemeChange
)
}
item { item {
ThemePickerListItem( ThemePickerListItem(
theme = preferencesState.theme, theme = preferencesState.theme,
onThemeChange = onThemeChange, onThemeChange = onThemeChange,
items = if (isPlus) 3 else 1,
index = 0
)
}
if (!isPlus) {
item { PlusDivider(setShowPaywall) }
}
item {
ColorSchemePickerListItem(
color = preferencesState.colorScheme.toColor(),
items = 3, items = 3,
index = 1, index = if (isPlus) 1 else 0,
modifier = Modifier isPlus = isPlus,
.clip(middleListItemShape) onColorChange = onColorSchemeChange,
) )
} }
item { item {
@@ -136,6 +143,7 @@ fun AppearanceSettings(
Switch( Switch(
checked = item.checked, checked = item.checked,
onCheckedChange = { item.onClick(it) }, onCheckedChange = { item.onClick(it) },
enabled = isPlus,
thumbContent = { thumbContent = {
if (item.checked) { if (item.checked) {
Icon( Icon(
@@ -168,11 +176,15 @@ fun AppearanceSettings(
@Composable @Composable
fun AppearanceSettingsPreview() { fun AppearanceSettingsPreview() {
val preferencesState = PreferencesState() val preferencesState = PreferencesState()
AppearanceSettings( TomatoTheme(dynamicColor = false) {
preferencesState = preferencesState, AppearanceSettings(
onBlackThemeChange = {}, preferencesState = preferencesState,
onThemeChange = {}, isPlus = false,
onColorSchemeChange = {}, onBlackThemeChange = {},
onBack = {} onThemeChange = {},
) onColorSchemeChange = {},
setShowPaywall = {},
onBack = {}
)
}
} }

View File

@@ -17,6 +17,11 @@
package org.nsh07.pomodoro.ui.settingsScreen.screens package org.nsh07.pomodoro.ui.settingsScreen.screens
import android.app.NotificationManager
import android.content.Context
import android.content.Intent
import android.provider.Settings
import android.widget.Toast
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.horizontalScroll
@@ -31,6 +36,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.TextFieldState
@@ -51,6 +57,7 @@ import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -59,6 +66,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.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
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.res.stringResource
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
@@ -67,6 +75,7 @@ import androidx.compose.ui.unit.dp
import org.nsh07.pomodoro.R import org.nsh07.pomodoro.R
import org.nsh07.pomodoro.ui.settingsScreen.SettingsSwitchItem import org.nsh07.pomodoro.ui.settingsScreen.SettingsSwitchItem
import org.nsh07.pomodoro.ui.settingsScreen.components.MinuteInputField import org.nsh07.pomodoro.ui.settingsScreen.components.MinuteInputField
import org.nsh07.pomodoro.ui.settingsScreen.components.PlusDivider
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
import org.nsh07.pomodoro.ui.theme.CustomColors.switchColors import org.nsh07.pomodoro.ui.theme.CustomColors.switchColors
@@ -76,19 +85,60 @@ import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.cardShape
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.middleListItemShape import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.middleListItemShape
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable @Composable
fun TimerSettings( fun TimerSettings(
isPlus: Boolean,
aodEnabled: Boolean, aodEnabled: Boolean,
dndEnabled: Boolean,
focusTimeInputFieldState: TextFieldState, focusTimeInputFieldState: TextFieldState,
shortBreakTimeInputFieldState: TextFieldState, shortBreakTimeInputFieldState: TextFieldState,
longBreakTimeInputFieldState: TextFieldState, longBreakTimeInputFieldState: TextFieldState,
sessionsSliderState: SliderState, sessionsSliderState: SliderState,
onAodEnabledChange: (Boolean) -> Unit, onAodEnabledChange: (Boolean) -> Unit,
onDndEnabledChange: (Boolean) -> Unit,
onBack: () -> Unit, onBack: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier,
setShowPaywall: (Boolean) -> Unit
) { ) {
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
val context = LocalContext.current
val appName = stringResource(R.string.app_name)
val notificationManagerService =
remember { context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager }
LaunchedEffect(Unit) {
if (!notificationManagerService.isNotificationPolicyAccessGranted())
onDndEnabledChange(false)
}
val switchItems = listOf(
SettingsSwitchItem(
checked = dndEnabled,
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)
}
onDndEnabledChange(it)
}
),
SettingsSwitchItem(
checked = aodEnabled,
icon = R.drawable.aod,
label = R.string.always_on_display,
description = R.string.always_on_display_desc,
onClick = onAodEnabledChange
)
)
Column(modifier.nestedScroll(scrollBehavior.nestedScrollConnection)) { Column(modifier.nestedScroll(scrollBehavior.nestedScrollConnection)) {
LargeFlexibleTopAppBar( LargeFlexibleTopAppBar(
@@ -213,14 +263,8 @@ fun TimerSettings(
) )
} }
item { Spacer(Modifier.height(12.dp)) } item { Spacer(Modifier.height(12.dp)) }
item {
val item = SettingsSwitchItem( itemsIndexed(if (isPlus) switchItems else switchItems.take(1)) { index, item ->
checked = aodEnabled,
icon = R.drawable.aod,
label = R.string.always_on_display,
description = R.string.always_on_display_desc,
onClick = onAodEnabledChange
)
ListItem( ListItem(
leadingContent = { leadingContent = {
Icon( Icon(
@@ -254,10 +298,61 @@ fun TimerSettings(
) )
}, },
colors = listItemColors, colors = listItemColors,
modifier = Modifier.clip(cardShape) modifier = Modifier.clip(
if (isPlus) when (index) {
0 -> topListItemShape
switchItems.size - 1 -> bottomListItemShape
else -> middleListItemShape
}
else cardShape
)
) )
} }
if (!isPlus) {
item {
PlusDivider(setShowPaywall)
}
itemsIndexed(switchItems.drop(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 = isPlus,
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(cardShape)
)
}
}
item { item {
var expanded by remember { mutableStateOf(false) } var expanded by remember { mutableStateOf(false) }
Column( Column(
@@ -306,12 +401,16 @@ private fun TimerSettingsPreview() {
steps = 6 steps = 6
) )
TimerSettings( TimerSettings(
isPlus = false,
aodEnabled = true,
dndEnabled = false,
focusTimeInputFieldState = focusTimeInputFieldState, focusTimeInputFieldState = focusTimeInputFieldState,
shortBreakTimeInputFieldState = shortBreakTimeInputFieldState, shortBreakTimeInputFieldState = shortBreakTimeInputFieldState,
longBreakTimeInputFieldState = longBreakTimeInputFieldState, longBreakTimeInputFieldState = longBreakTimeInputFieldState,
sessionsSliderState = sessionsSliderState, sessionsSliderState = sessionsSliderState,
aodEnabled = true, onAodEnabledChange = {},
onBack = {}, onDndEnabledChange = {},
onAodEnabledChange = {} setShowPaywall = {},
onBack = {}
) )
} }

View File

@@ -40,17 +40,25 @@ import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.nsh07.pomodoro.TomatoApplication import org.nsh07.pomodoro.TomatoApplication
import org.nsh07.pomodoro.billing.BillingManager
import org.nsh07.pomodoro.data.AppPreferenceRepository import org.nsh07.pomodoro.data.AppPreferenceRepository
import org.nsh07.pomodoro.data.TimerRepository import org.nsh07.pomodoro.data.TimerRepository
import org.nsh07.pomodoro.ui.Screen import org.nsh07.pomodoro.ui.Screen
@OptIn(FlowPreview::class, ExperimentalMaterial3Api::class) @OptIn(FlowPreview::class, ExperimentalMaterial3Api::class)
class SettingsViewModel( class SettingsViewModel(
private val billingManager: BillingManager,
private val preferenceRepository: AppPreferenceRepository, private val preferenceRepository: AppPreferenceRepository,
private val timerRepository: TimerRepository, private val timerRepository: TimerRepository,
) : ViewModel() { ) : ViewModel() {
val backStack = mutableStateListOf<Screen.Settings>(Screen.Settings.Main) val backStack = mutableStateListOf<Screen.Settings>(Screen.Settings.Main)
val isPlus = billingManager.isPlus
val isPurchaseStateLoaded = billingManager.isLoaded
private val _isSettingsLoaded = MutableStateFlow(false)
val isSettingsLoaded = _isSettingsLoaded.asStateFlow()
private val _preferencesState = MutableStateFlow(PreferencesState()) private val _preferencesState = MutableStateFlow(PreferencesState())
val preferencesState = _preferencesState.asStateFlow() val preferencesState = _preferencesState.asStateFlow()
@@ -85,26 +93,13 @@ class SettingsViewModel(
preferenceRepository.getBooleanPreferenceFlow("alarm_enabled").distinctUntilChanged() preferenceRepository.getBooleanPreferenceFlow("alarm_enabled").distinctUntilChanged()
val vibrateEnabled = val vibrateEnabled =
preferenceRepository.getBooleanPreferenceFlow("vibrate_enabled").distinctUntilChanged() preferenceRepository.getBooleanPreferenceFlow("vibrate_enabled").distinctUntilChanged()
val dndEnabled =
preferenceRepository.getBooleanPreferenceFlow("dnd_enabled").distinctUntilChanged()
init { init {
viewModelScope.launch { viewModelScope.launch {
val theme = preferenceRepository.getStringPreference("theme") reloadSettings()
?: preferenceRepository.saveStringPreference("theme", "auto") _isSettingsLoaded.value = true
val colorScheme = preferenceRepository.getStringPreference("color_scheme")
?: preferenceRepository.saveStringPreference("color_scheme", Color.White.toString())
val blackTheme = preferenceRepository.getBooleanPreference("black_theme")
?: preferenceRepository.saveBooleanPreference("black_theme", false)
val aodEnabled = preferenceRepository.getBooleanPreference("aod_enabled")
?: preferenceRepository.saveBooleanPreference("aod_enabled", false)
_preferencesState.update { currentState ->
currentState.copy(
theme = theme,
colorScheme = colorScheme,
blackTheme = blackTheme,
aodEnabled = aodEnabled
)
}
} }
} }
@@ -179,6 +174,13 @@ class SettingsViewModel(
} }
} }
fun saveDndEnabled(enabled: Boolean) {
viewModelScope.launch {
timerRepository.dndEnabled = enabled
preferenceRepository.saveBooleanPreference("dnd_enabled", enabled)
}
}
fun saveAlarmSound(uri: Uri?) { fun saveAlarmSound(uri: Uri?) {
viewModelScope.launch { viewModelScope.launch {
timerRepository.alarmSoundUri = uri timerRepository.alarmSoundUri = uri
@@ -222,14 +224,46 @@ class SettingsViewModel(
} }
} }
fun resetPaywalledSettings() {
_preferencesState.update { currentState ->
currentState.copy(
aodEnabled = false,
blackTheme = false,
colorScheme = Color.White.toString()
)
}
}
suspend fun reloadSettings() {
val theme = preferenceRepository.getStringPreference("theme")
?: preferenceRepository.saveStringPreference("theme", "auto")
val colorScheme = preferenceRepository.getStringPreference("color_scheme")
?: preferenceRepository.saveStringPreference("color_scheme", Color.White.toString())
val blackTheme = preferenceRepository.getBooleanPreference("black_theme")
?: preferenceRepository.saveBooleanPreference("black_theme", false)
val aodEnabled = preferenceRepository.getBooleanPreference("aod_enabled")
?: preferenceRepository.saveBooleanPreference("aod_enabled", false)
_preferencesState.update { currentState ->
currentState.copy(
theme = theme,
colorScheme = colorScheme,
blackTheme = blackTheme,
aodEnabled = aodEnabled
)
}
}
companion object { companion object {
val Factory: ViewModelProvider.Factory = viewModelFactory { val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer { initializer {
val application = (this[APPLICATION_KEY] as TomatoApplication) val application = (this[APPLICATION_KEY] as TomatoApplication)
val appPreferenceRepository = application.container.appPreferenceRepository val appPreferenceRepository = application.container.appPreferenceRepository
val appTimerRepository = application.container.appTimerRepository val appTimerRepository = application.container.appTimerRepository
val appBillingManager = application.container.billingManager
SettingsViewModel( SettingsViewModel(
billingManager = appBillingManager,
preferenceRepository = appPreferenceRepository, preferenceRepository = appPreferenceRepository,
timerRepository = appTimerRepository, timerRepository = appTimerRepository,
) )

View File

@@ -28,13 +28,77 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF) val primaryLight = Color(0xFF4C662B)
val PurpleGrey80 = Color(0xFFCCC2DC) val onPrimaryLight = Color(0xFFFFFFFF)
val Pink80 = Color(0xFFEFB8C8) val primaryContainerLight = Color(0xFFCDEDA3)
val onPrimaryContainerLight = Color(0xFF354E16)
val secondaryLight = Color(0xFF586249)
val onSecondaryLight = Color(0xFFFFFFFF)
val secondaryContainerLight = Color(0xFFDCE7C8)
val onSecondaryContainerLight = Color(0xFF404A33)
val tertiaryLight = Color(0xFF386663)
val onTertiaryLight = Color(0xFFFFFFFF)
val tertiaryContainerLight = Color(0xFFBCECE7)
val onTertiaryContainerLight = Color(0xFF1F4E4B)
val errorLight = Color(0xFFBA1A1A)
val onErrorLight = Color(0xFFFFFFFF)
val errorContainerLight = Color(0xFFFFDAD6)
val onErrorContainerLight = Color(0xFF93000A)
val backgroundLight = Color(0xFFF9FAEF)
val onBackgroundLight = Color(0xFF1A1C16)
val surfaceLight = Color(0xFFF9FAEF)
val onSurfaceLight = Color(0xFF1A1C16)
val surfaceVariantLight = Color(0xFFE1E4D5)
val onSurfaceVariantLight = Color(0xFF44483D)
val outlineLight = Color(0xFF75796C)
val outlineVariantLight = Color(0xFFC5C8BA)
val scrimLight = Color(0xFF000000)
val inverseSurfaceLight = Color(0xFF2F312A)
val inverseOnSurfaceLight = Color(0xFFF1F2E6)
val inversePrimaryLight = Color(0xFFB1D18A)
val surfaceDimLight = Color(0xFFDADBD0)
val surfaceBrightLight = Color(0xFFF9FAEF)
val surfaceContainerLowestLight = Color(0xFFFFFFFF)
val surfaceContainerLowLight = Color(0xFFF3F4E9)
val surfaceContainerLight = Color(0xFFEEEFE3)
val surfaceContainerHighLight = Color(0xFFE8E9DE)
val surfaceContainerHighestLight = Color(0xFFE2E3D8)
val Purple40 = Color(0xFF6650a4) val primaryDark = Color(0xFFB1D18A)
val PurpleGrey40 = Color(0xFF625b71) val onPrimaryDark = Color(0xFF1F3701)
val Pink40 = Color(0xFF7D5260) val primaryContainerDark = Color(0xFF354E16)
val onPrimaryContainerDark = Color(0xFFCDEDA3)
val secondaryDark = Color(0xFFBFCBAD)
val onSecondaryDark = Color(0xFF2A331E)
val secondaryContainerDark = Color(0xFF404A33)
val onSecondaryContainerDark = Color(0xFFDCE7C8)
val tertiaryDark = Color(0xFFA0D0CB)
val onTertiaryDark = Color(0xFF003735)
val tertiaryContainerDark = Color(0xFF1F4E4B)
val onTertiaryContainerDark = Color(0xFFBCECE7)
val errorDark = Color(0xFFFFB4AB)
val onErrorDark = Color(0xFF690005)
val errorContainerDark = Color(0xFF93000A)
val onErrorContainerDark = Color(0xFFFFDAD6)
val backgroundDark = Color(0xFF12140E)
val onBackgroundDark = Color(0xFFE2E3D8)
val surfaceDark = Color(0xFF12140E)
val onSurfaceDark = Color(0xFFE2E3D8)
val surfaceVariantDark = Color(0xFF44483D)
val onSurfaceVariantDark = Color(0xFFC5C8BA)
val outlineDark = Color(0xFF8F9285)
val outlineVariantDark = Color(0xFF44483D)
val scrimDark = Color(0xFF000000)
val inverseSurfaceDark = Color(0xFFE2E3D8)
val inverseOnSurfaceDark = Color(0xFF2F312A)
val inversePrimaryDark = Color(0xFF4C662B)
val surfaceDimDark = Color(0xFF12140E)
val surfaceBrightDark = Color(0xFF383A32)
val surfaceContainerLowestDark = Color(0xFF0C0F09)
val surfaceContainerLowDark = Color(0xFF1A1C16)
val surfaceContainerDark = Color(0xFF1E201A)
val surfaceContainerHighDark = Color(0xFF282B24)
val surfaceContainerHighestDark = Color(0xFF33362E)
object CustomColors { object CustomColors {
var black = false var black = false

View File

@@ -1,3 +1,20 @@
/*
* 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.ui.theme package org.nsh07.pomodoro.ui.theme
import android.app.Activity import android.app.Activity
@@ -19,26 +36,80 @@ import androidx.core.view.WindowCompat
import com.materialkolor.dynamiccolor.ColorSpec import com.materialkolor.dynamiccolor.ColorSpec
import com.materialkolor.rememberDynamicColorScheme import com.materialkolor.rememberDynamicColorScheme
private val DarkColorScheme = darkColorScheme( private val lightScheme = lightColorScheme(
primary = Purple80, primary = primaryLight,
secondary = PurpleGrey80, onPrimary = onPrimaryLight,
tertiary = Pink80 primaryContainer = primaryContainerLight,
onPrimaryContainer = onPrimaryContainerLight,
secondary = secondaryLight,
onSecondary = onSecondaryLight,
secondaryContainer = secondaryContainerLight,
onSecondaryContainer = onSecondaryContainerLight,
tertiary = tertiaryLight,
onTertiary = onTertiaryLight,
tertiaryContainer = tertiaryContainerLight,
onTertiaryContainer = onTertiaryContainerLight,
error = errorLight,
onError = onErrorLight,
errorContainer = errorContainerLight,
onErrorContainer = onErrorContainerLight,
background = backgroundLight,
onBackground = onBackgroundLight,
surface = surfaceLight,
onSurface = onSurfaceLight,
surfaceVariant = surfaceVariantLight,
onSurfaceVariant = onSurfaceVariantLight,
outline = outlineLight,
outlineVariant = outlineVariantLight,
scrim = scrimLight,
inverseSurface = inverseSurfaceLight,
inverseOnSurface = inverseOnSurfaceLight,
inversePrimary = inversePrimaryLight,
surfaceDim = surfaceDimLight,
surfaceBright = surfaceBrightLight,
surfaceContainerLowest = surfaceContainerLowestLight,
surfaceContainerLow = surfaceContainerLowLight,
surfaceContainer = surfaceContainerLight,
surfaceContainerHigh = surfaceContainerHighLight,
surfaceContainerHighest = surfaceContainerHighestLight,
) )
private val LightColorScheme = lightColorScheme( private val darkScheme = darkColorScheme(
primary = Purple40, primary = primaryDark,
secondary = PurpleGrey40, onPrimary = onPrimaryDark,
tertiary = Pink40 primaryContainer = primaryContainerDark,
onPrimaryContainer = onPrimaryContainerDark,
/* Other default colors to override secondary = secondaryDark,
background = Color(0xFFFFFBFE), onSecondary = onSecondaryDark,
surface = Color(0xFFFFFBFE), secondaryContainer = secondaryContainerDark,
onPrimary = Color.White, onSecondaryContainer = onSecondaryContainerDark,
onSecondary = Color.White, tertiary = tertiaryDark,
onTertiary = Color.White, onTertiary = onTertiaryDark,
onBackground = Color(0xFF1C1B1F), tertiaryContainer = tertiaryContainerDark,
onSurface = Color(0xFF1C1B1F), onTertiaryContainer = onTertiaryContainerDark,
*/ error = errorDark,
onError = onErrorDark,
errorContainer = errorContainerDark,
onErrorContainer = onErrorContainerDark,
background = backgroundDark,
onBackground = onBackgroundDark,
surface = surfaceDark,
onSurface = onSurfaceDark,
surfaceVariant = surfaceVariantDark,
onSurfaceVariant = onSurfaceVariantDark,
outline = outlineDark,
outlineVariant = outlineVariantDark,
scrim = scrimDark,
inverseSurface = inverseSurfaceDark,
inverseOnSurface = inverseOnSurfaceDark,
inversePrimary = inversePrimaryDark,
surfaceDim = surfaceDimDark,
surfaceBright = surfaceBrightDark,
surfaceContainerLowest = surfaceContainerLowestDark,
surfaceContainerLow = surfaceContainerLowDark,
surfaceContainer = surfaceContainerDark,
surfaceContainerHigh = surfaceContainerHighDark,
surfaceContainerHighest = surfaceContainerHighestDark,
) )
@OptIn(ExperimentalMaterial3ExpressiveApi::class) @OptIn(ExperimentalMaterial3ExpressiveApi::class)
@@ -56,8 +127,8 @@ fun TomatoTheme(
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
} }
darkTheme -> DarkColorScheme darkTheme -> darkScheme
else -> LightColorScheme else -> lightScheme
} }
val view = LocalView.current val view = LocalView.current

View File

@@ -1,8 +1,18 @@
/* /*
* Copyright (c) 2025 Nishant Mishra * Copyright (c) 2025 Nishant Mishra
* *
* You should have received a copy of the GNU General Public License * This file is part of Tomato - a minimalist pomodoro timer for Android.
* along with this program. If not, see <https://www.gnu.org/licenses/>. *
* 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 package org.nsh07.pomodoro.ui.timerScreen
@@ -92,10 +102,12 @@ import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerAction
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerMode import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerMode
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable @Composable
fun SharedTransitionScope.TimerScreen( fun SharedTransitionScope.TimerScreen(
timerState: TimerState, timerState: TimerState,
isPlus: Boolean,
progress: () -> Float, progress: () -> Float,
onAction: (TimerAction) -> Unit, onAction: (TimerAction) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
@@ -148,7 +160,8 @@ fun SharedTransitionScope.TimerScreen(
when (it) { when (it) {
TimerMode.BRAND -> TimerMode.BRAND ->
Text( Text(
stringResource(R.string.app_name), if (!isPlus) stringResource(R.string.app_name)
else stringResource(R.string.app_name_plus),
style = TextStyle( style = TextStyle(
fontFamily = robotoFlexTopBar, fontFamily = robotoFlexTopBar,
fontSize = 32.sp, fontSize = 32.sp,
@@ -541,6 +554,7 @@ fun TimerScreenPreview() {
SharedTransitionLayout { SharedTransitionLayout {
TimerScreen( TimerScreen(
timerState, timerState,
isPlus = true,
{ 0.3f }, { 0.3f },
{} {}
) )

View File

@@ -1,8 +1,18 @@
/* /*
* Copyright (c) 2025 Nishant Mishra * Copyright (c) 2025 Nishant Mishra
* *
* You should have received a copy of the GNU General Public License * This file is part of Tomato - a minimalist pomodoro timer for Android.
* along with this program. If not, see <https://www.gnu.org/licenses/>. *
* 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 package org.nsh07.pomodoro.ui.timerScreen.viewModel
@@ -85,6 +95,9 @@ class TimerViewModel(
timerRepository.vibrateEnabled = timerRepository.vibrateEnabled =
preferenceRepository.getBooleanPreference("vibrate_enabled") preferenceRepository.getBooleanPreference("vibrate_enabled")
?: preferenceRepository.saveBooleanPreference("vibrate_enabled", true) ?: preferenceRepository.saveBooleanPreference("vibrate_enabled", true)
timerRepository.dndEnabled =
preferenceRepository.getBooleanPreference("dnd_enabled")
?: preferenceRepository.saveBooleanPreference("dnd_enabled", false)
timerRepository.alarmSoundUri = ( timerRepository.alarmSoundUri = (
preferenceRepository.getStringPreference("alarm_sound") preferenceRepository.getStringPreference("alarm_sound")

View File

@@ -0,0 +1,30 @@
<!--
~ 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="40dp"
android:height="40dp"
android:viewportWidth="1279"
android:viewportHeight="1279">
<path
android:fillAlpha="0.7"
android:fillColor="#000000"
android:pathData="m670.4,590.8c-45.9,19.7 -98.1,42 -165.6,42 -28.3,-0.1 -56.4,-3.9 -83.6,-11.5l46.7,479.8c1.7,20 10.8,38.8 25.6,52.4 14.8,13.6 34.2,21.2 54.3,21.2 0,0 66.3,3.4 88.4,3.4 23.8,0 95.2,-3.4 95.2,-3.4 20.1,0 39.5,-7.6 54.3,-21.2 14.8,-13.6 23.9,-32.3 25.6,-52.4l50.1,-530.2c-22.4,-7.6 -44.9,-12.7 -70.4,-12.7 -44,-0 -79.5,15.1 -120.4,32.7z" />
<path
android:fillColor="#000000"
android:pathData="m1077.3,341.8 l-7,-35.5c-6.3,-31.8 -20.6,-61.9 -53.3,-73.5 -10.5,-3.7 -22.4,-5.3 -30.4,-12.9 -8,-7.6 -10.4,-19.5 -12.3,-30.4 -3.4,-20.1 -6.7,-40.3 -10.2,-60.4 -3,-17.3 -5.5,-36.7 -13.4,-52.6 -10.3,-21.3 -31.7,-33.8 -53,-42 -10.9,-4.1 -22.1,-7.5 -33.4,-10.3 -53.2,-14 -109.2,-19.2 -163.9,-22.1 -65.7,-3.6 -131.6,-2.5 -197.2,3.3 -48.8,4.4 -100.2,9.8 -146.6,26.7 -16.9,6.2 -34.4,13.6 -47.3,26.7 -15.8,16.1 -21,41 -9.4,61 8.2,14.2 22.1,24.3 36.9,31 19.2,8.6 39.3,15.1 59.8,19.5 57.3,12.7 116.6,17.6 175.2,19.8 64.9,2.6 129.9,0.5 194.4,-6.3 16,-1.8 31.9,-3.9 47.8,-6.3 18.7,-2.9 30.8,-27.4 25.2,-44.4 -6.6,-20.4 -24.4,-28.3 -44.4,-25.2 -3,0.5 -5.9,0.9 -8.9,1.3l-2.1,0.3c-6.8,0.9 -13.6,1.7 -20.4,2.4 -14.1,1.5 -28.1,2.8 -42.3,3.7 -31.6,2.2 -63.3,3.2 -95,3.3 -31.1,0 -62.3,-0.9 -93.4,-2.9 -14.2,-0.9 -28.3,-2.1 -42.4,-3.5 -6.4,-0.7 -12.8,-1.4 -19.2,-2.2l-6.1,-0.8 -1.3,-0.2 -6.3,-0.9c-12.9,-1.9 -25.8,-4.2 -38.6,-6.9 -1.3,-0.3 -2.4,-1 -3.3,-2 -0.8,-1 -1.3,-2.3 -1.3,-3.6 0,-1.3 0.4,-2.6 1.3,-3.6 0.8,-1 2,-1.7 3.3,-2h0.2c11.1,-2.4 22.2,-4.4 33.4,-6.1 3.7,-0.6 7.5,-1.2 11.2,-1.7h0.1c7,-0.5 14,-1.7 21,-2.5 60.6,-6.3 121.6,-8.5 182.5,-6.4 29.6,0.9 59.1,2.6 88.6,5.6 6.3,0.7 12.6,1.3 18.9,2.1 2.4,0.3 4.8,0.6 7.3,0.9l4.9,0.7c14.2,2.1 28.4,4.7 42.5,7.7 20.9,4.5 47.7,6 57,28.9 3,7.3 4.3,15.3 5.9,23l2.1,9.7c0.1,0.2 0.1,0.4 0.1,0.5 4.9,22.9 9.8,45.9 14.8,68.8 0.4,1.7 0.4,3.4 0,5.1 -0.3,1.7 -1,3.3 -2,4.7 -1,1.4 -2.3,2.6 -3.7,3.5 -1.5,0.9 -3.1,1.5 -4.8,1.7h-0.1l-3,0.4 -3,0.4c-9.4,1.2 -18.9,2.4 -28.3,3.4 -18.6,2.1 -37.3,4 -55.9,5.5 -37.1,3.1 -74.3,5.1 -111.6,6.1 -19,0.5 -38,0.7 -56.9,0.7 -75.5,-0.1 -151,-4.4 -226,-13.1 -8.1,-1 -16.2,-2 -24.4,-3 6.3,0.8 -4.6,-0.6 -6.8,-0.9 -5.2,-0.7 -10.3,-1.5 -15.5,-2.3 -17.3,-2.6 -34.5,-5.8 -51.8,-8.6 -20.9,-3.4 -40.9,-1.7 -59.8,8.6 -15.5,8.5 -28.1,21.5 -36,37.3 -8.2,16.9 -10.6,35.2 -14.2,53.3 -3.6,18.1 -9.3,37.6 -7.2,56.2 4.6,40.1 32.7,72.8 73.1,80.1 38,6.9 76.2,12.5 114.4,17.2 150.4,18.4 302.3,20.6 453.2,6.6 12.3,-1.1 24.6,-2.4 36.8,-3.8 3.8,-0.4 7.7,0 11.3,1.3 3.6,1.3 6.9,3.3 9.7,6 2.7,2.7 4.8,6 6.1,9.6 1.3,3.6 1.8,7.5 1.4,11.3l-3.8,37.1c-7.7,75 -15.4,150.1 -23.1,225.1 -8,78.8 -16.1,157.6 -24.2,236.3 -2.3,22.2 -4.6,44.4 -6.9,66.5 -2.2,21.8 -2.5,44.4 -6.7,65.9 -6.5,33.9 -29.5,54.8 -63,62.4 -30.7,7 -62.1,10.7 -93.6,10.9 -34.9,0.2 -69.8,-1.4 -104.7,-1.2 -37.3,0.2 -82.9,-3.2 -111.7,-31 -25.3,-24.4 -28.8,-62.5 -32.2,-95.5 -4.6,-43.7 -9.1,-87.3 -13.6,-131l-25.3,-242.8 -16.4,-157.1c-0.3,-2.6 -0.6,-5.2 -0.8,-7.8 -2,-18.7 -15.2,-37.1 -36.1,-36.1 -17.9,0.8 -38.2,16 -36.1,36.1l12.1,116.5 25.1,240.9c7.1,68.4 14.3,136.9 21.4,205.3 1.4,13.1 2.7,26.3 4.1,39.4 7.9,71.7 62.6,110.3 130.3,121.1 39.6,6.4 80.1,7.7 120.3,8.3 51.5,0.8 103.5,2.8 154.1,-6.5 75,-13.8 131.3,-63.9 139.4,-141.6 2.3,-22.4 4.6,-44.9 6.9,-67.3 7.6,-74.2 15.2,-148.5 22.9,-222.7l24.9,-242.6 11.4,-111.2c0.6,-5.5 2.9,-10.7 6.6,-14.8 3.7,-4.1 8.7,-6.9 14.1,-7.9 21.5,-4.2 42,-11.3 57.2,-27.7 24.3,-26 29.1,-59.9 20.5,-94.1zM998.5,383c-7.7,7.3 -19.3,10.7 -30.8,12.4 -128.7,19.1 -259.3,28.8 -389.4,24.5 -93.1,-3.2 -185.3,-13.5 -277.5,-26.5 -9,-1.3 -18.8,-2.9 -25,-9.6 -11.7,-12.6 -6,-37.9 -2.9,-53 2.8,-13.9 8.1,-32.4 24.7,-34.4 25.8,-3 55.8,7.9 81.3,11.7 30.7,4.7 61.6,8.4 92.6,11.3 132.2,12 266.6,10.2 398.2,-7.4 24,-3.2 47.9,-7 71.7,-11.2 21.2,-3.8 44.7,-10.9 57.6,11 8.8,15 10,35 8.6,51.9 -0.4,7.4 -3.6,14.3 -9,19.4z" />
</vector>

View File

@@ -1,9 +0,0 @@
<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="M440,720q-117,0 -198.5,-81.5T160,440v-240q0,-33 23.5,-56.5T240,120h500q58,0 99,41t41,99q0,58 -41,99t-99,41h-20v40q0,117 -81.5,198.5T440,720ZM240,320h400v-120L240,200v120ZM720,320h20q25,0 42.5,-17.5T800,260q0,-25 -17.5,-42.5T740,200h-20v120ZM200,840q-17,0 -28.5,-11.5T160,800q0,-17 11.5,-28.5T200,760h560q17,0 28.5,11.5T800,800q0,17 -11.5,28.5T760,840L200,840Z" />
</vector>

View File

@@ -0,0 +1,26 @@
<!--
~ Copyright (c) 2025 Nishant Mishra
~
~ This file is part of Tomato - a minimalist pomodoro timer for Android.
~
~ Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
~ General Public License as published by the Free Software Foundation, either version 3 of the
~ License, or (at your option) any later version.
~
~ Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
~ the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
~ Public License for more details.
~
~ You should have received a copy of the GNU General Public License along with Tomato.
~ If not, see <https://www.gnu.org/licenses/>.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#e3e3e3"
android:pathData="M320,520h320q17,0 28.5,-11.5T680,480q0,-17 -11.5,-28.5T640,440L320,440q-17,0 -28.5,11.5T280,480q0,17 11.5,28.5T320,520ZM480,880q-83,0 -156,-31.5T197,763q-54,-54 -85.5,-127T80,480q0,-83 31.5,-156T197,197q54,-54 127,-85.5T480,80q83,0 156,31.5T763,197q54,54 85.5,127T880,480q0,83 -31.5,156T763,763q-54,54 -127,85.5T480,880Z" />
</vector>

View File

@@ -1,10 +1,27 @@
<!--
~ 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" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:viewportWidth="6.35" android:viewportWidth="24"
android:viewportHeight="6.35"> android:viewportHeight="24">
<path <path
android:fillColor="#000000" android:fillColor="#ffffff"
android:pathData="M0.6744,6.3347C0.5991,6.3185 0.471,6.275 0.4362,6.2538 0.4204,6.2442 0.7343,5.9434 1.9712,4.7827L3.5262,3.3236 4.159,3.8713C4.5071,4.1725 4.7868,4.4231 4.7806,4.4283 4.767,4.4398 1.3189,6.2241 1.2202,6.2708 1.0678,6.3428 0.8376,6.3698 0.6744,6.3347ZM0.0712,5.8885C-0.0032,5.736 0,5.8606 0,3.1751c0,-2.6939 -0.0037,-2.5574 0.0735,-2.7187 0.0195,-0.0409 0.0396,-0.0743 0.0445,-0.0743 0.0049,0 0.7041,0.6015 1.5538,1.3366L3.2165,3.0555 1.6709,4.5056C0.8208,5.3031 0.1205,5.9557 0.1146,5.9557c-0.0059,0 -0.0254,-0.0302 -0.0435,-0.0672zM4.5028,3.6306 L3.826,3.0448 4.377,2.5266C4.6801,2.2415 4.9336,2.0083 4.9405,2.0083c0.0168,0 0.9615,0.4887 1.0486,0.5425C6.2037,2.6831 6.35,2.9363 6.35,3.1751 6.35,3.4137 6.2088,3.6595 5.9939,3.7948 5.9253,3.838 5.1959,4.2187 5.1846,4.2171 5.1818,4.2168 4.875,3.9528 4.5028,3.6306ZM3.241,2.5383C3.099,2.4141 2.409,1.8158 1.7076,1.2088 0.5332,0.1924 0.4344,0.1038 0.4572,0.0882 0.5059,0.0547 0.6943,0.008 0.8067,0.0015 0.9426,-0.0064 1.0774,0.0175 1.2002,0.0715 1.247,0.092 2.0206,0.4896 2.9193,0.955L4.5534,1.8012 4.0396,2.2834C3.757,2.5485 3.5198,2.7652 3.5125,2.7648 3.5052,2.7644 3.383,2.6625 3.241,2.5383Z" android:pathData="m12.213,13.862c-0.337,-0.338 -0.884,-0.338 -1.22,0 -0.883,0.884 -6.416,6.433 -7.161,7.175 0.865,0.922 2.431,1.145 3.528,0.475l7.946,-4.554c-0.733,-0.731 -2.349,-2.35 -3.092,-3.096zM10.993,10.2c0.337,0.338 0.884,0.338 1.22,0l1.856,-1.86 -0.002,-0.001L15.33,7.077 7.39,2.505C6.271,1.804 4.658,2.044 3.79,2.984 4.518,3.707 10.125,9.332 10.993,10.2ZM9.775,12.643c0.337,-0.338 0.337,-0.886 0,-1.224L4.805,6.434 3.02,4.652C3.007,4.756 2.998,4.87 3,4.98l0.001,14.069c0.004,0.132 0.023,0.286 0.044,0.418 1.175,-1.151 5.501,-5.573 6.73,-6.825zM19.519,9.489 L16.878,7.968c-0.774,0.768 -2.656,2.66 -3.447,3.452 -0.338,0.338 -0.338,0.886 0,1.224 0.796,0.792 2.645,2.656 3.424,3.428l2.639,-1.512c1.996,-0.99 2.011,-4.063 0.025,-5.071z"
android:strokeWidth="0.0120146" /> android:strokeWidth="0.599991" />
</vector> </vector>

View File

@@ -19,7 +19,7 @@
<string name="dynamic">ديناميكي</string> <string name="dynamic">ديناميكي</string>
<string name="color">لون</string> <string name="color">لون</string>
<string name="system_default">النظام (الافتراضي)</string> <string name="system_default">النظام (الافتراضي)</string>
<string name="alarm">منبه</string> <string name="alarm">المنبه</string>
<string name="light">فاتح</string> <string name="light">فاتح</string>
<string name="dark">داكن</string> <string name="dark">داكن</string>
<string name="choose_theme">اختر سمة</string> <string name="choose_theme">اختر سمة</string>
@@ -31,7 +31,7 @@
<string name="alarm_desc">التنبيه عند انتهاء المؤقت</string> <string name="alarm_desc">التنبيه عند انتهاء المؤقت</string>
<string name="vibrate">اهتزاز</string> <string name="vibrate">اهتزاز</string>
<string name="vibrate_desc">الاهتزاز عندما ينتهي المؤقت</string> <string name="vibrate_desc">الاهتزاز عندما ينتهي المؤقت</string>
<string name="theme">سمة</string> <string name="theme">السمة</string>
<string name="settings">الاعدادات</string> <string name="settings">الاعدادات</string>
<string name="session_length">طول الجلسة</string> <string name="session_length">طول الجلسة</string>
<string name="session_length_desc">فترات التركيز في الجلسة الواحدة: %1$d</string> <string name="session_length_desc">فترات التركيز في الجلسة الواحدة: %1$d</string>
@@ -57,4 +57,9 @@
<string name="timer">المؤقت</string> <string name="timer">المؤقت</string>
<string name="timer_progress">تقدم المؤقت</string> <string name="timer_progress">تقدم المؤقت</string>
<string name="last_year">السنة الماضية</string> <string name="last_year">السنة الماضية</string>
<string name="always_on_display">العرض الدائم علي الشاشة</string>
<string name="always_on_display_desc">اضغط في أي مكان أثناء عرض المؤقت للتبديل إلى وضع العرض الدائم على الشاشة</string>
<string name="appearance">المظهر</string>
<string name="durations">الفترات</string>
<string name="sound">الصوت</string>
</resources> </resources>

View File

@@ -18,7 +18,7 @@
<string name="color_scheme">Farbschema</string> <string name="color_scheme">Farbschema</string>
<string name="dynamic">Dynamisch</string> <string name="dynamic">Dynamisch</string>
<string name="color">Farbe</string> <string name="color">Farbe</string>
<string name="system_default">Systemstandard</string> <string name="system_default">System</string>
<string name="alarm">Alarm</string> <string name="alarm">Alarm</string>
<string name="light">Hell</string> <string name="light">Hell</string>
<string name="dark">Dunkel</string> <string name="dark">Dunkel</string>
@@ -56,4 +56,12 @@
<string name="up_next">Als nächstes</string> <string name="up_next">Als nächstes</string>
<string name="timer">Timer</string> <string name="timer">Timer</string>
<string name="timer_progress">Timer-Fortschritt</string> <string name="timer_progress">Timer-Fortschritt</string>
<string name="always_on_display">Always-On Display</string>
<string name="always_on_display_desc">Tippe irgendwo, während der Timer angezeigt wird, um in den AOD-Modus zu wechseln.</string>
<string name="last_year">Letztes Jahr</string>
<string name="appearance">Aussehen</string>
<string name="durations">Dauer</string>
<string name="sound">Sound</string>
<string name="dnd">Bitte nicht stören</string>
<string name="dnd_desc">Bitte nicht stören aktivieren, wenn ein Fokus-Timer läuft</string>
</resources> </resources>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="start">Démarrer</string> <string name="start">Démarrer</string>
<string name="stop">Arrêter</string> <string name="stop">Pause</string>
<string name="focus">Concentration</string> <string name="focus">Concentration</string>
<string name="short_break">Pause courte</string> <string name="short_break">Pause courte</string>
<string name="long_break">Pause longue</string> <string name="long_break">Pause longue</string>
@@ -18,7 +18,7 @@
<string name="color_scheme">Thème de couleurs</string> <string name="color_scheme">Thème de couleurs</string>
<string name="dynamic">Dynamique</string> <string name="dynamic">Dynamique</string>
<string name="color">Couleur</string> <string name="color">Couleur</string>
<string name="system_default">Valeur par défaut du système</string> <string name="system_default">Système</string>
<string name="alarm">Alarme</string> <string name="alarm">Alarme</string>
<string name="light">Clair</string> <string name="light">Clair</string>
<string name="dark">Sombre</string> <string name="dark">Sombre</string>
@@ -29,7 +29,7 @@
<string name="black_theme">Thème noir</string> <string name="black_theme">Thème noir</string>
<string name="black_theme_desc">Utiliser un thème sombre noir pur</string> <string name="black_theme_desc">Utiliser un thème sombre noir pur</string>
<string name="alarm_desc">Faire sonner lalarme à la fin du minuteur</string> <string name="alarm_desc">Faire sonner lalarme à la fin du minuteur</string>
<string name="vibrate">Vibrer</string> <string name="vibrate">Vibration</string>
<string name="vibrate_desc">Faire vibrer à la fin du minuteur</string> <string name="vibrate_desc">Faire vibrer à la fin du minuteur</string>
<string name="theme">Thème</string> <string name="theme">Thème</string>
<string name="settings">Paramètres</string> <string name="settings">Paramètres</string>
@@ -38,7 +38,7 @@
<string name="pomodoro_info">Une \"session\" est une séquence dintervalles Pomodoro comprenant des phases de concentration, des pauses courtes et une pause longue. La dernière pause dune session est toujours une pause longue.</string> <string name="pomodoro_info">Une \"session\" est une séquence dintervalles Pomodoro comprenant des phases de concentration, des pauses courtes et une pause longue. La dernière pause dune session est toujours une pause longue.</string>
<string name="stats">Statistiques</string> <string name="stats">Statistiques</string>
<string name="today">Aujourd\'hui</string> <string name="today">Aujourd\'hui</string>
<string name="break_">Pause</string> <string name="break_">Pauses</string>
<string name="last_week">7 derniers jours</string> <string name="last_week">7 derniers jours</string>
<string name="focus_per_day_avg">concentration moyenne par jour</string> <string name="focus_per_day_avg">concentration moyenne par jour</string>
<string name="more_info">Plus d\'infos</string> <string name="more_info">Plus d\'infos</string>
@@ -59,4 +59,15 @@
<string name="last_year">12 derniers mois</string> <string name="last_year">12 derniers mois</string>
<string name="always_on_display_desc">Appuyez n\'importe où lors de l\'affichage du minuteur pour passer en mode AOD</string> <string name="always_on_display_desc">Appuyez n\'importe où lors de l\'affichage du minuteur pour passer en mode AOD</string>
<string name="always_on_display">Affichage Permanent</string> <string name="always_on_display">Affichage Permanent</string>
<string name="appearance">Apparence</string>
<string name="durations">Durées</string>
<string name="sound">Son</string>
<string name="dnd">Ne pas déranger</string>
<string name="dnd_desc">Activer le mode ne pas déranger pendant un minuteur de concentration</string>
<string name="dynamic_color">Couleur dynamique</string>
<string name="dynamic_color_desc">Adapter les couleurs à celles de votre fond d\'écran</string>
<string name="tomato_foss_desc">Toutes les fonctionnalités sont déverrouillées dans cette version. Si mon application a fait une différence dans votre vie, veuillez envisager de me soutenir en faisant un don de %1$s.</string>
<string name="app_name_plus">Tomato+</string>
<string name="get_plus">Obtenir Tomato+</string>
<string name="tomato_foss">Tomato FOSS</string>
</resources> </resources>

View File

@@ -3,4 +3,63 @@
<string name="start">प्रारंभ करें</string> <string name="start">प्रारंभ करें</string>
<string name="stop">विराम करें</string> <string name="stop">विराम करें</string>
<string name="focus">ध्यान</string> <string name="focus">ध्यान</string>
<string name="alarm">अलार्म</string>
<string name="alarm_desc">टाइमर पूरा होने पर अलार्म बजाएँ</string>
<string name="alarm_sound">अलार्म ध्वनि</string>
<string name="always_on_display">ऑलवेज ऑन डिस्प्ले</string>
<string name="always_on_display_desc">AOD मोड चालू करने के लिए टाइमर देखते समय कहीं भी टैप करें</string>
<string name="black_theme">काली थीम</string>
<string name="black_theme_desc">पूर्णतः काले रंग की थीम का उपयोग करें</string>
<string name="break_">विश्राम</string>
<string name="choose_color_scheme">रंग स्कीम चुनें</string>
<string name="choose_theme">थीम चुनें</string>
<string name="color">रंग</string>
<string name="color_scheme">रंग स्कीम</string>
<string name="completed">पूर्ण</string>
<string name="dark">काला</string>
<string name="dynamic">डायनामिक</string>
<string name="exit">बंद करें</string>
<string name="focus_per_day_avg">प्रतिदिन ध्यान (औसत)</string>
<string name="last_month">पिछले महीने</string>
<string name="last_week">पिछले सप्ताह</string>
<string name="sound">ध्वनि</string>
<string name="last_year">पिछले साल</string>
<string name="light">सफ़ेद</string>
<string name="long_break">दीर्घ विश्राम</string>
<string name="min_remaining_notification">%1$s मिनट शेष</string>
<string name="monthly_productivity_analysis">मासिक उत्पादकता विश्लेषण</string>
<string name="more">अधिक</string>
<string name="more_info">अधिक जानकारी</string>
<string name="ok">ठीक</string>
<string name="pause">रोकें</string>
<string name="paused">रुका हुआ</string>
<string name="play">शुरू करें</string>
<string name="pomodoro_info">एक \"सत्र\" पोमोडोरो अंतरालों का एक क्रम होता है जिसमें ध्यान अंतराल, लघु विश्राम अंतराल और एक दीर्घ विश्राम अंतराल शामिल होता है। सत्र का अंतिम विश्राम हमेशा एक दीर्घ विश्राम होता है।</string>
<string name="productivity_analysis">उत्पादकता विश्लेषण</string>
<string name="productivity_analysis_desc">दिन के अलग-अलग समय पर ध्यान अवधि</string>
<string name="restart">पुनः शुरू करें</string>
<string name="session_length">सत्र की अवधि</string>
<string name="session_length_desc">एक सत्र में ध्यान अंतराल: %1$d</string>
<string name="settings">सेटिंग</string>
<string name="short_break">लघु विश्राम</string>
<string name="skip">अगला अंतराल</string>
<string name="skip_to_next">अगले पर जाएं</string>
<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_question">अलार्म बंद करें?</string>
<string name="system_default">सिस्टम</string>
<string name="theme">थीम</string>
<string name="timer">टाइमर</string>
<string name="timer_progress">टाइमर पूर्णता</string>
<string name="timer_session_count">%2$d में से %1$d</string>
<string name="today">आज</string>
<string name="up_next">आगामी</string>
<string name="up_next_notification">आगामी: %1$s (%2$s)</string>
<string name="vibrate">वाइब्रेट</string>
<string name="vibrate_desc">टाइमर पूरा होने पर वाइब्रेट करें</string>
<string name="weekly_productivity_analysis">साप्ताहिक उत्पादकता विश्लेषण</string>
<string name="appearance">दिखावट</string>
<string name="durations">अवधियां</string>
</resources> </resources>

View File

@@ -18,7 +18,7 @@
<string name="color_scheme">Renk şeması</string> <string name="color_scheme">Renk şeması</string>
<string name="dynamic">Dinamik</string> <string name="dynamic">Dinamik</string>
<string name="color">Renk</string> <string name="color">Renk</string>
<string name="system_default">Sistem varsayılanı</string> <string name="system_default">Sistem</string>
<string name="light">ık</string> <string name="light">ık</string>
<string name="dark">Koyu</string> <string name="dark">Koyu</string>
<string name="choose_theme">Tema seçin</string> <string name="choose_theme">Tema seçin</string>
@@ -61,4 +61,7 @@
<string name="always_on_display_desc">Zamanlayıcıyı görüntülerken AOD moduna geçmek için herhangi bir yere dokunun</string> <string name="always_on_display_desc">Zamanlayıcıyı görüntülerken AOD moduna geçmek için herhangi bir yere dokunun</string>
<string name="appearance">Görünüm</string> <string name="appearance">Görünüm</string>
<string name="durations">Süreler</string> <string name="durations">Süreler</string>
<string name="sound">Ses</string>
<string name="dnd">Rahatsız Etmeyin</string>
<string name="dnd_desc">Odaklanma sayacı çalışırken RE\'yi aç</string>
</resources> </resources>

View File

@@ -35,7 +35,7 @@
<string name="ok">ОК</string> <string name="ok">ОК</string>
<string name="dynamic">Динамічна</string> <string name="dynamic">Динамічна</string>
<string name="color">Колір</string> <string name="color">Колір</string>
<string name="system_default">Налаштування системи</string> <string name="system_default">Система</string>
<string name="alarm">Сигнал</string> <string name="alarm">Сигнал</string>
<string name="light">Світла</string> <string name="light">Світла</string>
<string name="dark">Темна</string> <string name="dark">Темна</string>
@@ -57,4 +57,17 @@
<string name="pause">Пауза</string> <string name="pause">Пауза</string>
<string name="play">Грати</string> <string name="play">Грати</string>
<string name="last_year">Минулого року</string> <string name="last_year">Минулого року</string>
<string name="always_on_display">Always On Display</string>
<string name="always_on_display_desc">Натисніть будь-де під час перегляду таймера, щоб перейти в режим AOD</string>
<string name="appearance">Вигляд</string>
<string name="durations">Тривалість</string>
<string name="sound">Звук</string>
<string name="dnd">Не турбувати</string>
<string name="dnd_desc">Активувати режим «Не турбувати», доки таймер активний</string>
<string name="app_name_plus">Tomato+</string>
<string name="get_plus">Отримати Tomato+</string>
<string name="dynamic_color">Динамічний колір</string>
<string name="dynamic_color_desc">Адаптуйте кольори теми зі своїх шпалер</string>
<string name="tomato_foss">Tomato FOSS</string>
<string name="tomato_foss_desc">Усі функції розблоковано в цій версії. Якщо мій додаток допоміг Вам у житті, будь ласка, підтримайте мене, зробивши пожертву на %1$s.</string>
</resources> </resources>

View File

@@ -18,7 +18,7 @@
<string name="color_scheme">配色方案</string> <string name="color_scheme">配色方案</string>
<string name="dynamic">动态</string> <string name="dynamic">动态</string>
<string name="color">颜色</string> <string name="color">颜色</string>
<string name="system_default">系统默认</string> <string name="system_default">系统</string>
<string name="alarm">响铃</string> <string name="alarm">响铃</string>
<string name="light">亮色</string> <string name="light">亮色</string>
<string name="dark">暗色</string> <string name="dark">暗色</string>
@@ -59,4 +59,9 @@
<string name="last_year">去年</string> <string name="last_year">去年</string>
<string name="always_on_display">息屏显示</string> <string name="always_on_display">息屏显示</string>
<string name="always_on_display_desc">查看计时器时点击任意位置切换至 AOD 模式</string> <string name="always_on_display_desc">查看计时器时点击任意位置切换至 AOD 模式</string>
<string name="appearance">外观</string>
<string name="durations">时长</string>
<string name="sound">声音</string>
<string name="dnd">勿扰模式</string>
<string name="dnd_desc">运行「专注」计时器时打开勿扰</string>
</resources> </resources>

View File

@@ -18,7 +18,7 @@
<string name="color_scheme">配色方案</string> <string name="color_scheme">配色方案</string>
<string name="dynamic">動態</string> <string name="dynamic">動態</string>
<string name="color">顏色</string> <string name="color">顏色</string>
<string name="system_default">系統默認</string> <string name="system_default">系統</string>
<string name="alarm">鬧鐘</string> <string name="alarm">鬧鐘</string>
<string name="light">亮色</string> <string name="light">亮色</string>
<string name="dark">暗色</string> <string name="dark">暗色</string>
@@ -57,4 +57,17 @@
<string name="timer">计时</string> <string name="timer">计时</string>
<string name="timer_progress">计时进度</string> <string name="timer_progress">计时进度</string>
<string name="last_year">上年</string> <string name="last_year">上年</string>
<string name="always_on_display">熄屏模式</string>
<string name="appearance">外觀</string>
<string name="always_on_display_desc">在查看計時器時,點擊任意位置即可切換至熄屏模式</string>
<string name="durations">持續時間</string>
<string name="sound">聲音</string>
<string name="dnd">請勿打擾</string>
<string name="dnd_desc">在執行專注計時器時開啟勿擾模式</string>
<string name="get_plus">獲取 Tomato+</string>
<string name="app_name_plus">Tomato+</string>
<string name="dynamic_color">動態顔色</string>
<string name="dynamic_color_desc">調整主題顏色為你的壁紙顔色</string>
<string name="tomato_foss">Tomato FOSS</string>
<string name="tomato_foss_desc">所有功能在此版本中均已解鎖。如果我的應用程式改變了您的生活,請考慮透過捐贈 %1$s 來支持我。</string>
</resources> </resources>

View File

@@ -79,4 +79,12 @@
<string name="appearance">Appearance</string> <string name="appearance">Appearance</string>
<string name="durations">Durations</string> <string name="durations">Durations</string>
<string name="sound">Sound</string> <string name="sound">Sound</string>
<string name="dnd">Do Not Disturb</string>
<string name="dnd_desc">Turn on DND when running a Focus timer</string>
<string name="app_name_plus">Tomato+</string>
<string name="get_plus">Get Tomato+</string>
<string name="dynamic_color">Dynamic color</string>
<string name="dynamic_color_desc">Adapt theme colors from your wallpaper</string>
<string name="tomato_foss">Tomato FOSS</string>
<string name="tomato_foss_desc">All features are unlocked in this version. If my app made a difference in your life, please consider supporting me by donating on %1$s.</string>
</resources> </resources>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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/>.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="com.android.vending.BILLING" />
</manifest>

View File

@@ -0,0 +1,63 @@
/*
* 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.billing
import android.util.Log
import com.revenuecat.purchases.Purchases
import com.revenuecat.purchases.getCustomerInfoWith
import com.revenuecat.purchases.interfaces.UpdatedCustomerInfoListener
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
private const val ENTITLEMENT_ID = "plus"
/**
* Google Play implementation of BillingManager
*/
class PlayBillingManager : BillingManager {
private val _isPlus = MutableStateFlow(false)
override val isPlus = _isPlus.asStateFlow()
private val _isLoaded = MutableStateFlow(false)
override val isLoaded = _isLoaded.asStateFlow()
private val purchases by lazy { Purchases.sharedInstance }
init {
purchases.updatedCustomerInfoListener =
UpdatedCustomerInfoListener { customerInfo ->
_isPlus.value = customerInfo.entitlements[ENTITLEMENT_ID]?.isActive == true
}
// Fetch initial customer info
purchases.getCustomerInfoWith(
onSuccess = { customerInfo ->
_isPlus.value = customerInfo.entitlements[ENTITLEMENT_ID]?.isActive == true
_isLoaded.value = true
},
onError = { error ->
Log.e("GooglePlayPaywallManager", "Error fetching customer info: $error")
_isLoaded.value = true
}
)
}
}
object BillingManagerProvider {
val manager: BillingManager = PlayBillingManager()
}

View File

@@ -0,0 +1,62 @@
/*
* 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.billing
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.FilledTonalIconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.revenuecat.purchases.ui.revenuecatui.Paywall
import com.revenuecat.purchases.ui.revenuecatui.PaywallOptions
import com.revenuecat.purchases.ui.revenuecatui.customercenter.CustomerCenter
import org.nsh07.pomodoro.R
@Composable
fun TomatoPlusPaywallDialog(
isPlus: Boolean,
onDismiss: () -> Unit
) {
val paywallOptions = remember {
PaywallOptions.Builder(dismissRequest = onDismiss).build()
}
Scaffold { innerPadding ->
if (!isPlus) {
Paywall(paywallOptions)
FilledTonalIconButton(
onClick = onDismiss,
modifier = Modifier
.padding(innerPadding)
.padding(16.dp)
) {
Icon(
painterResource(R.drawable.arrow_back),
null
)
}
} else {
CustomerCenter(onDismiss = onDismiss)
}
}
}

View File

@@ -0,0 +1,30 @@
/*
* 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.billing
import android.content.Context
import com.revenuecat.purchases.Purchases
import com.revenuecat.purchases.PurchasesConfiguration
fun initializePurchases(context: Context) {
Purchases.configure(
PurchasesConfiguration
.Builder(context, "goog_jBpRIBjTYvhKYluCqkPXSHbuFbX")
.build()
)
}

View File

@@ -1 +1,12 @@
<p><i>Tomato</i> ist ein minimalistischer Pomodoro-Timer für Android, der auf Material 3 Expressive basiert. </p><p><br><b>Funktionen:</b></p><ul><li>Einfache, minimalistische Benutzeroberfläche, die auf den neuesten Material 3 Expressive-Richtlinien basiert</li><li>Detaillierte Statistiken zu Arbeits-/Lernzeiten in leicht verständlicher Form<ul><li>Statistiken für den aktuellen Tag auf einen Blick sichtbar</li><li>Statistiken für die letzte Woche und den letzten Monat in einer übersichtlichen und sauberen Grafik dargestellt</li><li>Zusätzliche Statistiken für die letzte Woche und den letzten Monat zeigen, zu welcher Tageszeit Sie am produktivsten sind</li></ul></li><li>Anpassbare Timer Voreinstellungen</li></ul> <i>Tomato</i> ist ein minimalistischer Pomodoro-Timer für Android, der auf Material 3 Expressive basiert.
Tomato ist vollständig kostenlos und Open Source, für immer. Unter https://github.com/nsh07/Tomato finden Sie den Quellcode und können Fehler melden oder Funktionen vorschlagen
<b>Funktionen:</b>
-Einfache, minimalistische Benutzeroberfläche, die auf den neuesten Material 3 Expressive-Richtlinien basiert
-Detaillierte Statistiken zu Arbeits-/Lernzeiten in leicht verständlicher Form
-Statistiken für den aktuellen Tag auf einen Blick sichtbar
-Statistiken für die letzte Woche und den letzten Monat in einer übersichtlichen und sauberen Grafik dargestellt
-Zusätzliche Statistiken für die letzte Woche und den letzten Monat zeigen, zu welcher Tageszeit Sie am produktivsten sind
-Anpassbare Timer Voreinstellungen
-Unterstützung für Android 16 Live-Updates

View File

@@ -0,0 +1,5 @@
New features:
- The app can now automatically turn on Do Not Disturb mode when a focus timer is running (this needs to be enabled in Settings)
And other fixes.
This update also contains a new paywall system (For Google Play only)

View File

@@ -0,0 +1,12 @@
<i>टोमैटो</i> एंड्रॉइड के लिए मटेरियल 3 एक्सप्रेसिव पर आधारित एक न्यूनतम पोमोडोरो टाइमर है।
टोमैटो पूरी तरह से मुफ़्त और ओपन-सोर्स है, हमेशा के लिए। आप https://github.com/nsh07/Tomato पर सोर्स कोड पा सकते हैं, बग रिपोर्ट कर सकते हैं या सुविधाएँ सुझा सकते हैं।
<b>विशेषताएँ:</b>
- नवीनतम मटेरियल 3 एक्सप्रेसिव दिशानिर्देशों पर आधारित सरल, न्यूनतम UI
- समझने में आसान तरीके से काम/अध्ययन समय के विस्तृत आँकड़े
- एक नज़र में वर्तमान दिन के आँकड़े देखें
- पढ़ने में आसान, साफ़ ग्राफ़ में दिखाए गए पिछले हफ़्ते और पिछले महीने के आँकड़े देखें
- पिछले हफ़्ते और महीने के अतिरिक्त आँकड़े जो दिखाते हैं कि दिन के किस समय आप सबसे ज़्यादा उत्पादक होते हैं
- अनुकूलन योग्य टाइमर पैरामीटर
- एंड्रॉइड 16 लाइव अपडेट

View File

@@ -1,12 +1,12 @@
<i>Tomato</i> 是一个基于Material 3 Expressive的安卓极简主义番茄钟. <i>Tomato</i> 是一个基于Material 3 Expressive 的安卓极简主义番茄钟
Tomato 将永远保持完全免费和开源。如果你想获取源代码、报告程序错误bug或建议新功能请访问 https://github.com/nsh07/Tomato Tomato 将永远保持完全免费和开源。如果你想获取源代码、报告程序错误bug或建议新功能请访问 https://github.com/nsh07/Tomato
<b>功能:</b> <b>功能:</b>
- 基于最新Material 3 Expressive指南的简洁用户界面 - 基于最新Material 3 Expressive指南的简洁用户界面
- 以便于理解的方式提供工作/学习的详细统计数据 - 以便于理解的方式提供工作/学习时间的详细统计数据
- 当日统计数据一目了然 - 当日统计数据一目了然
- 清楚易读的上周和上月统计图表 - 清楚易读的上周和上月统计图表
- 上周和上月的额外统计数据帮您找到一天中最高效的时间段 - 上周和上月的额外统计数据帮您找到一天中最高效的时间段
- 可自定义的计时器参数 - 可自定义的计时器参数
- 支持 Android 16 即更新 Android 16 Live Updates - 支持 Android 16 即更新 Android 16 Live Updates

View File

@@ -0,0 +1,12 @@
<i>Tomato</i> 是一款基於 Material 3 Expressive 的 Android 極簡番茄鐘計時器。
Tomato 完全免費且開源,永遠如此。你可以在 https://github.com/nsh07/Tomato 取得原始碼,並回報問題或提出功能建議。
<b>功能:</b>
- 依循最新 Material 3 Expressive 設計規範的簡潔、極簡介面
- 以易懂方式呈現工作/讀書時間的詳細統計
- 當日統計一目了然
- 過去一週與一個月的統計,以清爽易讀的圖表呈現
- 另外提供過去一週與一個月在一天中何時最有效率的統計
- 計時器參數可自訂
- 支援 Android 16 的 Live Updates

View File

@@ -0,0 +1 @@
極簡番茄鐘

View File

@@ -12,6 +12,7 @@ ksp = "2.2.20-2.0.4"
lifecycleRuntimeKtx = "2.9.4" lifecycleRuntimeKtx = "2.9.4"
materialKolor = "3.0.1" materialKolor = "3.0.1"
navigation3 = "1.0.0-beta01" navigation3 = "1.0.0-beta01"
revenuecat = "9.12.0"
room = "2.8.3" room = "2.8.3"
vico = "2.2.1" vico = "2.2.1"
@@ -39,6 +40,8 @@ androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
junit = { group = "junit", name = "junit", version.ref = "junit" } junit = { group = "junit", name = "junit", version.ref = "junit" }
material-kolor = { module = "com.materialkolor:material-kolor", version.ref = "materialKolor" } material-kolor = { module = "com.materialkolor:material-kolor", version.ref = "materialKolor" }
revenuecat-purchases = { module = "com.revenuecat.purchases:purchases", version.ref = "revenuecat" }
revenuecat-purchases-ui = { module = "com.revenuecat.purchases:purchases-ui", version.ref = "revenuecat" }
vico-compose-m3 = { group = "com.patrykandpatrick.vico", name = "compose-m3", version.ref = "vico" } vico-compose-m3 = { group = "com.patrykandpatrick.vico", name = "compose-m3", version.ref = "vico" }
[plugins] [plugins]