diff --git a/app/src/main/java/com/rtbishop/look4sat/AprsForegroundService.kt b/app/src/main/java/com/rtbishop/look4sat/AprsForegroundService.kt index 2438c07f..7ff099de 100644 --- a/app/src/main/java/com/rtbishop/look4sat/AprsForegroundService.kt +++ b/app/src/main/java/com/rtbishop/look4sat/AprsForegroundService.kt @@ -24,8 +24,8 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch /** - * APRS 前台服务:系统通知保活,切换页面不断线。 - * START_STICKY:被系统杀掉后自动重启(APRSdroid 同款策略)。 + * APRS foreground service: kept alive by a system notification; keeps working across pages. + * START_STICKY: auto-restarted after being killed by the system (same strategy as APRSdroid). */ class AprsForegroundService : Service() { @@ -53,7 +53,7 @@ class AprsForegroundService : Service() { ACTION_STOP -> stopReporting() ACTION_REPORT_NOW -> { if (reporter == null) { - // 服务没在跑:先启动(未配置会 Toast 提示) + // Service not running: start it first (Toast hint when not configured) startReporting() } reporter?.reportNow() @@ -80,7 +80,7 @@ class AprsForegroundService : Service() { onReport = { report -> AprsStore.saveLastReport(this, report.ok, report.detail) updateNotification(cfg) - // 上报结果必达:成功 Toast 短显,失败 Toast 长显+原因 + // Report result always surfaces: success = short Toast, failure = long Toast + reason val msg = if (report.ok) { getString(R.string.aprs_toast_ok) } else { @@ -112,7 +112,7 @@ class AprsForegroundService : Service() { startForeground(NOTIF_ID, notif) } } catch (e: Exception) { - // 厂商 ROM / 旧系统兼容兜底:启动前台失败只停服务,不崩进程 + // Vendor ROM / old-system safety net: foreground-start failure only stops the service, never crashes the app stopSelf() } } @@ -146,9 +146,9 @@ class AprsForegroundService : Service() { nm.notify(NOTIF_ID, buildNotification(cfg)) } - /** 上报位置:优先用设置里的站位(用户拍板);站位无效时实时 GPS 兜底 */ + /** Report position: station QTH from settings first (per user); live GPS as fallback when invalid */ private fun stationPosition(): Pair? { - // ① 站位(设置页设置的位置) + // 1. Station QTH (position set in settings) val station = runCatching { val container = (application as MainApplication).getMainContainer() container.settingsRepo.stationPosition.value @@ -156,7 +156,7 @@ class AprsForegroundService : Service() { if (station != null && (station.latitude != 0.0 || station.longitude != 0.0)) { return Pair(station.latitude, station.longitude) } - // ② 兜底:实时 GPS 最后位置 + // 2. Fallback: last live GPS position return runCatching { val lm = getSystemService(Context.LOCATION_SERVICE) as android.location.LocationManager val providers = listOf( diff --git a/app/src/main/java/com/rtbishop/look4sat/MainApplication.kt b/app/src/main/java/com/rtbishop/look4sat/MainApplication.kt index 6ce1fb93..81defe26 100644 --- a/app/src/main/java/com/rtbishop/look4sat/MainApplication.kt +++ b/app/src/main/java/com/rtbishop/look4sat/MainApplication.kt @@ -30,7 +30,7 @@ class MainApplication : Application(), IContainerProvider { private lateinit var container: IMainContainer - /** 全局崩溃捕获:堆栈写入 files/crash_log.txt,重启后可查看(用户要求错误报告) */ + /** Global crash capture: stack written to files/crash_log.txt, viewable after restart (user asked for error reports) */ private fun installCrashHandler() { val defaultHandler = Thread.getDefaultUncaughtExceptionHandler() Thread.setDefaultUncaughtExceptionHandler { thread, throwable -> diff --git a/app/src/main/java/com/rtbishop/look4sat/MainScreen.kt b/app/src/main/java/com/rtbishop/look4sat/MainScreen.kt index 009e0ac2..c9af6d1a 100644 --- a/app/src/main/java/com/rtbishop/look4sat/MainScreen.kt +++ b/app/src/main/java/com/rtbishop/look4sat/MainScreen.kt @@ -148,10 +148,10 @@ fun MainScreen(navigateToRadar: () -> Unit = {}) { val container = (context.applicationContext as IContainerProvider).getMainContainer() val trackingState by container.radioTrackingService.state.collectAsStateWithLifecycle() val otherSettings by container.settingsRepo.otherSettings.collectAsStateWithLifecycle() - // UI 设置: 按 screenOrder 排序(空 = 默认顺序), 再按 hiddenScreens 过滤(设置页固定保留) + // UI settings: sort by screenOrder (empty = default order), then filter by hiddenScreens (Settings always kept) val allNavItems = listOf(Screen.Satellites, Screen.Passes, Screen.Radar, Screen.Mutual, Screen.Roaming, Screen.CwDecode, Screen.WavelogLog, Screen.AmSat, Screen.Map, Screen.Settings) .sortedBy { screen -> - // 未知(新页面如 CwDecode 不在旧持久化顺序里): 用默认顺序位置(漫游↔地图), 再兜底最后 + // Unknown pages (e.g. CwDecode not in old persisted order): use default-order position (Roaming<->Map), then fall back to last val idx = otherSettings.screenOrder.indexOf(screen.screenId) if (idx != -1) idx else com.rtbishop.look4sat.core.presentation.defaultScreenOrder.indexOf(screen.screenId).let { @@ -159,8 +159,8 @@ fun MainScreen(navigateToRadar: () -> Unit = {}) { } } .filter { it.screenId !in otherSettings.hiddenScreens || it is Screen.Settings } - // 4.5.1 折叠菜单: 主菜单(底部栏 5 槽) + 更多菜单(溢出页面) - // 老用户迁移: 已持久化的 subMenuOrder 不含新页面 WavelogLog → 追加到子菜单尾部 + // 4.5.1 foldable menu: main menu (5 bottom-bar slots) + More menu (overflow page) + // Legacy migration: persisted subMenuOrder lacks new pages (WavelogLog) -> append to the sub-menu tail val subOrder = (otherSettings.subMenuOrder.ifEmpty { com.rtbishop.look4sat.core.presentation.defaultSubMenuOrder }) .let { list -> if ("WavelogLog" in list) list else list + "WavelogLog" } .let { list -> if ("AMSAT" in list) list else list + "AMSAT" } @@ -171,7 +171,7 @@ fun MainScreen(navigateToRadar: () -> Unit = {}) { subOrder.mapNotNull { id -> allNavItems.find { it.screenId == id } } } var moreExpanded by remember { mutableStateOf(false) } - // 更多菜单打开时拦截返回: 先关菜单 + // Intercept Back while the More menu is open: close the menu first BackHandler(enabled = moreExpanded) { moreExpanded = false } // Activity-scoped so the mutual query results survive navigation to Radar and back val mutualViewModel: MutualViewModel = viewModel( @@ -212,7 +212,7 @@ fun MainScreen(navigateToRadar: () -> Unit = {}) { } ) } - // 更多菜单按钮(固定第 6 槽, 子菜单非空才显示) + // More-menu button (fixed slot 6; shown only when the sub-menu is non-empty) if (moreNavItems.isNotEmpty()) { item( icon = { @@ -346,7 +346,7 @@ fun MainScreen(navigateToRadar: () -> Unit = {}) { } } } - // 更多菜单弹出面板(覆盖在内容上, 底部栏上方; spring 弹跳) + // More-menu popup panel (overlays content above the bottom bar; spring bounce) AnimatedVisibility( visible = moreExpanded, modifier = Modifier.fillMaxSize(), diff --git a/app/src/main/java/com/rtbishop/look4sat/MoreMenuPopup.kt b/app/src/main/java/com/rtbishop/look4sat/MoreMenuPopup.kt index 8b90d581..8a34dc5b 100644 --- a/app/src/main/java/com/rtbishop/look4sat/MoreMenuPopup.kt +++ b/app/src/main/java/com/rtbishop/look4sat/MoreMenuPopup.kt @@ -1,9 +1,9 @@ /* - * MoreMenuPopup.kt — 底部导航「更多」二级菜单弹出面板(4.5.1)。 + * MoreMenuPopup.kt - bottom-nav "More" second-level menu popup panel (4.5.1). * - * 覆盖在内容区上(底部栏上方, 右对齐), 竖排菜单项(图标+文本+箭头), - * 当前页高亮; 点击遮罩关闭, 点击项跳转。弹出/收起动画由调用处 - * (MainScreen 的 AnimatedVisibility + spring)驱动。 + * Overlays the content area (above the bottom bar, right-aligned), vertical menu items (icon+text+arrow), + * current page highlighted; tap the scrim to close, tap an item to navigate. Open/close animation is driven by the caller + * (MainScreen's AnimatedVisibility + spring). */ package com.rtbishop.look4sat diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/aprs/AprsIsClient.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/aprs/AprsIsClient.kt index 0e8d56a8..1893f105 100644 --- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/aprs/AprsIsClient.kt +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/aprs/AprsIsClient.kt @@ -9,8 +9,8 @@ import java.net.InetSocketAddress import java.net.Socket /** - * APRS-IS TCP 客户端(从 APRSdroid TcpUploader.scala 逆向移植)。 - * 纯文本协议:登录一行 + 每行一个包;断线 30 秒重连。 + * APRS-IS TCP client (reverse-ported from APRSdroid TcpUploader.scala). + * Plain-text protocol: one login line + one packet per line; 30 s reconnect after drop. */ class AprsIsClient( private val host: String, @@ -30,7 +30,7 @@ class AprsIsClient( val isConnected: Boolean get() = synchronized(lock) { socket?.isConnected == true && !socket!!.isClosed } - /** 建立连接 + 登录(同步阻塞,调用方放后台线程) */ + /** Connect + login (synchronous/blocking; call from a background thread) */ @Throws(Exception::class) fun connect() { disconnect() @@ -43,10 +43,10 @@ class AprsIsClient( writer = PrintWriter(OutputStreamWriter(s.getOutputStream(), Charsets.ISO_8859_1), true) reader = BufferedReader(InputStreamReader(s.getInputStream(), Charsets.ISO_8859_1), 256) } - // 登录行 + // Login line val login = AprsPacket.formatLogin(callsign, ssid, passcode, version) + filter writer?.println(login) - // 读登录响应(aprsc 返回 # logresp ... verified/unverified) + // Read the login response (aprsc replies # logresp ... verified/unverified) runCatching { s.soTimeout = 8000 val resp = reader?.readLine() @@ -54,14 +54,14 @@ class AprsIsClient( resp.contains("unverified", ignoreCase = true))) { throw IllegalArgumentException(resp.trim()) } - // 恢复超时 + // Restore timeout s.soTimeout = timeoutSec * 1000 } } /** - * 发送一个 APRS 包(一行)并尝试读服务器确认。 - * 返回 null=发送失败;Pair(ok, detail)=发送结果(服务器错误原文在 detail) + * Sends one APRS packet (one line) and tries to read the server ack. + * Returns null=failed to send; Pair(ok, detail)=result (server error text lives in detail) */ fun sendPacket(packetLine: String): Pair? { synchronized(lock) { @@ -69,7 +69,7 @@ class AprsIsClient( w.println(packetLine) if (w.checkError()) return Pair(false, "write failed") } - // 尝试读服务器响应(短超时 3s;APRS-IS 对格式错误会回错误行) + // Try to read the server response (short 3 s timeout; APRS-IS replies with an error line on bad format) return runCatching { val s = socket ?: return@runCatching Pair(true, "OK") val oldTimeout = s.soTimeout @@ -88,7 +88,7 @@ class AprsIsClient( }.getOrElse { Pair(true, "OK") } } - /** 读一行(服务器响应,超时抛异常) */ + /** Read one line (server response; throws on timeout) */ fun readLine(): String? { return reader?.readLine() } diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/aprs/AprsReporter.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/aprs/AprsReporter.kt index 512174ff..7cb185f7 100644 --- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/aprs/AprsReporter.kt +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/aprs/AprsReporter.kt @@ -10,10 +10,10 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch -/** APRS 连接状态 */ +/** APRS connection state */ enum class AprsState { Idle, Connecting, Connected, Disconnected, Error } -/** APRS 配置(SharedPreferences 持久化,填入即保存) */ +/** APRS config (persisted in SharedPreferences, saved as filled) */ data class AprsConfig( val enabled: Boolean = false, val server: String = "euro.aprs2.net", @@ -29,7 +29,7 @@ data class AprsConfig( val includeAltitude: Boolean = true ) -/** APRS 上报结果 */ +/** APRS report result */ data class AprsReport( val timestamp: Long, val packet: String, @@ -37,7 +37,7 @@ data class AprsReport( val detail: String ) -/** 上报调度器(周期上报 + 手动触发),连接管理放在前台服务 */ +/** Report scheduler (periodic + manual trigger); connection management lives in the foreground service */ class AprsReporter( private val configProvider: () -> AprsConfig, private val positionProvider: () -> Pair? = { null }, @@ -51,7 +51,7 @@ class AprsReporter( val isRunning: Boolean get() = job?.isActive == true - /** 启动周期上报(前台服务调用) */ + /** Start periodic reporting (called by the foreground service) */ fun start() { stop() val cfg = configProvider() @@ -75,7 +75,7 @@ class AprsReporter( onState(AprsState.Idle) } - /** 手动触发一次(立即上报,不等待周期) */ + /** Trigger one report manually (immediately, without waiting for the cycle) */ fun reportNow() { manualJob?.cancel() manualJob = scope.launch { reportOnce() } @@ -112,7 +112,7 @@ class AprsReporter( } } - /** 构造位置包:BG7NTA-5>APRS:=DDMM.MMN/DDDMM.MME<状态文本 */ + /** Build position packet: BG7NTA-5>APRS:=DDMM.MMN/DDDMM.MME parse -> SatStatusPage */ class AmSatRepository(private val remoteSource: IRemoteSource) : IAmSatRepository { override suspend fun fetchStatus(): SatStatusPage? { diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/DatabaseRepo.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/DatabaseRepo.kt index 9fa00f97..42b637b7 100644 --- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/DatabaseRepo.kt +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/DatabaseRepo.kt @@ -69,7 +69,7 @@ class DatabaseRepo( val dataSourcesSettings = settingsRepo.dataSourcesSettings.value val tleUrls = buildMap { putAll(Sources.satelliteDataUrls) - // 开关开且 URL 非空 -> All 用自定义 URL;否则用默认 URL(在线更新默认源) + // Switch on + non-empty URL -> All uses the custom URL; otherwise the default URL (online-update default source) put("All", if (dataSourcesSettings.useCustomTLE && dataSourcesSettings.tleUrl.isNotBlank()) dataSourcesSettings.tleUrl else Sources.defaultTleUrl) }.filterValues { it.isNotBlank() } @@ -81,7 +81,7 @@ class DatabaseRepo( // launch all network requests concurrently val tleJobs = tleUrls.values.map { url -> async { url to remoteSource.getNetworkStream(url) } } val radioJobs = radioUrls.values.map { url -> async { url to remoteSource.getNetworkStream(url) } } - // 统计成功源数: 0 成功视为更新失败(不刷时间戳, 抛异常让 UI 提示) + // Count successful sources: zero successes = update failed (timestamp untouched, exception surfaced in the UI) val tleResults = tleJobs.awaitAll() val radioResults = radioJobs.awaitAll() val successCount = tleResults.count { it.second != null } + radioResults.count { it.second != null } diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SettingsRepo.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SettingsRepo.kt index 5d85007b..9b94710d 100644 --- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SettingsRepo.kt +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SettingsRepo.kt @@ -187,9 +187,9 @@ class SettingsRepo( return true } - /** GPS 定位: 一次性 getCurrentLocation(GPS 优先, 15 秒超时), 拿到位置才返回 true */ + /** GPS fix: one-shot getCurrentLocation (GPS first, 15 s timeout); returns true only with a fix */ override suspend fun setStationPosition(): Boolean { - // 权限前置: 无定位权限直接失败(不吞异常) + // Permission gate: fail fast without location permission (no swallowed exceptions) if (androidx.core.content.ContextCompat.checkSelfPermission( context, android.Manifest.permission.ACCESS_FINE_LOCATION ) != android.content.pm.PackageManager.PERMISSION_GRANTED @@ -202,7 +202,7 @@ class SettingsRepo( val signal = android.os.CancellationSignal() val handler = android.os.Handler(android.os.Looper.getMainLooper()) val executor = java.util.concurrent.Executor { handler.post(it) } - // 15 秒超时 + // 15 s timeout val timeout = handler.postDelayed({ signal.cancel() if (cont.isActive) cont.resume(false) { } @@ -432,8 +432,8 @@ class SettingsRepo( } private fun getDataSourcesSettings(): DataSourcesSettings { - // 4.4.8 修复: 旧版 example.com 占位 URL 视为未配置 -> 替换为真实默认 URL 且开关强制关闭, - // 否则在线更新的 All/SatNOGS 源会指向错误地址导致更新失败 + // 4.4.8 fix: legacy example.com placeholder URLs count as unconfigured -> replaced with the real default URL and the switch forced off, + // otherwise the online All/SatNOGS sources would point at the wrong address and fail to update val storedTleUrl = preferences.getString(keyTleUrl, Sources.defaultTleUrl) ?: Sources.defaultTleUrl val storedTxUrl = preferences.getString(keyTransceiversUrl, Sources.defaultTransceiversUrl) ?: Sources.defaultTransceiversUrl val tleUrl = if (storedTleUrl == "https://example.com/tle.txt") Sources.defaultTleUrl else storedTleUrl diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/source/AmSatParser.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/source/AmSatParser.kt index f67ae677..37f02570 100644 --- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/source/AmSatParser.kt +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/source/AmSatParser.kt @@ -8,17 +8,17 @@ import com.rtbishop.look4sat.core.domain.model.SatStatusPage import java.util.regex.Pattern /** - * AMSAT 卫星状态页解析器(https://amsat.org/status/) + * AMSAT satellite status page parser (https://amsat.org/status/) * - * 页面结构(静态 HTML, 2026-08 实测): - * - 状态表: 内 48 行, 表头 = Name + 6 天(每个 colspan=12) - * 数据行 73 格: [0]=卫星名, [1..72] = 6 天 × 12 个 2 小时槽(新→旧) - * 每格 , 有报告时数字带 - * docTips.show('id') 链接 - * - 报告详情在页面内嵌 JS: - * tips.a885153 = new Array(5,5,120,'状态
呼号
网格
日期
时间段 UTC') + * Page structure (static HTML, verified 2026-08): + * - Status table: 48 rows in
数字
, header = Name + 6 days (each colspan=12) + * Data row has 73 cells: [0]=satellite name, [1..72] = 6 days x 12 two-hour slots (new->old) + * Each cell: ; when reports exist the count carries a + * docTips.show('id') link + * - Report details live in the page's inline JS: + * tips.a885153 = new Array(5,5,120,'status
callsign
grid
date
time span UTC') * - * 状态色: #648fff=活跃, #ffb000=仅遥测, #dc267f=未听到, #fe6100=矛盾 + * Status colors: #648fff=active, #ffb000=telemetry only, #dc267f=not heard, #fe6100=conflicting */ object AmSatParser { @@ -38,14 +38,14 @@ object AmSatParser { "tips\\.(a\\d+)\\s*=\\s*new\\s+Array\\(\\s*\\d+,\\s*\\d+,\\s*\\d+,\\s*'([^']*)'" ) - /** 解析完整页面 */ + /** Parse the full page */ fun parse(html: String, fetchedAtUtcMs: Long): SatStatusPage { val reports = parseReports(html) val statuses = parseStatusTable(html, reports) return SatStatusPage(fetchedAtUtcMs, statuses, reports) } - /** 提取全部报告(tips.* JS 数组) */ + /** Extract all reports (tips.* JS arrays) */ fun parseReports(html: String): Map { val map = mutableMapOf() val m = tipRe.matcher(html) @@ -64,7 +64,7 @@ object AmSatParser { return map } - /** 解析状态表(48 行卫星) */ + /** Parse the status table (48 satellite rows) */ fun parseStatusTable(html: String, reports: Map): List { val result = mutableListOf() val tables = extractTables(html) @@ -79,7 +79,7 @@ object AmSatParser { val cellList = mutableListOf() while (cells.find()) cellList.add(cells.group(1)) if (rowIndex == 0) { - // 表头: Name + 6 天(每个 colspan=12) + // Header: Name + 6 days (each colspan=12) dayHeaders = cellList.drop(1).take(6).map { stripHtml(it) } rowIndex++ continue diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/wavelog/LotwSatellitesRepo.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/wavelog/LotwSatellitesRepo.kt index 5f8c10ff..c7773b18 100644 --- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/wavelog/LotwSatellitesRepo.kt +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/wavelog/LotwSatellitesRepo.kt @@ -1,7 +1,7 @@ -/* LotwSatellitesRepo.kt — LoTW 卫星列表运行时刷新(4.5.5)。 - * 下载 ARRL 官方 config.tq6(gzip XML), 解析 , - * 更新 LotwSatellites 动态表 + SharedPreferences 持久化(重启不丢)。 - * 触发: 设置页 WaveLog 区「更新卫星列表」按钮(用户拍板: 手动触发)。 +/* LotwSatellitesRepo.kt - runtime refresh of the LoTW satellite list (4.5.5). + * Downloads ARRL's official config.tq6 (gzip XML), parses , + * updates the LotwSatellites dynamic set + persists to SharedPreferences (survives restarts). + * Trigger: the "Update sats" button in the settings WaveLog section (user decided: manual). */ package com.rtbishop.look4sat.core.data.wavelog @@ -20,7 +20,7 @@ class LotwSatellitesRepo(private val context: Context) : ILotwSatellitesRepo { private val prefs = context.getSharedPreferences("lotw_satellites", Context.MODE_PRIVATE) - /** 从 SharedPreferences 恢复上次下载的表(进程启动时调用) */ + /** Restore the last downloaded list from SharedPreferences (call at process start) */ override fun restore() { val saved = prefs.getString(KEY_NAMES, null) if (!saved.isNullOrBlank()) { @@ -28,7 +28,7 @@ class LotwSatellitesRepo(private val context: Context) : ILotwSatellitesRepo { } } - /** 下载 config.tq6 → 解析 → 更新内存 + 持久化。返回结果供 UI 提示 */ + /** Download config.tq6 -> parse -> update memory + persist. Result is surfaced in the UI */ override suspend fun refresh(): ILotwSatellitesRepo.RefreshResult = withContext(Dispatchers.IO) { try { val url = URL("https://lotw.arrl.org/lotw/config.tq6") @@ -39,7 +39,7 @@ class LotwSatellitesRepo(private val context: Context) : ILotwSatellitesRepo { val raw = conn.getInputStream() val reader = BufferedReader(InputStreamReader(GZIPInputStream(raw), Charsets.UTF_8)) val text = reader.use { it.readText() } - // 解析 — 格式固定(ARRL 官方 XML) + // Parse - fixed format (official ARRL XML) val names = Regex(""" All 源用自定义 URL, 数据归入 All 类型 + // New semantics: switch on + non-empty URL -> the All source uses the custom URL, data lands in the All type assertEquals(listOf(25544), settingsRepo.satelliteTypeIdsByType["All"]) } diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/aprs/AprsPacket.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/aprs/AprsPacket.kt index bdf72dd9..d0b5a367 100644 --- a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/aprs/AprsPacket.kt +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/aprs/AprsPacket.kt @@ -4,12 +4,12 @@ import kotlin.math.abs import kotlin.math.round /** - * APRS-IS 协议核心(纯 Kotlin,无 Android 依赖)。 - * 从 APRSdroid 1.6.3d 逆向移植:AprsPacket$.scala + ab0oo Position.java。 + * APRS-IS protocol core (pure Kotlin, no Android dependencies). + * Reverse-ported from APRSdroid 1.6.3d: AprsPacket$.scala + ab0oo Position.java. */ object AprsPacket { - /** APRS-IS passcode 算法(标准):0x73E2 初值,呼号大写+\0,每对字符 XOR */ + /** APRS-IS passcode algorithm (standard): 0x73E2 seed, uppercase callsign + \0, XOR each char pair */ fun passcode(callsign: String): Int { val s = callsign.split("-")[0].uppercase() + "\u0000" var hash = 29666 // 0x73E2 @@ -21,30 +21,30 @@ object AprsPacket { return hash and 0x7FFF } - /** 登录行:user CALL-SSID pass XXXX vers XXXX */ + /** Login line: user CALL-SSID pass XXXX vers XXXX */ fun formatLogin(callsign: String, ssid: String, passcode: Int, version: String): String { val callSsid = formatCallSsid(callsign, ssid) return "user $callSsid pass $passcode vers $version" } - /** 呼号-SSID 拼接(BG7NTA + 5 → BG7NTA-5) */ + /** Callsign-SSID join (BG7NTA + 5 -> BG7NTA-5) */ fun formatCallSsid(callsign: String, ssid: String): String { if (ssid.isNullOrEmpty()) return callsign return "$callsign-$ssid" } - /** 可选距离过滤: filter r/lat/lon/dist */ + /** Optional distance filter: filter r/lat/lon/dist */ fun formatRangeFilter(latitude: Double, longitude: Double, distKm: Int): String { return String.format("r/%.3f/%.3f/%d", latitude, longitude, distKm) } - /** 高度扩展 /A=00000(英尺) */ + /** Altitude extension /A=00000 (feet) */ fun formatAltitude(altitudeMeters: Double?): String { if (altitudeMeters == null) return "" return String.format("/A=%06d", (altitudeMeters * 3.2808399).toInt()) } - /** 航速航向扩展(节/度) */ + /** Speed/course extension (knots/degrees) */ fun formatCourseSpeed(speedMps: Double?, bearing: Float?): String { if (speedMps == null || bearing == null) return "" val knots = (speedMps * 1.94384449).toInt() @@ -53,8 +53,8 @@ object AprsPacket { } /** - * APRS 位置编码(ab0oo Position.java 逆向移植)。 - * 非压缩:DDMM.MMN/DDDMM.MME;压缩:base91。 + * APRS position encoding (reverse-ported from ab0oo Position.java). + * Uncompressed: DDMM.MMN/DDDMM.MME; compressed: base91. */ class AprsPosition( val latitude: Double, @@ -64,14 +64,14 @@ class AprsPosition( val positionAmbiguity: Int = 0 ) { - /** 非压缩格式(APRS-IS 默认上报格式) */ + /** Uncompressed format (APRS-IS default reporting format) */ fun toUncompressedString(): String { val lat = getDMS(latitude, true) val lon = getDMS(longitude, false) return "$lat$symbolTable$lon$symbolCode" } - /** 压缩格式(base91,标准 APRS 算法) */ + /** Compressed format (base91, standard APRS algorithm) */ fun toCompressedString(): String { val jRound = round((90.0 - latitude) * 380926.0).toLong() val j = jRound / 753571 + 33 @@ -91,7 +91,7 @@ class AprsPosition( ((j8 % 91).toInt() + 33).toChar() + symbolCode } - /** 单轴 DMS 编码(百分位) */ + /** Single-axis DMS encoding (hundredths) */ private fun getDMS(value: Double, isLat: Boolean): String { var iRound = round(value * 6000.0).toInt() if (iRound < 0) iRound = -iRound diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/model/SatStatus.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/model/SatStatus.kt index 3b76516c..22c9fae8 100644 --- a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/model/SatStatus.kt +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/model/SatStatus.kt @@ -1,6 +1,6 @@ package com.rtbishop.look4sat.core.domain.model -/** 单条卫星状态报告(AMSAT 网站 tooltip 数据) */ +/** One satellite status report (AMSAT site tooltip data) */ data class SatReport( val id: String, // 报告 ID(a885153) val statusText: String, // Heard / Telemetry Only / Not Heard ... @@ -10,26 +10,26 @@ data class SatReport( val timeUtc: String // 2:46-:59 UTC ) -/** 单个 2 小时槽的状态 */ +/** State of one 2-hour slot */ data class SatSlot( val statusColor: Long, // ARGB 状态色(-1 = 无报告) val count: Int, // 报告数量(0 = 无) val reportIds: List = emptyList() // 该槽报告 ID 列表 ) -/** 卫星一天的状态(12 个 2 小时槽) */ +/** One satellite day (12 two-hour slots) */ data class SatDay( val dateLabel: String, // "Aug 4" val slots: List // 12 槽(00-02 ... 22-24) ) -/** 单个卫星 6 天状态 */ +/** One satellite, 6 days of state */ data class SatStatus( val name: String, // "AO-123_[FM]" val days: List // 6 天(新→旧) ) -/** 页面整体解析结果 */ +/** Overall page parse result */ data class SatStatusPage( val fetchedAtUtcMs: Long, val statuses: List, diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/model/Settings.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/model/Settings.kt index d8cbed65..425e3352 100644 --- a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/model/Settings.kt +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/model/Settings.kt @@ -63,13 +63,13 @@ data class OtherSettings( val sstvMode: String = "Auto", val lowElevation: Double = 15.0, val highElevation: Double = 45.0, - // UI 设置: 底部导航栏隐藏的页面(Screen simpleName 列表, 默认空 = 全部显示) + // UI settings: pages hidden from the bottom nav (Screen simpleName list; empty default = all shown) val hiddenScreens: List = emptyList(), - // UI 设置: 页面顺序(空 = 默认顺序: 卫星/过境/雷达/地图/设置) + // UI settings: page order (empty = default: Satellites/Passes/Radar/Map/Settings) val screenOrder: List = emptyList(), - // UI 设置: 更多菜单顺序(空 = 默认: 匹配/漫游/CW解码) + // UI settings: More-menu order (empty = default: Mutual/Roaming/CW decode) val subMenuOrder: List = emptyList(), - // WaveLog 日志系统(4.5.2): 服务器配置 + // WaveLog logging (4.5.2): server config val wavelogUrl: String = "", val wavelogApiKey: String = "", val wavelogStationId: String = "", diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/qrz/QrzGridClient.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/qrz/QrzGridClient.kt index 70299bc0..c6c9bb48 100644 --- a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/qrz/QrzGridClient.kt +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/qrz/QrzGridClient.kt @@ -1,9 +1,9 @@ -/* QrzGridClient.kt — QRZ 呼号网格爬虫(4.5.5, domain 层纯 JVM)。 - * 原理(已实测): 带用户 QRZ 登录 Cookie GET https://www.qrz.com/db/{呼号}, - * 页面 Detail 表格里有 。 - * 匿名无 Cookie 拿不到 Detail(已确认); 对方没填网格时表格无此行(返回 null)。 - * Cookie 由用户在设置页粘贴(EditThisCookie 导出的 JSON 数组 或 原始 Cookie 串), - * 绝不内置/进构建。 +/* QrzGridClient.kt - QRZ callsign grid scraper (4.5.5, pure JVM in domain). + * How it works (verified): GET https://www.qrz.com/db/{callsign} with the user's QRZ login Cookie, + * the Detail table on the page holds . + * Without cookies the Detail is unavailable (confirmed); if the other station has no grid, the row is absent (null). + * The Cookie is pasted by the user in settings (EditThisCookie JSON export or a raw cookie string), + * never built into the app. */ package com.rtbishop.look4sat.core.domain.qrz @@ -13,11 +13,11 @@ import java.net.URL object QrzGridClient { - /** 解析用户粘贴的 Cookie(兼容两种格式): EditThisCookie JSON 数组 或 原始 "k=v; k=v" 串 */ + /** Parse the pasted Cookie (both formats): EditThisCookie JSON array or raw "k=v; k=v" string */ fun parseCookies(raw: String): String { val text = raw.trim() if (text.isEmpty()) return "" - // JSON 数组格式: [{"name":"qz_userid","value":"1266043",...}, ...] + // JSON array format: [{"name":"qz_userid","value":"1266043",...}, ...] if (text.startsWith("[")) { return try { val arr = org.json.JSONArray(text) @@ -34,7 +34,7 @@ object QrzGridClient { return text } - /** 检测 cookies 对应的登录呼号(请求 db 首页, 提取账号菜单呼号)。失败返回 null */ + /** Detect the callsign logged in with these cookies (fetch db home, extract the account menu callsign). Null on failure */ suspend fun fetchOwnCallsign(cookieHeader: String): String? = withContext(Dispatchers.IO) { if (cookieHeader.isBlank()) return@withContext null try { @@ -45,13 +45,13 @@ object QrzGridClient { conn.setRequestProperty("User-Agent", "Mozilla/5.0 (Linux; Android 13) Look4Sat-Pro") conn.setRequestProperty("Cookie", cookieHeader) val html = conn.getInputStream().bufferedReader().use { it.readText() } - // 登录账号菜单(实测结构):
  • BG7NTA
      + // Logged-in account menu (verified):
    • BG7NTA
        val pattern = Regex("
      • ]*>\\s*([A-Z0-9/]+)\\s*Grid Square
  • """) .find(html) m?.groupValues?.get(1)?.trim()?.takeIf { it.isNotBlank() } diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/IAmSatRepository.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/IAmSatRepository.kt index c8f11b1c..71d37add 100644 --- a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/IAmSatRepository.kt +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/IAmSatRepository.kt @@ -2,8 +2,8 @@ package com.rtbishop.look4sat.core.domain.repository import com.rtbishop.look4sat.core.domain.model.SatStatusPage -/** AMSAT 卫星状态数据源 */ +/** AMSAT satellite status data source */ interface IAmSatRepository { - /** 抓取并解析 AMSAT 状态页; 失败返回 null */ + /** Fetch and parse the AMSAT status page; null on failure */ suspend fun fetchStatus(): SatStatusPage? } diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/IMainContainer.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/IMainContainer.kt index e019cedf..bef07d61 100644 --- a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/IMainContainer.kt +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/IMainContainer.kt @@ -43,7 +43,7 @@ interface IMainContainer { fun provideRxRadioController(): IRadioController fun provideAudioCapture(): IAudioCapture fun provideSaveImage(): ISaveImage - // WaveLog 日志(4.5.2) + // WaveLog logging (4.5.2) val wavelogQueue: com.rtbishop.look4sat.core.domain.wavelog.WavelogQueue fun provideWavelogUploader(): com.rtbishop.look4sat.core.domain.wavelog.WavelogUploader diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/ISettingsRepo.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/ISettingsRepo.kt index f2a5c3fe..69120461 100644 --- a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/ISettingsRepo.kt +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/ISettingsRepo.kt @@ -45,7 +45,7 @@ interface ISettingsRepo { //region # Station position settings val stationPosition: StateFlow fun setStationPosition(latitude: Double, longitude: Double, altitude: Double): Boolean - /** GPS 定位(挂起): 拿到位置才返回 true; 权限缺失/超时/无信号返回 false */ + /** GPS fix (suspend): true only with a fix; false on missing permission/timeout/no signal */ suspend fun setStationPosition(): Boolean fun setStationPosition(locator: String): Boolean //endregion diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/source/IRemoteSource.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/source/IRemoteSource.kt index 8437c927..c20cefde 100644 --- a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/source/IRemoteSource.kt +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/source/IRemoteSource.kt @@ -23,6 +23,6 @@ interface IRemoteSource { suspend fun getFileStream(uri: String): InputStream? suspend fun getNetworkStream(url: String): InputStream? - /** 抓取 AMSAT 状态页 HTML(带 UA, 返回 null = 失败) */ + /** Fetch AMSAT status page HTML (with UA; null = failure) */ suspend fun getStatusHtml(): String? } diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/source/Sources.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/source/Sources.kt index ddc4a0de..601db98f 100644 --- a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/source/Sources.kt +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/source/Sources.kt @@ -18,7 +18,7 @@ package com.rtbishop.look4sat.core.domain.source object Sources { - // 在线更新的默认 URL(用户可通过"自定义URL"对话框修改/重置) + // Default URL for online updates (user can change/reset it via the "Custom URL" dialog) val defaultTleUrl = "https://celestrak.org/NORAD/elements/gp.php?GROUP=active&FORMAT=csv" val defaultTransceiversUrl = "https://db.satnogs.org/api/transmitters/?format=json&status=active" diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/usecase/IShowToast.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/usecase/IShowToast.kt index 7c20324b..983fad50 100644 --- a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/usecase/IShowToast.kt +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/usecase/IShowToast.kt @@ -20,6 +20,6 @@ package com.rtbishop.look4sat.core.domain.usecase interface IShowToast { operator fun invoke(message: String) - /** 按资源 ID 显示(四语文案) */ + /** Show by resource ID (four-language text) */ operator fun invoke(resId: Int) } diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/utility/QthConverter.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/utility/QthConverter.kt index afea0353..907081cf 100644 --- a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/utility/QthConverter.kt +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/utility/QthConverter.kt @@ -118,7 +118,7 @@ fun qthToSquare(locator: String): String { * Builds the 3x3 grid of 4-char squares surrounding [square] (e.g. "OL42"). * Row 0 = north (lat +1), col 0 = west (lon -1). Handles field/square carry * at boundaries (e.g. "AA00" wraps to "RR99" at the south-west corner). - * Mirrors the neighbor logic decompiled from the QTH定位器 app. + * Mirrors the neighbor logic decompiled from the QTH Locator app. */ fun qthNeighbors(square: String): List { if (square.length != 4) return emptyList() diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/wavelog/ILotwSatellitesRepo.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/wavelog/ILotwSatellitesRepo.kt index c793111b..dbe84503 100644 --- a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/wavelog/ILotwSatellitesRepo.kt +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/wavelog/ILotwSatellitesRepo.kt @@ -1,5 +1,5 @@ -/* ILotwSatellitesRepo.kt — LoTW 卫星列表刷新接口(domain 层, 4.5.5)。 - * 实现: core/data 的 LotwSatellitesRepo(下载 ARRL config.tq6)。 +/* ILotwSatellitesRepo.kt - LoTW satellite list refresh interface (domain layer, 4.5.5). + * Impl: LotwSatellitesRepo in core/data (downloads ARRL config.tq6). */ package com.rtbishop.look4sat.core.domain.wavelog diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/wavelog/LotwSatellites.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/wavelog/LotwSatellites.kt index b00aba0f..e812efd3 100644 --- a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/wavelog/LotwSatellites.kt +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/wavelog/LotwSatellites.kt @@ -1,7 +1,7 @@ -/* LotwSatellites.kt — LoTW 支持的卫星名列表(112 个, 静态内置)。 - * 来源: https://lotw.arrl.org/lotw/config.tq6 (gzip XML, ARRL 官方!) - * WaveLog 用同一来源更新 satellite 表 lotw 字段(Update_model.php lotw_sats())。 - * 运行时可由 LotwSatellitesRepo 手动刷新(设置页 WaveLog 区按钮触发)。 +/* LotwSatellites.kt - LoTW-supported satellite name list (112, statically embedded). + * Source: https://lotw.arrl.org/lotw/config.tq6 (gzip XML, official ARRL!) + * WaveLog updates its satellite table's lotw field from the same source (Update_model.php lotw_sats()). + * At runtime LotwSatellitesRepo can refresh it manually (settings WaveLog section button). */ package com.rtbishop.look4sat.core.domain.wavelog @@ -11,11 +11,11 @@ object LotwSatellites { @Volatile private var dynamicNames: Set = emptySet() - /** 当前生效的卫星名: 静态内置 ∪ 运行时更新 */ + /** Effective satellite names: static embedded set union runtime updates */ val names: Set get() = if (dynamicNames.isEmpty()) staticNames else staticNames + dynamicNames - /** 运行时更新(设置页按钮 → LotwSatellitesRepo.refresh() 后调用) */ + /** Runtime update (settings button -> called after LotwSatellitesRepo.refresh()) */ fun updateNames(newNames: Set) { if (newNames.isNotEmpty()) dynamicNames = newNames } diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/wavelog/WaveLogApi.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/wavelog/WaveLogApi.kt index f179b149..2cd231ba 100644 --- a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/wavelog/WaveLogApi.kt +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/wavelog/WaveLogApi.kt @@ -1,12 +1,12 @@ /* - * WaveLogApi.kt — WaveLog 日志服务器 API 客户端(4.5.2 覆盖修复 2)。 + * WaveLogApi.kt - WaveLog log server API client (4.5.2 override fix 2). * - * 同时支持 v1 与 v2(用户服务器实测只有 v1, v2 返回 404): - * v2: POST {base}/api/v2/qso (Authorization: Bearer + JSON 字段) - * v1: POST {base}/index.php/api/qso (key 在 JSON body + ADIF 字符串) - * 策略: 优先 v2, 404 自动降级 v1。 - * 测试连接: v2 GET api/v2/token → 404 时 v1 POST api/get_contacts_adif。 - * 站点网格: 仅 v2 有 GET api/v2/station/{id}; v1 无此端点 → 降级用用户 QTH。 + * Supports both v1 and v2 (user's server only has v1 in practice; v2 returns 404): + * v2: POST {base}/api/v2/qso (Authorization: Bearer + JSON fields) + * v1: POST {base}/index.php/api/qso (key in JSON body + ADIF string) + * Strategy: try v2 first, auto-fallback to v1 on 404. + * Test connection: v2 GET api/v2/token; on 404 use v1 POST api/get_contacts_adif. + * Station grid: only v2 has GET api/v2/station/{id}; v1 lacks it -> fall back to user QTH. */ package com.rtbishop.look4sat.core.domain.wavelog @@ -20,7 +20,7 @@ import java.net.HttpURLConnection import java.net.URL import java.util.Locale -/** 站点信息(GET /api/v2/station/{id} 结果) */ +/** Station info (GET /api/v2/station/{id} result) */ data class WavelogStation( val id: Int, val name: String, @@ -37,7 +37,7 @@ object WaveLogApi { private const val TIMEOUT_MS = 15000 - /** 规范化服务器地址: 去尾斜杠/去尾部 index.php; 无协议前缀时补 https:// */ + /** Normalize server URL: strip trailing slash/index.php; prepend https:// when missing */ fun normalizeUrl(raw: String): String { var u = raw.trim().trimEnd('/') if (u.isBlank()) return "" @@ -46,7 +46,7 @@ object WaveLogApi { return u } - /** 测试连接: v2 GET api/v2/token → 404 时 v1 POST api/get_contacts_adif */ + /** Test connection: v2 GET api/v2/token; on 404 use v1 POST api/get_contacts_adif */ suspend fun testToken(url: String, apiKey: String, stationId: String = ""): WavelogResult = withContext(Dispatchers.IO) { val base = normalizeUrl(url) if (base.isBlank()) return@withContext WavelogResult.Failure("服务器地址为空") @@ -55,7 +55,7 @@ object WaveLogApi { val v2 = httpRequest("$base/index.php/api/v2/token", "GET", apiKey, null) if (v2.first in 200..299) return@withContext WavelogResult.Success("连接成功 (API v2)") - // v1: POST /index.php/api/get_contacts_adif(key 在 body) + // v1: POST /index.php/api/get_contacts_adif (key in body) if (stationId.isNotBlank()) { val body = JSONObject().apply { put("key", apiKey) @@ -66,7 +66,7 @@ object WaveLogApi { if (v1.first in 200..299) return@withContext WavelogResult.Success("连接成功 (API v1)") if (v1.first == 401) return@withContext WavelogResult.Failure("API 密钥无效 (v1: 401)") } - // 无 index.php 的 v1 尝试 + // v1 attempt without index.php val body = JSONObject().apply { put("key", apiKey) put("station_id", stationId) @@ -79,7 +79,7 @@ object WaveLogApi { WavelogResult.Failure("连接失败: v2 HTTP ${v2.first}, v1 HTTP ${v1b.first} — 请确认服务器地址/密钥正确") } - /** 站点信息: 仅 v2; v1 无此端点返回 null 标记(网格检测降级用用户 QTH) */ + /** Station info: v2 only; v1 lacks the endpoint (grid check falls back to user QTH) */ suspend fun getStation(url: String, apiKey: String, stationId: String): WavelogResult = withContext(Dispatchers.IO) { val base = normalizeUrl(url) if (base.isBlank()) return@withContext WavelogResult.Failure("服务器地址为空") @@ -102,19 +102,19 @@ object WaveLogApi { WavelogResult.Failure("解析失败: ${e.message}") } } - // v1 无 station 端点 → 返回 Success 空(调用方降级用用户 QTH) + // v1 has no station endpoint -> return empty Success (caller falls back to user QTH) WavelogResult.Success("") } - /** LoTW 认可的卫星名: 取括号前主名 + 大写(ISS 特判) */ + /** LoTW-recognized satellite name: main name before parentheses, uppercased (ISS special case) */ fun normalizeSatName(raw: String): String { val main = raw.substringBefore('(').trim() .ifBlank { raw.trim() } .uppercase(Locale.ENGLISH) - // 匹配逻辑(仿 WaveLog 卫星表 name/displayname 匹配 + LoTW 列表): - // 1. 已在 LoTW 列表(常见 TLE 名 == 通用名) → 原样返回 - // 2. 不在 → 查 Celestrak 别名映射(SAUDISAT 1C → SO-50 等) → 映射后必须在 LoTW 列表 - // 3. 仍不匹配 → 原样返回(不阻塞上传, QSO 仍保存) + // Matching logic (mirrors WaveLog satellite table name/displayname matching + LoTW list): + // 1. Already in LoTW list (common TLE name == common name) -> return as-is + // 2. Not present -> check Celestrak alias map (SAUDISAT 1C -> SO-50 etc.); mapped name must be in LoTW list + // 3. Still unmatched -> return as-is (uploads are not blocked; QSO is still saved) val commonName = mapOf( "ZARYA" to "ARISS", "ARISS" to "ARISS", @@ -128,7 +128,7 @@ object WaveLogApi { return if (candidate in LotwSatellites.names) candidate else main } - /** 创建 QSO: 优先 v2, 404 降级 v1(ADIF) */ + /** Create QSO: v2 first, fall back to v1 (ADIF) on 404 */ suspend fun postQso( url: String, apiKey: String, @@ -141,7 +141,7 @@ object WaveLogApi { val satName = normalizeSatName(qso.satName) - // v2: POST /index.php/api/v2/qso(JSON 字段) + // v2: POST /index.php/api/v2/qso (JSON fields) val v2Body = JSONObject().apply { put("station_profile_id", stationProfileId.toIntOrNull() ?: 0) put("call", qso.call) @@ -160,7 +160,7 @@ object WaveLogApi { if (code in 200..299) return@withContext WavelogResult.Success("已上传 (v2)") if (code == 409) return@withContext WavelogResult.Success("重复(已存在)") - // v1: POST /index.php/api/qso(key 在 body + ADIF) + // v1: POST /index.php/api/qso (key in body + ADIF) val v1Body = JSONObject().apply { put("key", apiKey) put("station_profile_id", stationProfileId) @@ -170,14 +170,14 @@ object WaveLogApi { val (code1, resp1) = httpRequest("$base/index.php/api/qso", "POST", apiKey, v1Body.toString()) if (code1 in 200..299) return@withContext WavelogResult.Success("已上传 (v1)") - // 无 index.php 的 v1 + // v1 without index.php val (code1b, resp1b) = httpRequest("$base/api/qso", "POST", apiKey, v1Body.toString()) if (code1b in 200..299) return@withContext WavelogResult.Success("已上传 (v1)") WavelogResult.Failure("上传失败: v2 HTTP $code, v1 HTTP $code1 — ${shortError(resp1.ifBlank { resp1b })}") } - /** v1 ADIF 字符串(频率 MHz, 长度=UTF-8 字节数, sat_name 已规范化) */ + /** v1 ADIF string (freq in MHz, length = UTF-8 byte count, sat_name normalized) */ private fun toAdif(qso: WavelogQso, gridsquare: String, satName: String): String { fun field(name: String, value: String): String { val bytes = value.toByteArray(Charsets.UTF_8).size @@ -204,7 +204,7 @@ object WaveLogApi { } } - /** 通用 HTTP 请求(返回 code + body) */ + /** Generic HTTP request (returns code + body) */ private fun httpRequest(url: String, method: String, apiKey: String, jsonBody: String?): Pair { return try { val conn = URL(url).openConnection() as HttpURLConnection @@ -230,7 +230,7 @@ object WaveLogApi { } private fun shortError(body: String): String { - if (body.startsWith("<")) return body.take(80) // HTML 错误页 + if (body.startsWith("<")) return body.take(80) // HTML error page return try { val obj = JSONObject(body) val err = obj.optJSONObject("error") diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/wavelog/WavelogQueue.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/wavelog/WavelogQueue.kt index 8592df0d..ab9bc962 100644 --- a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/wavelog/WavelogQueue.kt +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/wavelog/WavelogQueue.kt @@ -1,22 +1,22 @@ /* - * WavelogQueue.kt — WaveLog 本地日志队列(4.5.2)。 + * WavelogQueue.kt - WaveLog local log queue (4.5.2). * - * 纯 Kotlin(不依赖 Android): 存储走 IWavelogQueueStore 接口, - * 由 core/data 用 SharedPreferences 实现。 - * 队列上限 500 条(超出丢最旧)。 + * Pure Kotlin (no Android deps): storage goes through the IWavelogQueueStore interface, + * implemented with SharedPreferences in core/data. + * Queue capped at 500 entries (oldest dropped beyond that). */ package com.rtbishop.look4sat.core.domain.wavelog import org.json.JSONArray import org.json.JSONObject -/** 存储抽象(SharedPreferences 实现见 core/data) */ +/** Storage abstraction (SharedPreferences impl lives in core/data) */ interface IWavelogQueueStore { fun load(): String fun save(json: String) } -/** 待上传的 QSO 条目(本地队列元素,与 POST /api/v2/qso 字段对应) */ +/** QSO entry awaiting upload (local queue element, mirrors POST /api/v2/qso fields) */ data class WavelogQso( val id: String, // 本地唯一 id(UUID) val timeUtcMs: Long, // 回车时刻 UTC 毫秒(本地显示 + 组装 qso_date/time_on) @@ -73,18 +73,18 @@ class WavelogQueue(private val store: IWavelogQueueStore) { save(all().filter { it.id !in ids }) } - /** 标记为已上传(保留在队列, 表格打勾) */ + /** Mark as uploaded (kept in the queue; checkmark in the table) */ @Synchronized fun markUploaded(id: String) { save(all().map { if (it.id == id) it.copy(uploaded = true) else it }) } - /** 更新某条 QSO 的对方网格(QRZ 爬虫异步回填, 4.5.5) */ + /** Update a QSO's counterpart grid (async backfill from the QRZ scraper, 4.5.5) */ fun updateGridsquare(id: String, grid: String) { save(all().map { if (it.id == id) it.copy(gridsquare = grid) else it }) } - /** 移除所有已上传条目(可选项, 保持队列精简) */ + /** Remove all uploaded entries (optional; keeps the queue lean) */ @Synchronized fun removeUploaded() { save(all().filter { !it.uploaded }) diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/wavelog/WavelogUploader.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/wavelog/WavelogUploader.kt index 255d85a2..0bcf8908 100644 --- a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/wavelog/WavelogUploader.kt +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/wavelog/WavelogUploader.kt @@ -1,10 +1,10 @@ /* - * WavelogUploader.kt — WaveLog 队列上传调度(4.5.2)。 + * WavelogUploader.kt - WaveLog queue upload scheduler (4.5.2). * - * 手动/周期上传共用: 逐条网格检测(用户 QTH 前 4 位 vs 台站网格前 4 位) - * → POST /api/v2/qso → 成功移出队列。 - * 网格不一致: 返回 NeedConfirm(由 UI 弹窗「忽略并上传/取消」), - * 确认后带 force=true 重试本批。 + * Manual/periodic uploads share: per-batch grid check (user QTH first 4 chars vs station grid first 4 chars) + * -> POST /api/v2/qso -> success removes from the queue. + * Grid mismatch: returns NeedConfirm (UI dialog "Ignore and upload / Cancel"), + * retrying the batch with force=true once confirmed. */ package com.rtbishop.look4sat.core.domain.wavelog @@ -26,10 +26,10 @@ class WavelogUploader( private val queue: WavelogQueue ) { - // 台站网格缓存(每次上传前刷新; 失败用旧值) + // Station grid cache (refreshed before each upload; stale value kept on failure) private var cachedStationGrid: String? = null - /** 上传整个队列。force=true 跳过网格确认(用户已选「忽略并上传」) */ + /** Upload the whole queue. force=true skips the grid confirm (user chose "Ignore and upload") */ suspend fun uploadQueue(force: Boolean = false): UploadOutcome { val settings = settingsRepo.otherSettings.value val url = settings.wavelogUrl @@ -39,14 +39,14 @@ class WavelogUploader( return UploadOutcome.Done(0, queue.all().size, "未配置 WaveLog 服务器") } - // 1. 拉站点信息(拿台站网格); v1 无此端点时降级用用户 QTH + // 1. Fetch station info (station grid); fall back to user QTH when v1 lacks the endpoint val stationGrid = getStationGrid(url, apiKey, stationId) ?: userQthGrid() if (stationGrid.isNullOrBlank()) { return UploadOutcome.Done(0, queue.all().size, "无法获取站点信息(检查站点 ID/密钥权限)") } - // 2. 网格检测: 云端站点网格前 4 位 vs 当前站位网格前 4 位 - // (防站点配置错误; 与 QSO 对方网格无关 — 用户澄清) + // 2. Grid check: cloud station grid first 4 chars vs current station QTH first 4 chars + // (guards against a misconfigured station; unrelated to the QSO counterpart grid - per user) if (!force) { val userGrid = userQthGrid() if (userGrid != null && stationGrid.take(4).lowercase() != userGrid.take(4).lowercase()) { @@ -54,7 +54,7 @@ class WavelogUploader( } } - // 3. 逐条上传。ADIF 网格字段 = 对方网格(通联对象), 爬虫接入前留空 + // 3. Upload one by one. ADIF gridsquare = counterpart grid (the QSO partner); blank until the scraper lands val entries = queue.all() var ok = 0 var fail = 0 @@ -85,7 +85,7 @@ class WavelogUploader( return cachedStationGrid } - /** 用户当前 QTH 网格(前 4 位; 无 QTH 返回 null = 跳过检测) */ + /** User's current QTH grid (first 4 chars; null when no QTH = check skipped) */ private fun userQthGrid(): String? { return settingsRepo.stationPosition.value.qthLocator?.takeIf { it.length >= 4 } } diff --git a/core/domain/src/test/java/com/rtbishop/look4sat/core/domain/QthConverterTest.kt b/core/domain/src/test/java/com/rtbishop/look4sat/core/domain/QthConverterTest.kt index 4b87942a..bf73207a 100644 --- a/core/domain/src/test/java/com/rtbishop/look4sat/core/domain/QthConverterTest.kt +++ b/core/domain/src/test/java/com/rtbishop/look4sat/core/domain/QthConverterTest.kt @@ -87,7 +87,7 @@ class QthConverterTest { @Test fun `Given square returns correct 3x3 neighbors`() { - // Reference grid from the QTH定位器 screenshot: OL42 + // Reference grid from the QTH Locator screenshot: OL42 val neighbors = qthNeighbors("OL42") assert(neighbors == listOf( "OL33", "OL43", "OL53", diff --git a/core/presentation/src/main/java/com/rtbishop/look4sat/core/presentation/Navigation.kt b/core/presentation/src/main/java/com/rtbishop/look4sat/core/presentation/Navigation.kt index b44596b4..a722be18 100644 --- a/core/presentation/src/main/java/com/rtbishop/look4sat/core/presentation/Navigation.kt +++ b/core/presentation/src/main/java/com/rtbishop/look4sat/core/presentation/Navigation.kt @@ -54,10 +54,10 @@ sealed class Screen(val iconResId: Int, val titleResId: Int, val screenId: Strin data object Settings : Screen(R.drawable.ic_settings, R.string.nav_prefs, "Settings") } -// UI 设置: 默认主菜单页面顺序(底部栏 5 槽) +// UI settings: default main-menu page order (5 bottom-bar slots) val defaultScreenOrder = listOf("Satellites", "Passes", "Radar", "Map", "Settings") -// UI 设置: 默认更多菜单顺序(进「更多」的页面) +// UI settings: default More-menu order (pages behind "More") val defaultSubMenuOrder = listOf("Mutual", "Roaming", "CwDecode", "WavelogLog", "AMSAT") @Serializable diff --git a/feature/cw/src/main/java/B/RunnableC0001b.java b/feature/cw/src/main/java/B/RunnableC0001b.java index a5d5ea58..60c36483 100644 --- a/feature/cw/src/main/java/B/RunnableC0001b.java +++ b/feature/cw/src/main/java/B/RunnableC0001b.java @@ -6,11 +6,11 @@ import com.ve3nea.morse_expert.MainActivity; import H2.b; /** - * 照搬自 Morse Expert 1.15 B.RunnableC0001b, 类名保持混淆原名。 - * R8 合并 Runnable 手术: 构造按 smali 真实签名 5 参 (Object,Object,int,int,boolean), - * 字段 = i(Object)/h(Object)/g(int)/f(int); run() 仅保留 case 4(f==2 对应 pswitch_4: - * 解码文本 UI 更新, H2.b 调用), 其余 case(权限/Intent/通知等库代码)已裁剪。 - * 注: jadx 4 参构造为错误还原, 已按 smali 修正。 + * Ported from Morse Expert 1.15 B.RunnableC0001b; obfuscated class name kept. + * R8-merged Runnable surgery: constructor per the real smali 5-arg signature (Object,Object,int,int,boolean), + * fields = i(Object)/h(Object)/g(int)/f(int); run() keeps only case 4 (f==2 maps to pswitch_4: + * decode-text UI update, invoked by H2.b); other cases (permission/Intent/notification library code) were trimmed. + * Note: jadx's 4-arg constructor was a wrong reconstruction; fixed per smali. */ public final class RunnableC0001b implements Runnable { public final /* synthetic */ int f; diff --git a/feature/cw/src/main/java/B0/b.java b/feature/cw/src/main/java/B0/b.java index bbaba882..43d11171 100644 --- a/feature/cw/src/main/java/B0/b.java +++ b/feature/cw/src/main/java/B0/b.java @@ -7,11 +7,11 @@ import java.util.Locale; import d1.AbstractC1518b; /** - * 照搬自 Morse Expert 1.15 B0.b, 类名保持混淆原名。 - * R8 合并 Runnable 手术: 构造按 smali 真实签名 (H2/b, String, [I) -> f=4; - * 只保留 case 4(状态栏更新: dBFS/Hz/dB/WPM + ScaleView 频率标记, H2.b 调用), - * 其余 case(WorkManager/动画等库代码)已裁剪。 - * r3 = scaleView.getHeight()(smali 证实, jadx 寄存器未展开)。 + * Ported from Morse Expert 1.15 B0.b; obfuscated class name kept. + * R8-merged Runnable surgery: constructor per the real smali signature (H2/b, String, [I) -> f=4; + * keeps only case 4 (status-bar update: dBFS/Hz/dB/WPM + ScaleView frequency markers, invoked by H2.b), + * other cases (WorkManager/animation library code) were trimmed. + * r3 = scaleView.getHeight() (smali-verified; jadx never expanded the registers). */ public final class b implements Runnable { public final /* synthetic */ int f; @@ -50,7 +50,7 @@ public final class b implements Runnable { ScaleView scaleView = (ScaleView) mainActivity.f11035D.f11889h; int i4 = iArr[1]; scaleView.f11041g = iArr[5]; - // r3 = scaleView.getHeight()(smali 证实) + // r3 = scaleView.getHeight() (smali-verified) scaleView.f = (scaleView.getHeight() - 1) - Math.round(((Math.round(i4 * 0.128f) - H2.b.f606z) * scaleView.getHeight()) / H2.b.f603B); scaleView.invalidate(); return; diff --git a/feature/cw/src/main/java/D/n.java b/feature/cw/src/main/java/D/n.java index ddb2a7e2..c8b27a31 100644 --- a/feature/cw/src/main/java/D/n.java +++ b/feature/cw/src/main/java/D/n.java @@ -6,9 +6,9 @@ import android.opengl.GLUtils; import com.ve3nea.morse_expert.WaterfallSurfaceView; /** - * 照搬自 Morse Expert 1.15, 类名保持混淆原名。 - * R8 合并 Runnable 手术: 只保留 case 1(瀑布图频谱->OpenGL 纹理, H2.b 调用), - * 其余 case(库/广告)已裁剪。 + * Ported from Morse Expert 1.15; obfuscated class name kept. + * R8-merged Runnable surgery: keeps only case 1 (waterfall spectrum -> OpenGL texture, invoked by H2.b), + * other cases (library/ads) were trimmed. */ public final /* synthetic */ class n implements Runnable { public final /* synthetic */ int f; diff --git a/feature/cw/src/main/java/E2/g.java b/feature/cw/src/main/java/E2/g.java index 5f836223..7346052d 100644 --- a/feature/cw/src/main/java/E2/g.java +++ b/feature/cw/src/main/java/E2/g.java @@ -7,9 +7,9 @@ import com.ve3nea.morse_expert.MainActivity; import com.ve3nea.morse_expert.ScaleView; /** - * 照搬自 Morse Expert 1.15 (com.ve3nea.morse_expert), 类名保持混淆原名。 - * R8 合并类手术: 只保留 case 1(瀑布图触摸选频率=应用逻辑), - * 其余 case(广告 P4 / appcompat 焦点 / 弹窗)为库代码, 已裁剪。 + * Ported from Morse Expert 1.15 (com.ve3nea.morse_expert); obfuscated class name kept. + * R8-merged class surgery: keeps only case 1 (waterfall touch-to-select frequency = app logic), + * other cases (ad P4 / appcompat focus / dialogs) are library code, trimmed. */ public final class g implements View.OnTouchListener { public final /* synthetic */ int f; @@ -29,8 +29,8 @@ public final class g implements View.OnTouchListener { if (i4 == 1) { MainActivity mainActivity = (MainActivity) obj; if (motionEvent.getActionMasked() == 0 && mainActivity.f11037F != null) { - // smali 证实: p1 先被重赋值为 f11035D.h(ScaleView)再 getHeight; - // 公式 = round((f606z + ((scaleH-1-y)*f603B/scaleH)) / 0.128f) + // smali-verified: p1 is reassigned to f11035D.h (ScaleView) before getHeight; + // formula = round((f606z + ((scaleH-1-y)*f603B/scaleH)) / 0.128f) ScaleView scaleView = (ScaleView) mainActivity.f11035D.f11889h; float y3 = motionEvent.getY(); int round = Math.round((H2.b.f606z + ((((scaleView.getHeight() - 1) - y3) * H2.b.f603B) / scaleView.getHeight())) / 0.128f); diff --git a/feature/cw/src/main/java/F2/a.java b/feature/cw/src/main/java/F2/a.java index 6f2bc8b2..d1d51a6b 100644 --- a/feature/cw/src/main/java/F2/a.java +++ b/feature/cw/src/main/java/F2/a.java @@ -35,7 +35,7 @@ public final class a implements ActionMode.Callback { @Override // android.view.ActionMode.Callback public final boolean onCreateActionMode(ActionMode actionMode, Menu menu) { this.f564a.f11032n = true; - // 原: if (menu.findItem(R.id.shareText) == null) 检查(shareText 为原 APK 资源, 简化后始终添加) + // Original: if (menu.findItem(R.id.shareText) == null) check (shareText is an original APK resource; always added after simplification) menu.add(0, 74, 0, "Share").setIcon(R.drawable.ic_baseline_share_24); menu.add(0, 73, 0, "Save").setIcon(R.drawable.ic_baseline_save_24); return true; diff --git a/feature/cw/src/main/java/H2/b.java b/feature/cw/src/main/java/H2/b.java index 3012a6b3..9741661a 100644 --- a/feature/cw/src/main/java/H2/b.java +++ b/feature/cw/src/main/java/H2/b.java @@ -467,7 +467,7 @@ public final class b { fArr3[i11 - i10] = f4; i11++; } else { - // smali: 循环内无 try; 录音写入 try 在循环后(见下) + // smali: no try inside the loop; the recording-write try sits after the loop (see below) break; } } @@ -522,7 +522,7 @@ public final class b { } tNativeDecoder.setHamMode(this.f623s.get()); } catch (Exception e5) { - // jadx 幻觉区(空循环/空 if 为 catch 错位还原)已裁剪; 原 smali: 记录异常后跳回正常流程尾部 + // jadx phantom zones (empty loops/ifs from misplaced catch reconstruction) trimmed; original smali: logs the exception, then jumps back to the normal flow tail exc = e5; bVar = this; Log.e("Audio Processor", "Exception", exc); @@ -544,7 +544,7 @@ public final class b { try { handler.post(new RunnableC0001b(bVar, spannableStringBuilder, tNativeDecoder.getDeleteCount(), 2, false)); } catch (Exception e6) { - // jadx 幻觉区已裁剪(同 catch e5); 原 smali: 记录异常后跳回正常流程尾部 + // jadx phantom zone trimmed (same catch e5); original smali: logs the exception, then jumps back to the normal flow tail exc = e6; Log.e("Audio Processor", "Exception", exc); } @@ -552,7 +552,7 @@ public final class b { bVar = this; } } catch (Exception e7) { - // jadx 幻觉(e 变量)已裁剪 + // jadx phantom (the e variable) trimmed bVar = this; } i4 = f603B; diff --git a/feature/cw/src/main/java/I0/d.java b/feature/cw/src/main/java/I0/d.java index f63089ac..107b90d5 100644 --- a/feature/cw/src/main/java/I0/d.java +++ b/feature/cw/src/main/java/I0/d.java @@ -1,11 +1,11 @@ package I0; /** - * 照搬自 Morse Expert 1.15 I0.d, 类名保持混淆原名。 - * R8 合并类手术: 只保留 (i3.d, int, int, float[]) FFT 任务构造; - * run() 按 smali 原样还原: R8 已将任务体优化为死代码(aget 后 throw null, - * 永不执行), 保留原语义(行为与原 APK 完全一致)。 - * SystemForegroundService(work 库)case 已裁剪。 + * Ported from Morse Expert 1.15 I0.d; obfuscated class name kept. + * R8-merged class surgery: keeps only the (i3.d, int, int, float[]) FFT task constructor; + * run() restored verbatim per smali: R8 already optimized the body into dead code (aget then throw null, + * never executed); original semantics kept (behavior identical to the original APK). + * The SystemForegroundService (work library) case was trimmed. */ public final class d implements Runnable { public final /* synthetic */ int f = 0; @@ -37,6 +37,6 @@ public final class d implements Runnable { return; } float f = ((float[]) this.f655i)[i7 * 2]; - // R8 原代码: aget 后 throw null(死代码, 永不执行) —— 照搬语义, 不执行任何计算 + // Original R8 code: aget then throw null (dead code, never runs) - semantics ported, no computation executes } } diff --git a/feature/cw/src/main/java/J2/a.java b/feature/cw/src/main/java/J2/a.java index 5431d71a..dc84fa6f 100644 --- a/feature/cw/src/main/java/J2/a.java +++ b/feature/cw/src/main/java/J2/a.java @@ -28,7 +28,7 @@ public final class a { int i11 = (i9 >> 8) & 255; int i12 = i9 & 255; float f4 = i8; - // r10 = iArr2[i7] (下一个锚点色, smali: aget v10, v4, v8) + // r10 = iArr2[i7] (next anchor color, smali: aget v10, v4, v8) float f5 = (((iArr2[i7] >> 16) & 255) - i10) / f4; float f6 = (((iArr2[i7] >> 8) & 255) - i11) / f4; float f7 = ((iArr2[i7] & 255) - i12) / f4; diff --git a/feature/cw/src/main/java/K1/a.java b/feature/cw/src/main/java/K1/a.java index 8d6a1950..a3e3fe44 100644 --- a/feature/cw/src/main/java/K1/a.java +++ b/feature/cw/src/main/java/K1/a.java @@ -1,9 +1,9 @@ package K1; /** - * 照搬自 Morse Expert 1.15 K1.a 精简版: 仅保留 k3 FFT 库引用的内存统计静态字段 - * (K1.a.c 长整型记账)。原 K1.a(828 行)的其余方法为状态栏/库代码, 已裁剪 - * (状态栏逻辑已在 MainActivity 内联)。 + * Ported from Morse Expert 1.15 K1.a (slimmed): keeps only the memory-accounting static fields the k3 FFT library references + * (K1.a.c long accounting). The rest of the original K1.a (828 lines) was status-bar/library code and was trimmed + * (status-bar logic is inlined in MainActivity). */ public class a { public static long c = 0; diff --git a/feature/cw/src/main/java/com/rtbishop/look4sat/feature/cw/CwDecodeScreen.kt b/feature/cw/src/main/java/com/rtbishop/look4sat/feature/cw/CwDecodeScreen.kt index 447c9866..7ad77763 100644 --- a/feature/cw/src/main/java/com/rtbishop/look4sat/feature/cw/CwDecodeScreen.kt +++ b/feature/cw/src/main/java/com/rtbishop/look4sat/feature/cw/CwDecodeScreen.kt @@ -1,17 +1,17 @@ /* - * CwDecodeScreen.kt — Look4Sat 的 CW 解码页面(Compose 外壳)。 + * CwDecodeScreen.kt - Look4Sat CW decode page (Compose shell). * - * 照搬自 Morse Expert 1.15 的 com.ve3nea.morse_expert.MainActivity(已改为普通控制器类): - * - AndroidView 嵌入原版 activity_main.xml(ConstraintLayout 根, 含 decodedTextView/scaleView/ + * Ported from Morse Expert 1.15 com.ve3nea.morse_expert.MainActivity (converted to a plain controller class): + * - AndroidView embeds the original activity_main.xml (ConstraintLayout root, with decodedTextView/scaleView/ * statusTextView/verticalLayout/waterfallView); - * - 生命周期 onCreate/onResume/onPause/onDestroy 全部委托给控制器, 顺序与原 Activity 一致 - * (onCreate 必须在 onResume 前, onDispose 先 onPause 再 onDestroy); - * - 麦克风权限(RECORD_AUDIO)由 Compose 侧管理, 授予后调用 onPermissionGranted()(= v()), - * 用 initialized 标记保证只调一次(v() 会 new a() 覆盖 f11036E); - * - 原 options_menu 的 清除/暂停/保存/录音/设置 改为顶部一行按钮(图标用模块内原版 drawable)。 + * - Lifecycle onCreate/onResume/onPause/onDestroy fully delegated to the controller, same order as the original Activity + * (onCreate must run before onResume; onDispose runs onPause then onDestroy); + * - Mic permission (RECORD_AUDIO) is managed on the Compose side; onPermissionGranted() (= v()) is called once granted, + * an initialized flag guarantees it runs only once (v() news an a() which overwrites f11036E); + * - The original options_menu Clear/Pause/Save/Record/Settings became a top button row (icons from the module's original drawables). * - * 注意: 控制器 onCreate 内的 setImmersive(window, false) 会把宿主窗口恢复为 - * decorFitsSystemWindows(true)(原版行为照搬, 会对整个宿主 Activity 生效)。 + * Note: setImmersive(window, false) inside the controller's onCreate restores the host window to + * decorFitsSystemWindows(true) (ported original behavior; affects the whole host Activity). */ package com.rtbishop.look4sat.feature.cw @@ -67,9 +67,9 @@ fun CwDecodeScreen(navigateUp: () -> Unit = {}) { val activity = remember(context) { context as? Activity ?: error("CwDecodeScreen must be hosted in an Activity") } - // 照搬的控制器(普通类, 非 Activity); 每次进入页面新建实例 + // Ported controller (plain class, not an Activity); new instance per page entry val controller = remember { MainActivity() } - // 提前 inflate 原版布局, 供 AndroidView 与控制器 onCreate 共用同一根视图 + // Inflate the original layout early so AndroidView and the controller's onCreate share one root view val rootView = remember(context) { LayoutInflater.from(context).inflate(R.layout.activity_main, null) as ConstraintLayout } @@ -81,11 +81,11 @@ fun CwDecodeScreen(navigateUp: () -> Unit = {}) { ) } var permanentlyDenied by remember { mutableStateOf(false) } - // v() 会 new a() 覆盖 f11036E, 必须保证只初始化一次 + // v() news an a() which overwrites f11036E; must run exactly once var initialized by remember { mutableStateOf(false) } var showSettings by remember { mutableStateOf(false) } - // 生命周期: onCreate 必须在 onResume 前; onDispose 先 onPause 再 onDestroy + // Lifecycle: onCreate before onResume; onDispose runs onPause then onDestroy DisposableEffect(Unit) { controller.onCreate(activity, rootView) controller.onResume() @@ -95,12 +95,12 @@ fun CwDecodeScreen(navigateUp: () -> Unit = {}) { } } - // 权限授予后启动解码核心; "首次进入已授权" 与 "弹窗回调授权" 两条路径统一走这里 + // Start the decode core after permission granted; both "already granted on entry" and "granted via dialog" go here LaunchedEffect(permissionGranted) { if (permissionGranted && !initialized) { controller.onPermissionGranted() // = v(): 创建音频采集 + 解码核心 - // v() 只创建核心; 页面此刻已处于 resumed, 需再走一次 onResume 立即启动录音 - // (AudioRecord 是新建的, startRecording 不会重复; GLSurfaceView.onResume 幂等) + // v() only creates the core; the page is already resumed, so run onResume again to start recording immediately + // (AudioRecord is freshly created so startRecording won't double-start; GLSurfaceView.onResume is idempotent) controller.onResume() initialized = true } @@ -114,7 +114,7 @@ fun CwDecodeScreen(navigateUp: () -> Unit = {}) { !activity.shouldShowRequestPermissionRationale(Manifest.permission.RECORD_AUDIO) } - // 原版 tap_back_again_to_close: 单击返回只提示, 2 秒内再按才退出 + // Original tap_back_again_to_close: single Back shows a hint; press again within 2 s to exit BackHandler { if (!controller.handleBackPress()) navigateUp() } @@ -128,7 +128,7 @@ fun CwDecodeScreen(navigateUp: () -> Unit = {}) { .fillMaxSize() .systemBarsPadding() ) { - // 原 options_menu: 暂停/清除/保存(原版图标) + 录音/设置(文字按钮) + // Original options_menu: Pause/Clear/Save (original icons) + Record/Settings (text buttons) Row( modifier = Modifier .fillMaxWidth() @@ -154,7 +154,7 @@ fun CwDecodeScreen(navigateUp: () -> Unit = {}) { contentDescription = stringResource(R.string.save_text) ) } - // recordSignals 需要解码核心(f11037F), 未初始化时禁用 + // recordSignals needs the decode core (f11037F); disabled until initialized TextButton( onClick = { if (initialized) controller.recordSignals() }, enabled = initialized @@ -172,7 +172,7 @@ fun CwDecodeScreen(navigateUp: () -> Unit = {}) { modifier = Modifier.fillMaxSize() ) if (!permissionGranted) { - // 未授予麦克风权限: 遮罩 + 提示文字 + 请求按钮 + // Mic permission not granted: overlay + hint text + request button Box( modifier = Modifier .fillMaxSize() @@ -193,7 +193,7 @@ fun CwDecodeScreen(navigateUp: () -> Unit = {}) { Text(stringResource(R.string.cw_grant_permission)) } if (permanentlyDenied) { - // 勾选了"不再询问": 引导去系统设置开启 + // "Never ask again" checked: guide user to system settings TextButton(onClick = { activity.startActivity( Intent( diff --git a/feature/cw/src/main/java/com/rtbishop/look4sat/feature/cw/CwSettingsDialog.kt b/feature/cw/src/main/java/com/rtbishop/look4sat/feature/cw/CwSettingsDialog.kt index ce817298..ca3f30de 100644 --- a/feature/cw/src/main/java/com/rtbishop/look4sat/feature/cw/CwSettingsDialog.kt +++ b/feature/cw/src/main/java/com/rtbishop/look4sat/feature/cw/CwSettingsDialog.kt @@ -47,7 +47,7 @@ import androidx.compose.ui.unit.dp import com.ve3nea.morse_expert.MainActivity import kotlin.math.roundToInt -/** 键名/默认值照搬原 Morse Expert(root_preferences.xml + SettingsActivity 逻辑)。 */ +/** Keys/defaults ported from the original Morse Expert (root_preferences.xml + SettingsActivity logic). */ private const val KEY_MESSAGE_TYPE = "message_type" private const val KEY_TEXT_FONT_SIZE = "text_font_size" private const val DEFAULT_MESSAGE_TYPE = "general_text" @@ -55,7 +55,7 @@ private const val DEFAULT_TEXT_FONT_SIZE = 18 private const val MIN_FONT_SIZE = 7 private const val MAX_FONT_SIZE = 99 -/** 预置色板(9 色, 3x3 网格);原 ColorPreferenceCompat 用第三方 colorpicker, 不引入。 */ +/** Preset palette (9 colors, 3x3 grid); the original ColorPreferenceCompat used a third-party colorpicker, not imported. */ private val PaletteColors = listOf( 0xFFD0F0F0.toInt(), 0xFF000000.toInt(), 0xFFFFFFFF.toInt(), 0xFFAAAAAA.toInt(), 0xFFFF0000.toInt(), 0xFF00FF00.toInt(), @@ -63,11 +63,11 @@ private val PaletteColors = listOf( ) /** - * CW 设置弹窗(替代原 Morse Expert 的 SettingsActivity)。 - * 设置项与键名照搬原 APK: message_type / text_font_size / 9 个颜色 - * (I2.b.f663b 键名, I2.b.f664d 默认值, I2.b.c 英文标题)。 - * 存储: SharedPreferences(getPackageName() + "_preferences")。 - * 点击 OK 保存并调用 controller.onResume() 立即生效; Cancel/点外部仅关闭不保存。 + * CW settings dialog (replaces the original Morse Expert SettingsActivity). + * Items and keys ported from the original APK: message_type / text_font_size / 9 colors + * (I2.b.f663b keys, I2.b.f664d defaults, I2.b.c English titles). + * Storage: SharedPreferences(getPackageName() + "_preferences"). + * OK saves and calls controller.onResume() to take effect at once; Cancel/outside-tap only closes without saving. */ @Composable fun CwSettingsDialog(controller: MainActivity, onDismiss: () -> Unit) { @@ -142,7 +142,7 @@ fun CwSettingsDialog(controller: MainActivity, onDismiss: () -> Unit) { editor.putInt(I2.b.f663b[i], colorValues[i]) } editor.apply() - // 原 SettingsActivity 返回时由 MainActivity.onResume() 重新读取全部设置; 照搬逻辑已就位 + // The original re-read all settings in MainActivity.onResume() when SettingsActivity returned; ported logic in place controller.onResume() onDismiss() }) { Text("OK") } diff --git a/feature/cw/src/main/java/com/rtbishop/look4sat/feature/cw/suncompat/Cleaner.java b/feature/cw/src/main/java/com/rtbishop/look4sat/feature/cw/suncompat/Cleaner.java index d6e10aaf..6740e6f1 100644 --- a/feature/cw/src/main/java/com/rtbishop/look4sat/feature/cw/suncompat/Cleaner.java +++ b/feature/cw/src/main/java/com/rtbishop/look4sat/feature/cw/suncompat/Cleaner.java @@ -1,8 +1,8 @@ package com.rtbishop.look4sat.feature.cw.suncompat; /** - * 编译期 stub:Android 无 sun.misc.Cleaner(照搬 Morse Expert 1.15 k3 FFT 库的清理回调, - * 实际 FFT 1024 点不触发 native 路径)。 + * Compile-time stub: Android has no sun.misc.Cleaner (ported from Morse Expert 1.15's k3 FFT library cleanup callback, + * the real 1024-point FFT never hits the native path). */ public class Cleaner { public static Cleaner create(Object referent, Runnable thunk) { diff --git a/feature/cw/src/main/java/com/rtbishop/look4sat/feature/cw/suncompat/Unsafe.java b/feature/cw/src/main/java/com/rtbishop/look4sat/feature/cw/suncompat/Unsafe.java index fc90ae2f..8b2e7099 100644 --- a/feature/cw/src/main/java/com/rtbishop/look4sat/feature/cw/suncompat/Unsafe.java +++ b/feature/cw/src/main/java/com/rtbishop/look4sat/feature/cw/suncompat/Unsafe.java @@ -1,8 +1,8 @@ package com.rtbishop.look4sat.feature.cw.suncompat; /** - * 编译期 stub:Android 无 sun.misc.Unsafe(运行时通过反射获取,失败时 k3.r.f12109a 为 null)。 - * 照搬自 Morse Expert 1.15(k3 FFT 库的大数组 native 路径 >1GB 不触发,实际 FFT 走 float[] 分支)。 + * Compile-time stub: Android has no sun.misc.Unsafe (obtained via reflection at runtime; k3.r.f12109a is null on failure). + * Ported from Morse Expert 1.15 (the k3 FFT library's >1GB native path never triggers; real FFT uses the float[] branch). */ public class Unsafe { public long allocateMemory(long bytes) { diff --git a/feature/cw/src/main/java/com/ve3nea/morse_expert/DecodedTextView.java b/feature/cw/src/main/java/com/ve3nea/morse_expert/DecodedTextView.java index b6c16808..6ac78609 100644 --- a/feature/cw/src/main/java/com/ve3nea/morse_expert/DecodedTextView.java +++ b/feature/cw/src/main/java/com/ve3nea/morse_expert/DecodedTextView.java @@ -21,9 +21,9 @@ import java.util.Date; import java.util.concurrent.atomic.AtomicBoolean; /** - * 照搬自 Morse Expert 1.15 DecodedTextView, 类名保持混淆原名。 - * 适配: 原 extends l.C1702e0(appcompat 混淆类) -> 直接 extends TextView - * (C1702e0 仅提供 TextView 兼容功能, 本类未使用其特有方法)。 + * Ported from Morse Expert 1.15 DecodedTextView; obfuscated class name kept. + * Adaptation: originally extends l.C1702e0 (obfuscated appcompat class) -> now extends TextView directly + * (C1702e0 only provided TextView-compatible behavior; this class never used its specific methods). */ public class DecodedTextView extends TextView { diff --git a/feature/cw/src/main/java/com/ve3nea/morse_expert/MainActivity.java b/feature/cw/src/main/java/com/ve3nea/morse_expert/MainActivity.java index 547a740e..1b492bcf 100644 --- a/feature/cw/src/main/java/com/ve3nea/morse_expert/MainActivity.java +++ b/feature/cw/src/main/java/com/ve3nea/morse_expert/MainActivity.java @@ -52,8 +52,8 @@ public class MainActivity { } /** - * applyImmersive=false 用于嵌入小容器(如转发器 CW 面板): - * 不触碰宿主窗口的系统栏标志(避免影响整个 Activity)。 + * applyImmersive=false is used when embedding in a small container (e.g. the transponder CW panel): + * does not touch the host window's system-bar flags (avoids affecting the whole Activity). */ public void onCreate(android.app.Activity activity, ConstraintLayout constraintLayout, boolean applyImmersive) { this.mActivity = activity; diff --git a/feature/cw/src/main/java/d1/AbstractC1518b.java b/feature/cw/src/main/java/d1/AbstractC1518b.java index ca3196fa..805f3c28 100644 --- a/feature/cw/src/main/java/d1/AbstractC1518b.java +++ b/feature/cw/src/main/java/d1/AbstractC1518b.java @@ -1,7 +1,7 @@ package d1; /* renamed from: d1.b, reason: case insensitive filesystem */ -/* 照搬自 Morse Expert 1.15, 混淆名保持。b(Parcel)/c(y2.d) 为库工具已裁剪(L1/y2 库依赖)。 */ +/* Ported from Morse Expert 1.15; obfuscated names kept. b(Parcel)/c(y2.d) were library utilities, trimmed (L1/y2 library deps). */ public abstract /* synthetic */ class AbstractC1518b { public static float a(float f, float f4, k3.d dVar, long j4, float f5, float f6) { dVar.c(j4, f - f4); diff --git a/feature/cw/src/main/java/g3/c.java b/feature/cw/src/main/java/g3/c.java index b196010f..58d6997a 100644 --- a/feature/cw/src/main/java/g3/c.java +++ b/feature/cw/src/main/java/g3/c.java @@ -455,13 +455,13 @@ public abstract class c { if (z3) { i4 ^= 2; } - // smali 证实: f() = sin(象限 switch!): + // smali-verified: f() = sin (quadrant switch!): // case 0 -> g(d5,0)(sin(0-π/2)) - // case 1 -> c(d5,0)(sin(π/2-π) = cos(余角)) - // case 2 -> -g(d5,0)(sin(π-3π/2) = -sin(余角)) - // case 3 -> -c(d5,0)(sin(3π/2-2π) = -cos(余角)) - // jadx 畸形嵌套 if 把 case 0 还原成 return NaN(小角度全 NaN!), - // 且 case 1/2/3 全走 g() —— 旋转因子表 329 个 NaN + 频谱错乱的根源! + // case 1 -> c(d5,0) (sin(pi/2-pi) = cos of the complement) + // case 2 -> -g(d5,0) (sin(pi-3pi/2) = -sin of the complement) + // case 3 -> -c(d5,0) (sin(3pi/2-2pi) = -cos of the complement) + // jadx's malformed nested if reconstructed case 0 as return NaN (all small angles NaN!), + // and routed cases 1/2/3 all through g() - root cause of 329 NaNs in the twiddle table and the scrambled spectrum! if (i4 == 0) { return g(d5, d7); } @@ -474,8 +474,8 @@ public abstract class c { return -c(d5, d7); } d7 = d6; - // jadx 还原不完整(smali 650 行, jadx 只还原 63 行); f() 未被任何代码调用(死代码), - // 按 smali 尾部语义补 return(cond_0: v22 = v22 + v14; return v22) + // jadx's reconstruction is incomplete (650 smali lines vs 63 reconstructed); f() is dead code called by nothing, + // return restored per smali tail semantics (cond_0: v22 = v22 + v14; return v22) return d7; } diff --git a/feature/cw/src/main/java/i3/d.java b/feature/cw/src/main/java/i3/d.java index e709e0ac..8a4ec830 100644 --- a/feature/cw/src/main/java/i3/d.java +++ b/feature/cw/src/main/java/i3/d.java @@ -1213,8 +1213,8 @@ public final class d { } } } else { - // smali cond_22(a5==0, k=1 时运行时路径!): 单线程 FFT + 后处理 - // jadx 原把这段错塞进 if(i8!=1) 的 else(死代码), a5==0 的 else 是幻觉(throw null) + // smali cond_22 (a5==0, the k=1 runtime path!): single-thread FFT + post-processing + // jadx wrongly stuffed this into the else of if(i8!=1) (dead code); the a5==0 else is a phantom (throw null) int i26 = this.f11700a; if (i26 > 4) { j3.c.q(i26, fArr, i4, this.c, this.f11704g, this.f11703e); diff --git a/feature/cw/src/main/java/j1/C1646n.java b/feature/cw/src/main/java/j1/C1646n.java index aadaa907..e47da736 100644 --- a/feature/cw/src/main/java/j1/C1646n.java +++ b/feature/cw/src/main/java/j1/C1646n.java @@ -8,9 +8,9 @@ import com.ve3nea.morse_expert.ScaleView; import com.ve3nea.morse_expert.WaterfallSurfaceView; /** - * 照搬自 Morse Expert 1.15 j1.C1646n, 类名/字段名保持混淆原名。 - * R8 合并类手术: 只保留视图持有器(6 参构造 + 5 个 View 字段); - * 其余 158-1278 行(广告/计费合并方法)已整段裁剪(用户要求广告不迁移)。 + * Ported from Morse Expert 1.15 j1.C1646n; obfuscated class/field names kept. + * R8-merged class surgery: keeps only the view holder (6-arg constructor + 5 View fields); + * the remaining lines 158-1278 (ads/billing merged methods) were removed wholesale (user: no ad migration). */ public final class C1646n { diff --git a/feature/cw/src/main/java/j3/c.java b/feature/cw/src/main/java/j3/c.java index 3935c4fe..c3893343 100644 --- a/feature/cw/src/main/java/j3/c.java +++ b/feature/cw/src/main/java/j3/c.java @@ -9,12 +9,12 @@ import k3.d; import k3.s; /** - * 照搬自 Morse Expert 1.15 j3.c (R8 合并类), 类名保持混淆原名。 - * 仅保留 k3 FFT 库实际调用的静态 FFT 方法 (u/v/w/x/y/z/E/F/g/h/q/r 及其辅助 - * A/B/C/D/G/H/i/j/k/l/m/n/o/p/s/t/d/J), 其余 R8 合并的 billing/ads/TextView - * 兼容代码及 M.Z 接口实现已裁剪。依据: apktool smali (j3/c.smali) 静态调用关系。 - * d 方法中 K1.a.z 的 Guava lenientFormat 以 String.format 等价替换 - * (K1.a 已裁剪为仅保留内存统计字段)。 + * Ported from Morse Expert 1.15 j3.c (R8-merged class); obfuscated class name kept. + * Keeps only the static FFT methods the k3 FFT library actually calls (u/v/w/x/y/z/E/F/g/h/q/r and their helpers + * A/B/C/D/G/H/i/j/k/l/m/n/o/p/s/t/d/J); the rest of the R8-merged billing/ads/TextView + * compatibility code and M.Z interface implementations were trimmed. Based on apktool smali (j3/c.smali) static call analysis. + * In method d, K1.a.z's Guava lenientFormat is replaced with an equivalent String.format + * (K1.a was trimmed to memory-accounting fields only). */ public final class c { public static void A(int i4, int i5, int i6, float[] fArr, float[] fArr2) { diff --git a/feature/cw/src/main/java/k3/c.java b/feature/cw/src/main/java/k3/c.java index 90783e85..3794d29b 100644 --- a/feature/cw/src/main/java/k3/c.java +++ b/feature/cw/src/main/java/k3/c.java @@ -34,7 +34,7 @@ public abstract class c { } public static void b(Future[] futureArr) throws InterruptedException, java.util.concurrent.ExecutionException { - // smali: 无 catch 直接向上抛 -> Java 声明 throws(忠实表达) + // smali: no catch, thrown straight up -> Java declares throws (faithful) for (Future future : futureArr) { future.get(); } diff --git a/feature/cw/src/main/java/k3/q.java b/feature/cw/src/main/java/k3/q.java index 5f84ea2d..f5392658 100644 --- a/feature/cw/src/main/java/k3/q.java +++ b/feature/cw/src/main/java/k3/q.java @@ -1,10 +1,10 @@ package k3; /** - * R8 生成的伪 enum(k3 FFT 库的数组类型枚举)。 - * smali 中 q 的直接子类:l(LONG/5)、m(FLOAT/6)、n(DOUBLE/7), - * 其余常量(BYTE/UNSIGNED_BYTE/SHORT/INT/COMPLEX_FLOAT/COMPLEX_DOUBLE/STRING/OBJECT) - * 直接以 q 实例表达。 + * R8-generated pseudo-enum (the k3 FFT library's array-type enum). + * Direct q subclasses in smali: l (LONG/5), m (FLOAT/6), n (DOUBLE/7), + * the other constants (BYTE/UNSIGNED_BYTE/SHORT/INT/COMPLEX_FLOAT/COMPLEX_DOUBLE/STRING/OBJECT) + * are expressed directly as q instances. */ public class q { public static final l f; diff --git a/feature/cw/src/main/java/k3/r.java b/feature/cw/src/main/java/k3/r.java index fa9bbf88..92e81590 100644 --- a/feature/cw/src/main/java/k3/r.java +++ b/feature/cw/src/main/java/k3/r.java @@ -19,8 +19,8 @@ public abstract class r { declaredField.setAccessible(true); obj = declaredField.get(cls); } catch (ClassNotFoundException | IllegalAccessException | IllegalArgumentException | NoSuchFieldException | SecurityException ignored) { - // Android 适配: sun.misc.Unsafe 可能不可用(隐藏 API), 静默降级为 null; - // 大数组 native 路径(j4 > 1GB)不会在实际解码中触发 + // Android adaptation: sun.misc.Unsafe may be unavailable (hidden API); silently degrades to null; + // the large-array native path (j4 > 1GB) never triggers in real decoding } f12109a = (Unsafe) obj; } diff --git a/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/LogTab.kt b/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/LogTab.kt index 127a9cd3..70c7b270 100644 --- a/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/LogTab.kt +++ b/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/LogTab.kt @@ -1,11 +1,11 @@ /* - * LogTab.kt — 雷达页「Log」标签(4.5.2 WaveLog 日志)。 + * LogTab.kt - "Log" tab of the radar page (4.5.2 WaveLog logging). * - * 布局: 转发器列表(点开选一个, 边看频率边填) → 呼号输入回车存本地 → - * 下方本地记录列表(时间/频率/呼号)。记录支持自绘左滑删除: - * 滑动中显示黄色垃圾桶, 滑过 75% 变撤销键, 5 秒内不点撤销则删除。 + * Layout: transponder list (tap to select, watch frequency while typing) -> callsign input, Enter saves locally -> + * local records list below (time/freq/callsign). Records support custom swipe-to-delete: + * yellow trash icon shows while swiping; past 75% it becomes an undo key; not undone within 5 s means deleted. * - * 回车只存本地(WavelogQueue), 上传由 10 分钟周期/手动触发(防抄收错误)。 + * Enter only saves locally (WavelogQueue); upload is triggered by the 10-min cycle or manually (avoids mis-copied calls). */ package com.rtbishop.look4sat.feature.radar @@ -116,8 +116,8 @@ fun LogTab( ) } - // 转发器选择(下拉选择框): 选中后底下出现输入区 - // 选中项按 uuid 从 transceivers 实时派生(多普勒频率每秒更新, 频率显示随之刷新) + // Transponder picker (dropdown): input area appears below once selected + // Selected item derived live from transceivers by uuid (Doppler freq updates every second, display refreshes with it) var menuExpanded by remember { mutableStateOf(false) } val selectedRadio = transceivers.firstOrNull { it.uuid == selectedUuid } ExposedDropdownMenuBox( @@ -157,7 +157,7 @@ fun LogTab( } } - // 选中转发器后: 输入区(实时频率 + 呼号 + 模式) + // After selecting a transponder: input area (live freq + callsign + mode) selectedRadio?.let { radio -> ExpandedLogInput( radio = radio, @@ -171,7 +171,7 @@ fun LogTab( ) } - // 本地记录列表(时间/频率/呼号) + // Local records list (time / freq / callsign) Text( text = stringResource(id = R.string.wavelog_saved), fontSize = 12.sp, @@ -185,9 +185,9 @@ fun LogTab( modifier = Modifier.padding(vertical = 8.dp) ) } else { - // 按场次分组(sessionId = 卫星名-AOS 时间戳), 组间粗线分隔 + // Grouped by pass session (sessionId = satName-AOS timestamp), thick line between groups entries.groupBy { it.sessionId.ifBlank { "un" } }.forEach { (sessionId, groupEntries) -> - // 组标题 + 分隔线 + // Group title + divider HorizontalDivider(thickness = 2.dp, color = MaterialTheme.colorScheme.primary) Text( text = if (sessionId == "un") stringResource(id = R.string.wavelog_ungrouped) @@ -237,7 +237,7 @@ fun LogTab( } } -/** 转发器展开输入区: 实时频率 + 呼号(回车存本地) + 模式修正 */ +/** Expanded transponder input: live freq + callsign (Enter saves locally) + mode override */ @Composable private fun ExpandedLogInput( radio: SatRadio, @@ -259,7 +259,7 @@ private fun ExpandedLogInput( fun submit() { val call = callsign.trim().uppercase() if (call.length < 3) return - // 频率直接取转发器栏的数字(radio 每秒多普勒修正, 回车那一秒的值) + // Freq taken directly from the transponder bar (radio Doppler-corrected each second; value at the Enter moment) val tx = radio.uplinkLow ?: radio.downlinkLow ?: 0L val rx = radio.downlinkLow ?: radio.uplinkLow ?: 0L val qsoId = UUID.randomUUID().toString() @@ -278,7 +278,7 @@ private fun ExpandedLogInput( callsign = "" onSaved() showToast(savedMsg) - // QRZ 对方网格异步回填(4.5.5): 设置页填了 Cookie 才查, 失败静默 + // QRZ counterpart grid async backfill (4.5.5): only queried when Cookie is set; silent on failure scope.launch { val prefs = context.getSharedPreferences("qrz_cookie", android.content.Context.MODE_PRIVATE) val rawCookie = prefs.getString("cookie", "") ?: "" @@ -291,7 +291,7 @@ private fun ExpandedLogInput( } } - // 频率显示与转发器栏完全一致: TX 行 + RX 行(radio 来自每秒多普勒重算, 实时刷新) + // Freq display matches the transponder bar: TX row + RX row (radio from per-second Doppler recompute, live) val txForDisplay = radio.uplinkLow ?: radio.downlinkLow ?: 0L val rxForDisplay = radio.downlinkLow ?: radio.uplinkLow ?: 0L val upLow = radio.uplinkLow @@ -300,7 +300,7 @@ private fun ExpandedLogInput( val downHigh = radio.downlinkHigh val isLinearRange = upLow != null && upHigh != null && upLow != upHigh Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { - // TX 行 + // TX row Row(verticalAlignment = Alignment.CenterVertically) { Text( text = "TX: ", @@ -321,7 +321,7 @@ private fun ExpandedLogInput( ) } } - // RX 行(与转发器栏 RX 一致) + // RX row (same as transponder bar RX) Row(verticalAlignment = Alignment.CenterVertically) { Text( text = "RX: ", @@ -364,9 +364,9 @@ private fun ExpandedLogInput( } /** - * 自绘左滑删除行(用户定制交互): - * 左滑 → 右侧黄色垃圾桶; 滑过 75% → 变撤销键(待删除, 不立即删); - * 5 秒倒计时内点撤销恢复, 不点自动删除。 + * Custom swipe-to-delete row (user-requested interaction): + * swipe left -> yellow trash on the right; past 75% -> becomes undo key (pending delete, not immediate); + * tap undo within the 5 s countdown to restore; otherwise auto-deleted. */ @Composable internal fun SwipeDeleteRow( @@ -379,14 +379,14 @@ internal fun SwipeDeleteRow( var rowWidth by remember { mutableIntStateOf(0) } val threshold = rowWidth * 0.75f - // 平滑弹回/滑入(动画) + // Smooth spring back / slide in (animation) val animatedOffset by animateFloatAsState( targetValue = offsetX, animationSpec = if (pending) tween(200) else spring(dampingRatio = Spring.DampingRatioNoBouncy), label = "swipeOffset" ) - // 倒计时: 进入待删除后 5 秒自动删 + // Countdown: auto-delete 5 s after entering pending state LaunchedEffect(pending) { if (pending) { countdown = 5 @@ -407,7 +407,7 @@ internal fun SwipeDeleteRow( .fillMaxWidth() .onSizeChanged { rowWidth = it.width } ) { - // 背景层(右侧图标: 垃圾桶 / 撤销+倒计时) + // Background layer (right-side icons: trash / undo + countdown) Box( modifier = Modifier .matchParentSize() @@ -442,7 +442,7 @@ internal fun SwipeDeleteRow( ) } } else { - // 垃圾桶(滑动过程中一直显示, 黄色) + // Trash icon (always visible while swiping, yellow) Text( text = "🗑", fontSize = 20.sp, @@ -450,8 +450,8 @@ internal fun SwipeDeleteRow( ) } } - // 内容层(跟手滑动): 背景在 offset 内侧(随内容移动)! - // 平时盖住垃圾桶, 左滑时内容带背景移开 → 右侧露出黄色垃圾桶/撤销倒计时 + // Content layer (follows finger): background sits inside the offset (moves with content)! + // Normally covers the trash; swiping moves content+background away -> yellow trash / undo countdown revealed Box( modifier = Modifier .offset { IntOffset(animatedOffset.roundToInt(), 0) } @@ -486,7 +486,7 @@ private fun formatLocalTime(utcMs: Long): String { return "%02d:%02d".format(cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE)) } -/** 场次 ID: 卫星名-AOS 时间戳(UTC yyyyMMdd-HHmm); 无 AOS 时用回车时刻 */ +/** Pass session ID: satName-AOS timestamp (UTC yyyyMMdd-HHmm); falls back to Enter time without AOS */ private fun buildSessionId(satName: String, aosTimeMs: Long): String { val cal = Calendar.getInstance(TimeZone.getTimeZone("UTC")) cal.timeInMillis = if (aosTimeMs > 0) aosTimeMs else System.currentTimeMillis() diff --git a/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarScreen.kt b/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarScreen.kt index ae30c2c9..cca583cb 100644 --- a/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarScreen.kt +++ b/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarScreen.kt @@ -124,17 +124,17 @@ fun RadarDestination(navigateUp: () -> Unit) { viewModel.onAction(RadarAction.SstvPermissionResult(granted)) viewModel.onAction(RadarAction.CwPermissionResult(granted)) } - // WaveLog(4.5.2): 自动上传 — 每 10 分钟重试本地队列(开关开启时) + // WaveLog (4.5.2): auto-upload - retries the local queue every 10 min (when the switch is on) val wavelogUploader = remember { container.provideWavelogUploader() } LaunchedEffect(Unit) { while (true) { delay(10 * 60 * 1000L) val s = container.settingsRepo.otherSettings.value if (s.wavelogAutoUpload && s.wavelogUrl.isNotBlank()) { - // 网格不一致时静默跳过(留给手动上传确认), 其余失败自动下次重试 + // Grid mismatch is skipped silently (left for manual-upload confirmation); other failures retry on the next tick val outcome = wavelogUploader.uploadQueue() if (outcome is UploadOutcome.NeedConfirm) { - // 跳过本次; 用户可在设置页手动上传并确认 + // Skip this run; the user can upload and confirm manually in settings } } } diff --git a/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/WavelogLogScreen.kt b/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/WavelogLogScreen.kt index 4e3c001c..92ea90be 100644 --- a/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/WavelogLogScreen.kt +++ b/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/WavelogLogScreen.kt @@ -1,8 +1,8 @@ /* - * WavelogLogScreen.kt — 「日志」页面(更多菜单, 4.5.2 修复)。 + * WavelogLogScreen.kt - the Log page (More menu, 4.5.2 fix). * - * 表格形式展示本地存储的日志: 时间 | 频率 | 卫星 | 呼号 | 已上传(✓)。 - * 完整格子线(表头横线 + 行横线 + 列竖线 + 外边框); 行复用左滑删除。 + * Shows locally stored logs as a table: time | freq | satellite | callsign | uploaded (✓). + * Full grid lines (header rule + row rules + column rules + outer border); rows reuse swipe-to-delete. */ package com.rtbishop.look4sat.feature.radar @@ -59,13 +59,13 @@ fun WavelogLogScreen( .verticalScroll(rememberScrollState()) .padding(8.dp) ) { - // 表格外边框(左右 + 上下由表头/行底线构成) + // Table outer border (left/right; top/bottom formed by header/row bottom rules) Column( modifier = Modifier .fillMaxWidth() .background(MaterialTheme.colorScheme.surfaceContainer) ) { - // 表头 + // Header Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier @@ -82,9 +82,9 @@ fun WavelogLogScreen( GridVLine() HeaderCell(stringResource(id = R.string.wavelog_col_uploaded), 44.dp) } - // 表头横线 + // Header rule HorizontalDivider(thickness = 1.5.dp, color = GridLineColor) - // 表头与数据区留白 + // Space between header and data area Box(modifier = Modifier.height(8.dp)) if (entries.isEmpty()) { @@ -95,13 +95,13 @@ fun WavelogLogScreen( modifier = Modifier.padding(horizontal = 10.dp, vertical = 12.dp) ) } else { - // 按场次分组(sessionId = 卫星名-AOS 时间戳); 空 sessionId 归"未分组"放最后 + // Grouped by pass session (sessionId = satName-AOS timestamp); empty sessionId goes to "Uncategorized" last val groups = entries.groupBy { it.sessionId.ifBlank { UNGROUPED } } .toSortedMap(compareByDescending { id -> entries.filter { it.sessionId.ifBlank { UNGROUPED } == id }.minOfOrNull { it.timeUtcMs } ?: 0L }.thenByDescending { it }) groups.forEach { (sessionId, groupEntries) -> - // 组标题(场次名 + 本地时间) + // Group title (session name + local time) val (satPart, timePart) = parseSession(sessionId) Row(verticalAlignment = Alignment.CenterVertically) { Text( @@ -113,7 +113,7 @@ fun WavelogLogScreen( modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp) ) } - // 组间横线(md --- 效果) + // Rule between groups (md --- effect) HorizontalDivider(thickness = 1.5.dp, color = GridLineColor) groupEntries.forEach { entry -> SwipeDeleteRow( @@ -148,7 +148,7 @@ fun WavelogLogScreen( } } } - // 行分隔线 + // Row divider HorizontalDivider(thickness = 1.dp, color = GridLineColor) } } @@ -159,7 +159,7 @@ fun WavelogLogScreen( } } -/** 列竖线 */ +/** Column rule */ @Composable private fun GridVLine() { Box( @@ -196,7 +196,7 @@ private fun RowScope.Cell(text: String, width: Dp, weight: Float = 0f) { private const val UNGROUPED = "__ungrouped__" -/** 解析场次 ID: "ASRTU-1-20260804-2014" → (卫星名, "20260804-2014") */ +/** Parse session ID: "ASRTU-1-20260804-2014" -> (sat name, "20260804-2014") */ private fun parseSession(sessionId: String): Pair { val parts = sessionId.split('-') if (parts.size < 3) return sessionId to "" @@ -205,7 +205,7 @@ private fun parseSession(sessionId: String): Pair { return satPart to timePart } -/** 场次时间(UTC "20260804-2014") → 本地 "MM-dd HH:mm" */ +/** Session time (UTC "20260804-2014") -> local "MM-dd HH:mm" */ private fun formatSessionTime(timePart: String): String { if (timePart.length != 13 || timePart[8] != '-') return timePart return try { diff --git a/feature/roaming/src/main/java/com/rtbishop/look4sat/feature/roaming/RoamingScreen.kt b/feature/roaming/src/main/java/com/rtbishop/look4sat/feature/roaming/RoamingScreen.kt index a876519a..3bdd83f2 100644 --- a/feature/roaming/src/main/java/com/rtbishop/look4sat/feature/roaming/RoamingScreen.kt +++ b/feature/roaming/src/main/java/com/rtbishop/look4sat/feature/roaming/RoamingScreen.kt @@ -73,19 +73,19 @@ import java.util.Date import java.util.Locale /** - * 漫游页 —— 原样照搬自 QTH定位器 2.0 (com.us1pm.gridsquarelocator)。 - * UI: res/layout/main.xml (结构/尺寸/颜色逐项照搬, 不做任何更改) - * 逻辑: MainActivity.showLocation() / checkEnabled() / GPS 实时监听 - * 红线: UI 与逻辑均不更改, 与成品截图逐像素对齐。 + * Roaming page - ported verbatim from QTH Locator 2.0 (com.us1pm.gridsquarelocator). + * UI: res/layout/main.xml (structure/size/colors copied item by item, no changes) + * Logic: MainActivity.showLocation() / checkEnabled() / live GPS listening + * Hard rule: UI and logic unchanged, pixel-aligned with the original screenshots. */ @Composable fun RoamingScreen() { val context = LocalContext.current var state by remember { mutableStateOf(RoamingState()) } - // 照搬 onCreate: 日期固定文本 + 小时前缀(定位时拼接分钟) + // Ported from onCreate: fixed date text + hour prefix (minutes appended on locate) val dateText = remember { SimpleDateFormat("dd MMM yyyy", Locale.getDefault()).format(Date()) } var hourPrefix by remember { mutableStateOf("") } - // 照搬 onCreate: 初始 checkEnabled() -> 红点+设置按钮, 日期隐藏 + // Ported from onCreate: initial checkEnabled() -> red dot + settings button, date hidden var hasPermission by remember { mutableStateOf(false) } val permissionLauncher = rememberLauncherForActivityResult( ActivityResultContracts.RequestMultiplePermissions() @@ -102,25 +102,25 @@ fun RoamingScreen() { arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION) ) } else { - // 照搬 onResume: 绿点+进度条+提示+日期显示, 红点/按钮隐藏 + // Ported from onResume: green dot + progress + hint + date shown, red dot/button hidden state = state.copy(gpsOn = true, gpsOff = false, showSettings = false, showProgress = true, showNotice = true, showDate = true) } } - // 照搬 onResume/onPause: GPS + network 监听, 10 秒 / 10 米 + // Ported from onResume/onPause: GPS + network listeners, 10 s / 10 m val locationManager = remember { context.getSystemService(Context.LOCATION_SERVICE) as LocationManager } DisposableEffect(locationManager, hasPermission) { if (hasPermission) { @SuppressLint("MissingPermission") val listener = object : LocationListener { override fun onLocationChanged(location: Location) { - // 照搬 showLocation 开头: 只接受 gps / network provider + // Ported from showLocation start: accept only gps / network providers if (location.provider != "gps" && location.provider != "network") return state = roamingStateFromLocation(location.latitude, location.longitude, location.time, hourPrefix) .copy(date = dateText) } override fun onProviderDisabled(provider: String) { - // 照搬 checkEnabled: 红点+按钮, 日期隐藏 + // Ported from checkEnabled: red dot + button, date hidden state = state.copy(gpsOn = false, gpsOff = true, showSettings = true, showProgress = false, showNotice = false, showDate = false) } @@ -141,15 +141,15 @@ fun RoamingScreen() { onDispose { } } } - // 照搬 res/layout/main.xml: 浅色页面, 蓝色信息区, 43sp 定位码, 3x3 九宫格, 底部版权 - // 按用户要求: 上下往内缩进系统栏安全区, 避免内容被状态栏/导航栏遮挡 + // Ported from res/layout/main.xml: light page, blue info area, 43sp locator, 3x3 grid, bottom credit + // Per user request: inset from system bars top/bottom so content is not covered Column( modifier = Modifier .fillMaxSize() .background(MaterialTheme.colorScheme.background) .windowInsetsPadding(WindowInsets.systemBars) ) { - // relativeLayout7: 顶部 GPS 状态条, 25dp 高, holo_blue_bright + // relativeLayout7: top GPS status bar, 25dp high, holo_blue_bright Box( modifier = Modifier .fillMaxWidth() @@ -158,16 +158,16 @@ fun RoamingScreen() { .background(MaterialTheme.colorScheme.surfaceVariant) ) { Row(modifier = Modifier.fillMaxSize(), verticalAlignment = Alignment.CenterVertically) { - // tvTitleGPS: "GPS" 14sp 黑, 左 8dp + // tvTitleGPS: "GPS" 14sp black, left 8dp Text( text = "GPS", fontSize = 14.sp, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(start = 8.dp) ) - // imGps: toRightOf tvTitleGPS + marginLeft 15dp (成品 main.xml) + // imGps: toRightOf tvTitleGPS + marginLeft 15dp (original main.xml) Spacer(modifier = Modifier.width(15.dp)) - // imGps: 绿点 (15dp, 主题色 primary -> 夜间滤镜下可见) + // imGps: green dot (15dp, theme primary -> visible under night filter) if (state.gpsOn) { Box( modifier = Modifier @@ -175,7 +175,7 @@ fun RoamingScreen() { .background(MaterialTheme.colorScheme.primary, CircleShape) ) } - // imGpsOff: 红点叠层 (15dp, 主题色 error) + // imGpsOff: red dot overlay (15dp, theme error) if (state.gpsOff) { Box( modifier = Modifier @@ -184,12 +184,12 @@ fun RoamingScreen() { ) } Spacer(modifier = Modifier.weight(1f)) - // tvDate: 14sp 黑, 居中 + // tvDate: 14sp black, centered if (state.showDate) { Text(text = state.date.ifEmpty { dateText }, fontSize = 14.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) } Spacer(modifier = Modifier.weight(1f)) - // tvTime: 14sp 黑, 右对齐 + // tvTime: 14sp black, right-aligned Text( text = state.time, fontSize = 14.sp, @@ -197,8 +197,8 @@ fun RoamingScreen() { modifier = Modifier.padding(end = 8.dp) ) } - // btnLocationSettings: 黄色按钮, 居中 - // btnLocationSettings: 成品 #faf8f54a 半透明黄, 14sp 黑字, 居中 + // btnLocationSettings: yellow button, centered + // btnLocationSettings: original #faf8f54a translucent yellow, 14sp black text, centered if (state.showSettings) { Box( modifier = Modifier @@ -212,27 +212,27 @@ fun RoamingScreen() { } } } - // linearLayout4: 经纬度区, holo_blue_bright + // linearLayout4: lat/lon area, holo_blue_bright Column( modifier = Modifier .fillMaxWidth() .padding(horizontal = 2.dp) .background(MaterialTheme.colorScheme.surfaceVariant) ) { - // 纬度行: tvLati 16sp 黑 左 8dp | tvLat 右对齐 3dp + // Lat row: tvLati 16sp black left 8dp | tvLat right-aligned 3dp Row(modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp), verticalAlignment = Alignment.CenterVertically) { Text(text = state.latLabel, fontSize = 16.sp, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(start = 8.dp)) Spacer(modifier = Modifier.weight(1f)) Text(text = state.latValue, fontSize = 14.sp, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(end = 3.dp)) } - // 经度行 + // Lon row Row(modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp), verticalAlignment = Alignment.CenterVertically) { Text(text = state.lonLabel, fontSize = 16.sp, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(start = 8.dp)) Spacer(modifier = Modifier.weight(1f)) Text(text = state.lonValue, fontSize = 14.sp, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(end = 3.dp)) } } - // relativeLayout6: 定位码区, 43sp bold 黑居中 + // relativeLayout6: locator area, 43sp bold black centered Box( modifier = Modifier .fillMaxWidth() @@ -247,7 +247,7 @@ fun RoamingScreen() { textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth() ) - // progressBar: 加载中, 居中 + // progressBar: loading, centered if (state.showProgress) { CircularProgressIndicator( modifier = Modifier.size(24.dp).align(Alignment.Center), @@ -255,7 +255,7 @@ fun RoamingScreen() { strokeWidth = 2.dp ) } - // tvNotice: 提示文字, 居中顶部 + // tvNotice: hint text, centered top if (state.showNotice) { Text( text = "请稍等,加载可能需要几分钟的时间。", @@ -266,19 +266,19 @@ fun RoamingScreen() { ) } } - // relativeLayout: 九宫格, 占满剩余, 底部 15dp - // 用 Box 等价 RelativeLayout 布局: 左格贴左 / 中格水平居中 / 右格贴右 (成品 main.xml) + // relativeLayout: 3x3 grid, fills remaining space, bottom 15dp + // Box as RelativeLayout equivalent: left cell left / middle cell centered / right cell right (original main.xml) Column(modifier = Modifier.fillMaxWidth().weight(1f).padding(bottom = 15.dp)) { - // 行1: relativeLayout5 (上排: 纬度 +1) + // Row 1: relativeLayout5 (top row: lat +1) Box(modifier = Modifier.fillMaxWidth().weight(1f)) { GridCell(text = state.grids[0], modifier = Modifier.width(80.dp).fillMaxHeight().align(Alignment.CenterStart)) GridCell(text = state.grids[1], modifier = Modifier.width(200.dp).fillMaxHeight().align(Alignment.Center)) GridCell(text = state.grids[2], modifier = Modifier.width(80.dp).fillMaxHeight().align(Alignment.CenterEnd)) } - // 行2: relativeLayout4, 205dp 高 (中排: 中央) + // Row 2: relativeLayout4, 205dp high (middle row: center) Box(modifier = Modifier.fillMaxWidth().height(205.dp)) { GridCell(text = state.grids[3], modifier = Modifier.width(80.dp).fillMaxHeight().align(Alignment.CenterStart)) - // 中央列: tv22 (100x80dp centerInParent) 叠在 linearLayout3 (200x200dp) 之上 + // Center column: tv22 (100x80dp centerInParent) overlaid on linearLayout3 (200x200dp) Box(modifier = Modifier.width(200.dp).fillMaxHeight().align(Alignment.Center)) { // linearLayout3: 200x200dp, marginTop 2dp, holo_blue_bright Box( @@ -288,7 +288,7 @@ fun RoamingScreen() { .size(width = 200.dp, height = 200.dp) .background(MaterialTheme.colorScheme.surfaceVariant) ) { - // iv: 红点 10x10dp, top|left, leftMargin/topMargin 查表绝对定位 + // iv: red dot 10x10dp, top|left, leftMargin/topMargin from lookup tables Image( painter = painterResource(R.drawable.roam_pnt), contentDescription = null, @@ -297,7 +297,7 @@ fun RoamingScreen() { .offset(x = state.markerLeft.dp, y = state.markerTop.dp) ) } - // tv22: 100x80dp centerInParent, 30sp bold, textColorHighlight(白) + // tv22: 100x80dp centerInParent, 30sp bold, textColorHighlight (white) Text( text = state.grids[4], fontSize = 30.sp, @@ -311,14 +311,14 @@ fun RoamingScreen() { } GridCell(text = state.grids[5], modifier = Modifier.width(80.dp).fillMaxHeight().align(Alignment.CenterEnd)) } - // 行3: relativeLayout3 (下排: 纬度 -1) + // Row 3: relativeLayout3 (bottom row: lat -1) Box(modifier = Modifier.fillMaxWidth().weight(1f)) { GridCell(text = state.grids[6], modifier = Modifier.width(80.dp).fillMaxHeight().align(Alignment.CenterStart)) GridCell(text = state.grids[7], modifier = Modifier.width(200.dp).fillMaxHeight().align(Alignment.Center)) GridCell(text = state.grids[8], modifier = Modifier.width(80.dp).fillMaxHeight().align(Alignment.CenterEnd)) } } - // tv4: 底部版权, 10sp 黑 (往上拉, 底部预留空间避免被导航栏遮挡) + // tv4: bottom credit, 10sp black (raised; bottom space kept clear of nav bar) Text( text = "制作:US1PM 汉化:BA7LCE", fontSize = 10.sp, @@ -329,7 +329,7 @@ fun RoamingScreen() { } } -// 周边格: 60/200/60dp, #0BACF1, bold 居中, 2dp margin (照搬 tv11/tv13/tv21/tv23/tv31/tv33) +// Surrounding cells: 60/200/60dp, #0BACF1, bold centered, 2dp margin (ported tv11/tv13/tv21/tv23/tv31/tv33) @Composable private fun GridCell(text: String, modifier: Modifier) { Box( @@ -349,8 +349,8 @@ private fun GridCell(text: String, modifier: Modifier) { } /** - * 照搬 MainActivity.showLocation() 的状态。 - * 网格/九宫格/红点查表与成品逐行一致。 + * Ported state from MainActivity.showLocation(). + * Grid / 3x3 / red-dot lookup tables match the original line by line. */ data class RoamingState( val date: String = "", @@ -361,9 +361,9 @@ data class RoamingState( val lonValue: String = "", val loc: String = "", val grids: List = List(9) { "" }, - /** 红点 leftMargin(dp), 经度第 3 对字符查表 */ + /** Red dot leftMargin (dp), looked up by the 3rd char pair of longitude */ val markerLeft: Int = 0, - /** 红点 topMargin(dp), 纬度第 3 对字符查表(屏幕 Y 反转) */ + /** Red dot topMargin (dp), looked up by the 3rd char pair of latitude (screen Y inverted) */ val markerTop: Int = 0, val gpsOn: Boolean = false, val gpsOff: Boolean = true, @@ -374,16 +374,16 @@ data class RoamingState( ) /** - * 照搬 showLocation() 的完整计算: 时间/经纬度 DMS/8 位网格/九宫格/红点查表。 + * Ported full showLocation() computation: time / lat-lon DMS / 8-char grid / 3x3 / red-dot lookup. */ fun roamingStateFromLocation(lat: Double, lon: Double, fixTime: Long, hourPrefix: String): RoamingState { - // 照搬: tvTime = 缓存小时前缀 + 定位时间分钟 + // Ported: tvTime = cached hour prefix + locate time minutes val minute = SimpleDateFormat("mm", Locale.getDefault()).format(Date(fixTime)) val time = hourPrefix + minute - // 方向字符 + // Direction chars val ew = if (lon >= 0.0) "E" else "W" val ns = if (lat >= 0.0) "N" else "S" - // 照搬: 纬度 DMS (String.valueOf 无补零) + // Ported: latitude DMS (String.valueOf, no zero padding) val latAbs = if (lat < 0.0) -lat else lat val latDeg = latAbs.toInt() val latMinFull = (latAbs - latDeg) * 60.0 @@ -391,7 +391,7 @@ fun roamingStateFromLocation(lat: Double, lon: Double, fixTime: Long, hourPrefix val latSec = ((latMinFull - latMin) * 60.0).toInt() val latLabel = "纬度 $latDeg° $latMin' $latSec\" $ns" val latValue = "$lat° " - // 照搬: 经度 DMS + // Ported: longitude DMS val lonAbs = if (lon < 0.0) -lon else lon val lonDeg = lonAbs.toInt() val lonMinFull = (lonAbs - lonDeg) * 60.0 @@ -399,13 +399,13 @@ fun roamingStateFromLocation(lat: Double, lon: Double, fixTime: Long, hourPrefix val lonSec = ((lonMinFull - lonMin) * 60.0).toInt() val lonLabel = "经度 $lonDeg° $lonMin' $lonSec\" $ew" val lonValue = "$lon° " - // 照搬: 8 位网格编码 (区间查表) + // Ported: 8-char grid encoding (range lookup) val (str, str2, str3, str4) = encodeLon(lon) val (str5, str6, str38, str9) = encodeLat(lat) val loc = str + str5 + str2 + str6 + str3 + str38 + str4 + str9 - // 照搬: 九宫格 parseInt3 五分支 + // Ported: 3x3 parseInt3 five-branch logic val grids = buildGrids(str, str5, str2, str6, str3, str38) - // 照搬: 红点查表 (第 3 对字符) + // Ported: red-dot lookup (3rd char pair) val markerLeft = lonMargin[str3.firstOrNull()] ?: 0 val markerTop = latMargin[str38.firstOrNull()] ?: 0 return RoamingState( @@ -427,21 +427,21 @@ fun roamingStateFromLocation(lat: Double, lon: Double, fixTime: Long, hourPrefix ) } -// 红点 leftMargin 查表 (MainActivity 1286-1386 行): 经度第 3 对 a..x -> -2..190 dp +// Red-dot leftMargin lookup (MainActivity lines 1286-1386): lon 3rd pair a..x -> -2..190 dp private val lonMargin = mapOf( 'a' to -2, 'b' to 8, 'c' to 16, 'd' to 24, 'e' to 32, 'f' to 40, 'g' to 48, 'h' to 56, 'i' to 65, 'j' to 74, 'k' to 83, 'l' to 92, 'm' to 101, 'n' to 110, 'o' to 119, 'p' to 128, 'q' to 137, 'r' to 146, 's' to 155, 't' to 163, 'u' to 171, 'v' to 179, 'w' to 184, 'x' to 190 ) -// 红点 topMargin 查表 (MainActivity 1315-1363 行): 纬度第 3 对 a..x -> 190..-2 dp (屏幕 Y 反转) +// Red-dot topMargin lookup (MainActivity lines 1315-1363): lat 3rd pair a..x -> 190..-2 dp (screen Y inverted) private val latMargin = mapOf( 'a' to 190, 'b' to 182, 'c' to 174, 'd' to 169, 'e' to 163, 'f' to 155, 'g' to 146, 'h' to 137, 'i' to 128, 'j' to 119, 'k' to 110, 'l' to 101, 'm' to 92, 'n' to 83, 'o' to 74, 'p' to 65, 'q' to 56, 'r' to 48, 's' to 40, 't' to 32, 'u' to 24, 'v' to 16, 'w' to 8, 'x' to -2 ) -// 照搬 311-611 行: 经度 -> (20°区字母, 2°数字, 2'字母, 30"数字) +// Ported lines 311-611: longitude -> (20 deg zone letter, 2 deg digit, 2' letter, 30" digit) private fun encodeLon(inputLon: Double): List { var longitude = inputLon var d = 0.0 @@ -522,7 +522,7 @@ private fun encodeLon(inputLon: Double): List { return listOf(str, str2, str3, str4) } -// 照搬 612-914 行: 纬度 -> (10°区字母, 1°数字, 1'字母, 15"数字) +// Ported lines 612-914: latitude -> (10 deg zone letter, 1 deg digit, 1' letter, 15" digit) private fun encodeLat(inputLat: Double): List { var latitude = inputLat var d2 = 0.0 @@ -605,8 +605,8 @@ private fun encodeLat(inputLat: Double): List { } /** - * 照搬 917-1283 行: 九宫格 parseInt3 五分支(内部/西界/东界/北界/南界 + 四角)。 - * 返回 9 格文本: [上左, 上中, 上右, 左中, 中央, 右中, 下左, 下中, 下右]。 + * Ported lines 917-1283: 3x3 parseInt3 five-branch (interior / west / east / north / south edges + corners). + * Returns 9 cell texts: [top-left, top-center, top-right, mid-left, center, mid-right, bottom-left, bottom-center, bottom-right]. */ private fun buildGrids(str: String, str5: String, str2: String, str6: String, str3: String, str38: String): List { val parseInt = str2.toInt() @@ -618,7 +618,7 @@ private fun buildGrids(str: String, str5: String, str2: String, str6: String, st val g = MutableList(9) { "" } fun fmt2(n: Int) = if (n <= 9) "0$n" else n.toString() if ((parseInt > 0) && (parseInt < 9) && (parseInt2 > 0) && (parseInt2 < 9)) { - // 内部: 数字对 ±1 + // Interior: digit pairs +-1 val i7 = parseInt3 + 1 val i8 = i7 - 10 val valueOf9 = fmt2(i8) @@ -642,7 +642,7 @@ private fun buildGrids(str: String, str5: String, str2: String, str6: String, st g[7] = str39 + str14 g[8] = str39 + str15 } else if ((parseInt == 0) && (parseInt2 > 0) && (parseInt2 < 9)) { - // 西边界: 经度 0 -> 9, 经度字母 -1 + // West edge: lon digit 0 -> 9, lon letter -1 val str43 = (str[0] - 1).toString() + str5 val str44 = "9" + (parseInt2 + 1) val i12 = parseInt3 + 1 @@ -664,7 +664,7 @@ private fun buildGrids(str: String, str5: String, str2: String, str6: String, st g[7] = str39 + valueOf8 g[8] = str39 + str15 } else if ((parseInt == 9) && (parseInt2 > 0) && (parseInt2 < 9)) { - // 东边界: 经度 9 -> 0, 经度字母 +1 + // East edge: lon digit 9 -> 0, lon letter +1 val str47 = (str[0] + 1).toString() + str5 val i14 = parseInt3 + 1 val valueOf21 = (i14 - 10).toString() @@ -686,7 +686,7 @@ private fun buildGrids(str: String, str5: String, str2: String, str6: String, st g[7] = str39 + valueOf25 g[8] = str47 + str15 } else if ((parseInt < 9) && (parseInt > 0) && (parseInt2 == 9)) { - // 北边界: 纬度 9 -> 0, 纬度字母 +1 + // North edge: lat digit 9 -> 0, lat letter +1 val str50 = str + (str5[0] + 1).toString() val i16 = parseInt3 - 9 val i17 = i16 - 10 @@ -711,7 +711,7 @@ private fun buildGrids(str: String, str5: String, str2: String, str6: String, st g[7] = str39 + str14 g[8] = str39 + str15 } else if ((parseInt < 9) && (parseInt > 0) && (parseInt2 == 0)) { - // 南边界: 纬度 0 -> 9, 纬度字母 -1 + // South edge: lat digit 0 -> 9, lat letter -1 val str52 = str + (str5[0] - 1).toString() val i21 = parseInt3 + 1 val i22 = i21 - 10 @@ -735,7 +735,7 @@ private fun buildGrids(str: String, str5: String, str2: String, str6: String, st g[7] = str52 + str14 g[8] = str52 + str15 } else if (parseInt3 == 0) { - // 四角 00: 经纬度数字均 -1 进位 + // Corner 00: both digits -1 with carry val v32 = (str[0] - 1).toString() val v33 = (str5[0] - 1).toString() g[0] = v32 + str5 + "91" @@ -748,7 +748,7 @@ private fun buildGrids(str: String, str5: String, str2: String, str6: String, st g[7] = str + v33 + "09" g[8] = str + v33 + "19" } else if (parseInt3 == 9) { - // 四角 09: 经度 -1, 纬度 +1 + // Corner 09: lon -1, lat +1 val v34 = (str[0] - 1).toString() val v35 = (str5[0] + 1).toString() g[0] = v34 + v35 + "90" @@ -761,7 +761,7 @@ private fun buildGrids(str: String, str5: String, str2: String, str6: String, st g[7] = v34 + str5 + "08" g[8] = v34 + str5 + "18" } else if (parseInt3 == 99) { - // 四角 99: 经纬度数字均 +1 进位 + // Corner 99: both digits +1 with carry val v36 = (str[0] + 1).toString() val v37 = (str5[0] + 1).toString() g[0] = str + v37 + "80" @@ -774,7 +774,7 @@ private fun buildGrids(str: String, str5: String, str2: String, str6: String, st g[7] = str39 + "98" g[8] = v36 + str5 + "08" } else if (parseInt3 == 90) { - // 四角 90: 经度 +1, 纬度 -1 + // Corner 90: lon +1, lat -1 val v38 = (str[0] + 1).toString() val v39 = (str5[0] - 1).toString() g[0] = str39 + "81" diff --git a/feature/roaming/src/test/java/com/rtbishop/look4sat/feature/roaming/RoamingStateTest.kt b/feature/roaming/src/test/java/com/rtbishop/look4sat/feature/roaming/RoamingStateTest.kt index 1297fda8..99444302 100644 --- a/feature/roaming/src/test/java/com/rtbishop/look4sat/feature/roaming/RoamingStateTest.kt +++ b/feature/roaming/src/test/java/com/rtbishop/look4sat/feature/roaming/RoamingStateTest.kt @@ -22,7 +22,7 @@ import org.junit.Test class RoamingStateTest { - // 用户实测坐标 (QTH定位器 2.0 成品截图): 22.314066 / 108.706575 + // User-verified coordinates (QTH Locator 2.0 screenshot): 22.314066 / 108.706575 private fun stateOf(lat: Double, lon: Double) = roamingStateFromLocation(lat, lon, System.currentTimeMillis(), "21:") @@ -35,7 +35,7 @@ class RoamingStateTest { @Test fun `red marker from ih pair matches reference lookup`() { val state = stateOf(22.314066, 108.706575) - // 成品查表: 经度第3对 'i' -> leftMargin 65dp, 纬度第3对 'h' -> topMargin 137dp + // Original lookup: lon 3rd pair 'i' -> leftMargin 65dp, lat 3rd pair 'h' -> topMargin 137dp assertEquals(65, state.markerLeft) assertEquals(137, state.markerTop) } @@ -70,43 +70,43 @@ class RoamingStateTest { @Test fun `west edge borrows lon letter`() { - // 经度 0.5 纬度 22.314066: 数字对 02, 西边界 parseInt==0 + // lon 0.5 lat 22.314066: digit pair 02, west edge parseInt==0 val state = stateOf(22.314066, 0.5) assertEquals(8, state.loc.length) - // 左列经度 0->9, 经度字母 J-1=I: IL92 + // Left column lon 0->9, lon letter J-1=I: IL92 assertEquals("IL92", state.grids[3]) } @Test fun `north edge carries lat letter`() { - // 纬度 29.5 经度 108.706575: 数字对 49? -> 纬度数字 9 -> 北边界 + // lat 29.5 lon 108.706575: digit pair 49? -> lat digit 9 -> north edge val state = stateOf(29.5, 108.706575) assertEquals(8, state.loc.length) - // 上排纬度 +1 -> 0, 纬度字母 +1 (M) + // Top row lat +1 -> 0, lat letter +1 (M) assertEquals("OM40", state.grids[1]) } @Test fun `east edge carries lon letter`() { - // 经度 119.5 纬度 22.314066: 经度数字 9 -> 东边界 + // lon 119.5 lat 22.314066: lon digit 9 -> east edge val state = stateOf(22.314066, 119.5) assertEquals(8, state.loc.length) - // 右上 经度 9->0, 经度字母 O+1=P, 纬度 2+1=3: PL03 + // Top-right lon 9->0, lon letter O+1=P, lat 2+1=3: PL03 assertEquals("PL03", state.grids[2]) } @Test fun `south edge borrows lat letter`() { - // 纬度 0.5 经度 108.706575: 纬度数字 0 -> 南边界 + // lat 0.5 lon 108.706575: lat digit 0 -> south edge val state = stateOf(0.5, 108.706575) assertEquals(8, state.loc.length) - // 下中 纬度 0->9, 纬度字母 J-1=I: OI49 + // Bottom-center lat 0->9, lat letter J-1=I: OI49 assertEquals("OI49", state.grids[7]) } @Test fun `boundary values wrap like reference`() { - // (90, 180) -> RR99xx99 (成品区间查表行为) + // (90, 180) -> RR99xx99 (original range-lookup behavior) val state = stateOf(90.0, 180.0) assertEquals("RR99xx99", state.loc) } diff --git a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/AprsCard.kt b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/AprsCard.kt index e5e3e3cf..2dd6cf35 100644 --- a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/AprsCard.kt +++ b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/AprsCard.kt @@ -38,7 +38,7 @@ import com.rtbishop.look4sat.core.data.aprs.AprsStore import com.rtbishop.look4sat.core.presentation.CardButton import com.rtbishop.look4sat.core.presentation.R -/** APRS 卡片:卫星数据容器与数据输出容器之间;设置键弹窗收纳,不挤 */ +/** APRS card: sits between the satellite-data and data-output containers; settings live in a gear dialog to save space */ @Composable fun AprsCard() { val context = LocalContext.current @@ -63,7 +63,7 @@ fun AprsCard() { modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), verticalArrangement = Arrangement.spacedBy(4.dp) ) { - // 标题行:标题 + 设置键(弹窗,省空间) + // Title row: title + gear key (dialog, space-saving) Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween @@ -80,7 +80,7 @@ fun AprsCard() { ) } } - // 启用开关 + // Enable switch AprsSwitchRow(R.string.prefs_aprs_enable, config.enabled) { enabled -> config = config.copy(enabled = enabled) AprsStore.saveConfig(context, config) @@ -90,7 +90,7 @@ fun AprsCard() { .setAction(if (enabled) AprsStore.ACTION_START else AprsStore.ACTION_STOP) if (enabled) context.startForegroundService(intent) else context.startService(intent) } - // 简洁信息行(不挤) + // Compact info row (not crowded) Text( text = stringResource( id = R.string.prefs_aprs_summary, @@ -100,7 +100,7 @@ fun AprsCard() { fontSize = 13.sp, color = MaterialTheme.colorScheme.onSurfaceVariant ) - // 上次上报状态行 + // Last-report status row val lr = lastReport if (lr.time > 0L) { val timeStr = java.text.SimpleDateFormat("HH:mm:ss", java.util.Locale.getDefault()) @@ -117,7 +117,7 @@ fun AprsCard() { else MaterialTheme.colorScheme.error ) } - // 手动上报按钮 + // Manual report button CardButton( onClick = { val intent = Intent() @@ -131,7 +131,7 @@ fun AprsCard() { ) } } - // 上报后延迟刷新状态行(等结果落盘) + // Refresh the status row after reporting (wait for the result to persist) androidx.compose.runtime.LaunchedEffect(reportPing) { if (reportPing > 0) { kotlinx.coroutines.delay(6000) @@ -145,7 +145,7 @@ fun AprsCard() { onSave = { newConfig -> config = newConfig AprsStore.saveConfig(context, newConfig) - // 已启用时应用新配置(重启服务) + // Apply new config when enabled (restart the service) if (newConfig.enabled) { context.startService( Intent().setClassName(context.packageName, AprsStore.SERVICE_CLASS) @@ -162,7 +162,7 @@ fun AprsCard() { } } -/** APRS 设置弹窗(空间充裕,配置项齐全) */ +/** APRS settings dialog (plenty of room, full config) */ @Composable private fun AprsSettingsDialog( config: AprsConfig, @@ -207,7 +207,7 @@ private fun AprsSettingsDialog( singleLine = true, textStyle = textStyle ) - // 计算验证码按钮(用户要求:让用户算出结果) + // Passcode compute button (user request: let the user see the computed result) TextButton( onClick = { val call = callsign.trim().uppercase() @@ -302,7 +302,7 @@ private fun AprsSettingsDialog( ) } -/** 本文件内开关行(与 SettingsScreen 的私有 SwitchRow 一致) */ +/** Switch row local to this file (same as SettingsScreen's private SwitchRow) */ @Composable private fun AprsSwitchRow(labelResId: Int, checked: Boolean, onCheckedChange: ((Boolean) -> Unit)?) { Row( diff --git a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsScreen.kt b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsScreen.kt index 0dd9c79d..cc78e34b 100644 --- a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsScreen.kt +++ b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsScreen.kt @@ -121,7 +121,7 @@ fun SettingsDestination() { val uiState by viewModel.uiState.collectAsStateWithLifecycle() SettingsScreen(uiState, viewModel::onAction, container.provideLotwSatellitesRepo()) - // WaveLog 网格不一致确认弹窗(4.5.2) + // WaveLog grid mismatch confirmation dialog (4.5.2) val gridConfirm = viewModel.gridConfirm if (gridConfirm != null) { AlertDialog( @@ -149,7 +149,7 @@ fun SettingsDestination() { ) } - // WaveLog 错误详情弹窗(可一键复制) + // WaveLog error detail dialog (one-tap copy) val wavelogError = viewModel.wavelogError if (wavelogError != null) { AlertDialog( @@ -628,11 +628,11 @@ private fun OtherCard(settings: OtherSettings, onAction: (SettingsAction) -> Uni } /** - * UI 设置卡片: 控制底部菜单栏各页面显示/隐藏。 - * 顺序固定(卫星/过境/雷达/匹配/漫游/地图/设置), 关闭后其余自动靠拢; - * "设置"页固定显示(防止失去设置入口)。 + * UI settings card: controls which pages appear in the bottom menu bar. + * Order is fixed (Satellites/Passes/Radar/Mutual/Roaming/Map/Settings); turning one off makes the rest close up; + * the Settings page is always shown (so the settings entry can't be lost). */ -/** WaveLog 日志服务器设置卡片(4.5.2): 照用户截图布局(深色卡片/标签左输入右/按钮行) */ +/** WaveLog log server settings card (4.5.2): matches the user's screenshot (dark card / label left, input right / button row) */ @Composable private fun WavelogCard( settings: OtherSettings, @@ -813,7 +813,7 @@ private fun UiSettingsCard( R.string.nav_map to "Map", R.string.nav_prefs to "Settings" ) // name 必须与 Screen.screenId 一致 (R8 安全) - // 主菜单项: 排除更多菜单项, Settings 固定最后 + // Main menu items: exclude sub-menu items; Settings pinned last val mainItems = remember(screens, screenOrder, subMenuOrder) { val sub = subMenuOrder.ifEmpty { defaultSubMenuOrder } screens.map { it.second } @@ -827,7 +827,7 @@ private fun UiSettingsCard( } .sortedWith(compareBy { if (it == "Settings") 1 else 0 }) } - // 更多菜单项 + // More-menu items val subItems = remember(screens, subMenuOrder) { val sub = subMenuOrder.ifEmpty { defaultSubMenuOrder } sub.filter { s -> screens.any { (_, name) -> name == s } } @@ -843,7 +843,7 @@ private fun UiSettingsCard( ) screens.forEach { (labelRes, name) -> if (name == "Settings") { - // 设置页固定显示, 开关禁用 + // Settings always shown; its switch is disabled SwitchRow(labelRes, true, null) } else { SwitchRow(labelRes, name !in hiddenScreens) { @@ -855,7 +855,7 @@ private fun UiSettingsCard( text = stringResource(id = R.string.prefs_ui_order_title), color = MaterialTheme.colorScheme.primary ) - // 主菜单区(底部栏, 最多 5 项, 设置锁定最后) + // Main menu area (bottom bar, max 5 items, Settings locked last) Text( text = stringResource(id = R.string.prefs_ui_main_title), fontSize = 12.sp, @@ -875,7 +875,7 @@ private fun UiSettingsCard( }, onReorder = { main -> onUpdateMenu(main, subMenuOrder) } ) - // 更多菜单区(进「更多」的页面) + // More-menu area (pages behind "More") Text( text = stringResource(id = R.string.prefs_ui_sub_title), fontSize = 12.sp, @@ -885,7 +885,7 @@ private fun UiSettingsCard( items = subItems, labels = screens, moveLabel = stringResource(id = R.string.prefs_ui_move_back), - // 按钮常显; 主菜单满 5 时自动让位(最后一个非"设置"项移入更多, 交换) + // Buttons always visible; when the main menu hits 5, the last non-Settings item moves to More (swap) moveEnabled = { true }, onMove = { name -> val mainWithoutSettings = mainItems.filter { it != "Settings" } @@ -909,7 +909,7 @@ private fun UiSettingsCard( } } -/** 页面顺序拖拽列表: 每行右侧抓手, 按住上下拖动实时换位, 松手持久化 */ +/** Page-order drag list: handle on each row's right; drag up/down to reorder live; persists on release */ @Composable private fun DragOrderList( items: List, @@ -927,7 +927,7 @@ private fun DragOrderList( var draggingIndex by remember { mutableIntStateOf(-1) } var dragOffset by remember { mutableStateOf(0f) } - // 固定高度: 嵌套在 LazyVerticalGrid 内必须给有界高度, 否则无限高度约束崩溃 + // Fixed height: nested inside LazyVerticalGrid it needs a bounded height or infinite constraints crash LazyColumn( state = listState, modifier = Modifier @@ -956,7 +956,7 @@ private fun DragOrderList( } } if (isLocked) { - // 锁定行(设置页): 无抓手, 固定最后 + // Locked row (Settings): no handle, pinned last Text( text = stringResource(id = R.string.prefs_ui_order_locked), fontSize = 11.sp, @@ -995,7 +995,7 @@ private fun DragOrderList( ) } ) { - // 小圆点手柄(触控区 padding 20dp 居中 -> 8dp 圆点) + // Dot handle (touch area padding 20dp centered -> 8dp dot) Box( modifier = Modifier .fillMaxSize() @@ -1065,7 +1065,7 @@ private fun CardCredits(modifier: Modifier = Modifier) { Text( text = stringResource(id = R.string.prefs_outro_thanks) ) - // 感谢列表与保修声明之间保留约两行间距 (用户要求) + // Keep roughly two lines of spacing between the thanks list and the license block (user request) Spacer(modifier = Modifier.height(24.dp)) Text( text = stringResource(id = R.string.prefs_outro_license), diff --git a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsState.kt b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsState.kt index adc57d2c..1f09d805 100644 --- a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsState.kt +++ b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsState.kt @@ -64,25 +64,25 @@ sealed interface SettingsAction { data class ToggleSensor(val value: Boolean) : SettingsAction data class ToggleLightTheme(val value: Boolean) : SettingsAction data class ToggleNightMode(val value: Boolean) : SettingsAction - // UI 设置: 切换底部导航栏页面的隐藏状态(Screen simpleName) + // UI settings: toggle a bottom-nav page's hidden state (Screen simpleName) data class ToggleScreen(val screenName: String) : SettingsAction - // UI 设置: 更新页面顺序 + // UI settings: update page order data class ReorderScreens(val order: List) : SettingsAction - // UI 设置: 重置为默认顺序 + // UI settings: reset to default order data object ResetScreenOrder : SettingsAction - // UI 设置: 更新主菜单 + 更多菜单顺序(4.5.1 折叠菜单) + // UI settings: update main + More menu order (4.5.1 foldable menu) data class UpdateMenuOrder(val mainOrder: List, val subOrder: List) : SettingsAction - // UI 设置: 重置主菜单 + 更多菜单顺序 + // UI settings: reset main + More menu order data object ResetMenuOrder : SettingsAction - // WaveLog 日志(4.5.2): 更新服务器配置 + // WaveLog logging (4.5.2): update server config data class UpdateWavelogSettings( val url: String, val apiKey: String, val stationId: String, val autoUpload: Boolean ) : SettingsAction - // WaveLog: 测试连接 + // WaveLog: test connection data object TestWavelogConnection : SettingsAction - // WaveLog: 手动上传队列 + // WaveLog: upload queue manually data object UploadWavelogQueue : SettingsAction - // WaveLog: 更新 LoTW 卫星列表 + // WaveLog: update LoTW satellite list data object UpdateLotwSatellites : SettingsAction // Remote control diff --git a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsViewModel.kt b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsViewModel.kt index 2c519c2c..363c5584 100644 --- a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsViewModel.kt +++ b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsViewModel.kt @@ -146,7 +146,7 @@ class SettingsViewModel( is SettingsAction.UpdateRC -> settingsRepo.updateRCSettings(action.settings) is SettingsAction.UpdateRadioControl -> settingsRepo.updateRadioControlSettings(action.settings) is SettingsAction.UpdateDataSources -> settingsRepo.updateDataSourcesSettings(action.settings) - // WaveLog 日志(4.5.2) + // WaveLog logging (4.5.2) is SettingsAction.UpdateWavelogSettings -> settingsRepo.updateOtherSettings { it.copy( wavelogUrl = action.url, @@ -244,13 +244,13 @@ class SettingsViewModel( // region WaveLog(4.5.2) - // 由 SettingsScreen 注入的共享 uploader(容器级单例) + // Shared uploader injected by SettingsScreen (container-level singleton) var pendingUploader: WavelogUploader? = null - // 网格不一致确认弹窗状态(UI 观察后弹 AlertDialog) + // Grid-mismatch confirmation state (UI observes it and shows an AlertDialog) var gridConfirm by mutableStateOf(null) - // WaveLog 错误详情弹窗(UI 观察后弹 AlertDialog, 可一键复制) + // WaveLog error-detail dialog (UI observes it; one-tap copy) var wavelogError by mutableStateOf(null) data class GridConfirmData(val stationGrid: String, val userGrid: String) @@ -296,7 +296,7 @@ class SettingsViewModel( } } - /** 网格确认结果: 忽略并上传 / 取消 */ + /** Grid-confirm result: ignore and upload / cancel */ fun resolveGridConfirm(ignoreAndUpload: Boolean) { val confirm = gridConfirm ?: return gridConfirm = null diff --git a/feature/status/src/main/java/com/rtbishop/look4sat/feature/status/SatStatusScreen.kt b/feature/status/src/main/java/com/rtbishop/look4sat/feature/status/SatStatusScreen.kt index d45af0d2..e0b6dc07 100644 --- a/feature/status/src/main/java/com/rtbishop/look4sat/feature/status/SatStatusScreen.kt +++ b/feature/status/src/main/java/com/rtbishop/look4sat/feature/status/SatStatusScreen.kt @@ -55,7 +55,7 @@ import com.rtbishop.look4sat.core.domain.repository.IMainContainer import com.rtbishop.look4sat.core.presentation.R import java.util.Calendar -// ========== 官网状态色(amsat.org/status 原色) ========== +// ========== Official status colors (amsat.org/status originals) ========== val ActiveBlue = Color(0xFF648FFF) val TlmOrange = Color(0xFFFFB000) val NotHeardPink = Color(0xFFDC267F) @@ -73,7 +73,7 @@ fun SatStatusScreen(container: IMainContainer) { var selectedDay by remember { mutableStateOf?>(null) } Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp)) { - // 顶部: 图例 + 刷新 + // Top: legend + refresh StatusHeader( fetchedAtUtcMs = uiState.fetchedAtUtcMs, isRefreshing = uiState.isRefreshing, @@ -96,10 +96,10 @@ fun SatStatusScreen(container: IMainContainer) { } } else -> { - // 表头 + // Header HeaderRow(statuses = uiState.statuses) HorizontalDivider(thickness = 1.dp) - // 行 + // Row LazyColumn(modifier = Modifier.fillMaxSize()) { items(uiState.statuses, key = { it.name }) { status -> StatusRow( @@ -122,7 +122,7 @@ fun SatStatusScreen(container: IMainContainer) { } } -/** 顶部: 更新时间 + 刷新按钮(加载中旋转动画) */ +/** Top: update time + refresh button (spinner while loading) */ @Composable private fun StatusHeader( fetchedAtUtcMs: Long, @@ -168,7 +168,7 @@ private fun StatusHeader( } } -/** 图例(官网四色) */ +/** Legend (official four colors) */ @Composable private fun LegendRow() { val legend = listOf( @@ -196,7 +196,7 @@ private fun LegendRow() { } } -/** 表头: 卫星名 + 6 天日期 */ +/** Header: satellite name + 6 day dates */ @Composable private fun HeaderRow(statuses: List) { val dates = statuses.firstOrNull()?.days?.map { it.dateLabel } ?: emptyList() @@ -224,7 +224,7 @@ private fun HeaderRow(statuses: List) { } } -/** 卫星行: 名称 + 6 天色块(官网色 + 报告数) */ +/** Satellite row: name + 6 day color blocks (official colors + report counts) */ @Composable private fun StatusRow(status: SatStatus, onClickDay: (SatDay) -> Unit) { Row( @@ -251,7 +251,7 @@ private fun StatusRow(status: SatStatus, onClickDay: (SatDay) -> Unit) { } } -/** 单天色块: 取当天 12 槽中最新的有报告状态; 无报告则灰 */ +/** Day block: newest reported status among the day's 12 slots; gray when none */ @Composable private fun DayCell(slot: SatSlot, modifier: Modifier, onClick: () -> Unit) { val color = Color(slot.statusColor) @@ -274,7 +274,7 @@ private fun DayCell(slot: SatSlot, modifier: Modifier, onClick: () -> Unit) { } } -/** 报告详情弹窗(3 级: 呼号/日期/时间/网格) */ +/** Report detail dialog (3 levels: callsign/date/time/grid) */ @Composable private fun ReportDialog( statusName: String,
    countGrid SquareXXXGrid SquareXXX([^<]+)