feat(wavelog): QRZ grid lookup, LoTW satellite list refresh, RST 59
User-prioritized WaveLog fixes (4.5.5, commit-only per instruction): - QRZ 对方网格爬虫: QrzGridClient (domain, pure JVM) fetches https://www.qrz.com/db/{call} with user-supplied cookies (parses EditThisCookie JSON or raw "k=v; k=v"), extracts Grid Square from the Detail table. Cookies NEVER built in - entered in settings. - Settings: WaveLog card top-right gear opens QRZ cookie dialog with test query button (detects logged-in callsign from cookie, looks up its grid, shows result or failure in the dialog). - LogTab: on Enter, async lookup of the other station's grid -> queue.updateGridsquare -> uploaded with QSO (postQso gridsquare). - LoTW satellite list: 112 names embedded (lotw.arrl.org config.tq6), normalizeSatName maps Celestrak TLE names to LoTW names (SAUDISAT-1C -> SO-50, FUNCUBE-1 -> AO-73, DIWATA-2B -> PO-101, ZARYA/ARISS -> ARISS); "Update sats" button in WaveLog card downloads the live list (LotwSatellitesRepo, SharedPreferences persisted, never in build). - RST: rst_sent/rst_rcvd = 59/59 in both v1 ADIF and v2 JSON. - Grid mismatch dialog kept (cloud station grid vs station QTH); QSO gridsquare no longer uses station grid. - New strings in 5 locales; check_strings 9 files OK. Verified: core:domain/data + feature:settings/radar + app compileDebugKotlin BUILD SUCCESSFUL.
This commit is contained in:
1 parent
2c26272e0b
commit
0e767f4250
18 files changed
+375
-20
No files matched your search
@@ -70,6 +70,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import okhttp3.OkHttpClient
|
||||
import com.rtbishop.look4sat.core.data.wavelog.LotwSatellitesRepo
|
||||
|
||||
class MainContainer(private val context: Context) : IMainContainer {
|
||||
|
||||
@@ -112,6 +113,12 @@ class MainContainer(private val context: Context) : IMainContainer {
|
||||
}
|
||||
override fun provideWavelogUploader(): WavelogUploader = WavelogUploader(settingsRepo, wavelogQueue)
|
||||
|
||||
private val lotwRepo: LotwSatellitesRepo by lazy {
|
||||
LotwSatellitesRepo(context).also { it.restore() }
|
||||
}
|
||||
|
||||
override fun provideLotwSatellitesRepo(): com.rtbishop.look4sat.core.domain.wavelog.ILotwSatellitesRepo = lotwRepo
|
||||
|
||||
override fun provideBluetoothReporter(): IReporter {
|
||||
val manager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
|
||||
val rc = settingsRepo.rcSettings.value
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/* LotwSatellitesRepo.kt — LoTW 卫星列表运行时刷新(4.5.5)。
|
||||
* 下载 ARRL 官方 config.tq6(gzip XML), 解析 <satellite name="...">,
|
||||
* 更新 LotwSatellites 动态表 + SharedPreferences 持久化(重启不丢)。
|
||||
* 触发: 设置页 WaveLog 区「更新卫星列表」按钮(用户拍板: 手动触发)。
|
||||
*/
|
||||
package com.rtbishop.look4sat.core.data.wavelog
|
||||
|
||||
import android.content.Context
|
||||
import com.rtbishop.look4sat.core.domain.wavelog.ILotwSatellitesRepo
|
||||
import com.rtbishop.look4sat.core.domain.wavelog.LotwSatellites
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStreamReader
|
||||
import java.net.URL
|
||||
import java.util.zip.GZIPInputStream
|
||||
import kotlin.random.Random
|
||||
|
||||
class LotwSatellitesRepo(private val context: Context) : ILotwSatellitesRepo {
|
||||
|
||||
private val prefs = context.getSharedPreferences("lotw_satellites", Context.MODE_PRIVATE)
|
||||
|
||||
/** 从 SharedPreferences 恢复上次下载的表(进程启动时调用) */
|
||||
override fun restore() {
|
||||
val saved = prefs.getString(KEY_NAMES, null)
|
||||
if (!saved.isNullOrBlank()) {
|
||||
LotwSatellites.updateNames(saved.split(",").filter { it.isNotBlank() }.toSet())
|
||||
}
|
||||
}
|
||||
|
||||
/** 下载 config.tq6 → 解析 → 更新内存 + 持久化。返回结果供 UI 提示 */
|
||||
override suspend fun refresh(): ILotwSatellitesRepo.RefreshResult = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val url = URL("https://lotw.arrl.org/lotw/config.tq6")
|
||||
val conn = url.openConnection()
|
||||
conn.connectTimeout = 10_000
|
||||
conn.readTimeout = 20_000
|
||||
conn.setRequestProperty("User-Agent", "Look4Sat-Pro/4.5.5")
|
||||
val raw = conn.getInputStream()
|
||||
val reader = BufferedReader(InputStreamReader(GZIPInputStream(raw), Charsets.UTF_8))
|
||||
val text = reader.use { it.readText() }
|
||||
// 解析 <satellite name="XXX" ...> — 格式固定(ARRL 官方 XML)
|
||||
val names = Regex("""<satellite name="([^"]+)""").findAll(text)
|
||||
.map { it.groupValues[1].uppercase() }
|
||||
.toSet()
|
||||
if (names.isEmpty()) return@withContext ILotwSatellitesRepo.RefreshResult.Error("列表为空(响应异常)")
|
||||
LotwSatellites.updateNames(names)
|
||||
prefs.edit()
|
||||
.putString(KEY_NAMES, names.joinToString(","))
|
||||
.putLong(KEY_UPDATED, System.currentTimeMillis())
|
||||
.apply()
|
||||
ILotwSatellitesRepo.RefreshResult.Ok(names.size)
|
||||
} catch (e: Exception) {
|
||||
ILotwSatellitesRepo.RefreshResult.Error(e.message ?: "未知错误")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val KEY_NAMES = "names"
|
||||
private const val KEY_UPDATED = "updated"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/* QrzGridClient.kt — QRZ 呼号网格爬虫(4.5.5, domain 层纯 JVM)。
|
||||
* 原理(已实测): 带用户 QRZ 登录 Cookie GET https://www.qrz.com/db/{呼号},
|
||||
* 页面 Detail 表格里有 <td class="dh">Grid Square</td><td class="di">XXX</td>。
|
||||
* 匿名无 Cookie 拿不到 Detail(已确认); 对方没填网格时表格无此行(返回 null)。
|
||||
* Cookie 由用户在设置页粘贴(EditThisCookie 导出的 JSON 数组 或 原始 Cookie 串),
|
||||
* 绝不内置/进构建。
|
||||
*/
|
||||
package com.rtbishop.look4sat.core.domain.qrz
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.net.URL
|
||||
|
||||
object QrzGridClient {
|
||||
|
||||
/** 解析用户粘贴的 Cookie(兼容两种格式): EditThisCookie JSON 数组 或 原始 "k=v; k=v" 串 */
|
||||
fun parseCookies(raw: String): String {
|
||||
val text = raw.trim()
|
||||
if (text.isEmpty()) return ""
|
||||
// JSON 数组格式: [{"name":"qz_userid","value":"1266043",...}, ...]
|
||||
if (text.startsWith("[")) {
|
||||
return try {
|
||||
val arr = org.json.JSONArray(text)
|
||||
val parts = mutableListOf<String>()
|
||||
for (i in 0 until arr.length()) {
|
||||
val o = arr.getJSONObject(i)
|
||||
val name = o.optString("name")
|
||||
val value = o.optString("value")
|
||||
if (name.isNotBlank()) parts.add("$name=$value")
|
||||
}
|
||||
parts.joinToString("; ")
|
||||
} catch (_: Exception) { text }
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
/** 检测 cookies 对应的登录呼号(请求 db 首页, 提取账号菜单呼号)。失败返回 null */
|
||||
suspend fun fetchOwnCallsign(cookieHeader: String): String? = withContext(Dispatchers.IO) {
|
||||
if (cookieHeader.isBlank()) return@withContext null
|
||||
try {
|
||||
val url = URL("https://www.qrz.com/db/")
|
||||
val conn = url.openConnection()
|
||||
conn.connectTimeout = 10_000
|
||||
conn.readTimeout = 20_000
|
||||
conn.setRequestProperty("User-Agent", "Mozilla/5.0 (Linux; Android 13) Look4Sat-Pro")
|
||||
conn.setRequestProperty("Cookie", cookieHeader)
|
||||
val html = conn.getInputStream().bufferedReader().use { it.readText() }
|
||||
// 登录账号菜单(实测结构): <li class="leaf last" onclick="return true">BG7NTA <ul class="sub">
|
||||
val pattern = Regex("<li class=\"leaf last\"[^>]*>\\s*([A-Z0-9/]+)\\s*<ul")
|
||||
pattern.find(html)?.groupValues?.get(1)?.trim()
|
||||
} catch (_: Exception) { null }
|
||||
}
|
||||
|
||||
/** 查询呼号的网格。返回 null = 未填/查不到(静默, 不阻塞录入) */
|
||||
suspend fun lookupGrid(callsign: String, cookieHeader: String): String? = withContext(Dispatchers.IO) {
|
||||
if (callsign.isBlank() || cookieHeader.isBlank()) return@withContext null
|
||||
try {
|
||||
val url = URL("https://www.qrz.com/db/${callsign.trim().uppercase()}")
|
||||
val conn = url.openConnection()
|
||||
conn.connectTimeout = 10_000
|
||||
conn.readTimeout = 20_000
|
||||
conn.setRequestProperty("User-Agent", "Mozilla/5.0 (Linux; Android 13) Look4Sat-Pro")
|
||||
conn.setRequestProperty("Cookie", cookieHeader)
|
||||
val html = conn.getInputStream().bufferedReader().use { it.readText() }
|
||||
// Detail 表格 Grid Square 行(已实测格式)
|
||||
val m = Regex("""<td class="dh">Grid Square</td><td class="di">([^<]+)</td>""")
|
||||
.find(html)
|
||||
m?.groupValues?.get(1)?.trim()?.takeIf { it.isNotBlank() }
|
||||
} catch (_: Exception) { null }
|
||||
}
|
||||
}
|
||||
+2
@@ -46,6 +46,8 @@ interface IMainContainer {
|
||||
// WaveLog 日志(4.5.2)
|
||||
val wavelogQueue: com.rtbishop.look4sat.core.domain.wavelog.WavelogQueue
|
||||
fun provideWavelogUploader(): com.rtbishop.look4sat.core.domain.wavelog.WavelogUploader
|
||||
|
||||
fun provideLotwSatellitesRepo(): com.rtbishop.look4sat.core.domain.wavelog.ILotwSatellitesRepo
|
||||
}
|
||||
|
||||
data class MutualPassData(
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
/* ILotwSatellitesRepo.kt — LoTW 卫星列表刷新接口(domain 层, 4.5.5)。
|
||||
* 实现: core/data 的 LotwSatellitesRepo(下载 ARRL config.tq6)。
|
||||
*/
|
||||
package com.rtbishop.look4sat.core.domain.wavelog
|
||||
|
||||
interface ILotwSatellitesRepo {
|
||||
sealed class RefreshResult {
|
||||
data class Ok(val count: Int) : RefreshResult()
|
||||
data class Error(val message: String) : RefreshResult()
|
||||
}
|
||||
|
||||
fun restore()
|
||||
|
||||
suspend fun refresh(): RefreshResult
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/* LotwSatellites.kt — LoTW 支持的卫星名列表(112 个, 静态内置)。
|
||||
* 来源: https://lotw.arrl.org/lotw/config.tq6 (gzip XML, ARRL 官方!)
|
||||
* WaveLog 用同一来源更新 satellite 表 lotw 字段(Update_model.php lotw_sats())。
|
||||
* 运行时可由 LotwSatellitesRepo 手动刷新(设置页 WaveLog 区按钮触发)。
|
||||
*/
|
||||
package com.rtbishop.look4sat.core.domain.wavelog
|
||||
|
||||
object LotwSatellites {
|
||||
private val staticNames: Set<String> = setOf("AISAT1", "AO-10", "AO-109", "AO-123", "AO-13", "AO-16", "AO-21", "AO-27", "AO-3", "AO-4", "AO-40", "AO-51", "AO-6", "AO-7", "AO-73", "AO-8", "AO-85", "AO-91", "AO-92", "ARISS", "Arsene", "BO-102", "BY70-1", "CAS-2T", "CAS-3H", "CAS-4A", "CAS-4B", "DO-64", "EO-79", "EO-88", "FO-118", "FO-12", "FO-20", "FO-29", "FO-99", "FS-3", "HO-107", "HO-113", "HO-119", "HO-68", "INSPR7", "IO-117", "IO-86", "JO-97", "KEDR", "LEDSAT", "LO-19", "LO-78", "LO-87", "LO-90", "MAYA-3", "MAYA-4", "MIREX", "MO-112", "MO-122", "NO-103", "NO-104", "NO-44", "NO-83", "NO-84", "PO-101", "QO-100", "RS-1", "RS-10", "RS-11", "RS-12", "RS-13", "RS-15", "RS-2", "RS-44", "RS-5", "RS-6", "RS-7", "RS-8", "SAREX", "SO-121", "SO-124", "SO-125", "SO-35", "SO-41", "SO-50", "SO-67", "SONATE", "TAURUS", "TEVEL1", "TEVEL2", "TEVEL3", "TEVEL4", "TEVEL5", "TEVEL6", "TEVEL7", "TEVEL8", "TO-108", "UKUBE1", "UO-14", "UVSQ", "VO-52", "XW-2A", "XW-2B", "XW-2C", "XW-2D", "XW-2E", "XW-2F", "TEV2-1", "TEV2-2", "TEV2-3", "TEV2-4", "TEV2-5", "TEV2-6", "TEV2-7", "TEV2-8", "TEV2-9")
|
||||
|
||||
@Volatile
|
||||
private var dynamicNames: Set<String> = emptySet()
|
||||
|
||||
/** 当前生效的卫星名: 静态内置 ∪ 运行时更新 */
|
||||
val names: Set<String>
|
||||
get() = if (dynamicNames.isEmpty()) staticNames else staticNames + dynamicNames
|
||||
|
||||
/** 运行时更新(设置页按钮 → LotwSatellitesRepo.refresh() 后调用) */
|
||||
fun updateNames(newNames: Set<String>) {
|
||||
if (newNames.isNotEmpty()) dynamicNames = newNames
|
||||
}
|
||||
}
|
||||
@@ -111,12 +111,21 @@ object WaveLogApi {
|
||||
val main = raw.substringBefore('(').trim()
|
||||
.ifBlank { raw.trim() }
|
||||
.uppercase(Locale.ENGLISH)
|
||||
// ISS 特判: 主名是 ISS/ZARYA/ARISS 变体 → ISS(LoTW 认可名)
|
||||
return when {
|
||||
main == "ZARYA" || main.startsWith("ISS") -> "ISS"
|
||||
main == "ARISS" -> "ISS"
|
||||
else -> main
|
||||
}
|
||||
// 匹配逻辑(仿 WaveLog 卫星表 name/displayname 匹配 + LoTW 列表):
|
||||
// 1. 已在 LoTW 列表(常见 TLE 名 == 通用名) → 原样返回
|
||||
// 2. 不在 → 查 Celestrak 别名映射(SAUDISAT 1C → SO-50 等) → 映射后必须在 LoTW 列表
|
||||
// 3. 仍不匹配 → 原样返回(不阻塞上传, QSO 仍保存)
|
||||
val commonName = mapOf(
|
||||
"ZARYA" to "ARISS",
|
||||
"ARISS" to "ARISS",
|
||||
"FUNCUBE-1" to "AO-73",
|
||||
"DIWATA-2B" to "PO-101",
|
||||
"SAUDISAT-1C" to "SO-50",
|
||||
"SAUDISAT 1C" to "SO-50",
|
||||
"DIWATA-2A" to "PO-101"
|
||||
)
|
||||
val candidate = commonName[main] ?: main
|
||||
return if (candidate in LotwSatellites.names) candidate else main
|
||||
}
|
||||
|
||||
/** 创建 QSO: 优先 v2, 404 降级 v1(ADIF) */
|
||||
@@ -143,6 +152,8 @@ object WaveLogApi {
|
||||
put("freq", qso.freqTxHz)
|
||||
put("freq_rx", qso.freqRxHz)
|
||||
put("gridsquare", gridsquare)
|
||||
put("rst_sent", "59")
|
||||
put("rst_rcvd", "59")
|
||||
put("sat_name", satName)
|
||||
}
|
||||
val (code, resp) = httpRequest("$base/index.php/api/v2/qso", "POST", apiKey, v2Body.toString())
|
||||
@@ -182,6 +193,8 @@ object WaveLogApi {
|
||||
}
|
||||
append(field("qso_date", utcDateCompact(qso.timeUtcMs)))
|
||||
append(field("time_on", utcTimeCompact(qso.timeUtcMs)))
|
||||
append(field("rst_sent", "59"))
|
||||
append(field("rst_rcvd", "59"))
|
||||
if (gridsquare.isNotBlank()) append(field("gridsquare", gridsquare.take(4)))
|
||||
if (satName.isNotBlank()) {
|
||||
append(field("sat_name", satName))
|
||||
|
||||
@@ -26,6 +26,7 @@ data class WavelogQso(
|
||||
val freqRxHz: Long, // 下行
|
||||
val satName: String,
|
||||
val sessionId: String = "", // 场次 ID: 卫星名-AOS 时间戳(过境仰角 0 秒), 空=未分组(旧数据)
|
||||
val gridsquare: String = "", // 对方网格(QRZ 爬虫填入, 4.5.5), 空=未查到
|
||||
val uploaded: Boolean = false // 是否已成功上传(4.5.2 修复: 成功后保留标记, 表格打勾)
|
||||
)
|
||||
|
||||
@@ -78,6 +79,11 @@ class WavelogQueue(private val store: IWavelogQueueStore) {
|
||||
save(all().map { if (it.id == id) it.copy(uploaded = true) else it })
|
||||
}
|
||||
|
||||
/** 更新某条 QSO 的对方网格(QRZ 爬虫异步回填, 4.5.5) */
|
||||
fun updateGridsquare(id: String, grid: String) {
|
||||
save(all().map { if (it.id == id) it.copy(gridsquare = grid) else it })
|
||||
}
|
||||
|
||||
/** 移除所有已上传条目(可选项, 保持队列精简) */
|
||||
@Synchronized
|
||||
fun removeUploaded() {
|
||||
|
||||
+4
-3
@@ -45,7 +45,8 @@ class WavelogUploader(
|
||||
return UploadOutcome.Done(0, queue.all().size, "无法获取站点信息(检查站点 ID/密钥权限)")
|
||||
}
|
||||
|
||||
// 2. 网格检测: 用户当前 QTH 前 4 位 vs 台站网格前 4 位(v1 降级时网格相同, 跳过)
|
||||
// 2. 网格检测: 云端站点网格前 4 位 vs 当前站位网格前 4 位
|
||||
// (防站点配置错误; 与 QSO 对方网格无关 — 用户澄清)
|
||||
if (!force) {
|
||||
val userGrid = userQthGrid()
|
||||
if (userGrid != null && stationGrid.take(4).lowercase() != userGrid.take(4).lowercase()) {
|
||||
@@ -53,14 +54,14 @@ class WavelogUploader(
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 逐条上传(成功后标记 uploaded, 保留在本地供日志页打勾)
|
||||
// 3. 逐条上传。ADIF 网格字段 = 对方网格(通联对象), 爬虫接入前留空
|
||||
val entries = queue.all()
|
||||
var ok = 0
|
||||
var fail = 0
|
||||
var firstError = ""
|
||||
for (qso in entries) {
|
||||
if (qso.uploaded) { ok++; continue }
|
||||
val result = WaveLogApi.postQso(url, apiKey, stationId, qso, stationGrid)
|
||||
val result = WaveLogApi.postQso(url, apiKey, stationId, qso, qso.gridsquare)
|
||||
if (result is WavelogResult.Success) {
|
||||
ok++
|
||||
queue.markUploaded(qso.id)
|
||||
|
||||
@@ -227,6 +227,7 @@
|
||||
<string name="prefs_wavelog_station_hint">ID stasiun</string>
|
||||
<string name="prefs_wavelog_auto">Unggah otomatis</string>
|
||||
<string name="prefs_wavelog_test">Uji koneksi</string>
|
||||
<string name="prefs_wavelog_update_sats">Perbarui satelit</string>
|
||||
<string name="prefs_wavelog_upload">Unggah sekarang</string>
|
||||
<string name="wavelog_title">Log</string>
|
||||
<string name="wavelog_select_transponder">Pilih transponder</string>
|
||||
@@ -238,6 +239,9 @@
|
||||
<string name="wavelog_grid_mismatch">Grid QTH (%1$s) tidak cocok dengan grid stasiun (%2$s). Tetap unggah?</string>
|
||||
<string name="wavelog_ignore">Abaikan dan unggah</string>
|
||||
<string name="wavelog_cancel">Batal</string>
|
||||
<string name="prefs_wavelog_qrz_title">QRZ Cookie</string>
|
||||
<string name="prefs_wavelog_qrz_hint">Tempel cookie login QRZ Anda di sini (ekspor EditThisCookie atau string "k=v; k=v"). Digunakan untuk mengambil grid square stasiun lawan saat unggah.</string>
|
||||
<string name="prefs_wavelog_qrz_label">Cookie</string>
|
||||
<string name="wavelog_copy">Salin</string>
|
||||
<string name="wavelog_delete_undo">Urungkan</string>
|
||||
<string name="wavelog_col_time">Waktu</string>
|
||||
|
||||
@@ -226,6 +226,7 @@
|
||||
<string name="prefs_wavelog_station_hint">ID stasiun</string>
|
||||
<string name="prefs_wavelog_auto">Unggah otomatis</string>
|
||||
<string name="prefs_wavelog_test">Uji koneksi</string>
|
||||
<string name="prefs_wavelog_update_sats">Perbarui satelit</string>
|
||||
<string name="prefs_wavelog_upload">Unggah sekarang</string>
|
||||
<string name="wavelog_title">Log</string>
|
||||
<string name="wavelog_select_transponder">Pilih transponder</string>
|
||||
@@ -237,6 +238,9 @@
|
||||
<string name="wavelog_grid_mismatch">Grid QTH (%1$s) tidak cocok dengan grid stasiun (%2$s). Tetap unggah?</string>
|
||||
<string name="wavelog_ignore">Abaikan dan unggah</string>
|
||||
<string name="wavelog_cancel">Batal</string>
|
||||
<string name="prefs_wavelog_qrz_title">QRZ Cookie</string>
|
||||
<string name="prefs_wavelog_qrz_hint">Tempel cookie login QRZ Anda di sini (ekspor EditThisCookie atau string "k=v; k=v"). Digunakan untuk mengambil grid square stasiun lawan saat unggah.</string>
|
||||
<string name="prefs_wavelog_qrz_label">Cookie</string>
|
||||
<string name="wavelog_copy">Salin</string>
|
||||
<string name="wavelog_delete_undo">Urungkan</string>
|
||||
<string name="wavelog_col_time">Waktu</string>
|
||||
|
||||
@@ -236,6 +236,7 @@
|
||||
<string name="prefs_wavelog_station_hint">İstasyon kimliği</string>
|
||||
<string name="prefs_wavelog_auto">Otomatik yükleme</string>
|
||||
<string name="prefs_wavelog_test">Bağlantıyı test et</string>
|
||||
<string name="prefs_wavelog_update_sats">Uydu güncelle</string>
|
||||
<string name="prefs_wavelog_upload">Şimdi yükle</string>
|
||||
<string name="wavelog_title">Günlük</string>
|
||||
<string name="wavelog_select_transponder">Transponder seç</string>
|
||||
@@ -247,6 +248,9 @@
|
||||
<string name="wavelog_grid_mismatch">QTH ızgarası (%1$s) istasyon ızgarasıyla (%2$s) eşleşmiyor. Yine de yüklensin mi?</string>
|
||||
<string name="wavelog_ignore">Yoksay ve yükle</string>
|
||||
<string name="wavelog_cancel">İptal</string>
|
||||
<string name="prefs_wavelog_qrz_title">QRZ Cookie</string>
|
||||
<string name="prefs_wavelog_qrz_hint">QRZ giriş çerezlerinizi buraya yapıştırın (EditThisCookie dışa aktarımı veya "k=v; k=v" biçimi). Yükleme için karşı istasyonun grid karesini getirmekte kullanılır.</string>
|
||||
<string name="prefs_wavelog_qrz_label">Çerez</string>
|
||||
<string name="wavelog_copy">Kopyala</string>
|
||||
<string name="wavelog_delete_undo">Geri al</string>
|
||||
<string name="wavelog_col_time">Zaman</string>
|
||||
|
||||
@@ -219,6 +219,7 @@
|
||||
<string name="prefs_wavelog_station_hint">站点 ID</string>
|
||||
<string name="prefs_wavelog_auto">自动上传</string>
|
||||
<string name="prefs_wavelog_test">测试连接</string>
|
||||
<string name="prefs_wavelog_update_sats">更新卫星列表</string>
|
||||
<string name="prefs_wavelog_upload">手动上传</string>
|
||||
<string name="wavelog_title">日志</string>
|
||||
<string name="wavelog_select_transponder">选择转发器</string>
|
||||
@@ -230,6 +231,9 @@
|
||||
<string name="wavelog_grid_mismatch">当前 QTH 网格(%1$s)与站点网格(%2$s)不一致,仍要上传?</string>
|
||||
<string name="wavelog_ignore">忽略并上传</string>
|
||||
<string name="wavelog_cancel">取消</string>
|
||||
<string name="prefs_wavelog_qrz_title">QRZ Cookie</string>
|
||||
<string name="prefs_wavelog_qrz_hint">在此粘贴 QRZ 登录 Cookie(EditThisCookie 导出 或 "k=v; k=v" 格式)。用于上传时查询对方网格。</string>
|
||||
<string name="prefs_wavelog_qrz_label">Cookie</string>
|
||||
<string name="wavelog_copy">复制</string>
|
||||
<string name="wavelog_delete_undo">撤销</string>
|
||||
<string name="wavelog_col_time">时间</string>
|
||||
|
||||
@@ -250,6 +250,7 @@
|
||||
<string name="prefs_wavelog_station_hint">Station ID</string>
|
||||
<string name="prefs_wavelog_auto">Auto upload</string>
|
||||
<string name="prefs_wavelog_test">Test connection</string>
|
||||
<string name="prefs_wavelog_update_sats">Update sats</string>
|
||||
<string name="prefs_wavelog_upload">Upload now</string>
|
||||
<string name="wavelog_title">Log</string>
|
||||
<string name="wavelog_select_transponder">Select transponder</string>
|
||||
@@ -261,6 +262,9 @@
|
||||
<string name="wavelog_grid_mismatch">QTH grid (%1$s) does not match station grid (%2$s). Upload anyway?</string>
|
||||
<string name="wavelog_ignore">Ignore and upload</string>
|
||||
<string name="wavelog_cancel">Cancel</string>
|
||||
<string name="prefs_wavelog_qrz_title">QRZ Cookie</string>
|
||||
<string name="prefs_wavelog_qrz_hint">Paste your QRZ login cookies here (EditThisCookie export or "k=v; k=v" string). Used to fetch the other station grid square for uploads.</string>
|
||||
<string name="prefs_wavelog_qrz_label">Cookie</string>
|
||||
<string name="wavelog_copy">Copy</string>
|
||||
<string name="wavelog_delete_undo">Undo</string>
|
||||
<string name="wavelog_col_time">Time</string>
|
||||
|
||||
@@ -45,11 +45,13 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -72,6 +74,7 @@ import com.rtbishop.look4sat.core.domain.wavelog.WavelogQueue
|
||||
import com.rtbishop.look4sat.core.presentation.R
|
||||
import com.rtbishop.look4sat.core.presentation.formatFrequency
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.Calendar
|
||||
import java.util.TimeZone
|
||||
import java.util.UUID
|
||||
@@ -92,6 +95,8 @@ fun LogTab(
|
||||
aosTimeMs: Long = 0L,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val context = androidx.compose.ui.platform.LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
var selectedUuid by remember { mutableStateOf<String?>(null) }
|
||||
var refreshTick by remember { mutableIntStateOf(0) }
|
||||
val entries = remember(refreshTick) { queue.all() }
|
||||
@@ -244,6 +249,8 @@ private fun ExpandedLogInput(
|
||||
aosTimeMs: Long = 0L,
|
||||
onSaved: () -> Unit
|
||||
) {
|
||||
val context = androidx.compose.ui.platform.LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
var callsign by remember { mutableStateOf("") }
|
||||
var mode by remember { mutableStateOf(radio.uplinkMode ?: "FM") }
|
||||
|
||||
@@ -255,9 +262,10 @@ private fun ExpandedLogInput(
|
||||
// 频率直接取转发器栏的数字(radio 每秒多普勒修正, 回车那一秒的值)
|
||||
val tx = radio.uplinkLow ?: radio.downlinkLow ?: 0L
|
||||
val rx = radio.downlinkLow ?: radio.uplinkLow ?: 0L
|
||||
val qsoId = UUID.randomUUID().toString()
|
||||
queue.add(
|
||||
WavelogQso(
|
||||
id = UUID.randomUUID().toString(),
|
||||
id = qsoId,
|
||||
timeUtcMs = System.currentTimeMillis(),
|
||||
call = call,
|
||||
mode = mode.trim().ifBlank { "FM" }.uppercase(),
|
||||
@@ -270,6 +278,17 @@ private fun ExpandedLogInput(
|
||||
callsign = ""
|
||||
onSaved()
|
||||
showToast(savedMsg)
|
||||
// QRZ 对方网格异步回填(4.5.5): 设置页填了 Cookie 才查, 失败静默
|
||||
scope.launch {
|
||||
val prefs = context.getSharedPreferences("qrz_cookie", android.content.Context.MODE_PRIVATE)
|
||||
val rawCookie = prefs.getString("cookie", "") ?: ""
|
||||
if (rawCookie.isNotBlank()) {
|
||||
val grid = com.rtbishop.look4sat.core.domain.qrz.QrzGridClient.lookupGrid(
|
||||
call, com.rtbishop.look4sat.core.domain.qrz.QrzGridClient.parseCookies(rawCookie)
|
||||
)
|
||||
if (grid != null) queue.updateGridsquare(qsoId, grid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 频率显示与转发器栏完全一致: TX 行 + RX 行(radio 来自每秒多普勒重算, 实时刷新)
|
||||
|
||||
+109
-8
@@ -50,6 +50,7 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
@@ -64,6 +65,7 @@ import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.Stable
|
||||
@@ -109,6 +111,7 @@ import com.rtbishop.look4sat.core.presentation.isVerticalLayout
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun SettingsDestination() {
|
||||
@@ -116,7 +119,7 @@ fun SettingsDestination() {
|
||||
val container = (context.applicationContext as IContainerProvider).getMainContainer()
|
||||
val viewModel: SettingsViewModel = viewModel(factory = SettingsViewModel.factory(container))
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
SettingsScreen(uiState, viewModel::onAction)
|
||||
SettingsScreen(uiState, viewModel::onAction, container.provideLotwSatellitesRepo())
|
||||
|
||||
// WaveLog 网格不一致确认弹窗(4.5.2)
|
||||
val gridConfirm = viewModel.gridConfirm
|
||||
@@ -177,7 +180,11 @@ fun SettingsDestination() {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingsScreen(uiState: SettingsState, onAction: (SettingsAction) -> Unit) {
|
||||
private fun SettingsScreen(
|
||||
uiState: SettingsState,
|
||||
onAction: (SettingsAction) -> Unit,
|
||||
lotwRepo: com.rtbishop.look4sat.core.domain.wavelog.ILotwSatellitesRepo
|
||||
) {
|
||||
val dialogs = rememberDialogVisibility()
|
||||
val pendingCustomSourcesGrant = remember { mutableStateOf<(() -> Unit)?>(null) }
|
||||
val pendingCustomSourcesDeny = remember { mutableStateOf<(() -> Unit)?>(null) }
|
||||
@@ -392,7 +399,7 @@ private fun SettingsScreen(uiState: SettingsState, onAction: (SettingsAction) ->
|
||||
)
|
||||
}
|
||||
item { OtherCard(uiState.otherSettings, onAction) }
|
||||
item { WavelogCard(uiState.otherSettings, onAction) }
|
||||
item { WavelogCard(uiState.otherSettings, onAction, lotwRepo) }
|
||||
item {
|
||||
UiSettingsCard(
|
||||
hiddenScreens = uiState.otherSettings.hiddenScreens,
|
||||
@@ -629,17 +636,35 @@ private fun OtherCard(settings: OtherSettings, onAction: (SettingsAction) -> Uni
|
||||
@Composable
|
||||
private fun WavelogCard(
|
||||
settings: OtherSettings,
|
||||
onAction: (SettingsAction) -> Unit
|
||||
onAction: (SettingsAction) -> Unit,
|
||||
lotwRepo: com.rtbishop.look4sat.core.domain.wavelog.ILotwSatellitesRepo
|
||||
) {
|
||||
val qrzContext = LocalContext.current
|
||||
var showQrzDialog by remember { mutableStateOf(false) }
|
||||
var qrzCookie by remember {
|
||||
mutableStateOf(
|
||||
qrzContext.getSharedPreferences("qrz_cookie", android.content.Context.MODE_PRIVATE)
|
||||
.getString("cookie", "") ?: ""
|
||||
)
|
||||
}
|
||||
var qrzTestResult by remember { mutableStateOf<String?>(null) }
|
||||
var qrzTesting by remember { mutableStateOf(false) }
|
||||
val qrzScope = rememberCoroutineScope()
|
||||
ElevatedCard(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.prefs_wavelog_title),
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.prefs_wavelog_title),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
IconButton(onClick = { showQrzDialog = true }) {
|
||||
Text("\u2699")
|
||||
}
|
||||
}
|
||||
OutlinedTextField(
|
||||
value = settings.wavelogUrl,
|
||||
onValueChange = {
|
||||
@@ -680,6 +705,11 @@ private fun WavelogCard(
|
||||
text = stringResource(id = R.string.prefs_wavelog_test),
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
CardButton(
|
||||
onClick = { onAction(SettingsAction.UpdateLotwSatellites) },
|
||||
text = stringResource(id = R.string.prefs_wavelog_update_sats),
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
CardButton(
|
||||
onClick = { onAction(SettingsAction.UploadWavelogQueue) },
|
||||
text = stringResource(id = R.string.prefs_wavelog_upload),
|
||||
@@ -688,6 +718,77 @@ private fun WavelogCard(
|
||||
}
|
||||
}
|
||||
}
|
||||
if (showQrzDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showQrzDialog = false },
|
||||
title = { Text(text = stringResource(id = R.string.prefs_wavelog_qrz_title)) },
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.prefs_wavelog_qrz_hint),
|
||||
fontSize = 12.sp
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = qrzCookie,
|
||||
onValueChange = { qrzCookie = it },
|
||||
label = { Text(stringResource(id = R.string.prefs_wavelog_qrz_label)) },
|
||||
minLines = 3,
|
||||
maxLines = 8,
|
||||
textStyle = LocalTextStyle.current.copy(fontSize = 12.sp),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
if (qrzTestResult != null) {
|
||||
Text(
|
||||
text = qrzTestResult!!,
|
||||
fontSize = 13.sp,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
TextButton(
|
||||
onClick = {
|
||||
qrzTesting = true
|
||||
qrzTestResult = null
|
||||
qrzScope.launch {
|
||||
val header = com.rtbishop.look4sat.core.domain.qrz.QrzGridClient.parseCookies(qrzCookie)
|
||||
if (header.isBlank()) {
|
||||
qrzTestResult = "Cookie 为空, 请先粘贴"
|
||||
qrzTesting = false
|
||||
return@launch
|
||||
}
|
||||
val own = com.rtbishop.look4sat.core.domain.qrz.QrzGridClient.fetchOwnCallsign(header)
|
||||
if (own == null) {
|
||||
qrzTestResult = "Cookie 无效或已过期(无法识别登录呼号)"
|
||||
} else {
|
||||
val grid = com.rtbishop.look4sat.core.domain.qrz.QrzGridClient.lookupGrid(own, header)
|
||||
qrzTestResult = if (grid != null) {
|
||||
"登录呼号: $own 网格: $grid ✅"
|
||||
} else {
|
||||
"登录呼号: $own 网格: 未填写"
|
||||
}
|
||||
}
|
||||
qrzTesting = false
|
||||
}
|
||||
},
|
||||
enabled = !qrzTesting
|
||||
) {
|
||||
Text(text = if (qrzTesting) "测试中…" else "测试查询")
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
qrzContext.getSharedPreferences("qrz_cookie", android.content.Context.MODE_PRIVATE)
|
||||
.edit().putString("cookie", qrzCookie.trim()).apply()
|
||||
showQrzDialog = false
|
||||
}) { Text(text = stringResource(id = R.string.btn_accept)) }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showQrzDialog = false }) {
|
||||
Text(text = stringResource(id = R.string.wavelog_cancel))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -82,6 +82,8 @@ sealed interface SettingsAction {
|
||||
data object TestWavelogConnection : SettingsAction
|
||||
// WaveLog: 手动上传队列
|
||||
data object UploadWavelogQueue : SettingsAction
|
||||
// WaveLog: 更新 LoTW 卫星列表
|
||||
data object UpdateLotwSatellites : SettingsAction
|
||||
|
||||
// Remote control
|
||||
data class UpdateRC(val settings: RCSettings) : SettingsAction
|
||||
|
||||
+16
-2
@@ -41,7 +41,8 @@ import androidx.compose.runtime.setValue
|
||||
class SettingsViewModel(
|
||||
private val databaseRepo: IDatabaseRepo,
|
||||
private val settingsRepo: ISettingsRepo,
|
||||
private val showToast: IShowToast
|
||||
private val showToast: IShowToast,
|
||||
private val lotwRepo: com.rtbishop.look4sat.core.domain.wavelog.ILotwSatellitesRepo
|
||||
) : ViewModel() {
|
||||
|
||||
private val defaultPosSettings = PositionSettings(false, settingsRepo.stationPosition.value, 0)
|
||||
@@ -156,6 +157,7 @@ class SettingsViewModel(
|
||||
}
|
||||
SettingsAction.TestWavelogConnection -> testWavelogConnection()
|
||||
SettingsAction.UploadWavelogQueue -> uploadWavelogQueue()
|
||||
SettingsAction.UpdateLotwSatellites -> updateLotwSatellites()
|
||||
// System
|
||||
is SettingsAction.ShowToast -> showToast(action.message)
|
||||
}
|
||||
@@ -264,6 +266,17 @@ class SettingsViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateLotwSatellites() {
|
||||
viewModelScope.launch {
|
||||
when (val r = lotwRepo.refresh()) {
|
||||
is com.rtbishop.look4sat.core.domain.wavelog.ILotwSatellitesRepo.RefreshResult.Ok ->
|
||||
showToast("LoTW 卫星列表已更新: ${r.count} 个")
|
||||
is com.rtbishop.look4sat.core.domain.wavelog.ILotwSatellitesRepo.RefreshResult.Error ->
|
||||
wavelogError = "LoTW 卫星列表更新失败: ${r.message}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun uploadWavelogQueue() {
|
||||
viewModelScope.launch {
|
||||
val result = pendingUploader?.uploadQueue()
|
||||
@@ -306,7 +319,8 @@ class SettingsViewModel(
|
||||
SettingsViewModel(
|
||||
databaseRepo = container.databaseRepo,
|
||||
settingsRepo = container.settingsRepo,
|
||||
showToast = container.provideShowToast()
|
||||
showToast = container.provideShowToast(),
|
||||
lotwRepo = container.provideLotwSatellitesRepo()
|
||||
).apply { pendingUploader = container.provideWavelogUploader() }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user