forked from melod1n/fast-messenger
Upstream changes (#23)
This commit is contained in:
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,59 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.library)
|
||||
alias(libs.plugins.org.jetbrains.kotlin.android)
|
||||
alias(libs.plugins.com.google.devtools.ksp)
|
||||
alias(libs.plugins.kotlin.compose.compiler)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
}
|
||||
|
||||
group = "com.meloda.app.fast.languagepicker"
|
||||
|
||||
android {
|
||||
namespace = "com.meloda.app.fast.languagepicker"
|
||||
compileSdk = Configs.compileSdk
|
||||
|
||||
defaultConfig {
|
||||
minSdk = Configs.minSdk
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = Configs.java
|
||||
targetCompatibility = Configs.java
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = Configs.java.toString()
|
||||
freeCompilerArgs = listOf("-opt-in=kotlin.RequiresOptIn", "-Xcontext-receivers")
|
||||
}
|
||||
buildFeatures {
|
||||
compose = true
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
useLiveLiterals = true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.core.data)
|
||||
implementation(projects.core.model)
|
||||
implementation(projects.core.ui)
|
||||
|
||||
implementation(libs.nanokt.android)
|
||||
implementation(libs.nanokt.jvm)
|
||||
implementation(libs.nanokt)
|
||||
|
||||
implementation(libs.koin.android)
|
||||
implementation(libs.koin.androidx.compose)
|
||||
|
||||
implementation(platform(libs.compose.bom))
|
||||
implementation(libs.bundles.compose)
|
||||
|
||||
implementation(libs.coil.compose)
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
implementation(libs.kotlin.serialization)
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
</manifest>
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
package com.meloda.app.fast.languagepicker
|
||||
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.meloda.app.fast.common.extensions.setValue
|
||||
import com.meloda.app.fast.languagepicker.model.LanguagePickerScreenState
|
||||
import com.meloda.app.fast.languagepicker.model.SelectableLanguage
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
interface LanguagePickerViewModel {
|
||||
val screenState: StateFlow<LanguagePickerScreenState>
|
||||
|
||||
fun setLanguages(languages: List<SelectableLanguage>)
|
||||
|
||||
fun onLanguagePicked(newLanguage: SelectableLanguage)
|
||||
|
||||
fun onApplyButtonClicked()
|
||||
|
||||
fun updateCurrentLocale(locale: String)
|
||||
}
|
||||
|
||||
class LanguagePickerViewModelImpl : LanguagePickerViewModel, ViewModel() {
|
||||
|
||||
override val screenState = MutableStateFlow(
|
||||
LanguagePickerScreenState(
|
||||
languages = emptyList(),
|
||||
currentLanguage = AppCompatDelegate.getApplicationLocales().toLanguageTags()
|
||||
)
|
||||
)
|
||||
|
||||
override fun setLanguages(languages: List<SelectableLanguage>) {
|
||||
screenState.setValue { old -> old.copy(languages = languages) }
|
||||
}
|
||||
|
||||
override fun onLanguagePicked(newLanguage: SelectableLanguage) {
|
||||
val newList = screenState.value.languages.map { language ->
|
||||
language.copy(isSelected = language.key == newLanguage.key)
|
||||
}
|
||||
|
||||
screenState.setValue { old -> old.copy(languages = newList) }
|
||||
}
|
||||
|
||||
override fun onApplyButtonClicked() {
|
||||
val selectableLanguage =
|
||||
screenState.value.languages.singleOrNull(SelectableLanguage::isSelected)
|
||||
|
||||
if (selectableLanguage != null) {
|
||||
val newCode = selectableLanguage.key
|
||||
AppCompatDelegate.setApplicationLocales(LocaleListCompat.forLanguageTags(newCode))
|
||||
screenState.setValue { old -> old.copy(currentLanguage = newCode) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateCurrentLocale(locale: String) {
|
||||
val selected = screenState.value.languages.singleOrNull(SelectableLanguage::isSelected)
|
||||
|
||||
if (selected != null) {
|
||||
if (AppCompatDelegate.getApplicationLocales()
|
||||
.getFirstMatch(arrayOf(selected.key))?.language == locale
|
||||
) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
screenState.setValue { old ->
|
||||
old.copy(
|
||||
languages = old.languages.map { language ->
|
||||
language.copy(isSelected = language.key == locale)
|
||||
},
|
||||
currentLanguage = locale
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package com.meloda.app.fast.languagepicker.di
|
||||
|
||||
import com.meloda.app.fast.languagepicker.LanguagePickerViewModelImpl
|
||||
import org.koin.androidx.viewmodel.dsl.viewModelOf
|
||||
import org.koin.dsl.module
|
||||
|
||||
val languagePickerModule = module {
|
||||
viewModelOf(::LanguagePickerViewModelImpl)
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package com.meloda.app.fast.languagepicker.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
@Immutable
|
||||
data class LanguagePickerScreenState(
|
||||
val languages: List<SelectableLanguage>,
|
||||
val currentLanguage: String?,
|
||||
)
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package com.meloda.app.fast.languagepicker.model
|
||||
|
||||
data class SelectableLanguage(
|
||||
val local: String,
|
||||
val language: String,
|
||||
val key: String,
|
||||
val isSelected: Boolean
|
||||
)
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
package com.meloda.app.fast.languagepicker.navigation
|
||||
|
||||
import android.content.res.Resources
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.composable
|
||||
import com.meloda.app.fast.common.UiText
|
||||
import com.meloda.app.fast.common.parseString
|
||||
import com.meloda.app.fast.designsystem.R
|
||||
import com.meloda.app.fast.languagepicker.LanguagePickerViewModel
|
||||
import com.meloda.app.fast.languagepicker.LanguagePickerViewModelImpl
|
||||
import com.meloda.app.fast.languagepicker.model.SelectableLanguage
|
||||
import com.meloda.app.fast.languagepicker.presentation.LanguagePickerScreen
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
|
||||
@Serializable
|
||||
object LanguagePicker
|
||||
|
||||
private fun getLanguages(resources: Resources): List<SelectableLanguage> {
|
||||
return listOf(
|
||||
Triple(
|
||||
"",
|
||||
UiText.Resource(R.string.language_key_system),
|
||||
UiText.Resource(R.string.language_system)
|
||||
),
|
||||
Triple(
|
||||
"en-US",
|
||||
UiText.Resource(R.string.language_key_english),
|
||||
UiText.Resource(R.string.language_english),
|
||||
),
|
||||
Triple(
|
||||
"ru-RU",
|
||||
UiText.Resource(R.string.language_key_russian),
|
||||
UiText.Resource(R.string.language_russian)
|
||||
),
|
||||
Triple(
|
||||
"uk-UA",
|
||||
UiText.Resource(R.string.language_key_ukrainian),
|
||||
UiText.Resource(R.string.language_ukrainian)
|
||||
)
|
||||
).map { (key, language, local) ->
|
||||
Triple(
|
||||
key,
|
||||
language.parseString(resources).orEmpty(),
|
||||
local.parseString(resources).orEmpty()
|
||||
)
|
||||
}.map { (key, language, local) ->
|
||||
SelectableLanguage(
|
||||
local = local,
|
||||
language = language,
|
||||
key = key,
|
||||
isSelected = key == AppCompatDelegate.getApplicationLocales().toLanguageTags()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun NavGraphBuilder.languagePickerRoute(
|
||||
onBack: () -> Unit,
|
||||
) {
|
||||
composable<LanguagePicker> {
|
||||
val languages = getLanguages(LocalContext.current.resources)
|
||||
|
||||
val viewModel: LanguagePickerViewModel = koinViewModel<LanguagePickerViewModelImpl>()
|
||||
viewModel.setLanguages(languages)
|
||||
|
||||
LanguagePickerScreen(
|
||||
onBack = onBack,
|
||||
viewModel = viewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun NavController.navigateToLanguagePicker() {
|
||||
this.navigate(LanguagePicker)
|
||||
}
|
||||
+232
@@ -0,0 +1,232 @@
|
||||
package com.meloda.app.fast.languagepicker.presentation
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.calculateEndPadding
|
||||
import androidx.compose.foundation.layout.calculateStartPadding
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
|
||||
import androidx.compose.material.icons.outlined.MoreVert
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LargeTopAppBar
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
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.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.LifecycleResumeEffect
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.meloda.app.fast.languagepicker.LanguagePickerViewModel
|
||||
import com.meloda.app.fast.languagepicker.LanguagePickerViewModelImpl
|
||||
import com.meloda.app.fast.languagepicker.model.SelectableLanguage
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import com.meloda.app.fast.designsystem.R as UiR
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun LanguagePickerScreen(
|
||||
onBack: () -> Unit,
|
||||
viewModel: LanguagePickerViewModel = koinViewModel<LanguagePickerViewModelImpl>()
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
||||
val languages = screenState.languages
|
||||
|
||||
LifecycleResumeEffect(true) {
|
||||
viewModel.updateCurrentLocale(AppCompatDelegate.getApplicationLocales().toLanguageTags())
|
||||
|
||||
onPauseOrDispose {}
|
||||
}
|
||||
|
||||
val isButtonEnabled by remember(screenState) {
|
||||
derivedStateOf {
|
||||
screenState.currentLanguage != null &&
|
||||
languages.isNotEmpty() &&
|
||||
languages.find(SelectableLanguage::isSelected)?.key != screenState.currentLanguage
|
||||
}
|
||||
}
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
var dropDownMenuExpanded by remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
LargeTopAppBar(
|
||||
title = { Text(text = stringResource(id = UiR.string.title_application_language)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
|
||||
contentDescription = "Navigate back"
|
||||
)
|
||||
}
|
||||
},
|
||||
scrollBehavior = scrollBehavior,
|
||||
actions = {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||
return@LargeTopAppBar
|
||||
}
|
||||
|
||||
IconButton(
|
||||
onClick = {
|
||||
dropDownMenuExpanded = true
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.MoreVert,
|
||||
contentDescription = "Options"
|
||||
)
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
modifier = Modifier.defaultMinSize(minWidth = 140.dp),
|
||||
expanded = dropDownMenuExpanded,
|
||||
onDismissRequest = {
|
||||
dropDownMenuExpanded = false
|
||||
},
|
||||
offset = DpOffset(x = (-10).dp, y = (-60).dp)
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
dropDownMenuExpanded = false
|
||||
|
||||
context.startActivity(
|
||||
Intent(Settings.ACTION_APP_LOCALE_SETTINGS).apply {
|
||||
data = Uri.fromParts(
|
||||
"package",
|
||||
context.packageName,
|
||||
null
|
||||
)
|
||||
}
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Text(text = stringResource(id = UiR.string.open_system_language_picker))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||
) { padding ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(
|
||||
top = padding.calculateTopPadding(),
|
||||
start = padding.calculateStartPadding(LayoutDirection.Ltr),
|
||||
end = padding.calculateEndPadding(LayoutDirection.Ltr)
|
||||
)
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
items(screenState.languages.toList()) { item ->
|
||||
LanguageItem(
|
||||
item = item,
|
||||
onClick = viewModel::onLanguagePicked
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.height(64.dp)
|
||||
.navigationBarsPadding()
|
||||
.padding(bottom = 4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = viewModel::onApplyButtonClicked,
|
||||
enabled = isButtonEnabled,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp)
|
||||
.navigationBarsPadding()
|
||||
.padding(bottom = 4.dp)
|
||||
.align(Alignment.BottomCenter)
|
||||
.height(64.dp)
|
||||
) {
|
||||
Text(text = stringResource(id = UiR.string.action_apply))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LanguageItem(
|
||||
item: SelectableLanguage,
|
||||
onClick: (item: SelectableLanguage) -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(64.dp)
|
||||
.clickable { onClick(item) }
|
||||
.padding(horizontal = 24.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
RadioButton(
|
||||
selected = item.isSelected,
|
||||
onClick = null
|
||||
)
|
||||
Spacer(modifier = Modifier.width(24.dp))
|
||||
Column {
|
||||
Text(
|
||||
text = item.language.uppercase(),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
modifier = Modifier.alpha(0.7f),
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Text(
|
||||
text = item.local
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user