diff --git a/app/build.gradle b/app/build.gradle index ad5ab2196..b753a4e31 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -62,6 +62,11 @@ android { buildFeatures { viewBinding true buildConfig true + compose true + } + + composeOptions { + kotlinCompilerExtensionVersion = "1.5.13" } buildTypes { @@ -194,6 +199,14 @@ dependencies { implementation 'androidx.fragment:fragment-ktx:1.6.2' + // Compose + def composeBom = platform('androidx.compose:compose-bom:2025.01.00') + implementation composeBom + implementation 'androidx.compose.material:material' + // Preview support + implementation 'androidx.compose.ui:ui-tooling-preview' + debugImplementation 'androidx.compose.ui:ui-tooling' + // 23.0.0 to 23.2.0 breaks the apk for some reason (too small, fails to read android-manifest) implementation "com.google.android.gms:play-services-ads:22.6.0" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index fbc85c93a..26352a2cf 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -39,6 +39,7 @@ android:roundIcon="@mipmap/ic_roundapp" android:supportsRtl="false" android:theme="@style/AppTheme.Orange.NoActionBar" + android:localeConfig="@xml/locales_config" tools:replace="android:allowBackup,android:supportsRtl"> + + + + + + { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + val intent = Intent( + android.provider.Settings.ACTION_APP_LOCALE_SETTINGS, + Uri.parse("package:${requireContext().packageName}"), + ) + startActivity(intent) + } else { + LanguagePickerDialog().show(parentFragmentManager, null) + } + return true + } + else -> return super.onPreferenceTreeClick(preference) } } diff --git a/app/src/main/java/com/pr0gramm/app/ui/dialogs/LanguagePickerDialog.kt b/app/src/main/java/com/pr0gramm/app/ui/dialogs/LanguagePickerDialog.kt new file mode 100644 index 000000000..7ee72e1c2 --- /dev/null +++ b/app/src/main/java/com/pr0gramm/app/ui/dialogs/LanguagePickerDialog.kt @@ -0,0 +1,217 @@ +package com.pr0gramm.app.ui.dialogs + +import android.os.Build +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatDelegate +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.ButtonDefaults.textButtonColors +import androidx.compose.material.Card +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.ListItem +import androidx.compose.material.MaterialTheme +import androidx.compose.material.ProvideTextStyle +import androidx.compose.material.RadioButton +import androidx.compose.material.RadioButtonDefaults +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.core.os.LocaleListCompat +import com.pr0gramm.app.R +import com.pr0gramm.app.ui.base.BaseDialogFragment +import org.xmlpull.v1.XmlPullParser +import java.util.Locale + +class LanguagePickerDialog : BaseDialogFragment("LanguagePickerDialog") { + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + val supportedLocales = getSupportedLocales() + val currentLocale = getCurrentLocale(supportedLocales) + + return ComposeView(requireContext()).apply { + setContent { + DialogContent(supportedLocales, currentLocale) + } + } + } + + @Composable + fun DialogContent( + supportedLocales: List, + initialLocale: Locale + ) { + val selectedLocale = remember { mutableStateOf(initialLocale) } + + Dialog( + onDismissRequest = { dismiss() } + ) { + Card( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(16.dp), + shape = RoundedCornerShape(16.dp), + ) { + val titleStyle = MaterialTheme.typography.subtitle1 + val subtitleStyle = MaterialTheme.typography.body2 + Column { + ProvideTextStyle(titleStyle) { + Text( + stringResource(R.string.language_picker_title), + Modifier + .padding(start = 16.dp, end = 16.dp, top = 16.dp) + .align(Alignment.Start), + ) + } + ProvideTextStyle(subtitleStyle) { + Text( + stringResource(R.string.language_picker_subtitle), + Modifier + .padding(vertical = 4.dp, horizontal = 16.dp) + .align(Alignment.Start), + ) + } + supportedLocales.forEach { + LanguageItem( + it, + selected = it == selectedLocale.value, + onClick = { + selectedLocale.value = it + }, + ) + } + Row( + modifier = Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.End, + ) { + TextButton( + onClick = { dismiss() }, + modifier = Modifier.padding(8.dp), + colors = textButtonColors( + contentColor = colorResource(R.color.orange_primary) + ), + ) { + Text(stringResource(R.string.language_picker_dismiss)) + } + TextButton( + onClick = { + AppCompatDelegate.setApplicationLocales( + LocaleListCompat.create( + selectedLocale.value + ) + ) + dismiss() + }, + modifier = Modifier.padding(8.dp), + colors = textButtonColors( + contentColor = colorResource(R.color.orange_primary) + ), + ) { + Text(stringResource(R.string.language_picker_confirm)) + } + } + } + } + } + } + + @OptIn(ExperimentalMaterialApi::class) + @Composable + fun LanguageItem( + locale: Locale, + selected: Boolean, + onClick: (() -> Unit), + ) { + ListItem( + modifier = Modifier.clickable { + onClick() + }, + text = { Text(locale.getDisplayLanguage(locale)) }, + trailing = { + RadioButton( + selected = selected, + onClick = onClick, + colors = RadioButtonDefaults.colors( + selectedColor = colorResource(R.color.orange_primary) + ) + ) + } + ) + } + + private fun getSupportedLocales(): List { + val locales = mutableListOf() + + try { + val parser = resources.getXml(R.xml.locales_config) + var eventType = parser.eventType + val namespace = "http://schemas.android.com/apk/res/android" + + while (eventType != XmlPullParser.END_DOCUMENT) { + if (eventType == XmlPullParser.START_TAG && parser.name == "locale") { + val languageTag = parser.getAttributeValue(namespace, "name") + if (languageTag != null) { + val locale = Locale.forLanguageTag(languageTag) + locales.add(locale) + } + } + eventType = parser.next() + } + + parser.close() + } catch (e: Exception) { + logger.error("Error parsing locales config!", e) + } + + return locales + } + + private fun getCurrentLocale(supportedLocales: List): Locale { + val applicationLocales = AppCompatDelegate.getApplicationLocales() + val appLocale = if (!applicationLocales.isEmpty) { + applicationLocales.get(0)!! + } else { + getCurrentAppLocale() + } + + val candidate = supportedLocales.firstOrNull { it.isO3Language == appLocale.isO3Language } + + if (candidate != null) { + return candidate + } + + return Locale.forLanguageTag("en-US") + } + + private fun getCurrentAppLocale(): Locale { + val config = requireContext().resources.configuration + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + config.locales[0] + } else { + @Suppress("DEPRECATION") + config.locale + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pr0gramm/app/util/Extensions.kt b/app/src/main/java/com/pr0gramm/app/util/Extensions.kt index 4d9bc8294..f99fc5cf0 100644 --- a/app/src/main/java/com/pr0gramm/app/util/Extensions.kt +++ b/app/src/main/java/com/pr0gramm/app/util/Extensions.kt @@ -294,7 +294,7 @@ inline fun LruCache.getOrPut(key: K, creator: (K) -> V) } } -fun lruCache(maxSize: Int, creator: (K) -> V?): LruCache { +fun lruCache(maxSize: Int, creator: (K) -> V?): LruCache { return object : LruCache(maxSize) { override fun create(key: K) = creator(key) } diff --git a/app/src/main/res/drawable/ic_white_translate.xml b/app/src/main/res/drawable/ic_white_translate.xml new file mode 100644 index 000000000..a6814415d --- /dev/null +++ b/app/src/main/res/drawable/ic_white_translate.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 1a2b1e705..b9dd3ba29 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -602,4 +602,11 @@ Aktiviere diese Option um die \'Müll\' Kategorie in der Navigation zu zeigen. Mülltrennung Deaktiviere diese Option um \'Müll\' auch in \'Neu\' anzuzeigen. + + Sprache + Ändere die Sprache der App unabhängig deiner Systemsprache + Sprache wählen + Bitte wähle eine der verfügbaren Sprachen. + Abbrechen + Bestätigen diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6be026fbd..b5baa5b65 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -622,5 +622,12 @@ Video quality Junk Category \'Junk\' + + Language + Change the app language independent of your system language + Choose language + Please choose one of the available languages. + Dismiss + Confirm diff --git a/app/src/main/res/xml/locales_config.xml b/app/src/main/res/xml/locales_config.xml new file mode 100644 index 000000000..11e3e3651 --- /dev/null +++ b/app/src/main/res/xml/locales_config.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 570f6b796..80634f991 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -363,6 +363,12 @@ + +