diff --git a/.github/repo_photos/banner.png b/.github/repo_photos/banner.png
deleted file mode 100644
index 9095115..0000000
Binary files a/.github/repo_photos/banner.png and /dev/null differ
diff --git a/.github/repo_photos/googleplay.png b/.github/repo_photos/googleplay.png
new file mode 100644
index 0000000..4886f07
Binary files /dev/null and b/.github/repo_photos/googleplay.png differ
diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml
index e21b4fc..7661929 100644
--- a/.github/workflows/android.yml
+++ b/.github/workflows/android.yml
@@ -19,13 +19,13 @@ jobs:
run: chmod +x gradlew
- name: Build debug APK with Gradle
- run: ./gradlew assembleDebug
+ run: ./gradlew assembleFossDebug
- name: Run tests
- run: ./gradlew testDebugUnitTest
+ run: ./gradlew testFossDebugUnitTest
- name: Upload debug APK artifact
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v5
with:
name: tomato-debug
path: ./app/build/outputs/apk/debug/app-debug.apk
diff --git a/README.md b/README.md
index 1562443..3e85865 100644
--- a/README.md
+++ b/README.md
@@ -7,9 +7,6 @@ Tomato is a minimalist Pomodoro timer for Android based on Material 3 Expressive
-> [!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.
-
@@ -24,15 +21,18 @@ Tomato is a minimalist Pomodoro timer for Android based on Material 3 Expressive
-

+
-
-
+
+
+
+
+
@@ -48,9 +48,11 @@ Tomato is a minimalist Pomodoro timer for Android based on Material 3 Expressive
-> *"... 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/)
@@ -92,23 +94,20 @@ translating this project into languages you know.
## Download
-- **Google Play Store** (recommended): Tomato is available for closed testing on Google Play. Use
- [this link to join the testers Google Group](https://groups.google.com/g/nsh07-app-testers), then
- [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.
+- **Google Play Store** (recommended): Tomato will soon be available (currently in closed testing)
+ on the Google Play Store.
+ [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
- your preferred F-Droid app and search for Tomato.
- Updates on F-Droid are generally a week late. To get faster updates, you can install it through
+ your preferred F-Droid app and search for Tomato. Updates on F-Droid are generally a week late. To
+ get faster updates, you can install it through
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
- the [Releases](https://github.com/nsh07/Tomato/releases/latest) section of this repo (This
- method is not recommended, use Obtainium instead).
+ the [Releases](https://github.com/nsh07/Tomato/releases/latest) section of this repo (This method
+ is not recommended, use Google Play/F-Droid instead).
> [!TIP]
> 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
> 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
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 139e2d9..713c0f0 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -43,8 +43,8 @@ android {
applicationId = "org.nsh07.pomodoro"
minSdk = 27
targetSdk = 36
- versionCode = 15
- versionName = "1.6.0"
+ versionCode = 17
+ versionName = "1.6.2"
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 {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
@@ -103,6 +116,9 @@ dependencies {
implementation(libs.androidx.room.ktx)
ksp(libs.androidx.room.compiler)
+ "playImplementation"(libs.revenuecat.purchases)
+ "playImplementation"(libs.revenuecat.purchases.ui)
+
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
diff --git a/app/src/foss/java/org/nsh07/pomodoro/billing/FossBillingManager.kt b/app/src/foss/java/org/nsh07/pomodoro/billing/FossBillingManager.kt
new file mode 100644
index 0000000..529b58c
--- /dev/null
+++ b/app/src/foss/java/org/nsh07/pomodoro/billing/FossBillingManager.kt
@@ -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 .
+ */
+
+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()
+}
\ No newline at end of file
diff --git a/app/src/foss/java/org/nsh07/pomodoro/billing/TomatoPlusPaywallDialog.kt b/app/src/foss/java/org/nsh07/pomodoro/billing/TomatoPlusPaywallDialog.kt
new file mode 100644
index 0000000..6df4870
--- /dev/null
+++ b/app/src/foss/java/org/nsh07/pomodoro/billing/TomatoPlusPaywallDialog.kt
@@ -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 .
+ */
+
+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")
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/foss/java/org/nsh07/pomodoro/billing/initializePurchases.kt b/app/src/foss/java/org/nsh07/pomodoro/billing/initializePurchases.kt
new file mode 100644
index 0000000..7a44002
--- /dev/null
+++ b/app/src/foss/java/org/nsh07/pomodoro/billing/initializePurchases.kt
@@ -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 .
+ */
+
+package org.nsh07.pomodoro.billing
+
+import android.content.Context
+
+fun initializePurchases(context: Context) {}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 3f3f018..62a7ee8 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt b/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt
index a129af6..1917159 100644
--- a/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt
+++ b/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt
@@ -30,12 +30,10 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.nsh07.pomodoro.ui.AppScreen
import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsViewModel
import org.nsh07.pomodoro.ui.theme.TomatoTheme
-import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerViewModel
import org.nsh07.pomodoro.utils.toColor
class MainActivity : ComponentActivity() {
- private val timerViewModel: TimerViewModel by viewModels(factoryProducer = { TimerViewModel.Factory })
private val settingsViewModel: SettingsViewModel by viewModels(factoryProducer = { SettingsViewModel.Factory })
private val appContainer by lazy {
@@ -62,6 +60,20 @@ class MainActivity : ComponentActivity() {
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(
darkTheme = darkTheme,
seedColor = seed,
@@ -73,7 +85,7 @@ class MainActivity : ComponentActivity() {
}
AppScreen(
- timerViewModel = timerViewModel,
+ isPlus = isPlus,
isAODEnabled = preferencesState.aodEnabled,
setTimerFrequency = {
appContainer.appTimerRepository.timerFrequency = it
diff --git a/app/src/main/java/org/nsh07/pomodoro/TomatoApplication.kt b/app/src/main/java/org/nsh07/pomodoro/TomatoApplication.kt
index 87be588..47de7fe 100644
--- a/app/src/main/java/org/nsh07/pomodoro/TomatoApplication.kt
+++ b/app/src/main/java/org/nsh07/pomodoro/TomatoApplication.kt
@@ -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 .
+ */
+
package org.nsh07.pomodoro
import android.app.Application
import android.app.NotificationChannel
import android.app.NotificationManager
+import org.nsh07.pomodoro.billing.initializePurchases
import org.nsh07.pomodoro.data.AppContainer
import org.nsh07.pomodoro.data.DefaultAppContainer
@@ -12,6 +30,8 @@ class TomatoApplication : Application() {
super.onCreate()
container = DefaultAppContainer(this)
+ initializePurchases(this)
+
val notificationChannel = NotificationChannel(
"timer",
getString(R.string.timer_progress),
diff --git a/app/src/main/java/org/nsh07/pomodoro/billing/BillingManager.kt b/app/src/main/java/org/nsh07/pomodoro/billing/BillingManager.kt
new file mode 100644
index 0000000..4cdc106
--- /dev/null
+++ b/app/src/main/java/org/nsh07/pomodoro/billing/BillingManager.kt
@@ -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 .
+ */
+
+package org.nsh07.pomodoro.billing
+
+import kotlinx.coroutines.flow.StateFlow
+
+interface BillingManager {
+ val isPlus: StateFlow
+ val isLoaded: StateFlow
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/nsh07/pomodoro/data/AppContainer.kt b/app/src/main/java/org/nsh07/pomodoro/data/AppContainer.kt
index f36a179..bf445c8 100644
--- a/app/src/main/java/org/nsh07/pomodoro/data/AppContainer.kt
+++ b/app/src/main/java/org/nsh07/pomodoro/data/AppContainer.kt
@@ -17,6 +17,7 @@
package org.nsh07.pomodoro.data
+import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import androidx.compose.ui.graphics.Color
@@ -25,6 +26,8 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import kotlinx.coroutines.flow.MutableStateFlow
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.ui.timerScreen.viewModel.TimerState
import org.nsh07.pomodoro.utils.millisecondsToStr
@@ -33,7 +36,9 @@ interface AppContainer {
val appPreferenceRepository: AppPreferenceRepository
val appStatRepository: AppStatRepository
val appTimerRepository: AppTimerRepository
+ val billingManager: BillingManager
val notificationManager: NotificationManagerCompat
+ val notificationManagerService: NotificationManager
val notificationBuilder: NotificationCompat.Builder
val timerState: MutableStateFlow
val time: MutableStateFlow
@@ -52,10 +57,15 @@ class DefaultAppContainer(context: Context) : AppContainer {
override val appTimerRepository: AppTimerRepository by lazy { AppTimerRepository() }
+ override val billingManager: BillingManager by lazy { BillingManagerProvider.manager }
+
override val notificationManager: NotificationManagerCompat by lazy {
NotificationManagerCompat.from(context)
}
+ override val notificationManagerService: NotificationManager =
+ context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+
override val notificationBuilder: NotificationCompat.Builder by lazy {
NotificationCompat.Builder(context, "timer")
.setSmallIcon(R.drawable.tomato_logo_notification)
diff --git a/app/src/main/java/org/nsh07/pomodoro/data/TimerRepository.kt b/app/src/main/java/org/nsh07/pomodoro/data/TimerRepository.kt
index a7a7b46..788d1af 100644
--- a/app/src/main/java/org/nsh07/pomodoro/data/TimerRepository.kt
+++ b/app/src/main/java/org/nsh07/pomodoro/data/TimerRepository.kt
@@ -1,8 +1,18 @@
/*
* Copyright (c) 2025 Nishant Mishra
*
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
+ * 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 .
*/
package org.nsh07.pomodoro.data
@@ -27,6 +37,7 @@ interface TimerRepository {
var alarmEnabled: Boolean
var vibrateEnabled: Boolean
+ var dndEnabled: Boolean
var colorScheme: ColorScheme
@@ -46,6 +57,7 @@ class AppTimerRepository : TimerRepository {
override var timerFrequency: Float = 10f
override var alarmEnabled = true
override var vibrateEnabled = true
+ override var dndEnabled: Boolean = false
override var colorScheme = lightColorScheme()
override var alarmSoundUri: Uri? =
Settings.System.DEFAULT_ALARM_ALERT_URI ?: Settings.System.DEFAULT_RINGTONE_URI
diff --git a/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt b/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt
index b2423d1..8932c50 100644
--- a/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt
+++ b/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt
@@ -18,6 +18,7 @@
package org.nsh07.pomodoro.service
import android.annotation.SuppressLint
+import android.app.NotificationManager
import android.app.Service
import android.content.Intent
import android.media.AudioAttributes
@@ -30,7 +31,6 @@ import android.os.Vibrator
import android.os.VibratorManager
import androidx.compose.ui.graphics.toArgb
import androidx.core.app.NotificationCompat
-import androidx.core.app.NotificationManagerCompat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -53,7 +53,8 @@ class TimerService : Service() {
private val timerRepository by lazy { appContainer.appTimerRepository }
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 _timerState by lazy { appContainer.timerState }
private val _time by lazy { appContainer.time }
@@ -106,6 +107,7 @@ class TimerService : Service() {
runBlocking {
job.cancel()
saveTimeToDb()
+ setDoNotDisturb(false)
notificationManager.cancel(1)
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()
@@ -140,6 +142,7 @@ class TimerService : Service() {
updateProgressSegments()
if (timerState.value.timerRunning) {
+ setDoNotDisturb(false)
notificationBuilder.clearActions().addTimerActions(
this, R.drawable.play, getString(R.string.start)
)
@@ -149,6 +152,8 @@ class TimerService : Service() {
}
pauseTime = SystemClock.elapsedRealtime()
} else {
+ if (timerState.value.timerMode == TimerMode.FOCUS) setDoNotDisturb(true)
+ else setDoNotDisturb(false)
notificationBuilder.clearActions().addTimerActions(
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()
- skipScope.launch {
- saveTimeToDb()
- updateProgressSegments()
- showTimerNotification(0, paused = true, complete = !fromButton)
- startTime = 0L
- pauseTime = 0L
- pauseDuration = 0L
+ saveTimeToDb()
+ updateProgressSegments()
+ showTimerNotification(0, paused = true, complete = !fromButton)
+ startTime = 0L
+ pauseTime = 0L
+ pauseDuration = 0L
- cycles = (cycles + 1) % (timerRepository.sessionLength * 2)
+ cycles = (cycles + 1) % (timerRepository.sessionLength * 2)
- if (cycles % 2 == 0) {
- time = timerRepository.focusTime
- _timerState.update { currentState ->
- currentState.copy(
- timerMode = TimerMode.FOCUS,
- timeStr = millisecondsToStr(time),
- totalTime = time,
- nextTimerMode = if (cycles == (timerRepository.sessionLength - 1) * 2) TimerMode.LONG_BREAK else TimerMode.SHORT_BREAK,
- nextTimeStr = if (cycles == (timerRepository.sessionLength - 1) * 2) millisecondsToStr(
- timerRepository.longBreakTime
- ) else millisecondsToStr(
- timerRepository.shortBreakTime
- ),
- currentFocusCount = cycles / 2 + 1,
- totalFocusCount = timerRepository.sessionLength
- )
- }
- } else {
- val long = cycles == (timerRepository.sessionLength * 2) - 1
- time = if (long) timerRepository.longBreakTime else timerRepository.shortBreakTime
+ if (cycles % 2 == 0) {
+ if (timerState.value.timerRunning) setDoNotDisturb(true)
+ time = timerRepository.focusTime
+ _timerState.update { currentState ->
+ currentState.copy(
+ timerMode = TimerMode.FOCUS,
+ timeStr = millisecondsToStr(time),
+ totalTime = time,
+ nextTimerMode = if (cycles == (timerRepository.sessionLength - 1) * 2) TimerMode.LONG_BREAK else TimerMode.SHORT_BREAK,
+ nextTimeStr = if (cycles == (timerRepository.sessionLength - 1) * 2) millisecondsToStr(
+ timerRepository.longBreakTime
+ ) else millisecondsToStr(
+ timerRepository.shortBreakTime
+ ),
+ currentFocusCount = cycles / 2 + 1,
+ totalFocusCount = timerRepository.sessionLength
+ )
+ }
+ } else {
+ if (timerState.value.timerRunning) setDoNotDisturb(false)
+ val long = cycles == (timerRepository.sessionLength * 2) - 1
+ time = if (long) timerRepository.longBreakTime else timerRepository.shortBreakTime
- _timerState.update { currentState ->
- currentState.copy(
- timerMode = if (long) TimerMode.LONG_BREAK else TimerMode.SHORT_BREAK,
- timeStr = millisecondsToStr(time),
- totalTime = time,
- nextTimerMode = TimerMode.FOCUS,
- nextTimeStr = millisecondsToStr(timerRepository.focusTime)
- )
- }
+ _timerState.update { currentState ->
+ currentState.copy(
+ timerMode = if (long) TimerMode.LONG_BREAK else TimerMode.SHORT_BREAK,
+ timeStr = millisecondsToStr(time),
+ totalTime = time,
+ nextTimerMode = TimerMode.FOCUS,
+ 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 = initializeMediaPlayer()
}
diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt b/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt
index 905e377..16a539b 100644
--- a/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt
+++ b/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt
@@ -23,6 +23,8 @@ import androidx.compose.animation.Crossfade
import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
+import androidx.compose.animation.slideInVertically
+import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.calculateEndPadding
@@ -41,8 +43,10 @@ import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLayoutDirection
@@ -54,6 +58,7 @@ import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.ui.NavDisplay
import androidx.window.core.layout.WindowSizeClass
+import org.nsh07.pomodoro.billing.TomatoPlusPaywallDialog
import org.nsh07.pomodoro.service.TimerService
import org.nsh07.pomodoro.ui.settingsScreen.SettingsScreenRoot
import org.nsh07.pomodoro.ui.statsScreen.StatsScreenRoot
@@ -65,10 +70,11 @@ import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerViewModel
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun AppScreen(
- modifier: Modifier = Modifier,
- timerViewModel: TimerViewModel = viewModel(factory = TimerViewModel.Factory),
isAODEnabled: Boolean,
- setTimerFrequency: (Float) -> Unit
+ isPlus: Boolean,
+ setTimerFrequency: (Float) -> Unit,
+ modifier: Modifier = Modifier,
+ timerViewModel: TimerViewModel = viewModel(factory = TimerViewModel.Factory)
) {
val context = LocalContext.current
@@ -91,6 +97,7 @@ fun AppScreen(
}
}
+ var showPaywall by remember { mutableStateOf(false) }
Scaffold(
bottomBar = {
@@ -157,6 +164,7 @@ fun AppScreen(
entry {
TimerScreen(
timerState = uiState,
+ isPlus = isPlus,
progress = { progress },
onAction = { action ->
when (action) {
@@ -218,6 +226,7 @@ fun AppScreen(
entry {
SettingsScreenRoot(
+ setShowPaywall = { showPaywall = it },
modifier = modifier.padding(
start = contentPadding.calculateStartPadding(layoutDirection),
end = contentPadding.calculateEndPadding(layoutDirection),
@@ -240,4 +249,12 @@ fun AppScreen(
)
}
}
+
+ AnimatedVisibility(
+ showPaywall,
+ enter = slideInVertically { it },
+ exit = slideOutVertically { it }
+ ) {
+ TomatoPlusPaywallDialog(isPlus = isPlus) { showPaywall = false }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt
index 512df9a..2f8d2dd 100644
--- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt
+++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt
@@ -17,6 +17,7 @@
package org.nsh07.pomodoro.ui.settingsScreen
+import android.annotation.SuppressLint
import android.content.Intent
import android.net.Uri
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.settingsScreen.components.AboutCard
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.AppearanceSettings
import org.nsh07.pomodoro.ui.settingsScreen.screens.TimerSettings
@@ -80,6 +82,7 @@ import org.nsh07.pomodoro.ui.theme.CustomColors.topBarColors
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreenRoot(
+ setShowPaywall: (Boolean) -> Unit,
modifier: Modifier = Modifier,
viewModel: SettingsViewModel = viewModel(factory = SettingsViewModel.Factory)
) {
@@ -102,8 +105,10 @@ fun SettingsScreenRoot(
viewModel.longBreakTimeTextFieldState
}
+ val isPlus by viewModel.isPlus.collectAsStateWithLifecycle()
val alarmEnabled by viewModel.alarmEnabled.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 preferencesState by viewModel.preferencesState.collectAsStateWithLifecycle()
@@ -118,6 +123,7 @@ fun SettingsScreenRoot(
}
SettingsScreen(
+ isPlus = isPlus,
preferencesState = preferencesState,
backStack = backStack,
focusTimeInputFieldState = focusTimeInputFieldState,
@@ -126,11 +132,13 @@ fun SettingsScreenRoot(
sessionsSliderState = sessionsSliderState,
alarmEnabled = alarmEnabled,
vibrateEnabled = vibrateEnabled,
+ dndEnabled = dndEnabled,
alarmSound = alarmSound,
onAlarmEnabledChange = viewModel::saveAlarmEnabled,
onVibrateEnabledChange = viewModel::saveVibrateEnabled,
onBlackThemeChange = viewModel::saveBlackTheme,
onAodEnabledChange = viewModel::saveAodEnabled,
+ onDndEnabledChange = viewModel::saveDndEnabled,
onAlarmSoundChanged = {
viewModel.saveAlarmSound(it)
Intent(context, TimerService::class.java).apply {
@@ -140,13 +148,16 @@ fun SettingsScreenRoot(
},
onThemeChange = viewModel::saveTheme,
onColorSchemeChange = viewModel::saveColorScheme,
+ setShowPaywall = setShowPaywall,
modifier = modifier
)
}
+@SuppressLint("LocalContextGetResourceValueCall")
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
private fun SettingsScreen(
+ isPlus: Boolean,
preferencesState: PreferencesState,
backStack: SnapshotStateList,
focusTimeInputFieldState: TextFieldState,
@@ -155,14 +166,17 @@ private fun SettingsScreen(
sessionsSliderState: SliderState,
alarmEnabled: Boolean,
vibrateEnabled: Boolean,
+ dndEnabled: Boolean,
alarmSound: String,
onAlarmEnabledChange: (Boolean) -> Unit,
onVibrateEnabledChange: (Boolean) -> Unit,
onBlackThemeChange: (Boolean) -> Unit,
onAodEnabledChange: (Boolean) -> Unit,
+ onDndEnabledChange: (Boolean) -> Unit,
onAlarmSoundChanged: (Uri?) -> Unit,
onThemeChange: (String) -> Unit,
onColorSchemeChange: (Color) -> Unit,
+ setShowPaywall: (Boolean) -> Unit,
modifier: Modifier = Modifier
) {
val context = LocalContext.current
@@ -212,10 +226,20 @@ private fun SettingsScreen(
) {
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)) }
+ if (isPlus) item {
+ PlusPromo(isPlus, setShowPaywall)
+ Spacer(Modifier.height(14.dp))
+ }
+
itemsIndexed(settingsScreens) { index, item ->
ClickableListItem(
leadingContent = {
@@ -261,21 +285,27 @@ private fun SettingsScreen(
entry {
AppearanceSettings(
preferencesState = preferencesState,
+ isPlus = isPlus,
onBlackThemeChange = onBlackThemeChange,
onThemeChange = onThemeChange,
onColorSchemeChange = onColorSchemeChange,
+ setShowPaywall = setShowPaywall,
onBack = backStack::removeLastOrNull
)
}
entry {
TimerSettings(
+ isPlus = isPlus,
aodEnabled = preferencesState.aodEnabled,
+ dndEnabled = dndEnabled,
focusTimeInputFieldState = focusTimeInputFieldState,
shortBreakTimeInputFieldState = shortBreakTimeInputFieldState,
longBreakTimeInputFieldState = longBreakTimeInputFieldState,
sessionsSliderState = sessionsSliderState,
onAodEnabledChange = onAodEnabledChange,
- onBack = backStack::removeLastOrNull
+ onDndEnabledChange = onDndEnabledChange,
+ setShowPaywall = setShowPaywall,
+ onBack = backStack::removeLastOrNull,
)
}
}
diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/AboutCard.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/AboutCard.kt
index fe0e1ce..1f85c76 100644
--- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/AboutCard.kt
+++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/AboutCard.kt
@@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
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
@Composable
-fun AboutCard(modifier: Modifier = Modifier) {
+fun AboutCard(
+ isPlus: Boolean,
+ modifier: Modifier = Modifier
+) {
val uriHandler = LocalUriHandler.current
val context = LocalContext.current
@@ -76,7 +80,8 @@ fun AboutCard(modifier: Modifier = Modifier) {
) {
Column {
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,
fontFamily = robotoFlexTopBar
)
@@ -123,8 +128,9 @@ fun AboutCard(modifier: Modifier = Modifier) {
verticalAlignment = Alignment.CenterVertically
) {
Icon(
- painterResource(R.drawable.coffee),
+ painterResource(R.drawable.bmc),
contentDescription = "Buy me a coffee",
+ modifier = Modifier.height(24.dp)
)
Text(text = "Buy me a coffee")
diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ColorSchemePickerListItem.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ColorSchemePickerListItem.kt
index 03549b3..095ffec 100644
--- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ColorSchemePickerListItem.kt
+++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ColorSchemePickerListItem.kt
@@ -17,9 +17,9 @@
package org.nsh07.pomodoro.ui.settingsScreen.components
+import android.os.Build
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
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.lazy.LazyRow
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.Icon
import androidx.compose.material3.IconButton
@@ -38,6 +40,7 @@ import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
@@ -56,6 +59,7 @@ fun ColorSchemePickerListItem(
color: Color,
items: Int,
index: Int,
+ isPlus: Boolean,
onColorChange: (Color) -> Unit,
modifier: Modifier = Modifier
) {
@@ -65,6 +69,7 @@ fun ColorSchemePickerListItem(
Color(0xff9fd75c), Color(0xffc1d02d), Color(0xfffabd00), Color(0xffffb86e),
Color.White
)
+ val zeroCorner = remember { CornerSize(0) }
Column(
modifier
@@ -76,45 +81,49 @@ fun ColorSchemePickerListItem(
}
)
) {
- ListItem(
- leadingContent = {
- Icon(
- painterResource(R.drawable.colors),
- null
- )
- },
- headlineContent = { Text("Dynamic color") },
- supportingContent = { Text("Adapt theme colors from your wallpaper") },
- trailingContent = {
- val checked = color == colorSchemes.last()
- Switch(
- checked = checked,
- onCheckedChange = {
- if (it) onColorChange(colorSchemes.last())
- else onColorChange(colorSchemes.first())
- },
- thumbContent = {
- if (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(middleListItemShape)
- )
- Spacer(Modifier.height(2.dp))
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ ListItem(
+ leadingContent = {
+ Icon(
+ painterResource(R.drawable.colors),
+ null
+ )
+ },
+ headlineContent = { Text(stringResource(R.string.dynamic_color)) },
+ supportingContent = { Text(stringResource(R.string.dynamic_color_desc)) },
+ trailingContent = {
+ val checked = color == colorSchemes.last()
+ Switch(
+ checked = checked,
+ onCheckedChange = {
+ if (it) onColorChange(colorSchemes.last())
+ else onColorChange(colorSchemes.first())
+ },
+ enabled = isPlus,
+ thumbContent = {
+ if (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(middleListItemShape)
+ )
+ Spacer(Modifier.height(2.dp))
+ }
+
ListItem(
leadingContent = {
Icon(
@@ -131,24 +140,31 @@ fun ColorSchemePickerListItem(
)
},
colors = listItemColors,
- modifier = Modifier.clip(middleListItemShape)
+ modifier = Modifier.clip(
+ RoundedCornerShape(
+ topStart = middleListItemShape.topStart,
+ topEnd = middleListItemShape.topEnd,
+ zeroCorner,
+ zeroCorner
+ )
+ )
)
- Column(
- verticalArrangement = Arrangement.spacedBy(8.dp),
+ LazyRow(
+ contentPadding = PaddingValues(horizontal = 48.dp),
+ userScrollEnabled = isPlus,
modifier = Modifier
.background(listItemColors.containerColor)
.padding(bottom = 8.dp)
) {
- LazyRow(contentPadding = PaddingValues(horizontal = 48.dp)) {
- items(colorSchemes.dropLast(1)) {
- ColorPickerButton(
- it,
- it == color,
- modifier = Modifier.padding(4.dp)
- ) {
- onColorChange(it)
- }
+ items(colorSchemes.dropLast(1)) {
+ ColorPickerButton(
+ color = it,
+ isSelected = it == color,
+ enabled = isPlus,
+ modifier = Modifier.padding(4.dp)
+ ) {
+ onColorChange(it)
}
}
}
@@ -160,12 +176,17 @@ fun ColorSchemePickerListItem(
fun ColorPickerButton(
color: Color,
isSelected: Boolean,
+ enabled: Boolean,
modifier: Modifier = Modifier,
onClick: () -> Unit
) {
IconButton(
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),
onClick = onClick
) {
diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/PlusDivider.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/PlusDivider.kt
new file mode 100644
index 0000000..e950564
--- /dev/null
+++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/PlusDivider.kt
@@ -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 .
+ */
+
+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)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/PlusPromo.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/PlusPromo.kt
new file mode 100644
index 0000000..858dbdb
--- /dev/null
+++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/PlusPromo.kt
@@ -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 .
+ */
+
+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
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ThemePickerListItem.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ThemePickerListItem.kt
index 0392765..411de79 100644
--- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ThemePickerListItem.kt
+++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ThemePickerListItem.kt
@@ -43,6 +43,7 @@ import androidx.compose.ui.unit.dp
import org.nsh07.pomodoro.R
import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
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.topListItemShape
@@ -69,11 +70,13 @@ fun ThemePickerListItem(
Column(
modifier
.clip(
- when (index) {
- 0 -> topListItemShape
- items - 1 -> bottomListItemShape
- else -> middleListItemShape
- },
+ if (items > 1)
+ when (index) {
+ 0 -> topListItemShape
+ items - 1 -> bottomListItemShape
+ else -> middleListItemShape
+ }
+ else cardShape,
),
) {
ListItem(
diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AppearanceSettings.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AppearanceSettings.kt
index eb55fb8..d5a12ff 100644
--- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AppearanceSettings.kt
+++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AppearanceSettings.kt
@@ -48,6 +48,7 @@ import androidx.compose.ui.unit.dp
import org.nsh07.pomodoro.R
import org.nsh07.pomodoro.ui.settingsScreen.SettingsSwitchItem
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.viewModel.PreferencesState
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.topBarColors
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
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun AppearanceSettings(
preferencesState: PreferencesState,
+ isPlus: Boolean,
onBlackThemeChange: (Boolean) -> Unit,
onThemeChange: (String) -> Unit,
onColorSchemeChange: (Color) -> Unit,
+ setShowPaywall: (Boolean) -> Unit,
onBack: () -> Unit,
modifier: Modifier = Modifier
) {
@@ -100,22 +103,26 @@ fun AppearanceSettings(
item {
Spacer(Modifier.height(14.dp))
}
- item {
- ColorSchemePickerListItem(
- color = preferencesState.colorScheme.toColor(),
- items = 3,
- index = 0,
- onColorChange = onColorSchemeChange
- )
- }
item {
ThemePickerListItem(
theme = preferencesState.theme,
onThemeChange = onThemeChange,
+ items = if (isPlus) 3 else 1,
+ index = 0
+ )
+ }
+
+ if (!isPlus) {
+ item { PlusDivider(setShowPaywall) }
+ }
+
+ item {
+ ColorSchemePickerListItem(
+ color = preferencesState.colorScheme.toColor(),
items = 3,
- index = 1,
- modifier = Modifier
- .clip(middleListItemShape)
+ index = if (isPlus) 1 else 0,
+ isPlus = isPlus,
+ onColorChange = onColorSchemeChange,
)
}
item {
@@ -136,6 +143,7 @@ fun AppearanceSettings(
Switch(
checked = item.checked,
onCheckedChange = { item.onClick(it) },
+ enabled = isPlus,
thumbContent = {
if (item.checked) {
Icon(
@@ -168,11 +176,15 @@ fun AppearanceSettings(
@Composable
fun AppearanceSettingsPreview() {
val preferencesState = PreferencesState()
- AppearanceSettings(
- preferencesState = preferencesState,
- onBlackThemeChange = {},
- onThemeChange = {},
- onColorSchemeChange = {},
- onBack = {}
- )
+ TomatoTheme(dynamicColor = false) {
+ AppearanceSettings(
+ preferencesState = preferencesState,
+ isPlus = false,
+ onBlackThemeChange = {},
+ onThemeChange = {},
+ onColorSchemeChange = {},
+ setShowPaywall = {},
+ onBack = {}
+ )
+ }
}
diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/TimerSettings.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/TimerSettings.kt
index 01dddba..b0c7ea3 100644
--- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/TimerSettings.kt
+++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/TimerSettings.kt
@@ -17,6 +17,11 @@
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.foundation.background
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.width
import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
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.TopAppBarDefaults
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -59,6 +66,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
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.ui.settingsScreen.SettingsSwitchItem
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.CustomColors.listItemColors
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.topListItemShape
+
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun TimerSettings(
+ isPlus: Boolean,
aodEnabled: Boolean,
+ dndEnabled: Boolean,
focusTimeInputFieldState: TextFieldState,
shortBreakTimeInputFieldState: TextFieldState,
longBreakTimeInputFieldState: TextFieldState,
sessionsSliderState: SliderState,
onAodEnabledChange: (Boolean) -> Unit,
+ onDndEnabledChange: (Boolean) -> Unit,
onBack: () -> Unit,
- modifier: Modifier = Modifier
+ modifier: Modifier = Modifier,
+ setShowPaywall: (Boolean) -> Unit
) {
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)) {
LargeFlexibleTopAppBar(
@@ -213,14 +263,8 @@ fun TimerSettings(
)
}
item { Spacer(Modifier.height(12.dp)) }
- item {
- val item = SettingsSwitchItem(
- checked = aodEnabled,
- icon = R.drawable.aod,
- label = R.string.always_on_display,
- description = R.string.always_on_display_desc,
- onClick = onAodEnabledChange
- )
+
+ itemsIndexed(if (isPlus) switchItems else switchItems.take(1)) { index, item ->
ListItem(
leadingContent = {
Icon(
@@ -254,10 +298,61 @@ fun TimerSettings(
)
},
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 {
var expanded by remember { mutableStateOf(false) }
Column(
@@ -306,12 +401,16 @@ private fun TimerSettingsPreview() {
steps = 6
)
TimerSettings(
+ isPlus = false,
+ aodEnabled = true,
+ dndEnabled = false,
focusTimeInputFieldState = focusTimeInputFieldState,
shortBreakTimeInputFieldState = shortBreakTimeInputFieldState,
longBreakTimeInputFieldState = longBreakTimeInputFieldState,
sessionsSliderState = sessionsSliderState,
- aodEnabled = true,
- onBack = {},
- onAodEnabledChange = {}
+ onAodEnabledChange = {},
+ onDndEnabledChange = {},
+ setShowPaywall = {},
+ onBack = {}
)
}
\ No newline at end of file
diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt
index 76ebb8c..94ba2c6 100644
--- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt
+++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt
@@ -40,17 +40,25 @@ import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.nsh07.pomodoro.TomatoApplication
+import org.nsh07.pomodoro.billing.BillingManager
import org.nsh07.pomodoro.data.AppPreferenceRepository
import org.nsh07.pomodoro.data.TimerRepository
import org.nsh07.pomodoro.ui.Screen
@OptIn(FlowPreview::class, ExperimentalMaterial3Api::class)
class SettingsViewModel(
+ private val billingManager: BillingManager,
private val preferenceRepository: AppPreferenceRepository,
private val timerRepository: TimerRepository,
) : ViewModel() {
val backStack = mutableStateListOf(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())
val preferencesState = _preferencesState.asStateFlow()
@@ -85,26 +93,13 @@ class SettingsViewModel(
preferenceRepository.getBooleanPreferenceFlow("alarm_enabled").distinctUntilChanged()
val vibrateEnabled =
preferenceRepository.getBooleanPreferenceFlow("vibrate_enabled").distinctUntilChanged()
+ val dndEnabled =
+ preferenceRepository.getBooleanPreferenceFlow("dnd_enabled").distinctUntilChanged()
init {
viewModelScope.launch {
- 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
- )
- }
+ reloadSettings()
+ _isSettingsLoaded.value = true
}
}
@@ -179,6 +174,13 @@ class SettingsViewModel(
}
}
+ fun saveDndEnabled(enabled: Boolean) {
+ viewModelScope.launch {
+ timerRepository.dndEnabled = enabled
+ preferenceRepository.saveBooleanPreference("dnd_enabled", enabled)
+ }
+ }
+
fun saveAlarmSound(uri: Uri?) {
viewModelScope.launch {
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 {
val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer {
val application = (this[APPLICATION_KEY] as TomatoApplication)
val appPreferenceRepository = application.container.appPreferenceRepository
val appTimerRepository = application.container.appTimerRepository
+ val appBillingManager = application.container.billingManager
SettingsViewModel(
+ billingManager = appBillingManager,
preferenceRepository = appPreferenceRepository,
timerRepository = appTimerRepository,
)
diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/theme/Color.kt b/app/src/main/java/org/nsh07/pomodoro/ui/theme/Color.kt
index 064d7d8..d259777 100644
--- a/app/src/main/java/org/nsh07/pomodoro/ui/theme/Color.kt
+++ b/app/src/main/java/org/nsh07/pomodoro/ui/theme/Color.kt
@@ -28,13 +28,77 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
-val Purple80 = Color(0xFFD0BCFF)
-val PurpleGrey80 = Color(0xFFCCC2DC)
-val Pink80 = Color(0xFFEFB8C8)
+val primaryLight = Color(0xFF4C662B)
+val onPrimaryLight = Color(0xFFFFFFFF)
+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 PurpleGrey40 = Color(0xFF625b71)
-val Pink40 = Color(0xFF7D5260)
+val primaryDark = Color(0xFFB1D18A)
+val onPrimaryDark = Color(0xFF1F3701)
+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 {
var black = false
diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/theme/Theme.kt b/app/src/main/java/org/nsh07/pomodoro/ui/theme/Theme.kt
index da61be6..e5015f0 100644
--- a/app/src/main/java/org/nsh07/pomodoro/ui/theme/Theme.kt
+++ b/app/src/main/java/org/nsh07/pomodoro/ui/theme/Theme.kt
@@ -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 .
+ */
+
package org.nsh07.pomodoro.ui.theme
import android.app.Activity
@@ -19,26 +36,80 @@ import androidx.core.view.WindowCompat
import com.materialkolor.dynamiccolor.ColorSpec
import com.materialkolor.rememberDynamicColorScheme
-private val DarkColorScheme = darkColorScheme(
- primary = Purple80,
- secondary = PurpleGrey80,
- tertiary = Pink80
+private val lightScheme = lightColorScheme(
+ primary = primaryLight,
+ onPrimary = onPrimaryLight,
+ 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(
- primary = Purple40,
- secondary = PurpleGrey40,
- tertiary = Pink40
-
- /* Other default colors to override
- background = Color(0xFFFFFBFE),
- surface = Color(0xFFFFFBFE),
- onPrimary = Color.White,
- onSecondary = Color.White,
- onTertiary = Color.White,
- onBackground = Color(0xFF1C1B1F),
- onSurface = Color(0xFF1C1B1F),
- */
+private val darkScheme = darkColorScheme(
+ primary = primaryDark,
+ onPrimary = onPrimaryDark,
+ primaryContainer = primaryContainerDark,
+ onPrimaryContainer = onPrimaryContainerDark,
+ secondary = secondaryDark,
+ onSecondary = onSecondaryDark,
+ secondaryContainer = secondaryContainerDark,
+ onSecondaryContainer = onSecondaryContainerDark,
+ tertiary = tertiaryDark,
+ onTertiary = onTertiaryDark,
+ tertiaryContainer = tertiaryContainerDark,
+ 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)
@@ -56,8 +127,8 @@ fun TomatoTheme(
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
- darkTheme -> DarkColorScheme
- else -> LightColorScheme
+ darkTheme -> darkScheme
+ else -> lightScheme
}
val view = LocalView.current
diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/TimerScreen.kt b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/TimerScreen.kt
index 68b9094..a1a44f4 100644
--- a/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/TimerScreen.kt
+++ b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/TimerScreen.kt
@@ -1,8 +1,18 @@
/*
* Copyright (c) 2025 Nishant Mishra
*
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
+ * 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 .
*/
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.TimerState
+
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun SharedTransitionScope.TimerScreen(
timerState: TimerState,
+ isPlus: Boolean,
progress: () -> Float,
onAction: (TimerAction) -> Unit,
modifier: Modifier = Modifier
@@ -148,7 +160,8 @@ fun SharedTransitionScope.TimerScreen(
when (it) {
TimerMode.BRAND ->
Text(
- stringResource(R.string.app_name),
+ if (!isPlus) stringResource(R.string.app_name)
+ else stringResource(R.string.app_name_plus),
style = TextStyle(
fontFamily = robotoFlexTopBar,
fontSize = 32.sp,
@@ -541,6 +554,7 @@ fun TimerScreenPreview() {
SharedTransitionLayout {
TimerScreen(
timerState,
+ isPlus = true,
{ 0.3f },
{}
)
diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt
index 53c23b5..2c4e493 100644
--- a/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt
+++ b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt
@@ -1,8 +1,18 @@
/*
* Copyright (c) 2025 Nishant Mishra
*
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
+ * 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 .
*/
package org.nsh07.pomodoro.ui.timerScreen.viewModel
@@ -85,6 +95,9 @@ class TimerViewModel(
timerRepository.vibrateEnabled =
preferenceRepository.getBooleanPreference("vibrate_enabled")
?: preferenceRepository.saveBooleanPreference("vibrate_enabled", true)
+ timerRepository.dndEnabled =
+ preferenceRepository.getBooleanPreference("dnd_enabled")
+ ?: preferenceRepository.saveBooleanPreference("dnd_enabled", false)
timerRepository.alarmSoundUri = (
preferenceRepository.getStringPreference("alarm_sound")
diff --git a/app/src/main/res/drawable/bmc.xml b/app/src/main/res/drawable/bmc.xml
new file mode 100644
index 0000000..110fa4b
--- /dev/null
+++ b/app/src/main/res/drawable/bmc.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/coffee.xml b/app/src/main/res/drawable/coffee.xml
deleted file mode 100644
index 05b913c..0000000
--- a/app/src/main/res/drawable/coffee.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
diff --git a/app/src/main/res/drawable/dnd.xml b/app/src/main/res/drawable/dnd.xml
new file mode 100644
index 0000000..16113b4
--- /dev/null
+++ b/app/src/main/res/drawable/dnd.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/play_store.xml b/app/src/main/res/drawable/play_store.xml
index fc7ef76..51103b7 100644
--- a/app/src/main/res/drawable/play_store.xml
+++ b/app/src/main/res/drawable/play_store.xml
@@ -1,10 +1,27 @@
+
+
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ android:fillColor="#ffffff"
+ 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.599991" />
diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml
index 09780a8..23c0782 100644
--- a/app/src/main/res/values-ar/strings.xml
+++ b/app/src/main/res/values-ar/strings.xml
@@ -19,7 +19,7 @@
ديناميكي
لون
النظام (الافتراضي)
- منبه
+ المنبه
فاتح
داكن
اختر سمة
@@ -31,7 +31,7 @@
التنبيه عند انتهاء المؤقت
اهتزاز
الاهتزاز عندما ينتهي المؤقت
- سمة
+ السمة
الاعدادات
طول الجلسة
فترات التركيز في الجلسة الواحدة: %1$d
@@ -57,4 +57,9 @@
المؤقت
تقدم المؤقت
السنة الماضية
+ العرض الدائم علي الشاشة
+ اضغط في أي مكان أثناء عرض المؤقت للتبديل إلى وضع العرض الدائم على الشاشة
+ المظهر
+ الفترات
+ الصوت
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 3c08f8f..4f8636e 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -18,7 +18,7 @@
Farbschema
Dynamisch
Farbe
- Systemstandard
+ System
Alarm
Hell
Dunkel
@@ -56,4 +56,12 @@
Als nächstes
Timer
Timer-Fortschritt
+ Always-On Display
+ Tippe irgendwo, während der Timer angezeigt wird, um in den AOD-Modus zu wechseln.
+ Letztes Jahr
+ Aussehen
+ Dauer
+ Sound
+ Bitte nicht stören
+ ‚Bitte nicht stören‘ aktivieren, wenn ein Fokus-Timer läuft
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index 60b8b1d..e4112f4 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -1,7 +1,7 @@
Démarrer
- Arrêter
+ Pause
Concentration
Pause courte
Pause longue
@@ -18,7 +18,7 @@
Thème de couleurs
Dynamique
Couleur
- Valeur par défaut du système
+ Système
Alarme
Clair
Sombre
@@ -29,7 +29,7 @@
Thème noir
Utiliser un thème sombre noir pur
Faire sonner l’alarme à la fin du minuteur
- Vibrer
+ Vibration
Faire vibrer à la fin du minuteur
Thème
Paramètres
@@ -38,7 +38,7 @@
Une \"session\" est une séquence d’intervalles Pomodoro comprenant des phases de concentration, des pauses courtes et une pause longue. La dernière pause d’une session est toujours une pause longue.
Statistiques
Aujourd\'hui
- Pause
+ Pauses
7 derniers jours
concentration moyenne par jour
Plus d\'infos
@@ -59,4 +59,15 @@
12 derniers mois
Appuyez n\'importe où lors de l\'affichage du minuteur pour passer en mode AOD
Affichage Permanent
+ Apparence
+ Durées
+ Son
+ Ne pas déranger
+ Activer le mode ne pas déranger pendant un minuteur de concentration
+ Couleur dynamique
+ Adapter les couleurs à celles de votre fond d\'écran
+ 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.
+ Tomato+
+ Obtenir Tomato+
+ Tomato FOSS
diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml
index 2f3ad16..a9b9992 100644
--- a/app/src/main/res/values-hi/strings.xml
+++ b/app/src/main/res/values-hi/strings.xml
@@ -3,4 +3,63 @@
प्रारंभ करें
विराम करें
ध्यान
+ अलार्म
+ टाइमर पूरा होने पर अलार्म बजाएँ
+ अलार्म ध्वनि
+ ऑलवेज ऑन डिस्प्ले
+ AOD मोड चालू करने के लिए टाइमर देखते समय कहीं भी टैप करें
+ काली थीम
+ पूर्णतः काले रंग की थीम का उपयोग करें
+ विश्राम
+ रंग स्कीम चुनें
+ थीम चुनें
+ रंग
+ रंग स्कीम
+ पूर्ण
+ काला
+ डायनामिक
+ बंद करें
+ प्रतिदिन ध्यान (औसत)
+ पिछले महीने
+ पिछले सप्ताह
+ ध्वनि
+ पिछले साल
+ सफ़ेद
+ दीर्घ विश्राम
+ %1$s मिनट शेष
+ मासिक उत्पादकता विश्लेषण
+ अधिक
+ अधिक जानकारी
+ ठीक
+ रोकें
+ रुका हुआ
+ शुरू करें
+ एक \"सत्र\" पोमोडोरो अंतरालों का एक क्रम होता है जिसमें ध्यान अंतराल, लघु विश्राम अंतराल और एक दीर्घ विश्राम अंतराल शामिल होता है। सत्र का अंतिम विश्राम हमेशा एक दीर्घ विश्राम होता है।
+ उत्पादकता विश्लेषण
+ दिन के अलग-अलग समय पर ध्यान अवधि
+ पुनः शुरू करें
+ सत्र की अवधि
+ एक सत्र में ध्यान अंतराल: %1$d
+ सेटिंग
+ लघु विश्राम
+ अगला अंतराल
+ अगले पर जाएं
+ अगला शुरू करें
+ आँकड़े
+ अलार्म बंद करें
+ वर्तमान टाइमर सत्र पूरा हो गया है। अलार्म बंद करने के लिए कहीं भी टैप करें।
+ अलार्म बंद करें?
+ सिस्टम
+ थीम
+ टाइमर
+ टाइमर पूर्णता
+ %2$d में से %1$d
+ आज
+ आगामी
+ आगामी: %1$s (%2$s)
+ वाइब्रेट
+ टाइमर पूरा होने पर वाइब्रेट करें
+ साप्ताहिक उत्पादकता विश्लेषण
+ दिखावट
+ अवधियां
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index a06754d..e4d397e 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -18,7 +18,7 @@
Renk şeması
Dinamik
Renk
- Sistem varsayılanı
+ Sistem
Açık
Koyu
Tema seçin
@@ -61,4 +61,7 @@
Zamanlayıcıyı görüntülerken AOD moduna geçmek için herhangi bir yere dokunun
Görünüm
Süreler
+ Ses
+ Rahatsız Etmeyin
+ Odaklanma sayacı çalışırken RE\'yi aç
diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml
index f20c905..6b782fa 100644
--- a/app/src/main/res/values-uk/strings.xml
+++ b/app/src/main/res/values-uk/strings.xml
@@ -35,7 +35,7 @@
ОК
Динамічна
Колір
- Налаштування системи
+ Система
Сигнал
Світла
Темна
@@ -57,4 +57,17 @@
Пауза
Грати
Минулого року
+ Always On Display
+ Натисніть будь-де під час перегляду таймера, щоб перейти в режим AOD
+ Вигляд
+ Тривалість
+ Звук
+ Не турбувати
+ Активувати режим «Не турбувати», доки таймер активний
+ Tomato+
+ Отримати Tomato+
+ Динамічний колір
+ Адаптуйте кольори теми зі своїх шпалер
+ Tomato FOSS
+ Усі функції розблоковано в цій версії. Якщо мій додаток допоміг Вам у житті, будь ласка, підтримайте мене, зробивши пожертву на %1$s.
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index ad99d0d..53dfcb2 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -18,7 +18,7 @@
配色方案
动态
颜色
- 系统默认
+ 系统
响铃
亮色
暗色
@@ -59,4 +59,9 @@
去年
息屏显示
查看计时器时点击任意位置切换至 AOD 模式
+ 外观
+ 时长
+ 声音
+ 勿扰模式
+ 运行「专注」计时器时打开勿扰
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
index 6260170..ee44adb 100644
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -18,7 +18,7 @@
配色方案
動態
顏色
- 系統默認
+ 系統
鬧鐘
亮色
暗色
@@ -57,4 +57,17 @@
计时
计时进度
上年
+ 熄屏模式
+ 外觀
+ 在查看計時器時,點擊任意位置即可切換至熄屏模式
+ 持續時間
+ 聲音
+ 請勿打擾
+ 在執行專注計時器時開啟勿擾模式
+ 獲取 Tomato+
+ Tomato+
+ 動態顔色
+ 調整主題顏色為你的壁紙顔色
+ Tomato FOSS
+ 所有功能在此版本中均已解鎖。如果我的應用程式改變了您的生活,請考慮透過捐贈 %1$s 來支持我。
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index cf51e15..79d3d99 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -79,4 +79,12 @@
Appearance
Durations
Sound
+ Do Not Disturb
+ Turn on DND when running a Focus timer
+ Tomato+
+ Get Tomato+
+ Dynamic color
+ Adapt theme colors from your wallpaper
+ Tomato FOSS
+ 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.
\ No newline at end of file
diff --git a/app/src/play/AndroidManifest.xml b/app/src/play/AndroidManifest.xml
new file mode 100644
index 0000000..97908e4
--- /dev/null
+++ b/app/src/play/AndroidManifest.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/play/java/org/nsh07/pomodoro/billing/PlayBillingManager.kt b/app/src/play/java/org/nsh07/pomodoro/billing/PlayBillingManager.kt
new file mode 100644
index 0000000..a658884
--- /dev/null
+++ b/app/src/play/java/org/nsh07/pomodoro/billing/PlayBillingManager.kt
@@ -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 .
+ */
+
+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()
+}
\ No newline at end of file
diff --git a/app/src/play/java/org/nsh07/pomodoro/billing/TomatoPlusPaywallDialog.kt b/app/src/play/java/org/nsh07/pomodoro/billing/TomatoPlusPaywallDialog.kt
new file mode 100644
index 0000000..bd7b318
--- /dev/null
+++ b/app/src/play/java/org/nsh07/pomodoro/billing/TomatoPlusPaywallDialog.kt
@@ -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 .
+ */
+
+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)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/play/java/org/nsh07/pomodoro/billing/initializePurchases.kt b/app/src/play/java/org/nsh07/pomodoro/billing/initializePurchases.kt
new file mode 100644
index 0000000..00e2437
--- /dev/null
+++ b/app/src/play/java/org/nsh07/pomodoro/billing/initializePurchases.kt
@@ -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 .
+ */
+
+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()
+ )
+}
\ No newline at end of file
diff --git a/fastlane/metadata/android/de-DE/full_description.txt b/fastlane/metadata/android/de-DE/full_description.txt
index 1c1fcad..a6e9c5e 100644
--- a/fastlane/metadata/android/de-DE/full_description.txt
+++ b/fastlane/metadata/android/de-DE/full_description.txt
@@ -1 +1,12 @@
-Tomato ist ein minimalistischer Pomodoro-Timer für Android, der auf Material 3 Expressive basiert.
Funktionen:
- 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
+Tomato 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
+
+Funktionen:
+-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
diff --git a/fastlane/metadata/android/en-US/changelogs/17.txt b/fastlane/metadata/android/en-US/changelogs/17.txt
new file mode 100644
index 0000000..cd66b44
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/17.txt
@@ -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)
diff --git a/fastlane/metadata/android/hi-IN/full_description.txt b/fastlane/metadata/android/hi-IN/full_description.txt
new file mode 100644
index 0000000..fe2fe9f
--- /dev/null
+++ b/fastlane/metadata/android/hi-IN/full_description.txt
@@ -0,0 +1,12 @@
+टोमैटो एंड्रॉइड के लिए मटेरियल 3 एक्सप्रेसिव पर आधारित एक न्यूनतम पोमोडोरो टाइमर है।
+
+टोमैटो पूरी तरह से मुफ़्त और ओपन-सोर्स है, हमेशा के लिए। आप https://github.com/nsh07/Tomato पर सोर्स कोड पा सकते हैं, बग रिपोर्ट कर सकते हैं या सुविधाएँ सुझा सकते हैं।
+
+विशेषताएँ:
+- नवीनतम मटेरियल 3 एक्सप्रेसिव दिशानिर्देशों पर आधारित सरल, न्यूनतम UI
+- समझने में आसान तरीके से काम/अध्ययन समय के विस्तृत आँकड़े
+ - एक नज़र में वर्तमान दिन के आँकड़े देखें
+ - पढ़ने में आसान, साफ़ ग्राफ़ में दिखाए गए पिछले हफ़्ते और पिछले महीने के आँकड़े देखें
+ - पिछले हफ़्ते और महीने के अतिरिक्त आँकड़े जो दिखाते हैं कि दिन के किस समय आप सबसे ज़्यादा उत्पादक होते हैं
+- अनुकूलन योग्य टाइमर पैरामीटर
+- एंड्रॉइड 16 लाइव अपडेट
diff --git a/fastlane/metadata/android/zh-CN/full_description.txt b/fastlane/metadata/android/zh-CN/full_description.txt
index a1505f3..580c24f 100644
--- a/fastlane/metadata/android/zh-CN/full_description.txt
+++ b/fastlane/metadata/android/zh-CN/full_description.txt
@@ -1,12 +1,12 @@
-Tomato 是一个基于Material 3 Expressive的安卓极简主义番茄钟.
+Tomato 是一个基于Material 3 Expressive 的安卓极简主义番茄钟。
-Tomato 将永远保持完全免费和开源。如果你想获取源代码、报告程序错误(bug)或建议新功能,请访问 https://github.com/nsh07/Tomato。
+Tomato 将永远保持完全免费和开源。如果你想获取源代码、报告程序错误(bug)或建议新功能,请访问 https://github.com/nsh07/Tomato
功能:
- 基于最新Material 3 Expressive指南的简洁用户界面
-- 以便于理解的方式提供工作/学习的详细统计数据
+- 以便于理解的方式提供工作/学习时间的详细统计数据
- 当日统计数据一目了然
- 清楚易读的上周和上月统计图表
- 上周和上月的额外统计数据帮您找到一天中最高效的时间段
- 可自定义的计时器参数
-- 支持 Android 16 即時更新 (Android 16 Live Updates)
+- 支持 Android 16 即时更新 (Android 16 Live Updates)
diff --git a/fastlane/metadata/android/zh-TW/full_description.txt b/fastlane/metadata/android/zh-TW/full_description.txt
new file mode 100644
index 0000000..558c1fe
--- /dev/null
+++ b/fastlane/metadata/android/zh-TW/full_description.txt
@@ -0,0 +1,12 @@
+Tomato 是一款基於 Material 3 Expressive 的 Android 極簡番茄鐘計時器。
+
+Tomato 完全免費且開源,永遠如此。你可以在 https://github.com/nsh07/Tomato 取得原始碼,並回報問題或提出功能建議。
+
+功能:
+- 依循最新 Material 3 Expressive 設計規範的簡潔、極簡介面
+- 以易懂方式呈現工作/讀書時間的詳細統計
+ - 當日統計一目了然
+ - 過去一週與一個月的統計,以清爽易讀的圖表呈現
+ - 另外提供過去一週與一個月在一天中何時最有效率的統計
+- 計時器參數可自訂
+- 支援 Android 16 的 Live Updates
diff --git a/fastlane/metadata/android/zh-TW/short_description.txt b/fastlane/metadata/android/zh-TW/short_description.txt
new file mode 100644
index 0000000..a817390
--- /dev/null
+++ b/fastlane/metadata/android/zh-TW/short_description.txt
@@ -0,0 +1 @@
+極簡番茄鐘
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 783dd5d..85f21bf 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -12,6 +12,7 @@ ksp = "2.2.20-2.0.4"
lifecycleRuntimeKtx = "2.9.4"
materialKolor = "3.0.1"
navigation3 = "1.0.0-beta01"
+revenuecat = "9.12.0"
room = "2.8.3"
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" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
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" }
[plugins]