mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Support configuring interface language per user
This commit is contained in:
parent
59ccce9d54
commit
4dc40f2c3c
11 changed files with 894 additions and 1 deletions
|
|
@ -84,6 +84,15 @@
|
|||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
||||
android:enabled="false"
|
||||
android:exported="false">
|
||||
<meta-data
|
||||
android:name="autoStoreLocales"
|
||||
android:value="false" />
|
||||
</service>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ import java.util.UUID
|
|||
SeerrUser::class,
|
||||
|
||||
],
|
||||
version = 31,
|
||||
version = 32,
|
||||
exportSchema = true,
|
||||
autoMigrations = [
|
||||
AutoMigration(3, 4),
|
||||
|
|
@ -55,6 +55,7 @@ import java.util.UUID
|
|||
AutoMigration(12, 20),
|
||||
AutoMigration(20, 30),
|
||||
AutoMigration(30, 31),
|
||||
AutoMigration(31, 32),
|
||||
],
|
||||
)
|
||||
@TypeConverters(Converters::class)
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ data class JellyfinUser(
|
|||
val serverId: UUID,
|
||||
val accessToken: String?,
|
||||
val pin: String? = null,
|
||||
val uiLanguage: String? = null,
|
||||
) {
|
||||
val hasPin: Boolean get() = pin.isNotNullOrBlank()
|
||||
|
||||
|
|
|
|||
|
|
@ -673,6 +673,13 @@ sealed interface AppPreference<Pref, T> {
|
|||
summary = R.string.customize_home_summary,
|
||||
)
|
||||
|
||||
val UserInterfaceLanguage =
|
||||
AppClickablePreference<AppPreferences>(
|
||||
title = R.string.user_interface_language,
|
||||
getter = { },
|
||||
setter = { prefs, _ -> prefs },
|
||||
)
|
||||
|
||||
val SendCrashReports =
|
||||
AppSwitchPreference<AppPreferences>(
|
||||
title = R.string.send_crash_reports,
|
||||
|
|
@ -1079,6 +1086,7 @@ val basicPreferences =
|
|||
AppPreference.RequireProfilePin,
|
||||
AppPreference.CustomizeHome,
|
||||
AppPreference.UserPinnedNavDrawerItems,
|
||||
AppPreference.UserInterfaceLanguage,
|
||||
),
|
||||
),
|
||||
PreferenceGroup(
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ package com.github.damontecres.wholphin.services
|
|||
|
||||
import android.content.Context
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import androidx.lifecycle.asFlow
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.github.damontecres.wholphin.BuildConfig
|
||||
|
|
@ -289,6 +291,13 @@ class UserSwitchListener
|
|||
|
||||
private suspend fun switchUser(user: JellyfinUser) =
|
||||
supervisorScope {
|
||||
val uiLanguage = user.uiLanguage
|
||||
val localeList =
|
||||
user.uiLanguage?.let { LocaleListCompat.forLanguageTags(uiLanguage) }
|
||||
?: LocaleListCompat.getEmptyLocaleList()
|
||||
Timber.i("Switch locale to %s", localeList)
|
||||
AppCompatDelegate.setApplicationLocales(localeList)
|
||||
|
||||
// Check for home settings
|
||||
launchIO {
|
||||
homeSettingsService.loadCurrentSettings(user.id)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,188 @@
|
|||
package com.github.damontecres.wholphin.ui.preferences
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.XmlResourceParser
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.asFlow
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.tv.material3.Text
|
||||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.data.ServerRepository
|
||||
import com.github.damontecres.wholphin.ui.components.DialogItem
|
||||
import com.github.damontecres.wholphin.ui.components.DialogPopupContent
|
||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||
import com.github.damontecres.wholphin.ui.components.SelectedLeadingContent
|
||||
import com.github.damontecres.wholphin.ui.launchDefault
|
||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||
import com.github.damontecres.wholphin.util.LoadingState
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.update
|
||||
import timber.log.Timber
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class LocaleChoiceViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
@param:ApplicationContext private val context: Context,
|
||||
private val serverRepository: ServerRepository,
|
||||
) : ViewModel() {
|
||||
fun changeLocale(locale: Locale) {
|
||||
viewModelScope.launchDefault {
|
||||
serverRepository.current.value?.let {
|
||||
Timber.i("Updating user's locale to %s", locale.toLanguageTag())
|
||||
val newUser = it.user.copy(uiLanguage = locale.toLanguageTag())
|
||||
serverRepository.changeUser(it.server, newUser)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val _state = MutableStateFlow(LocaleChoiceState())
|
||||
val state: StateFlow<LocaleChoiceState> = _state
|
||||
|
||||
init {
|
||||
viewModelScope.launchDefault {
|
||||
serverRepository.currentUser.value?.let {
|
||||
val availableLocales = extractAvailableLocales()
|
||||
Timber.v("availableLocales=%s", availableLocales)
|
||||
_state.update {
|
||||
it.copy(
|
||||
loading = LoadingState.Success,
|
||||
availableLocales = availableLocales,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
viewModelScope.launchDefault {
|
||||
serverRepository.currentUser.asFlow().collectLatest { user ->
|
||||
val userLocale = user?.uiLanguage?.let { Locale.forLanguageTag(it) } ?: Locale.getDefault()
|
||||
_state.update { it.copy(userLocale = userLocale) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractAvailableLocales(): List<Locale> {
|
||||
val id =
|
||||
context.resources.getIdentifier(
|
||||
"_generated_res_locale_config",
|
||||
"xml",
|
||||
context.packageName,
|
||||
)
|
||||
return if (id != 0) {
|
||||
try {
|
||||
val locales = mutableListOf<Locale>()
|
||||
context.resources.getXml(id).use { parser ->
|
||||
var eventType: Int = parser.eventType
|
||||
while (eventType != XmlResourceParser.END_DOCUMENT) {
|
||||
if (eventType == XmlResourceParser.START_TAG) {
|
||||
val tagName: String? = parser.name
|
||||
|
||||
// Check for a specific tag name
|
||||
if ("locale" == tagName) {
|
||||
val attr =
|
||||
parser.getAttributeValue(
|
||||
"http://schemas.android.com/apk/res/android",
|
||||
"name",
|
||||
)
|
||||
attr?.let { locales.add(Locale.forLanguageTag(it)) }
|
||||
}
|
||||
}
|
||||
eventType = parser.next()
|
||||
}
|
||||
}
|
||||
locales
|
||||
} catch (ex: Exception) {
|
||||
Timber.w(ex, "Exception while parsing generated available locales")
|
||||
context.assets.locales.map { Locale.forLanguageTag(it) }
|
||||
}
|
||||
} else {
|
||||
context.assets.locales.map { Locale.forLanguageTag(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class LocaleChoiceState(
|
||||
val loading: LoadingState = LoadingState.Pending,
|
||||
val availableLocales: List<Locale> = emptyList(),
|
||||
val userLocale: Locale = Locale.getDefault(),
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun LocaleChoiceDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
viewModel: LocaleChoiceViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.state.collectAsState()
|
||||
Dialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
properties = DialogProperties(),
|
||||
) {
|
||||
when (val st = state.loading) {
|
||||
is LoadingState.Error -> {
|
||||
ErrorMessage(st)
|
||||
}
|
||||
|
||||
LoadingState.Loading,
|
||||
LoadingState.Pending,
|
||||
-> {
|
||||
LoadingPage()
|
||||
}
|
||||
|
||||
LoadingState.Success -> {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
|
||||
val dialogItems =
|
||||
remember(state.userLocale, state.availableLocales) {
|
||||
val userLanguage = state.userLocale.toLanguageTag()
|
||||
state.availableLocales.map { locale ->
|
||||
val tag = locale.toLanguageTag()
|
||||
DialogItem(
|
||||
selected = tag == userLanguage,
|
||||
leadingContent = {
|
||||
Box {
|
||||
SelectedLeadingContent(tag == userLanguage)
|
||||
}
|
||||
},
|
||||
headlineContent = {
|
||||
Text(locale.getDisplayName(locale))
|
||||
},
|
||||
supportingContent = {
|
||||
Text(locale.getDisplayName(Locale.ENGLISH))
|
||||
},
|
||||
dismissOnClick = true,
|
||||
onClick = {
|
||||
viewModel.changeLocale(locale)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
DialogPopupContent(
|
||||
title = stringResource(R.string.user_interface_language),
|
||||
dialogItems = dialogItems,
|
||||
waiting = false,
|
||||
onDismissRequest = onDismissRequest,
|
||||
modifier = Modifier.focusRequester(focusRequester),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -83,6 +83,7 @@ import com.github.damontecres.wholphin.util.ExceptionHandler
|
|||
import com.github.damontecres.wholphin.util.LoadingState
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.util.Locale
|
||||
|
||||
@Composable
|
||||
fun PreferencesContent(
|
||||
|
|
@ -110,6 +111,7 @@ fun PreferencesContent(
|
|||
val seerrConnection by viewModel.seerrConnection.collectAsState()
|
||||
var seerrDialogMode by remember { mutableStateOf<SeerrDialogMode>(SeerrDialogMode.None) }
|
||||
var showQuickConnectDialog by remember { mutableStateOf(false) }
|
||||
var showLocaleChoiceDialog by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.preferenceDataStore.data.collect {
|
||||
|
|
@ -518,6 +520,24 @@ fun PreferencesContent(
|
|||
)
|
||||
}
|
||||
|
||||
AppPreference.UserInterfaceLanguage -> {
|
||||
val locale =
|
||||
remember(currentUser?.uiLanguage) {
|
||||
currentUser?.uiLanguage?.let { Locale.forLanguageTag(it) }
|
||||
?: Locale.getDefault()
|
||||
}
|
||||
ClickPreference(
|
||||
title = stringResource(pref.title),
|
||||
onClick = {
|
||||
showLocaleChoiceDialog = true
|
||||
},
|
||||
modifier = Modifier,
|
||||
summary = locale.getDisplayName(locale),
|
||||
onLongClick = null,
|
||||
interactionSource = interactionSource,
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
val value = pref.getter.invoke(preferences)
|
||||
ComposablePreference(
|
||||
|
|
@ -693,6 +713,11 @@ fun PreferencesContent(
|
|||
}
|
||||
}
|
||||
}
|
||||
if (showLocaleChoiceDialog) {
|
||||
LocaleChoiceDialog(
|
||||
onDismissRequest = { showLocaleChoiceDialog = false },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
1
app/src/main/res/resources.properties
Normal file
1
app/src/main/res/resources.properties
Normal file
|
|
@ -0,0 +1 @@
|
|||
unqualifiedResLocale=en-US
|
||||
|
|
@ -778,6 +778,7 @@
|
|||
<string name="skip_behavior_summary">For intros, credits, etc</string>
|
||||
<string name="play_with">Play with</string>
|
||||
<string name="transcoding">Transcoding</string>
|
||||
<string name="user_interface_language">User interface language</string>
|
||||
<array name="ass_subtitle_modes">
|
||||
<item>@string/ass_subtitle_mode_libass</item>
|
||||
<item>@string/ass_subtitle_mode_exoplayer</item>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue