Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38cf7a3569 | ||
|
|
760a26b007 | ||
|
|
a7b41c95c7 |
No files matched your search
@@ -17,16 +17,23 @@
|
|||||||
*/
|
*/
|
||||||
package com.rtbishop.look4sat
|
package com.rtbishop.look4sat
|
||||||
|
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.animateContentSize
|
||||||
import androidx.compose.animation.core.LinearEasing
|
import androidx.compose.animation.core.LinearEasing
|
||||||
import androidx.compose.animation.core.RepeatMode
|
import androidx.compose.animation.core.RepeatMode
|
||||||
|
import androidx.compose.animation.core.Spring
|
||||||
import androidx.compose.animation.core.animateFloat
|
import androidx.compose.animation.core.animateFloat
|
||||||
import androidx.compose.animation.core.infiniteRepeatable
|
import androidx.compose.animation.core.infiniteRepeatable
|
||||||
import androidx.compose.animation.core.rememberInfiniteTransition
|
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||||
|
import androidx.compose.animation.core.spring
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.animation.expandVertically
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.animation.scaleIn
|
import androidx.compose.animation.scaleIn
|
||||||
import androidx.compose.animation.scaleOut
|
import androidx.compose.animation.scaleOut
|
||||||
|
import androidx.compose.animation.shrinkVertically
|
||||||
import androidx.compose.animation.slideInHorizontally
|
import androidx.compose.animation.slideInHorizontally
|
||||||
import androidx.compose.animation.slideOutHorizontally
|
import androidx.compose.animation.slideOutHorizontally
|
||||||
import androidx.compose.animation.togetherWith
|
import androidx.compose.animation.togetherWith
|
||||||
@@ -53,6 +60,9 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
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.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
@@ -137,7 +147,7 @@ fun MainScreen(navigateToRadar: () -> Unit = {}) {
|
|||||||
val trackingState by container.radioTrackingService.state.collectAsStateWithLifecycle()
|
val trackingState by container.radioTrackingService.state.collectAsStateWithLifecycle()
|
||||||
val otherSettings by container.settingsRepo.otherSettings.collectAsStateWithLifecycle()
|
val otherSettings by container.settingsRepo.otherSettings.collectAsStateWithLifecycle()
|
||||||
// UI 设置: 按 screenOrder 排序(空 = 默认顺序), 再按 hiddenScreens 过滤(设置页固定保留)
|
// UI 设置: 按 screenOrder 排序(空 = 默认顺序), 再按 hiddenScreens 过滤(设置页固定保留)
|
||||||
val navItems = listOf(Screen.Satellites, Screen.Passes, Screen.Radar, Screen.Mutual, Screen.Roaming, Screen.CwDecode, Screen.Map, Screen.Settings)
|
val allNavItems = listOf(Screen.Satellites, Screen.Passes, Screen.Radar, Screen.Mutual, Screen.Roaming, Screen.CwDecode, Screen.Map, Screen.Settings)
|
||||||
.sortedBy { screen ->
|
.sortedBy { screen ->
|
||||||
// 未知(新页面如 CwDecode 不在旧持久化顺序里): 用默认顺序位置(漫游↔地图), 再兜底最后
|
// 未知(新页面如 CwDecode 不在旧持久化顺序里): 用默认顺序位置(漫游↔地图), 再兜底最后
|
||||||
val idx = otherSettings.screenOrder.indexOf(screen.screenId)
|
val idx = otherSettings.screenOrder.indexOf(screen.screenId)
|
||||||
@@ -147,6 +157,17 @@ fun MainScreen(navigateToRadar: () -> Unit = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.filter { it.screenId !in otherSettings.hiddenScreens || it is Screen.Settings }
|
.filter { it.screenId !in otherSettings.hiddenScreens || it is Screen.Settings }
|
||||||
|
// 4.5.1 折叠菜单: 主菜单(底部栏 5 槽) + 更多菜单(溢出页面)
|
||||||
|
val subOrder = otherSettings.subMenuOrder.ifEmpty { com.rtbishop.look4sat.core.presentation.defaultSubMenuOrder }
|
||||||
|
val mainNavItems = remember(allNavItems, subOrder) {
|
||||||
|
allNavItems.filter { it.screenId !in subOrder }.take(5)
|
||||||
|
}
|
||||||
|
val moreNavItems = remember(allNavItems, subOrder) {
|
||||||
|
subOrder.mapNotNull { id -> allNavItems.find { it.screenId == id } }
|
||||||
|
}
|
||||||
|
var moreExpanded by remember { mutableStateOf(false) }
|
||||||
|
// 更多菜单打开时拦截返回: 先关菜单
|
||||||
|
BackHandler(enabled = moreExpanded) { moreExpanded = false }
|
||||||
// Activity-scoped so the mutual query results survive navigation to Radar and back
|
// Activity-scoped so the mutual query results survive navigation to Radar and back
|
||||||
val mutualViewModel: MutualViewModel = viewModel(
|
val mutualViewModel: MutualViewModel = viewModel(
|
||||||
viewModelStoreOwner = context as ViewModelStoreOwner,
|
viewModelStoreOwner = context as ViewModelStoreOwner,
|
||||||
@@ -161,7 +182,7 @@ fun MainScreen(navigateToRadar: () -> Unit = {}) {
|
|||||||
) {
|
) {
|
||||||
NavigationSuiteScaffold(
|
NavigationSuiteScaffold(
|
||||||
navigationSuiteItems = {
|
navigationSuiteItems = {
|
||||||
navItems.forEach { screen ->
|
mainNavItems.forEach { screen ->
|
||||||
val isSelected = when (currentKey) {
|
val isSelected = when (currentKey) {
|
||||||
is Screen.Satellites -> screen is Screen.Satellites
|
is Screen.Satellites -> screen is Screen.Satellites
|
||||||
is Screen.Passes -> screen is Screen.Passes
|
is Screen.Passes -> screen is Screen.Passes
|
||||||
@@ -178,11 +199,26 @@ fun MainScreen(navigateToRadar: () -> Unit = {}) {
|
|||||||
selected = isSelected,
|
selected = isSelected,
|
||||||
onClick = {
|
onClick = {
|
||||||
if (isSelected) return@item
|
if (isSelected) return@item
|
||||||
|
moreExpanded = false
|
||||||
while (backStack.size > 1) backStack.removeAt(backStack.size - 1)
|
while (backStack.size > 1) backStack.removeAt(backStack.size - 1)
|
||||||
if (screen !is Screen.Passes) backStack.add(screen)
|
if (screen !is Screen.Passes) backStack.add(screen)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
// 更多菜单按钮(固定第 6 槽, 子菜单非空才显示)
|
||||||
|
if (moreNavItems.isNotEmpty()) {
|
||||||
|
item(
|
||||||
|
icon = {
|
||||||
|
Icon(
|
||||||
|
painterResource(com.rtbishop.look4sat.R.drawable.ic_more),
|
||||||
|
stringResource(com.rtbishop.look4sat.core.presentation.R.string.nav_more)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
label = { Text(stringResource(com.rtbishop.look4sat.core.presentation.R.string.nav_more)) },
|
||||||
|
selected = moreExpanded,
|
||||||
|
onClick = { moreExpanded = !moreExpanded }
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
navigationSuiteColors = NavigationSuiteDefaults.colors(
|
navigationSuiteColors = NavigationSuiteDefaults.colors(
|
||||||
navigationRailContainerColor = MaterialTheme.colorScheme.surfaceContainer
|
navigationRailContainerColor = MaterialTheme.colorScheme.surfaceContainer
|
||||||
@@ -193,108 +229,130 @@ fun MainScreen(navigateToRadar: () -> Unit = {}) {
|
|||||||
else -> NavigationSuiteType.ShortNavigationBarMedium
|
else -> NavigationSuiteType.ShortNavigationBarMedium
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Column {
|
Box {
|
||||||
NavDisplay(
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
backStack = backStack,
|
NavDisplay(
|
||||||
modifier = Modifier.weight(1f),
|
backStack = backStack,
|
||||||
onBack = navigateBack,
|
modifier = Modifier.weight(1f).fillMaxWidth(),
|
||||||
transitionSpec = { fadeTransition },
|
onBack = navigateBack,
|
||||||
popTransitionSpec = { fadeTransition },
|
transitionSpec = { fadeTransition },
|
||||||
predictivePopTransitionSpec = { fadeTransition },
|
popTransitionSpec = { fadeTransition },
|
||||||
entryDecorators = listOf(
|
predictivePopTransitionSpec = { fadeTransition },
|
||||||
// Required for saving Compose state per entry
|
entryDecorators = listOf(
|
||||||
rememberSaveableStateHolderNavEntryDecorator(),
|
// Required for saving Compose state per entry
|
||||||
// Required for ViewModel scoping per entry
|
rememberSaveableStateHolderNavEntryDecorator(),
|
||||||
rememberViewModelStoreNavEntryDecorator()
|
// Required for ViewModel scoping per entry
|
||||||
),
|
rememberViewModelStoreNavEntryDecorator()
|
||||||
entryProvider = entryProvider {
|
),
|
||||||
entry<Screen.Satellites> {
|
entryProvider = entryProvider {
|
||||||
SatellitesDestination(navigateUp = navigateBack)
|
entry<Screen.Satellites> {
|
||||||
}
|
SatellitesDestination(navigateUp = navigateBack)
|
||||||
entry<Screen.Passes> {
|
|
||||||
PassesDestination { catNum, aosTime ->
|
|
||||||
container.setMutualPassData(MutualPassData())
|
|
||||||
container.satelliteRepo.selectPass(catNum, aosTime)
|
|
||||||
backStack.add(Screen.Radar)
|
|
||||||
// navigateToRadar()
|
|
||||||
}
|
}
|
||||||
}
|
entry<Screen.Passes> {
|
||||||
entry<Screen.Radar> {
|
PassesDestination { catNum, aosTime ->
|
||||||
RadarDestination(navigateUp = navigateBack)
|
container.setMutualPassData(MutualPassData())
|
||||||
}
|
|
||||||
entry<Screen.Map> {
|
|
||||||
MapDestination()
|
|
||||||
}
|
|
||||||
entry<Screen.Mutual> {
|
|
||||||
MutualScreen(
|
|
||||||
viewModel = mutualViewModel,
|
|
||||||
navigateUp = navigateBack,
|
|
||||||
navigateToRadar = { catNum, aosTime, pass ->
|
|
||||||
container.setMutualPassData(pass ?: MutualPassData())
|
|
||||||
container.satelliteRepo.selectPass(catNum, aosTime)
|
container.satelliteRepo.selectPass(catNum, aosTime)
|
||||||
backStack.add(Screen.Radar)
|
backStack.add(Screen.Radar)
|
||||||
}
|
// navigateToRadar()
|
||||||
)
|
|
||||||
}
|
|
||||||
entry<Screen.Roaming> {
|
|
||||||
RoamingScreen()
|
|
||||||
}
|
|
||||||
entry<Screen.CwDecode> {
|
|
||||||
CwDecodeScreen()
|
|
||||||
}
|
|
||||||
entry<Screen.Settings> {
|
|
||||||
SettingsDestination()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
// Radio tracking status banner
|
|
||||||
if (trackingState.isActive) {
|
|
||||||
val infiniteTransition = rememberInfiniteTransition(label = "trackingPulse")
|
|
||||||
val alpha by infiniteTransition.animateFloat(
|
|
||||||
initialValue = 1f, targetValue = 0.4f,
|
|
||||||
animationSpec = infiniteRepeatable(
|
|
||||||
animation = tween(1000, easing = LinearEasing),
|
|
||||||
repeatMode = RepeatMode.Reverse
|
|
||||||
), label = "pulseAlpha"
|
|
||||||
)
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.background(MaterialTheme.colorScheme.primaryContainer)
|
|
||||||
.clickable {
|
|
||||||
val pass = trackingState.currentPass
|
|
||||||
if (pass != null) {
|
|
||||||
container.setMutualPassData(MutualPassData())
|
|
||||||
container.satelliteRepo.selectPass(pass.catNum, pass.aosTime)
|
|
||||||
backStack.add(Screen.Radar)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(horizontal = 12.dp, vertical = 6.dp)
|
entry<Screen.Radar> {
|
||||||
) {
|
RadarDestination(navigateUp = navigateBack)
|
||||||
Box(
|
}
|
||||||
|
entry<Screen.Map> {
|
||||||
|
MapDestination()
|
||||||
|
}
|
||||||
|
entry<Screen.Mutual> {
|
||||||
|
MutualScreen(
|
||||||
|
viewModel = mutualViewModel,
|
||||||
|
navigateUp = navigateBack,
|
||||||
|
navigateToRadar = { catNum, aosTime, pass ->
|
||||||
|
container.setMutualPassData(pass ?: MutualPassData())
|
||||||
|
container.satelliteRepo.selectPass(catNum, aosTime)
|
||||||
|
backStack.add(Screen.Radar)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
entry<Screen.Roaming> {
|
||||||
|
RoamingScreen()
|
||||||
|
}
|
||||||
|
entry<Screen.CwDecode> {
|
||||||
|
CwDecodeScreen()
|
||||||
|
}
|
||||||
|
entry<Screen.Settings> {
|
||||||
|
SettingsDestination()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
// Radio tracking status banner
|
||||||
|
if (trackingState.isActive) {
|
||||||
|
val infiniteTransition = rememberInfiniteTransition(label = "trackingPulse")
|
||||||
|
val alpha by infiniteTransition.animateFloat(
|
||||||
|
initialValue = 1f, targetValue = 0.4f,
|
||||||
|
animationSpec = infiniteRepeatable(
|
||||||
|
animation = tween(1000, easing = LinearEasing),
|
||||||
|
repeatMode = RepeatMode.Reverse
|
||||||
|
), label = "pulseAlpha"
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(8.dp)
|
.fillMaxWidth()
|
||||||
.clip(CircleShape)
|
.background(MaterialTheme.colorScheme.primaryContainer)
|
||||||
.background(Color(0xFF4CAF50).copy(alpha = alpha))
|
.clickable {
|
||||||
)
|
val pass = trackingState.currentPass
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
if (pass != null) {
|
||||||
Text(
|
container.setMutualPassData(MutualPassData())
|
||||||
text = "Tracking: ${trackingState.currentPass?.name ?: ""}",
|
container.satelliteRepo.selectPass(pass.catNum, pass.aosTime)
|
||||||
fontSize = 13.sp,
|
backStack.add(Screen.Radar)
|
||||||
fontWeight = FontWeight.Medium,
|
}
|
||||||
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
}
|
||||||
modifier = Modifier.weight(1f)
|
.padding(horizontal = 12.dp, vertical = 6.dp)
|
||||||
)
|
) {
|
||||||
val txOk = if (trackingState.txConnected) "TX" else ""
|
Box(
|
||||||
val rxOk = if (trackingState.rxConnected) "RX" else ""
|
modifier = Modifier
|
||||||
Text(
|
.size(8.dp)
|
||||||
text = listOf(txOk, rxOk).filter { it.isNotBlank() }.joinToString("/"),
|
.clip(CircleShape)
|
||||||
fontSize = 12.sp,
|
.background(Color(0xFF4CAF50).copy(alpha = alpha))
|
||||||
color = MaterialTheme.colorScheme.onPrimaryContainer
|
)
|
||||||
)
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(com.rtbishop.look4sat.core.presentation.R.string.tracking_status, trackingState.currentPass?.name ?: ""),
|
||||||
|
fontSize = 13.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
val txOk = if (trackingState.txConnected) "TX" else ""
|
||||||
|
val rxOk = if (trackingState.rxConnected) "RX" else ""
|
||||||
|
Text(
|
||||||
|
text = listOf(txOk, rxOk).filter { it.isNotBlank() }.joinToString("/"),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 更多菜单弹出面板(覆盖在内容上, 底部栏上方; spring 弹跳)
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = moreExpanded,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
enter = expandVertically(
|
||||||
|
animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy)
|
||||||
|
) + fadeIn(),
|
||||||
|
exit = shrinkVertically() + fadeOut()
|
||||||
|
) {
|
||||||
|
MoreMenuPopup(
|
||||||
|
items = moreNavItems,
|
||||||
|
currentKey = currentKey,
|
||||||
|
onDismiss = { moreExpanded = false },
|
||||||
|
onSelect = { screen ->
|
||||||
|
moreExpanded = false
|
||||||
|
while (backStack.size > 1) backStack.removeAt(backStack.size - 1)
|
||||||
|
if (screen !is Screen.Passes) backStack.add(screen)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
/*
|
||||||
|
* MoreMenuPopup.kt — 底部导航「更多」二级菜单弹出面板(4.5.1)。
|
||||||
|
*
|
||||||
|
* 覆盖在内容区上(底部栏上方, 右对齐), 竖排菜单项(图标+文本+箭头),
|
||||||
|
* 当前页高亮; 点击遮罩关闭, 点击项跳转。弹出/收起动画由调用处
|
||||||
|
* (MainScreen 的 AnimatedVisibility + spring)驱动。
|
||||||
|
*/
|
||||||
|
package com.rtbishop.look4sat
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
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.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
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 androidx.navigation3.runtime.NavKey
|
||||||
|
import com.rtbishop.look4sat.core.presentation.Screen
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MoreMenuPopup(
|
||||||
|
items: List<Screen>,
|
||||||
|
currentKey: NavKey?,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onSelect: (Screen) -> Unit
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(MaterialTheme.colorScheme.scrim.copy(alpha = 0.35f))
|
||||||
|
.clickable(onClick = onDismiss)
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomEnd)
|
||||||
|
.padding(12.dp),
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(vertical = 4.dp)) {
|
||||||
|
items.forEach { screen ->
|
||||||
|
val isSelected = when (currentKey) {
|
||||||
|
is Screen.Satellites -> screen is Screen.Satellites
|
||||||
|
is Screen.Passes -> screen is Screen.Passes
|
||||||
|
is Screen.Radar -> screen is Screen.Radar
|
||||||
|
is Screen.Mutual -> screen is Screen.Mutual
|
||||||
|
is Screen.CwDecode -> screen is Screen.CwDecode
|
||||||
|
is Screen.Map -> screen is Screen.Map
|
||||||
|
is Screen.Settings -> screen is Screen.Settings
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { onSelect(screen) }
|
||||||
|
.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(screen.iconResId),
|
||||||
|
contentDescription = stringResource(screen.titleResId),
|
||||||
|
tint = if (isSelected) MaterialTheme.colorScheme.primary
|
||||||
|
else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(screen.titleResId),
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,
|
||||||
|
color = if (isSelected) MaterialTheme.colorScheme.primary
|
||||||
|
else MaterialTheme.colorScheme.onSurface,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "›",
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z" />
|
||||||
|
</vector>
|
||||||
@@ -65,8 +65,10 @@ data class OtherSettings(
|
|||||||
val highElevation: Double = 45.0,
|
val highElevation: Double = 45.0,
|
||||||
// UI 设置: 底部导航栏隐藏的页面(Screen simpleName 列表, 默认空 = 全部显示)
|
// UI 设置: 底部导航栏隐藏的页面(Screen simpleName 列表, 默认空 = 全部显示)
|
||||||
val hiddenScreens: List<String> = emptyList(),
|
val hiddenScreens: List<String> = emptyList(),
|
||||||
// UI 设置: 页面顺序(空 = 默认顺序: 卫星/过境/雷达/匹配/漫游/地图/设置)
|
// UI 设置: 页面顺序(空 = 默认顺序: 卫星/过境/雷达/地图/设置)
|
||||||
val screenOrder: List<String> = emptyList()
|
val screenOrder: List<String> = emptyList(),
|
||||||
|
// UI 设置: 更多菜单顺序(空 = 默认: 匹配/漫游/CW解码)
|
||||||
|
val subMenuOrder: List<String> = emptyList()
|
||||||
)
|
)
|
||||||
|
|
||||||
data class DataSourcesSettings(
|
data class DataSourcesSettings(
|
||||||
|
|||||||
+5
-2
@@ -48,8 +48,11 @@ sealed class Screen(val iconResId: Int, val titleResId: Int, val screenId: Strin
|
|||||||
data object Settings : Screen(R.drawable.ic_settings, R.string.nav_prefs, "Settings")
|
data object Settings : Screen(R.drawable.ic_settings, R.string.nav_prefs, "Settings")
|
||||||
}
|
}
|
||||||
|
|
||||||
// UI 设置: 默认页面顺序(与 MainScreen navItems 一致)
|
// UI 设置: 默认主菜单页面顺序(底部栏 5 槽)
|
||||||
val defaultScreenOrder = listOf("Satellites", "Passes", "Radar", "Mutual", "Roaming", "CwDecode", "Map", "Settings")
|
val defaultScreenOrder = listOf("Satellites", "Passes", "Radar", "Map", "Settings")
|
||||||
|
|
||||||
|
// UI 设置: 默认更多菜单顺序(进「更多」的页面)
|
||||||
|
val defaultSubMenuOrder = listOf("Mutual", "Roaming", "CwDecode")
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data object RadarDestination : NavKey
|
data object RadarDestination : NavKey
|
||||||
|
|||||||
@@ -0,0 +1,210 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">Look4Sat Pro</string>
|
||||||
|
<string name="btn_accept">Terima</string>
|
||||||
|
<string name="btn_cancel">Batal</string>
|
||||||
|
<string name="empty_list_message">Tidak ada data untuk ditampilkan</string>
|
||||||
|
<string name="nav_sat">Satelit</string>
|
||||||
|
<string name="nav_pass">Lintasan</string>
|
||||||
|
<string name="nav_radar">Radar</string>
|
||||||
|
<string name="nav_mutual">Pasangan</string>
|
||||||
|
<string name="nav_roaming">Jelajah</string>
|
||||||
|
<string name="nav_cw">Dekoder CW</string>
|
||||||
|
<string name="nav_map">Peta</string>
|
||||||
|
<string name="nav_prefs">Pengaturan</string>
|
||||||
|
<string name="nav_more">Lainnya</string>
|
||||||
|
<string name="sat_type_hint">Jenis: %s</string>
|
||||||
|
<string name="sat_type_title">Pilih jenis satelit</string>
|
||||||
|
<string name="sat_search_hint">Id - Nama</string>
|
||||||
|
<string name="sat_search_clear">Bersihkan</string>
|
||||||
|
<string name="sat_clear_all">Bersihkan semua</string>
|
||||||
|
<string name="sat_select_all">Pilih semua</string>
|
||||||
|
<string name="sat_empty_list_message">Pastikan kata kunci pencarian Anda benar dan DB sudah diperbarui</string>
|
||||||
|
<string name="sat_warning_title">Peringatan\!</string>
|
||||||
|
<string name="sat_warning_message">
|
||||||
|
Ada lebih dari 9000 satelit yang terdaftar di aplikasi ini.
|
||||||
|
Tidak masuk akal untuk melacak semuanya sekaligus.
|
||||||
|
\n\nSelalu coba persempit daftar hanya ke satelit yang
|
||||||
|
Anda minati melalui pencarian dan pemilih jenis.</string>
|
||||||
|
<string name="pass_filter_title">Filter lintasan</string>
|
||||||
|
<string name="pass_filter_elev">Elevasi minimal</string>
|
||||||
|
<string name="pass_filter_hours">Waktu ke depan</string>
|
||||||
|
<string name="pass_filter_aos_time">Jendela AOS</string>
|
||||||
|
<string name="pass_filter_invert_time">Balik jendela AOS</string>
|
||||||
|
<string name="pass_filter_deep_space">DeepSpace (periode >225 mnt)</string>
|
||||||
|
<string name="pass_time_placeholder"> -- : -- : -- </string>
|
||||||
|
<string name="pass_modes_title">Pilih jenis modulasi</string>
|
||||||
|
<string name="pass_satId">%05d</string>
|
||||||
|
<string name="pass_elevation">Elevasi: %.1f°</string>
|
||||||
|
<string name="pass_deep_space">DeepSpace</string>
|
||||||
|
<string name="pass_altitude">Ketinggian: %d km</string>
|
||||||
|
<string name="pass_aosLos">%03d° - %03d°</string>
|
||||||
|
<string name="pass_empty_list_message">
|
||||||
|
Pastikan filter Anda benar dan Anda sudah memilih satelit</string>
|
||||||
|
<string name="pass_error_message">Tidak ada lintasan satelit mendatang untuk ditampilkan</string>
|
||||||
|
<string name="pass_welcome_title">Selamat datang di Look4Sat\!</string>
|
||||||
|
<string name="pass_welcome_message">
|
||||||
|
Pastikan Anda mengatur posisi melalui GPS, Lat/Lon atau QTH di Pengaturan.
|
||||||
|
\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>
|
||||||
|
<string name="radar_back">Kembali</string>
|
||||||
|
<string name="radar_notify">Beri tahu</string>
|
||||||
|
<string name="radar_az_text">Azimuth</string>
|
||||||
|
<string name="radar_az_value">%.1f°</string>
|
||||||
|
<string name="radar_el_text">Elevasi</string>
|
||||||
|
<string name="radar_el_value">%.1f°</string>
|
||||||
|
<string name="radar_alt_text">Ketinggian</string>
|
||||||
|
<string name="radar_alt_value">%.0f km</string>
|
||||||
|
<string name="radar_dist_text">Jarak</string>
|
||||||
|
<string name="radar_dist_value">%.0f km</string>
|
||||||
|
<string name="radar_eclipsed">Gerhana</string>
|
||||||
|
<string name="radar_downlink">Downlink</string>
|
||||||
|
<string name="radar_uplink">Uplink</string>
|
||||||
|
<string name="radar_link_low">%.4f</string>
|
||||||
|
<string name="radar_link_lowHigh">%.4f - %.4f</string>
|
||||||
|
<string name="radar_no_link">- - . - -</string>
|
||||||
|
<string name="radar_no_data">Satelit ini tidak memiliki transceiver</string>
|
||||||
|
<string name="radar_add">Tambah</string>
|
||||||
|
<string name="radar_data">Transceiver satelit:</string>
|
||||||
|
<string name="radar_inverted">Terbalik: %s</string>
|
||||||
|
<string name="radar_mode">Mode: %s</string>
|
||||||
|
<string name="radar_string_no">Tidak</string>
|
||||||
|
<string name="radar_string_yes">Ya</string>
|
||||||
|
<string name="radar_visible">Terlihat</string>
|
||||||
|
<string name="radar_doppler_calc">Kalkulator Frekuensi Doppler</string>
|
||||||
|
<string name="radar_doppler_tx_hint">Masukkan frekuensi TX (MHz)</string>
|
||||||
|
<string name="radar_doppler_rx_hint">Masukkan frekuensi RX (MHz)</string>
|
||||||
|
<string name="radar_doppler_offset_hint">Offset (kHz)</string>
|
||||||
|
<string name="radar_doppler_info">Untuk transponder linier, ketik satu frekuensi untuk melihat frekuensi lainnya</string>
|
||||||
|
<string name="radar_cw_decoder">Dekoder CW</string>
|
||||||
|
<string name="radar_cw_start">Mulai</string>
|
||||||
|
<string name="radar_cw_stop">Berhenti</string>
|
||||||
|
<string name="radar_cw_reset">Bersihkan</string>
|
||||||
|
<string name="map_prev">Sebelumnya</string>
|
||||||
|
<string name="map_next">Berikutnya</string>
|
||||||
|
<string name="map_copyright">© Kontributor OpenStreetMap</string>
|
||||||
|
<string name="map_azimuth">Azimuth: %.1f°</string>
|
||||||
|
<string name="map_elevation">Elevasi: %.1f°</string>
|
||||||
|
<string name="map_altitude">Ketinggian: %.0f km</string>
|
||||||
|
<string name="map_distance">Jarak: %.0f km</string>
|
||||||
|
<string name="map_latitude">Lintang: %.1f°</string>
|
||||||
|
<string name="map_longitude">Bujur: %.1f°</string>
|
||||||
|
<string name="map_qth">QTH: %s</string>
|
||||||
|
<string name="map_phase">Fase: %.1f°</string>
|
||||||
|
<string name="map_eclipsed">Gerhana</string>
|
||||||
|
<string name="map_period">Periode: %.0f mnt</string>
|
||||||
|
<string name="map_velocity">Kecepatan: %.2f km/d</string>
|
||||||
|
<string name="map_visibility">Visibilitas: %s</string>
|
||||||
|
<string name="map_visible">Terlihat</string>
|
||||||
|
<string name="prefs_app_title">Look4Sat Pro v%s</string>
|
||||||
|
<string name="prefs_app_url">https://play.google.com/store/apps/details?id=com.rtbishop.look4sat</string>
|
||||||
|
<string name="prefs_donate_title">Donasi</string>
|
||||||
|
<string name="prefs_donate_url">https://ko-fi.com/rt_bishop</string>
|
||||||
|
<string name="prefs_fdroid_title">F-Droid</string>
|
||||||
|
<string name="prefs_fdroid_url">https://f-droid.org/en/packages/com.rtbishop.look4sat/</string>
|
||||||
|
<string name="prefs_github_title">GitHub</string>
|
||||||
|
<string name="prefs_github_url">https://github.com/rt-bishop/Look4Sat/</string>
|
||||||
|
<string name="prefs_license_title">GPLv3</string>
|
||||||
|
<string name="prefs_license_url">https://www.gnu.org/licenses/gpl-3.0.html</string>
|
||||||
|
<string name="prefs_privacy_title">Kebijakan Privasi</string>
|
||||||
|
<string name="prefs_privacy_url">https://sites.google.com/view/look4sat-privacy-policy/home</string>
|
||||||
|
<string name="prefs_updated_title">Diperbarui: %s</string>
|
||||||
|
<string name="prefs_updated_time">d MMM yyyy - HH:mm:ss</string>
|
||||||
|
<string name="prefs_loc_title">Posisi stasiun</string>
|
||||||
|
<string name="prefs_loc_gps_title">GPS</string>
|
||||||
|
<string name="prefs_loc_gps_error">Periksa izin lokasi Anda</string>
|
||||||
|
<string name="prefs_loc_input_title">Input</string>
|
||||||
|
<string name="prefs_loc_input_error">Lokasi yang dimasukkan tidak valid</string>
|
||||||
|
<string name="prefs_loc_qth_title">QTH</string>
|
||||||
|
<string name="prefs_loc_qth_error">Lokator QTH tidak valid</string>
|
||||||
|
<string name="prefs_loc_success">Posisi berhasil diperbarui</string>
|
||||||
|
<string name="prefs_station_title">Pengaturan posisi stasiun</string>
|
||||||
|
<string name="prefs_station_lat_text">Atur garis lintang stasiun Anda</string>
|
||||||
|
<string name="prefs_station_lon_text">Atur garis bujur stasiun Anda</string>
|
||||||
|
<string name="prefs_locator_title">Pengaturan lokator QTH</string>
|
||||||
|
<string name="prefs_locator_text">Atur posisi stasiun menggunakan lokator</string>
|
||||||
|
<string name="prefs_data_title">Data satelit</string>
|
||||||
|
<string name="prefs_data_entries">Satelit: %s</string>
|
||||||
|
<string name="prefs_data_radios">Transceiver: %s</string>
|
||||||
|
<string name="prefs_data_update">Perbarui</string>
|
||||||
|
<string name="prefs_data_import">URL Kustom</string>
|
||||||
|
<string name="prefs_data_clear">Bersihkan</string>
|
||||||
|
<string name="prefs_data_clear_success">Data berhasil dibersihkan</string>
|
||||||
|
<string name="prefs_data_update_success">Pembaruan berhasil diselesaikan</string>
|
||||||
|
<string name="prefs_data_import_satellites_error">Tidak ada satelit yang diimpor. Pilih file TLE/3LE (.txt) atau OMM (.csv) yang valid.</string>
|
||||||
|
<string name="prefs_data_import_transceivers_error">Tidak ada transceiver yang diimpor. Pilih file SatNOGS (.json) yang valid.</string>
|
||||||
|
<string name="prefs_data_sources_title">Sumber data kustom</string>
|
||||||
|
<string name="prefs_data_sources_tle_switch">URL TLE Kustom</string>
|
||||||
|
<string name="prefs_data_sources_transceivers_switch">URL transceiver kustom</string>
|
||||||
|
<string name="prefs_data_sources_url_title">URL (HTTPS)</string>
|
||||||
|
<string name="prefs_data_reset_url">Setel ulang ke URL bawaan</string>
|
||||||
|
<string name="prefs_data_output_title">Keluaran data</string>
|
||||||
|
<string name="prefs_net_output">Jaringan</string>
|
||||||
|
<string name="prefs_bt_output">Bluetooth</string>
|
||||||
|
<string name="prefs_cat_output">CAT</string>
|
||||||
|
<string name="nav_radiocontrol">Kontrol Radio</string>
|
||||||
|
<string name="rc_settings_title">Kontrol Radio CAT</string>
|
||||||
|
<string name="rc_radio_model">Model Radio</string>
|
||||||
|
<string name="rc_tx_device_hint">Alamat BT Radio TX</string>
|
||||||
|
<string name="rc_rx_device_hint">Alamat BT Radio RX</string>
|
||||||
|
<string name="rc_tx_name_hint">Nama Radio TX</string>
|
||||||
|
<string name="rc_rx_name_hint">Nama Radio RX</string>
|
||||||
|
<string name="rc_enable_switch">Aktifkan Kontrol CAT</string>
|
||||||
|
<string name="prefs_net_title">Keluaran data jaringan</string>
|
||||||
|
<string name="prefs_net_rotator_switch">Aktifkan keluaran rotator</string>
|
||||||
|
<string name="prefs_net_rotator_address_hint">IP:Port</string>
|
||||||
|
<string name="prefs_net_rotator_format_hint">Format</string>
|
||||||
|
<string name="prefs_net_frequency_switch">Aktifkan keluaran frekuensi</string>
|
||||||
|
<string name="prefs_net_frequency_address_hint">IP:Port</string>
|
||||||
|
<string name="prefs_net_frequency_format_hint">Format</string>
|
||||||
|
<string name="prefs_bt_title">Keluaran data Bluetooth</string>
|
||||||
|
<string name="prefs_bt_rotator_switch">Aktifkan keluaran rotator</string>
|
||||||
|
<string name="prefs_bt_rotator_device_hint">Id perangkat</string>
|
||||||
|
<string name="prefs_bt_rotator_output_hint">Format data</string>
|
||||||
|
<string name="prefs_bt_frequency_switch">Aktifkan keluaran frekuensi</string>
|
||||||
|
<string name="prefs_bt_frequency_device_hint">Id perangkat</string>
|
||||||
|
<string name="prefs_bt_frequency_output_hint">Format data</string>
|
||||||
|
<string name="prefs_bt_perm_error">Periksa izin bluetooth Anda</string>
|
||||||
|
<string name="prefs_net_perm_error">Periksa izin jaringan Anda</string>
|
||||||
|
<string name="tracking_status">Melacak: %s</string>
|
||||||
|
<string name="loc_lat_text">Lintang: %s°</string>
|
||||||
|
<string name="loc_lon_text">Bujur: %s°</string>
|
||||||
|
<string name="loc_qth_text">QTH: %s</string>
|
||||||
|
<string name="connect_radio">Hubungkan</string>
|
||||||
|
<string name="disconnect_radio">Putuskan</string>
|
||||||
|
<string name="track_radio">Lacak</string>
|
||||||
|
<string name="stop_radio">Berhenti</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>
|
||||||
|
<string name="prefs_ui_main_title">Menu utama (bilah bawah)</string>
|
||||||
|
<string name="prefs_ui_sub_title">Menu lainnya</string>
|
||||||
|
<string name="prefs_ui_move_out">Pindah ke menu lainnya</string>
|
||||||
|
<string name="prefs_ui_move_back">Kembalikan ke menu utama</string>
|
||||||
|
<string name="prefs_ui_order_locked">Tetap (tidak dapat dipindahkan)</string>
|
||||||
|
<string name="prefs_ui_order_reset">Setel ulang urutan</string>
|
||||||
|
<string name="prefs_other_switch_utc">Tampilkan waktu lintasan dalam UTC</string>
|
||||||
|
<string name="prefs_other_switch_update">Aktifkan pembaruan data otomatis</string>
|
||||||
|
<string name="prefs_other_switch_sweep">Aktifkan animasi sapuan radar</string>
|
||||||
|
<string name="prefs_other_switch_sensors">Gunakan sensor untuk memutar tampilan radar</string>
|
||||||
|
<string name="prefs_other_switch_night_mode">Aktifkan filter mode malam merah</string>
|
||||||
|
<string name="prefs_highlight_title">Sorotan elevasi</string>
|
||||||
|
<string name="prefs_highlight_low">Rendah < %1$d°</string>
|
||||||
|
<string name="prefs_highlight_mid">Sedang %1$d–%2$d°</string>
|
||||||
|
<string name="prefs_highlight_high">Tinggi > %1$d°</string>
|
||||||
|
<string name="prefs_outro_title">BG7NTA & penulis asli mengucapkan terima kasih</string>
|
||||||
|
<string name="prefs_outro_thanks">
|
||||||
|
* Pengguna dan kontributor Look4Sat!
|
||||||
|
\n* David A. B. Johnson (predict4java)
|
||||||
|
\n* Dave Moten (predict4java)
|
||||||
|
\n* Alexandru Csete (Gpredict)
|
||||||
|
\n* Dr T.S. Kelso (Celestrak)
|
||||||
|
\n* Libre Space Foundation (SatNOGS)
|
||||||
|
\n* xdsopl dan kontributor Robot36!
|
||||||
|
\n* BA7OPF (fitur pencocokan lintasan)
|
||||||
|
\n* BG7NTA</string>
|
||||||
|
<string name="prefs_outro_license">Aplikasi ini hadir tanpa jaminan</string>
|
||||||
|
</resources>
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
<string name="nav_cw">CW Dekoder</string>
|
<string name="nav_cw">CW Dekoder</string>
|
||||||
<string name="nav_map">Harita</string>
|
<string name="nav_map">Harita</string>
|
||||||
<string name="nav_prefs">Ayarlar</string>
|
<string name="nav_prefs">Ayarlar</string>
|
||||||
|
<string name="nav_more">Daha Fazla</string>
|
||||||
|
|
||||||
<!-- Satellites screen -->
|
<!-- Satellites screen -->
|
||||||
<string name="sat_type_hint">Uydu türü: %s</string>
|
<string name="sat_type_hint">Uydu türü: %s</string>
|
||||||
@@ -52,8 +53,8 @@
|
|||||||
\n\nDoğru tahminler alabilmek için veritabanını en az haftada bir güncelleyin.</string>
|
\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_title" translatable="false">What\'s new in Look4Sat</string>
|
||||||
<string name="pass_whatsnew_message" translatable="false">
|
<string name="pass_whatsnew_message" translatable="false">
|
||||||
* Transponder CW kod çözücü paneli artık geliştirilmiş kod çözme motorunu canlı şelale (waterfall) görüntüsüyle kullanıyor
|
* Alt gezinme artık 5 ana sekme artı Daha Fazla menüsü kullanıyor (katlanabilir ikinci düzey menü)
|
||||||
* CW kod çözme sayfaları aynı motoru paylaşıyor: gezinme sayfası ve transponder paneli</string>
|
* UI Ayarları: sayfa sırası ana menü ve daha fazla menü olarak ayrıldı; sayfalar arasında taşıma yapılabilir</string>
|
||||||
|
|
||||||
<!-- Radar screen -->
|
<!-- Radar screen -->
|
||||||
<string name="radar_back">Geri</string>
|
<string name="radar_back">Geri</string>
|
||||||
@@ -178,9 +179,22 @@
|
|||||||
<string name="prefs_bt_perm_error">Bluetooth izninizi kontrol edin</string>
|
<string name="prefs_bt_perm_error">Bluetooth izninizi kontrol edin</string>
|
||||||
<string name="prefs_net_perm_error">Ağ izninizi kontrol edin</string>
|
<string name="prefs_net_perm_error">Ağ izninizi kontrol edin</string>
|
||||||
|
|
||||||
|
<string name="tracking_status">İzleniyor: %s</string>
|
||||||
|
<string name="loc_lat_text">Enlem: %s°</string>
|
||||||
|
<string name="loc_lon_text">Boylam: %s°</string>
|
||||||
|
<string name="loc_qth_text">QTH: %s</string>
|
||||||
|
<string name="connect_radio">Bağlan</string>
|
||||||
|
<string name="disconnect_radio">Bağlantıyı Kes</string>
|
||||||
|
<string name="track_radio">Takip</string>
|
||||||
|
<string name="stop_radio">Dur</string>
|
||||||
<string name="prefs_other_title">Diğer ayarlar</string>
|
<string name="prefs_other_title">Diğer ayarlar</string>
|
||||||
<string name="prefs_ui_title">UI Ayarları</string>
|
<string name="prefs_ui_title">UI Ayarları</string>
|
||||||
<string name="prefs_ui_order_title">Sayfa sırası</string>
|
<string name="prefs_ui_order_title">Sayfa sırası</string>
|
||||||
|
<string name="prefs_ui_main_title">Ana menü (alt çubuk)</string>
|
||||||
|
<string name="prefs_ui_sub_title">Daha fazla menü</string>
|
||||||
|
<string name="prefs_ui_move_out">Daha fazla menüye taşı</string>
|
||||||
|
<string name="prefs_ui_move_back">Ana menüye geri taşı</string>
|
||||||
|
<string name="prefs_ui_order_locked">Sabit (taşınamaz)</string>
|
||||||
<string name="prefs_ui_order_reset">Sıralamayı sıfırla</string>
|
<string name="prefs_ui_order_reset">Sıralamayı sıfırla</string>
|
||||||
<string name="prefs_other_switch_utc">Geçiş saatini UTC olarak göster</string>
|
<string name="prefs_other_switch_utc">Geçiş saatini UTC olarak göster</string>
|
||||||
<string name="prefs_other_switch_update">Otomatik veri güncellemeyi etkinleştir</string>
|
<string name="prefs_other_switch_update">Otomatik veri güncellemeyi etkinleştir</string>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
<string name="nav_cw">CW解码</string>
|
<string name="nav_cw">CW解码</string>
|
||||||
<string name="nav_map">地图</string>
|
<string name="nav_map">地图</string>
|
||||||
<string name="nav_prefs">设置</string>
|
<string name="nav_prefs">设置</string>
|
||||||
|
<string name="nav_more">更多</string>
|
||||||
|
|
||||||
<!-- Satellites screen -->
|
<!-- Satellites screen -->
|
||||||
<string name="sat_type_hint">类型:%s</string>
|
<string name="sat_type_hint">类型:%s</string>
|
||||||
@@ -42,8 +43,8 @@
|
|||||||
<string name="pass_welcome_message">请务必在设置中通过GPS、经纬度或QTH定位您的位置\n建议至少每周更新一次数据库,以确保预测结果的准确性</string>
|
<string name="pass_welcome_message">请务必在设置中通过GPS、经纬度或QTH定位您的位置\n建议至少每周更新一次数据库,以确保预测结果的准确性</string>
|
||||||
<string name="pass_whatsnew_title">Look4Sat Pro 更新内容</string>
|
<string name="pass_whatsnew_title">Look4Sat Pro 更新内容</string>
|
||||||
<string name="pass_whatsnew_message" translatable="false">
|
<string name="pass_whatsnew_message" translatable="false">
|
||||||
* 转发器 CW 解码面板已接入增强解码引擎,带实时瀑布图显示
|
* 底部导航改为 5 个主标签 + 更多菜单(可折叠二级菜单)
|
||||||
* CW 解码入口共用同一引擎:导航页与转发器面板</string>
|
* UI 设置: 页面顺序分为主菜单与更多菜单,可拖拽或移动页面</string>
|
||||||
|
|
||||||
<!-- Radar screen -->
|
<!-- Radar screen -->
|
||||||
<string name="radar_back">后退</string>
|
<string name="radar_back">后退</string>
|
||||||
@@ -161,9 +162,22 @@
|
|||||||
<string name="rc_rx_name_hint">接收电台名称</string>
|
<string name="rc_rx_name_hint">接收电台名称</string>
|
||||||
<string name="rc_enable_switch">启用 CAT 控制</string>
|
<string name="rc_enable_switch">启用 CAT 控制</string>
|
||||||
|
|
||||||
|
<string name="tracking_status">跟踪: %s</string>
|
||||||
|
<string name="loc_lat_text">纬度: %s°</string>
|
||||||
|
<string name="loc_lon_text">经度: %s°</string>
|
||||||
|
<string name="loc_qth_text">QTH: %s</string>
|
||||||
|
<string name="connect_radio">连接</string>
|
||||||
|
<string name="disconnect_radio">断开</string>
|
||||||
|
<string name="track_radio">跟踪</string>
|
||||||
|
<string name="stop_radio">停止</string>
|
||||||
<string name="prefs_other_title">其他设置</string>
|
<string name="prefs_other_title">其他设置</string>
|
||||||
<string name="prefs_ui_title">UI设置</string>
|
<string name="prefs_ui_title">UI设置</string>
|
||||||
<string name="prefs_ui_order_title">页面顺序</string>
|
<string name="prefs_ui_order_title">页面顺序</string>
|
||||||
|
<string name="prefs_ui_main_title">主菜单(底部栏)</string>
|
||||||
|
<string name="prefs_ui_sub_title">更多菜单</string>
|
||||||
|
<string name="prefs_ui_move_out">移入更多菜单</string>
|
||||||
|
<string name="prefs_ui_move_back">移回主菜单</string>
|
||||||
|
<string name="prefs_ui_order_locked">固定(不可移动)</string>
|
||||||
<string name="prefs_ui_order_reset">重置顺序</string>
|
<string name="prefs_ui_order_reset">重置顺序</string>
|
||||||
<string name="prefs_other_switch_utc">以 UTC 时间显示</string>
|
<string name="prefs_other_switch_utc">以 UTC 时间显示</string>
|
||||||
<string name="prefs_other_switch_update">启用卫星数据自动更新</string>
|
<string name="prefs_other_switch_update">启用卫星数据自动更新</string>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
<string name="nav_cw">CW Decoder</string>
|
<string name="nav_cw">CW Decoder</string>
|
||||||
<string name="nav_map">Map</string>
|
<string name="nav_map">Map</string>
|
||||||
<string name="nav_prefs">Settings</string>
|
<string name="nav_prefs">Settings</string>
|
||||||
|
<string name="nav_more">More</string>
|
||||||
|
|
||||||
<!-- Satellites screen -->
|
<!-- Satellites screen -->
|
||||||
<string name="sat_type_hint">Type: %s</string>
|
<string name="sat_type_hint">Type: %s</string>
|
||||||
@@ -54,8 +55,8 @@
|
|||||||
\n\nPlease update the database at least weekly to get accurate predictions.</string>
|
\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_title" translatable="false">What\'s new in Look4Sat</string>
|
||||||
<string name="pass_whatsnew_message" translatable="false">
|
<string name="pass_whatsnew_message" translatable="false">
|
||||||
* Transponder CW decoder panel now uses the enhanced decoding engine with a live waterfall display
|
* Bottom navigation now uses 5 main tabs plus a More menu (collapsible second-level menu)
|
||||||
* CW decoding pages share the same engine: navigation page and transponder panel</string>
|
* UI Settings: page order split into main menu and more menu; drag or move pages between them</string>
|
||||||
|
|
||||||
<!-- Radar screen -->
|
<!-- Radar screen -->
|
||||||
<string name="radar_back">Back</string>
|
<string name="radar_back">Back</string>
|
||||||
@@ -192,9 +193,22 @@
|
|||||||
<string name="prefs_bt_perm_error">Check your bluetooth permission</string>
|
<string name="prefs_bt_perm_error">Check your bluetooth permission</string>
|
||||||
<string name="prefs_net_perm_error">Check your network permission</string>
|
<string name="prefs_net_perm_error">Check your network permission</string>
|
||||||
|
|
||||||
|
<string name="tracking_status">Tracking: %s</string>
|
||||||
|
<string name="loc_lat_text">Lat: %s°</string>
|
||||||
|
<string name="loc_lon_text">Lon: %s°</string>
|
||||||
|
<string name="loc_qth_text">Qth: %s</string>
|
||||||
|
<string name="connect_radio">Connect</string>
|
||||||
|
<string name="disconnect_radio">Disconnect</string>
|
||||||
|
<string name="track_radio">Track</string>
|
||||||
|
<string name="stop_radio">Stop</string>
|
||||||
<string name="prefs_other_title">Other settings</string>
|
<string name="prefs_other_title">Other settings</string>
|
||||||
<string name="prefs_ui_title">UI Settings</string>
|
<string name="prefs_ui_title">UI Settings</string>
|
||||||
<string name="prefs_ui_order_title">Page order</string>
|
<string name="prefs_ui_order_title">Page order</string>
|
||||||
|
<string name="prefs_ui_main_title">Main menu (bottom bar)</string>
|
||||||
|
<string name="prefs_ui_sub_title">More menu</string>
|
||||||
|
<string name="prefs_ui_move_out">Move to more menu</string>
|
||||||
|
<string name="prefs_ui_move_back">Move back to main menu</string>
|
||||||
|
<string name="prefs_ui_order_locked">Fixed (cannot be moved)</string>
|
||||||
<string name="prefs_ui_order_reset">Reset order</string>
|
<string name="prefs_ui_order_reset">Reset order</string>
|
||||||
<string name="prefs_other_switch_utc">Show pass time in UTC</string>
|
<string name="prefs_other_switch_utc">Show pass time in UTC</string>
|
||||||
<string name="prefs_other_switch_update">Enable automatic data update</string>
|
<string name="prefs_other_switch_update">Enable automatic data update</string>
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ fun CwDecodeScreen(navigateUp: () -> Unit = {}) {
|
|||||||
) {
|
) {
|
||||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
Text(
|
Text(
|
||||||
text = "Microphone permission is required for CW decoding",
|
text = stringResource(R.string.cw_mic_permission),
|
||||||
color = Color.White,
|
color = Color.White,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
modifier = Modifier.padding(horizontal = 24.dp)
|
modifier = Modifier.padding(horizontal = 24.dp)
|
||||||
@@ -190,7 +190,7 @@ fun CwDecodeScreen(navigateUp: () -> Unit = {}) {
|
|||||||
Button(onClick = {
|
Button(onClick = {
|
||||||
permissionLauncher.launch(Manifest.permission.RECORD_AUDIO)
|
permissionLauncher.launch(Manifest.permission.RECORD_AUDIO)
|
||||||
}) {
|
}) {
|
||||||
Text("Grant permission")
|
Text(stringResource(R.string.cw_grant_permission))
|
||||||
}
|
}
|
||||||
if (permanentlyDenied) {
|
if (permanentlyDenied) {
|
||||||
// 勾选了"不再询问": 引导去系统设置开启
|
// 勾选了"不再询问": 引导去系统设置开启
|
||||||
@@ -202,7 +202,7 @@ fun CwDecodeScreen(navigateUp: () -> Unit = {}) {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}) {
|
}) {
|
||||||
Text("Open app settings")
|
Text(stringResource(R.string.cw_open_settings))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="purple_500">#ff6200ee</color>
|
||||||
|
<integer name="default_font_size">18</integer>
|
||||||
|
<string name="idle">Diam</string>
|
||||||
|
<string name="waterfall_display">Tampilan waterfall</string>
|
||||||
|
<string name="frequency_scale">Skala frekuensi</string>
|
||||||
|
<string name="decoded_text">Teks terdekode</string>
|
||||||
|
<string name="clear">Bersihkan</string>
|
||||||
|
<string name="pause">Jeda Dekode</string>
|
||||||
|
<string name="save_text">Simpan Teks</string>
|
||||||
|
<string name="record_signals">Rekam Sinyal</string>
|
||||||
|
<string name="settings">Pengaturan</string>
|
||||||
|
<string name="help">Bantuan Daring</string>
|
||||||
|
<string name="rate">Nilai Aplikasi Ini</string>
|
||||||
|
<string name="premium">Dapatkan Versi Premium</string>
|
||||||
|
<string name="tap_back_again_to_close">Ketuk Kembali lagi untuk menutup aplikasi</string>
|
||||||
|
<string name="message_type">Tipe Pesan</string>
|
||||||
|
<string name="cw_mic_permission">Izin mikrofon diperlukan untuk dekode CW</string>
|
||||||
|
<string name="cw_grant_permission">Beri izin</string>
|
||||||
|
<string name="cw_open_settings">Buka pengaturan aplikasi</string>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="purple_500">#ff6200ee</color>
|
||||||
|
<integer name="default_font_size">18</integer>
|
||||||
|
<string name="idle">Beklemede</string>
|
||||||
|
<string name="waterfall_display">Şelale görüntüsü</string>
|
||||||
|
<string name="frequency_scale">Frekans ölçeği</string>
|
||||||
|
<string name="decoded_text">Çözülen metin</string>
|
||||||
|
<string name="clear">Temizle</string>
|
||||||
|
<string name="pause">Çözmeyi Duraklat</string>
|
||||||
|
<string name="save_text">Metni Kaydet</string>
|
||||||
|
<string name="record_signals">Sinyalleri Kaydet</string>
|
||||||
|
<string name="settings">Ayarlar</string>
|
||||||
|
<string name="help">Çevrimiçi Yardım</string>
|
||||||
|
<string name="rate">Bu Uygulamayı Değerlendir</string>
|
||||||
|
<string name="premium">Premium Sürümü Al</string>
|
||||||
|
<string name="tap_back_again_to_close">Kapatmak için Geriye tekrar dokunun</string>
|
||||||
|
<string name="message_type">Mesaj Türü</string>
|
||||||
|
<string name="cw_mic_permission">CW çözme için mikrofon izni gereklidir</string>
|
||||||
|
<string name="cw_grant_permission">İzin ver</string>
|
||||||
|
<string name="cw_open_settings">Uygulama ayarlarını aç</string>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="purple_500">#ff6200ee</color>
|
||||||
|
<integer name="default_font_size">18</integer>
|
||||||
|
<string name="idle">空闲</string>
|
||||||
|
<string name="waterfall_display">瀑布图</string>
|
||||||
|
<string name="frequency_scale">频率刻度</string>
|
||||||
|
<string name="decoded_text">解码文本</string>
|
||||||
|
<string name="clear">清除</string>
|
||||||
|
<string name="pause">暂停解码</string>
|
||||||
|
<string name="save_text">保存文本</string>
|
||||||
|
<string name="record_signals">录音信号</string>
|
||||||
|
<string name="settings">设置</string>
|
||||||
|
<string name="help">在线帮助</string>
|
||||||
|
<string name="rate">评价此应用</string>
|
||||||
|
<string name="premium">获取高级版</string>
|
||||||
|
<string name="tap_back_again_to_close">再次按返回键退出应用</string>
|
||||||
|
<string name="message_type">消息类型</string>
|
||||||
|
<string name="cw_mic_permission">CW 解码需要麦克风权限</string>
|
||||||
|
<string name="cw_grant_permission">授予权限</string>
|
||||||
|
<string name="cw_open_settings">打开应用设置</string>
|
||||||
|
</resources>
|
||||||
@@ -16,4 +16,7 @@
|
|||||||
<string name="premium">Get Premium Version</string>
|
<string name="premium">Get Premium Version</string>
|
||||||
<string name="tap_back_again_to_close">Tap Back again to close the app</string>
|
<string name="tap_back_again_to_close">Tap Back again to close the app</string>
|
||||||
<string name="message_type">Message Type</string>
|
<string name="message_type">Message Type</string>
|
||||||
|
<string name="cw_mic_permission">Microphone permission is required for CW decoding</string>
|
||||||
|
<string name="cw_grant_permission">Grant permission</string>
|
||||||
|
<string name="cw_open_settings">Open app settings</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -44,6 +44,7 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.OutlinedButton
|
import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Slider
|
import androidx.compose.material3.Slider
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
@@ -57,6 +58,7 @@ import androidx.compose.ui.res.painterResource
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.rtbishop.look4sat.core.domain.repository.MutualPassData
|
import com.rtbishop.look4sat.core.domain.repository.MutualPassData
|
||||||
@@ -94,22 +96,29 @@ fun MutualScreen(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
bottomInfo = {
|
bottomInfo = {
|
||||||
if (state.mutualPasses.isNotEmpty()) {
|
Text(
|
||||||
Text(
|
text = mutualStatusText(state),
|
||||||
text = "找到 ${state.mutualPasses.size} 个匹配",
|
style = MaterialTheme.typography.bodySmall,
|
||||||
style = MaterialTheme.typography.bodySmall,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
maxLines = 1,
|
||||||
)
|
overflow = TextOverflow.Ellipsis,
|
||||||
}
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
},
|
},
|
||||||
endAction = {
|
endAction = {
|
||||||
if (state.isCalculating) {
|
Row(
|
||||||
CircularProgressIndicator(
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier
|
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
.height(24.dp)
|
) {
|
||||||
.width(24.dp),
|
if (state.isCalculating) {
|
||||||
strokeWidth = 2.dp
|
CircularProgressIndicator(
|
||||||
)
|
modifier = Modifier
|
||||||
|
.height(24.dp)
|
||||||
|
.width(24.dp),
|
||||||
|
strokeWidth = 2.dp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
MutualStatusChip(state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -187,8 +196,6 @@ private fun MutualContent(
|
|||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
verticalArrangement = Arrangement.spacedBy(6.dp)
|
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
) {
|
) {
|
||||||
MatchIntroCard()
|
|
||||||
|
|
||||||
if (isVertical) {
|
if (isVertical) {
|
||||||
StationInputCard(
|
StationInputCard(
|
||||||
title = "你的位置",
|
title = "你的位置",
|
||||||
@@ -304,25 +311,50 @@ private fun MutualContent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun MatchIntroCard(modifier: Modifier = Modifier) {
|
private fun MutualStatusChip(state: MutualUiState) {
|
||||||
ElevatedCard(modifier = modifier.fillMaxWidth()) {
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
Column(
|
val containerColor = when {
|
||||||
modifier = Modifier.padding(12.dp),
|
state.isCalculating -> colorScheme.primaryContainer
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
state.errorMessage != null -> colorScheme.errorContainer
|
||||||
) {
|
state.mutualPasses.isNotEmpty() -> colorScheme.primary
|
||||||
Text(
|
state.hasSearched -> colorScheme.surfaceVariant
|
||||||
text = "位置匹配",
|
else -> colorScheme.surfaceVariant
|
||||||
style = MaterialTheme.typography.titleSmall,
|
|
||||||
fontWeight = FontWeight.Medium,
|
|
||||||
color = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = "输入双方位置,查找共同可见的卫星过境窗口",
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
val contentColor = when {
|
||||||
|
state.isCalculating -> colorScheme.onPrimaryContainer
|
||||||
|
state.errorMessage != null -> colorScheme.onErrorContainer
|
||||||
|
state.mutualPasses.isNotEmpty() -> colorScheme.onPrimary
|
||||||
|
else -> colorScheme.onSurfaceVariant
|
||||||
|
}
|
||||||
|
Surface(
|
||||||
|
color = containerColor,
|
||||||
|
contentColor = contentColor,
|
||||||
|
shape = MaterialTheme.shapes.small
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = mutualStatusChipText(state),
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
maxLines = 1,
|
||||||
|
modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mutualStatusText(state: MutualUiState): String = when {
|
||||||
|
state.isCalculating -> "正在计算双方共同可见窗口…"
|
||||||
|
state.errorMessage != null -> "请修正提示后重新查询"
|
||||||
|
state.mutualPasses.isNotEmpty() -> "找到 ${state.mutualPasses.size} 个匹配 · 点击卡片展开曲线"
|
||||||
|
state.hasSearched -> "未找到共同可见窗口,调整时间或仰角后重试"
|
||||||
|
else -> "输入双方位置后点击查询"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mutualStatusChipText(state: MutualUiState): String = when {
|
||||||
|
state.isCalculating -> "计算中"
|
||||||
|
state.errorMessage != null -> "有错误"
|
||||||
|
state.mutualPasses.isNotEmpty() -> "${state.mutualPasses.size}个匹配"
|
||||||
|
state.hasSearched -> "无匹配"
|
||||||
|
else -> "待查询"
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
+10
-1
@@ -49,6 +49,7 @@ data class MutualUiState(
|
|||||||
val hoursAhead: Int = 24,
|
val hoursAhead: Int = 24,
|
||||||
val mutualPasses: List<MutualPass> = emptyList(),
|
val mutualPasses: List<MutualPass> = emptyList(),
|
||||||
val isCalculating: Boolean = false,
|
val isCalculating: Boolean = false,
|
||||||
|
val hasSearched: Boolean = false,
|
||||||
val selectedPassIndex: Int = -1,
|
val selectedPassIndex: Int = -1,
|
||||||
val errorMessage: String? = null
|
val errorMessage: String? = null
|
||||||
)
|
)
|
||||||
@@ -167,7 +168,15 @@ class MutualViewModel(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_uiState.update { it.copy(isCalculating = true, errorMessage = null, mutualPasses = emptyList()) }
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
isCalculating = true,
|
||||||
|
hasSearched = true,
|
||||||
|
errorMessage = null,
|
||||||
|
mutualPasses = emptyList(),
|
||||||
|
selectedPassIndex = -1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val time = System.currentTimeMillis()
|
val time = System.currentTimeMillis()
|
||||||
|
|||||||
+17
-3
@@ -38,6 +38,7 @@ import kotlinx.coroutines.flow.update
|
|||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Calendar
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.TimeZone
|
import java.util.TimeZone
|
||||||
@@ -162,7 +163,10 @@ class PassesViewModel(
|
|||||||
return SimpleDateFormat(pattern, Locale.getDefault()).also { it.timeZone = tz }
|
return SimpleDateFormat(pattern, Locale.getDefault()).also { it.timeZone = tz }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Computes sunrise/sunset strings for each unique calendar day in the pass list, plus today for DeepSpace
|
// Computes sunrise/sunset strings for each unique calendar day in the pass list, plus today for DeepSpace.
|
||||||
|
// CelestialComputer.findSunRiseSet() returns the next rise/set after the supplied time, so pass
|
||||||
|
// the calendar day's 00:00 in the same timezone used by the visible date group. Passing a pass's
|
||||||
|
// AOS time can jump to the following day's sunrise/sunset when that AOS is after the local event.
|
||||||
private fun computeSunTimes(passes: List<OrbitalPass>, isUtc: Boolean): Map<String, Pair<String, String>> {
|
private fun computeSunTimes(passes: List<OrbitalPass>, isUtc: Boolean): Map<String, Pair<String, String>> {
|
||||||
val stationPos = settingsRepo.stationPosition.value
|
val stationPos = settingsRepo.stationPosition.value
|
||||||
val tz = if (isUtc) TimeZone.getTimeZone("UTC") else TimeZone.getDefault()
|
val tz = if (isUtc) TimeZone.getTimeZone("UTC") else TimeZone.getDefault()
|
||||||
@@ -171,7 +175,7 @@ class PassesViewModel(
|
|||||||
val result = LinkedHashMap<String, Pair<String, String>>()
|
val result = LinkedHashMap<String, Pair<String, String>>()
|
||||||
// DeepSpace group always shows today's sun times
|
// DeepSpace group always shows today's sun times
|
||||||
if (passes.any { it.isDeepSpace }) {
|
if (passes.any { it.isDeepSpace }) {
|
||||||
val riseSet = CelestialComputer.findSunRiseSet(stationPos, System.currentTimeMillis())
|
val riseSet = CelestialComputer.findSunRiseSet(stationPos, startOfDayMillis(System.currentTimeMillis(), tz))
|
||||||
val rise = if (riseSet.riseTimeMillis > 0) sdfTime.format(Date(riseSet.riseTimeMillis)) else "--:--"
|
val rise = if (riseSet.riseTimeMillis > 0) sdfTime.format(Date(riseSet.riseTimeMillis)) else "--:--"
|
||||||
val set = if (riseSet.setTimeMillis > 0) sdfTime.format(Date(riseSet.setTimeMillis)) else "--:--"
|
val set = if (riseSet.setTimeMillis > 0) sdfTime.format(Date(riseSet.setTimeMillis)) else "--:--"
|
||||||
result["DeepSpace (period >225min)"] = rise to set
|
result["DeepSpace (period >225min)"] = rise to set
|
||||||
@@ -180,7 +184,7 @@ class PassesViewModel(
|
|||||||
if (pass.isDeepSpace) continue
|
if (pass.isDeepSpace) continue
|
||||||
val label = sdfDate.format(Date(pass.aosTime))
|
val label = sdfDate.format(Date(pass.aosTime))
|
||||||
if (label in result) continue
|
if (label in result) continue
|
||||||
val riseSet = CelestialComputer.findSunRiseSet(stationPos, pass.aosTime)
|
val riseSet = CelestialComputer.findSunRiseSet(stationPos, startOfDayMillis(pass.aosTime, tz))
|
||||||
val rise = if (riseSet.riseTimeMillis > 0) sdfTime.format(Date(riseSet.riseTimeMillis)) else "--:--"
|
val rise = if (riseSet.riseTimeMillis > 0) sdfTime.format(Date(riseSet.riseTimeMillis)) else "--:--"
|
||||||
val set = if (riseSet.setTimeMillis > 0) sdfTime.format(Date(riseSet.setTimeMillis)) else "--:--"
|
val set = if (riseSet.setTimeMillis > 0) sdfTime.format(Date(riseSet.setTimeMillis)) else "--:--"
|
||||||
result[label] = rise to set
|
result[label] = rise to set
|
||||||
@@ -188,6 +192,16 @@ class PassesViewModel(
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun startOfDayMillis(timeMillis: Long, tz: TimeZone): Long {
|
||||||
|
return Calendar.getInstance(tz, Locale.getDefault()).apply {
|
||||||
|
timeInMillis = timeMillis
|
||||||
|
set(Calendar.HOUR_OF_DAY, 0)
|
||||||
|
set(Calendar.MINUTE, 0)
|
||||||
|
set(Calendar.SECOND, 0)
|
||||||
|
set(Calendar.MILLISECOND, 0)
|
||||||
|
}.timeInMillis
|
||||||
|
}
|
||||||
|
|
||||||
private fun groupPasses(passes: List<OrbitalPass>, isUtc: Boolean): Map<String, List<OrbitalPass>> {
|
private fun groupPasses(passes: List<OrbitalPass>, isUtc: Boolean): Map<String, List<OrbitalPass>> {
|
||||||
val tz = if (isUtc) TimeZone.getTimeZone("UTC") else TimeZone.getDefault()
|
val tz = if (isUtc) TimeZone.getTimeZone("UTC") else TimeZone.getDefault()
|
||||||
val sdfDate = dateFormat(tz)
|
val sdfDate = dateFormat(tz)
|
||||||
|
|||||||
@@ -461,19 +461,19 @@ private fun ExpandedRadioControl(
|
|||||||
if (!radioControl.txPanel.isConnected && !radioControl.rxPanel.isConnected) {
|
if (!radioControl.txPanel.isConnected && !radioControl.rxPanel.isConnected) {
|
||||||
CardButton(
|
CardButton(
|
||||||
onClick = { onAction(RadarAction.ConnectRadios) },
|
onClick = { onAction(RadarAction.ConnectRadios) },
|
||||||
text = "Connect",
|
text = stringResource(R.string.connect_radio),
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
CardButton(
|
CardButton(
|
||||||
onClick = { onAction(RadarAction.DisconnectRadios) },
|
onClick = { onAction(RadarAction.DisconnectRadios) },
|
||||||
text = "Disconnect",
|
text = stringResource(R.string.disconnect_radio),
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
CardButton(
|
CardButton(
|
||||||
onClick = { onAction(RadarAction.ToggleTracking) },
|
onClick = { onAction(RadarAction.ToggleTracking) },
|
||||||
text = if (radioControl.isTracking) "Stop" else "Track",
|
text = if (radioControl.isTracking) stringResource(R.string.stop_radio) else stringResource(R.string.track_radio),
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+131
-58
@@ -33,6 +33,7 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.heightIn
|
import androidx.compose.foundation.layout.heightIn
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
@@ -96,6 +97,8 @@ import com.rtbishop.look4sat.core.presentation.R
|
|||||||
import com.rtbishop.look4sat.core.presentation.ScreenColumn
|
import com.rtbishop.look4sat.core.presentation.ScreenColumn
|
||||||
import com.rtbishop.look4sat.core.presentation.TopBar
|
import com.rtbishop.look4sat.core.presentation.TopBar
|
||||||
import com.rtbishop.look4sat.core.presentation.infiniteMarquee
|
import com.rtbishop.look4sat.core.presentation.infiniteMarquee
|
||||||
|
import com.rtbishop.look4sat.core.presentation.defaultScreenOrder
|
||||||
|
import com.rtbishop.look4sat.core.presentation.defaultSubMenuOrder
|
||||||
import com.rtbishop.look4sat.core.presentation.isVerticalLayout
|
import com.rtbishop.look4sat.core.presentation.isVerticalLayout
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
@@ -327,9 +330,11 @@ private fun SettingsScreen(uiState: SettingsState, onAction: (SettingsAction) ->
|
|||||||
UiSettingsCard(
|
UiSettingsCard(
|
||||||
hiddenScreens = uiState.otherSettings.hiddenScreens,
|
hiddenScreens = uiState.otherSettings.hiddenScreens,
|
||||||
screenOrder = uiState.otherSettings.screenOrder,
|
screenOrder = uiState.otherSettings.screenOrder,
|
||||||
|
subMenuOrder = uiState.otherSettings.subMenuOrder,
|
||||||
onToggle = { name -> onAction(SettingsAction.ToggleScreen(name)) },
|
onToggle = { name -> onAction(SettingsAction.ToggleScreen(name)) },
|
||||||
onReorder = { order -> onAction(SettingsAction.ReorderScreens(order)) },
|
onReorder = { order -> onAction(SettingsAction.ReorderScreens(order)) },
|
||||||
onResetOrder = { onAction(SettingsAction.ResetScreenOrder) }
|
onResetOrder = { onAction(SettingsAction.ResetMenuOrder) },
|
||||||
|
onUpdateMenu = { main, sub -> onAction(SettingsAction.UpdateMenuOrder(main, sub)) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
item { CardCredits() }
|
item { CardCredits() }
|
||||||
@@ -370,11 +375,11 @@ private fun LocationCard(
|
|||||||
Text(text = formatUpdateTime(updateTime = settings.stationPos.timestamp))
|
Text(text = formatUpdateTime(updateTime = settings.stationPos.timestamp))
|
||||||
Spacer(modifier = Modifier.height(2.dp))
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
|
Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
|
||||||
Text(text = "Lat: ${settings.stationPos.latitude}°")
|
Text(text = stringResource(R.string.loc_lat_text, settings.stationPos.latitude))
|
||||||
Text(text = "Lon: ${settings.stationPos.longitude}°")
|
Text(text = stringResource(R.string.loc_lon_text, settings.stationPos.longitude))
|
||||||
}
|
}
|
||||||
Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
|
Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
|
||||||
Text(text = "Qth: ${settings.stationPos.qthLocator}")
|
Text(text = stringResource(R.string.loc_qth_text, settings.stationPos.qthLocator))
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(1.dp))
|
Spacer(modifier = Modifier.height(1.dp))
|
||||||
Row(horizontalArrangement = Arrangement.SpaceEvenly) {
|
Row(horizontalArrangement = Arrangement.SpaceEvenly) {
|
||||||
@@ -557,9 +562,11 @@ private fun OtherCard(settings: OtherSettings, onAction: (SettingsAction) -> Uni
|
|||||||
private fun UiSettingsCard(
|
private fun UiSettingsCard(
|
||||||
hiddenScreens: List<String>,
|
hiddenScreens: List<String>,
|
||||||
screenOrder: List<String>,
|
screenOrder: List<String>,
|
||||||
|
subMenuOrder: List<String>,
|
||||||
onToggle: (String) -> Unit,
|
onToggle: (String) -> Unit,
|
||||||
onReorder: (List<String>) -> Unit,
|
onReorder: (List<String>) -> Unit,
|
||||||
onResetOrder: () -> Unit
|
onResetOrder: () -> Unit,
|
||||||
|
onUpdateMenu: (List<String>, List<String>) -> Unit
|
||||||
) {
|
) {
|
||||||
val screens = listOf(
|
val screens = listOf(
|
||||||
R.string.nav_sat to "Satellites",
|
R.string.nav_sat to "Satellites",
|
||||||
@@ -571,6 +578,25 @@ private fun UiSettingsCard(
|
|||||||
R.string.nav_map to "Map",
|
R.string.nav_map to "Map",
|
||||||
R.string.nav_prefs to "Settings"
|
R.string.nav_prefs to "Settings"
|
||||||
) // name 必须与 Screen.screenId 一致 (R8 安全)
|
) // name 必须与 Screen.screenId 一致 (R8 安全)
|
||||||
|
// 主菜单项: 排除更多菜单项, Settings 固定最后
|
||||||
|
val mainItems = remember(screens, screenOrder, subMenuOrder) {
|
||||||
|
val sub = subMenuOrder.ifEmpty { defaultSubMenuOrder }
|
||||||
|
screens.map { it.second }
|
||||||
|
.filter { it !in sub }
|
||||||
|
.sortedBy { name ->
|
||||||
|
screenOrder.indexOf(name).let { idx ->
|
||||||
|
if (idx != -1) idx else defaultScreenOrder.indexOf(name).let {
|
||||||
|
if (it != -1) it else Int.MAX_VALUE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sortedWith(compareBy { if (it == "Settings") 1 else 0 })
|
||||||
|
}
|
||||||
|
// 更多菜单项
|
||||||
|
val subItems = remember(screens, subMenuOrder) {
|
||||||
|
val sub = subMenuOrder.ifEmpty { defaultSubMenuOrder }
|
||||||
|
sub.filter { s -> screens.any { (_, name) -> name == s } }
|
||||||
|
}
|
||||||
ElevatedCard(modifier = Modifier.fillMaxWidth()) {
|
ElevatedCard(modifier = Modifier.fillMaxWidth()) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
|
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||||
@@ -594,7 +620,45 @@ private fun UiSettingsCard(
|
|||||||
text = stringResource(id = R.string.prefs_ui_order_title),
|
text = stringResource(id = R.string.prefs_ui_order_title),
|
||||||
color = MaterialTheme.colorScheme.primary
|
color = MaterialTheme.colorScheme.primary
|
||||||
)
|
)
|
||||||
DragOrderList(screens = screens, screenOrder = screenOrder, onReorder = onReorder)
|
// 主菜单区(底部栏, 最多 5 项, 设置锁定最后)
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.prefs_ui_main_title),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
DragOrderList(
|
||||||
|
items = mainItems,
|
||||||
|
labels = screens,
|
||||||
|
locked = setOf("Settings"),
|
||||||
|
moveLabel = stringResource(id = R.string.prefs_ui_move_out),
|
||||||
|
moveEnabled = { name -> name != "Settings" },
|
||||||
|
onMove = { name ->
|
||||||
|
onUpdateMenu(
|
||||||
|
mainItems.filter { it != name },
|
||||||
|
(subMenuOrder.ifEmpty { defaultSubMenuOrder } + name).distinct()
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onReorder = { main -> onUpdateMenu(main, subMenuOrder) }
|
||||||
|
)
|
||||||
|
// 更多菜单区(进「更多」的页面)
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.prefs_ui_sub_title),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
DragOrderList(
|
||||||
|
items = subItems,
|
||||||
|
labels = screens,
|
||||||
|
moveLabel = stringResource(id = R.string.prefs_ui_move_back),
|
||||||
|
moveEnabled = { mainItems.size < 5 },
|
||||||
|
onMove = { name ->
|
||||||
|
onUpdateMenu(
|
||||||
|
(screenOrder.ifEmpty { defaultScreenOrder } + name).distinct(),
|
||||||
|
subItems.filter { it != name }
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onReorder = { sub -> onUpdateMenu(screenOrder, sub) }
|
||||||
|
)
|
||||||
TextButton(onClick = onResetOrder) {
|
TextButton(onClick = onResetOrder) {
|
||||||
Text(text = stringResource(id = R.string.prefs_ui_order_reset))
|
Text(text = stringResource(id = R.string.prefs_ui_order_reset))
|
||||||
}
|
}
|
||||||
@@ -605,25 +669,18 @@ private fun UiSettingsCard(
|
|||||||
/** 页面顺序拖拽列表: 每行右侧抓手, 按住上下拖动实时换位, 松手持久化 */
|
/** 页面顺序拖拽列表: 每行右侧抓手, 按住上下拖动实时换位, 松手持久化 */
|
||||||
@Composable
|
@Composable
|
||||||
private fun DragOrderList(
|
private fun DragOrderList(
|
||||||
screens: List<Pair<Int, String>>,
|
items: List<String>,
|
||||||
screenOrder: List<String>,
|
labels: List<Pair<Int, String>>,
|
||||||
|
locked: Set<String> = emptySet(),
|
||||||
|
moveLabel: String? = null,
|
||||||
|
moveEnabled: (String) -> Boolean = { true },
|
||||||
|
onMove: ((String) -> Unit)? = null,
|
||||||
onReorder: (List<String>) -> Unit
|
onReorder: (List<String>) -> Unit
|
||||||
) {
|
) {
|
||||||
val itemHeight = 48.dp
|
val itemHeight = 48.dp
|
||||||
val itemHeightPx = with(LocalDensity.current) { itemHeight.toPx() }
|
val itemHeightPx = with(LocalDensity.current) { itemHeight.toPx() }
|
||||||
// 按 screenOrder 排序(空 = 默认顺序); remember 以 screens 参数为 key, 持久化后重组
|
|
||||||
val ordered = remember(screens, screenOrder) {
|
|
||||||
screens.sortedBy { (_, name) ->
|
|
||||||
// 未知(新页面如 CwDecode 不在旧持久化顺序里): 用默认顺序位置, 再兜底最后
|
|
||||||
screenOrder.indexOf(name).let {
|
|
||||||
if (it != -1) it else com.rtbishop.look4sat.core.presentation.defaultScreenOrder.indexOf(name).let {
|
|
||||||
if (it != -1) it else Int.MAX_VALUE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
val items = remember(ordered) { mutableStateListOf<Pair<Int, String>>().apply { addAll(ordered) } }
|
val localItems = remember(items) { mutableStateListOf<String>().apply { addAll(items) } }
|
||||||
var draggingIndex by remember { mutableIntStateOf(-1) }
|
var draggingIndex by remember { mutableIntStateOf(-1) }
|
||||||
var dragOffset by remember { mutableStateOf(0f) }
|
var dragOffset by remember { mutableStateOf(0f) }
|
||||||
|
|
||||||
@@ -632,10 +689,11 @@ private fun DragOrderList(
|
|||||||
state = listState,
|
state = listState,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(itemHeight * items.size)
|
.height(itemHeight * localItems.size)
|
||||||
) {
|
) {
|
||||||
itemsIndexed(items, key = { _, s -> s.second }) { index, screen ->
|
itemsIndexed(localItems, key = { _, name -> name }) { index, name ->
|
||||||
val isDragging = draggingIndex == index
|
val isDragging = draggingIndex == index
|
||||||
|
val isLocked = name in locked
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -646,46 +704,61 @@ private fun DragOrderList(
|
|||||||
.animateItem()
|
.animateItem()
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(id = screen.first),
|
text = stringResource(id = labels.first { it.second == name }.first),
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
Box(
|
if (moveLabel != null && onMove != null && moveEnabled(name)) {
|
||||||
modifier = Modifier
|
TextButton(onClick = { onMove(name) }, contentPadding = PaddingValues(horizontal = 4.dp)) {
|
||||||
.size(48.dp)
|
Text(moveLabel, fontSize = 11.sp)
|
||||||
.padding(20.dp)
|
}
|
||||||
.pointerInput(screen.second) {
|
}
|
||||||
detectDragGestures(
|
if (isLocked) {
|
||||||
onDragStart = { draggingIndex = items.indexOf(screen) },
|
// 锁定行(设置页): 无抓手, 固定最后
|
||||||
onDrag = { change, dragAmount ->
|
Text(
|
||||||
change.consume()
|
text = stringResource(id = R.string.prefs_ui_order_locked),
|
||||||
dragOffset += dragAmount.y
|
fontSize = 11.sp,
|
||||||
val currentIndex = draggingIndex
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
val target = (currentIndex + (dragOffset / itemHeightPx).roundToInt())
|
modifier = Modifier.padding(horizontal = 12.dp)
|
||||||
.coerceIn(0, items.size - 1)
|
)
|
||||||
if (target != currentIndex) {
|
} else {
|
||||||
items.add(target, items.removeAt(currentIndex))
|
|
||||||
dragOffset += (currentIndex - target) * itemHeightPx
|
|
||||||
draggingIndex = target
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDragEnd = {
|
|
||||||
onReorder(items.map { it.second })
|
|
||||||
draggingIndex = -1
|
|
||||||
dragOffset = 0f
|
|
||||||
},
|
|
||||||
onDragCancel = {
|
|
||||||
draggingIndex = -1
|
|
||||||
dragOffset = 0f
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
// 小圆点手柄(触控区 padding 20dp 居中 -> 8dp 圆点)
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.size(48.dp)
|
||||||
.background(MaterialTheme.colorScheme.onSurfaceVariant, CircleShape)
|
.padding(20.dp)
|
||||||
)
|
.pointerInput(name) {
|
||||||
|
detectDragGestures(
|
||||||
|
onDragStart = { draggingIndex = localItems.indexOf(name) },
|
||||||
|
onDrag = { change, dragAmount ->
|
||||||
|
change.consume()
|
||||||
|
dragOffset += dragAmount.y
|
||||||
|
val currentIndex = draggingIndex
|
||||||
|
val target = (currentIndex + (dragOffset / itemHeightPx).roundToInt())
|
||||||
|
.coerceIn(0, localItems.size - 1)
|
||||||
|
if (target != currentIndex) {
|
||||||
|
localItems.add(target, localItems.removeAt(currentIndex))
|
||||||
|
dragOffset += (currentIndex - target) * itemHeightPx
|
||||||
|
draggingIndex = target
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDragEnd = {
|
||||||
|
onReorder(localItems.toList())
|
||||||
|
draggingIndex = -1
|
||||||
|
dragOffset = 0f
|
||||||
|
},
|
||||||
|
onDragCancel = {
|
||||||
|
draggingIndex = -1
|
||||||
|
dragOffset = 0f
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
// 小圆点手柄(触控区 padding 20dp 居中 -> 8dp 圆点)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(MaterialTheme.colorScheme.onSurfaceVariant, CircleShape)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,6 +70,10 @@ sealed interface SettingsAction {
|
|||||||
data class ReorderScreens(val order: List<String>) : SettingsAction
|
data class ReorderScreens(val order: List<String>) : SettingsAction
|
||||||
// UI 设置: 重置为默认顺序
|
// UI 设置: 重置为默认顺序
|
||||||
data object ResetScreenOrder : SettingsAction
|
data object ResetScreenOrder : SettingsAction
|
||||||
|
// UI 设置: 更新主菜单 + 更多菜单顺序(4.5.1 折叠菜单)
|
||||||
|
data class UpdateMenuOrder(val mainOrder: List<String>, val subOrder: List<String>) : SettingsAction
|
||||||
|
// UI 设置: 重置主/子菜单为默认
|
||||||
|
data object ResetMenuOrder : SettingsAction
|
||||||
|
|
||||||
// Remote control
|
// Remote control
|
||||||
data class UpdateRC(val settings: RCSettings) : SettingsAction
|
data class UpdateRC(val settings: RCSettings) : SettingsAction
|
||||||
|
|||||||
+6
@@ -128,6 +128,12 @@ class SettingsViewModel(
|
|||||||
}
|
}
|
||||||
is SettingsAction.ReorderScreens -> settingsRepo.updateOtherSettings { it.copy(screenOrder = action.order) }
|
is SettingsAction.ReorderScreens -> settingsRepo.updateOtherSettings { it.copy(screenOrder = action.order) }
|
||||||
SettingsAction.ResetScreenOrder -> settingsRepo.updateOtherSettings { it.copy(screenOrder = emptyList()) }
|
SettingsAction.ResetScreenOrder -> settingsRepo.updateOtherSettings { it.copy(screenOrder = emptyList()) }
|
||||||
|
is SettingsAction.UpdateMenuOrder -> settingsRepo.updateOtherSettings {
|
||||||
|
it.copy(screenOrder = action.mainOrder, subMenuOrder = action.subOrder)
|
||||||
|
}
|
||||||
|
SettingsAction.ResetMenuOrder -> settingsRepo.updateOtherSettings {
|
||||||
|
it.copy(screenOrder = emptyList(), subMenuOrder = emptyList())
|
||||||
|
}
|
||||||
// Remote control & data sources
|
// Remote control & data sources
|
||||||
is SettingsAction.UpdateRC -> settingsRepo.updateRCSettings(action.settings)
|
is SettingsAction.UpdateRC -> settingsRepo.updateRCSettings(action.settings)
|
||||||
is SettingsAction.UpdateRadioControl -> settingsRepo.updateRadioControlSettings(action.settings)
|
is SettingsAction.UpdateRadioControl -> settingsRepo.updateRadioControlSettings(action.settings)
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
[versions]
|
[versions]
|
||||||
#noinspection UnusedVersionCatalogEntry
|
#noinspection UnusedVersionCatalogEntry
|
||||||
appVersionCode = "450"
|
appVersionCode = "451"
|
||||||
#noinspection UnusedVersionCatalogEntry
|
#noinspection UnusedVersionCatalogEntry
|
||||||
appVersionName = "4.5.0"
|
appVersionName = "4.5.1"
|
||||||
#noinspection UnusedVersionCatalogEntry
|
#noinspection UnusedVersionCatalogEntry
|
||||||
compileSdk = "37"
|
compileSdk = "37"
|
||||||
#noinspection UnusedVersionCatalogEntry
|
#noinspection UnusedVersionCatalogEntry
|
||||||
|
|||||||
Reference in new issue
Block a user