diff --git a/app/src/main/java/com/rtbishop/look4sat/MainScreen.kt b/app/src/main/java/com/rtbishop/look4sat/MainScreen.kt index 83688899..f36df999 100644 --- a/app/src/main/java/com/rtbishop/look4sat/MainScreen.kt +++ b/app/src/main/java/com/rtbishop/look4sat/MainScreen.kt @@ -135,8 +135,12 @@ fun MainScreen(navigateToRadar: () -> Unit = {}) { val container = (context.applicationContext as IContainerProvider).getMainContainer() val trackingState by container.radioTrackingService.state.collectAsStateWithLifecycle() val otherSettings by container.settingsRepo.otherSettings.collectAsStateWithLifecycle() - // UI 设置: 按 hiddenScreens 过滤导航项(顺序固定, 关闭项自动靠拢; 设置页固定保留) + // UI 设置: 按 screenOrder 排序(空 = 默认顺序), 再按 hiddenScreens 过滤(设置页固定保留) val navItems = listOf(Screen.Satellites, Screen.Passes, Screen.Radar, Screen.Mutual, Screen.Roaming, Screen.Map, Screen.Settings) + .sortedBy { screen -> + val idx = otherSettings.screenOrder.indexOf(screen.screenId) + if (idx == -1) Int.MAX_VALUE else idx + } .filter { it.screenId !in otherSettings.hiddenScreens || it is Screen.Settings } // Activity-scoped so the mutual query results survive navigation to Radar and back val mutualViewModel: MutualViewModel = viewModel( diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SettingsRepo.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SettingsRepo.kt index f071829f..2a3f0e74 100644 --- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SettingsRepo.kt +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SettingsRepo.kt @@ -78,6 +78,7 @@ class SettingsRepo( private val keyStateOfLightTheme = "stateOfLightTheme" private val keyStateOfNightMode = "stateOfNightMode" private val keyHiddenScreens = "hiddenScreens" + private val keyScreenOrder = "screenOrder" private val keyStationAltitude = "stationAltitude" private val keyStationLatitude = "stationLatitude" private val keyStationLongitude = "stationLongitude" @@ -359,6 +360,7 @@ class SettingsRepo( putLong(keyLowElevation, new.lowElevation.toRawBits()) putLong(keyHighElevation, new.highElevation.toRawBits()) putStringSet(keyHiddenScreens, new.hiddenScreens.toSet()) + putString(keyScreenOrder, new.screenOrder.joinToString(",")) } new } @@ -375,6 +377,7 @@ class SettingsRepo( shouldSeeWhatsNew = preferences.getBoolean(keyShouldSeeWhatsNew, true), sstvMode = preferences.getString(keySstvMode, null) ?: "Auto", hiddenScreens = preferences.getStringSet(keyHiddenScreens, emptySet())?.toList() ?: emptyList(), + screenOrder = preferences.getString(keyScreenOrder, null)?.split(",")?.filter { it.isNotBlank() } ?: emptyList(), lowElevation = Double.fromBits(preferences.getLong(keyLowElevation, 15.0.toRawBits())), highElevation = Double.fromBits(preferences.getLong(keyHighElevation, 45.0.toRawBits())) ) diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/model/Settings.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/model/Settings.kt index 6caac071..a0458623 100644 --- a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/model/Settings.kt +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/model/Settings.kt @@ -64,7 +64,9 @@ data class OtherSettings( val lowElevation: Double = 15.0, val highElevation: Double = 45.0, // UI 设置: 底部导航栏隐藏的页面(Screen simpleName 列表, 默认空 = 全部显示) - val hiddenScreens: List = emptyList() + val hiddenScreens: List = emptyList(), + // UI 设置: 页面顺序(空 = 默认顺序: 卫星/过境/雷达/匹配/漫游/地图/设置) + val screenOrder: List = emptyList() ) data class DataSourcesSettings( diff --git a/core/presentation/src/main/java/com/rtbishop/look4sat/core/presentation/Navigation.kt b/core/presentation/src/main/java/com/rtbishop/look4sat/core/presentation/Navigation.kt index 9c176dfc..05ba2c7d 100644 --- a/core/presentation/src/main/java/com/rtbishop/look4sat/core/presentation/Navigation.kt +++ b/core/presentation/src/main/java/com/rtbishop/look4sat/core/presentation/Navigation.kt @@ -45,6 +45,9 @@ 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") } +// UI 设置: 默认页面顺序(与 MainScreen navItems 一致) +val defaultScreenOrder = listOf("Satellites", "Passes", "Radar", "Mutual", "Roaming", "Map", "Settings") + @Serializable data object RadarDestination : NavKey diff --git a/core/presentation/src/main/res/drawable/ic_drag.xml b/core/presentation/src/main/res/drawable/ic_drag.xml new file mode 100644 index 00000000..fa9b3221 --- /dev/null +++ b/core/presentation/src/main/res/drawable/ic_drag.xml @@ -0,0 +1,10 @@ + + + + diff --git a/core/presentation/src/main/res/values-tr/strings.xml b/core/presentation/src/main/res/values-tr/strings.xml index 8035c1ed..37cce761 100644 --- a/core/presentation/src/main/res/values-tr/strings.xml +++ b/core/presentation/src/main/res/values-tr/strings.xml @@ -59,6 +59,7 @@ \n\n* Ayarlar: "İçe aktar" düğmesi "Özel URL" olarak yeniden adlandırıldı - TLE/alıcı-verici kaynak URL\'leri doğrudan düzenlenebilir \n\n* Özel kaynak anahtarları varsayılan olarak KAPALI; eski örnek URL\'ler gerçek varsayılanlara sıfırlanır \n\n* Yeni UI Ayarları kartı: alt gezinme sayfalarını göster/gizle (sıra sabit, Ayarlar her zaman görünür) + \n\n* Sayfa sırası: UI Ayarları'nda her sayfanın yanındaki tutamacı sürükleyerek alt gezinmeyi yeniden sıralayın, tek dokunuşla varsayılana sıfırlayın @@ -186,6 +187,8 @@ Diğer ayarlar UI Ayarları + Sayfa sırası + Sıralamayı sıfırla Geçiş saatini UTC olarak göster Otomatik veri güncellemeyi etkinleştir Radar taramasını etkinleştir diff --git a/core/presentation/src/main/res/values-zh/strings.xml b/core/presentation/src/main/res/values-zh/strings.xml index 5c95adfa..8e56f538 100644 --- a/core/presentation/src/main/res/values-zh/strings.xml +++ b/core/presentation/src/main/res/values-zh/strings.xml @@ -49,6 +49,7 @@ \n\n* 设置:导入文件按钮更名为自定义URL,可直接编辑 TLE/收发机数据源地址 \n\n* 自定义数据源开关默认关闭;旧版占位 URL 自动重置为真实默认地址 \n\n* 新增 UI设置 卡片:可显示/隐藏底部菜单栏页面(顺序固定,设置页始终显示) + \n\n* 新增 页面顺序:在 UI设置 中拖动每页右侧抓手可调整底部菜单栏顺序,一键重置为默认顺序 @@ -169,6 +170,8 @@ 其他设置 UI设置 + 页面顺序 + 重置顺序 以 UTC 时间显示 启用卫星数据自动更新 启用雷达扫描动画 diff --git a/core/presentation/src/main/res/values/strings.xml b/core/presentation/src/main/res/values/strings.xml index 4e2ad77f..c0bd47a8 100644 --- a/core/presentation/src/main/res/values/strings.xml +++ b/core/presentation/src/main/res/values/strings.xml @@ -61,6 +61,7 @@ \n\n* Settings: "Import" button renamed to "Custom URL" - edit TLE/transceiver source URLs directly \n\n* Custom source toggles default OFF; legacy placeholder URLs are reset to the real defaults \n\n* New UI Settings card: show/hide bottom navigation pages (order fixed, Settings always visible) + \n\n* Page order: drag the handle next to each page in UI Settings to reorder the bottom navigation, one-tap reset to default @@ -200,6 +201,8 @@ Other settings UI Settings + Page order + Reset order Show pass time in UTC Enable automatic data update Enable radar sweep animation diff --git a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsScreen.kt b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsScreen.kt index 27c2b9eb..186145d1 100644 --- a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsScreen.kt +++ b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsScreen.kt @@ -32,6 +32,12 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridItemSpan import androidx.compose.foundation.lazy.grid.LazyVerticalGrid @@ -41,15 +47,28 @@ import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Switch import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable +import kotlin.math.roundToInt import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment +import androidx.compose.ui.zIndex +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext @@ -303,7 +322,10 @@ private fun SettingsScreen(uiState: SettingsState, onAction: (SettingsAction) -> item { UiSettingsCard( hiddenScreens = uiState.otherSettings.hiddenScreens, - onToggle = { name -> onAction(SettingsAction.ToggleScreen(name)) } + screenOrder = uiState.otherSettings.screenOrder, + onToggle = { name -> onAction(SettingsAction.ToggleScreen(name)) }, + onReorder = { order -> onAction(SettingsAction.ReorderScreens(order)) }, + onResetOrder = { onAction(SettingsAction.ResetScreenOrder) } ) } item { CardCredits() } @@ -528,7 +550,13 @@ private fun OtherCard(settings: OtherSettings, onAction: (SettingsAction) -> Uni * "设置"页固定显示(防止失去设置入口)。 */ @Composable -private fun UiSettingsCard(hiddenScreens: List, onToggle: (String) -> Unit) { +private fun UiSettingsCard( + hiddenScreens: List, + screenOrder: List, + onToggle: (String) -> Unit, + onReorder: (List) -> Unit, + onResetOrder: () -> Unit +) { val screens = listOf( R.string.nav_sat to "Satellites", R.string.nav_pass to "Passes", @@ -557,6 +585,87 @@ private fun UiSettingsCard(hiddenScreens: List, onToggle: (String) -> Un } } } + Text( + text = stringResource(id = R.string.prefs_ui_order_title), + color = MaterialTheme.colorScheme.primary + ) + DragOrderList(screens = screens, screenOrder = screenOrder, onReorder = onReorder) + TextButton(onClick = onResetOrder) { + Text(text = stringResource(id = R.string.prefs_ui_order_reset)) + } + } + } +} + +/** 页面顺序拖拽列表: 每行右侧抓手, 按住上下拖动实时换位, 松手持久化 */ +@Composable +private fun DragOrderList( + screens: List>, + screenOrder: List, + onReorder: (List) -> Unit +) { + val itemHeight = 48.dp + val itemHeightPx = with(LocalDensity.current) { itemHeight.toPx() } + // 按 screenOrder 排序(空 = 默认顺序); remember 以 screens 参数为 key, 持久化后重组 + val ordered = remember(screens, screenOrder) { + screens.sortedBy { (_, name) -> + screenOrder.indexOf(name).let { if (it == -1) Int.MAX_VALUE else it } + } + } + val listState = rememberLazyListState() + val items = remember(ordered) { mutableStateListOf>().apply { addAll(ordered) } } + var draggingIndex by remember { mutableIntStateOf(-1) } + var dragOffset by remember { mutableStateOf(0f) } + + LazyColumn(state = listState, modifier = Modifier.fillMaxWidth()) { + itemsIndexed(items, key = { _, s -> s.second }) { index, screen -> + val isDragging = draggingIndex == index + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .height(itemHeight) + .zIndex(if (isDragging) 1f else 0f) + .graphicsLayer { translationY = if (isDragging) dragOffset else 0f } + .animateItem() + ) { + Text( + text = stringResource(id = screen.first), + modifier = Modifier.weight(1f) + ) + Icon( + painter = painterResource(id = R.drawable.ic_drag), + contentDescription = stringResource(id = R.string.prefs_ui_order_title), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier + .size(48.dp) + .pointerInput(screen.second) { + detectDragGestures( + onDragStart = { draggingIndex = index }, + onDrag = { change, dragAmount -> + change.consume() + dragOffset += dragAmount.y + val target = (index + (dragOffset / itemHeightPx).roundToInt()) + .coerceIn(0, items.size - 1) + if (target != index) { + items.add(target, items.removeAt(index)) + dragOffset += (index - target) * itemHeightPx + draggingIndex = target + } + }, + onDragEnd = { + onReorder(items.map { it.second }) + draggingIndex = -1 + dragOffset = 0f + }, + onDragCancel = { + draggingIndex = -1 + dragOffset = 0f + } + ) + } + ) + } } } } diff --git a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsState.kt b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsState.kt index 98c99c15..f2d66cbf 100644 --- a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsState.kt +++ b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsState.kt @@ -66,6 +66,10 @@ sealed interface SettingsAction { data class ToggleNightMode(val value: Boolean) : SettingsAction // UI 设置: 切换底部导航栏页面的隐藏状态(Screen simpleName) data class ToggleScreen(val screenName: String) : SettingsAction + // UI 设置: 更新页面顺序 + data class ReorderScreens(val order: List) : SettingsAction + // UI 设置: 重置为默认顺序 + data object ResetScreenOrder : SettingsAction // Remote control data class UpdateRC(val settings: RCSettings) : SettingsAction diff --git a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsViewModel.kt b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsViewModel.kt index 9f74ef9f..13dc7a92 100644 --- a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsViewModel.kt +++ b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsViewModel.kt @@ -126,6 +126,8 @@ class SettingsViewModel( if (action.screenName in hidden) hidden.remove(action.screenName) else hidden.add(action.screenName) current.copy(hiddenScreens = hidden) } + is SettingsAction.ReorderScreens -> settingsRepo.updateOtherSettings { it.copy(screenOrder = action.order) } + SettingsAction.ResetScreenOrder -> settingsRepo.updateOtherSettings { it.copy(screenOrder = emptyList()) } // Remote control & data sources is SettingsAction.UpdateRC -> settingsRepo.updateRCSettings(action.settings) is SettingsAction.UpdateRadioControl -> settingsRepo.updateRadioControlSettings(action.settings)