i18n: localize mutual match page to English

This commit is contained in:
atsunatsu committed 2026-08-07 19:53:36 +08:00
1 parent a8afc63bba
commit 80a58bed61
104 files changed
+18309 -63

No files matched your search

@@ -59,7 +59,6 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.constraintlayout.widget.ConstraintLayout
import com.ve3nea.morse_expert.MainActivity
@Composable
fun CwDecodeScreen(navigateUp: () -> Unit = {}) {
@@ -67,8 +66,8 @@ fun CwDecodeScreen(navigateUp: () -> Unit = {}) {
val activity = remember(context) {
context as? Activity ?: error("CwDecodeScreen must be hosted in an Activity")
}
// 照搬的控制器(普通类, 非 Activity); 每次进入页面新建实例
val controller = remember { MainActivity() }
// fldigi 解码器控制器; 每次进入页面新建实例
val controller = remember { FldigiCwController() }
// 提前 inflate 原版布局, 供 AndroidView 与控制器 onCreate 共用同一根视图
val rootView = remember(context) {
LayoutInflater.from(context).inflate(R.layout.activity_main, null) as ConstraintLayout
@@ -44,7 +44,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.ve3nea.morse_expert.MainActivity
import com.rtbishop.look4sat.feature.cw.FldigiCwController
import kotlin.math.roundToInt
/** 键名/默认值照搬原 Morse Expert(root_preferences.xml + SettingsActivity 逻辑)。 */
@@ -55,6 +55,30 @@ private const val DEFAULT_TEXT_FONT_SIZE = 18
private const val MIN_FONT_SIZE = 7
private const val MAX_FONT_SIZE = 99
/** 颜色键名(照搬原 Morse Expert I2.b.f663b) */
private val ColorKeys = arrayOf(
"bg_color", "text_color", "text_color_weak",
"call_color", "call_color_weak",
"cq_color", "cq_color_weak",
"rst_color", "rst_color_weak"
)
/** 颜色默认值(照搬原 Morse Expert I2.b.f664d,ARGB int) */
private val ColorDefaults = intArrayOf(
-3084048, -16777216, -5592406,
-65536, -30584,
-16776961, -7829249,
-65281, -30465
)
/** 颜色名称(照搬原 Morse Expert I2.b.c) */
private val ColorNames = arrayOf(
"Background color", "Text color", "Text color, weak",
"Callsign color", "Callsign color, weak",
"CQ color", "CQ color, weak",
"RST color", "RST color, weak"
)
/** 预置色板(9 色, 3x3 网格);原 ColorPreferenceCompat 用第三方 colorpicker, 不引入。 */
private val PaletteColors = listOf(
0xFFD0F0F0.toInt(), 0xFF000000.toInt(), 0xFFFFFFFF.toInt(),
@@ -70,7 +94,7 @@ private val PaletteColors = listOf(
* 点击 OK 保存并调用 controller.onResume() 立即生效; Cancel/点外部仅关闭不保存。
*/
@Composable
fun CwSettingsDialog(controller: MainActivity, onDismiss: () -> Unit) {
fun CwSettingsDialog(controller: FldigiCwController, onDismiss: () -> Unit) {
val activity = controller.mActivity ?: return
val prefs = remember(activity) {
activity.getSharedPreferences(activity.packageName + "_preferences", Context.MODE_PRIVATE)
@@ -86,7 +110,7 @@ fun CwSettingsDialog(controller: MainActivity, onDismiss: () -> Unit) {
)
}
var colorValues by remember {
mutableStateOf(IntArray(9) { i -> prefs.getInt(I2.b.f663b[i], I2.b.f664d[i]) })
mutableStateOf(IntArray(9) { i -> prefs.getInt(ColorKeys[i], ColorDefaults[i]) })
}
AlertDialog(
@@ -124,7 +148,7 @@ fun CwSettingsDialog(controller: MainActivity, onDismiss: () -> Unit) {
Text("Colors", style = MaterialTheme.typography.titleSmall)
for (i in 0 until 9) {
ColorSettingRow(
title = I2.b.c[i],
title = ColorNames[i],
value = colorValues[i],
onSelect = { selected ->
colorValues = colorValues.copyOf().also { it[i] = selected }
@@ -139,7 +163,7 @@ fun CwSettingsDialog(controller: MainActivity, onDismiss: () -> Unit) {
editor.putString(KEY_MESSAGE_TYPE, messageType)
editor.putInt(KEY_TEXT_FONT_SIZE, fontSize.roundToInt())
for (i in 0 until 9) {
editor.putInt(I2.b.f663b[i], colorValues[i])
editor.putInt(ColorKeys[i], colorValues[i])
}
editor.apply()
// 原 SettingsActivity 返回时由 MainActivity.onResume() 重新读取全部设置; 照搬逻辑已就位
@@ -0,0 +1,205 @@
package com.rtbishop.look4sat.feature.cw
import android.Manifest
import android.app.Activity
import android.content.Context
import android.content.pm.PackageManager
import android.media.AudioFormat
import android.media.AudioRecord
import android.media.MediaRecorder
import android.view.View
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
/**
* fldigi CW 解码器控制器。
* 替代 com.ve3nea.morse_expert.MainActivity,提供相同生命周期接口:
* onCreate / onResume / onPause / onDestroy / onPermissionGranted
* 内部使用 AudioRecord 采集麦克风,送入 fldigi 原生解码器。
*/
class FldigiCwController {
/** 兼容 CwSettingsDialog 对 controller.mActivity 的引用 */
var mActivity: Activity? = null
private set
private var nativeHandle: Long = 0L
private var audioRecord: AudioRecord? = null
private var isRunning = false
private var audioThread: Thread? = null
private val scope = CoroutineScope(Dispatchers.Main + Job())
private val _decodedText = MutableStateFlow("")
val decodedText: StateFlow<String> = _decodedText.asStateFlow()
// 轮询解码文本
private var pollJob: Job? = null
/** 创建解码器, 保存 Activity 引用。rootView 暂不处理(UI 由 AndroidView 管理)。 */
fun onCreate(activity: Activity, rootView: View?) {
mActivity = activity
if (nativeHandle == 0L) {
nativeHandle = FldigiNative.create()
}
}
/** 授予权限后启动录音和解码线程 */
fun onPermissionGranted() {
startAudioCapture()
}
/** 恢复录音和解码 */
fun onResume() {
if (hasPermission()) {
startAudioCapture()
}
// 启动轮询
pollJob?.cancel()
pollJob = scope.launch {
while (isActive) {
val text = FldigiNative.getDecodedText(nativeHandle)
if (text.isNotEmpty()) {
_decodedText.value += text
}
delay(100L)
}
}
}
/** 暂停录音 */
fun onPause() {
stopAudioCapture()
pollJob?.cancel()
pollJob = null
}
/** 销毁解码器 */
fun onDestroy() {
stopAudioCapture()
pollJob?.cancel()
if (nativeHandle != 0L) {
FldigiNative.destroy(nativeHandle)
nativeHandle = 0L
}
}
/** 获取当前解码文本(供 Compose 读取) */
fun getCurrentText(): String = _decodedText.value
/** 清除解码文本 */
fun clearText() {
_decodedText.value = ""
}
// ── 兼容 MainActivity 接口(CwDecodeScreen 顶部按钮用)──
/** 暂停/恢复解码 */
fun togglePause() {
if (isRunning) {
stopAudioCapture()
} else {
startAudioCapture()
}
}
/** 清除解码文本(= clearText) */
fun clearDecoded() = clearText()
/** 保存解码文本到文件。当前简单实现: 不落盘(可选扩展) */
fun saveText() {
// 可选: 把 _decodedText.value 写入外部存储
}
/** 录音信号保存。占位实现 */
fun recordSignals() {
// 可选扩展
}
/** 返回是否处理了返回键(占位,直接返回 false 让上层导航) */
fun handleBackPress(): Boolean = false
// ── 内部 ──
private fun hasPermission(): Boolean {
val ctx = mActivity ?: return false
return ctx.checkSelfPermission(Manifest.permission.RECORD_AUDIO) ==
PackageManager.PERMISSION_GRANTED
}
private fun startAudioCapture() {
if (isRunning) return
val ctx = mActivity ?: return
if (!hasPermission()) return
val sampleRate = 8000 // fldigi CW 解码器需要的采样率
val bufferSize = AudioRecord.getMinBufferSize(
sampleRate,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT
).coerceAtLeast(4096)
try {
audioRecord = AudioRecord(
MediaRecorder.AudioSource.MIC,
sampleRate,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT,
bufferSize
)
} catch (e: SecurityException) {
return
}
val record = audioRecord ?: return
if (record.state != AudioRecord.STATE_INITIALIZED) {
record.release()
audioRecord = null
return
}
record.startRecording()
isRunning = true
// 16-bit PCM → float 转换送入解码器
val shortBuf = ShortArray(4096)
val floatBuf = FloatArray(4096)
audioThread = Thread {
android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_URGENT_AUDIO)
while (isRunning && nativeHandle != 0L) {
val read = record.read(shortBuf, 0, shortBuf.size)
if (read > 0) {
// 转 float [-1.0, 1.0]
for (i in 0 until read) {
floatBuf[i] = shortBuf[i].toFloat() / 32768f
}
FldigiNative.process(nativeHandle, floatBuf.copyOf(read))
}
}
}.apply {
name = "fldigi-cw-audio"
start()
}
}
private fun stopAudioCapture() {
isRunning = false
audioThread?.join(500)
audioThread = null
try {
audioRecord?.apply {
if (recordingState == AudioRecord.RECORDSTATE_RECORDING) {
stop()
}
release()
}
} catch (_: Exception) { }
audioRecord = null
}
}
@@ -0,0 +1,23 @@
package com.rtbishop.look4sat.feature.cw
/**
* JNI 绑定: fldigi CW 解码器原生库。
* 对应 C++ 文件: fldigi_cw_jni.cpp
*/
object FldigiNative {
init {
System.loadLibrary("fldigi_cw")
}
/** 创建解码器实例, 返回 native handle (long) */
external fun create(): Long
/** 销毁解码器 */
external fun destroy(handle: Long)
/** 送入 PCM 音频数据 (float[], mono, 8000Hz), 解码 */
external fun process(handle: Long, audio: FloatArray)
/** 获取并清空积压的解码文本, 返回空字符串表示无新输出 */
external fun getDecodedText(handle: Long): String
}