diff --git a/app/src/main/java/com/stand/hypnosis/Constants.kt b/app/src/main/java/com/stand/hypnosis/Constants.kt new file mode 100644 index 0000000..381c521 --- /dev/null +++ b/app/src/main/java/com/stand/hypnosis/Constants.kt @@ -0,0 +1,18 @@ +package com.stand.hypnosis + +import androidx.annotation.Keep + +object AppInfo { + + @Keep + const val COPYRIGHT = "Copyright © 2025 " + + object Author { + @Keep + const val NAME = "Stand" + @Keep + const val BILI_URL = "https://space.bilibili.com/382365750" + @Keep + const val GITHUB_URL = "https://github.com/Stand404/HypnosisAPP" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/stand/hypnosis/MainActivity.kt b/app/src/main/java/com/stand/hypnosis/MainActivity.kt index f54c5b2..704e76c 100644 --- a/app/src/main/java/com/stand/hypnosis/MainActivity.kt +++ b/app/src/main/java/com/stand/hypnosis/MainActivity.kt @@ -1,5 +1,6 @@ package com.stand.hypnosis +import android.content.Context import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent @@ -16,6 +17,8 @@ import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat import androidx.lifecycle.lifecycleScope +import com.stand.hypnosis.utils.AppLanguageUtils.applyLanguageConfiguration +import com.stand.hypnosis.utils.AppLanguageUtils.readLanguageTag import kotlinx.coroutines.launch class MainActivity : ComponentActivity() { @@ -63,6 +66,12 @@ class MainActivity : ComponentActivity() { } } + override fun attachBaseContext(newBase: Context) { + val localeTag = readLanguageTag(newBase) + val newContext = applyLanguageConfiguration(newBase, localeTag) + super.attachBaseContext(newContext) + } + fun initConfig() { // 初始化配置管理器 configManager = ConfigManager.getInstance(this) diff --git a/app/src/main/java/com/stand/hypnosis/TopSwipeDrawer.kt b/app/src/main/java/com/stand/hypnosis/TopSwipeDrawer.kt index 95b8cdb..d5525ee 100644 --- a/app/src/main/java/com/stand/hypnosis/TopSwipeDrawer.kt +++ b/app/src/main/java/com/stand/hypnosis/TopSwipeDrawer.kt @@ -1,5 +1,6 @@ package com.stand.hypnosis +import android.content.Context import androidx.compose.animation.core.CubicBezierEasing import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.animateFloatAsState @@ -13,8 +14,12 @@ import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowDropDown import androidx.compose.material3.AlertDialog +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Slider import androidx.compose.material3.Switch @@ -43,14 +48,21 @@ import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.stand.hypnosis.model.Language +import com.stand.hypnosis.utils.AppLanguageUtils.changeAppLanguage +import com.stand.hypnosis.utils.AppLanguageUtils.readLanguageTag +import com.stand.hypnosis.utils.AppLanguageUtils.saveLanguageTag import kotlinx.coroutines.delay import kotlin.math.roundToInt @@ -69,7 +81,11 @@ fun TopSwipeDrawerLayer( var isFullScreen by remember { mutableStateOf(configManager.isFullScreen) } var ringColor by remember { mutableIntStateOf(configManager.ringColor) } var bgColor by remember { mutableIntStateOf(configManager.bgColor) } - + val context = LocalContext.current + // 读取pref中存储的语言 + val (currentLanguageTag, setCurrentLanguageTag) = remember(context) { + mutableStateOf(readLanguageTag(context)) + } // 当配置改变时,直接更新 ConfigManager fun updateIsBeating(value: Boolean) { isBeating = value @@ -213,10 +229,11 @@ fun TopSwipeDrawerLayer( ) ){ Column{ - Text("设置面板", style = MaterialTheme.typography.headlineMedium, + Text( + stringResource(R.string.settings_panel), style = MaterialTheme.typography.headlineMedium, color = Color.White, modifier = Modifier.padding(top = 35.dp, start = 16.dp)) - Text("往下滑动打开,往上滑动关闭", + Text(stringResource(R.string.panel_swipe_hint), color = Color.White, modifier = Modifier.padding(top = 10.dp, start = 16.dp, bottom = 20.dp)) @@ -225,7 +242,7 @@ fun TopSwipeDrawerLayer( // 心跳动画开关 { SwitchSettingItem( - title = "心跳动画", + title = stringResource(R.string.heartbeat_animation), checked = isBeating, onCheckedChange = { newValue -> updateIsBeating(newValue) @@ -234,7 +251,7 @@ fun TopSwipeDrawerLayer( }, { SwitchSettingItem( - title = "全屏", + title = stringResource(R.string.fullscreen), checked = isFullScreen, onCheckedChange = { newValue -> updateIsFullScreen(newValue) @@ -244,7 +261,7 @@ fun TopSwipeDrawerLayer( // 圆环颜色选择 { ColorSettingItem( - title = "动画颜色", + title = stringResource(R.string.animation_color), color = ringColor, onColorClick = { showRingColorPicker = true } ) @@ -252,10 +269,24 @@ fun TopSwipeDrawerLayer( // 背景颜色选择 { ColorSettingItem( - title = "背景颜色", + title = stringResource(R.string.background_color), color = bgColor, onColorClick = { showBgColorPicker = true } ) + }, + // 语言选择下拉 + { + LanguageSettingItem( + currentLocaleTag = currentLanguageTag, + onLanguageSelected = {context, newLocaleTag -> + saveLanguageTag(context, newLocaleTag) // 持久化语言 + setCurrentLanguageTag(newLocaleTag) // 更新 Compose 状态 + changeAppLanguage( + context, + newLocaleTag + ) // 重建 Activity 加载资源 + } + ) } // 添加更多配置项... ) @@ -285,31 +316,31 @@ fun TopSwipeDrawerLayer( // 第一行:版权信息 Row { Text( - text = "Copyright © 2025 ", + text = AppInfo.COPYRIGHT, color = Color.White.copy(0.7f), fontSize = 12.sp ) Text( - text = "Stand", + text = AppInfo.Author.NAME, color = Color(0xFF29C6FF), fontSize = 12.sp, textDecoration = TextDecoration.Underline, modifier = Modifier.clickable { - uriHandler.openUri("https://space.bilibili.com/382365750") + uriHandler.openUri(AppInfo.Author.BILI_URL) } ) } // 第二行:GitHub 链接 Text( - text = "GitHub开源地址", + text = stringResource(R.string.github_repo), color = Color(0xFF6CC644), // GitHub 绿色 fontSize = 12.sp, textDecoration = TextDecoration.Underline, modifier = Modifier .clickable { - uriHandler.openUri("https://github.com/Stand404/HypnosisAPP") + uriHandler.openUri(AppInfo.Author.GITHUB_URL) } .padding(top = 4.dp) ) @@ -451,7 +482,7 @@ fun ColorPickerDialog( AlertDialog( onDismissRequest = onDismiss, - title = { Text("选择颜色")}, + title = { Text(stringResource(R.string.choose_color))}, text = { Column( modifier = Modifier @@ -476,15 +507,16 @@ fun ColorPickerDialog( Spacer(modifier = Modifier.height(16.dp)) // 红色通道 + Text( + stringResource(R.string.color_red), + modifier = Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.secondary + ) Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth() ) { - Text( - "红", - modifier = Modifier.width(24.dp), - color = MaterialTheme.colorScheme.secondary - ) + Slider( value = red, onValueChange = { red = it }, @@ -498,15 +530,15 @@ fun ColorPickerDialog( } // 绿色通道 + Text( + stringResource(R.string.color_green), + modifier = Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.secondary + ) Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth() ) { - Text( - "绿", - modifier = Modifier.width(24.dp), - color = MaterialTheme.colorScheme.secondary - ) Slider( value = green, onValueChange = { green = it }, @@ -520,15 +552,15 @@ fun ColorPickerDialog( } // 蓝色通道 + Text( + stringResource(R.string.color_blue), + modifier = Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.secondary + ) Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth() ) { - Text( - "蓝", - modifier = Modifier.width(24.dp), - color = MaterialTheme.colorScheme.secondary - ) Slider( value = blue, onValueChange = { blue = it }, @@ -545,13 +577,13 @@ fun ColorPickerDialog( // 快速预设颜色 val presetColors = listOf( - 0xFFFF0000 to "红", 0xFF00FF00 to "绿", 0xFF0000FF to "蓝", - 0xFFFFFF00 to "黄", 0xFFFF00FF to "紫", 0xFF00FFFF to "青", - 0xFFFFFFFF to "白", 0xFF000000 to "黑", 0xFFFFA500 to "橙", - 0xFF808080 to "灰", 0xFFA52A2A to "棕", 0xFFFFC0CB to "粉" + 0xFFFF0000 to stringResource(R.string.color_red), 0xFF00FF00 to stringResource(R.string.color_green), 0xFF0000FF to stringResource(R.string.color_blue), + 0xFFFFFF00 to stringResource(R.string.color_yellow), 0xFFFF00FF to stringResource(R.string.color_purple), 0xFF00FFFF to stringResource(R.string.color_cyan), + 0xFFFFFFFF to stringResource(R.string.color_white), 0xFF000000 to stringResource(R.string.color_black), 0xFFFFA500 to stringResource(R.string.color_orange), + 0xFF808080 to stringResource(R.string.color_gray), 0xFFA52A2A to stringResource(R.string.color_brown), 0xFFFFC0CB to stringResource(R.string.color_pink) ) - Text("快速选择:", modifier = Modifier.padding(vertical = 8.dp)) + Text(stringResource(R.string.quick_select), modifier = Modifier.padding(vertical = 8.dp)) LazyVerticalGrid( columns = GridCells.Fixed(4), verticalArrangement = Arrangement.spacedBy(8.dp), @@ -589,15 +621,76 @@ fun ColorPickerDialog( TextButton(onClick = { onDismiss onColorSelected(selectedColor.toArgb())}) { - Text("确定") + Text(stringResource(R.string.confirm)) } }, dismissButton = { TextButton(onClick = onDismiss) { - Text("取消") + Text(stringResource(R.string.cancel)) } }, titleContentColor = MaterialTheme.colorScheme.secondary, textContentColor = MaterialTheme.colorScheme.onSurface ) } + +// 语言选择下拉项 +@Composable +fun LanguageSettingItem( + currentLocaleTag: String, + onLanguageSelected: (Context, String) -> Unit +) { + val context = LocalContext.current + val (expanded, setExpanded) = remember { mutableStateOf(false) } + val supportedLanguages = listOf( + Language(name = "English", localeTag = "en"), + Language(name = "简体中文", localeTag = "zh"), + Language(name = "繁體中文", localeTag = "zh-rTW"), + Language(name = "日本語", localeTag = "ja") + ) + + val selectedLanguage = supportedLanguages.find { it.localeTag == currentLocaleTag } + val selectedLanguageName = selectedLanguage?.name ?: "System Default" + + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { setExpanded(true) } + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painter = painterResource(R.drawable.baseline_lang_24), + contentDescription = null, + tint = Color.White + ) + + Spacer(modifier = Modifier.width(20.dp)) + + Text(text = selectedLanguageName, + color = Color.White, + fontSize = 18.sp, + modifier = Modifier.weight(1f)) + + Icon( + imageVector = Icons.Default.ArrowDropDown, + contentDescription = null, + tint = Color.White + ) + + DropdownMenu( + expanded = expanded, + onDismissRequest = { setExpanded(false) } + ) { + supportedLanguages.forEach { language -> + DropdownMenuItem( + onClick = { + setExpanded(false) + onLanguageSelected(context, language.localeTag) + }, + text = { Text(text = language.name) }, + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/stand/hypnosis/model/Language.kt b/app/src/main/java/com/stand/hypnosis/model/Language.kt new file mode 100644 index 0000000..b840ea8 --- /dev/null +++ b/app/src/main/java/com/stand/hypnosis/model/Language.kt @@ -0,0 +1,6 @@ +package com.stand.hypnosis.model + +data class Language( + val name: String, + val localeTag: String +) \ No newline at end of file diff --git a/app/src/main/java/com/stand/hypnosis/utils/AppLanguage.kt b/app/src/main/java/com/stand/hypnosis/utils/AppLanguage.kt new file mode 100644 index 0000000..c9d7da4 --- /dev/null +++ b/app/src/main/java/com/stand/hypnosis/utils/AppLanguage.kt @@ -0,0 +1,94 @@ +package com.stand.hypnosis.utils + +import android.app.Activity +import android.content.Context +import java.util.Locale +import androidx.core.content.edit + +/** + * 含应用语言设置的持久化和配置应用工具 + * + * 提供应用语言的读取、保存、切换和配置功能,支持多语言切换和界面实时更新。 + * + * @author misaka10032w + * @since 2025年11月30日 + */ +object AppLanguageUtils { + const val PREFS_NAME = "app_settings" + const val KEY_LANG_TAG = "language_tag" + const val DEFAULT_LANG = "zh" + + /** + * 从 SharedPreferences 中读取用户保存的语言标签。 + * + * @param context 当前应用上下文,不能为null + * @return 返回保存的语言标签(如 "zh", "en"),如果未找到则返回 [DEFAULT_LANG] + * @see saveLanguageTag + */ + fun readLanguageTag(context: Context): String { + val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + return prefs.getString(KEY_LANG_TAG, DEFAULT_LANG) ?: DEFAULT_LANG + } + + /** + * 将新的语言标签保存到 SharedPreferences 中。 + * + * @param context 当前应用上下文,不能为null + * @param tag 要保存的语言标签(如 "zh", "ja"),不能为null或空字符串 + * @throws IllegalArgumentException 当tag为null或空时抛出 + * @see readLanguageTag + */ + fun saveLanguageTag(context: Context, tag: String) { + val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + prefs.edit { putString(KEY_LANG_TAG, tag) } + } + + /** + * 应用新的语言配置到当前 Context 的资源,并强制 Activity 重建以刷新 UI。 + * + * @param context 当前应用上下文,不能为null + * @param localeTag 要切换到的语言标签(如 "zh", "zh-rTW"),不能为null + * @see applyLanguageConfiguration + */ + fun changeAppLanguage(context: Context, localeTag: String) { + val locale = parseLocale(localeTag) + val resources = context.resources + val configuration = resources.configuration + configuration.setLocale(locale) + + @Suppress("DEPRECATION") + resources.updateConfiguration(configuration, resources.displayMetrics) + + (context as? Activity)?.recreate() + } + + /** + * 根据语言标签创建一个应用了新 Locale 配置的 Context。 + * 主要用于 Activity 的 attachBaseContext 方法,以确保在 Activity 创建之前加载正确的资源。 + * + * @param context 传入的基础 Context,不能为null + * @param localeTag 要应用的语言标签,不能为null + * @return 返回一个新的、应用了指定语言配置的 Context + * @see changeAppLanguage + */ + fun applyLanguageConfiguration(context: Context, localeTag: String): Context { + val locale = parseLocale(localeTag) + val config = context.resources.configuration + config.setLocale(locale) + return context.createConfigurationContext(config) + } + + /** + * 将语言标签解析为 Locale 对象。 + * + * @param localeTag 语言标签 + * @return 对应的 Locale 对象 + */ + private fun parseLocale(localeTag: String): Locale { + return if (localeTag == "zh-rTW") { + Locale.Builder().setLanguage("zh").setRegion("TW").build() + } else { + Locale.forLanguageTag(localeTag) + } + } +} diff --git a/app/src/main/res/drawable/baseline_lang_24.xml b/app/src/main/res/drawable/baseline_lang_24.xml new file mode 100644 index 0000000..9c0407d --- /dev/null +++ b/app/src/main/res/drawable/baseline_lang_24.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml new file mode 100644 index 0000000..50d9058 --- /dev/null +++ b/app/src/main/res/values-en/strings.xml @@ -0,0 +1,28 @@ + + + 催眠APPSettings Panel + Swipe down to open, swipe up to close + Heartbeat Animation + Fullscreen + Animation Color + Background Color + GitHub Repository + Choose Color + Red + Green + Blue + Yellow + Purple + Cyan + White + Black + Orange + Gray + Brown + Pink + Quick Select: + Confirm + Cancel + Language + + \ No newline at end of file diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml new file mode 100644 index 0000000..7c75e0a --- /dev/null +++ b/app/src/main/res/values-ja/strings.xml @@ -0,0 +1,29 @@ + + + 催眠APP + 設定パネル + 下スワイプで開く、上スワイプで閉じる + ハートビートアニメーション + 全画面 + アニメーション色 + 背景色 + GitHubリポジトリ + 色を選択 + + + + + + シアン + + + オレンジ + + + ピンク + クイック選択: + 確定 + キャンセル + 言語 + + \ No newline at end of file diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml new file mode 100644 index 0000000..d7ef48b --- /dev/null +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -0,0 +1,28 @@ + + + 催眠APP + 設定面板 + 向下滑動開啟,向上滑動關閉 + 心跳動畫 + 全螢幕 + 動畫顏色 + 背景顏色 + GitHub開源地址 + 選擇顏色 + + + + + + + + + + + + + 快速選擇: + 確定 + 取消 + 語言 + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e23b50a..df0a2d6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,31 @@ 催眠APP + 设置面板 + 向下滑动打开,向上滑动关闭 + 心跳动画 + 全屏 + 动画颜色 + 背景颜色 + GitHub开源地址 + 选择颜色 + + 绿 + + + + + + + + + + + 快速选择: + 确定 + 取消 + 语言 + 简体中文 + 正體中文 + 日本語 + English \ No newline at end of file