Переключение языка
This commit is contained in:
@@ -67,6 +67,8 @@ dependencies {
|
||||
ksp(libs.androidx.hilt.compiler)
|
||||
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.androidx.datastore.preferences)
|
||||
implementation(libs.androidx.work.runtime.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Wallenc"
|
||||
android:localeConfig="@xml/locales_config"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
tools:targetApi="37">
|
||||
<activity
|
||||
|
||||
@@ -5,9 +5,9 @@ import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -23,7 +23,7 @@ import javax.inject.Inject
|
||||
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : ComponentActivity() {
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
@Inject
|
||||
lateinit var yandexSignInService: YandexSignInService
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package com.github.nullptroma.wallenc.app
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import androidx.work.Configuration
|
||||
import com.github.nullptroma.wallenc.app.di.HiltWorkerFactoryEntryPoint
|
||||
import com.github.nullptroma.wallenc.app.locale.AppLocaleStorage
|
||||
import com.github.nullptroma.wallenc.app.sync.StorageSyncBootstrap
|
||||
import com.github.nullptroma.wallenc.app.tasks.TaskPipelineForegroundBootstrap
|
||||
import dagger.hilt.android.EntryPointAccessors
|
||||
@@ -18,8 +20,15 @@ class WallencApplication : Application(), Configuration.Provider {
|
||||
@Inject
|
||||
lateinit var storageSyncBootstrap: StorageSyncBootstrap
|
||||
|
||||
override fun attachBaseContext(base: Context) {
|
||||
AppLocaleStorage.applyStored(base)
|
||||
super.attachBaseContext(base)
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
AppLocaleStorage.migrateLegacyDataStoreIfNeeded(this)
|
||||
AppLocaleStorage.applyStored(this)
|
||||
taskPipelineForegroundBootstrap.start()
|
||||
storageSyncBootstrap.start()
|
||||
}
|
||||
@@ -34,4 +43,4 @@ class WallencApplication : Application(), Configuration.Provider {
|
||||
.setWorkerFactory(factory)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.github.nullptroma.wallenc.app.di
|
||||
|
||||
import com.github.nullptroma.wallenc.app.locale.AppLocaleControllerImpl
|
||||
import com.github.nullptroma.wallenc.ui.locale.AppLocaleController
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
abstract class LocaleModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun bindAppLocaleController(impl: AppLocaleControllerImpl): AppLocaleController
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.github.nullptroma.wallenc.app.locale
|
||||
|
||||
import android.content.Context
|
||||
import com.github.nullptroma.wallenc.ui.locale.AppLanguage
|
||||
import com.github.nullptroma.wallenc.ui.locale.AppLocaleController
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class AppLocaleControllerImpl @Inject constructor(
|
||||
@param:ApplicationContext private val context: Context,
|
||||
) : AppLocaleController {
|
||||
|
||||
override val language: Flow<AppLanguage> = AppLocaleStorage.languageFlow(context)
|
||||
|
||||
override suspend fun setLanguage(language: AppLanguage) {
|
||||
AppLocaleStorage.persistLanguage(context, language)
|
||||
}
|
||||
|
||||
override suspend fun applyStoredLocale() {
|
||||
AppLocaleStorage.applyStored(context)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package com.github.nullptroma.wallenc.app.locale
|
||||
|
||||
import android.content.Context
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import com.github.nullptroma.wallenc.ui.locale.AppLanguage
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
/**
|
||||
* Хранение выбранного языка. SharedPreferences — для синхронного чтения в
|
||||
* [android.app.Application.attachBaseContext]; DataStore — только для миграции со старой версии.
|
||||
*/
|
||||
internal object AppLocaleStorage {
|
||||
|
||||
private const val PREFS_NAME = "app_locale"
|
||||
private const val PREFS_KEY_LANGUAGE = "app_language"
|
||||
|
||||
private val legacyLanguageKey = stringPreferencesKey(PREFS_KEY_LANGUAGE)
|
||||
|
||||
private val Context.legacyLocaleDataStore: DataStore<Preferences> by preferencesDataStore(
|
||||
name = "app_locale",
|
||||
)
|
||||
|
||||
/** В [android.app.Application.attachBaseContext] [Context.getApplicationContext] ещё null. */
|
||||
private fun storageContext(context: Context): Context =
|
||||
context.applicationContext ?: context
|
||||
|
||||
private fun prefs(context: Context) =
|
||||
storageContext(context).getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
|
||||
fun languageFlow(context: Context): Flow<AppLanguage> = callbackFlow {
|
||||
val storage = storageContext(context)
|
||||
val listener =
|
||||
android.content.SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
|
||||
if (key == PREFS_KEY_LANGUAGE) {
|
||||
trySend(readLanguageSync(storage))
|
||||
}
|
||||
}
|
||||
trySend(readLanguageSync(storage))
|
||||
prefs(storage).registerOnSharedPreferenceChangeListener(listener)
|
||||
awaitClose { prefs(storage).unregisterOnSharedPreferenceChangeListener(listener) }
|
||||
}
|
||||
|
||||
fun persistLanguage(context: Context, language: AppLanguage) {
|
||||
prefs(context).edit().putString(PREFS_KEY_LANGUAGE, language.storageValue).apply()
|
||||
applyLanguage(language)
|
||||
}
|
||||
|
||||
fun readLanguageSync(context: Context): AppLanguage {
|
||||
val raw = prefs(context).getString(PREFS_KEY_LANGUAGE, null)
|
||||
return raw?.toAppLanguage() ?: AppLanguage.System
|
||||
}
|
||||
|
||||
/** Без блокировок; безопасно из [android.app.Application.attachBaseContext]. */
|
||||
fun applyStored(context: Context) {
|
||||
applyLanguage(readLanguageSync(context))
|
||||
}
|
||||
|
||||
fun applyLanguage(language: AppLanguage) {
|
||||
val localeList = when (language) {
|
||||
AppLanguage.System -> LocaleListCompat.getEmptyLocaleList()
|
||||
AppLanguage.English -> LocaleListCompat.forLanguageTags("en")
|
||||
AppLanguage.Russian -> LocaleListCompat.forLanguageTags("ru")
|
||||
}
|
||||
AppCompatDelegate.setApplicationLocales(localeList)
|
||||
}
|
||||
|
||||
/** Однократно переносит значение из DataStore (если SP ещё пуст). */
|
||||
fun migrateLegacyDataStoreIfNeeded(context: Context) {
|
||||
val storage = storageContext(context)
|
||||
if (prefs(storage).contains(PREFS_KEY_LANGUAGE)) {
|
||||
return
|
||||
}
|
||||
runBlocking {
|
||||
runCatching {
|
||||
val legacy = storage.legacyLocaleDataStore.data.first()[legacyLanguageKey]
|
||||
if (legacy != null) {
|
||||
prefs(storage).edit().putString(PREFS_KEY_LANGUAGE, legacy).apply()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.toAppLanguage(): AppLanguage = when (this) {
|
||||
STORAGE_EN -> AppLanguage.English
|
||||
STORAGE_RU -> AppLanguage.Russian
|
||||
else -> AppLanguage.System
|
||||
}
|
||||
|
||||
private val AppLanguage.storageValue: String
|
||||
get() = when (this) {
|
||||
AppLanguage.System -> STORAGE_SYSTEM
|
||||
AppLanguage.English -> STORAGE_EN
|
||||
AppLanguage.Russian -> STORAGE_RU
|
||||
}
|
||||
|
||||
private const val STORAGE_SYSTEM = "system"
|
||||
private const val STORAGE_EN = "en"
|
||||
private const val STORAGE_RU = "ru"
|
||||
}
|
||||
@@ -17,6 +17,8 @@ import com.github.nullptroma.wallenc.domain.tasks.ITaskOrchestrator
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskForegroundItem
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskForegroundUiState
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskId
|
||||
import com.github.nullptroma.wallenc.ui.resources.UiStringResolver
|
||||
import com.github.nullptroma.wallenc.ui.resources.resolve
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -43,6 +45,9 @@ class TaskPipelineForegroundService : Service() {
|
||||
@Inject
|
||||
lateinit var orchestrator: ITaskOrchestrator
|
||||
|
||||
@Inject
|
||||
lateinit var uiStrings: UiStringResolver
|
||||
|
||||
private var repeat = false
|
||||
private var canPush = true
|
||||
private var lastUiState: TaskForegroundUiState? = null
|
||||
@@ -291,7 +296,7 @@ class TaskPipelineForegroundService : Service() {
|
||||
remoteViews.setViewVisibility(TASK_ROW_IDS[index], View.VISIBLE)
|
||||
remoteViews.setViewVisibility(TASK_LABEL_BAR_ROW_IDS[index], View.VISIBLE)
|
||||
remoteViews.setTextViewText(TASK_TITLE_IDS[index], task.title)
|
||||
val label = task.progress?.label?.trim().orEmpty()
|
||||
val label = task.progress?.label?.resolve(uiStrings).orEmpty()
|
||||
val fraction = task.progress?.fraction
|
||||
if (fraction != null) {
|
||||
if (label.isNotEmpty()) {
|
||||
|
||||
9
app/src/main/res/values-ru/strings.xml
Normal file
9
app/src/main/res/values-ru/strings.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Wallenc</string>
|
||||
<string name="task_notification_channel_name">Фоновые задачи</string>
|
||||
<string name="task_notification_title">Задачи Wallenc</string>
|
||||
<string name="task_notification_preparing">Подготовка…</string>
|
||||
<string name="task_notification_indeterminate">Выполняется…</string>
|
||||
<string name="task_notification_cancel">Отмена</string>
|
||||
</resources>
|
||||
@@ -1,8 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Wallenc</string>
|
||||
<string name="task_notification_channel_name">Фоновые задачи</string>
|
||||
<string name="task_notification_title">Задачи Wallenc</string>
|
||||
<string name="task_notification_preparing">Подготовка…</string>
|
||||
<string name="task_notification_indeterminate">Выполняется…</string>
|
||||
<string name="task_notification_cancel">Отмена</string>
|
||||
<string name="task_notification_channel_name">Background tasks</string>
|
||||
<string name="task_notification_title">Wallenc tasks</string>
|
||||
<string name="task_notification_preparing">Preparing…</string>
|
||||
<string name="task_notification_indeterminate">Running…</string>
|
||||
<string name="task_notification_cancel">Cancel</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<style name="Theme.Wallenc" parent="android:Theme.Material.Light.NoActionBar">
|
||||
<style name="Theme.Wallenc" parent="Theme.AppCompat.Light.NoActionBar">
|
||||
<!-- До первого кадра Compose и системный splash (12+) -->
|
||||
<item name="android:windowBackground">@color/splash_screen_background</item>
|
||||
<item name="android:windowSplashScreenBackground" tools:targetApi="31">
|
||||
|
||||
5
app/src/main/res/xml/locales_config.xml
Normal file
5
app/src/main/res/xml/locales_config.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<locale-config xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<locale android:name="en" />
|
||||
<locale android:name="ru" />
|
||||
</locale-config>
|
||||
Reference in New Issue
Block a user