mirror of
https://github.com/atsunatsu/Look4Sat.git
synced 2026-10-02 03:15:37 +00:00
feat(radar,cw): transponder CW panel switches to the Morse Expert engine
Background: the transponder panel CW decoder (added by the upstream fork author) used a lightweight Kotlin Bayesian engine (core/domain/cw, kept untouched as a fallback). This change makes the panel use the Morse Expert engine ported in 4.5.0, so both CW entry points share the same decoder with a live waterfall. Changes: - New mini layout cw_panel_main.xml (waterfall 80dp + decoded text, status line hidden but ID kept for controller lookup) - MainActivity.onCreate overload with applyImmersive flag; panel binds with false so the host window system bars are not touched - CwDecoderPanel now embeds the mini layout via AndroidView and drives the MainActivity controller: start/stop/reset map to the engine, lifecycle follows panel expand (start) / collapse (release mic) - radar module now depends on feature:cw (+ constraintlayout 2.2.1, same as cw) for layout + controller reuse - What's new rewritten in EN/TR/ZH for this release only Verified: :feature:radar:compileDebugKotlin and :app:compileDebugKotlin BUILD SUCCESSFUL locally; check_strings.py OK (7 files, no bare apostrophes).
This commit is contained in:
1 parent
f731775346
commit
47f3e424ff
7 files changed
+139
-49
No files matched your search
@@ -52,8 +52,8 @@
|
||||
\n\nDoğru tahminler alabilmek için veritabanını en az haftada bir güncelleyin.</string>
|
||||
<string name="pass_whatsnew_title" translatable="false">What\'s new in Look4Sat</string>
|
||||
<string name="pass_whatsnew_message" translatable="false">
|
||||
* Yeni CW Kod Çözücü sayfası (Roaming ile Harita arasında): mikrofondan canlı Morse kodu çözme ve şelale (waterfall) görüntüsü
|
||||
* Ayarlar: mesaj tipi, yazı boyutu ve 9 renk teması</string>
|
||||
* Transponder CW kod çözücü paneli artık geliştirilmiş kod çözme motorunu canlı şelale (waterfall) görüntüsüyle kullanıyor
|
||||
* CW kod çözme sayfaları aynı motoru paylaşıyor: gezinme sayfası ve transponder paneli</string>
|
||||
|
||||
<!-- Radar screen -->
|
||||
<string name="radar_back">Geri</string>
|
||||
|
||||
@@ -42,8 +42,8 @@
|
||||
<string name="pass_welcome_message">请务必在设置中通过GPS、经纬度或QTH定位您的位置\n建议至少每周更新一次数据库,以确保预测结果的准确性</string>
|
||||
<string name="pass_whatsnew_title">Look4Sat Pro 更新内容</string>
|
||||
<string name="pass_whatsnew_message" translatable="false">
|
||||
* 新增 CW 解码页(漫游与地图之间): 麦克风实时摩尔斯电码解码 + 瀑布图显示
|
||||
* 设置: 消息类型、字体大小、9 种颜色主题</string>
|
||||
* 转发器 CW 解码面板已接入增强解码引擎,带实时瀑布图显示
|
||||
* CW 解码入口共用同一引擎:导航页与转发器面板</string>
|
||||
|
||||
<!-- Radar screen -->
|
||||
<string name="radar_back">后退</string>
|
||||
|
||||
@@ -54,8 +54,8 @@
|
||||
\n\nPlease update the database at least weekly to get accurate predictions.</string>
|
||||
<string name="pass_whatsnew_title" translatable="false">What\'s new in Look4Sat</string>
|
||||
<string name="pass_whatsnew_message" translatable="false">
|
||||
* New CW Decoder page (between Roaming and Map): live Morse code decoding from the microphone with waterfall display
|
||||
* Settings: message type, font size and 9 color themes</string>
|
||||
* Transponder CW decoder panel now uses the enhanced decoding engine with a live waterfall display
|
||||
* CW decoding pages share the same engine: navigation page and transponder panel</string>
|
||||
|
||||
<!-- Radar screen -->
|
||||
<string name="radar_back">Back</string>
|
||||
|
||||
@@ -48,9 +48,19 @@ public class MainActivity {
|
||||
public android.app.Activity mActivity;
|
||||
|
||||
public void onCreate(android.app.Activity activity, ConstraintLayout constraintLayout) {
|
||||
onCreate(activity, constraintLayout, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* applyImmersive=false 用于嵌入小容器(如转发器 CW 面板):
|
||||
* 不触碰宿主窗口的系统栏标志(避免影响整个 Activity)。
|
||||
*/
|
||||
public void onCreate(android.app.Activity activity, ConstraintLayout constraintLayout, boolean applyImmersive) {
|
||||
this.mActivity = activity;
|
||||
// K1.a.M(getWindow(), false): immersive status bar (nav/fullscreen flags, ported as-is)
|
||||
setImmersive(activity.getWindow(), false);
|
||||
if (applyImmersive) {
|
||||
// K1.a.M(getWindow(), false): immersive status bar (nav/fullscreen flags, ported as-is)
|
||||
setImmersive(activity.getWindow(), false);
|
||||
}
|
||||
View inflate = constraintLayout;
|
||||
int i4 = R.id.decodedTextView;
|
||||
DecodedTextView decodedTextView = (DecodedTextView) findViewRecursive(inflate, R.id.decodedTextView);
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:id="@+id/verticalLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:keepScreenOn="true">
|
||||
<TextView
|
||||
android:textColor="#ffffff"
|
||||
android:textSize="10sp"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="end"
|
||||
android:visibility="gone"
|
||||
android:id="@+id/statusTextView"
|
||||
android:background="@color/purple_500"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/idle"/>
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="80dp">
|
||||
<com.ve3nea.morse_expert.WaterfallSurfaceView
|
||||
android:id="@+id/waterfallView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:contentDescription="@string/waterfall_display"/>
|
||||
<com.ve3nea.morse_expert.ScaleView
|
||||
android:id="@+id/scaleView"
|
||||
android:background="#24545a"
|
||||
android:layout_width="8dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="0"
|
||||
android:contentDescription="@string/frequency_scale"/>
|
||||
</LinearLayout>
|
||||
<com.ve3nea.morse_expert.DecodedTextView
|
||||
android:textSize="12sp"
|
||||
android:typeface="monospace"
|
||||
android:textColor="#444444"
|
||||
android:textColorHint="#888888"
|
||||
android:gravity="start|top"
|
||||
android:id="@+id/decodedTextView"
|
||||
android:background="#d1f0f4"
|
||||
android:scrollbars="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:hint="@string/decoded_text"
|
||||
android:freezesText="false"
|
||||
android:layout_weight="1"
|
||||
android:minHeight="48dp"
|
||||
android:inputType="none"
|
||||
android:fadeScrollbars="false"
|
||||
android:textIsSelectable="true"/>
|
||||
</LinearLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -8,4 +8,8 @@ android {
|
||||
|
||||
dependencies {
|
||||
implementation(project(":feature:mutual"))
|
||||
// CW 解码面板: 复用 feature:cw 的 Morse Expert 引擎(布局 cw_panel_main + MainActivity 控制器)
|
||||
implementation(project(":feature:cw"))
|
||||
// 与 feature:cw 一致(控制器按 ID 递归查找 ConstraintLayout 视图)
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.2.1")
|
||||
}
|
||||
+58
-41
@@ -17,6 +17,8 @@
|
||||
*/
|
||||
package com.rtbishop.look4sat.feature.radar
|
||||
|
||||
import android.app.Activity
|
||||
import android.view.LayoutInflater
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
@@ -50,6 +52,7 @@ import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -60,6 +63,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
@@ -68,6 +72,8 @@ import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import com.rtbishop.look4sat.core.domain.model.SatRadio
|
||||
import com.rtbishop.look4sat.core.domain.predict.OrbitalPos
|
||||
import com.rtbishop.look4sat.core.domain.utility.DopplerFrequencyCalculator
|
||||
@@ -75,6 +81,8 @@ import com.rtbishop.look4sat.core.presentation.CardButton
|
||||
import com.rtbishop.look4sat.core.presentation.R
|
||||
import com.rtbishop.look4sat.core.presentation.formatFrequency
|
||||
import com.rtbishop.look4sat.core.presentation.infiniteMarquee
|
||||
import com.rtbishop.look4sat.feature.cw.R as CwR
|
||||
import com.ve3nea.morse_expert.MainActivity
|
||||
import java.util.Locale
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
@@ -661,19 +669,56 @@ private fun CwDecoderPanel(
|
||||
}
|
||||
|
||||
AnimatedVisibility(visible = cw.isExpanded) {
|
||||
// Morse Expert 引擎(4.5.0 移植): 小布局 cw_panel_main(瀑布图 + 解码文本)
|
||||
// 自带 Kotlin 贝叶斯引擎(CwDecoder)不再用于此面板(代码保留作回退)
|
||||
val context = LocalContext.current
|
||||
val activity = remember { context as? Activity }
|
||||
val controller = remember { MainActivity() }
|
||||
val rootView = remember {
|
||||
LayoutInflater.from(context).inflate(CwR.layout.cw_panel_main, null) as ConstraintLayout
|
||||
}
|
||||
var initialized by remember { mutableStateOf(false) }
|
||||
var listening by remember { mutableStateOf(false) }
|
||||
|
||||
// 生命周期: 展开即绑定视图(不改宿主窗口); 收起/离开自动释放
|
||||
DisposableEffect(Unit) {
|
||||
if (activity != null) {
|
||||
controller.onCreate(activity, rootView, false)
|
||||
}
|
||||
onDispose {
|
||||
controller.onPause()
|
||||
controller.onDestroy()
|
||||
}
|
||||
}
|
||||
// 权限已授予(首次进入/授权回调统一入口): 初始化引擎 + 启动录音
|
||||
LaunchedEffect(cw.hasPermission) {
|
||||
if (cw.hasPermission && !initialized) {
|
||||
controller.onPermissionGranted() // = v(): 创建音频采集 + 解码核心
|
||||
controller.onResume() // 立即启动录音
|
||||
initialized = true
|
||||
listening = true
|
||||
}
|
||||
}
|
||||
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
// Control buttons row
|
||||
// Control buttons row(映射到控制器)
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
if (cw.status == CwStatus.Idle) {
|
||||
if (!listening) {
|
||||
Button(
|
||||
onClick = {
|
||||
if (!cw.hasPermission) {
|
||||
requestMicPermission()
|
||||
} else if (!initialized) {
|
||||
controller.onPermissionGranted()
|
||||
controller.onResume()
|
||||
initialized = true
|
||||
listening = true
|
||||
} else {
|
||||
onAction(RadarAction.CwStartListening)
|
||||
controller.onResume()
|
||||
listening = true
|
||||
}
|
||||
},
|
||||
modifier = Modifier.weight(1f)
|
||||
@@ -682,58 +727,30 @@ private fun CwDecoderPanel(
|
||||
}
|
||||
} else {
|
||||
Button(
|
||||
onClick = { onAction(RadarAction.CwStopListening) },
|
||||
onClick = {
|
||||
controller.onPause()
|
||||
listening = false
|
||||
},
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(stringResource(R.string.radar_cw_stop))
|
||||
}
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = { onAction(RadarAction.CwReset) },
|
||||
onClick = { controller.clearDecoded() },
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(stringResource(R.string.radar_cw_reset))
|
||||
}
|
||||
}
|
||||
|
||||
// Signal strength indicator
|
||||
if (cw.status == CwStatus.Listening && cw.signalStrength > 0f) {
|
||||
val strengthPct = (cw.signalStrength * 100).toInt()
|
||||
Text(
|
||||
text = "Signal: $strengthPct%",
|
||||
fontSize = 12.sp,
|
||||
color = if (cw.signalStrength > 0.5f) Color(0xFF4CAF50)
|
||||
else if (cw.signalStrength > 0.2f) Color(0xFFFFC107)
|
||||
else MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
// Decoded text output
|
||||
ElevatedCard(
|
||||
// 瀑布图 + 解码文本(原版布局迷你版, 适配容器)
|
||||
AndroidView(
|
||||
factory = { rootView },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(120.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Decoded:",
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = cw.decodedText.ifEmpty { "Waiting for CW signal..." },
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
}
|
||||
.height(150.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user