diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 208963e1..87d4d177 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -147,6 +147,15 @@
android:value="" />
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/chat/revolt/activities/MainActivity.kt b/app/src/main/java/chat/revolt/activities/MainActivity.kt
index efc814b4..a5e8332e 100644
--- a/app/src/main/java/chat/revolt/activities/MainActivity.kt
+++ b/app/src/main/java/chat/revolt/activities/MainActivity.kt
@@ -9,6 +9,7 @@ import android.util.Log
import android.widget.Toast
import androidx.activity.compose.setContent
import androidx.activity.viewModels
+import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.core.EaseInOutExpo
import androidx.compose.animation.core.FiniteAnimationSpec
@@ -30,7 +31,6 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat
-import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
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.DebugSettingsScreen
import chat.revolt.screens.settings.ExperimentsSettingsScreen
+import chat.revolt.screens.settings.LanguagePickerSettingsScreen
import chat.revolt.screens.settings.ProfileSettingsScreen
import chat.revolt.screens.settings.SessionSettingsScreen
import chat.revolt.screens.settings.SettingsScreen
@@ -256,7 +257,7 @@ class MainActivityViewModel @Inject constructor(
}
@AndroidEntryPoint
-class MainActivity : FragmentActivity() {
+class MainActivity : AppCompatActivity() {
private val viewModel by viewModels()
// Fix for SDK >=31, where core-splashscreen accidentally removes dynamic colours
@@ -454,6 +455,7 @@ fun AppEntrypoint(
composable("settings/debug") { DebugSettingsScreen(navController) }
composable("settings/experiments") { ExperimentsSettingsScreen(navController) }
composable("settings/changelogs") { ChangelogsSettingsScreen(navController) }
+ composable("settings/language") { LanguagePickerSettingsScreen(navController) }
composable("settings/channel/{channelId}") { backStackEntry ->
val channelId = backStackEntry.arguments?.getString("channelId") ?: ""
diff --git a/app/src/main/java/chat/revolt/internals/extensions/Locales.kt b/app/src/main/java/chat/revolt/internals/extensions/Locales.kt
new file mode 100644
index 00000000..a972e44f
--- /dev/null
+++ b/app/src/main/java/chat/revolt/internals/extensions/Locales.kt
@@ -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 {
+ val id = resources.getIdentifier(
+ "_generated_res_locale_config",
+ "xml",
+ packageName
+ )
+ val localeXml = resources.getXml(id)
+ val locales = ArrayList()
+ 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)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/chat/revolt/screens/settings/LanguagePickerSettingsScreen.kt b/app/src/main/java/chat/revolt/screens/settings/LanguagePickerSettingsScreen.kt
new file mode 100644
index 00000000..75d89dc6
--- /dev/null
+++ b/app/src/main/java/chat/revolt/screens/settings/LanguagePickerSettingsScreen.kt
@@ -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()
+
+ var currentLocale by mutableStateOf(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
+ }
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/chat/revolt/screens/settings/SettingsScreen.kt b/app/src/main/java/chat/revolt/screens/settings/SettingsScreen.kt
index f4c5adf7..b0cb1f30 100644
--- a/app/src/main/java/chat/revolt/screens/settings/SettingsScreen.kt
+++ b/app/src/main/java/chat/revolt/screens/settings/SettingsScreen.kt
@@ -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(
headlineContent = {
Text(
diff --git a/app/src/main/res/drawable/ic_earth_24dp.xml b/app/src/main/res/drawable/ic_earth_24dp.xml
new file mode 100644
index 00000000..e06fdb99
--- /dev/null
+++ b/app/src/main/res/drawable/ic_earth_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
\ 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 d1d3fd1d..f594f4bb 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -655,6 +655,9 @@
Changelog for %1$s
that version
+ Language
+ System default
+
Channel Settings
#%1$s
Overview