mirror of
https://github.com/atsunatsu/Look4Sat.git
synced 2026-10-02 03:15:37 +00:00
fix(wavelog,radar): 4.5.2 user-test fixes (404 upload, swipe delete, transponder picker, log page)
Background: user tested 4.5.2 and reported 4 issues. Same-version
overwrite per user (4.5.2 exists solely for the logbook system).
Fixes:
1. Upload 404 — root cause: user server URL ending in /index.php was
concatenated again (/index.php/index.php/api/v2/...) -> 404. Now:
- normalizeUrl strips trailing /index.php
- every request tries the index.php path first, falls back to the
rewritten path on 404
- 409 conflict (duplicate QSO) counts as success (moves out of queue)
- failure messages include the actual URL + HTTP code for debugging
2. Swipe-to-delete was dead: rowWidth was never measured (0) so the
75% threshold was 0 and the drag was clamped to 0. Now measured via
onSizeChanged + smooth spring/tween snap-back animation.
3. Transponder picker: long card list replaced with an
ExposedDropdownMenuBox dropdown (scrollable menu, pick one).
4. New Log page under the More menu: table view (time / frequency /
satellite / callsign / uploaded checkmark). WavelogQso gains
uploaded flag; uploader marks instead of removing; queue keeps
uploaded entries (500 cap). Old persisted subMenuOrder gets
WavelogLog appended (migration).
Verified: check_strings.py OK (8 files, 452/4.5.2);
:app:compileDebugKotlin BUILD SUCCESSFUL.
This commit is contained in:
1 parent
916a0d0f0a
commit
5351afe508
13 files changed
+407
-113
No files matched your search
@@ -9,6 +9,10 @@
|
||||
*/
|
||||
package com.rtbishop.look4sat.feature.radar
|
||||
|
||||
import androidx.compose.animation.core.Spring
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
|
||||
@@ -20,6 +24,7 @@ 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.ui.layout.onSizeChanged
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
@@ -29,8 +34,11 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
||||
import androidx.compose.material3.MenuAnchorType
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
@@ -51,6 +59,7 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
@@ -68,6 +77,7 @@ import kotlin.math.roundToInt
|
||||
|
||||
private val WaveLogYellow = Color(0xFFFFC107)
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun LogTab(
|
||||
transceivers: List<SatRadio>,
|
||||
@@ -97,46 +107,58 @@ fun LogTab(
|
||||
)
|
||||
}
|
||||
|
||||
// 转发器选择: 点开一个, 边看频率边填日志
|
||||
transceivers.forEach { radio ->
|
||||
val isExpanded = radio.uuid == selectedUuid
|
||||
Card(
|
||||
// 转发器选择(下拉选择框): 选中后底下出现输入区
|
||||
var menuExpanded by remember { mutableStateOf(false) }
|
||||
var selectedRadio by remember { mutableStateOf<SatRadio?>(null) }
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = menuExpanded,
|
||||
onExpandedChange = { menuExpanded = it }
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = selectedRadio?.info ?: "",
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
label = { Text(stringResource(id = R.string.wavelog_select_transponder)) },
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = menuExpanded) },
|
||||
textStyle = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier
|
||||
.menuAnchor(MenuAnchorType.PrimaryNotEditable)
|
||||
.fillMaxWidth()
|
||||
.clickable { selectedUuid = if (isExpanded) null else radio.uuid },
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = if (isExpanded) MaterialTheme.colorScheme.surfaceContainerHighest
|
||||
else MaterialTheme.colorScheme.surfaceContainer
|
||||
)
|
||||
)
|
||||
ExposedDropdownMenu(
|
||||
expanded = menuExpanded,
|
||||
onDismissRequest = { menuExpanded = false }
|
||||
) {
|
||||
Column(modifier = Modifier.padding(horizontal = 10.dp, vertical = 8.dp)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = radio.info,
|
||||
fontSize = 13.sp,
|
||||
fontWeight = if (isExpanded) FontWeight.Bold else FontWeight.Normal,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Text(
|
||||
text = if (isExpanded) "▾" else "▸",
|
||||
fontSize = 14.sp,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
if (isExpanded) {
|
||||
ExpandedLogInput(
|
||||
radio = radio,
|
||||
orbitalPos = orbitalPos,
|
||||
satelliteName = satelliteName,
|
||||
queue = queue,
|
||||
showToast = showToast,
|
||||
onSaved = { refreshTick++ }
|
||||
)
|
||||
}
|
||||
transceivers.forEach { radio ->
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
text = radio.info,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
selectedRadio = radio
|
||||
menuExpanded = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 选中转发器后: 输入区(实时频率 + 呼号 + 模式)
|
||||
selectedRadio?.let { radio ->
|
||||
ExpandedLogInput(
|
||||
radio = radio,
|
||||
orbitalPos = orbitalPos,
|
||||
satelliteName = satelliteName,
|
||||
queue = queue,
|
||||
showToast = showToast,
|
||||
onSaved = { refreshTick++ }
|
||||
)
|
||||
}
|
||||
|
||||
// 本地记录列表(时间/频率/呼号)
|
||||
Text(
|
||||
text = stringResource(id = R.string.wavelog_saved),
|
||||
@@ -275,7 +297,7 @@ private fun ExpandedLogInput(
|
||||
* 5 秒倒计时内点撤销恢复, 不点自动删除。
|
||||
*/
|
||||
@Composable
|
||||
private fun SwipeDeleteRow(
|
||||
internal fun SwipeDeleteRow(
|
||||
onDelete: () -> Unit,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
@@ -285,6 +307,13 @@ private fun SwipeDeleteRow(
|
||||
var rowWidth by remember { mutableIntStateOf(0) }
|
||||
val threshold = rowWidth * 0.75f
|
||||
|
||||
// 平滑弹回/滑入(动画)
|
||||
val animatedOffset by animateFloatAsState(
|
||||
targetValue = offsetX,
|
||||
animationSpec = if (pending) tween(200) else spring(dampingRatio = Spring.DampingRatioNoBouncy),
|
||||
label = "swipeOffset"
|
||||
)
|
||||
|
||||
// 倒计时: 进入待删除后 5 秒自动删
|
||||
LaunchedEffect(pending) {
|
||||
if (pending) {
|
||||
@@ -301,7 +330,11 @@ private fun SwipeDeleteRow(
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.fillMaxWidth()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.onSizeChanged { rowWidth = it.width }
|
||||
) {
|
||||
// 背景层(右侧图标: 垃圾桶 / 撤销+倒计时)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
@@ -348,7 +381,7 @@ private fun SwipeDeleteRow(
|
||||
// 内容层(跟手滑动)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.offset { IntOffset(offsetX.roundToInt(), 0) }
|
||||
.offset { IntOffset(animatedOffset.roundToInt(), 0) }
|
||||
.pointerInput(Unit) {
|
||||
detectHorizontalDragGestures(
|
||||
onDragStart = { if (pending) { pending = false } },
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* WavelogLogScreen.kt — 「日志」页面(更多菜单, 4.5.2 修复)。
|
||||
*
|
||||
* 表格形式展示本地存储的日志: 时间 | 频率 | 卫星 | 呼号 | 已上传(✓)。
|
||||
* 数据来自 WavelogQueue(与雷达页 Log 标签共用)。空态提示; 表格行
|
||||
* 复用左滑删除(同 LogTab 交互)。
|
||||
*/
|
||||
package com.rtbishop.look4sat.feature.radar
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
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.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.rtbishop.look4sat.core.domain.wavelog.WavelogQueue
|
||||
import com.rtbishop.look4sat.core.presentation.R
|
||||
import com.rtbishop.look4sat.core.presentation.formatFrequency
|
||||
import java.util.Calendar
|
||||
import java.util.TimeZone
|
||||
|
||||
private val CheckGreen = Color(0xFF4CAF50)
|
||||
|
||||
@Composable
|
||||
fun WavelogLogScreen(
|
||||
queue: WavelogQueue,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
var refreshTick by remember { mutableIntStateOf(0) }
|
||||
val entries = remember(refreshTick) { queue.all() }
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
// 表头
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 10.dp, vertical = 6.dp)
|
||||
) {
|
||||
HeaderCell(stringResource(id = R.string.wavelog_col_time), 52.dp)
|
||||
HeaderCell(stringResource(id = R.string.wavelog_col_freq), 70.dp)
|
||||
HeaderCell(stringResource(id = R.string.wavelog_col_sat), 0.dp, weight = 1.2f)
|
||||
HeaderCell(stringResource(id = R.string.wavelog_col_call), 0.dp, weight = 1f)
|
||||
HeaderCell(stringResource(id = R.string.wavelog_col_uploaded), 44.dp)
|
||||
}
|
||||
|
||||
if (entries.isEmpty()) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.wavelog_empty),
|
||||
fontSize = 13.sp,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(vertical = 12.dp)
|
||||
)
|
||||
} else {
|
||||
entries.forEach { entry ->
|
||||
SwipeDeleteRow(
|
||||
onDelete = {
|
||||
queue.remove(entry.id)
|
||||
refreshTick++
|
||||
}
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.surfaceContainer)
|
||||
.padding(horizontal = 10.dp, vertical = 8.dp)
|
||||
) {
|
||||
Cell(formatLocalTime(entry.timeUtcMs), 52.dp)
|
||||
Cell(formatFrequency(entry.freqTxHz), 70.dp)
|
||||
Cell(entry.satName, 0.dp, weight = 1.2f)
|
||||
Cell(entry.call, 0.dp, weight = 1f)
|
||||
Box(modifier = Modifier.width(44.dp)) {
|
||||
if (entry.uploaded) {
|
||||
Text(
|
||||
text = "✓",
|
||||
fontSize = 15.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = CheckGreen
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.HeaderCell(text: String, width: androidx.compose.ui.unit.Dp, weight: Float = 0f) {
|
||||
val mod = if (weight > 0f) Modifier.weight(weight) else Modifier.width(width)
|
||||
Text(
|
||||
text = text,
|
||||
fontSize = 11.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
maxLines = 1,
|
||||
modifier = mod
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.Cell(text: String, width: androidx.compose.ui.unit.Dp, weight: Float = 0f) {
|
||||
val mod = if (weight > 0f) Modifier.weight(weight) else Modifier.width(width)
|
||||
Text(
|
||||
text = text,
|
||||
fontSize = 12.sp,
|
||||
maxLines = 1,
|
||||
modifier = mod
|
||||
)
|
||||
}
|
||||
|
||||
private fun formatLocalTime(utcMs: Long): String {
|
||||
val cal = Calendar.getInstance(TimeZone.getDefault())
|
||||
cal.timeInMillis = utcMs
|
||||
return "%02d:%02d".format(cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE))
|
||||
}
|
||||
Reference in new issue
Block a user