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