mirror of
https://github.com/atsunatsu/Look4Sat.git
synced 2026-10-04 11:37:22 +00:00
i18n: localize mutual match page to English
This commit is contained in:
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
|
||||
}
|
||||
Reference in new issue
Block a user