mirror of
https://github.com/atsunatsu/Look4Sat.git
synced 2026-10-03 03:17:21 +00:00
feat: LoTW upload + logbook (port from Iu-yang1's fork)
- Port TQ8 signing/upload pipeline (core/data/lotw: LoTWConfig, LoTWKeyMaterial,
LoTWSigner, LoTWStorage, LoTWUploadRepository) with bundled ARRL config.tq6
- Multi-grid station locations: primary grid in GRIDSQUARE + full set in
MY_VUCC_GRIDS (unsigned tSTATION extra, IOTA precedent); signer matches
myGrid against the whole grid set
- Satellite-name normalization: SatNOGS names ("SO-50 (SaudiOSCAR 50)") resolve
to official LoTW names by exact-then-substring match
- Logbook storage: separate QsoDatabase (version 1, no migration on existing
installs), QsoRepository with dedupe/confirmation merge; LoTW sync (manual +
auto) feeds confirmations into the logbook and marks local uploads confirmed
- Radar Log tab: single-callsign recording, per-satellite mode preset
(CW/SSB/FT4), English social post generation (copy/share), LoTW upload with
one-shot preview confirmation + receipts ledger
- Settings: 日志本 section (records + confirmation state, delete on tap) above
LoTW config; LoTW upload config (TrustedQSL .p12 import + password, station
grid list incl. IOTA/CQZ/ITUZ)
- i18n strings (en/zh)
This commit is contained in:
1 parent
d33629f015
commit
8e549cd43c
36 files changed
+2696
-4
No files matched your search
+178
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*/
|
||||
package com.rtbishop.look4sat.feature.settings
|
||||
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.rtbishop.look4sat.core.domain.repository.LoTWCertificate
|
||||
import com.rtbishop.look4sat.core.domain.repository.LoTWStation
|
||||
import com.rtbishop.look4sat.core.presentation.R
|
||||
|
||||
@Composable
|
||||
fun LoTWUploadCard(
|
||||
hasCertificate: Boolean,
|
||||
stationGrid: String,
|
||||
showUploadConfigDialog: () -> Unit
|
||||
) {
|
||||
ElevatedCard(modifier = Modifier.fillMaxWidth().clickable { showUploadConfigDialog() }) {
|
||||
Column(modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)) {
|
||||
Text(
|
||||
text = stringResource(R.string.prefs_lotw_upload_title),
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Text(
|
||||
text = if (hasCertificate) {
|
||||
stringResource(R.string.prefs_lotw_upload_configured, stationGrid)
|
||||
} else {
|
||||
stringResource(R.string.prefs_lotw_upload_not_configured)
|
||||
},
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
maxLines = 2
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LoTWUploadConfigDialog(
|
||||
certificate: LoTWCertificate?,
|
||||
station: LoTWStation?,
|
||||
busy: Boolean,
|
||||
onDismiss: () -> Unit,
|
||||
onImport: (ByteArray, CharArray) -> Unit,
|
||||
onRemove: () -> Unit,
|
||||
onSaveStation: (LoTWStation) -> Unit
|
||||
) {
|
||||
var password by remember { mutableStateOf("") }
|
||||
var grid by remember { mutableStateOf(station?.grid.orEmpty()) }
|
||||
var cqZone by remember { mutableStateOf(station?.cqZone.orEmpty()) }
|
||||
var ituZone by remember { mutableStateOf(station?.ituZone.orEmpty()) }
|
||||
var iota by remember { mutableStateOf(station?.iota.orEmpty()) }
|
||||
val context = LocalContext.current
|
||||
|
||||
val filePicker = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
|
||||
if (uri != null && password.isNotBlank()) {
|
||||
val bytes = runCatching {
|
||||
context.contentResolver.openInputStream(uri)?.use { it.readBytes() }
|
||||
}.getOrNull()
|
||||
if (bytes != null && bytes.isNotEmpty()) {
|
||||
onImport(bytes, password.toCharArray())
|
||||
password = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(stringResource(R.string.prefs_lotw_upload_title)) },
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
if (busy) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
CircularProgressIndicator(modifier = Modifier.height(20.dp), strokeWidth = 2.dp)
|
||||
Text(stringResource(R.string.prefs_lotw_upload_busy), fontSize = 13.sp)
|
||||
}
|
||||
}
|
||||
if (certificate == null) {
|
||||
Text(stringResource(R.string.prefs_lotw_upload_cert_hint), fontSize = 13.sp)
|
||||
OutlinedTextField(
|
||||
value = password,
|
||||
onValueChange = { password = it },
|
||||
label = { Text(stringResource(R.string.prefs_lotw_upload_password), fontSize = 13.sp) },
|
||||
singleLine = true,
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Button(
|
||||
onClick = { filePicker.launch(arrayOf("*/*")) },
|
||||
enabled = password.isNotBlank() && !busy,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) { Text(stringResource(R.string.prefs_lotw_upload_import), fontSize = 13.sp) }
|
||||
} else {
|
||||
Text(stringResource(R.string.prefs_lotw_upload_cert_info, certificate.callsign, certificate.dxcc, certificate.expires), fontSize = 13.sp)
|
||||
OutlinedButton(onClick = onRemove, enabled = !busy) {
|
||||
Text(stringResource(R.string.prefs_lotw_upload_remove), fontSize = 13.sp)
|
||||
}
|
||||
}
|
||||
Text(stringResource(R.string.prefs_lotw_upload_station_title), style = MaterialTheme.typography.titleSmall)
|
||||
// Grid field accepts a comma-separated multi-grid set (e.g. "OL62,OL72").
|
||||
OutlinedTextField(
|
||||
value = grid,
|
||||
onValueChange = { grid = it.uppercase() },
|
||||
label = { Text(stringResource(R.string.prefs_lotw_upload_grid), fontSize = 13.sp) },
|
||||
singleLine = true,
|
||||
keyboardOptions = androidx.compose.foundation.text.KeyboardOptions(capitalization = KeyboardCapitalization.Characters),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
OutlinedTextField(
|
||||
value = cqZone,
|
||||
onValueChange = { cqZone = it },
|
||||
label = { Text("CQZ", fontSize = 12.sp) },
|
||||
singleLine = true,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = ituZone,
|
||||
onValueChange = { ituZone = it },
|
||||
label = { Text("ITUZ", fontSize = 12.sp) },
|
||||
singleLine = true,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = iota,
|
||||
onValueChange = { iota = it.uppercase() },
|
||||
label = { Text("IOTA", fontSize = 12.sp) },
|
||||
singleLine = true,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = { onSaveStation(LoTWStation(grid, cqZone, ituZone, "", "", iota)) },
|
||||
enabled = grid.isNotBlank() && !busy
|
||||
) { Text(stringResource(R.string.prefs_lotw_upload_save)) }
|
||||
},
|
||||
dismissButton = { TextButton(onClick = onDismiss) { Text(stringResource(R.string.prefs_lotw_upload_close)) } }
|
||||
)
|
||||
}
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*/
|
||||
package com.rtbishop.look4sat.feature.settings
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.rtbishop.look4sat.core.domain.logbook.QsoRecord
|
||||
import com.rtbishop.look4sat.core.domain.logbook.displayMode
|
||||
import com.rtbishop.look4sat.core.presentation.R
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
|
||||
@Composable
|
||||
fun LogbookCard(recordCount: Int, showLogbookDialog: () -> Unit) {
|
||||
ElevatedCard(modifier = Modifier.fillMaxWidth().clickable { showLogbookDialog() }) {
|
||||
Column(modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)) {
|
||||
Text(
|
||||
text = stringResource(R.string.prefs_logbook_title),
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.prefs_logbook_count, recordCount),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
maxLines = 2
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LogbookDialog(
|
||||
records: List<QsoRecord>,
|
||||
onDismiss: () -> Unit,
|
||||
onDelete: (Long) -> Unit
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(stringResource(R.string.prefs_logbook_title)) },
|
||||
text = {
|
||||
if (records.isEmpty()) {
|
||||
Text(stringResource(R.string.prefs_logbook_empty), fontSize = 14.sp)
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||
) {
|
||||
items(records, key = { it.id }) { record ->
|
||||
LogbookRow(record, onDelete = { onDelete(record.id) })
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = { TextButton(onClick = onDismiss) { Text(stringResource(R.string.prefs_logbook_close)) } }
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LogbookRow(record: QsoRecord, onDelete: () -> Unit) {
|
||||
val time = remember(record.startUtcMillis) {
|
||||
SimpleDateFormat("MM-dd HH:mm'Z'", Locale.US).apply {
|
||||
timeZone = TimeZone.getTimeZone("UTC")
|
||||
}.format(Date(record.startUtcMillis))
|
||||
}
|
||||
val satShort = record.satelliteName.substringBefore('(').trim()
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().clickable { onDelete() }.padding(horizontal = 4.dp, vertical = 2.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = "$time ${record.theirCallsign} ${record.displayMode}",
|
||||
fontSize = 13.sp,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Text(text = satShort, fontSize = 11.sp, color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 1)
|
||||
}
|
||||
Text(
|
||||
text = if (record.lotwConfirmed) "✓" else "",
|
||||
fontSize = 14.sp,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
+36
-1
@@ -261,6 +261,24 @@ private fun SettingsScreen(uiState: SettingsState, onAction: (SettingsAction) ->
|
||||
onSyncIncremental = { onAction(SettingsAction.SyncLoTWGrids(it, LoTWSyncMode.Incremental)) }
|
||||
)
|
||||
}
|
||||
if (dialogs.logbook) {
|
||||
LogbookDialog(
|
||||
records = uiState.logbookRecords,
|
||||
onDismiss = { dialogs.logbook = false },
|
||||
onDelete = { onAction(SettingsAction.DeleteLogbookRecord(it)) }
|
||||
)
|
||||
}
|
||||
if (dialogs.lotwUpload) {
|
||||
LoTWUploadConfigDialog(
|
||||
certificate = uiState.lotwCertificate,
|
||||
station = uiState.lotwStation,
|
||||
busy = uiState.lotwUploadBusy,
|
||||
onDismiss = { dialogs.lotwUpload = false },
|
||||
onImport = { bytes, password -> onAction(SettingsAction.ImportLoTWCertificate(bytes, password)) },
|
||||
onRemove = { onAction(SettingsAction.RemoveLoTWCertificate) },
|
||||
onSaveStation = { onAction(SettingsAction.SaveLoTWStation(it)) }
|
||||
)
|
||||
}
|
||||
|
||||
// URLs for top bar
|
||||
val uriHandler = LocalUriHandler.current
|
||||
@@ -365,6 +383,19 @@ private fun SettingsScreen(uiState: SettingsState, onAction: (SettingsAction) ->
|
||||
onRadioControlClick = { dialogs.radioControl = true }
|
||||
)
|
||||
}
|
||||
item {
|
||||
LogbookCard(
|
||||
recordCount = uiState.logbookRecords.size,
|
||||
showLogbookDialog = { dialogs.logbook = true }
|
||||
)
|
||||
}
|
||||
item {
|
||||
LoTWUploadCard(
|
||||
hasCertificate = uiState.lotwCertificate != null,
|
||||
stationGrid = uiState.lotwStation?.grid.orEmpty(),
|
||||
showUploadConfigDialog = { onAction(SettingsAction.LoadLoTWUploadStatus); dialogs.lotwUpload = true }
|
||||
)
|
||||
}
|
||||
item {
|
||||
LoTWCard(
|
||||
settings = uiState.lotwSettings,
|
||||
@@ -895,6 +926,8 @@ private class DialogVisibility {
|
||||
var wavelog by mutableStateOf(false)
|
||||
var lotw by mutableStateOf(false)
|
||||
var compassCalibration by mutableStateOf(false)
|
||||
var logbook by mutableStateOf(false)
|
||||
var lotwUpload by mutableStateOf(false)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -902,7 +935,7 @@ private fun rememberDialogVisibility(): DialogVisibility {
|
||||
return rememberSaveable(saver = run {
|
||||
androidx.compose.runtime.saveable.Saver(
|
||||
save = {
|
||||
listOf(it.position, it.locator, it.dataSources, it.network, it.bluetooth, it.radioControl, it.wavelog, it.lotw, it.compassCalibration)
|
||||
listOf(it.position, it.locator, it.dataSources, it.network, it.bluetooth, it.radioControl, it.wavelog, it.lotw, it.compassCalibration, it.logbook, it.lotwUpload)
|
||||
},
|
||||
restore = {
|
||||
DialogVisibility().apply {
|
||||
@@ -910,6 +943,8 @@ private fun rememberDialogVisibility(): DialogVisibility {
|
||||
network = it[3]; bluetooth = it[4]; radioControl = it[5]; wavelog = it[6]
|
||||
lotw = it.getOrElse(7) { false }
|
||||
compassCalibration = it.getOrElse(8) { false }
|
||||
logbook = it.getOrElse(9) { false }
|
||||
lotwUpload = it.getOrElse(10) { false }
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
+17
@@ -68,6 +68,13 @@ data class SettingsState(
|
||||
val lotwError: LoTWError? = null,
|
||||
/** Epoch ms of the last successful LoTW sync (0 = never) — shown like the ephemeris update time. */
|
||||
val lotwLastSyncEpochMs: Long = 0L,
|
||||
/** Logbook (QSO records + LoTW confirmations), newest first. */
|
||||
val logbookRecords: List<com.rtbishop.look4sat.core.domain.logbook.QsoRecord> = emptyList(),
|
||||
/** Imported LoTW upload certificate (null when none). */
|
||||
val lotwCertificate: com.rtbishop.look4sat.core.domain.repository.LoTWCertificate? = null,
|
||||
/** LoTW upload station location (null when unset). */
|
||||
val lotwStation: com.rtbishop.look4sat.core.domain.repository.LoTWStation? = null,
|
||||
val lotwUploadBusy: Boolean = false,
|
||||
/** 指南针校准精度等级 (校准对话框进度条). */
|
||||
val compassAccuracy: CompassAccuracy = CompassAccuracy.UNRELIABLE,
|
||||
/** 校正后航向(度, 含磁偏角+手动偏置), 校准对话框实时显示. */
|
||||
@@ -130,6 +137,16 @@ sealed interface SettingsAction {
|
||||
/** Abort an in-flight LoTW sync (wrong button / changed mind). */
|
||||
data object CancelLoTWSync : SettingsAction
|
||||
|
||||
// Logbook (QSO records + LoTW confirmations)
|
||||
data object RefreshLogbook : SettingsAction
|
||||
data class DeleteLogbookRecord(val id: Long) : SettingsAction
|
||||
|
||||
// LoTW upload configuration (certificate + station)
|
||||
data object LoadLoTWUploadStatus : SettingsAction
|
||||
data class ImportLoTWCertificate(val bytes: ByteArray, val password: CharArray) : SettingsAction
|
||||
data object RemoveLoTWCertificate : SettingsAction
|
||||
data class SaveLoTWStation(val station: com.rtbishop.look4sat.core.domain.repository.LoTWStation) : SettingsAction
|
||||
|
||||
// Update checker
|
||||
data object CheckForUpdate : SettingsAction
|
||||
data object DownloadUpdate : SettingsAction
|
||||
|
||||
+75
@@ -37,9 +37,11 @@ import com.rtbishop.look4sat.core.domain.repository.resolveLoTWSyncMode
|
||||
import com.rtbishop.look4sat.core.domain.repository.IWavelogRepository
|
||||
import com.rtbishop.look4sat.core.domain.usecase.IShowToast
|
||||
import com.rtbishop.look4sat.core.domain.utility.VersionComparator
|
||||
import com.rtbishop.look4sat.core.domain.logbook.toConfirmedRecord
|
||||
import com.rtbishop.look4sat.core.presentation.R
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
@@ -50,6 +52,8 @@ class SettingsViewModel(
|
||||
private val updateRepo: IUpdateRepository,
|
||||
private val wavelogRepo: IWavelogRepository,
|
||||
private val lotwRepo: com.rtbishop.look4sat.core.domain.repository.ILoTWRepository,
|
||||
private val qsoRepository: com.rtbishop.look4sat.core.domain.logbook.IQsoRepository,
|
||||
private val lotwUploadRepository: com.rtbishop.look4sat.core.domain.repository.ILoTWUploadRepository,
|
||||
private val sensorsRepo: ISensorsRepo,
|
||||
private val apkFile: File,
|
||||
private val showToast: IShowToast
|
||||
@@ -154,6 +158,11 @@ class SettingsViewModel(
|
||||
_uiState.update { it.copy(lotwSettings = settings) }
|
||||
}
|
||||
}
|
||||
viewModelScope.launch {
|
||||
qsoRepository.records.collect { records ->
|
||||
_uiState.update { it.copy(logbookRecords = records) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -200,6 +209,14 @@ class SettingsViewModel(
|
||||
is SettingsAction.UpdateLoTW -> settingsRepo.updateLoTWSettings(action.settings)
|
||||
is SettingsAction.SyncLoTWGrids -> syncLoTWGrids(action.settings, action.mode)
|
||||
SettingsAction.CancelLoTWSync -> cancelLoTWSync()
|
||||
// Logbook
|
||||
SettingsAction.RefreshLogbook -> refreshLogbook()
|
||||
is SettingsAction.DeleteLogbookRecord -> viewModelScope.launch { qsoRepository.delete(action.id) }
|
||||
// LoTW upload configuration
|
||||
SettingsAction.LoadLoTWUploadStatus -> loadLoTWUploadStatus()
|
||||
is SettingsAction.ImportLoTWCertificate -> importLoTWCertificate(action.bytes, action.password)
|
||||
SettingsAction.RemoveLoTWCertificate -> removeLoTWCertificate()
|
||||
is SettingsAction.SaveLoTWStation -> saveLoTWStation(action.station)
|
||||
// Update checker
|
||||
SettingsAction.CheckForUpdate -> checkForUpdate()
|
||||
SettingsAction.DownloadUpdate -> downloadUpdate()
|
||||
@@ -263,6 +280,13 @@ class SettingsViewModel(
|
||||
// by call + QSO time); full: the fresh report replaces it all.
|
||||
// Shared with the automatic sync on app start (MainApplication).
|
||||
val mergedGridsCount = applyLoTWGridResult(settingsRepo, result, effectiveMode, callsign)
|
||||
// Feed the logbook: imported confirmations show up in 日志本 and
|
||||
// match local uploads (sameConfirmedContact) to mark them confirmed.
|
||||
viewModelScope.launch {
|
||||
val confirmed = result.qsos.values.flatten()
|
||||
.map { it.toConfirmedRecord(callsign) }
|
||||
qsoRepository.mergeLoTW(confirmed)
|
||||
}
|
||||
_uiState.update { state ->
|
||||
state.copy(
|
||||
lotwSyncing = false, lotwSyncMode = null, lotwProgress = null,
|
||||
@@ -312,6 +336,55 @@ class SettingsViewModel(
|
||||
|
||||
// endregion
|
||||
|
||||
// region Logbook + LoTW upload configuration
|
||||
|
||||
private fun refreshLogbook() {
|
||||
viewModelScope.launch {
|
||||
_uiState.update { it.copy(logbookRecords = qsoRepository.records.first()) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadLoTWUploadStatus() {
|
||||
viewModelScope.launch {
|
||||
val cert = lotwUploadRepository.certificate()
|
||||
val station = lotwUploadRepository.station()
|
||||
_uiState.update { it.copy(lotwCertificate = cert, lotwStation = station, lotwUploadBusy = false) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun importLoTWCertificate(bytes: ByteArray, password: CharArray) {
|
||||
viewModelScope.launch {
|
||||
_uiState.update { it.copy(lotwUploadBusy = true) }
|
||||
try {
|
||||
val cert = lotwUploadRepository.importCertificate(bytes, password)
|
||||
_uiState.update { it.copy(lotwCertificate = cert, lotwUploadBusy = false) }
|
||||
} catch (_: Exception) {
|
||||
_uiState.update { it.copy(lotwUploadBusy = false) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeLoTWCertificate() {
|
||||
viewModelScope.launch {
|
||||
lotwUploadRepository.removeCertificate()
|
||||
_uiState.update { it.copy(lotwCertificate = null, lotwStation = null) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveLoTWStation(station: com.rtbishop.look4sat.core.domain.repository.LoTWStation) {
|
||||
viewModelScope.launch {
|
||||
_uiState.update { it.copy(lotwUploadBusy = true) }
|
||||
try {
|
||||
val saved = lotwUploadRepository.saveStation(station)
|
||||
_uiState.update { it.copy(lotwStation = saved, lotwUploadBusy = false) }
|
||||
} catch (_: Exception) {
|
||||
_uiState.update { it.copy(lotwUploadBusy = false) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Update checker
|
||||
|
||||
private fun checkForUpdate() {
|
||||
@@ -447,6 +520,8 @@ class SettingsViewModel(
|
||||
updateRepo = container.updateRepo,
|
||||
wavelogRepo = container.wavelogRepo,
|
||||
lotwRepo = container.lotwRepo,
|
||||
qsoRepository = container.qsoRepository,
|
||||
lotwUploadRepository = container.lotwUploadRepository,
|
||||
sensorsRepo = container.provideSensorsRepo(),
|
||||
apkFile = File(context.cacheDir, "look4sat-update.apk"),
|
||||
showToast = container.provideShowToast()
|
||||
|
||||
Reference in new issue
Block a user