mirror of
https://github.com/ReVanced/revanced-manager.git
synced 2026-01-11 13:46:17 +00:00
feat: Add language settings (#2913)
This commit is contained in:
@@ -149,7 +149,6 @@ android {
|
||||
debug {
|
||||
applicationIdSuffix = ".debug"
|
||||
resValue("string", "app_name", "ReVanced Manager (Debug)")
|
||||
isPseudoLocalesEnabled = true
|
||||
|
||||
buildConfigField("long", "BUILD_ID", "${Random.nextLong()}L")
|
||||
}
|
||||
@@ -243,6 +242,8 @@ android {
|
||||
version = "3.22.1"
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets["main"].kotlin.srcDir(layout.buildDirectory.dir("generated/source/locales"))
|
||||
}
|
||||
|
||||
kotlin {
|
||||
@@ -250,6 +251,47 @@ kotlin {
|
||||
}
|
||||
|
||||
tasks {
|
||||
val generateSupportedLocales by registering {
|
||||
description = "Generate list of supported locales from resource directories"
|
||||
|
||||
val resDir = file("src/main/res")
|
||||
val outputDir = layout.buildDirectory.dir("generated/source/locales")
|
||||
|
||||
inputs.dir(resDir)
|
||||
outputs.dir(outputDir)
|
||||
|
||||
doLast {
|
||||
val locales = resDir.listFiles()
|
||||
.orEmpty()
|
||||
.filter { it.isDirectory && it.name.matches(Regex("values-[a-z]{2}(-r[A-Z]{2})?")) }
|
||||
.map { it.name.removePrefix("values-").replace("-r", "-") }
|
||||
.sorted()
|
||||
.joinToString(",\n ") { "Locale.forLanguageTag(\"$it\")" }
|
||||
|
||||
val output = outputDir.get().asFile.resolve("app/revanced/manager/util/GeneratedLocales.kt")
|
||||
output.parentFile.mkdirs()
|
||||
output.writeText(
|
||||
"""
|
||||
|package app.revanced.manager.util
|
||||
|
|
||||
|import java.util.Locale
|
||||
|
|
||||
|object GeneratedLocales {
|
||||
| val SUPPORTED_LOCALES = listOf(
|
||||
| Locale.ENGLISH,
|
||||
| $locales,
|
||||
| )
|
||||
|}
|
||||
""".trimMargin()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
preBuild {
|
||||
dependsOn(generateSupportedLocales)
|
||||
}
|
||||
|
||||
|
||||
// Needed by gradle-semantic-release-plugin.
|
||||
// Tracking: https://github.com/KengoTODA/gradle-semantic-release-plugin/issues/435.
|
||||
val publish by registering {
|
||||
|
||||
@@ -3,11 +3,11 @@ package app.revanced.manager
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.animation.slideOutHorizontally
|
||||
@@ -63,7 +63,7 @@ import org.koin.androidx.compose.navigation.koinNavViewModel
|
||||
import org.koin.core.parameter.parametersOf
|
||||
import org.koin.androidx.viewmodel.ext.android.getViewModel as getActivityViewModel
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
class MainActivity : AppCompatActivity() {
|
||||
@ExperimentalAnimationApi
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
@@ -8,6 +8,8 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
@@ -19,6 +21,7 @@ import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
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.ui.Alignment
|
||||
@@ -38,6 +41,7 @@ import app.revanced.manager.ui.theme.Theme
|
||||
import app.revanced.manager.ui.viewmodel.GeneralSettingsViewModel
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import org.koin.compose.koinInject
|
||||
import java.util.Locale
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -48,6 +52,7 @@ fun GeneralSettingsScreen(
|
||||
val prefs = viewModel.prefs
|
||||
val coroutineScope = viewModel.viewModelScope
|
||||
var showThemePicker by rememberSaveable { mutableStateOf(false) }
|
||||
var showLanguagePicker by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
if (showThemePicker) {
|
||||
ThemePicker(
|
||||
@@ -55,6 +60,17 @@ fun GeneralSettingsScreen(
|
||||
onConfirm = { viewModel.setTheme(it) }
|
||||
)
|
||||
}
|
||||
|
||||
if (showLanguagePicker) {
|
||||
LanguagePicker(
|
||||
supportedLocales = viewModel.getSupportedLocales(),
|
||||
currentLocale = viewModel.getCurrentLocale(),
|
||||
onDismiss = { showLanguagePicker = false },
|
||||
onConfirm = { viewModel.setLocale(it) },
|
||||
getDisplayName = { viewModel.getLocaleDisplayName(it) }
|
||||
)
|
||||
}
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
|
||||
Scaffold(
|
||||
@@ -74,6 +90,24 @@ fun GeneralSettingsScreen(
|
||||
) {
|
||||
GroupHeader(stringResource(R.string.appearance))
|
||||
|
||||
val currentLocale = viewModel.getCurrentLocale()
|
||||
val currentLanguageDisplay = remember(currentLocale) {
|
||||
currentLocale?.let { viewModel.getLocaleDisplayName(it) }
|
||||
}
|
||||
SettingsListItem(
|
||||
modifier = Modifier.clickable { showLanguagePicker = true },
|
||||
headlineContent = stringResource(R.string.language),
|
||||
supportingContent = stringResource(R.string.language_description),
|
||||
trailingContent = {
|
||||
FilledTonalButton(onClick = { showLanguagePicker = true }) {
|
||||
Text(
|
||||
currentLanguageDisplay
|
||||
?: stringResource(R.string.language_system_default)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
val theme by prefs.theme.getAsState()
|
||||
SettingsListItem(
|
||||
modifier = Modifier.clickable { showThemePicker = true },
|
||||
@@ -156,4 +190,64 @@ private fun ThemePicker(
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LanguagePicker(
|
||||
supportedLocales: List<Locale>,
|
||||
currentLocale: Locale?,
|
||||
onDismiss: () -> Unit,
|
||||
onConfirm: (Locale?) -> Unit,
|
||||
getDisplayName: (Locale) -> String
|
||||
) {
|
||||
var selectedLocale by remember { mutableStateOf(currentLocale) }
|
||||
val systemDefaultString = stringResource(R.string.language_system_default)
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(stringResource(R.string.language)) },
|
||||
text = {
|
||||
Column(
|
||||
modifier = Modifier.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { selectedLocale = null },
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
HapticRadioButton(
|
||||
selected = selectedLocale == null,
|
||||
onClick = { selectedLocale = null }
|
||||
)
|
||||
Text(systemDefaultString)
|
||||
}
|
||||
|
||||
supportedLocales.forEach { locale ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { selectedLocale = locale },
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
HapticRadioButton(
|
||||
selected = selectedLocale == locale,
|
||||
onClick = { selectedLocale = locale }
|
||||
)
|
||||
Text(getDisplayName(locale))
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
onConfirm(selectedLocale)
|
||||
onDismiss()
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.apply))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -1,17 +1,26 @@
|
||||
package app.revanced.manager.ui.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.revanced.manager.domain.manager.PreferencesManager
|
||||
import app.revanced.manager.ui.theme.Theme
|
||||
import app.revanced.manager.util.SupportedLocales
|
||||
import app.revanced.manager.util.resetListItemColorsCached
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.Locale
|
||||
|
||||
class GeneralSettingsViewModel(
|
||||
private val app: Application,
|
||||
val prefs: PreferencesManager
|
||||
) : ViewModel() {
|
||||
fun setTheme(theme: Theme) = viewModelScope.launch {
|
||||
prefs.theme.update(theme)
|
||||
resetListItemColorsCached()
|
||||
}
|
||||
|
||||
fun getSupportedLocales() = SupportedLocales.getSupportedLocales(app)
|
||||
fun getCurrentLocale() = SupportedLocales.getCurrentLocale()
|
||||
fun setLocale(locale: Locale?) = SupportedLocales.setLocale(locale)
|
||||
fun getLocaleDisplayName(locale: Locale) = SupportedLocales.getDisplayName(locale)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package app.revanced.manager.util
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.LocaleList
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import java.util.Locale
|
||||
|
||||
object SupportedLocales {
|
||||
fun getSupportedLocales(context: Context): List<Locale> {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
runCatching {
|
||||
android.app.LocaleConfig(context).supportedLocales?.toList()
|
||||
}.getOrNull() ?: GeneratedLocales.SUPPORTED_LOCALES
|
||||
} else {
|
||||
GeneratedLocales.SUPPORTED_LOCALES
|
||||
}
|
||||
}
|
||||
|
||||
fun getCurrentLocale(): Locale? =
|
||||
AppCompatDelegate.getApplicationLocales().takeIf { !it.isEmpty }?.get(0)
|
||||
|
||||
fun setLocale(locale: Locale?) = AppCompatDelegate.setApplicationLocales(
|
||||
locale?.let { LocaleListCompat.create(it) } ?: LocaleListCompat.getEmptyLocaleList()
|
||||
)
|
||||
|
||||
fun getDisplayName(locale: Locale) =
|
||||
locale.getDisplayName(locale).replaceFirstChar { it.uppercase(locale) }
|
||||
|
||||
private fun LocaleList.toList() = (0 until size()).map { get(it) }
|
||||
}
|
||||
@@ -83,7 +83,7 @@ Second \"item\" text"</string>
|
||||
<string name="auto_updates_dialog_note">These settings can be changed later.</string>
|
||||
|
||||
<string name="general">General</string>
|
||||
<string name="general_description">Theme, dynamic color</string>
|
||||
<string name="general_description">Language, theme, dynamic color</string>
|
||||
<string name="updates">Updates</string>
|
||||
<string name="updates_description">Check for updates and view changelogs</string>
|
||||
<string name="downloads">Downloads</string>
|
||||
@@ -104,6 +104,9 @@ Second \"item\" text"</string>
|
||||
<string name="pure_black_theme_description">Use pure black backgrounds for dark theme</string>
|
||||
<string name="theme">Theme</string>
|
||||
<string name="theme_description">Choose between light or dark theme</string>
|
||||
<string name="language">Language</string>
|
||||
<string name="language_description">Choose the app display language</string>
|
||||
<string name="language_system_default">System default</string>
|
||||
<string name="safeguards">Safeguards</string>
|
||||
<string name="patch_compat_check">Disable version compatibility check</string>
|
||||
<string name="patch_compat_check_description">Do not restrict patches to compatible app versions</string>
|
||||
|
||||
Reference in New Issue
Block a user