feat(android): add notification sounds and zh-Hant support

pull/14/head
alimu 1 week ago
parent 16bcf34e4a
commit 02046bd3f4

@ -0,0 +1,20 @@
作品Notification sound effects
来源ear0
作者Risteard
许可CC-BY 保留署名许可协议
描述:通知铃声。
-------------------------------------------------
作品load
来源ear0
作者weidu27
许可CC0 公众共享许可协议
描述:叮咚
-------------------------------------------------
作品nameit5
来源Freesound
作者bumpelsnake
许可CC-BY 保留署名许可协议
描述some chime, maybe a notification sound
-------------------------------------------------
Thank you to all the creators mentioned above.
感谢上述所有创作者。

@ -24,7 +24,8 @@ data class UserPreferences(
val shouldAutoReconnect: Boolean,
val themeId: String = "blue",
val useDynamicColor: Boolean = true,
val language: String = "zh" // 默认中文
val language: String = "zh", // 默认中文
val notificationSound: String = "default"
)
class UserPreferencesRepository(
@ -50,7 +51,8 @@ class UserPreferencesRepository(
shouldAutoReconnect = prefs[KEY_SHOULD_AUTO_RECONNECT] ?: false,
themeId = prefs[KEY_THEME_ID] ?: "blue",
useDynamicColor = prefs[KEY_USE_DYNAMIC_COLOR] ?: true,
language = prefs[KEY_LANGUAGE] ?: "zh"
language = prefs[KEY_LANGUAGE] ?: "zh",
notificationSound = prefs[KEY_NOTIFICATION_SOUND] ?: "default"
)
}
@ -66,6 +68,12 @@ class UserPreferencesRepository(
}
}
suspend fun setNotificationSound(sound: String) {
context.dataStore.edit { prefs ->
prefs[KEY_NOTIFICATION_SOUND] = sound
}
}
suspend fun setUseDynamicColor(enabled: Boolean) {
context.dataStore.edit { prefs ->
prefs[KEY_USE_DYNAMIC_COLOR] = enabled
@ -155,5 +163,6 @@ class UserPreferencesRepository(
val KEY_THEME_ID: Preferences.Key<String> = stringPreferencesKey("theme_id")
val KEY_USE_DYNAMIC_COLOR: Preferences.Key<Boolean> = booleanPreferencesKey("use_dynamic_color")
val KEY_LANGUAGE: Preferences.Key<String> = stringPreferencesKey("language")
val KEY_NOTIFICATION_SOUND: Preferences.Key<String> = stringPreferencesKey("notification_sound")
}
}

@ -46,6 +46,7 @@ import androidx.compose.material.icons.rounded.Forum
import androidx.compose.material.icons.rounded.Keyboard
import androidx.compose.material.icons.rounded.Settings
import androidx.compose.material.icons.rounded.Language
import androidx.compose.material.icons.rounded.MusicNote
import androidx.compose.material.icons.rounded.PlayArrow
import androidx.compose.material.icons.rounded.Stop
import androidx.compose.material.icons.rounded.KeyboardVoice
@ -241,7 +242,8 @@ fun OnlineMsgApp(viewModel: ChatViewModel = viewModel()) {
onClearMessages = viewModel::clearMessages,
onThemeChange = viewModel::updateTheme,
onUseDynamicColorChange = viewModel::updateUseDynamicColor,
onLanguageChange = viewModel::updateLanguage
onLanguageChange = viewModel::updateLanguage,
onNotificationSoundChange = viewModel::updateNotificationSound
)
}
}
@ -1038,7 +1040,8 @@ private fun SettingsTab(
onClearMessages: () -> Unit,
onThemeChange: (String) -> Unit,
onUseDynamicColorChange: (Boolean) -> Unit,
onLanguageChange: (String) -> Unit
onLanguageChange: (String) -> Unit,
onNotificationSoundChange: (String) -> Unit
) {
fun t(key: String) = LanguageManager.getString(key, state.language)
@ -1073,6 +1076,33 @@ private fun SettingsTab(
}
}
item {
Card(modifier = settingsCardModifier) {
Column(
modifier = settingsCardContentModifier,
verticalArrangement = settingsCardContentSpacing
) {
Text(t("settings.notification_sound"), style = MaterialTheme.typography.titleMedium)
LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
items(listOf("default", "ding", "nameit5", "wind_chime")) { sound ->
FilterChip(
selected = state.notificationSound == sound,
onClick = { onNotificationSoundChange(sound) },
label = { Text(t("sound.$sound")) },
leadingIcon = {
Icon(
imageVector = Icons.Rounded.MusicNote,
contentDescription = null,
modifier = Modifier.size(16.dp)
)
}
)
}
}
}
}
}
item {
Card(modifier = settingsCardModifier) {
Column(

@ -5,9 +5,12 @@ import android.app.Application
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.media.AudioAttributes
import android.net.Uri
import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
@ -201,14 +204,13 @@ object ChatSessionManager {
preferencesRepository = UserPreferencesRepository(application, json)
cryptoManager = RsaCryptoManager(application)
historyRepository = ChatHistoryRepository(ChatDatabase.getInstance(application).chatMessageDao())
ensureMessageNotificationChannel()
scope.launch {
val pref = preferencesRepository.preferencesFlow.first()
val historyMessages = withContext(Dispatchers.IO) {
historyRepository.loadMessages(MAX_MESSAGES)
}
keepAliveRequested = pref.shouldAutoReconnect
ensureMessageNotificationChannel(pref.notificationSound)
_uiState.update { current ->
current.copy(
displayName = pref.displayName,
@ -219,6 +221,7 @@ object ChatSessionManager {
themeId = pref.themeId,
useDynamicColor = pref.useDynamicColor,
language = pref.language,
notificationSound = pref.notificationSound,
messages = historyMessages
)
}
@ -253,6 +256,18 @@ object ChatSessionManager {
}
}
/**
* 更新通知音效
* @param sound 音效代号
*/
fun updateNotificationSound(sound: String) {
_uiState.update { it.copy(notificationSound = sound) }
scope.launch {
preferencesRepository.setNotificationSound(sound)
ensureMessageNotificationChannel(sound)
}
}
/**
* 更改使用动态颜色
* @param enabled 主题名
@ -1467,19 +1482,42 @@ object ChatSessionManager {
/**
* 创建消息通知渠道Android O+
*/
private fun ensureMessageNotificationChannel() {
private fun ensureMessageNotificationChannel(soundCode: String = "default") {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
val manager = app.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val channelId = "${MESSAGE_CHANNEL_ID}_$soundCode"
if (manager.getNotificationChannel(channelId) != null) return
val channel = NotificationChannel(
MESSAGE_CHANNEL_ID,
channelId,
t("session.notification.channel_name"),
NotificationManager.IMPORTANCE_DEFAULT
).apply {
description = t("session.notification.channel_desc")
getSoundUri(soundCode)?.let { uri ->
setSound(
uri,
AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_NOTIFICATION)
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.build()
)
}
}
manager.createNotificationChannel(channel)
}
private fun getSoundUri(code: String): Uri? {
val resId = when (code) {
"ding" -> R.raw.load
"nameit5" -> R.raw.nameit5
"wind_chime" -> R.raw.notification_sound_effects
"default" -> R.raw.default_sound
else -> return null
}
return Uri.parse("${ContentResolver.SCHEME_ANDROID_RESOURCE}://${app.packageName}/$resId")
}
/**
* 显示新消息到达的通知
* @param title 通知标题
@ -1502,8 +1540,10 @@ object ChatSessionManager {
launchIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val channelId = "${MESSAGE_CHANNEL_ID}_${_uiState.value.notificationSound}"
ensureMessageNotificationChannel(_uiState.value.notificationSound)
val notification = NotificationCompat.Builder(app, MESSAGE_CHANNEL_ID)
val notification = NotificationCompat.Builder(app, channelId)
.setSmallIcon(android.R.drawable.stat_notify_chat)
.setContentTitle(title.ifBlank { "OnlineMsg" })
.setContentText(body.take(120))

@ -97,7 +97,8 @@ data class ChatUiState(
val loadingPublicKey: Boolean = false,
val themeId: String = "blue",
val useDynamicColor: Boolean = true,
val language: String = "zh"
val language: String = "zh",
val notificationSound: String = "default"
) {
/**
* 是否允许连接

@ -37,4 +37,5 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
fun updateTheme(themeId: String) = ChatSessionManager.updateTheme(themeId)
fun updateUseDynamicColor(enabled: Boolean) = ChatSessionManager.updateUseDynamicColor(enabled)
fun updateLanguage(language: String) = ChatSessionManager.updateLanguage(language)
fun updateNotificationSound(sound: String) = ChatSessionManager.updateNotificationSound(sound)
}

@ -6,6 +6,76 @@ package com.onlinemsg.client.util
*/
object LanguageManager {
private val zhHantOverrides = mapOf(
"tab.settings" to "設定",
"settings.personal" to "個人設定",
"settings.display_name" to "顯示名稱",
"settings.server" to "伺服器",
"settings.server_url" to "伺服器位址",
"settings.save_server" to "儲存位址",
"settings.remove_current" to "刪除目前",
"settings.saved_servers" to "已儲存位址",
"settings.identity" to "身份與安全",
"settings.reveal_key" to "檢視/產生公鑰",
"settings.copy_key" to "複製公鑰",
"settings.my_key" to "我的公鑰",
"settings.theme" to "主題",
"settings.preset_themes" to "預設主題",
"settings.language" to "語言",
"settings.diagnostics" to "診斷",
"settings.status_hint" to "連線提示",
"settings.current_status" to "目前狀態",
"settings.cert_fingerprint" to "憑證指紋",
"settings.show_system" to "顯示系統訊息",
"settings.clear_msg" to "清空訊息",
"settings.chat_data" to "聊天資料",
"settings.dynamic_color" to "使用動態顏色",
"settings.notification_sound" to "通知音效",
"status.idle" to "未連線",
"status.connecting" to "連線中",
"status.ready" to "已連線",
"status.error" to "異常中斷",
"hint.tap_to_connect" to "點擊連線開始聊天",
"hint.connecting_server" to "正在連線伺服器...",
"hint.ready_chat" to "已連線,可以開始聊天",
"hint.closed" to "連線已關閉",
"hint.reconnecting" to "連線已中斷,正在重試",
"hint.reconnect_invalid_server" to "重連失敗:伺服器位址無效",
"hint.fill_target_key" to "請先填寫目標公鑰,再傳送私訊",
"hint.server_rejected_prefix" to "伺服器拒絕連線:",
"hint.audio_send_failed_prefix" to "語音傳送失敗:",
"chat.private" to "私訊",
"chat.target_key" to "目標公鑰",
"chat.input_placeholder" to "輸入訊息",
"chat.send" to "傳送",
"chat.sending" to "傳送中",
"chat.empty_hint" to "連線後即可聊天。預設為廣播,切換到私訊後可填寫目標公鑰。",
"chat.mode_text" to "文字",
"chat.mode_audio" to "語音",
"chat.audio_hold_to_talk" to "按住說話",
"chat.audio_release_send" to "鬆開即可傳送",
"chat.audio_release_cancel" to "鬆開即可取消",
"chat.audio_slide_cancel" to "按住時上滑即可取消",
"chat.audio_canceled" to "已取消語音傳送",
"chat.audio_sent" to "語音訊息已傳送",
"chat.audio_too_short" to "錄音時間太短",
"chat.audio_too_long" to "錄音時間太長",
"chat.audio_record_failed" to "錄音失敗",
"chat.audio_permission_required" to "需要麥克風權限",
"chat.audio_recording" to "錄音中",
"chat.audio_play" to "播放語音",
"chat.audio_stop" to "停止播放",
"sound.default" to "預設",
"sound.ding" to "",
"sound.nameit5" to "音效 5",
"sound.wind_chime" to "風鈴",
"theme.blue" to "蔚藍",
"theme.gray" to "商務灰",
"theme.green" to "翠綠",
"theme.red" to "緋紅",
"theme.warm" to "溫暖"
)
private val translations = mapOf(
"zh" to mapOf(
"tab.chat" to "聊天",
@ -34,6 +104,11 @@ object LanguageManager {
"settings.clear_msg" to "清空消息",
"settings.chat_data" to "聊天数据",
"settings.dynamic_color" to "使用动态颜色",
"settings.notification_sound" to "通知音效",
"sound.default" to "默认",
"sound.ding" to "",
"sound.nameit5" to "音效 5",
"sound.wind_chime" to "风铃",
"status.idle" to "未连接",
"status.connecting" to "连接中",
"status.ready" to "已连接",
@ -183,6 +258,11 @@ object LanguageManager {
"settings.clear_msg" to "ClearMsg",
"settings.chat_data" to "Chat Data",
"settings.dynamic_color" to "Use dynamic color",
"settings.notification_sound" to "Notification Sound",
"sound.default" to "Default",
"sound.ding" to "Ding",
"sound.nameit5" to "Sound 5",
"sound.wind_chime" to "Wind Chime",
"status.idle" to "Offline",
"status.connecting" to "Connecting",
"status.ready" to "Connected",
@ -332,6 +412,11 @@ object LanguageManager {
"settings.clear_msg" to "履歴を消去",
"settings.chat_data" to "チャットデータ",
"settings.dynamic_color" to "動的カラーを使用",
"settings.notification_sound" to "通知音",
"sound.default" to "デフォルト",
"sound.ding" to "ディン",
"sound.nameit5" to "効果音 5",
"sound.wind_chime" to "風鈴",
"status.idle" to "未接続",
"status.connecting" to "接続中",
"status.ready" to "接続済み",
@ -481,6 +566,11 @@ object LanguageManager {
"settings.clear_msg" to "정보 삭제",
"settings.chat_data" to "채팅 데이터",
"settings.dynamic_color" to "동적 색상 사용",
"settings.notification_sound" to "알림 효과음",
"sound.default" to "기본값",
"sound.ding" to "",
"sound.nameit5" to "효과음 5",
"sound.wind_chime" to "풍경",
"status.idle" to "연결 안 됨",
"status.connecting" to "연결 중",
"status.ready" to "연결됨",
@ -602,15 +692,24 @@ object LanguageManager {
"theme.green" to "초록",
"theme.red" to "빨강",
"theme.warm" to "따뜻함"
)
),
"zh-Hant" to zhHantOverrides
)
fun getString(key: String, lang: String): String {
return translations[lang]?.get(key) ?: translations["en"]?.get(key) ?: key
return when (lang) {
"zh-Hant" -> translations["zh-Hant"]?.get(key)
?: translations["zh"]?.get(key)
?: translations["en"]?.get(key)
?: key
else -> translations[lang]?.get(key) ?: translations["en"]?.get(key) ?: key
}
}
val supportedLanguages = listOf(
LanguageOption("zh", "中文"),
LanguageOption("zh", "中文简体"),
LanguageOption("zh-Hant", "繁體中文"),
LanguageOption("en", "English"),
LanguageOption("ja", "日本语"),
LanguageOption("ko", "한국어")

Loading…
Cancel
Save