From f7402a3531585b780e7047d49f56934cc815531f Mon Sep 17 00:00:00 2001 From: QIU Date: Tue, 4 Aug 2026 15:57:41 +0000 Subject: [PATCH] feat(status): 4.5.3 AMSAT satellite status page + radar log pass divider New feature/status module: fetches https://amsat.org/status/ and renders a live status grid in the official site colors: - Parser (AmSatParser): 47 satellites x 6 days x 12 two-hour slots, official colors (blue=Active, orange=TLM/Beacon, pink=Not Heard, deep-orange=Conflicting, gray=none); 598+ report details extracted from inline JS tooltips (callsign/date/time/grid) - Three-level viz: color grid -> report count -> tap day cell opens report list dialog - Manual refresh with spin animation + last-updated timestamp + legend row; loading/error states - New "AMSAT" entry in the More menu (Screen.AmSat), integrated with page-order / hide-page settings (SettingsScreen screens list, defaultSubMenuOrder, allNavItems, migration for existing users) - AmSatRepository via IRemoteSource.getStatusHtml() (UA header); shared remoteSource promoted to a lazy class property in MainContainer Radar page Log tab: local entries now grouped by pass session with a thick divider + satellite label between groups (matches the log page). What's new updated in EN/ZH/TR/IN/ID. Version bumped to 4.5.3/453. Not released (user gates all releases). --- .../java/com/rtbishop/look4sat/MainScreen.kt | 8 +- .../look4sat/convention/ApplicationPlugin.kt | 1 + .../core/data/injection/MainContainer.kt | 4 +- .../core/data/repository/AmSatRepository.kt | 21 ++ .../look4sat/core/data/source/AmSatParser.kt | 136 +++++++ .../look4sat/core/data/source/RemoteSource.kt | 16 + .../look4sat/core/domain/model/SatStatus.kt | 37 ++ .../domain/repository/IAmSatRepository.kt | 9 + .../core/domain/repository/IMainContainer.kt | 1 + .../core/domain/source/IRemoteSource.kt | 3 + .../look4sat/core/presentation/Navigation.kt | 5 +- .../src/main/res/values-id/strings.xml | 22 +- .../src/main/res/values-in/strings.xml | 21 +- .../src/main/res/values-tr/strings.xml | 19 +- .../src/main/res/values-zh/strings.xml | 19 +- .../src/main/res/values/strings.xml | 19 +- .../rtbishop/look4sat/feature/radar/LogTab.kt | 16 +- .../feature/settings/SettingsScreen.kt | 1 + feature/status/build.gradle.kts | 17 + .../feature/status/SatStatusScreen.kt | 343 ++++++++++++++++++ .../feature/status/SatStatusViewModel.kt | 84 +++++ gradle/libs.versions.toml | 4 +- settings.gradle.kts | 3 +- 23 files changed, 790 insertions(+), 19 deletions(-) create mode 100644 core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/AmSatRepository.kt create mode 100644 core/data/src/main/java/com/rtbishop/look4sat/core/data/source/AmSatParser.kt create mode 100644 core/domain/src/main/java/com/rtbishop/look4sat/core/domain/model/SatStatus.kt create mode 100644 core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/IAmSatRepository.kt create mode 100644 feature/status/build.gradle.kts create mode 100644 feature/status/src/main/java/com/rtbishop/look4sat/feature/status/SatStatusScreen.kt create mode 100644 feature/status/src/main/java/com/rtbishop/look4sat/feature/status/SatStatusViewModel.kt diff --git a/app/src/main/java/com/rtbishop/look4sat/MainScreen.kt b/app/src/main/java/com/rtbishop/look4sat/MainScreen.kt index 18d9fa23..009e0ac2 100644 --- a/app/src/main/java/com/rtbishop/look4sat/MainScreen.kt +++ b/app/src/main/java/com/rtbishop/look4sat/MainScreen.kt @@ -97,6 +97,7 @@ import com.rtbishop.look4sat.feature.mutual.MutualViewModel import com.rtbishop.look4sat.feature.passes.PassesDestination import com.rtbishop.look4sat.feature.radar.RadarDestination import com.rtbishop.look4sat.feature.radar.WavelogLogScreen +import com.rtbishop.look4sat.feature.status.SatStatusScreen import com.rtbishop.look4sat.feature.roaming.RoamingScreen import com.rtbishop.look4sat.feature.satellites.SatellitesDestination import com.rtbishop.look4sat.feature.settings.SettingsDestination @@ -148,7 +149,7 @@ fun MainScreen(navigateToRadar: () -> Unit = {}) { val trackingState by container.radioTrackingService.state.collectAsStateWithLifecycle() val otherSettings by container.settingsRepo.otherSettings.collectAsStateWithLifecycle() // UI 设置: 按 screenOrder 排序(空 = 默认顺序), 再按 hiddenScreens 过滤(设置页固定保留) - val allNavItems = listOf(Screen.Satellites, Screen.Passes, Screen.Radar, Screen.Mutual, Screen.Roaming, Screen.CwDecode, Screen.WavelogLog, Screen.Map, Screen.Settings) + val allNavItems = listOf(Screen.Satellites, Screen.Passes, Screen.Radar, Screen.Mutual, Screen.Roaming, Screen.CwDecode, Screen.WavelogLog, Screen.AmSat, Screen.Map, Screen.Settings) .sortedBy { screen -> // 未知(新页面如 CwDecode 不在旧持久化顺序里): 用默认顺序位置(漫游↔地图), 再兜底最后 val idx = otherSettings.screenOrder.indexOf(screen.screenId) @@ -162,6 +163,7 @@ fun MainScreen(navigateToRadar: () -> Unit = {}) { // 老用户迁移: 已持久化的 subMenuOrder 不含新页面 WavelogLog → 追加到子菜单尾部 val subOrder = (otherSettings.subMenuOrder.ifEmpty { com.rtbishop.look4sat.core.presentation.defaultSubMenuOrder }) .let { list -> if ("WavelogLog" in list) list else list + "WavelogLog" } + .let { list -> if ("AMSAT" in list) list else list + "AMSAT" } val mainNavItems = remember(allNavItems, subOrder) { allNavItems.filter { it.screenId !in subOrder }.take(5) } @@ -193,6 +195,7 @@ fun MainScreen(navigateToRadar: () -> Unit = {}) { is Screen.Mutual -> screen is Screen.Mutual is Screen.CwDecode -> screen is Screen.CwDecode is Screen.WavelogLog -> screen is Screen.WavelogLog + is Screen.AmSat -> screen is Screen.AmSat is Screen.Map -> screen is Screen.Map is Screen.Settings -> screen is Screen.Settings else -> false @@ -283,6 +286,9 @@ fun MainScreen(navigateToRadar: () -> Unit = {}) { entry { CwDecodeScreen() } + entry { + SatStatusScreen(container = container) + } entry { WavelogLogScreen(queue = container.wavelogQueue) } diff --git a/build-logic/convention/src/main/java/com/rtbishop/look4sat/convention/ApplicationPlugin.kt b/build-logic/convention/src/main/java/com/rtbishop/look4sat/convention/ApplicationPlugin.kt index 2ff4f72d..e91f4ed1 100644 --- a/build-logic/convention/src/main/java/com/rtbishop/look4sat/convention/ApplicationPlugin.kt +++ b/build-logic/convention/src/main/java/com/rtbishop/look4sat/convention/ApplicationPlugin.kt @@ -39,6 +39,7 @@ internal class ApplicationPlugin : Plugin { implementation(project(":feature:roaming")) implementation(project(":feature:satellites")) implementation(project(":feature:settings")) + implementation(project(":feature:status")) implementation(libs.androidx.core.splashscreen) implementation(libs.compose.material3.adaptive) implementation(libs.compose.navigation3) diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/injection/MainContainer.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/injection/MainContainer.kt index bf9b71b4..ac93635d 100644 --- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/injection/MainContainer.kt +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/injection/MainContainer.kt @@ -29,6 +29,7 @@ import com.rtbishop.look4sat.core.data.framework.Ft817Controller import com.rtbishop.look4sat.core.data.framework.Ic705Controller import com.rtbishop.look4sat.core.data.framework.NetworkReporter import com.rtbishop.look4sat.core.data.framework.RadioTrackingService +import com.rtbishop.look4sat.core.data.repository.AmSatRepository import com.rtbishop.look4sat.core.data.repository.DatabaseRepo import com.rtbishop.look4sat.core.data.repository.SatelliteRepo import com.rtbishop.look4sat.core.data.repository.SelectionRepo @@ -73,12 +74,14 @@ import okhttp3.OkHttpClient class MainContainer(private val context: Context) : IMainContainer { private val localSource = provideLocalSource() + private val remoteSource by lazy { provideRemoteSource() } private val mainHandler = CoroutineExceptionHandler { _, error -> println("MainHandler: $error") } override val appScope = CoroutineScope(SupervisorJob() + Dispatchers.Default + mainHandler) override val settingsRepo = provideSettingsRepo() override val selectionRepo = provideSelectionRepo() override val satelliteRepo = provideSatelliteRepo() override val databaseRepo = provideDatabaseRepo() + override val amSatRepo = AmSatRepository(remoteSource) override val radioTrackingService: IRadioTrackingService by lazy { val manager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager RadioTrackingService(appScope, manager, satelliteRepo, settingsRepo) @@ -162,7 +165,6 @@ class MainContainer(private val context: Context) : IMainContainer { private fun provideDatabaseRepo(): IDatabaseRepo { val dbDispatcher = Dispatchers.Default val dataParser = DataParser(dbDispatcher) - val remoteSource = provideRemoteSource() return DatabaseRepo(dbDispatcher, dataParser, localSource, remoteSource, settingsRepo) } diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/AmSatRepository.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/AmSatRepository.kt new file mode 100644 index 00000000..b992bfe5 --- /dev/null +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/AmSatRepository.kt @@ -0,0 +1,21 @@ +package com.rtbishop.look4sat.core.data.repository + +import com.rtbishop.look4sat.core.data.source.AmSatParser +import com.rtbishop.look4sat.core.domain.model.SatStatusPage +import com.rtbishop.look4sat.core.domain.repository.IAmSatRepository +import com.rtbishop.look4sat.core.domain.source.IRemoteSource + +/** AMSAT 状态仓库: 抓 HTML → 解析 → SatStatusPage */ +class AmSatRepository(private val remoteSource: IRemoteSource) : IAmSatRepository { + + override suspend fun fetchStatus(): SatStatusPage? { + val html = remoteSource.getStatusHtml() ?: return null + if (html.isBlank()) return null + return try { + AmSatParser.parse(html, System.currentTimeMillis()) + } catch (exception: Exception) { + println("AmSatRepository parse exception: $exception") + null + } + } +} diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/source/AmSatParser.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/source/AmSatParser.kt new file mode 100644 index 00000000..f67ae677 --- /dev/null +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/source/AmSatParser.kt @@ -0,0 +1,136 @@ +package com.rtbishop.look4sat.core.data.source + +import com.rtbishop.look4sat.core.domain.model.SatDay +import com.rtbishop.look4sat.core.domain.model.SatReport +import com.rtbishop.look4sat.core.domain.model.SatSlot +import com.rtbishop.look4sat.core.domain.model.SatStatus +import com.rtbishop.look4sat.core.domain.model.SatStatusPage +import java.util.regex.Pattern + +/** + * AMSAT 卫星状态页解析器(https://amsat.org/status/) + * + * 页面结构(静态 HTML, 2026-08 实测): + * - 状态表: 内 48 行, 表头 = Name + 6 天(每个 colspan=12) + * 数据行 73 格: [0]=卫星名, [1..72] = 6 天 × 12 个 2 小时槽(新→旧) + * 每格 , 有报告时数字带 + * docTips.show('id') 链接 + * - 报告详情在页面内嵌 JS: + * tips.a885153 = new Array(5,5,120,'状态
呼号
网格
日期
时间段 UTC') + * + * 状态色: #648fff=活跃, #ffb000=仅遥测, #dc267f=未听到, #fe6100=矛盾 + */ +object AmSatParser { + + private val STATUS_COLORS = mapOf( + "#648fff" to 0xFF648FFF.toLong(), + "#ffb000" to 0xFFFFB000.toLong(), + "#dc267f" to 0xFFDC267F.toLong(), + "#fe6100" to 0xFFFE6100.toLong() + ) + private val GRAY = 0xFFC0C0C0.toLong() + + private val rowRe = Pattern.compile("(.*?)", Pattern.DOTALL) + private val cellRe = Pattern.compile("(]*>.*?)", Pattern.DOTALL) + private val bgRe = Pattern.compile("bgcolor=\"?(#[0-9a-fA-F]{6}|C0C0C0)\"?") + private val linkRe = Pattern.compile("docTips\\.show\\('(a\\d+)'\\)") + private val tipRe = Pattern.compile( + "tips\\.(a\\d+)\\s*=\\s*new\\s+Array\\(\\s*\\d+,\\s*\\d+,\\s*\\d+,\\s*'([^']*)'" + ) + + /** 解析完整页面 */ + fun parse(html: String, fetchedAtUtcMs: Long): SatStatusPage { + val reports = parseReports(html) + val statuses = parseStatusTable(html, reports) + return SatStatusPage(fetchedAtUtcMs, statuses, reports) + } + + /** 提取全部报告(tips.* JS 数组) */ + fun parseReports(html: String): Map { + val map = mutableMapOf() + val m = tipRe.matcher(html) + while (m.find()) { + val id = m.group(1) + val parts = m.group(2).split("
") + map[id] = SatReport( + id = id, + statusText = parts.getOrElse(0) { "" }.trim(), + call = parts.getOrElse(1) { "" }.trim(), + grid = parts.getOrElse(2) { "" }.trim(), + dateUtc = parts.getOrElse(3) { "" }.trim(), + timeUtc = parts.getOrElse(4) { "" }.trim() + ) + } + return map + } + + /** 解析状态表(48 行卫星) */ + fun parseStatusTable(html: String, reports: Map): List { + val result = mutableListOf() + val tables = extractTables(html) + for (table in tables) { + val rows = rowRe.matcher(table) + val parsed = mutableListOf() + var rowIndex = 0 + var dayHeaders: List = emptyList() + while (rows.find()) { + val rowHtml = rows.group(1) + val cells = cellRe.matcher(rowHtml) + val cellList = mutableListOf() + while (cells.find()) cellList.add(cells.group(1)) + if (rowIndex == 0) { + // 表头: Name + 6 天(每个 colspan=12) + dayHeaders = cellList.drop(1).take(6).map { stripHtml(it) } + rowIndex++ + continue + } + if (cellList.size < 7) { rowIndex++; continue } + val name = stripHtml(cellList[0]) + if (name.isBlank()) { rowIndex++; continue } + val days = mutableListOf() + for (d in 0 until 6) { + val start = 1 + d * 12 + val end = start + 12 + val slots = (start until end).mapNotNull { i -> + cellList.getOrNull(i)?.let { cell -> + val bg = bgRe.matcher(cell) + val color = if (bg.find()) { + STATUS_COLORS[bg.group(1).lowercase()] ?: GRAY + } else GRAY + val link = linkRe.matcher(cell) + val ids = mutableListOf() + while (link.find()) ids.add(link.group(1)) + val hasReport = ids.isNotEmpty() + val count = if (hasReport) { + stripHtml(cell).trim().toIntOrNull() ?: ids.size + } else 0 + SatSlot( + statusColor = if (hasReport) color else GRAY, + count = count, + reportIds = ids + ) + } + } + days.add(SatDay(dayHeaders.getOrElse(d) { "" }, slots)) + } + parsed.add(SatStatus(name, days)) + rowIndex++ + } + if (parsed.isNotEmpty()) { + result.addAll(parsed) + break + } + } + return result + } + + private fun extractTables(html: String): List { + val result = mutableListOf() + val m = Pattern.compile("", Pattern.DOTALL).matcher(html) + while (m.find()) result.add(m.group()) + return result + } + + private fun stripHtml(s: String): String = + s.replace(Regex("<[^>]+>"), "").trim() +} diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/source/RemoteSource.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/source/RemoteSource.kt index 300e930d..b65cfb8e 100644 --- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/source/RemoteSource.kt +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/source/RemoteSource.kt @@ -43,6 +43,22 @@ class RemoteSource( } } + override suspend fun getStatusHtml(): String? = withContext(dispatcher) { + try { + val request = Request.Builder() + .url("https://amsat.org/status/") + .header("User-Agent", "Mozilla/5.0 (Linux; Android 13) Look4Sat/4.5") + .build() + httpClient.newCall(request).execute().use { response -> + if (!response.isSuccessful) return@use null + response.body?.string() + } + } catch (exception: Exception) { + println("RemoteSource amsat status exception: $exception") + null + } + } + override suspend fun getNetworkStream(url: String): InputStream? = withContext(dispatcher) { try { val networkRequest = Request.Builder().url(url).build() diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/model/SatStatus.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/model/SatStatus.kt new file mode 100644 index 00000000..3b76516c --- /dev/null +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/model/SatStatus.kt @@ -0,0 +1,37 @@ +package com.rtbishop.look4sat.core.domain.model + +/** 单条卫星状态报告(AMSAT 网站 tooltip 数据) */ +data class SatReport( + val id: String, // 报告 ID(a885153) + val statusText: String, // Heard / Telemetry Only / Not Heard ... + val call: String, // 呼号 + val grid: String, // 网格坐标(可为空) + val dateUtc: String, // 2026-08-04 + val timeUtc: String // 2:46-:59 UTC +) + +/** 单个 2 小时槽的状态 */ +data class SatSlot( + val statusColor: Long, // ARGB 状态色(-1 = 无报告) + val count: Int, // 报告数量(0 = 无) + val reportIds: List = emptyList() // 该槽报告 ID 列表 +) + +/** 卫星一天的状态(12 个 2 小时槽) */ +data class SatDay( + val dateLabel: String, // "Aug 4" + val slots: List // 12 槽(00-02 ... 22-24) +) + +/** 单个卫星 6 天状态 */ +data class SatStatus( + val name: String, // "AO-123_[FM]" + val days: List // 6 天(新→旧) +) + +/** 页面整体解析结果 */ +data class SatStatusPage( + val fetchedAtUtcMs: Long, + val statuses: List, + val reports: Map // id → 报告 +) diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/IAmSatRepository.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/IAmSatRepository.kt new file mode 100644 index 00000000..c8f11b1c --- /dev/null +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/IAmSatRepository.kt @@ -0,0 +1,9 @@ +package com.rtbishop.look4sat.core.domain.repository + +import com.rtbishop.look4sat.core.domain.model.SatStatusPage + +/** AMSAT 卫星状态数据源 */ +interface IAmSatRepository { + /** 抓取并解析 AMSAT 状态页; 失败返回 null */ + suspend fun fetchStatus(): SatStatusPage? +} diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/IMainContainer.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/IMainContainer.kt index ff0bfd00..1268f581 100644 --- a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/IMainContainer.kt +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/IMainContainer.kt @@ -30,6 +30,7 @@ interface IMainContainer { val selectionRepo: ISelectionRepo val satelliteRepo: ISatelliteRepo val databaseRepo: IDatabaseRepo + val amSatRepo: IAmSatRepository val radioTrackingService: IRadioTrackingService val mutualPassData: StateFlow fun setMutualPassData(data: MutualPassData) diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/source/IRemoteSource.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/source/IRemoteSource.kt index 1ba5d62d..8437c927 100644 --- a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/source/IRemoteSource.kt +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/source/IRemoteSource.kt @@ -22,4 +22,7 @@ import java.io.InputStream interface IRemoteSource { suspend fun getFileStream(uri: String): InputStream? suspend fun getNetworkStream(url: String): InputStream? + + /** 抓取 AMSAT 状态页 HTML(带 UA, 返回 null = 失败) */ + suspend fun getStatusHtml(): String? } 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 912e85cc..b44596b4 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 @@ -47,6 +47,9 @@ sealed class Screen(val iconResId: Int, val titleResId: Int, val screenId: Strin @Serializable data object WavelogLog : Screen(R.drawable.ic_log, R.string.nav_log, "WavelogLog") + @Serializable + data object AmSat : Screen(R.drawable.ic_satellites, R.string.nav_amsat, "AMSAT") + @Serializable data object Settings : Screen(R.drawable.ic_settings, R.string.nav_prefs, "Settings") } @@ -55,7 +58,7 @@ sealed class Screen(val iconResId: Int, val titleResId: Int, val screenId: Strin val defaultScreenOrder = listOf("Satellites", "Passes", "Radar", "Map", "Settings") // UI 设置: 默认更多菜单顺序(进「更多」的页面) -val defaultSubMenuOrder = listOf("Mutual", "Roaming", "CwDecode", "WavelogLog") +val defaultSubMenuOrder = listOf("Mutual", "Roaming", "CwDecode", "WavelogLog", "AMSAT") @Serializable data object RadarDestination : NavKey diff --git a/core/presentation/src/main/res/values-id/strings.xml b/core/presentation/src/main/res/values-id/strings.xml index 42aebd52..2d476b00 100644 --- a/core/presentation/src/main/res/values-id/strings.xml +++ b/core/presentation/src/main/res/values-id/strings.xml @@ -14,6 +14,18 @@ Pengaturan Lainnya Log + AMSAT + Status Satelit AMSAT + Diperbarui: + Aktif + TLM/beacon + Tidak terdengar + Konflik + Name + Gagal memuat - periksa jaringan dan coba lagi + Tidak ada laporan untuk periode ini + Tutup + Jenis: %s Pilih jenis satelit Id - Nama @@ -48,9 +60,13 @@ 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 - - * WaveLog kini mendukung API v1 dan v2 (terdeteksi otomatis); dialog kesalahan dengan tombol salin - * Frekuensi log cocok dengan panel Doppler; transponder linier menampilkan rentang penuh + + * BARU: Halaman status satelit AMSAT - tabel status langsung (warna resmi), jumlah laporan, laporan detail per satelit/hari + * Log dikelompokkan per lintasan satelit (sesi = satelit + waktu AOS), garis pemisah antar sesi + * GPS kini hanya menampilkan sukses saat posisi benar-benar didapat (timeout 15 dtk, cek izin) + * Pesan kegagalan jelas saat database satelit gagal diunduh + * Frekuensi log mengikuti panel Doppler secara real-time; visual hapus geser dipulihkan + * Perbaikan bahasa Indonesia (values-id) Kembali Beri tahu Azimuth diff --git a/core/presentation/src/main/res/values-in/strings.xml b/core/presentation/src/main/res/values-in/strings.xml index a055c8b3..0b89acaf 100644 --- a/core/presentation/src/main/res/values-in/strings.xml +++ b/core/presentation/src/main/res/values-in/strings.xml @@ -14,6 +14,17 @@ Pengaturan Lainnya Log + AMSAT + Status Satelit AMSAT + Diperbarui: + Aktif + TLM/beacon + Tidak terdengar + Konflik + Name + Gagal memuat - periksa jaringan dan coba lagi + Tidak ada laporan untuk periode ini + Tutup Jenis: %s Pilih jenis satelit Id - Nama @@ -48,9 +59,13 @@ 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 - - * WaveLog kini mendukung API v1 dan v2 (terdeteksi otomatis); dialog kesalahan dengan tombol salin - * Frekuensi log cocok dengan panel Doppler; transponder linier menampilkan rentang penuh + + * BARU: Halaman status satelit AMSAT - tabel status langsung (warna resmi), jumlah laporan, laporan detail per satelit/hari + * Log dikelompokkan per lintasan satelit (sesi = satelit + waktu AOS), garis pemisah antar sesi + * GPS kini hanya menampilkan sukses saat posisi benar-benar didapat (timeout 15 dtk, cek izin) + * Pesan kegagalan jelas saat database satelit gagal diunduh + * Frekuensi log mengikuti panel Doppler secara real-time; visual hapus geser dipulihkan + * Perbaikan bahasa Indonesia (values-id) Kembali Beri tahu Azimuth diff --git a/core/presentation/src/main/res/values-tr/strings.xml b/core/presentation/src/main/res/values-tr/strings.xml index df14a0de..6a924337 100644 --- a/core/presentation/src/main/res/values-tr/strings.xml +++ b/core/presentation/src/main/res/values-tr/strings.xml @@ -15,6 +15,17 @@ Ayarlar Daha Fazla Günlük + AMSAT + AMSAT Uydu Durumu + Güncellendi: + Aktif + TLM/işaret + Duyulmadı + Çelişkili + Name + Yükleme başarısız - ağı kontrol edip tekrar deneyin + Bu dönem için rapor yok + Kapat Uydu türü: %s @@ -54,8 +65,12 @@ \n\nDoğru tahminler alabilmek için veritabanını en az haftada bir güncelleyin. What\'s new in Look4Sat - * WaveLog artık hem API v1 hem v2 destekliyor (otomatik algılama); kopyalama butonlu hata penceresi - * Günlük frekansları Doppler paneliyle eşleşiyor; doğrusal transponderler tam aralığı gösteriyor + * YENİ: AMSAT uydu durum sayfası - canlı durum tablosu (resmi renkler), rapor sayıları, uydu/gün bazında detaylı raporlar + * Günlükler uydu geçişine göre gruplanır (oturum = uydu + AOS zamanı), oturumlar arasında ayırıcı çizgi + * GPS artık yalnızca gerçek konum alındığında başarı gösterir (15 sn zaman aşımı, izin kontrolü) + * Uydu veritabanı indirilemediğinde net hata mesajı gösterilir + * Günlük frekansları Doppler panelini gerçek zamanlı takip eder; kaydırarak silme görselleri düzeltildi + * Endonezya dili düzeltmesi (values-id) Geri diff --git a/core/presentation/src/main/res/values-zh/strings.xml b/core/presentation/src/main/res/values-zh/strings.xml index 97ae0f51..ae2b403d 100644 --- a/core/presentation/src/main/res/values-zh/strings.xml +++ b/core/presentation/src/main/res/values-zh/strings.xml @@ -16,6 +16,17 @@ 设置 更多 日志 + AMSAT + AMSAT 卫星状态 + 更新: + 活跃 + 仅遥测 + 未听到 + 矛盾 + Name + 加载失败,请检查网络后重试 + 该时段无报告 + 关闭 类型:%s @@ -44,8 +55,12 @@ 请务必在设置中通过GPS、经纬度或QTH定位您的位置\n建议至少每周更新一次数据库,以确保预测结果的准确性 Look4Sat Pro 更新内容 - * WaveLog 同时支持 API v1 和 v2(自动识别); 错误弹窗带一键复制 - * 日志频率与多普勒面板一致; 线性转发器显示完整频段区间 + * 新增: AMSAT 卫星状态页 - 实时状态表格(官网配色)、报告数量、逐卫星/逐日详细报告 + * 日志按场次分组(场次 = 卫星 + AOS 时间), 组间横线分隔 + * GPS 真正定位成功才会提示成功(15 秒超时, 权限检查) + * 卫星数据库下载失败时有明确失败提示 + * 日志频率实时跟随多普勒面板; 左滑删除视觉恢复 + * 印尼语识别修复(values-id) 后退 diff --git a/core/presentation/src/main/res/values/strings.xml b/core/presentation/src/main/res/values/strings.xml index 1324d113..d802234d 100644 --- a/core/presentation/src/main/res/values/strings.xml +++ b/core/presentation/src/main/res/values/strings.xml @@ -17,6 +17,17 @@ Settings More Log + AMSAT + AMSAT Satellite Status + Updated: + Active + TLM/Beacon + Not Heard + Conflicting + Name + Load failed - check network and retry + No reports for this period + Close Type: %s @@ -56,8 +67,12 @@ \n\nPlease update the database at least weekly to get accurate predictions. What\'s new in Look4Sat - * WaveLog now supports both API v1 and v2 (auto-detected); error dialog with copy button - * Log frequencies match the Doppler panel; linear transponders show the full range + * NEW: AMSAT satellite status page - live status grid (official colors), report counts, detailed reports per satellite/day + * Log entries grouped by satellite pass (session ID = satellite + AOS time), divider lines between sessions + * GPS now only reports success when a real fix is acquired (15s timeout, permission check) + * Data update shows a clear failure message when the satellite database cannot be downloaded + * Log frequencies follow the Doppler panel in real time; swipe-to-delete visuals restored + * Indonesian locale fix (values-id) Back diff --git a/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/LogTab.kt b/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/LogTab.kt index 61cd4209..89896d9e 100644 --- a/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/LogTab.kt +++ b/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/LogTab.kt @@ -40,6 +40,7 @@ import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.ExposedDropdownMenuDefaults import androidx.compose.material3.MenuAnchorType import androidx.compose.material3.Icon +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text @@ -179,7 +180,19 @@ fun LogTab( modifier = Modifier.padding(vertical = 8.dp) ) } else { - entries.forEach { entry -> + // 按场次分组(sessionId = 卫星名-AOS 时间戳), 组间粗线分隔 + entries.groupBy { it.sessionId.ifBlank { "un" } }.forEach { (sessionId, groupEntries) -> + // 组标题 + 分隔线 + HorizontalDivider(thickness = 2.dp, color = MaterialTheme.colorScheme.primary) + Text( + text = if (sessionId == "un") stringResource(id = R.string.wavelog_ungrouped) + else sessionId.substringBeforeLast('-'), + fontSize = 11.sp, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp) + ) + groupEntries.forEach { entry -> SwipeDeleteRow( onDelete = { queue.remove(entry.id) @@ -213,6 +226,7 @@ fun LogTab( ) } } + } } } } 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 617b7564..f5a33c29 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 @@ -705,6 +705,7 @@ private fun UiSettingsCard( R.string.nav_roaming to "Roaming", R.string.nav_cw to "CwDecode", R.string.nav_log to "WavelogLog", + R.string.nav_amsat to "AMSAT", R.string.nav_map to "Map", R.string.nav_prefs to "Settings" ) // name 必须与 Screen.screenId 一致 (R8 安全) diff --git a/feature/status/build.gradle.kts b/feature/status/build.gradle.kts new file mode 100644 index 00000000..77b30387 --- /dev/null +++ b/feature/status/build.gradle.kts @@ -0,0 +1,17 @@ +import com.android.build.api.dsl.LibraryExtension + +plugins { + alias(libs.plugins.convention.featurePlugin) +} + +android { + namespace = "com.rtbishop.look4sat.feature.status" + compileOptions { + encoding = "UTF-8" + } +} + +dependencies { + implementation(project(":core:domain")) + implementation(project(":core:presentation")) +} diff --git a/feature/status/src/main/java/com/rtbishop/look4sat/feature/status/SatStatusScreen.kt b/feature/status/src/main/java/com/rtbishop/look4sat/feature/status/SatStatusScreen.kt new file mode 100644 index 00000000..9d3d923c --- /dev/null +++ b/feature/status/src/main/java/com/rtbishop/look4sat/feature/status/SatStatusScreen.kt @@ -0,0 +1,343 @@ +package com.rtbishop.look4sat.feature.status + +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +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.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +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 +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.initializer +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.lifecycle.viewmodel.viewModelFactory +import com.rtbishop.look4sat.core.domain.model.SatDay +import com.rtbishop.look4sat.core.domain.model.SatReport +import com.rtbishop.look4sat.core.domain.model.SatSlot +import com.rtbishop.look4sat.core.domain.model.SatStatus +import com.rtbishop.look4sat.core.domain.repository.IMainContainer +import com.rtbishop.look4sat.core.presentation.R +import java.util.Calendar + +// ========== 官网状态色(amsat.org/status 原色) ========== +val ActiveBlue = Color(0xFF648FFF) +val TlmOrange = Color(0xFFFFB000) +val NotHeardPink = Color(0xFFDC267F) +val ConflictDeepOrange = Color(0xFFFE6100) +val NoReportGray = Color(0xFFC0C0C0) + +@Composable +fun SatStatusScreen(container: IMainContainer) { + val viewModel: SatStatusViewModel = viewModel( + factory = viewModelFactory { + initializer { SatStatusViewModel(container) } + } + ) + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + var selectedDay by remember { mutableStateOf?>(null) } + + Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp)) { + // 顶部: 图例 + 刷新 + StatusHeader( + fetchedAtUtcMs = uiState.fetchedAtUtcMs, + isRefreshing = uiState.isRefreshing, + onRefresh = { viewModel.refresh() } + ) + LegendRow() + + when { + uiState.isLoading -> { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator() + } + } + uiState.error != null && uiState.statuses.isEmpty() -> { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Text( + text = stringResource(id = R.string.amsat_load_failed), + color = MaterialTheme.colorScheme.error + ) + } + } + else -> { + // 表头 + HeaderRow(statuses = uiState.statuses) + HorizontalDivider(thickness = 1.dp) + // 行 + LazyColumn(modifier = Modifier.fillMaxSize()) { + items(uiState.statuses, key = { it.name }) { status -> + StatusRow( + status = status, + onClickDay = { day -> selectedDay = status to day } + ) + } + } + } + } + } + + selectedDay?.let { (status, day) -> + ReportDialog( + statusName = status.name, + day = day, + reports = uiState.reports, + onDismiss = { selectedDay = null } + ) + } +} + +/** 顶部: 更新时间 + 刷新按钮(加载中旋转动画) */ +@Composable +private fun StatusHeader( + fetchedAtUtcMs: Long, + isRefreshing: Boolean, + onRefresh: () -> Unit +) { + val infiniteTransition = rememberInfiniteTransition(label = "refresh") + val angle by infiniteTransition.animateFloat( + initialValue = 0f, + targetValue = 360f, + animationSpec = infiniteRepeatable(tween(800), RepeatMode.Restart), + label = "angle" + ) + Row( + modifier = Modifier.fillMaxWidth().padding(vertical = 6.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = if (fetchedAtUtcMs > 0) stringResource(id = R.string.amsat_updated) + " " + formatFetchedAt(fetchedAtUtcMs) else stringResource(id = R.string.amsat_title), + fontSize = 13.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.weight(1f) + ) + Box( + modifier = Modifier + .size(34.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.surfaceVariant) + .clickable(onClick = onRefresh), + contentAlignment = Alignment.Center + ) { + if (isRefreshing) { + CircularProgressIndicator(modifier = Modifier.size(18.dp), strokeWidth = 2.dp) + } else { + Text( + text = "↻", + fontSize = 20.sp, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier.rotate(angle) + ) + } + } + } +} + +/** 图例(官网四色) */ +@Composable +private fun LegendRow() { + val legend = listOf( + stringResource(id = R.string.amsat_active) to ActiveBlue, + stringResource(id = R.string.amsat_tlm) to TlmOrange, + stringResource(id = R.string.amsat_not_heard) to NotHeardPink, + stringResource(id = R.string.amsat_conflict) to ConflictDeepOrange + ) + Row( + modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + legend.forEach { (label, color) -> + Row(verticalAlignment = Alignment.CenterVertically) { + Box( + modifier = Modifier + .size(10.dp) + .clip(RoundedCornerShape(2.dp)) + .background(color) + ) + Spacer(modifier = Modifier.width(3.dp)) + Text(text = label, fontSize = 10.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) + } + } + } +} + +/** 表头: 卫星名 + 6 天日期 */ +@Composable +private fun HeaderRow(statuses: List) { + val dates = statuses.firstOrNull()?.days?.map { it.dateLabel } ?: emptyList() + Row( + modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = stringResource(id = R.string.amsat_name), + fontSize = 11.sp, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier.weight(1.4f).padding(start = 4.dp) + ) + dates.forEach { date -> + Text( + text = date, + fontSize = 10.sp, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier.weight(1f) + ) + } + } +} + +/** 卫星行: 名称 + 6 天色块(官网色 + 报告数) */ +@Composable +private fun StatusRow(status: SatStatus, onClickDay: (SatDay) -> Unit) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 3.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = status.name, + fontSize = 11.sp, + maxLines = 1, + overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis, + modifier = Modifier.weight(1.4f).padding(start = 4.dp) + ) + status.days.forEach { day -> + val slot = day.slots.firstOrNull { it.statusColor != NoReportGray.value.toInt().toLong() } ?: day.slots.first() + DayCell( + slot = slot, + modifier = Modifier.weight(1f).padding(horizontal = 1.dp), + onClick = { onClickDay(day) } + ) + } + } +} + +/** 单天色块: 取当天 12 槽中最新的有报告状态; 无报告则灰 */ +@Composable +private fun DayCell(slot: SatSlot, modifier: Modifier, onClick: () -> Unit) { + val color = Color(slot.statusColor) + Box( + modifier = modifier + .height(26.dp) + .clip(RoundedCornerShape(4.dp)) + .background(color) + .clickable(onClick = onClick), + contentAlignment = Alignment.Center + ) { + if (slot.count > 0) { + Text( + text = slot.count.toString(), + fontSize = 11.sp, + fontWeight = FontWeight.Bold, + color = Color.White + ) + } + } +} + +/** 报告详情弹窗(3 级: 呼号/日期/时间/网格) */ +@Composable +private fun ReportDialog( + statusName: String, + day: SatDay, + reports: Map, + onDismiss: () -> Unit +) { + val dayReports = day.slots.flatMap { it.reportIds } + .mapNotNull { reports[it] } + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(text = "$statusName · ${day.dateLabel}") }, + text = { + if (dayReports.isEmpty()) { + Text(stringResource(id = R.string.amsat_no_reports)) + } else { + LazyColumn(modifier = Modifier.height(320.dp)) { + items(dayReports) { report -> + Column(modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + Box( + modifier = Modifier + .size(8.dp) + .clip(CircleShape) + .background(statusColorOf(report.statusText)) + ) + Spacer(modifier = Modifier.width(6.dp)) + Text( + text = report.statusText, + fontSize = 13.sp, + fontWeight = FontWeight.Bold + ) + } + Text( + text = "${report.call} ${report.dateUtc} ${report.timeUtc}" + + if (report.grid.isNotBlank() && report.grid != "-") " ${report.grid}" else "", + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + HorizontalDivider(thickness = 0.5.dp) + } + } + } + }, + confirmButton = { + TextButton(onClick = onDismiss) { Text(stringResource(id = R.string.amsat_close)) } + } + ) +} + +private fun statusColorOf(statusText: String): Color = when { + statusText.contains("Heard", ignoreCase = true) && !statusText.contains("Not", ignoreCase = true) -> ActiveBlue + statusText.contains("Telemetry", ignoreCase = true) || statusText.contains("Beacon", ignoreCase = true) -> TlmOrange + statusText.contains("Not Heard", ignoreCase = true) -> NotHeardPink + else -> ConflictDeepOrange +} + +private fun formatFetchedAt(utcMs: Long): String { + val cal = Calendar.getInstance() + cal.timeInMillis = utcMs + return "%02d-%02d %02d:%02d".format( + cal.get(Calendar.MONTH) + 1, cal.get(Calendar.DAY_OF_MONTH), + cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE) + ) +} diff --git a/feature/status/src/main/java/com/rtbishop/look4sat/feature/status/SatStatusViewModel.kt b/feature/status/src/main/java/com/rtbishop/look4sat/feature/status/SatStatusViewModel.kt new file mode 100644 index 00000000..a6997781 --- /dev/null +++ b/feature/status/src/main/java/com/rtbishop/look4sat/feature/status/SatStatusViewModel.kt @@ -0,0 +1,84 @@ +package com.rtbishop.look4sat.feature.status + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.rtbishop.look4sat.core.domain.model.SatReport +import com.rtbishop.look4sat.core.domain.model.SatStatus +import com.rtbishop.look4sat.core.domain.repository.IMainContainer +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +data class SatStatusUiState( + val isLoading: Boolean = false, + val isRefreshing: Boolean = false, + val statuses: List = emptyList(), + val reports: Map = emptyMap(), + val fetchedAtUtcMs: Long = 0L, + val error: String? = null +) + +class SatStatusViewModel( + private val container: IMainContainer +) : ViewModel() { + + private val _uiState = MutableStateFlow(SatStatusUiState(isLoading = true)) + val uiState: StateFlow = _uiState + + init { + fetch() + } + + fun fetch() { + viewModelScope.launch { + _uiState.update { it.copy(isLoading = true, error = null) } + try { + val page = container.amSatRepo.fetchStatus() + if (page != null && page.statuses.isNotEmpty()) { + _uiState.update { + it.copy( + isLoading = false, + isRefreshing = false, + statuses = page.statuses, + reports = page.reports, + fetchedAtUtcMs = page.fetchedAtUtcMs + ) + } + } else { + _uiState.update { + it.copy(isLoading = false, isRefreshing = false, error = "fetch_failed") + } + } + } catch (exception: Exception) { + _uiState.update { + it.copy(isLoading = false, isRefreshing = false, error = exception.message ?: "fetch_failed") + } + } + } + } + + fun refresh() { + if (_uiState.value.isRefreshing) return + _uiState.update { it.copy(isRefreshing = true, error = null) } + viewModelScope.launch { + try { + val page = container.amSatRepo.fetchStatus() + if (page != null && page.statuses.isNotEmpty()) { + _uiState.update { + it.copy( + isRefreshing = false, + statuses = page.statuses, + reports = page.reports, + fetchedAtUtcMs = page.fetchedAtUtcMs + ) + } + } else { + _uiState.update { it.copy(isRefreshing = false, error = "fetch_failed") } + } + } catch (exception: Exception) { + _uiState.update { it.copy(isRefreshing = false, error = exception.message ?: "fetch_failed") } + } + } + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3b1d35b6..41c552b9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,8 +1,8 @@ [versions] #noinspection UnusedVersionCatalogEntry -appVersionCode = "452" +appVersionCode = "453" #noinspection UnusedVersionCatalogEntry -appVersionName = "4.5.2" +appVersionName = "4.5.3" #noinspection UnusedVersionCatalogEntry compileSdk = "37" #noinspection UnusedVersionCatalogEntry diff --git a/settings.gradle.kts b/settings.gradle.kts index 0e2533e8..bdfa71fd 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -28,5 +28,6 @@ include( ":feature:radar", ":feature:roaming", ":feature:satellites", - ":feature:settings" + ":feature:settings", + ":feature:status" )
数字