diff --git a/app/src/main/java/com/rtbishop/look4sat/MainScreen.kt b/app/src/main/java/com/rtbishop/look4sat/MainScreen.kt index 935938e1..fd95bb63 100644 --- a/app/src/main/java/com/rtbishop/look4sat/MainScreen.kt +++ b/app/src/main/java/com/rtbishop/look4sat/MainScreen.kt @@ -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 { - SatellitesDestination(navigateUp = navigateBack) - } - entry { - 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 { + SatellitesDestination(navigateUp = navigateBack) } - } - entry { - RadarDestination(navigateUp = navigateBack) - } - entry { - MapDestination() - } - entry { - MutualScreen( - viewModel = mutualViewModel, - navigateUp = navigateBack, - navigateToRadar = { catNum, aosTime, pass -> - container.setMutualPassData(pass ?: MutualPassData()) + entry { + PassesDestination { catNum, aosTime -> + container.setMutualPassData(MutualPassData()) container.satelliteRepo.selectPass(catNum, aosTime) backStack.add(Screen.Radar) - } - ) - } - entry { - RoamingScreen() - } - entry { - CwDecodeScreen() - } - entry { - 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 { + RadarDestination(navigateUp = navigateBack) + } + entry { + MapDestination() + } + entry { + MutualScreen( + viewModel = mutualViewModel, + navigateUp = navigateBack, + navigateToRadar = { catNum, aosTime, pass -> + container.setMutualPassData(pass ?: MutualPassData()) + container.satelliteRepo.selectPass(catNum, aosTime) + backStack.add(Screen.Radar) + } + ) + } + entry { + RoamingScreen() + } + entry { + CwDecodeScreen() + } + entry { + 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) + } + ) + } } } } diff --git a/app/src/main/java/com/rtbishop/look4sat/MoreMenuPopup.kt b/app/src/main/java/com/rtbishop/look4sat/MoreMenuPopup.kt new file mode 100644 index 00000000..0e3aa175 --- /dev/null +++ b/app/src/main/java/com/rtbishop/look4sat/MoreMenuPopup.kt @@ -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, + 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 + ) + } + } + } + } + } +} diff --git a/app/src/main/res/drawable/ic_more.xml b/app/src/main/res/drawable/ic_more.xml new file mode 100644 index 00000000..3ca18a0a --- /dev/null +++ b/app/src/main/res/drawable/ic_more.xml @@ -0,0 +1,9 @@ + + + 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 a0458623..78d02c7a 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 @@ -65,8 +65,10 @@ data class OtherSettings( val highElevation: Double = 45.0, // UI 设置: 底部导航栏隐藏的页面(Screen simpleName 列表, 默认空 = 全部显示) val hiddenScreens: List = emptyList(), - // UI 设置: 页面顺序(空 = 默认顺序: 卫星/过境/雷达/匹配/漫游/地图/设置) - val screenOrder: List = emptyList() + // UI 设置: 页面顺序(空 = 默认顺序: 卫星/过境/雷达/地图/设置) + val screenOrder: List = emptyList(), + // UI 设置: 更多菜单顺序(空 = 默认: 匹配/漫游/CW解码) + val subMenuOrder: 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 91f7ae0f..50ce065e 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 @@ -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 diff --git a/core/presentation/src/main/res/values-in/strings.xml b/core/presentation/src/main/res/values-in/strings.xml new file mode 100644 index 00000000..2a83450c --- /dev/null +++ b/core/presentation/src/main/res/values-in/strings.xml @@ -0,0 +1,210 @@ + + + Look4Sat Pro + Terima + Batal + Tidak ada data untuk ditampilkan + Satelit + Lintasan + Radar + Pasangan + Jelajah + Dekoder CW + Peta + Pengaturan + Lainnya + Jenis: %s + Pilih jenis satelit + Id - Nama + Bersihkan + Bersihkan semua + Pilih semua + Pastikan kata kunci pencarian Anda benar dan DB sudah diperbarui + Peringatan\! + + 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. + Filter lintasan + Elevasi minimal + Waktu ke depan + Jendela AOS + Balik jendela AOS + DeepSpace (periode >225 mnt) + -- : -- : -- + Pilih jenis modulasi + %05d + Elevasi: %.1f° + DeepSpace + Ketinggian: %d km + %03d° - %03d° + + Pastikan filter Anda benar dan Anda sudah memilih satelit + Tidak ada lintasan satelit mendatang untuk ditampilkan + Selamat datang di Look4Sat\! + + Pastikan Anda mengatur posisi melalui GPS, Lat/Lon atau QTH di Pengaturan. + \n\nPerbarui database setidaknya seminggu sekali untuk prediksi yang akurat. + Apa yang baru di Look4Sat + + * 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 + Kembali + Beri tahu + Azimuth + %.1f° + Elevasi + %.1f° + Ketinggian + %.0f km + Jarak + %.0f km + Gerhana + Downlink + Uplink + %.4f + %.4f - %.4f + - - . - - + Satelit ini tidak memiliki transceiver + Tambah + Transceiver satelit: + Terbalik: %s + Mode: %s + Tidak + Ya + Terlihat + Kalkulator Frekuensi Doppler + Masukkan frekuensi TX (MHz) + Masukkan frekuensi RX (MHz) + Offset (kHz) + Untuk transponder linier, ketik satu frekuensi untuk melihat frekuensi lainnya + Dekoder CW + Mulai + Berhenti + Bersihkan + Sebelumnya + Berikutnya + © Kontributor OpenStreetMap + Azimuth: %.1f° + Elevasi: %.1f° + Ketinggian: %.0f km + Jarak: %.0f km + Lintang: %.1f° + Bujur: %.1f° + QTH: %s + Fase: %.1f° + Gerhana + Periode: %.0f mnt + Kecepatan: %.2f km/d + Visibilitas: %s + Terlihat + Look4Sat Pro v%s + https://play.google.com/store/apps/details?id=com.rtbishop.look4sat + Donasi + https://ko-fi.com/rt_bishop + F-Droid + https://f-droid.org/en/packages/com.rtbishop.look4sat/ + GitHub + https://github.com/rt-bishop/Look4Sat/ + GPLv3 + https://www.gnu.org/licenses/gpl-3.0.html + Kebijakan Privasi + https://sites.google.com/view/look4sat-privacy-policy/home + Diperbarui: %s + d MMM yyyy - HH:mm:ss + Posisi stasiun + GPS + Periksa izin lokasi Anda + Input + Lokasi yang dimasukkan tidak valid + QTH + Lokator QTH tidak valid + Posisi berhasil diperbarui + Pengaturan posisi stasiun + Atur garis lintang stasiun Anda + Atur garis bujur stasiun Anda + Pengaturan lokator QTH + Atur posisi stasiun menggunakan lokator + Data satelit + Satelit: %s + Transceiver: %s + Perbarui + URL Kustom + Bersihkan + Data berhasil dibersihkan + Pembaruan berhasil diselesaikan + Tidak ada satelit yang diimpor. Pilih file TLE/3LE (.txt) atau OMM (.csv) yang valid. + Tidak ada transceiver yang diimpor. Pilih file SatNOGS (.json) yang valid. + Sumber data kustom + URL TLE Kustom + URL transceiver kustom + URL (HTTPS) + Setel ulang ke URL bawaan + Keluaran data + Jaringan + Bluetooth + CAT + Kontrol Radio + Kontrol Radio CAT + Model Radio + Alamat BT Radio TX + Alamat BT Radio RX + Nama Radio TX + Nama Radio RX + Aktifkan Kontrol CAT + Keluaran data jaringan + Aktifkan keluaran rotator + IP:Port + Format + Aktifkan keluaran frekuensi + IP:Port + Format + Keluaran data Bluetooth + Aktifkan keluaran rotator + Id perangkat + Format data + Aktifkan keluaran frekuensi + Id perangkat + Format data + Periksa izin bluetooth Anda + Periksa izin jaringan Anda + Melacak: %s + Lintang: %s° + Bujur: %s° + QTH: %s + Hubungkan + Putuskan + Lacak + Berhenti + Pengaturan lainnya + Pengaturan UI + Urutan halaman + Menu utama (bilah bawah) + Menu lainnya + Pindah ke menu lainnya + Kembalikan ke menu utama + Tetap (tidak dapat dipindahkan) + Setel ulang urutan + Tampilkan waktu lintasan dalam UTC + Aktifkan pembaruan data otomatis + Aktifkan animasi sapuan radar + Gunakan sensor untuk memutar tampilan radar + Aktifkan filter mode malam merah + Sorotan elevasi + Rendah < %1$d° + Sedang %1$d–%2$d° + Tinggi > %1$d° + BG7NTA & penulis asli mengucapkan terima kasih + + * 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 + Aplikasi ini hadir tanpa jaminan + diff --git a/core/presentation/src/main/res/values-tr/strings.xml b/core/presentation/src/main/res/values-tr/strings.xml index 9d9a0b98..d5558bc4 100644 --- a/core/presentation/src/main/res/values-tr/strings.xml +++ b/core/presentation/src/main/res/values-tr/strings.xml @@ -13,6 +13,7 @@ CW Dekoder Harita Ayarlar + Daha Fazla Uydu türü: %s @@ -52,8 +53,8 @@ \n\nDoğru tahminler alabilmek için veritabanını en az haftada bir güncelleyin. What\'s new in Look4Sat - * 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 + * 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 Geri @@ -178,9 +179,22 @@ Bluetooth izninizi kontrol edin Ağ izninizi kontrol edin + İzleniyor: %s + Enlem: %s° + Boylam: %s° + QTH: %s + Bağlan + Bağlantıyı Kes + Takip + Dur Diğer ayarlar UI Ayarları Sayfa sırası + Ana menü (alt çubuk) + Daha fazla menü + Daha fazla menüye taşı + Ana menüye geri taşı + Sabit (taşınamaz) Sıralamayı sıfırla Geçiş saatini UTC olarak göster Otomatik veri güncellemeyi 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 4267a597..977b1865 100644 --- a/core/presentation/src/main/res/values-zh/strings.xml +++ b/core/presentation/src/main/res/values-zh/strings.xml @@ -14,6 +14,7 @@ CW解码 地图 设置 + 更多 类型:%s @@ -42,8 +43,8 @@ 请务必在设置中通过GPS、经纬度或QTH定位您的位置\n建议至少每周更新一次数据库,以确保预测结果的准确性 Look4Sat Pro 更新内容 - * 转发器 CW 解码面板已接入增强解码引擎,带实时瀑布图显示 - * CW 解码入口共用同一引擎:导航页与转发器面板 + * 底部导航改为 5 个主标签 + 更多菜单(可折叠二级菜单) + * UI 设置: 页面顺序分为主菜单与更多菜单,可拖拽或移动页面 后退 @@ -161,9 +162,22 @@ 接收电台名称 启用 CAT 控制 + 跟踪: %s + 纬度: %s° + 经度: %s° + QTH: %s + 连接 + 断开 + 跟踪 + 停止 其他设置 UI设置 页面顺序 + 主菜单(底部栏) + 更多菜单 + 移入更多菜单 + 移回主菜单 + 固定(不可移动) 重置顺序 以 UTC 时间显示 启用卫星数据自动更新 diff --git a/core/presentation/src/main/res/values/strings.xml b/core/presentation/src/main/res/values/strings.xml index 3602d047..f97b41c8 100644 --- a/core/presentation/src/main/res/values/strings.xml +++ b/core/presentation/src/main/res/values/strings.xml @@ -15,6 +15,7 @@ CW Decoder Map Settings + More Type: %s @@ -54,8 +55,8 @@ \n\nPlease update the database at least weekly to get accurate predictions. What\'s new in Look4Sat - * 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 + * 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 Back @@ -192,9 +193,22 @@ Check your bluetooth permission Check your network permission + Tracking: %s + Lat: %s° + Lon: %s° + Qth: %s + Connect + Disconnect + Track + Stop Other settings UI Settings Page order + Main menu (bottom bar) + More menu + Move to more menu + Move back to main menu + Fixed (cannot be moved) Reset order Show pass time in UTC Enable automatic data update diff --git a/feature/cw/src/main/java/com/rtbishop/look4sat/feature/cw/CwDecodeScreen.kt b/feature/cw/src/main/java/com/rtbishop/look4sat/feature/cw/CwDecodeScreen.kt index c2387315..447c9866 100644 --- a/feature/cw/src/main/java/com/rtbishop/look4sat/feature/cw/CwDecodeScreen.kt +++ b/feature/cw/src/main/java/com/rtbishop/look4sat/feature/cw/CwDecodeScreen.kt @@ -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)) } } } diff --git a/feature/cw/src/main/res/values-in/app_values.xml b/feature/cw/src/main/res/values-in/app_values.xml new file mode 100644 index 00000000..19601995 --- /dev/null +++ b/feature/cw/src/main/res/values-in/app_values.xml @@ -0,0 +1,22 @@ + + + #ff6200ee + 18 + Diam + Tampilan waterfall + Skala frekuensi + Teks terdekode + Bersihkan + Jeda Dekode + Simpan Teks + Rekam Sinyal + Pengaturan + Bantuan Daring + Nilai Aplikasi Ini + Dapatkan Versi Premium + Ketuk Kembali lagi untuk menutup aplikasi + Tipe Pesan + Izin mikrofon diperlukan untuk dekode CW + Beri izin + Buka pengaturan aplikasi + diff --git a/feature/cw/src/main/res/values-tr/app_values.xml b/feature/cw/src/main/res/values-tr/app_values.xml new file mode 100644 index 00000000..a5d91c08 --- /dev/null +++ b/feature/cw/src/main/res/values-tr/app_values.xml @@ -0,0 +1,22 @@ + + + #ff6200ee + 18 + Beklemede + Şelale görüntüsü + Frekans ölçeği + Çözülen metin + Temizle + Çözmeyi Duraklat + Metni Kaydet + Sinyalleri Kaydet + Ayarlar + Çevrimiçi Yardım + Bu Uygulamayı Değerlendir + Premium Sürümü Al + Kapatmak için Geriye tekrar dokunun + Mesaj Türü + CW çözme için mikrofon izni gereklidir + İzin ver + Uygulama ayarlarını aç + diff --git a/feature/cw/src/main/res/values-zh/app_values.xml b/feature/cw/src/main/res/values-zh/app_values.xml new file mode 100644 index 00000000..acf5972c --- /dev/null +++ b/feature/cw/src/main/res/values-zh/app_values.xml @@ -0,0 +1,22 @@ + + + #ff6200ee + 18 + 空闲 + 瀑布图 + 频率刻度 + 解码文本 + 清除 + 暂停解码 + 保存文本 + 录音信号 + 设置 + 在线帮助 + 评价此应用 + 获取高级版 + 再次按返回键退出应用 + 消息类型 + CW 解码需要麦克风权限 + 授予权限 + 打开应用设置 + diff --git a/feature/cw/src/main/res/values/app_values.xml b/feature/cw/src/main/res/values/app_values.xml index 49c0e442..83e4e89b 100644 --- a/feature/cw/src/main/res/values/app_values.xml +++ b/feature/cw/src/main/res/values/app_values.xml @@ -16,4 +16,7 @@ Get Premium Version Tap Back again to close the app Message Type + Microphone permission is required for CW decoding + Grant permission + Open app settings diff --git a/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/TransceiversPage.kt b/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/TransceiversPage.kt index 2581228d..4dff5258 100644 --- a/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/TransceiversPage.kt +++ b/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/TransceiversPage.kt @@ -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) ) } 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 48ed96a6..9c7a6ce9 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 @@ -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, screenOrder: List, + subMenuOrder: List, onToggle: (String) -> Unit, onReorder: (List) -> Unit, - onResetOrder: () -> Unit + onResetOrder: () -> Unit, + onUpdateMenu: (List, List) -> 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>, - screenOrder: List, + items: List, + labels: List>, + locked: Set = emptySet(), + moveLabel: String? = null, + moveEnabled: (String) -> Boolean = { true }, + onMove: ((String) -> Unit)? = null, 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) -> - // 未知(新页面如 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>().apply { addAll(ordered) } } + val localItems = remember(items) { mutableStateListOf().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) + ) + } } } } 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 f2d66cbf..3239649d 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 @@ -70,6 +70,10 @@ sealed interface SettingsAction { data class ReorderScreens(val order: List) : SettingsAction // UI 设置: 重置为默认顺序 data object ResetScreenOrder : SettingsAction + // UI 设置: 更新主菜单 + 更多菜单顺序(4.5.1 折叠菜单) + data class UpdateMenuOrder(val mainOrder: List, val subOrder: List) : SettingsAction + // UI 设置: 重置主/子菜单为默认 + data object ResetMenuOrder : 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 13dc7a92..e39280ee 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 @@ -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) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b3e79886..51eebe39 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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