mirror of
https://github.com/atsunatsu/Look4Sat.git
synced 2026-10-03 11:17:25 +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
+99
@@ -46,14 +46,18 @@ import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.GridItemSpan
|
||||
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.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
@@ -80,6 +84,7 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -111,6 +116,34 @@ fun SettingsDestination() {
|
||||
val viewModel: SettingsViewModel = viewModel(factory = SettingsViewModel.factory(container))
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
SettingsScreen(uiState, viewModel::onAction)
|
||||
|
||||
// WaveLog 网格不一致确认弹窗(4.5.2)
|
||||
val gridConfirm = viewModel.gridConfirm
|
||||
if (gridConfirm != null) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { viewModel.resolveGridConfirm(false) },
|
||||
title = { Text(text = stringResource(id = R.string.wavelog_title)) },
|
||||
text = {
|
||||
Text(
|
||||
text = stringResource(
|
||||
id = R.string.wavelog_grid_mismatch,
|
||||
gridConfirm.userGrid.take(4),
|
||||
gridConfirm.stationGrid.take(4)
|
||||
)
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = { viewModel.resolveGridConfirm(true) }) {
|
||||
Text(text = stringResource(id = R.string.wavelog_ignore))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { viewModel.resolveGridConfirm(false) }) {
|
||||
Text(text = stringResource(id = R.string.wavelog_cancel))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -326,6 +359,7 @@ private fun SettingsScreen(uiState: SettingsState, onAction: (SettingsAction) ->
|
||||
)
|
||||
}
|
||||
item { OtherCard(uiState.otherSettings, onAction) }
|
||||
item { WavelogCard(uiState.otherSettings, onAction) }
|
||||
item {
|
||||
UiSettingsCard(
|
||||
hiddenScreens = uiState.otherSettings.hiddenScreens,
|
||||
@@ -558,6 +592,71 @@ private fun OtherCard(settings: OtherSettings, onAction: (SettingsAction) -> Uni
|
||||
* 顺序固定(卫星/过境/雷达/匹配/漫游/地图/设置), 关闭后其余自动靠拢;
|
||||
* "设置"页固定显示(防止失去设置入口)。
|
||||
*/
|
||||
/** WaveLog 日志服务器设置卡片(4.5.2): 照用户截图布局(深色卡片/标签左输入右/按钮行) */
|
||||
@Composable
|
||||
private fun WavelogCard(
|
||||
settings: OtherSettings,
|
||||
onAction: (SettingsAction) -> Unit
|
||||
) {
|
||||
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
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = settings.wavelogUrl,
|
||||
onValueChange = {
|
||||
onAction(SettingsAction.UpdateWavelogSettings(it, settings.wavelogApiKey, settings.wavelogStationId, settings.wavelogAutoUpload))
|
||||
},
|
||||
label = { Text(stringResource(id = R.string.prefs_wavelog_url_hint)) },
|
||||
singleLine = true,
|
||||
textStyle = LocalTextStyle.current.copy(fontSize = 14.sp),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = settings.wavelogApiKey,
|
||||
onValueChange = {
|
||||
onAction(SettingsAction.UpdateWavelogSettings(settings.wavelogUrl, it, settings.wavelogStationId, settings.wavelogAutoUpload))
|
||||
},
|
||||
label = { Text(stringResource(id = R.string.prefs_wavelog_key_hint)) },
|
||||
singleLine = true,
|
||||
textStyle = LocalTextStyle.current.copy(fontSize = 14.sp),
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = settings.wavelogStationId,
|
||||
onValueChange = {
|
||||
onAction(SettingsAction.UpdateWavelogSettings(settings.wavelogUrl, settings.wavelogApiKey, it, settings.wavelogAutoUpload))
|
||||
},
|
||||
label = { Text(stringResource(id = R.string.prefs_wavelog_station_hint)) },
|
||||
singleLine = true,
|
||||
textStyle = LocalTextStyle.current.copy(fontSize = 14.sp),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
SwitchRow(R.string.prefs_wavelog_auto, settings.wavelogAutoUpload) {
|
||||
onAction(SettingsAction.UpdateWavelogSettings(settings.wavelogUrl, settings.wavelogApiKey, settings.wavelogStationId, it))
|
||||
}
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
CardButton(
|
||||
onClick = { onAction(SettingsAction.TestWavelogConnection) },
|
||||
text = stringResource(id = R.string.prefs_wavelog_test),
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
CardButton(
|
||||
onClick = { onAction(SettingsAction.UploadWavelogQueue) },
|
||||
text = stringResource(id = R.string.prefs_wavelog_upload),
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UiSettingsCard(
|
||||
hiddenScreens: List<String>,
|
||||
|
||||
+9
-1
@@ -72,8 +72,16 @@ sealed interface SettingsAction {
|
||||
data object ResetScreenOrder : SettingsAction
|
||||
// UI 设置: 更新主菜单 + 更多菜单顺序(4.5.1 折叠菜单)
|
||||
data class UpdateMenuOrder(val mainOrder: List<String>, val subOrder: List<String>) : SettingsAction
|
||||
// UI 设置: 重置主/子菜单为默认
|
||||
// UI 设置: 重置主菜单 + 更多菜单顺序
|
||||
data object ResetMenuOrder : SettingsAction
|
||||
// WaveLog 日志(4.5.2): 更新服务器配置
|
||||
data class UpdateWavelogSettings(
|
||||
val url: String, val apiKey: String, val stationId: String, val autoUpload: Boolean
|
||||
) : SettingsAction
|
||||
// WaveLog: 测试连接
|
||||
data object TestWavelogConnection : SettingsAction
|
||||
// WaveLog: 手动上传队列
|
||||
data object UploadWavelogQueue : SettingsAction
|
||||
|
||||
// Remote control
|
||||
data class UpdateRC(val settings: RCSettings) : SettingsAction
|
||||
|
||||
+71
-1
@@ -25,11 +25,18 @@ import com.rtbishop.look4sat.core.domain.repository.IDatabaseRepo
|
||||
import com.rtbishop.look4sat.core.domain.repository.IMainContainer
|
||||
import com.rtbishop.look4sat.core.domain.repository.ISettingsRepo
|
||||
import com.rtbishop.look4sat.core.domain.usecase.IShowToast
|
||||
import com.rtbishop.look4sat.core.domain.wavelog.UploadOutcome
|
||||
import com.rtbishop.look4sat.core.domain.wavelog.WaveLogApi
|
||||
import com.rtbishop.look4sat.core.domain.wavelog.WavelogResult
|
||||
import com.rtbishop.look4sat.core.domain.wavelog.WavelogUploader
|
||||
import com.rtbishop.look4sat.core.presentation.R
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
|
||||
class SettingsViewModel(
|
||||
private val databaseRepo: IDatabaseRepo,
|
||||
@@ -138,6 +145,17 @@ 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)
|
||||
is SettingsAction.UpdateWavelogSettings -> settingsRepo.updateOtherSettings {
|
||||
it.copy(
|
||||
wavelogUrl = action.url,
|
||||
wavelogApiKey = action.apiKey,
|
||||
wavelogStationId = action.stationId,
|
||||
wavelogAutoUpload = action.autoUpload
|
||||
)
|
||||
}
|
||||
SettingsAction.TestWavelogConnection -> testWavelogConnection()
|
||||
SettingsAction.UploadWavelogQueue -> uploadWavelogQueue()
|
||||
// System
|
||||
is SettingsAction.ShowToast -> showToast(action.message)
|
||||
}
|
||||
@@ -210,6 +228,58 @@ class SettingsViewModel(
|
||||
}
|
||||
|
||||
|
||||
// endregion
|
||||
|
||||
// region WaveLog(4.5.2)
|
||||
|
||||
private fun testWavelogConnection() {
|
||||
viewModelScope.launch {
|
||||
val s = settingsRepo.otherSettings.value
|
||||
val result = WaveLogApi.testToken(s.wavelogUrl, s.wavelogApiKey)
|
||||
showToast(
|
||||
when (result) {
|
||||
is WavelogResult.Success -> "WaveLog: 连接成功"
|
||||
is WavelogResult.Failure -> "WaveLog: ${result.message}"
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 由 SettingsScreen 注入的共享 uploader(容器级单例)
|
||||
var pendingUploader: WavelogUploader? = null
|
||||
|
||||
// 网格不一致确认弹窗状态(UI 观察后弹 AlertDialog)
|
||||
var gridConfirm by mutableStateOf<GridConfirmData?>(null)
|
||||
|
||||
data class GridConfirmData(val stationGrid: String, val userGrid: String)
|
||||
|
||||
private fun uploadWavelogQueue() {
|
||||
viewModelScope.launch {
|
||||
val result = pendingUploader?.uploadQueue()
|
||||
when (result) {
|
||||
is UploadOutcome.Done -> showToast("WaveLog: ${result.message}")
|
||||
is UploadOutcome.NeedConfirm -> {
|
||||
gridConfirm = GridConfirmData(result.stationGrid, result.userGrid)
|
||||
}
|
||||
null -> showToast("WaveLog: 上传失败")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 网格确认结果: 忽略并上传 / 取消 */
|
||||
fun resolveGridConfirm(ignoreAndUpload: Boolean) {
|
||||
val confirm = gridConfirm ?: return
|
||||
gridConfirm = null
|
||||
if (!ignoreAndUpload) return
|
||||
viewModelScope.launch {
|
||||
val result = pendingUploader?.uploadQueue(force = true)
|
||||
when (result) {
|
||||
is UploadOutcome.Done -> showToast("WaveLog: ${result.message}")
|
||||
else -> showToast("WaveLog: 上传失败")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
companion object {
|
||||
@@ -220,7 +290,7 @@ class SettingsViewModel(
|
||||
databaseRepo = container.databaseRepo,
|
||||
settingsRepo = container.settingsRepo,
|
||||
showToast = container.provideShowToast()
|
||||
)
|
||||
).apply { pendingUploader = container.provideWavelogUploader() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user