feat(settings): drag-to-reorder page order in UI Settings card
Users can now change the bottom navigation order (previously fixed): - New "Page order" section under the Settings toggle in the UI Settings card: vertical list of all 7 pages, each with a drag handle (new ic_drag drawable, Material drag_indicator glyph) on the right. - Drag a handle up/down: the item follows the finger with live swap + animateItem() placement animation; on release the order is persisted (OtherSettings.screenOrder, comma-separated in prefs so order survives; StringSet would not). - "Reset order" button restores the default order (Satellites/Passes/Radar/Mutual/Roaming/Map/Settings). - MainScreen now sorts navItems by screenOrder (empty = default, stable sort keeps the canonical order); hiddenScreens filtering unchanged; Settings entry always visible. - New strings prefs_ui_order_title / prefs_ui_order_reset in en/zh/tr; What's-new updated in all three locales. Version stays 4.4.9 (覆盖 per user). Verified: core:data tests pass, settings + app compile.
This commit is contained in:
1 parent
c496d90d5a
commit
0ea9c0ede1
11 files changed
+150
-4
No files matched your search
@@ -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(
|
||||
|
||||
@@ -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()))
|
||||
)
|
||||
|
||||
@@ -64,7 +64,9 @@ data class OtherSettings(
|
||||
val lowElevation: Double = 15.0,
|
||||
val highElevation: Double = 45.0,
|
||||
// UI 设置: 底部导航栏隐藏的页面(Screen simpleName 列表, 默认空 = 全部显示)
|
||||
val hiddenScreens: List<String> = emptyList()
|
||||
val hiddenScreens: List<String> = emptyList(),
|
||||
// UI 设置: 页面顺序(空 = 默认顺序: 卫星/过境/雷达/匹配/漫游/地图/设置)
|
||||
val screenOrder: List<String> = emptyList()
|
||||
)
|
||||
|
||||
data class DataSourcesSettings(
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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="@android:color/white"
|
||||
android:pathData="M11,18c0,1.1 -0.9,2 -2,2s-2,-0.9 -2,-2 0.9,-2 2,-2 2,0.9 2,2zM9,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM9,4c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM15,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM15,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM15,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z" />
|
||||
</vector>
|
||||
@@ -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
|
||||
</string>
|
||||
|
||||
<!-- Radar screen -->
|
||||
@@ -186,6 +187,8 @@
|
||||
|
||||
<string name="prefs_other_title">Diğer 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_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_update">Otomatik veri güncellemeyi etkinleştir</string>
|
||||
<string name="prefs_other_switch_sweep">Radar taramasını etkinleştir</string>
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
\n\n* 设置:导入文件按钮更名为自定义URL,可直接编辑 TLE/收发机数据源地址
|
||||
\n\n* 自定义数据源开关默认关闭;旧版占位 URL 自动重置为真实默认地址
|
||||
\n\n* 新增 UI设置 卡片:可显示/隐藏底部菜单栏页面(顺序固定,设置页始终显示)
|
||||
\n\n* 新增 页面顺序:在 UI设置 中拖动每页右侧抓手可调整底部菜单栏顺序,一键重置为默认顺序
|
||||
</string>
|
||||
|
||||
<!-- Radar screen -->
|
||||
@@ -169,6 +170,8 @@
|
||||
|
||||
<string name="prefs_other_title">其他设置</string>
|
||||
<string name="prefs_ui_title">UI设置</string>
|
||||
<string name="prefs_ui_order_title">页面顺序</string>
|
||||
<string name="prefs_ui_order_reset">重置顺序</string>
|
||||
<string name="prefs_other_switch_utc">以 UTC 时间显示</string>
|
||||
<string name="prefs_other_switch_update">启用卫星数据自动更新</string>
|
||||
<string name="prefs_other_switch_sweep">启用雷达扫描动画</string>
|
||||
|
||||
@@ -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
|
||||
</string>
|
||||
|
||||
<!-- Radar screen -->
|
||||
@@ -200,6 +201,8 @@
|
||||
|
||||
<string name="prefs_other_title">Other settings</string>
|
||||
<string name="prefs_ui_title">UI Settings</string>
|
||||
<string name="prefs_ui_order_title">Page 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_update">Enable automatic data update</string>
|
||||
<string name="prefs_other_switch_sweep">Enable radar sweep animation</string>
|
||||
|
||||
+111
-2
@@ -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<String>, onToggle: (String) -> Unit) {
|
||||
private fun UiSettingsCard(
|
||||
hiddenScreens: List<String>,
|
||||
screenOrder: List<String>,
|
||||
onToggle: (String) -> Unit,
|
||||
onReorder: (List<String>) -> 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<String>, 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<Pair<Int, String>>,
|
||||
screenOrder: List<String>,
|
||||
onReorder: (List<String>) -> 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<Pair<Int, String>>().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
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String>) : SettingsAction
|
||||
// UI 设置: 重置为默认顺序
|
||||
data object ResetScreenOrder : SettingsAction
|
||||
|
||||
// Remote control
|
||||
data class UpdateRC(val settings: RCSettings) : SettingsAction
|
||||
|
||||
+2
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user