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)