mirror of
https://github.com/atsunatsu/Look4Sat.git
synced 2026-10-02 03:15:37 +00:00
feat(wavelog,radar,settings): WaveLog logbook integration + Log tab
Background: satellite operators want to log QSOs during a pass while
watching live frequencies. 4.5.2 adds WaveLog (logbook server) API v2
integration: log from the radar page, upload to a self-hosted WaveLog
instance with grid-mismatch protection.
Changes:
- Radar page: new third tab "Log" between Transceivers and SSTV
- pick a transponder, watch live TX/RX Doppler-corrected frequencies
- enter callsign, Enter stores locally (UTC time + that second's
frequencies sampled together)
- local entry list shows time/frequency/callsign only (no upload
status, per user: proves the entry was saved)
- swipe-to-delete: yellow trash while swiping, turns into Undo at
75%, 5s countdown before auto-delete (custom gesture, no
SwipeToDismissBox)
- Settings: new WaveLog card (server URL / API key / station ID /
auto-upload switch / test connection / upload now buttons) matching
the user's reference screenshot layout
- Upload pipeline: POST /index.php/api/v2/qso with required fields
(station_profile_id, call, band=SAT, mode, qso_date, time_on UTC)
plus freq/freq_rx (Hz), gridsquare (from station profile via
GET /api/v2/station/{id}), sat_name; RST omitted per user
- Grid check: station gridsquare (first 4) vs user QTH (first 4);
mismatch shows a confirm dialog (ignore & upload / cancel)
- Auto upload: 10-minute in-app retry loop (only when switch on);
manual upload button; local queue capped at 500, all entries stored
locally regardless of switch
- Fixed: subMenuOrder was never persisted (4.5.1 regression)
- core/domain: compileOnly org.json (runtime uses Android's)
- Version 4.5.2 (452)
Verified: check_strings.py OK (8 files, 452/4.5.2);
:app:compileDebugKotlin BUILD SUCCESSFUL locally.
This commit is contained in:
1 parent
38cf7a3569
commit
916a0d0f0a
18 files changed
+1064
-20
No files matched your search
@@ -40,6 +40,9 @@ import com.rtbishop.look4sat.core.data.usecase.AddToCalendar
|
||||
import com.rtbishop.look4sat.core.data.usecase.AudioCapture
|
||||
import com.rtbishop.look4sat.core.data.usecase.SaveImage
|
||||
import com.rtbishop.look4sat.core.data.usecase.ShowToast
|
||||
import com.rtbishop.look4sat.core.domain.wavelog.IWavelogQueueStore
|
||||
import com.rtbishop.look4sat.core.domain.wavelog.WavelogQueue
|
||||
import com.rtbishop.look4sat.core.domain.wavelog.WavelogUploader
|
||||
import com.rtbishop.look4sat.core.domain.model.RadioControlSettings
|
||||
import com.rtbishop.look4sat.core.domain.repository.IDatabaseRepo
|
||||
import com.rtbishop.look4sat.core.domain.repository.IMainContainer
|
||||
@@ -96,6 +99,16 @@ class MainContainer(private val context: Context) : IMainContainer {
|
||||
|
||||
override fun provideSaveImage(): ISaveImage = SaveImage(context)
|
||||
|
||||
// WaveLog 日志(4.5.2): 本地队列 + 上传器(共享实例)
|
||||
override val wavelogQueue: WavelogQueue by lazy {
|
||||
val prefs = context.getSharedPreferences("wavelog", Context.MODE_PRIVATE)
|
||||
WavelogQueue(object : IWavelogQueueStore {
|
||||
override fun load(): String = prefs.getString("wavelog_queue", "[]") ?: "[]"
|
||||
override fun save(json: String) = prefs.edit().putString("wavelog_queue", json).apply()
|
||||
})
|
||||
}
|
||||
override fun provideWavelogUploader(): WavelogUploader = WavelogUploader(settingsRepo, wavelogQueue)
|
||||
|
||||
override fun provideBluetoothReporter(): IReporter {
|
||||
val manager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
|
||||
val rc = settingsRepo.rcSettings.value
|
||||
|
||||
@@ -94,6 +94,11 @@ class SettingsRepo(
|
||||
private val keyUseCustomTransceivers = "useCustomTransceivers"
|
||||
private val keyTleUrl = "tleUrl"
|
||||
private val keyTransceiversUrl = "transceiversUrl"
|
||||
private val keySubMenuOrder = "subMenuOrder"
|
||||
private val keyWavelogUrl = "wavelogUrl"
|
||||
private val keyWavelogApiKey = "wavelogApiKey"
|
||||
private val keyWavelogStationId = "wavelogStationId"
|
||||
private val keyWavelogAutoUpload = "wavelogAutoUpload"
|
||||
private val separatorComma = ","
|
||||
|
||||
//region # Satellites selection settings
|
||||
@@ -361,6 +366,11 @@ class SettingsRepo(
|
||||
putLong(keyHighElevation, new.highElevation.toRawBits())
|
||||
putStringSet(keyHiddenScreens, new.hiddenScreens.toSet())
|
||||
putString(keyScreenOrder, new.screenOrder.joinToString(","))
|
||||
putString(keySubMenuOrder, new.subMenuOrder.joinToString(","))
|
||||
putString(keyWavelogUrl, new.wavelogUrl)
|
||||
putString(keyWavelogApiKey, new.wavelogApiKey)
|
||||
putString(keyWavelogStationId, new.wavelogStationId)
|
||||
putBoolean(keyWavelogAutoUpload, new.wavelogAutoUpload)
|
||||
}
|
||||
new
|
||||
}
|
||||
@@ -378,8 +388,13 @@ class SettingsRepo(
|
||||
sstvMode = preferences.getString(keySstvMode, null) ?: "Auto",
|
||||
hiddenScreens = preferences.getStringSet(keyHiddenScreens, emptySet())?.toList() ?: emptyList(),
|
||||
screenOrder = preferences.getString(keyScreenOrder, null)?.split(",")?.filter { it.isNotBlank() } ?: emptyList(),
|
||||
subMenuOrder = preferences.getString(keySubMenuOrder, null)?.split(",")?.filter { it.isNotBlank() } ?: emptyList(),
|
||||
lowElevation = Double.fromBits(preferences.getLong(keyLowElevation, 15.0.toRawBits())),
|
||||
highElevation = Double.fromBits(preferences.getLong(keyHighElevation, 45.0.toRawBits()))
|
||||
highElevation = Double.fromBits(preferences.getLong(keyHighElevation, 45.0.toRawBits())),
|
||||
wavelogUrl = preferences.getString(keyWavelogUrl, null) ?: "",
|
||||
wavelogApiKey = preferences.getString(keyWavelogApiKey, null) ?: "",
|
||||
wavelogStationId = preferences.getString(keyWavelogStationId, null) ?: "",
|
||||
wavelogAutoUpload = preferences.getBoolean(keyWavelogAutoUpload, false)
|
||||
)
|
||||
//endregion
|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
plugins {
|
||||
alias(libs.plugins.convention.coreDomainPlugin)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// 编译期使用 org.json(构造/解析 WaveLog API 请求体); 运行时用 Android 系统自带的 org.json
|
||||
compileOnly("org.json:json:20240303")
|
||||
}
|
||||
@@ -68,7 +68,12 @@ data class OtherSettings(
|
||||
// UI 设置: 页面顺序(空 = 默认顺序: 卫星/过境/雷达/地图/设置)
|
||||
val screenOrder: List<String> = emptyList(),
|
||||
// UI 设置: 更多菜单顺序(空 = 默认: 匹配/漫游/CW解码)
|
||||
val subMenuOrder: List<String> = emptyList()
|
||||
val subMenuOrder: List<String> = emptyList(),
|
||||
// WaveLog 日志系统(4.5.2): 服务器配置
|
||||
val wavelogUrl: String = "",
|
||||
val wavelogApiKey: String = "",
|
||||
val wavelogStationId: String = "",
|
||||
val wavelogAutoUpload: Boolean = false
|
||||
)
|
||||
|
||||
data class DataSourcesSettings(
|
||||
|
||||
+3
@@ -42,6 +42,9 @@ interface IMainContainer {
|
||||
fun provideRxRadioController(): IRadioController
|
||||
fun provideAudioCapture(): IAudioCapture
|
||||
fun provideSaveImage(): ISaveImage
|
||||
// WaveLog 日志(4.5.2)
|
||||
val wavelogQueue: com.rtbishop.look4sat.core.domain.wavelog.WavelogQueue
|
||||
fun provideWavelogUploader(): com.rtbishop.look4sat.core.domain.wavelog.WavelogUploader
|
||||
}
|
||||
|
||||
data class MutualPassData(
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
* WaveLogApi.kt — WaveLog 日志服务器 API v2 客户端(4.5.2)。
|
||||
*
|
||||
* 端点(文档 /temporary/wavelog_docs/docs/developer/api-v2/):
|
||||
* GET {url}/index.php/api/v2/token whoami(测试连接,任意有效 token)
|
||||
* GET {url}/index.php/api/v2/station/{id} 站点信息(id/name/gridsquare)
|
||||
* POST {url}/index.php/api/v2/qso 创建 QSO(qso:write)
|
||||
* 认证: Authorization: Bearer wl2_xxx(v2 token)
|
||||
*
|
||||
* 轻量实现: HttpURLConnection + 协程(Dispatchers.IO),不引新依赖。
|
||||
*/
|
||||
package com.rtbishop.look4sat.core.domain.wavelog
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStreamReader
|
||||
import java.io.OutputStreamWriter
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
|
||||
/** 站点信息(GET /api/v2/station/{id} 结果) */
|
||||
data class WavelogStation(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val callsign: String,
|
||||
val gridsquare: String
|
||||
)
|
||||
|
||||
sealed class WavelogResult {
|
||||
data class Success(val message: String) : WavelogResult()
|
||||
data class Failure(val message: String) : WavelogResult()
|
||||
}
|
||||
|
||||
object WaveLogApi {
|
||||
|
||||
private const val TIMEOUT_MS = 15000
|
||||
|
||||
/** 规范化服务器地址: 去尾斜杠; 无协议前缀时补 https:// */
|
||||
fun normalizeUrl(raw: String): String {
|
||||
var u = raw.trim().trimEnd('/')
|
||||
if (u.isBlank()) return ""
|
||||
if (!u.startsWith("http://") && !u.startsWith("https://")) u = "https://$u"
|
||||
return u
|
||||
}
|
||||
|
||||
/** 测试连接: GET /index.php/api/v2/token */
|
||||
suspend fun testToken(url: String, apiKey: String): WavelogResult = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val conn = openConn("${normalizeUrl(url)}/index.php/api/v2/token", "GET", apiKey)
|
||||
val code = conn.responseCode
|
||||
val body = readBody(conn, code)
|
||||
if (code in 200..299) {
|
||||
// 返回当前 token 元数据(有 type/name 等)
|
||||
WavelogResult.Success("连接成功")
|
||||
} else {
|
||||
WavelogResult.Failure("HTTP $code: ${shortError(body)}")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
WavelogResult.Failure("网络错误: ${e.message ?: e.javaClass.simpleName}")
|
||||
}
|
||||
}
|
||||
|
||||
/** 站点信息: GET /index.php/api/v2/station/{id} */
|
||||
suspend fun getStation(url: String, apiKey: String, stationId: String): WavelogResult = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val conn = openConn("${normalizeUrl(url)}/index.php/api/v2/station/$stationId", "GET", apiKey)
|
||||
val code = conn.responseCode
|
||||
val body = readBody(conn, code)
|
||||
if (code in 200..299) {
|
||||
val obj = JSONObject(body)
|
||||
val data = obj.optJSONObject("data") ?: obj
|
||||
val station = WavelogStation(
|
||||
id = data.optInt("id"),
|
||||
name = data.optString("name"),
|
||||
callsign = data.optString("callsign"),
|
||||
gridsquare = data.optString("gridsquare")
|
||||
)
|
||||
WavelogResult.Success(JSONObject().apply {
|
||||
put("id", station.id); put("name", station.name)
|
||||
put("callsign", station.callsign); put("gridsquare", station.gridsquare)
|
||||
}.toString())
|
||||
} else {
|
||||
WavelogResult.Failure("HTTP $code: ${shortError(body)}")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
WavelogResult.Failure("网络错误: ${e.message ?: e.javaClass.simpleName}")
|
||||
}
|
||||
}
|
||||
|
||||
/** 创建 QSO: POST /index.php/api/v2/qso(单条 JSON) */
|
||||
suspend fun postQso(
|
||||
url: String,
|
||||
apiKey: String,
|
||||
stationProfileId: String,
|
||||
qso: WavelogQso,
|
||||
gridsquare: String
|
||||
): WavelogResult = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val body = JSONObject().apply {
|
||||
put("station_profile_id", stationProfileId.toIntOrNull() ?: 0)
|
||||
put("call", qso.call)
|
||||
put("band", "SAT")
|
||||
put("mode", qso.mode)
|
||||
put("qso_date", utcDate(qso.timeUtcMs))
|
||||
put("time_on", utcTime(qso.timeUtcMs))
|
||||
put("freq", qso.freqTxHz)
|
||||
put("freq_rx", qso.freqRxHz)
|
||||
put("gridsquare", gridsquare)
|
||||
put("sat_name", qso.satName)
|
||||
}
|
||||
val conn = openConn("${normalizeUrl(url)}/index.php/api/v2/qso", "POST", apiKey)
|
||||
conn.doOutput = true
|
||||
conn.setRequestProperty("Content-Type", "application/json")
|
||||
OutputStreamWriter(conn.outputStream, Charsets.UTF_8).use { it.write(body.toString()) }
|
||||
val code = conn.responseCode
|
||||
val resp = readBody(conn, code)
|
||||
if (code in 200..299) {
|
||||
WavelogResult.Success("已上传")
|
||||
} else {
|
||||
WavelogResult.Failure("HTTP $code: ${shortError(resp)}")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
WavelogResult.Failure("网络错误: ${e.message ?: e.javaClass.simpleName}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun openConn(url: String, method: String, apiKey: String): HttpURLConnection {
|
||||
val conn = URL(url).openConnection() as HttpURLConnection
|
||||
conn.requestMethod = method
|
||||
conn.connectTimeout = TIMEOUT_MS
|
||||
conn.readTimeout = TIMEOUT_MS
|
||||
if (apiKey.isNotBlank()) conn.setRequestProperty("Authorization", "Bearer $apiKey")
|
||||
return conn
|
||||
}
|
||||
|
||||
private fun readBody(conn: HttpURLConnection, code: Int): String {
|
||||
val stream = if (code in 200..299) conn.inputStream else conn.errorStream
|
||||
?: return ""
|
||||
return BufferedReader(InputStreamReader(stream, Charsets.UTF_8)).use { it.readText() }
|
||||
}
|
||||
|
||||
private fun shortError(body: String): String {
|
||||
return try {
|
||||
val obj = JSONObject(body)
|
||||
obj.optString("message").ifBlank { body.take(120) }
|
||||
} catch (_: Exception) {
|
||||
body.take(120)
|
||||
}
|
||||
}
|
||||
|
||||
private fun utcDate(ms: Long): String {
|
||||
val cal = java.util.Calendar.getInstance(java.util.TimeZone.getTimeZone("UTC"))
|
||||
cal.timeInMillis = ms
|
||||
return "%04d-%02d-%02d".format(
|
||||
cal.get(java.util.Calendar.YEAR), cal.get(java.util.Calendar.MONTH) + 1,
|
||||
cal.get(java.util.Calendar.DAY_OF_MONTH)
|
||||
)
|
||||
}
|
||||
|
||||
private fun utcTime(ms: Long): String {
|
||||
val cal = java.util.Calendar.getInstance(java.util.TimeZone.getTimeZone("UTC"))
|
||||
cal.timeInMillis = ms
|
||||
return "%02d:%02d:%02d".format(
|
||||
cal.get(java.util.Calendar.HOUR_OF_DAY), cal.get(java.util.Calendar.MINUTE),
|
||||
cal.get(java.util.Calendar.SECOND)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* WavelogQueue.kt — WaveLog 本地日志队列(4.5.2)。
|
||||
*
|
||||
* 纯 Kotlin(不依赖 Android): 存储走 IWavelogQueueStore 接口,
|
||||
* 由 core/data 用 SharedPreferences 实现。
|
||||
* 队列上限 500 条(超出丢最旧)。
|
||||
*/
|
||||
package com.rtbishop.look4sat.core.domain.wavelog
|
||||
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
|
||||
/** 存储抽象(SharedPreferences 实现见 core/data) */
|
||||
interface IWavelogQueueStore {
|
||||
fun load(): String
|
||||
fun save(json: String)
|
||||
}
|
||||
|
||||
/** 待上传的 QSO 条目(本地队列元素,与 POST /api/v2/qso 字段对应) */
|
||||
data class WavelogQso(
|
||||
val id: String, // 本地唯一 id(UUID)
|
||||
val timeUtcMs: Long, // 回车时刻 UTC 毫秒(本地显示 + 组装 qso_date/time_on)
|
||||
val call: String,
|
||||
val mode: String,
|
||||
val freqTxHz: Long, // 上行(回车那一秒多普勒修正)
|
||||
val freqRxHz: Long, // 下行
|
||||
val satName: String
|
||||
)
|
||||
|
||||
class WavelogQueue(private val store: IWavelogQueueStore) {
|
||||
|
||||
private val key = "wavelog_queue"
|
||||
|
||||
fun all(): List<WavelogQso> {
|
||||
val raw = store.load()
|
||||
return try {
|
||||
val arr = JSONArray(raw)
|
||||
(0 until arr.length()).map { i ->
|
||||
val o = arr.getJSONObject(i)
|
||||
WavelogQso(
|
||||
id = o.getString("id"),
|
||||
timeUtcMs = o.getLong("timeUtcMs"),
|
||||
call = o.optString("call"),
|
||||
mode = o.optString("mode"),
|
||||
freqTxHz = o.optLong("freqTxHz"),
|
||||
freqRxHz = o.optLong("freqRxHz"),
|
||||
satName = o.optString("satName")
|
||||
)
|
||||
}
|
||||
} catch (_: Exception) { emptyList() }
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun add(qso: WavelogQso) {
|
||||
val list = all().toMutableList()
|
||||
list.add(0, qso) // 最新在前
|
||||
if (list.size > 500) list.removeAt(list.size - 1)
|
||||
save(list)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun remove(id: String) {
|
||||
save(all().filter { it.id != id })
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun removeAll(ids: Set<String>) {
|
||||
save(all().filter { it.id !in ids })
|
||||
}
|
||||
|
||||
private fun save(list: List<WavelogQso>) {
|
||||
val arr = JSONArray()
|
||||
list.forEach { q ->
|
||||
arr.put(JSONObject().apply {
|
||||
put("id", q.id); put("timeUtcMs", q.timeUtcMs); put("call", q.call)
|
||||
put("mode", q.mode); put("freqTxHz", q.freqTxHz)
|
||||
put("freqRxHz", q.freqRxHz); put("satName", q.satName)
|
||||
})
|
||||
}
|
||||
store.save(arr.toString())
|
||||
}
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* WavelogUploader.kt — WaveLog 队列上传调度(4.5.2)。
|
||||
*
|
||||
* 手动/周期上传共用: 逐条网格检测(用户 QTH 前 4 位 vs 台站网格前 4 位)
|
||||
* → POST /api/v2/qso → 成功移出队列。
|
||||
* 网格不一致: 返回 NeedConfirm(由 UI 弹窗「忽略并上传/取消」),
|
||||
* 确认后带 force=true 重试本批。
|
||||
*/
|
||||
package com.rtbishop.look4sat.core.domain.wavelog
|
||||
|
||||
import com.rtbishop.look4sat.core.domain.repository.ISettingsRepo
|
||||
import org.json.JSONObject
|
||||
|
||||
sealed class UploadOutcome {
|
||||
data class NeedConfirm(val stationGrid: String, val userGrid: String) : UploadOutcome()
|
||||
data class Done(val successCount: Int, val failedCount: Int, val message: String) : UploadOutcome()
|
||||
}
|
||||
|
||||
class WavelogUploader(
|
||||
private val settingsRepo: ISettingsRepo,
|
||||
private val queue: WavelogQueue
|
||||
) {
|
||||
|
||||
// 台站网格缓存(每次上传前刷新; 失败用旧值)
|
||||
private var cachedStationGrid: String? = null
|
||||
|
||||
/** 上传整个队列。force=true 跳过网格确认(用户已选「忽略并上传」) */
|
||||
suspend fun uploadQueue(force: Boolean = false): UploadOutcome {
|
||||
val settings = settingsRepo.otherSettings.value
|
||||
val url = settings.wavelogUrl
|
||||
val apiKey = settings.wavelogApiKey
|
||||
val stationId = settings.wavelogStationId
|
||||
if (url.isBlank() || apiKey.isBlank() || stationId.isBlank()) {
|
||||
return UploadOutcome.Done(0, queue.all().size, "未配置 WaveLog 服务器")
|
||||
}
|
||||
|
||||
// 1. 拉站点信息(拿台站网格)
|
||||
val stationGrid = getStationGrid(url, apiKey, stationId) ?: return UploadOutcome.Done(
|
||||
0, queue.all().size, "无法获取站点信息(检查站点 ID/密钥权限)"
|
||||
)
|
||||
|
||||
// 2. 网格检测: 用户当前 QTH 前 4 位 vs 台站网格前 4 位
|
||||
if (!force) {
|
||||
val userGrid = userQthGrid()
|
||||
if (userGrid != null && stationGrid.take(4).lowercase() != userGrid.take(4).lowercase()) {
|
||||
return UploadOutcome.NeedConfirm(stationGrid, userGrid)
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 逐条上传
|
||||
val entries = queue.all()
|
||||
var ok = 0
|
||||
var fail = 0
|
||||
val uploadedIds = mutableSetOf<String>()
|
||||
for (qso in entries) {
|
||||
val result = WaveLogApi.postQso(url, apiKey, stationId, qso, stationGrid)
|
||||
if (result is WavelogResult.Success) {
|
||||
ok++
|
||||
uploadedIds.add(qso.id)
|
||||
} else {
|
||||
fail++
|
||||
}
|
||||
}
|
||||
if (uploadedIds.isNotEmpty()) queue.removeAll(uploadedIds)
|
||||
val message = if (fail == 0) "成功上传 $ok 条" else "成功 $ok 条, 失败 $fail 条(保留待重试)"
|
||||
return UploadOutcome.Done(ok, fail, message)
|
||||
}
|
||||
|
||||
private suspend fun getStationGrid(url: String, apiKey: String, stationId: String): String? {
|
||||
val result = WaveLogApi.getStation(url, apiKey, stationId)
|
||||
if (result is WavelogResult.Success) {
|
||||
return try {
|
||||
JSONObject(result.message).optString("gridsquare").takeIf { it.isNotBlank() }
|
||||
?: cachedStationGrid
|
||||
} catch (_: Exception) { cachedStationGrid }
|
||||
}
|
||||
return cachedStationGrid
|
||||
}
|
||||
|
||||
/** 用户当前 QTH 网格(前 4 位; 无 QTH 返回 null = 跳过检测) */
|
||||
private fun userQthGrid(): String? {
|
||||
return settingsRepo.stationPosition.value.qthLocator?.takeIf { it.length >= 4 }
|
||||
}
|
||||
}
|
||||
@@ -48,8 +48,8 @@
|
||||
\n\nPerbarui database setidaknya seminggu sekali untuk prediksi yang akurat.</string>
|
||||
<string name="pass_whatsnew_title">Apa yang baru di Look4Sat</string>
|
||||
<string name="pass_whatsnew_message">
|
||||
* Navigasi bawah kini menggunakan 5 tab utama plus menu Lainnya (menu tingkat kedua yang dapat dilipat)
|
||||
* Pengaturan UI: urutan halaman dipisah menjadi menu utama dan menu lainnya; pindahkan halaman di antara keduanya</string>
|
||||
* Integrasi logbook WaveLog baru: catat kontak satelit dari halaman radar (tab Log)
|
||||
* Unggah otomatis/manual ke server WaveLog Anda dengan deteksi ketidakcocokan grid</string>
|
||||
<string name="radar_back">Kembali</string>
|
||||
<string name="radar_notify">Beri tahu</string>
|
||||
<string name="radar_az_text">Azimuth</string>
|
||||
@@ -177,6 +177,23 @@
|
||||
<string name="disconnect_radio">Putuskan</string>
|
||||
<string name="track_radio">Lacak</string>
|
||||
<string name="stop_radio">Berhenti</string>
|
||||
<string name="prefs_wavelog_title">Server log WaveLog</string>
|
||||
<string name="prefs_wavelog_url_hint">Alamat server</string>
|
||||
<string name="prefs_wavelog_key_hint">Kunci API</string>
|
||||
<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_upload">Unggah sekarang</string>
|
||||
<string name="wavelog_title">Log</string>
|
||||
<string name="wavelog_call_hint">Tanda panggil</string>
|
||||
<string name="wavelog_mode_hint">Mode</string>
|
||||
<string name="wavelog_saved">Tersimpan ke log lokal</string>
|
||||
<string name="wavelog_need_config">Konfigurasikan WaveLog di Pengaturan terlebih dahulu</string>
|
||||
<string name="wavelog_empty">Belum ada entri log</string>
|
||||
<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="wavelog_delete_undo">Urungkan</string>
|
||||
<string name="prefs_other_title">Pengaturan lainnya</string>
|
||||
<string name="prefs_ui_title">Pengaturan UI</string>
|
||||
<string name="prefs_ui_order_title">Urutan halaman</string>
|
||||
|
||||
@@ -53,8 +53,8 @@
|
||||
\n\nDoğru tahminler alabilmek için veritabanını en az haftada bir güncelleyin.</string>
|
||||
<string name="pass_whatsnew_title" translatable="false">What\'s new in Look4Sat</string>
|
||||
<string name="pass_whatsnew_message" translatable="false">
|
||||
* Alt gezinme artık 5 ana sekme artı Daha Fazla menüsü kullanıyor (katlanabilir ikinci düzey menü)
|
||||
* UI Ayarları: sayfa sırası ana menü ve daha fazla menü olarak ayrıldı; sayfalar arasında taşıma yapılabilir</string>
|
||||
* Yeni WaveLog günlük entegrasyonu: radar sayfasından uydu temaslarını kaydedin (Günlük sekmesi)
|
||||
* Izgara uyuşmazlığı algılamayla WaveLog sunucunuza otomatik/manuel yükleme</string>
|
||||
|
||||
<!-- Radar screen -->
|
||||
<string name="radar_back">Geri</string>
|
||||
@@ -187,6 +187,23 @@
|
||||
<string name="disconnect_radio">Bağlantıyı Kes</string>
|
||||
<string name="track_radio">Takip</string>
|
||||
<string name="stop_radio">Dur</string>
|
||||
<string name="prefs_wavelog_title">WaveLog günlük sunucusu</string>
|
||||
<string name="prefs_wavelog_url_hint">Sunucu adresi</string>
|
||||
<string name="prefs_wavelog_key_hint">API anahtarı</string>
|
||||
<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_upload">Şimdi yükle</string>
|
||||
<string name="wavelog_title">Günlük</string>
|
||||
<string name="wavelog_call_hint">Çağrı işareti</string>
|
||||
<string name="wavelog_mode_hint">Mod</string>
|
||||
<string name="wavelog_saved">Yerel günlüğe kaydedildi</string>
|
||||
<string name="wavelog_need_config">Önce Ayarlar\'da WaveLog\'u yapılandırın</string>
|
||||
<string name="wavelog_empty">Henüz günlük kaydı yok</string>
|
||||
<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="wavelog_delete_undo">Geri al</string>
|
||||
<string name="prefs_other_title">Diğer ayarlar</string>
|
||||
<string name="prefs_ui_title">UI Ayarları</string>
|
||||
<string name="prefs_ui_order_title">Sayfa sırası</string>
|
||||
|
||||
@@ -43,8 +43,8 @@
|
||||
<string name="pass_welcome_message">请务必在设置中通过GPS、经纬度或QTH定位您的位置\n建议至少每周更新一次数据库,以确保预测结果的准确性</string>
|
||||
<string name="pass_whatsnew_title">Look4Sat Pro 更新内容</string>
|
||||
<string name="pass_whatsnew_message" translatable="false">
|
||||
* 底部导航改为 5 个主标签 + 更多菜单(可折叠二级菜单)
|
||||
* UI 设置: 页面顺序分为主菜单与更多菜单,可拖拽或移动页面</string>
|
||||
* 新增 WaveLog 日志系统: 雷达页 Log 标签记录卫星通联日志
|
||||
* 自动/手动上传到 WaveLog 服务器,带网格不一致检测</string>
|
||||
|
||||
<!-- Radar screen -->
|
||||
<string name="radar_back">后退</string>
|
||||
@@ -170,6 +170,23 @@
|
||||
<string name="disconnect_radio">断开</string>
|
||||
<string name="track_radio">跟踪</string>
|
||||
<string name="stop_radio">停止</string>
|
||||
<string name="prefs_wavelog_title">WaveLog 日志服务器</string>
|
||||
<string name="prefs_wavelog_url_hint">服务器地址</string>
|
||||
<string name="prefs_wavelog_key_hint">API 密钥</string>
|
||||
<string name="prefs_wavelog_station_hint">站点 ID</string>
|
||||
<string name="prefs_wavelog_auto">自动上传</string>
|
||||
<string name="prefs_wavelog_test">测试连接</string>
|
||||
<string name="prefs_wavelog_upload">手动上传</string>
|
||||
<string name="wavelog_title">日志</string>
|
||||
<string name="wavelog_call_hint">对方呼号</string>
|
||||
<string name="wavelog_mode_hint">模式</string>
|
||||
<string name="wavelog_saved">已存入本地日志</string>
|
||||
<string name="wavelog_need_config">请先在设置中配置 WaveLog</string>
|
||||
<string name="wavelog_empty">暂无日志记录</string>
|
||||
<string name="wavelog_grid_mismatch">当前 QTH 网格(%1$s)与站点网格(%2$s)不一致,仍要上传?</string>
|
||||
<string name="wavelog_ignore">忽略并上传</string>
|
||||
<string name="wavelog_cancel">取消</string>
|
||||
<string name="wavelog_delete_undo">撤销</string>
|
||||
<string name="prefs_other_title">其他设置</string>
|
||||
<string name="prefs_ui_title">UI设置</string>
|
||||
<string name="prefs_ui_order_title">页面顺序</string>
|
||||
|
||||
@@ -55,8 +55,8 @@
|
||||
\n\nPlease update the database at least weekly to get accurate predictions.</string>
|
||||
<string name="pass_whatsnew_title" translatable="false">What\'s new in Look4Sat</string>
|
||||
<string name="pass_whatsnew_message" translatable="false">
|
||||
* Bottom navigation now uses 5 main tabs plus a More menu (collapsible second-level menu)
|
||||
* UI Settings: page order split into main menu and more menu; drag or move pages between them</string>
|
||||
* New WaveLog logbook integration: log satellite contacts from the radar page (Log tab)
|
||||
* Auto/manual upload to your WaveLog server with grid mismatch detection</string>
|
||||
|
||||
<!-- Radar screen -->
|
||||
<string name="radar_back">Back</string>
|
||||
@@ -201,6 +201,23 @@
|
||||
<string name="disconnect_radio">Disconnect</string>
|
||||
<string name="track_radio">Track</string>
|
||||
<string name="stop_radio">Stop</string>
|
||||
<string name="prefs_wavelog_title">WaveLog log server</string>
|
||||
<string name="prefs_wavelog_url_hint">Server address</string>
|
||||
<string name="prefs_wavelog_key_hint">API key</string>
|
||||
<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_upload">Upload now</string>
|
||||
<string name="wavelog_title">Log</string>
|
||||
<string name="wavelog_call_hint">Callsign</string>
|
||||
<string name="wavelog_mode_hint">Mode</string>
|
||||
<string name="wavelog_saved">Saved to local log</string>
|
||||
<string name="wavelog_need_config">Configure WaveLog in Settings first</string>
|
||||
<string name="wavelog_empty">No log entries yet</string>
|
||||
<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="wavelog_delete_undo">Undo</string>
|
||||
<string name="prefs_other_title">Other settings</string>
|
||||
<string name="prefs_ui_title">UI Settings</string>
|
||||
<string name="prefs_ui_order_title">Page order</string>
|
||||
|
||||
Reference in new issue
Block a user