Compare commits

...
3 Commits
Author SHA1 Message Date
mckero 38cf7a3569 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.
2026-08-04 08:58:05 +00:00
mckero 760a26b007 Merge remote-tracking branch 'atsunatsu/main' 2026-08-04 08:10:17 +00:00
atsunatsu a7b41c95c7 fix: improve match header and daily sun times
Match page UI:
- Always show a meaningful status line in the top bar instead of leaving the second row blank on first entry.
- Add a compact status chip for waiting, calculating, result, no-match, and error states.
- Remove the duplicate intro card so the first screen starts directly with station inputs.

Pass list sun times:
- Compute sunrise/sunset from each visible date group's 00:00 in the selected timezone.
- Avoid using an arbitrary pass AOS as the rise/set search start, which could jump later-day headers to the following day's events.
2026-08-03 21:38:57 +08:00
22 changed files with 846 additions and 210 deletions

No files matched your search

@@ -17,16 +17,23 @@
*/ */
package com.rtbishop.look4sat 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.LinearEasing
import androidx.compose.animation.core.RepeatMode import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut import androidx.compose.animation.scaleOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.togetherWith import androidx.compose.animation.togetherWith
@@ -53,6 +60,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue 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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
@@ -137,7 +147,7 @@ fun MainScreen(navigateToRadar: () -> Unit = {}) {
val trackingState by container.radioTrackingService.state.collectAsStateWithLifecycle() val trackingState by container.radioTrackingService.state.collectAsStateWithLifecycle()
val otherSettings by container.settingsRepo.otherSettings.collectAsStateWithLifecycle() val otherSettings by container.settingsRepo.otherSettings.collectAsStateWithLifecycle()
// UI 设置: 按 screenOrder 排序(空 = 默认顺序), 再按 hiddenScreens 过滤(设置页固定保留) // 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 -> .sortedBy { screen ->
// 未知(新页面如 CwDecode 不在旧持久化顺序里): 用默认顺序位置(漫游↔地图), 再兜底最后 // 未知(新页面如 CwDecode 不在旧持久化顺序里): 用默认顺序位置(漫游↔地图), 再兜底最后
val idx = otherSettings.screenOrder.indexOf(screen.screenId) 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 } .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 // Activity-scoped so the mutual query results survive navigation to Radar and back
val mutualViewModel: MutualViewModel = viewModel( val mutualViewModel: MutualViewModel = viewModel(
viewModelStoreOwner = context as ViewModelStoreOwner, viewModelStoreOwner = context as ViewModelStoreOwner,
@@ -161,7 +182,7 @@ fun MainScreen(navigateToRadar: () -> Unit = {}) {
) { ) {
NavigationSuiteScaffold( NavigationSuiteScaffold(
navigationSuiteItems = { navigationSuiteItems = {
navItems.forEach { screen -> mainNavItems.forEach { screen ->
val isSelected = when (currentKey) { val isSelected = when (currentKey) {
is Screen.Satellites -> screen is Screen.Satellites is Screen.Satellites -> screen is Screen.Satellites
is Screen.Passes -> screen is Screen.Passes is Screen.Passes -> screen is Screen.Passes
@@ -178,11 +199,26 @@ fun MainScreen(navigateToRadar: () -> Unit = {}) {
selected = isSelected, selected = isSelected,
onClick = { onClick = {
if (isSelected) return@item if (isSelected) return@item
moreExpanded = false
while (backStack.size > 1) backStack.removeAt(backStack.size - 1) while (backStack.size > 1) backStack.removeAt(backStack.size - 1)
if (screen !is Screen.Passes) backStack.add(screen) 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( navigationSuiteColors = NavigationSuiteDefaults.colors(
navigationRailContainerColor = MaterialTheme.colorScheme.surfaceContainer navigationRailContainerColor = MaterialTheme.colorScheme.surfaceContainer
@@ -193,108 +229,130 @@ fun MainScreen(navigateToRadar: () -> Unit = {}) {
else -> NavigationSuiteType.ShortNavigationBarMedium else -> NavigationSuiteType.ShortNavigationBarMedium
} }
) { ) {
Column { Box {
NavDisplay( Column(modifier = Modifier.fillMaxSize()) {
backStack = backStack, NavDisplay(
modifier = Modifier.weight(1f), backStack = backStack,
onBack = navigateBack, modifier = Modifier.weight(1f).fillMaxWidth(),
transitionSpec = { fadeTransition }, onBack = navigateBack,
popTransitionSpec = { fadeTransition }, transitionSpec = { fadeTransition },
predictivePopTransitionSpec = { fadeTransition }, popTransitionSpec = { fadeTransition },
entryDecorators = listOf( predictivePopTransitionSpec = { fadeTransition },
// Required for saving Compose state per entry entryDecorators = listOf(
rememberSaveableStateHolderNavEntryDecorator(), // Required for saving Compose state per entry
// Required for ViewModel scoping per entry rememberSaveableStateHolderNavEntryDecorator(),
rememberViewModelStoreNavEntryDecorator() // Required for ViewModel scoping per entry
), rememberViewModelStoreNavEntryDecorator()
entryProvider = entryProvider { ),
entry<Screen.Satellites> { entryProvider = entryProvider {
SatellitesDestination(navigateUp = navigateBack) 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()
} }
} entry<Screen.Passes> {
entry<Screen.Radar> { PassesDestination { catNum, aosTime ->
RadarDestination(navigateUp = navigateBack) container.setMutualPassData(MutualPassData())
}
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) container.satelliteRepo.selectPass(catNum, aosTime)
backStack.add(Screen.Radar) backStack.add(Screen.Radar)
} // navigateToRadar()
)
}
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)
} }
} }
.padding(horizontal = 12.dp, vertical = 6.dp) entry<Screen.Radar> {
) { RadarDestination(navigateUp = navigateBack)
Box( }
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 modifier = Modifier
.size(8.dp) .fillMaxWidth()
.clip(CircleShape) .background(MaterialTheme.colorScheme.primaryContainer)
.background(Color(0xFF4CAF50).copy(alpha = alpha)) .clickable {
) val pass = trackingState.currentPass
Spacer(modifier = Modifier.width(8.dp)) if (pass != null) {
Text( container.setMutualPassData(MutualPassData())
text = "Tracking: ${trackingState.currentPass?.name ?: ""}", container.satelliteRepo.selectPass(pass.catNum, pass.aosTime)
fontSize = 13.sp, backStack.add(Screen.Radar)
fontWeight = FontWeight.Medium, }
color = MaterialTheme.colorScheme.onPrimaryContainer, }
modifier = Modifier.weight(1f) .padding(horizontal = 12.dp, vertical = 6.dp)
) ) {
val txOk = if (trackingState.txConnected) "TX" else "" Box(
val rxOk = if (trackingState.rxConnected) "RX" else "" modifier = Modifier
Text( .size(8.dp)
text = listOf(txOk, rxOk).filter { it.isNotBlank() }.joinToString("/"), .clip(CircleShape)
fontSize = 12.sp, .background(Color(0xFF4CAF50).copy(alpha = alpha))
color = MaterialTheme.colorScheme.onPrimaryContainer )
) 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, val highElevation: Double = 45.0,
// UI 设置: 底部导航栏隐藏的页面(Screen simpleName 列表, 默认空 = 全部显示) // UI 设置: 底部导航栏隐藏的页面(Screen simpleName 列表, 默认空 = 全部显示)
val hiddenScreens: List<String> = emptyList(), val hiddenScreens: List<String> = emptyList(),
// UI 设置: 页面顺序(空 = 默认顺序: 卫星/过境/雷达/匹配/漫游/地图/设置) // UI 设置: 页面顺序(空 = 默认顺序: 卫星/过境/雷达/地图/设置)
val screenOrder: List<String> = emptyList() val screenOrder: List<String> = emptyList(),
// UI 设置: 更多菜单顺序(空 = 默认: 匹配/漫游/CW解码)
val subMenuOrder: List<String> = emptyList()
) )
data class DataSourcesSettings( 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") data object Settings : Screen(R.drawable.ic_settings, R.string.nav_prefs, "Settings")
} }
// UI 设置: 默认页面顺序(与 MainScreen navItems 一致) // UI 设置: 默认主菜单页面顺序(底部栏 5 槽)
val defaultScreenOrder = listOf("Satellites", "Passes", "Radar", "Mutual", "Roaming", "CwDecode", "Map", "Settings") val defaultScreenOrder = listOf("Satellites", "Passes", "Radar", "Map", "Settings")
// UI 设置: 默认更多菜单顺序(进「更多」的页面)
val defaultSubMenuOrder = listOf("Mutual", "Roaming", "CwDecode")
@Serializable @Serializable
data object RadarDestination : NavKey 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_cw">CW Dekoder</string>
<string name="nav_map">Harita</string> <string name="nav_map">Harita</string>
<string name="nav_prefs">Ayarlar</string> <string name="nav_prefs">Ayarlar</string>
<string name="nav_more">Daha Fazla</string>
<!-- Satellites screen --> <!-- Satellites screen -->
<string name="sat_type_hint">Uydu türü: %s</string> <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> \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_title" translatable="false">What\'s new in Look4Sat</string>
<string name="pass_whatsnew_message" translatable="false"> <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 * Alt gezinme artık 5 ana sekme artı Daha Fazla menüsü kullanıyor (katlanabilir ikinci düzey menü)
* CW kod çözme sayfaları aynı motoru paylaşıyor: gezinme sayfası ve transponder paneli</string> * UI Ayarları: sayfa sırası ana menü ve daha fazla menü olarak ayrıldı; sayfalar arasında taşıma yapılabilir</string>
<!-- Radar screen --> <!-- Radar screen -->
<string name="radar_back">Geri</string> <string name="radar_back">Geri</string>
@@ -178,9 +179,22 @@
<string name="prefs_bt_perm_error">Bluetooth izninizi kontrol edin</string> <string name="prefs_bt_perm_error">Bluetooth izninizi kontrol edin</string>
<string name="prefs_net_perm_error">Ağ 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_other_title">Diğer ayarlar</string>
<string name="prefs_ui_title">UI 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_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_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_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_update">Otomatik veri güncellemeyi etkinleştir</string>
@@ -14,6 +14,7 @@
<string name="nav_cw">CW解码</string> <string name="nav_cw">CW解码</string>
<string name="nav_map">地图</string> <string name="nav_map">地图</string>
<string name="nav_prefs">设置</string> <string name="nav_prefs">设置</string>
<string name="nav_more">更多</string>
<!-- Satellites screen --> <!-- Satellites screen -->
<string name="sat_type_hint">类型:%s</string> <string name="sat_type_hint">类型:%s</string>
@@ -42,8 +43,8 @@
<string name="pass_welcome_message">请务必在设置中通过GPS、经纬度或QTH定位您的位置\n建议至少每周更新一次数据库,以确保预测结果的准确性</string> <string name="pass_welcome_message">请务必在设置中通过GPS、经纬度或QTH定位您的位置\n建议至少每周更新一次数据库,以确保预测结果的准确性</string>
<string name="pass_whatsnew_title">Look4Sat Pro 更新内容</string> <string name="pass_whatsnew_title">Look4Sat Pro 更新内容</string>
<string name="pass_whatsnew_message" translatable="false"> <string name="pass_whatsnew_message" translatable="false">
* 转发器 CW 解码面板已接入增强解码引擎,带实时瀑布图显示 * 底部导航改为 5 个主标签 + 更多菜单(可折叠二级菜单)
* CW 解码入口共用同一引擎:导航页与转发器面板</string> * UI 设置: 页面顺序分为主菜单与更多菜单,可拖拽或移动页面</string>
<!-- Radar screen --> <!-- Radar screen -->
<string name="radar_back">后退</string> <string name="radar_back">后退</string>
@@ -161,9 +162,22 @@
<string name="rc_rx_name_hint">接收电台名称</string> <string name="rc_rx_name_hint">接收电台名称</string>
<string name="rc_enable_switch">启用 CAT 控制</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_other_title">其他设置</string>
<string name="prefs_ui_title">UI设置</string> <string name="prefs_ui_title">UI设置</string>
<string name="prefs_ui_order_title">页面顺序</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_ui_order_reset">重置顺序</string>
<string name="prefs_other_switch_utc">以 UTC 时间显示</string> <string name="prefs_other_switch_utc">以 UTC 时间显示</string>
<string name="prefs_other_switch_update">启用卫星数据自动更新</string> <string name="prefs_other_switch_update">启用卫星数据自动更新</string>
@@ -15,6 +15,7 @@
<string name="nav_cw">CW Decoder</string> <string name="nav_cw">CW Decoder</string>
<string name="nav_map">Map</string> <string name="nav_map">Map</string>
<string name="nav_prefs">Settings</string> <string name="nav_prefs">Settings</string>
<string name="nav_more">More</string>
<!-- Satellites screen --> <!-- Satellites screen -->
<string name="sat_type_hint">Type: %s</string> <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> \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_title" translatable="false">What\'s new in Look4Sat</string>
<string name="pass_whatsnew_message" translatable="false"> <string name="pass_whatsnew_message" translatable="false">
* Transponder CW decoder panel now uses the enhanced decoding engine with a live waterfall display * Bottom navigation now uses 5 main tabs plus a More menu (collapsible second-level menu)
* CW decoding pages share the same engine: navigation page and transponder panel</string> * UI Settings: page order split into main menu and more menu; drag or move pages between them</string>
<!-- Radar screen --> <!-- Radar screen -->
<string name="radar_back">Back</string> <string name="radar_back">Back</string>
@@ -192,9 +193,22 @@
<string name="prefs_bt_perm_error">Check your bluetooth permission</string> <string name="prefs_bt_perm_error">Check your bluetooth permission</string>
<string name="prefs_net_perm_error">Check your network 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_other_title">Other settings</string>
<string name="prefs_ui_title">UI Settings</string> <string name="prefs_ui_title">UI Settings</string>
<string name="prefs_ui_order_title">Page order</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_ui_order_reset">Reset order</string>
<string name="prefs_other_switch_utc">Show pass time in UTC</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_update">Enable automatic data update</string>
@@ -181,7 +181,7 @@ fun CwDecodeScreen(navigateUp: () -> Unit = {}) {
) { ) {
Column(horizontalAlignment = Alignment.CenterHorizontally) { Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text( Text(
text = "Microphone permission is required for CW decoding", text = stringResource(R.string.cw_mic_permission),
color = Color.White, color = Color.White,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp) modifier = Modifier.padding(horizontal = 24.dp)
@@ -190,7 +190,7 @@ fun CwDecodeScreen(navigateUp: () -> Unit = {}) {
Button(onClick = { Button(onClick = {
permissionLauncher.launch(Manifest.permission.RECORD_AUDIO) permissionLauncher.launch(Manifest.permission.RECORD_AUDIO)
}) { }) {
Text("Grant permission") Text(stringResource(R.string.cw_grant_permission))
} }
if (permanentlyDenied) { 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="premium">Get Premium Version</string>
<string name="tap_back_again_to_close">Tap Back again to close the app</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="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> </resources>
@@ -44,6 +44,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Slider import androidx.compose.material3.Slider
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
@@ -57,6 +58,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.rtbishop.look4sat.core.domain.repository.MutualPassData import com.rtbishop.look4sat.core.domain.repository.MutualPassData
@@ -94,22 +96,29 @@ fun MutualScreen(
) )
}, },
bottomInfo = { bottomInfo = {
if (state.mutualPasses.isNotEmpty()) { Text(
Text( text = mutualStatusText(state),
text = "找到 ${state.mutualPasses.size} 个匹配", style = MaterialTheme.typography.bodySmall,
style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant,
color = MaterialTheme.colorScheme.onSurfaceVariant maxLines = 1,
) overflow = TextOverflow.Ellipsis,
} modifier = Modifier.weight(1f)
)
}, },
endAction = { endAction = {
if (state.isCalculating) { Row(
CircularProgressIndicator( verticalAlignment = Alignment.CenterVertically,
modifier = Modifier horizontalArrangement = Arrangement.spacedBy(6.dp)
.height(24.dp) ) {
.width(24.dp), if (state.isCalculating) {
strokeWidth = 2.dp CircularProgressIndicator(
) modifier = Modifier
.height(24.dp)
.width(24.dp),
strokeWidth = 2.dp
)
}
MutualStatusChip(state)
} }
} }
) )
@@ -187,8 +196,6 @@ private fun MutualContent(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(6.dp) verticalArrangement = Arrangement.spacedBy(6.dp)
) { ) {
MatchIntroCard()
if (isVertical) { if (isVertical) {
StationInputCard( StationInputCard(
title = "你的位置", title = "你的位置",
@@ -304,25 +311,50 @@ private fun MutualContent(
} }
@Composable @Composable
private fun MatchIntroCard(modifier: Modifier = Modifier) { private fun MutualStatusChip(state: MutualUiState) {
ElevatedCard(modifier = modifier.fillMaxWidth()) { val colorScheme = MaterialTheme.colorScheme
Column( val containerColor = when {
modifier = Modifier.padding(12.dp), state.isCalculating -> colorScheme.primaryContainer
verticalArrangement = Arrangement.spacedBy(4.dp) state.errorMessage != null -> colorScheme.errorContainer
) { state.mutualPasses.isNotEmpty() -> colorScheme.primary
Text( state.hasSearched -> colorScheme.surfaceVariant
text = "位置匹配", else -> colorScheme.surfaceVariant
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.primary
)
Text(
text = "输入双方位置,查找共同可见的卫星过境窗口",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
} }
val contentColor = when {
state.isCalculating -> colorScheme.onPrimaryContainer
state.errorMessage != null -> colorScheme.onErrorContainer
state.mutualPasses.isNotEmpty() -> colorScheme.onPrimary
else -> colorScheme.onSurfaceVariant
}
Surface(
color = containerColor,
contentColor = contentColor,
shape = MaterialTheme.shapes.small
) {
Text(
text = mutualStatusChipText(state),
style = MaterialTheme.typography.labelSmall,
fontWeight = FontWeight.Medium,
maxLines = 1,
modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp)
)
}
}
private fun mutualStatusText(state: MutualUiState): String = when {
state.isCalculating -> "正在计算双方共同可见窗口…"
state.errorMessage != null -> "请修正提示后重新查询"
state.mutualPasses.isNotEmpty() -> "找到 ${state.mutualPasses.size} 个匹配 · 点击卡片展开曲线"
state.hasSearched -> "未找到共同可见窗口,调整时间或仰角后重试"
else -> "输入双方位置后点击查询"
}
private fun mutualStatusChipText(state: MutualUiState): String = when {
state.isCalculating -> "计算中"
state.errorMessage != null -> "有错误"
state.mutualPasses.isNotEmpty() -> "${state.mutualPasses.size}个匹配"
state.hasSearched -> "无匹配"
else -> "待查询"
} }
@Composable @Composable
@@ -49,6 +49,7 @@ data class MutualUiState(
val hoursAhead: Int = 24, val hoursAhead: Int = 24,
val mutualPasses: List<MutualPass> = emptyList(), val mutualPasses: List<MutualPass> = emptyList(),
val isCalculating: Boolean = false, val isCalculating: Boolean = false,
val hasSearched: Boolean = false,
val selectedPassIndex: Int = -1, val selectedPassIndex: Int = -1,
val errorMessage: String? = null val errorMessage: String? = null
) )
@@ -167,7 +168,15 @@ class MutualViewModel(
return return
} }
_uiState.update { it.copy(isCalculating = true, errorMessage = null, mutualPasses = emptyList()) } _uiState.update {
it.copy(
isCalculating = true,
hasSearched = true,
errorMessage = null,
mutualPasses = emptyList(),
selectedPassIndex = -1
)
}
viewModelScope.launch { viewModelScope.launch {
val time = System.currentTimeMillis() val time = System.currentTimeMillis()
@@ -38,6 +38,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
import java.util.TimeZone import java.util.TimeZone
@@ -162,7 +163,10 @@ class PassesViewModel(
return SimpleDateFormat(pattern, Locale.getDefault()).also { it.timeZone = tz } return SimpleDateFormat(pattern, Locale.getDefault()).also { it.timeZone = tz }
} }
// Computes sunrise/sunset strings for each unique calendar day in the pass list, plus today for DeepSpace // Computes sunrise/sunset strings for each unique calendar day in the pass list, plus today for DeepSpace.
// CelestialComputer.findSunRiseSet() returns the next rise/set after the supplied time, so pass
// the calendar day's 00:00 in the same timezone used by the visible date group. Passing a pass's
// AOS time can jump to the following day's sunrise/sunset when that AOS is after the local event.
private fun computeSunTimes(passes: List<OrbitalPass>, isUtc: Boolean): Map<String, Pair<String, String>> { private fun computeSunTimes(passes: List<OrbitalPass>, isUtc: Boolean): Map<String, Pair<String, String>> {
val stationPos = settingsRepo.stationPosition.value val stationPos = settingsRepo.stationPosition.value
val tz = if (isUtc) TimeZone.getTimeZone("UTC") else TimeZone.getDefault() val tz = if (isUtc) TimeZone.getTimeZone("UTC") else TimeZone.getDefault()
@@ -171,7 +175,7 @@ class PassesViewModel(
val result = LinkedHashMap<String, Pair<String, String>>() val result = LinkedHashMap<String, Pair<String, String>>()
// DeepSpace group always shows today's sun times // DeepSpace group always shows today's sun times
if (passes.any { it.isDeepSpace }) { if (passes.any { it.isDeepSpace }) {
val riseSet = CelestialComputer.findSunRiseSet(stationPos, System.currentTimeMillis()) val riseSet = CelestialComputer.findSunRiseSet(stationPos, startOfDayMillis(System.currentTimeMillis(), tz))
val rise = if (riseSet.riseTimeMillis > 0) sdfTime.format(Date(riseSet.riseTimeMillis)) else "--:--" val rise = if (riseSet.riseTimeMillis > 0) sdfTime.format(Date(riseSet.riseTimeMillis)) else "--:--"
val set = if (riseSet.setTimeMillis > 0) sdfTime.format(Date(riseSet.setTimeMillis)) else "--:--" val set = if (riseSet.setTimeMillis > 0) sdfTime.format(Date(riseSet.setTimeMillis)) else "--:--"
result["DeepSpace (period >225min)"] = rise to set result["DeepSpace (period >225min)"] = rise to set
@@ -180,7 +184,7 @@ class PassesViewModel(
if (pass.isDeepSpace) continue if (pass.isDeepSpace) continue
val label = sdfDate.format(Date(pass.aosTime)) val label = sdfDate.format(Date(pass.aosTime))
if (label in result) continue if (label in result) continue
val riseSet = CelestialComputer.findSunRiseSet(stationPos, pass.aosTime) val riseSet = CelestialComputer.findSunRiseSet(stationPos, startOfDayMillis(pass.aosTime, tz))
val rise = if (riseSet.riseTimeMillis > 0) sdfTime.format(Date(riseSet.riseTimeMillis)) else "--:--" val rise = if (riseSet.riseTimeMillis > 0) sdfTime.format(Date(riseSet.riseTimeMillis)) else "--:--"
val set = if (riseSet.setTimeMillis > 0) sdfTime.format(Date(riseSet.setTimeMillis)) else "--:--" val set = if (riseSet.setTimeMillis > 0) sdfTime.format(Date(riseSet.setTimeMillis)) else "--:--"
result[label] = rise to set result[label] = rise to set
@@ -188,6 +192,16 @@ class PassesViewModel(
return result return result
} }
private fun startOfDayMillis(timeMillis: Long, tz: TimeZone): Long {
return Calendar.getInstance(tz, Locale.getDefault()).apply {
timeInMillis = timeMillis
set(Calendar.HOUR_OF_DAY, 0)
set(Calendar.MINUTE, 0)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}.timeInMillis
}
private fun groupPasses(passes: List<OrbitalPass>, isUtc: Boolean): Map<String, List<OrbitalPass>> { private fun groupPasses(passes: List<OrbitalPass>, isUtc: Boolean): Map<String, List<OrbitalPass>> {
val tz = if (isUtc) TimeZone.getTimeZone("UTC") else TimeZone.getDefault() val tz = if (isUtc) TimeZone.getTimeZone("UTC") else TimeZone.getDefault()
val sdfDate = dateFormat(tz) val sdfDate = dateFormat(tz)
@@ -461,19 +461,19 @@ private fun ExpandedRadioControl(
if (!radioControl.txPanel.isConnected && !radioControl.rxPanel.isConnected) { if (!radioControl.txPanel.isConnected && !radioControl.rxPanel.isConnected) {
CardButton( CardButton(
onClick = { onAction(RadarAction.ConnectRadios) }, onClick = { onAction(RadarAction.ConnectRadios) },
text = "Connect", text = stringResource(R.string.connect_radio),
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) )
} else { } else {
CardButton( CardButton(
onClick = { onAction(RadarAction.DisconnectRadios) }, onClick = { onAction(RadarAction.DisconnectRadios) },
text = "Disconnect", text = stringResource(R.string.disconnect_radio),
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) )
} }
CardButton( CardButton(
onClick = { onAction(RadarAction.ToggleTracking) }, 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) 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.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.layout.width 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.ScreenColumn
import com.rtbishop.look4sat.core.presentation.TopBar import com.rtbishop.look4sat.core.presentation.TopBar
import com.rtbishop.look4sat.core.presentation.infiniteMarquee 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 com.rtbishop.look4sat.core.presentation.isVerticalLayout
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
@@ -327,9 +330,11 @@ private fun SettingsScreen(uiState: SettingsState, onAction: (SettingsAction) ->
UiSettingsCard( UiSettingsCard(
hiddenScreens = uiState.otherSettings.hiddenScreens, hiddenScreens = uiState.otherSettings.hiddenScreens,
screenOrder = uiState.otherSettings.screenOrder, screenOrder = uiState.otherSettings.screenOrder,
subMenuOrder = uiState.otherSettings.subMenuOrder,
onToggle = { name -> onAction(SettingsAction.ToggleScreen(name)) }, onToggle = { name -> onAction(SettingsAction.ToggleScreen(name)) },
onReorder = { order -> onAction(SettingsAction.ReorderScreens(order)) }, onReorder = { order -> onAction(SettingsAction.ReorderScreens(order)) },
onResetOrder = { onAction(SettingsAction.ResetScreenOrder) } onResetOrder = { onAction(SettingsAction.ResetMenuOrder) },
onUpdateMenu = { main, sub -> onAction(SettingsAction.UpdateMenuOrder(main, sub)) }
) )
} }
item { CardCredits() } item { CardCredits() }
@@ -370,11 +375,11 @@ private fun LocationCard(
Text(text = formatUpdateTime(updateTime = settings.stationPos.timestamp)) Text(text = formatUpdateTime(updateTime = settings.stationPos.timestamp))
Spacer(modifier = Modifier.height(2.dp)) Spacer(modifier = Modifier.height(2.dp))
Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) { Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
Text(text = "Lat: ${settings.stationPos.latitude}°") Text(text = stringResource(R.string.loc_lat_text, settings.stationPos.latitude))
Text(text = "Lon: ${settings.stationPos.longitude}°") Text(text = stringResource(R.string.loc_lon_text, settings.stationPos.longitude))
} }
Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) { 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)) Spacer(modifier = Modifier.height(1.dp))
Row(horizontalArrangement = Arrangement.SpaceEvenly) { Row(horizontalArrangement = Arrangement.SpaceEvenly) {
@@ -557,9 +562,11 @@ private fun OtherCard(settings: OtherSettings, onAction: (SettingsAction) -> Uni
private fun UiSettingsCard( private fun UiSettingsCard(
hiddenScreens: List<String>, hiddenScreens: List<String>,
screenOrder: List<String>, screenOrder: List<String>,
subMenuOrder: List<String>,
onToggle: (String) -> Unit, onToggle: (String) -> Unit,
onReorder: (List<String>) -> Unit, onReorder: (List<String>) -> Unit,
onResetOrder: () -> Unit onResetOrder: () -> Unit,
onUpdateMenu: (List<String>, List<String>) -> Unit
) { ) {
val screens = listOf( val screens = listOf(
R.string.nav_sat to "Satellites", R.string.nav_sat to "Satellites",
@@ -571,6 +578,25 @@ private fun UiSettingsCard(
R.string.nav_map to "Map", R.string.nav_map to "Map",
R.string.nav_prefs to "Settings" R.string.nav_prefs to "Settings"
) // name 必须与 Screen.screenId 一致 (R8 安全) ) // 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()) { ElevatedCard(modifier = Modifier.fillMaxWidth()) {
Column( Column(
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), 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), text = stringResource(id = R.string.prefs_ui_order_title),
color = MaterialTheme.colorScheme.primary 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) { TextButton(onClick = onResetOrder) {
Text(text = stringResource(id = R.string.prefs_ui_order_reset)) Text(text = stringResource(id = R.string.prefs_ui_order_reset))
} }
@@ -605,25 +669,18 @@ private fun UiSettingsCard(
/** 页面顺序拖拽列表: 每行右侧抓手, 按住上下拖动实时换位, 松手持久化 */ /** 页面顺序拖拽列表: 每行右侧抓手, 按住上下拖动实时换位, 松手持久化 */
@Composable @Composable
private fun DragOrderList( private fun DragOrderList(
screens: List<Pair<Int, String>>, items: List<String>,
screenOrder: 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 onReorder: (List<String>) -> Unit
) { ) {
val itemHeight = 48.dp val itemHeight = 48.dp
val itemHeightPx = with(LocalDensity.current) { itemHeight.toPx() } 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 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 draggingIndex by remember { mutableIntStateOf(-1) }
var dragOffset by remember { mutableStateOf(0f) } var dragOffset by remember { mutableStateOf(0f) }
@@ -632,10 +689,11 @@ private fun DragOrderList(
state = listState, state = listState,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .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 isDragging = draggingIndex == index
val isLocked = name in locked
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier modifier = Modifier
@@ -646,46 +704,61 @@ private fun DragOrderList(
.animateItem() .animateItem()
) { ) {
Text( Text(
text = stringResource(id = screen.first), text = stringResource(id = labels.first { it.second == name }.first),
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) )
Box( if (moveLabel != null && onMove != null && moveEnabled(name)) {
modifier = Modifier TextButton(onClick = { onMove(name) }, contentPadding = PaddingValues(horizontal = 4.dp)) {
.size(48.dp) Text(moveLabel, fontSize = 11.sp)
.padding(20.dp) }
.pointerInput(screen.second) { }
detectDragGestures( if (isLocked) {
onDragStart = { draggingIndex = items.indexOf(screen) }, // 锁定行(设置页): 无抓手, 固定最后
onDrag = { change, dragAmount -> Text(
change.consume() text = stringResource(id = R.string.prefs_ui_order_locked),
dragOffset += dragAmount.y fontSize = 11.sp,
val currentIndex = draggingIndex color = MaterialTheme.colorScheme.onSurfaceVariant,
val target = (currentIndex + (dragOffset / itemHeightPx).roundToInt()) modifier = Modifier.padding(horizontal = 12.dp)
.coerceIn(0, items.size - 1) )
if (target != currentIndex) { } else {
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 圆点)
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .size(48.dp)
.background(MaterialTheme.colorScheme.onSurfaceVariant, CircleShape) .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 data class ReorderScreens(val order: List<String>) : SettingsAction
// UI 设置: 重置为默认顺序 // UI 设置: 重置为默认顺序
data object ResetScreenOrder : SettingsAction 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 // Remote control
data class UpdateRC(val settings: RCSettings) : SettingsAction data class UpdateRC(val settings: RCSettings) : SettingsAction
@@ -128,6 +128,12 @@ class SettingsViewModel(
} }
is SettingsAction.ReorderScreens -> settingsRepo.updateOtherSettings { it.copy(screenOrder = action.order) } is SettingsAction.ReorderScreens -> settingsRepo.updateOtherSettings { it.copy(screenOrder = action.order) }
SettingsAction.ResetScreenOrder -> settingsRepo.updateOtherSettings { it.copy(screenOrder = emptyList()) } 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 // Remote control & data sources
is SettingsAction.UpdateRC -> settingsRepo.updateRCSettings(action.settings) is SettingsAction.UpdateRC -> settingsRepo.updateRCSettings(action.settings)
is SettingsAction.UpdateRadioControl -> settingsRepo.updateRadioControlSettings(action.settings) is SettingsAction.UpdateRadioControl -> settingsRepo.updateRadioControlSettings(action.settings)
+2 -2
View File
@@ -1,8 +1,8 @@
[versions] [versions]
#noinspection UnusedVersionCatalogEntry #noinspection UnusedVersionCatalogEntry
appVersionCode = "450" appVersionCode = "451"
#noinspection UnusedVersionCatalogEntry #noinspection UnusedVersionCatalogEntry
appVersionName = "4.5.0" appVersionName = "4.5.1"
#noinspection UnusedVersionCatalogEntry #noinspection UnusedVersionCatalogEntry
compileSdk = "37" compileSdk = "37"
#noinspection UnusedVersionCatalogEntry #noinspection UnusedVersionCatalogEntry