diff --git a/README.md b/README.md
index 3aebfc0..2bbc547 100644
--- a/README.md
+++ b/README.md
@@ -94,9 +94,8 @@ translating this project into languages you know.
## Download
-- **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).
+- **Google Play Store** (recommended): Tomato is available on the Google Play Store.
+ [You can download 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
@@ -109,7 +108,7 @@ translating this project into languages you know.
> To [verify](https://developer.android.com/studio/command-line/apksigner#usage-verify) the APK
> downloaded from GitHub, use the following signing certificate fingerprints:
> ```
-> SHA1: B1:4E:17:93:11:E8:DB:D5:35:EF:8D:E9:FB:8F:FF:08:F8:EC:65:08
+> SHA1: B1:4E:17:93:11:E8:DB:D5:35:EF:8D:E9:FB:8F:FF:08:F8:EC:65:08
> SHA256: 07:BE:F3:05:81:BA:EE:8F:45:EC:93:E4:7E:E6:8E:F2:08:74:E5:0E:F5:70:9C:78:B2:EE:67:AC:86:BE:4C:3D
> ```
> The SHA256 and MD5 hashes of the individual APK files are also available in the `checksum.txt`
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 713c0f0..18ba6ae 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -47,6 +47,10 @@ android {
versionName = "1.6.2"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+
+ androidResources {
+ generateLocaleConfig = true
+ }
}
buildTypes {
diff --git a/app/src/foss/java/org/nsh07/pomodoro/billing/TomatoPlusPaywallDialog.kt b/app/src/foss/java/org/nsh07/pomodoro/billing/TomatoPlusPaywallDialog.kt
index 6df4870..0d261bc 100644
--- a/app/src/foss/java/org/nsh07/pomodoro/billing/TomatoPlusPaywallDialog.kt
+++ b/app/src/foss/java/org/nsh07/pomodoro/billing/TomatoPlusPaywallDialog.kt
@@ -78,7 +78,7 @@ fun TomatoPlusPaywallDialog(
)
Spacer(Modifier.height(16.dp))
Button(onClick = { uriHandler.openUri("https://coff.ee/nsh07") }) {
- Text("Buy Me A Coffee")
+ Text(stringResource(R.string.bmc))
}
}
}
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 2f8d2dd..e523c16 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
@@ -18,8 +18,10 @@
package org.nsh07.pomodoro.ui.settingsScreen
import android.annotation.SuppressLint
+import android.app.LocaleManager
import android.content.Intent
import android.net.Uri
+import android.os.Build
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInHorizontally
@@ -46,8 +48,10 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -68,6 +72,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.LocaleBottomSheet
import org.nsh07.pomodoro.ui.settingsScreen.components.PlusPromo
import org.nsh07.pomodoro.ui.settingsScreen.screens.AlarmSettings
import org.nsh07.pomodoro.ui.settingsScreen.screens.AppearanceSettings
@@ -76,6 +81,7 @@ import org.nsh07.pomodoro.ui.settingsScreen.viewModel.PreferencesState
import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsViewModel
import org.nsh07.pomodoro.ui.settingsScreens
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
+import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
import org.nsh07.pomodoro.ui.theme.CustomColors.topBarColors
@@ -182,6 +188,22 @@ private fun SettingsScreen(
val context = LocalContext.current
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
+ val currentLocales =
+ if (Build.VERSION.SDK_INT >= 33) {
+ context
+ .getSystemService(LocaleManager::class.java)
+ .applicationLocales
+ } else null
+ val currentLocalesSize = currentLocales?.size() ?: 0
+
+ var showLocaleSheet by remember { mutableStateOf(false) }
+
+ if (showLocaleSheet && currentLocales != null)
+ LocaleBottomSheet(
+ currentLocales = currentLocales,
+ setShowSheet = { showLocaleSheet = it }
+ )
+
NavDisplay(
backStack = backStack,
onBack = backStack::removeLastOrNull,
@@ -266,6 +288,30 @@ private fun SettingsScreen(
}
item { Spacer(Modifier.height(12.dp)) }
+
+ if (currentLocales != null)
+ item {
+ ClickableListItem(
+ leadingContent = {
+ Icon(
+ painterResource(R.drawable.language),
+ contentDescription = null
+ )
+ },
+ headlineContent = { Text(stringResource(R.string.language)) },
+ supportingContent = {
+ Text(
+ if (currentLocalesSize > 0) currentLocales.get(0).displayName
+ else stringResource(R.string.system_default)
+ )
+ },
+ colors = listItemColors,
+ items = 1,
+ index = 0
+ ) { showLocaleSheet = true }
+ }
+
+ item { Spacer(Modifier.height(12.dp)) }
}
}
}
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 1f85c76..14028fe 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
@@ -129,11 +129,11 @@ fun AboutCard(
) {
Icon(
painterResource(R.drawable.bmc),
- contentDescription = "Buy me a coffee",
+ contentDescription = null,
modifier = Modifier.height(24.dp)
)
- Text(text = "Buy me a coffee")
+ Text(text = stringResource(R.string.bmc))
}
}
@@ -147,11 +147,11 @@ fun AboutCard(
) {
Icon(
painterResource(R.drawable.play_store),
- contentDescription = "Rate on Google Play",
+ contentDescription = null,
modifier = Modifier.size(20.dp)
)
- Text(text = "Rate on Google Play")
+ Text(text = stringResource(R.string.rate_on_google_play))
}
}
}
diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/LocaleBottomSheet.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/LocaleBottomSheet.kt
new file mode 100644
index 0000000..2872fcf
--- /dev/null
+++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/LocaleBottomSheet.kt
@@ -0,0 +1,209 @@
+/*
+ * 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 android.app.LocaleConfig
+import android.app.LocaleManager
+import android.os.Build
+import android.os.LocaleList
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
+import androidx.compose.material3.Icon
+import androidx.compose.material3.ListItem
+import androidx.compose.material3.ListItemDefaults
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.MaterialTheme.colorScheme
+import androidx.compose.material3.MaterialTheme.shapes
+import androidx.compose.material3.ModalBottomSheet
+import androidx.compose.material3.Text
+import androidx.compose.material3.rememberModalBottomSheetState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.launch
+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.middleListItemShape
+import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape
+import java.util.Locale
+
+@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
+@Composable
+fun LocaleBottomSheet(
+ currentLocales: LocaleList,
+ setShowSheet: (Boolean) -> Unit,
+ modifier: Modifier = Modifier
+) {
+ val context = LocalContext.current
+ val scope = rememberCoroutineScope()
+
+ val supportedLocales = remember {
+ if (Build.VERSION.SDK_INT >= 33) {
+ LocaleConfig(context).supportedLocales
+ } else null
+ }
+ val supportedLocalesSize = supportedLocales?.size() ?: 0
+
+ val supportedLocalesList: List? = remember {
+ if (supportedLocales != null) {
+ buildList {
+ for (i in 0 until supportedLocalesSize) {
+ add(AppLocale(supportedLocales.get(i), supportedLocales.get(i).displayName))
+ }
+ sortWith(compareBy { it.name })
+ }
+ } else null
+ }
+
+ val bottomSheetState = rememberModalBottomSheetState()
+ val listState = rememberLazyListState()
+
+ ModalBottomSheet(
+ onDismissRequest = { setShowSheet(false) },
+ sheetState = bottomSheetState,
+ modifier = modifier
+ ) {
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ Text(
+ text = stringResource(R.string.choose_language),
+ style = MaterialTheme.typography.labelLarge,
+ modifier = Modifier.padding(bottom = 16.dp)
+ )
+ if (supportedLocalesList != null) {
+ LazyColumn(
+ verticalArrangement = Arrangement.spacedBy(2.dp),
+ state = listState,
+ modifier = Modifier
+ .padding(horizontal = 16.dp)
+ .clip(shapes.largeIncreased)
+ ) {
+ item {
+ ListItem(
+ headlineContent = {
+ Text(stringResource(R.string.system_default))
+ },
+ trailingContent = {
+ if (currentLocales.isEmpty)
+ Icon(
+ painterResource(R.drawable.check),
+ contentDescription = stringResource(R.string.selected)
+ )
+ },
+ colors =
+ if (currentLocales.isEmpty)
+ ListItemDefaults.colors(
+ containerColor = colorScheme.primaryContainer.copy(
+ 0.3f
+ )
+ )
+ else listItemColors,
+ modifier = Modifier
+ .clip(if (currentLocales.isEmpty) CircleShape else shapes.largeIncreased)
+ .clickable(
+ onClick = {
+ scope
+ .launch { bottomSheetState.hide() }
+ .invokeOnCompletion {
+ if (Build.VERSION.SDK_INT >= 33) {
+ context
+ .getSystemService(LocaleManager::class.java)
+ .applicationLocales = LocaleList()
+ }
+ setShowSheet(false)
+ }
+ }
+ )
+ )
+ }
+ item {
+ Spacer(Modifier.height(12.dp))
+ }
+ itemsIndexed(
+ supportedLocalesList,
+ key = { _: Int, it: AppLocale -> it.name }
+ ) { index, it ->
+ ListItem(
+ headlineContent = {
+ Text(it.name)
+ },
+ trailingContent = {
+ if (!currentLocales.isEmpty && it.locale == currentLocales.get(0))
+ Icon(
+ painterResource(R.drawable.check),
+ tint = colorScheme.primary,
+ contentDescription = stringResource(R.string.selected)
+ )
+ },
+ colors =
+ if (!currentLocales.isEmpty && it.locale == currentLocales.get(0))
+ ListItemDefaults.colors(containerColor = colorScheme.primaryContainer)
+ else listItemColors,
+ modifier = Modifier
+ .clip(
+ if (!currentLocales.isEmpty && it.locale == currentLocales.get(0))
+ CircleShape
+ else when (index) {
+ 0 -> topListItemShape
+ supportedLocalesSize - 1 -> bottomListItemShape
+ else -> middleListItemShape
+ }
+ )
+ .clickable(
+ onClick = {
+ scope
+ .launch { bottomSheetState.hide() }
+ .invokeOnCompletion { _ ->
+ if (Build.VERSION.SDK_INT >= 33) {
+ context.getSystemService(LocaleManager::class.java)
+ .applicationLocales =
+ LocaleList(it.locale)
+ }
+ setShowSheet(false)
+ }
+ }
+ )
+ )
+ }
+ }
+ }
+ }
+ }
+}
+
+data class AppLocale(
+ val locale: Locale,
+ val name: String
+)
\ No newline at end of file
diff --git a/app/src/main/res/drawable/language.xml b/app/src/main/res/drawable/language.xml
new file mode 100644
index 0000000..c1ab95e
--- /dev/null
+++ b/app/src/main/res/drawable/language.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
diff --git a/app/src/main/res/resources.properties b/app/src/main/res/resources.properties
new file mode 100644
index 0000000..d694416
--- /dev/null
+++ b/app/src/main/res/resources.properties
@@ -0,0 +1,17 @@
+#
+# 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 .
+#
+unqualifiedResLocale=en
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 79d3d99..a608b21 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -87,4 +87,9 @@
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.
+ Language
+ Choose language
+ Rate on Google Play
+ BuyMeACoffee
+ Selected
\ No newline at end of file