diff --git a/manager/app/src/main/java/com/sukisu/ultra/KernelSUApplication.kt b/manager/app/src/main/java/com/sukisu/ultra/KernelSUApplication.kt index 40061fba..8f9ae1e7 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/KernelSUApplication.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/KernelSUApplication.kt @@ -1,17 +1,63 @@ package com.sukisu.ultra +import android.annotation.SuppressLint import android.app.Application +import android.content.Context +import android.content.res.Configuration +import android.content.res.Resources +import android.os.Build import coil.Coil import coil.ImageLoader import com.dergoogler.mmrl.platform.Platform import me.zhanghai.android.appiconloader.coil.AppIconFetcher import me.zhanghai.android.appiconloader.coil.AppIconKeyer import java.io.File +import java.util.Locale lateinit var ksuApp: KernelSUApplication class KernelSUApplication : Application() { + override fun attachBaseContext(base: Context) { + val prefs = base.getSharedPreferences("settings", MODE_PRIVATE) + val languageCode = prefs.getString("app_language", "") ?: "" + + var context = base + if (languageCode.isNotEmpty()) { + val locale = Locale.forLanguageTag(languageCode) + Locale.setDefault(locale) + + val config = Configuration(base.resources.configuration) + config.setLocale(locale) + + context = base.createConfigurationContext(config) + } + + super.attachBaseContext(context) + } + + @SuppressLint("ObsoleteSdkInt") + override fun getResources(): Resources { + val resources = super.getResources() + val prefs = getSharedPreferences("settings", MODE_PRIVATE) + val languageCode = prefs.getString("app_language", "") ?: "" + + if (languageCode.isNotEmpty()) { + val locale = Locale.forLanguageTag(languageCode) + val config = Configuration(resources.configuration) + config.setLocale(locale) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + return createConfigurationContext(config).resources + } else { + @Suppress("DEPRECATION") + resources.updateConfiguration(config, resources.displayMetrics) + } + } + + return resources + } + override fun onCreate() { super.onCreate() ksuApp = this @@ -35,5 +81,30 @@ class KernelSUApplication : Application() { } } + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + applyLanguageSetting() + } + @SuppressLint("ObsoleteSdkInt") + private fun applyLanguageSetting() { + val prefs = getSharedPreferences("settings", MODE_PRIVATE) + val languageCode = prefs.getString("app_language", "") ?: "" + + if (languageCode.isNotEmpty()) { + val locale = Locale.forLanguageTag(languageCode) + Locale.setDefault(locale) + + val resources = resources + val config = Configuration(resources.configuration) + config.setLocale(locale) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + createConfigurationContext(config) + } else { + @Suppress("DEPRECATION") + resources.updateConfiguration(config, resources.displayMetrics) + } + } + } } \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/MainActivity.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/MainActivity.kt index 7e08653b..93c755cc 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/MainActivity.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/MainActivity.kt @@ -1,6 +1,8 @@ package com.sukisu.ultra.ui +import android.annotation.SuppressLint import android.content.Context +import android.content.res.Configuration import android.database.ContentObserver import android.os.Build import android.os.Bundle @@ -36,6 +38,7 @@ import com.sukisu.ultra.ui.util.* import androidx.core.content.edit import com.sukisu.ultra.ui.theme.CardConfig.cardElevation import com.sukisu.ultra.ui.webui.initPlatform +import java.util.Locale class MainActivity : ComponentActivity() { private inner class ThemeChangeContentObserver( @@ -48,8 +51,49 @@ class MainActivity : ComponentActivity() { } } + // 应用保存的语言设置 + @SuppressLint("ObsoleteSdkInt") + private fun applyLanguageSetting() { + val prefs = getSharedPreferences("settings", MODE_PRIVATE) + val languageCode = prefs.getString("app_language", "") ?: "" + + if (languageCode.isNotEmpty()) { + val locale = Locale.forLanguageTag(languageCode) + Locale.setDefault(locale) + + val resources = resources + val config = Configuration(resources.configuration) + config.setLocale(locale) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + createConfigurationContext(config) + } else { + @Suppress("DEPRECATION") + resources.updateConfiguration(config, resources.displayMetrics) + } + } + } + + override fun attachBaseContext(newBase: Context) { + val prefs = newBase.getSharedPreferences("settings", MODE_PRIVATE) + val languageCode = prefs.getString("app_language", "") ?: "" + + var context = newBase + if (languageCode.isNotEmpty()) { + val locale = Locale.forLanguageTag(languageCode) + Locale.setDefault(locale) + + val config = Configuration(newBase.resources.configuration) + config.setLocale(locale) + context = newBase.createConfigurationContext(config) + } + + super.attachBaseContext(context) + } + override fun onCreate(savedInstanceState: Bundle?) { - // 应用DPI设置(仅对当前应用生效) + // 确保应用正确的语言设置 + applyLanguageSetting() + applyCustomDpi() // Enable edge to edge @@ -138,7 +182,7 @@ class MainActivity : ComponentActivity() { } } - // 应用自定义DPI设置(仅对当前应用生效) + // 应用自定义DPI设置 private fun applyCustomDpi() { val prefs = getSharedPreferences("settings", MODE_PRIVATE) val customDpi = prefs.getInt("app_dpi", 0) @@ -147,9 +191,8 @@ class MainActivity : ComponentActivity() { try { val resources = resources val metrics = resources.displayMetrics - - // 仅更新应用内显示,不影响系统状态栏 metrics.density = customDpi / 160f + @Suppress("DEPRECATION") metrics.scaledDensity = customDpi / 160f metrics.densityDpi = customDpi } catch (e: Exception) { @@ -169,6 +212,8 @@ class MainActivity : ComponentActivity() { override fun onResume() { super.onResume() + applyLanguageSetting() + if (!ThemeConfig.backgroundImageLoaded && !ThemeConfig.preventBackgroundRefresh) { loadCustomBackground() } @@ -180,6 +225,11 @@ class MainActivity : ComponentActivity() { destroyListeners.forEach { it() } super.onDestroy() } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + applyLanguageSetting() + } } @OptIn(ExperimentalMaterial3Api::class) diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/SettingsItem.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/SettingsItem.kt index 17647443..59bcc68d 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/component/SettingsItem.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/component/SettingsItem.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.selection.toggleable import androidx.compose.material3.Icon import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.RadioButton import androidx.compose.material3.Switch import androidx.compose.material3.Text @@ -62,7 +63,8 @@ fun SwitchItem( Icon( modifier = Modifier.then(stateAlpha), imageVector = icon, - contentDescription = title + contentDescription = title, + tint = MaterialTheme.colorScheme.primary ) } }, diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/SwitchItem.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/SwitchItem.kt index ed9d06ae..4d994a42 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/component/SwitchItem.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/component/SwitchItem.kt @@ -60,7 +60,7 @@ fun SwitchItem( Text( text = title, style = MaterialTheme.typography.titleMedium, - maxLines = 1, + maxLines = Int.MAX_VALUE, overflow = TextOverflow.Ellipsis ) }, diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/MoreSettings.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/MoreSettings.kt index 40f889a5..5eb61438 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/MoreSettings.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/MoreSettings.kt @@ -1,7 +1,10 @@ package com.sukisu.ultra.ui.screen +import android.annotation.SuppressLint +import android.app.Activity import android.content.Context import android.content.Intent +import android.content.res.Configuration import android.net.Uri import android.os.Build import android.widget.Toast @@ -36,6 +39,7 @@ import androidx.compose.material.icons.filled.ColorLens import androidx.compose.material.icons.filled.DarkMode import androidx.compose.material.icons.filled.KeyboardArrowDown import androidx.compose.material.icons.filled.KeyboardArrowUp +import androidx.compose.material.icons.filled.Language import androidx.compose.material.icons.filled.Opacity import androidx.compose.material.icons.filled.Palette import androidx.compose.material.icons.filled.Security @@ -84,6 +88,7 @@ import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.sukisu.ultra.R +import com.sukisu.ultra.ui.MainActivity import com.sukisu.ultra.ui.component.ImageEditorDialog import com.sukisu.ultra.ui.component.KsuIsValid import com.sukisu.ultra.ui.component.SwitchItem @@ -103,12 +108,14 @@ import com.sukisu.ultra.ui.util.susfsSUS_SU_Mode import com.topjohnwu.superuser.Shell import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import java.util.Locale import kotlin.math.roundToInt fun saveCardConfig(context: Context) { CardConfig.save(context) } +@SuppressLint("LocalContextConfigurationRead", "ObsoleteSdkInt") @OptIn(ExperimentalMaterial3Api::class) @Destination @Composable @@ -143,6 +150,128 @@ fun MoreSettingsScreen(navigator: DestinationsNavigator) { stringResource(R.string.theme_dark) ) + // 获取当前语言设置 + var currentLanguage by remember { + mutableStateOf(prefs.getString("app_language", "") ?: "") + } + + // 获取支持的语言列表 + val supportedLanguages = remember { + val languages = mutableListOf>() + languages.add("" to context.getString(R.string.language_follow_system)) + val locales = context.resources.configuration.locales + for (i in 0 until locales.size()) { + val locale = locales.get(i) + val code = locale.toLanguageTag() + if (!languages.any { it.first == code }) { + languages.add(code to locale.getDisplayName(locale)) + } + } + + val commonLocales = listOf( + Locale.forLanguageTag("en"), + Locale.forLanguageTag("zh-CN"), + Locale.forLanguageTag("zh-HK"), + Locale.forLanguageTag("zh-TW"), + Locale.forLanguageTag("ja"), // 日语 + Locale.forLanguageTag("fr"), // 法语 + Locale.forLanguageTag("de"), // 德语 + Locale.forLanguageTag("es"), // 西班牙语 + Locale.forLanguageTag("it"), // 意大利语 + Locale.forLanguageTag("ru"), // 俄语 + Locale.forLanguageTag("pt"), // 葡萄牙语 + Locale.forLanguageTag("ko") // 韩语 + ) + + for (locale in commonLocales) { + val code = locale.toLanguageTag() + if (!languages.any { it.first == code }) { + val config = Configuration(context.resources.configuration) + config.setLocale(locale) + try { + val testContext = context.createConfigurationContext(config) + testContext.getString(R.string.language_follow_system) + languages.add(code to locale.getDisplayName(locale)) + } catch (_: Exception) { + } + } + } + languages + } + + var showLanguageDialog by remember { mutableStateOf(false) } + + // 语言切换对话框 + if (showLanguageDialog) { + AlertDialog( + onDismissRequest = { showLanguageDialog = false }, + title = { Text(stringResource(R.string.language_setting)) }, + text = { + Column(modifier = Modifier.verticalScroll(rememberScrollState())) { + supportedLanguages.forEach { (code, name) -> + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { + if (currentLanguage != code) { + prefs.edit { + putString("app_language", code) + commit() + } + + currentLanguage = code + + Toast.makeText( + context, + context.getString(R.string.language_changed), + Toast.LENGTH_SHORT + ).show() + + val locale = if (code.isEmpty()) Locale.getDefault() else Locale.forLanguageTag(code) + Locale.setDefault(locale) + val config = Configuration(context.resources.configuration) + config.setLocale(locale) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + context.createConfigurationContext(config) + } else { + @Suppress("DEPRECATION") + context.resources.updateConfiguration(config, context.resources.displayMetrics) + } + + val intent = Intent(context, MainActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(intent) + + if (context is Activity) { + context.finish() + } + } + showLanguageDialog = false + } + .padding(vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton( + selected = currentLanguage == code, + onClick = null + ) + Spacer(modifier = Modifier.width(8.dp)) + Text(name) + } + } + } + }, + confirmButton = { + TextButton( + onClick = { showLanguageDialog = false } + ) { + Text(stringResource(R.string.cancel)) + } + } + ) + } + // 简洁模式开关状态 var isSimpleMode by remember { mutableStateOf(prefs.getBoolean("is_simple_mode", false)) @@ -408,6 +537,35 @@ fun MoreSettingsScreen(navigator: DestinationsNavigator) { onToggle = { isAppearanceExpanded = !isAppearanceExpanded } ) + AnimatedVisibility( + visible = isAppearanceExpanded, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically() + ) { + ListItem( + headlineContent = { Text(stringResource(R.string.language_setting)) }, + supportingContent = { + Text(supportedLanguages.find { it.first == currentLanguage }?.second + ?: stringResource(R.string.language_follow_system)) + }, + leadingContent = { + Icon( + Icons.Default.Language, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + }, + trailingContent = { + Icon( + Icons.AutoMirrored.Filled.NavigateNext, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + }, + modifier = Modifier.clickable { showLanguageDialog = true } + ) + } + AnimatedVisibility( visible = isAppearanceExpanded, enter = fadeIn() + expandVertically(), diff --git a/manager/app/src/main/res/values-zh-rCN/strings.xml b/manager/app/src/main/res/values-zh-rCN/strings.xml index 8a8fcd87..dabd1847 100644 --- a/manager/app/src/main/res/values-zh-rCN/strings.xml +++ b/manager/app/src/main/res/values-zh-rCN/strings.xml @@ -342,4 +342,8 @@ 你确定要将应用DPI从 %1$d 更改为 %2$d 吗? 应用需要重启以应用新的DPI设置,不会影响系统状态栏或其他应用 DPI 已设置为 %1$d,重启应用后生效 + + 应用语言 + 跟随系统 + 语言已更改,重启应用以应用更改 diff --git a/manager/app/src/main/res/values/strings.xml b/manager/app/src/main/res/values/strings.xml index 653987c5..745edaee 100644 --- a/manager/app/src/main/res/values/strings.xml +++ b/manager/app/src/main/res/values/strings.xml @@ -346,4 +346,8 @@ Are you sure you want to change the application DPI from %1$d to %2$d? Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications DPI has been set to %1$d, effective after restarting the application + + App Language + Follow System + Language changed, restarting to apply changes