Merge pull request #114 from nsh07/language-selection

Language selection on API 33+
This commit is contained in:
Nishant Mishra
2025-10-31 22:25:18 +05:30
committed by GitHub
9 changed files with 315 additions and 9 deletions

View File

@@ -94,9 +94,8 @@ translating this project into languages you know.
## Download ## Download
- **Google Play Store** (recommended): Tomato will soon be available (currently in closed testing) - **Google Play Store** (recommended): Tomato is available on the Google Play Store.
on the Google Play Store. [You can download it through this link](https://play.google.com/store/apps/details?id=org.nsh07.pomodoro).
[You can find it through this link](https://play.google.com/store/apps/details?id=org.nsh07.pomodoro).
- **F-Droid** (recommended): Tomato is available on the official F-Droid repository. Simply open - **F-Droid** (recommended): Tomato is available on the official F-Droid repository. Simply open
your preferred F-Droid app and search for Tomato. Updates on F-Droid are generally a week late. To 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 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 > To [verify](https://developer.android.com/studio/command-line/apksigner#usage-verify) the APK
> downloaded from GitHub, use the following signing certificate fingerprints: > downloaded from GitHub, use the following signing certificate fingerprints:
> ``` > ```
> SHA1: B1:4E:17:93:11:E8:DB:D5:35:EF:8D:E9:FB:8F:FF:08:F8:EC:65:08 > SHA1: B1:4E:17:93:11:E8:DB:D5:35:EF:8D:E9:FB:8F:FF:08:F8:EC:65:08
> SHA256: 07:BE:F3:05:81:BA:EE:8F:45:EC:93:E4:7E:E6:8E:F2:08:74:E5:0E:F5:70:9C:78:B2:EE:67:AC:86:BE:4C:3D > SHA256: 07:BE:F3:05:81:BA:EE:8F:45:EC:93:E4:7E:E6:8E:F2:08:74:E5:0E:F5:70:9C:78:B2:EE:67:AC:86:BE:4C:3D
> ``` > ```
> The SHA256 and MD5 hashes of the individual APK files are also available in the `checksum.txt` > The SHA256 and MD5 hashes of the individual APK files are also available in the `checksum.txt`

View File

@@ -47,6 +47,10 @@ android {
versionName = "1.6.2" versionName = "1.6.2"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
androidResources {
generateLocaleConfig = true
}
} }
buildTypes { buildTypes {

View File

@@ -78,7 +78,7 @@ fun TomatoPlusPaywallDialog(
) )
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(16.dp))
Button(onClick = { uriHandler.openUri("https://coff.ee/nsh07") }) { Button(onClick = { uriHandler.openUri("https://coff.ee/nsh07") }) {
Text("Buy Me A Coffee") Text(stringResource(R.string.bmc))
} }
} }
} }

View File

@@ -18,8 +18,10 @@
package org.nsh07.pomodoro.ui.settingsScreen package org.nsh07.pomodoro.ui.settingsScreen
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.LocaleManager
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Build
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideInHorizontally
@@ -46,8 +48,10 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.Screen
import org.nsh07.pomodoro.ui.settingsScreen.components.AboutCard import org.nsh07.pomodoro.ui.settingsScreen.components.AboutCard
import org.nsh07.pomodoro.ui.settingsScreen.components.ClickableListItem import org.nsh07.pomodoro.ui.settingsScreen.components.ClickableListItem
import org.nsh07.pomodoro.ui.settingsScreen.components.LocaleBottomSheet
import org.nsh07.pomodoro.ui.settingsScreen.components.PlusPromo import org.nsh07.pomodoro.ui.settingsScreen.components.PlusPromo
import org.nsh07.pomodoro.ui.settingsScreen.screens.AlarmSettings import org.nsh07.pomodoro.ui.settingsScreen.screens.AlarmSettings
import org.nsh07.pomodoro.ui.settingsScreen.screens.AppearanceSettings import org.nsh07.pomodoro.ui.settingsScreen.screens.AppearanceSettings
@@ -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.settingsScreen.viewModel.SettingsViewModel
import org.nsh07.pomodoro.ui.settingsScreens import org.nsh07.pomodoro.ui.settingsScreens
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
import org.nsh07.pomodoro.ui.theme.CustomColors.topBarColors import org.nsh07.pomodoro.ui.theme.CustomColors.topBarColors
@@ -182,6 +188,22 @@ private fun SettingsScreen(
val context = LocalContext.current val context = LocalContext.current
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() 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( NavDisplay(
backStack = backStack, backStack = backStack,
onBack = backStack::removeLastOrNull, onBack = backStack::removeLastOrNull,
@@ -266,6 +288,30 @@ private fun SettingsScreen(
} }
item { Spacer(Modifier.height(12.dp)) } 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)) }
} }
} }
} }

View File

@@ -129,11 +129,11 @@ fun AboutCard(
) { ) {
Icon( Icon(
painterResource(R.drawable.bmc), painterResource(R.drawable.bmc),
contentDescription = "Buy me a coffee", contentDescription = null,
modifier = Modifier.height(24.dp) modifier = Modifier.height(24.dp)
) )
Text(text = "Buy me a coffee") Text(text = stringResource(R.string.bmc))
} }
} }
@@ -147,11 +147,11 @@ fun AboutCard(
) { ) {
Icon( Icon(
painterResource(R.drawable.play_store), painterResource(R.drawable.play_store),
contentDescription = "Rate on Google Play", contentDescription = null,
modifier = Modifier.size(20.dp) modifier = Modifier.size(20.dp)
) )
Text(text = "Rate on Google Play") Text(text = stringResource(R.string.rate_on_google_play))
} }
} }
} }

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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<AppLocale>? = 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
)

View File

@@ -0,0 +1,26 @@
<!--
~ Copyright (c) 2025 Nishant Mishra
~
~ This file is part of Tomato - a minimalist pomodoro timer for Android.
~
~ Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
~ General Public License as published by the Free Software Foundation, either version 3 of the
~ License, or (at your option) any later version.
~
~ Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
~ the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
~ Public License for more details.
~
~ You should have received a copy of the GNU General Public License along with Tomato.
~ If not, see <https://www.gnu.org/licenses/>.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#e3e3e3"
android:pathData="M480,880q-82,0 -155,-31.5t-127.5,-86Q143,708 111.5,635T80,480q0,-83 31.5,-155.5t86,-127Q252,143 325,111.5T480,80q83,0 155.5,31.5t127,86q54.5,54.5 86,127T880,480q0,82 -31.5,155t-86,127.5q-54.5,54.5 -127,86T480,880ZM480,798q26,-36 45,-75t31,-83L404,640q12,44 31,83t45,75ZM376,782q-18,-33 -31.5,-68.5T322,640L204,640q29,50 72.5,87t99.5,55ZM584,782q56,-18 99.5,-55t72.5,-87L638,640q-9,38 -22.5,73.5T584,782ZM170,560h136q-3,-20 -4.5,-39.5T300,480q0,-21 1.5,-40.5T306,400L170,400q-5,20 -7.5,39.5T160,480q0,21 2.5,40.5T170,560ZM386,560h188q3,-20 4.5,-39.5T580,480q0,-21 -1.5,-40.5T574,400L386,400q-3,20 -4.5,39.5T380,480q0,21 1.5,40.5T386,560ZM654,560h136q5,-20 7.5,-39.5T800,480q0,-21 -2.5,-40.5T790,400L654,400q3,20 4.5,39.5T660,480q0,21 -1.5,40.5T654,560ZM638,320h118q-29,-50 -72.5,-87T584,178q18,33 31.5,68.5T638,320ZM404,320h152q-12,-44 -31,-83t-45,-75q-26,36 -45,75t-31,83ZM204,320h118q9,-38 22.5,-73.5T376,178q-56,18 -99.5,55T204,320Z" />
</vector>

View File

@@ -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 <https://www.gnu.org/licenses/>.
#
unqualifiedResLocale=en

View File

@@ -87,4 +87,9 @@
<string name="dynamic_color_desc">Adapt theme colors from your wallpaper</string> <string name="dynamic_color_desc">Adapt theme colors from your wallpaper</string>
<string name="tomato_foss">Tomato FOSS</string> <string name="tomato_foss">Tomato FOSS</string>
<string name="tomato_foss_desc">All features are unlocked in this version. If my app made a difference in your life, please consider supporting me by donating on %1$s.</string> <string name="tomato_foss_desc">All features are unlocked in this version. If my app made a difference in your life, please consider supporting me by donating on %1$s.</string>
<string name="language">Language</string>
<string name="choose_language">Choose language</string>
<string name="rate_on_google_play">Rate on Google Play</string>
<string name="bmc">BuyMeACoffee</string>
<string name="selected">Selected</string>
</resources> </resources>