feat(settings): language picker
Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
parent
4e29fba724
commit
1b4080520a
|
|
@ -147,6 +147,15 @@
|
||||||
android:value="" />
|
android:value="" />
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
||||||
|
android:enabled="false"
|
||||||
|
android:exported="false">
|
||||||
|
<meta-data
|
||||||
|
android:name="autoStoreLocales"
|
||||||
|
android:value="true" />
|
||||||
|
</service>
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
@ -9,6 +9,7 @@ import android.util.Log
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.compose.animation.AnimatedContentTransitionScope
|
import androidx.compose.animation.AnimatedContentTransitionScope
|
||||||
import androidx.compose.animation.core.EaseInOutExpo
|
import androidx.compose.animation.core.EaseInOutExpo
|
||||||
import androidx.compose.animation.core.FiniteAnimationSpec
|
import androidx.compose.animation.core.FiniteAnimationSpec
|
||||||
|
|
@ -30,7 +31,6 @@ import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import androidx.fragment.app.FragmentActivity
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
|
|
@ -72,6 +72,7 @@ import chat.revolt.screens.settings.ChangelogsSettingsScreen
|
||||||
import chat.revolt.screens.settings.ChatSettingsScreen
|
import chat.revolt.screens.settings.ChatSettingsScreen
|
||||||
import chat.revolt.screens.settings.DebugSettingsScreen
|
import chat.revolt.screens.settings.DebugSettingsScreen
|
||||||
import chat.revolt.screens.settings.ExperimentsSettingsScreen
|
import chat.revolt.screens.settings.ExperimentsSettingsScreen
|
||||||
|
import chat.revolt.screens.settings.LanguagePickerSettingsScreen
|
||||||
import chat.revolt.screens.settings.ProfileSettingsScreen
|
import chat.revolt.screens.settings.ProfileSettingsScreen
|
||||||
import chat.revolt.screens.settings.SessionSettingsScreen
|
import chat.revolt.screens.settings.SessionSettingsScreen
|
||||||
import chat.revolt.screens.settings.SettingsScreen
|
import chat.revolt.screens.settings.SettingsScreen
|
||||||
|
|
@ -256,7 +257,7 @@ class MainActivityViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class MainActivity : FragmentActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
private val viewModel by viewModels<MainActivityViewModel>()
|
private val viewModel by viewModels<MainActivityViewModel>()
|
||||||
|
|
||||||
// Fix for SDK >=31, where core-splashscreen accidentally removes dynamic colours
|
// Fix for SDK >=31, where core-splashscreen accidentally removes dynamic colours
|
||||||
|
|
@ -454,6 +455,7 @@ fun AppEntrypoint(
|
||||||
composable("settings/debug") { DebugSettingsScreen(navController) }
|
composable("settings/debug") { DebugSettingsScreen(navController) }
|
||||||
composable("settings/experiments") { ExperimentsSettingsScreen(navController) }
|
composable("settings/experiments") { ExperimentsSettingsScreen(navController) }
|
||||||
composable("settings/changelogs") { ChangelogsSettingsScreen(navController) }
|
composable("settings/changelogs") { ChangelogsSettingsScreen(navController) }
|
||||||
|
composable("settings/language") { LanguagePickerSettingsScreen(navController) }
|
||||||
|
|
||||||
composable("settings/channel/{channelId}") { backStackEntry ->
|
composable("settings/channel/{channelId}") { backStackEntry ->
|
||||||
val channelId = backStackEntry.arguments?.getString("channelId") ?: ""
|
val channelId = backStackEntry.arguments?.getString("channelId") ?: ""
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
package chat.revolt.internals.extensions
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.XmlResourceParser
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
@SuppressLint("DiscouragedApi")
|
||||||
|
fun Context.getSupportedLocales(): List<Locale> {
|
||||||
|
val id = resources.getIdentifier(
|
||||||
|
"_generated_res_locale_config",
|
||||||
|
"xml",
|
||||||
|
packageName
|
||||||
|
)
|
||||||
|
val localeXml = resources.getXml(id)
|
||||||
|
val locales = ArrayList<String>()
|
||||||
|
var event = localeXml.next()
|
||||||
|
while (event != XmlResourceParser.END_DOCUMENT) {
|
||||||
|
if (event == XmlResourceParser.START_TAG && localeXml.name == "locale") {
|
||||||
|
locales.add(localeXml.getAttributeValue(0))
|
||||||
|
}
|
||||||
|
event = localeXml.next()
|
||||||
|
}
|
||||||
|
return locales.map {
|
||||||
|
Locale.forLanguageTag(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,166 @@
|
||||||
|
package chat.revolt.screens.settings
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.LargeTopAppBar
|
||||||
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.RadioButton
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
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.mutableStateListOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.core.os.LocaleListCompat
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import chat.revolt.R
|
||||||
|
import chat.revolt.internals.extensions.getSupportedLocales
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class LanguagePickerSettingsScreen : ViewModel() {
|
||||||
|
val locales = mutableStateListOf<Locale>()
|
||||||
|
|
||||||
|
var currentLocale by mutableStateOf<Locale?>(null)
|
||||||
|
|
||||||
|
fun initLanguages(context: Context) {
|
||||||
|
locales.clear()
|
||||||
|
locales.addAll(context.getSupportedLocales())
|
||||||
|
currentLocale = determineCurrentlySelectedLocale()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun determineCurrentlySelectedLocale(): Locale? {
|
||||||
|
return AppCompatDelegate.getApplicationLocales()[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun LanguagePickerSettingsScreen(
|
||||||
|
navController: NavController,
|
||||||
|
viewModel: LanguagePickerSettingsScreen = viewModel()
|
||||||
|
) {
|
||||||
|
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
viewModel.initLanguages(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
|
topBar = {
|
||||||
|
LargeTopAppBar(
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
stringResource(id = R.string.settings_language),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
navController?.let {
|
||||||
|
IconButton(onClick = {
|
||||||
|
navController.popBackStack()
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Default.ArrowBack,
|
||||||
|
contentDescription = stringResource(id = R.string.back)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) { pv ->
|
||||||
|
LazyColumn(
|
||||||
|
contentPadding = pv,
|
||||||
|
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||||
|
) {
|
||||||
|
item(key = "auto") {
|
||||||
|
ListItem(
|
||||||
|
headlineContent = {
|
||||||
|
Text(
|
||||||
|
stringResource(id = R.string.settings_language_auto),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
},
|
||||||
|
leadingContent = {
|
||||||
|
RadioButton(
|
||||||
|
selected = viewModel.currentLocale == null,
|
||||||
|
onClick = null
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier.clickable {
|
||||||
|
AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList())
|
||||||
|
viewModel.currentLocale = null
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
items(
|
||||||
|
viewModel.locales.size,
|
||||||
|
key = { index -> viewModel.locales[index].toLanguageTag() }
|
||||||
|
) { index ->
|
||||||
|
val locale = viewModel.locales[index]
|
||||||
|
ListItem(
|
||||||
|
headlineContent = {
|
||||||
|
Text(
|
||||||
|
locale.displayLanguage,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
},
|
||||||
|
supportingContent = {
|
||||||
|
if (!locale.displayCountry.isNullOrEmpty()) {
|
||||||
|
Text(
|
||||||
|
locale.displayCountry,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trailingContent = {
|
||||||
|
Text(
|
||||||
|
locale.getDisplayLanguage(locale),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
},
|
||||||
|
leadingContent = {
|
||||||
|
RadioButton(
|
||||||
|
selected = locale.toLanguageTag() == viewModel.currentLocale?.toLanguageTag(),
|
||||||
|
onClick = null
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable {
|
||||||
|
AppCompatDelegate.setApplicationLocales(
|
||||||
|
LocaleListCompat.forLanguageTags(
|
||||||
|
locale.toLanguageTag()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
viewModel.currentLocale = locale
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -174,6 +174,25 @@ fun SettingsScreen(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ListItem(
|
||||||
|
headlineContent = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.settings_language)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
leadingContent = {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_earth_24dp),
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.testTag("settings_view_language")
|
||||||
|
.clickable {
|
||||||
|
navController.navigate("settings/language")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = {
|
headlineContent = {
|
||||||
Text(
|
Text(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:height="24dp"
|
||||||
|
android:width="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:pathData="M17.9,17.39C17.64,16.59 16.89,16 16,16H15V13A1,1 0 0,0 14,12H8V10H10A1,1 0 0,0 11,9V7H13A2,2 0 0,0 15,5V4.59C17.93,5.77 20,8.64 20,12C20,14.08 19.2,15.97 17.9,17.39M11,19.93C7.05,19.44 4,16.08 4,12C4,11.38 4.08,10.78 4.21,10.21L9,15V16A2,2 0 0,0 11,18M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" />
|
||||||
|
</vector>
|
||||||
|
|
@ -655,6 +655,9 @@
|
||||||
<string name="settings_changelogs_historical_version_header">Changelog for %1$s</string>
|
<string name="settings_changelogs_historical_version_header">Changelog for %1$s</string>
|
||||||
<string name="settings_changelogs_historical_version_header_placeholder">that version</string>
|
<string name="settings_changelogs_historical_version_header_placeholder">that version</string>
|
||||||
|
|
||||||
|
<string name="settings_language">Language</string>
|
||||||
|
<string name="settings_language_auto">System default</string>
|
||||||
|
|
||||||
<string name="channel_settings">Channel Settings</string>
|
<string name="channel_settings">Channel Settings</string>
|
||||||
<string name="channel_settings_header">#%1$s</string>
|
<string name="channel_settings_header">#%1$s</string>
|
||||||
<string name="channel_settings_overview">Overview</string>
|
<string name="channel_settings_overview">Overview</string>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue