feat(nav,settings): bottom nav 5+N collapsible menu + Indonesian locale

Background: 8 bottom-nav items squeeze long English labels on narrow
screens. 4.5.1 introduces the 5+N pattern: 5 main tabs plus a fixed
6th "More" button that pops a second-level menu (spring bounce) with
the remaining pages.

Changes:
- MainScreen: nav split into main (<=5, screenOrder-driven) + more
  (subMenuOrder); More button with popup panel + spring animation;
  BackHandler closes the menu before navigating back
- MoreMenuPopup: bottom-end card, current page highlighted, scrim
  click to dismiss
- UI Settings: page order card now has two zones (main menu, max 5,
  Settings locked last with no drag handle / more menu); move buttons
  between zones, drag-to-reorder within zones; new subMenuOrder pref
- Defaults: main = Satellites/Passes/Radar/Map/Settings,
  more = Mutual/Roaming/CwDecode; old screenOrder migrates by
  classifying pages against the default sub menu
- Hard-coded UI strings localized (Tracking/Lat/Lon/Qth/Connect/
  Track/Stop/CW permission prompts) into EN/ZH/TR
- NEW Indonesian locale (values-in, 181 strings + cw module strings)
  - user rule: every future release must update EN/ZH/TR/IN
- Version 4.5.1 (451)

Verified: check_strings.py OK (8 files, no bare apostrophes);
:app:compileDebugKotlin BUILD SUCCESSFUL locally.
This commit is contained in:
mckero committed 2026-08-04 08:58:05 +00:00
1 parent 760a26b007
commit 38cf7a3569
19 files changed
+753 -172

No files matched your search

@@ -17,16 +17,23 @@
*/
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.RepeatMode
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.togetherWith
@@ -53,6 +60,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
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.Modifier
import androidx.compose.ui.draw.clip
@@ -137,7 +147,7 @@ fun MainScreen(navigateToRadar: () -> Unit = {}) {
val trackingState by container.radioTrackingService.state.collectAsStateWithLifecycle()
val otherSettings by container.settingsRepo.otherSettings.collectAsStateWithLifecycle()
// 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 ->
// 未知(新页面如 CwDecode 不在旧持久化顺序里): 用默认顺序位置(漫游↔地图), 再兜底最后
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 }
// 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
val mutualViewModel: MutualViewModel = viewModel(
viewModelStoreOwner = context as ViewModelStoreOwner,
@@ -161,7 +182,7 @@ fun MainScreen(navigateToRadar: () -> Unit = {}) {
) {
NavigationSuiteScaffold(
navigationSuiteItems = {
navItems.forEach { screen ->
mainNavItems.forEach { screen ->
val isSelected = when (currentKey) {
is Screen.Satellites -> screen is Screen.Satellites
is Screen.Passes -> screen is Screen.Passes
@@ -178,11 +199,26 @@ fun MainScreen(navigateToRadar: () -> Unit = {}) {
selected = isSelected,
onClick = {
if (isSelected) return@item
moreExpanded = false
while (backStack.size > 1) backStack.removeAt(backStack.size - 1)
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(
navigationRailContainerColor = MaterialTheme.colorScheme.surfaceContainer
@@ -193,108 +229,130 @@ fun MainScreen(navigateToRadar: () -> Unit = {}) {
else -> NavigationSuiteType.ShortNavigationBarMedium
}
) {
Column {
NavDisplay(
backStack = backStack,
modifier = Modifier.weight(1f),
onBack = navigateBack,
transitionSpec = { fadeTransition },
popTransitionSpec = { fadeTransition },
predictivePopTransitionSpec = { fadeTransition },
entryDecorators = listOf(
// Required for saving Compose state per entry
rememberSaveableStateHolderNavEntryDecorator(),
// Required for ViewModel scoping per entry
rememberViewModelStoreNavEntryDecorator()
),
entryProvider = entryProvider {
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()
Box {
Column(modifier = Modifier.fillMaxSize()) {
NavDisplay(
backStack = backStack,
modifier = Modifier.weight(1f).fillMaxWidth(),
onBack = navigateBack,
transitionSpec = { fadeTransition },
popTransitionSpec = { fadeTransition },
predictivePopTransitionSpec = { fadeTransition },
entryDecorators = listOf(
// Required for saving Compose state per entry
rememberSaveableStateHolderNavEntryDecorator(),
// Required for ViewModel scoping per entry
rememberViewModelStoreNavEntryDecorator()
),
entryProvider = entryProvider {
entry<Screen.Satellites> {
SatellitesDestination(navigateUp = navigateBack)
}
}
entry<Screen.Radar> {
RadarDestination(navigateUp = navigateBack)
}
entry<Screen.Map> {
MapDestination()
}
entry<Screen.Mutual> {
MutualScreen(
viewModel = mutualViewModel,
navigateUp = navigateBack,
navigateToRadar = { catNum, aosTime, pass ->
container.setMutualPassData(pass ?: MutualPassData())
entry<Screen.Passes> {
PassesDestination { catNum, aosTime ->
container.setMutualPassData(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
.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)
// navigateToRadar()
}
}
.padding(horizontal = 12.dp, vertical = 6.dp)
) {
Box(
entry<Screen.Radar> {
RadarDestination(navigateUp = navigateBack)
}
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
.size(8.dp)
.clip(CircleShape)
.background(Color(0xFF4CAF50).copy(alpha = alpha))
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "Tracking: ${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
)
.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)
) {
Box(
modifier = Modifier
.size(8.dp)
.clip(CircleShape)
.background(Color(0xFF4CAF50).copy(alpha = alpha))
)
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
)
}
}
}
}
}
}
+9
View File
@@ -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,
// UI 设置: 底部导航栏隐藏的页面(Screen simpleName 列表, 默认空 = 全部显示)
val hiddenScreens: List<String> = emptyList(),
// UI 设置: 页面顺序(空 = 默认顺序: 卫星/过境/雷达/匹配/漫游/地图/设置)
val screenOrder: List<String> = emptyList()
// UI 设置: 页面顺序(空 = 默认顺序: 卫星/过境/雷达/地图/设置)
val screenOrder: List<String> = emptyList(),
// UI 设置: 更多菜单顺序(空 = 默认: 匹配/漫游/CW解码)
val subMenuOrder: List<String> = emptyList()
)
data class DataSourcesSettings(
@@ -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")
}
// UI 设置: 默认页面顺序(与 MainScreen navItems 一致)
val defaultScreenOrder = listOf("Satellites", "Passes", "Radar", "Mutual", "Roaming", "CwDecode", "Map", "Settings")
// UI 设置: 默认主菜单页面顺序(底部栏 5 槽)
val defaultScreenOrder = listOf("Satellites", "Passes", "Radar", "Map", "Settings")
// UI 设置: 默认更多菜单顺序(进「更多」的页面)
val defaultSubMenuOrder = listOf("Mutual", "Roaming", "CwDecode")
@Serializable
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 &lt; %1$d°</string>
<string name="prefs_highlight_mid">Sedang %1$d–%2$d°</string>
<string name="prefs_highlight_high">Tinggi &gt; %1$d°</string>
<string name="prefs_outro_title">BG7NTA &amp; 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_map">Harita</string>
<string name="nav_prefs">Ayarlar</string>
<string name="nav_more">Daha Fazla</string>
<!-- Satellites screen -->
<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>
<string name="pass_whatsnew_title" translatable="false">What\'s new in Look4Sat</string>
<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
* CW kod çözme sayfaları aynı motoru paylaşıyor: gezinme sayfası ve transponder paneli</string>
* Alt gezinme artık 5 ana sekme artı Daha Fazla menüsü kullanıyor (katlanabilir ikinci düzey menü)
* UI Ayarları: sayfa sırası ana menü ve daha fazla menü olarak ayrıldı; sayfalar arasında taşıma yapılabilir</string>
<!-- Radar screen -->
<string name="radar_back">Geri</string>
@@ -178,9 +179,22 @@
<string name="prefs_bt_perm_error">Bluetooth 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_ui_title">UI Ayarları</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_other_switch_utc">Geçiş saatini UTC olarak göster</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_map">地图</string>
<string name="nav_prefs">设置</string>
<string name="nav_more">更多</string>
<!-- Satellites screen -->
<string name="sat_type_hint">类型:%s</string>
@@ -42,8 +43,8 @@
<string name="pass_welcome_message">请务必在设置中通过GPS、经纬度或QTH定位您的位置\n建议至少每周更新一次数据库,以确保预测结果的准确性</string>
<string name="pass_whatsnew_title">Look4Sat Pro 更新内容</string>
<string name="pass_whatsnew_message" translatable="false">
* 转发器 CW 解码面板已接入增强解码引擎,带实时瀑布图显示
* CW 解码入口共用同一引擎:导航页与转发器面板</string>
* 底部导航改为 5 个主标签 + 更多菜单(可折叠二级菜单)
* UI 设置: 页面顺序分为主菜单与更多菜单,可拖拽或移动页面</string>
<!-- Radar screen -->
<string name="radar_back">后退</string>
@@ -161,9 +162,22 @@
<string name="rc_rx_name_hint">接收电台名称</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_ui_title">UI设置</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_other_switch_utc">以 UTC 时间显示</string>
<string name="prefs_other_switch_update">启用卫星数据自动更新</string>
@@ -15,6 +15,7 @@
<string name="nav_cw">CW Decoder</string>
<string name="nav_map">Map</string>
<string name="nav_prefs">Settings</string>
<string name="nav_more">More</string>
<!-- Satellites screen -->
<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>
<string name="pass_whatsnew_title" translatable="false">What\'s new in Look4Sat</string>
<string name="pass_whatsnew_message" translatable="false">
* Transponder CW decoder panel now uses the enhanced decoding engine with a live waterfall display
* CW decoding pages share the same engine: navigation page and transponder panel</string>
* Bottom navigation now uses 5 main tabs plus a More menu (collapsible second-level menu)
* UI Settings: page order split into main menu and more menu; drag or move pages between them</string>
<!-- Radar screen -->
<string name="radar_back">Back</string>
@@ -192,9 +193,22 @@
<string name="prefs_bt_perm_error">Check your bluetooth 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_ui_title">UI Settings</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_other_switch_utc">Show pass time in UTC</string>
<string name="prefs_other_switch_update">Enable automatic data update</string>
@@ -181,7 +181,7 @@ fun CwDecodeScreen(navigateUp: () -> Unit = {}) {
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = "Microphone permission is required for CW decoding",
text = stringResource(R.string.cw_mic_permission),
color = Color.White,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp)
@@ -190,7 +190,7 @@ fun CwDecodeScreen(navigateUp: () -> Unit = {}) {
Button(onClick = {
permissionLauncher.launch(Manifest.permission.RECORD_AUDIO)
}) {
Text("Grant permission")
Text(stringResource(R.string.cw_grant_permission))
}
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="tap_back_again_to_close">Tap Back again to close the app</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>
@@ -461,19 +461,19 @@ private fun ExpandedRadioControl(
if (!radioControl.txPanel.isConnected && !radioControl.rxPanel.isConnected) {
CardButton(
onClick = { onAction(RadarAction.ConnectRadios) },
text = "Connect",
text = stringResource(R.string.connect_radio),
modifier = Modifier.weight(1f)
)
} else {
CardButton(
onClick = { onAction(RadarAction.DisconnectRadios) },
text = "Disconnect",
text = stringResource(R.string.disconnect_radio),
modifier = Modifier.weight(1f)
)
}
CardButton(
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)
)
}
@@ -33,6 +33,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
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.TopBar
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 java.text.SimpleDateFormat
import java.util.Date
@@ -327,9 +330,11 @@ private fun SettingsScreen(uiState: SettingsState, onAction: (SettingsAction) ->
UiSettingsCard(
hiddenScreens = uiState.otherSettings.hiddenScreens,
screenOrder = uiState.otherSettings.screenOrder,
subMenuOrder = uiState.otherSettings.subMenuOrder,
onToggle = { name -> onAction(SettingsAction.ToggleScreen(name)) },
onReorder = { order -> onAction(SettingsAction.ReorderScreens(order)) },
onResetOrder = { onAction(SettingsAction.ResetScreenOrder) }
onResetOrder = { onAction(SettingsAction.ResetMenuOrder) },
onUpdateMenu = { main, sub -> onAction(SettingsAction.UpdateMenuOrder(main, sub)) }
)
}
item { CardCredits() }
@@ -370,11 +375,11 @@ private fun LocationCard(
Text(text = formatUpdateTime(updateTime = settings.stationPos.timestamp))
Spacer(modifier = Modifier.height(2.dp))
Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
Text(text = "Lat: ${settings.stationPos.latitude}°")
Text(text = "Lon: ${settings.stationPos.longitude}°")
Text(text = stringResource(R.string.loc_lat_text, settings.stationPos.latitude))
Text(text = stringResource(R.string.loc_lon_text, settings.stationPos.longitude))
}
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))
Row(horizontalArrangement = Arrangement.SpaceEvenly) {
@@ -557,9 +562,11 @@ private fun OtherCard(settings: OtherSettings, onAction: (SettingsAction) -> Uni
private fun UiSettingsCard(
hiddenScreens: List<String>,
screenOrder: List<String>,
subMenuOrder: List<String>,
onToggle: (String) -> Unit,
onReorder: (List<String>) -> Unit,
onResetOrder: () -> Unit
onResetOrder: () -> Unit,
onUpdateMenu: (List<String>, List<String>) -> Unit
) {
val screens = listOf(
R.string.nav_sat to "Satellites",
@@ -571,6 +578,25 @@ private fun UiSettingsCard(
R.string.nav_map to "Map",
R.string.nav_prefs to "Settings"
) // 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()) {
Column(
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),
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) {
Text(text = stringResource(id = R.string.prefs_ui_order_reset))
}
@@ -605,25 +669,18 @@ private fun UiSettingsCard(
/** 页面顺序拖拽列表: 每行右侧抓手, 按住上下拖动实时换位, 松手持久化 */
@Composable
private fun DragOrderList(
screens: List<Pair<Int, String>>,
screenOrder: List<String>,
items: 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
) {
val itemHeight = 48.dp
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 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 dragOffset by remember { mutableStateOf(0f) }
@@ -632,10 +689,11 @@ private fun DragOrderList(
state = listState,
modifier = Modifier
.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 isLocked = name in locked
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
@@ -646,46 +704,61 @@ private fun DragOrderList(
.animateItem()
) {
Text(
text = stringResource(id = screen.first),
text = stringResource(id = labels.first { it.second == name }.first),
modifier = Modifier.weight(1f)
)
Box(
modifier = Modifier
.size(48.dp)
.padding(20.dp)
.pointerInput(screen.second) {
detectDragGestures(
onDragStart = { draggingIndex = items.indexOf(screen) },
onDrag = { change, dragAmount ->
change.consume()
dragOffset += dragAmount.y
val currentIndex = draggingIndex
val target = (currentIndex + (dragOffset / itemHeightPx).roundToInt())
.coerceIn(0, items.size - 1)
if (target != currentIndex) {
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 圆点)
if (moveLabel != null && onMove != null && moveEnabled(name)) {
TextButton(onClick = { onMove(name) }, contentPadding = PaddingValues(horizontal = 4.dp)) {
Text(moveLabel, fontSize = 11.sp)
}
}
if (isLocked) {
// 锁定行(设置页): 无抓手, 固定最后
Text(
text = stringResource(id = R.string.prefs_ui_order_locked),
fontSize = 11.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = 12.dp)
)
} else {
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.onSurfaceVariant, CircleShape)
)
.size(48.dp)
.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
// UI 设置: 重置为默认顺序
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
data class UpdateRC(val settings: RCSettings) : SettingsAction
@@ -128,6 +128,12 @@ class SettingsViewModel(
}
is SettingsAction.ReorderScreens -> settingsRepo.updateOtherSettings { it.copy(screenOrder = action.order) }
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
is SettingsAction.UpdateRC -> settingsRepo.updateRCSettings(action.settings)
is SettingsAction.UpdateRadioControl -> settingsRepo.updateRadioControlSettings(action.settings)
+2 -2
View File
@@ -1,8 +1,8 @@
[versions]
#noinspection UnusedVersionCatalogEntry
appVersionCode = "450"
appVersionCode = "451"
#noinspection UnusedVersionCatalogEntry
appVersionName = "4.5.0"
appVersionName = "4.5.1"
#noinspection UnusedVersionCatalogEntry
compileSdk = "37"
#noinspection UnusedVersionCatalogEntry