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