feat(settings): 致谢与声明卡、版本卡打开检查更新、浅色主题、品牌与 GPL 头

This commit is contained in:
atsunatsu committed 2026-09-28 23:46:52 +08:00
1 parent 5acf71b2a6
commit 45cd72faf7
23 files changed
+410 -100

No files matched your search

+2
View File
@@ -4,6 +4,8 @@
**BA7OPF 定制版** — 基于 [rt-bishop/Look4Sat](https://github.com/rt-bishop/Look4Sat) 的业余无线电卫星追踪器,增加了线性卫星频率计算器等功能。
非官方修改版(fork),独立维护;首次发布 2026-09,持续修改中。
## 本仓库特色功能
- **网格地图模式(Grid Mode)** — 地图页内置 VUCC 网格覆盖图:已通联网格绿色填充、漫游网格蓝色 45° 斜纹(疏朗半透明样式)、当前所在方格淡黄加粗框;点击已通联网格弹出该网格内通联明细弹窗;进入网格模式默认 VUCC 视图
+1 -1
View File
@@ -44,7 +44,7 @@
<meta-data
android:name="android.telephony.PROPERTY_SATELLITE_DATA_OPTIMIZED"
android:value="com.rtbishop.look4sat" />
android:value="cn.ba7opf.look4sat" />
<provider
android:name="androidx.core.content.FileProvider"
@@ -27,7 +27,9 @@ import android.view.View
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.runtime.getValue
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import com.rtbishop.look4sat.core.domain.repository.IContainerProvider
import com.rtbishop.look4sat.core.presentation.MainTheme
@@ -47,9 +49,12 @@ class MainActivity : ComponentActivity() {
installSplashScreen()
enableEdgeToEdge()
super.onCreate(savedInstanceState)
val settingsRepo = (applicationContext as IContainerProvider).getMainContainer().settingsRepo
observeNightFilterState()
setContent {
MainTheme(isDarkTheme = true) { NavRoot() }
val otherSettings by settingsRepo.otherSettings.collectAsStateWithLifecycle()
// Light theme is opt-in: the app stays dark until the user turns it on.
MainTheme(isDarkTheme = !otherSettings.stateOfLightTheme) { NavRoot() }
}
}
@@ -1,3 +1,21 @@
/*
* Look4Sat-BA7OPF. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2026 BA7OPF.
* Based on Look4Sat by Arty Bishop and contributors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.core.data.repository
import com.rtbishop.look4sat.core.domain.model.LatestRelease
@@ -29,7 +29,7 @@ class AddToCalendar(private val context: Context) : IAddToCalendar {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
setData(CalendarContract.Events.CONTENT_URI)
putExtra(CalendarContract.Events.TITLE, name)
putExtra(CalendarContract.Events.DESCRIPTION, "Look4Sat Pass")
putExtra(CalendarContract.Events.DESCRIPTION, "Look4Sat-BA7OPF Pass")
putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, aosTime)
putExtra(CalendarContract.EXTRA_EVENT_END_TIME, losTime)
}
@@ -1,3 +1,21 @@
/*
* Look4Sat-BA7OPF. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2026 BA7OPF.
* Based on Look4Sat by Arty Bishop and contributors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.core.presentation
import androidx.compose.foundation.Canvas
@@ -61,8 +61,9 @@ fun MainTheme(isDarkTheme: Boolean = isSystemInDarkTheme(), content: @Composable
SideEffect {
val window = (view.context as ComponentActivity).window
val insetsController = WindowCompat.getInsetsController(window, view)
insetsController.isAppearanceLightStatusBars = false
insetsController.isAppearanceLightNavigationBars = false
// Light theme needs dark status/nav bar icons; the dark theme needs light ones.
insetsController.isAppearanceLightStatusBars = !isDarkTheme
insetsController.isAppearanceLightNavigationBars = !isDarkTheme
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
window.isNavigationBarContrastEnforced = false
}
@@ -1,3 +1,21 @@
/*
* Look4Sat-BA7OPF. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2026 BA7OPF.
* Based on Look4Sat by Arty Bishop and contributors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.core.presentation
import androidx.compose.animation.core.animateDpAsState
@@ -83,7 +83,6 @@
<string name="map_visible">Visible</string>
<!-- Settings screen -->
<string name="prefs_donate_title">Soporte</string>
<string name="prefs_privacy_title">Política de privacidad</string>
<string name="prefs_updated_title">Actualizado: %s</string>
@@ -82,7 +82,6 @@
<string name="map_visible">Видимый</string>
<!-- Settings screen -->
<string name="prefs_donate_title">Поддержать</string>
<string name="prefs_privacy_title">Политика конфиденциальности</string>
<string name="prefs_updated_title">Обновлено: %s</string>
@@ -83,7 +83,6 @@
<string name="map_visible">දෘශ්‍ය</string>
<!-- Settings screen -->
<string name="prefs_donate_title">දායකත්ව</string>
<string name="prefs_privacy_title">රහස්යතා ප්රතිපත්තිය</string>
<string name="prefs_updated_title">යාවත්කාලීන කරන ලදී: %s</string>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Commmon -->
<string name="app_name" translatable="false">Look4Sat</string>
<string name="app_name" translatable="false">Look4Sat-BA7OPF</string>
<string name="btn_accept">Kabul et</string>
<string name="btn_cancel">İptal</string>
<string name="empty_list_message">Gösterilecek veri bulunamadı</string>
@@ -104,18 +104,16 @@
<string name="map_visible">Görünür</string>
<!-- Settings screen -->
<string name="prefs_app_title" translatable="false">Look4Sat v%s</string>
<string name="prefs_app_url" translatable="false">https://play.google.com/store/apps/details?id=com.rtbishop.look4sat</string>
<string name="prefs_donate_title">Bağış yap</string>
<string name="prefs_donate_url" translatable="false">https://ko-fi.com/rt_bishop</string>
<string name="prefs_fdroid_title" translatable="false">F-Droid</string>
<string name="prefs_fdroid_url" translatable="false">https://f-droid.org/en/packages/com.rtbishop.look4sat/</string>
<string name="prefs_app_title" translatable="false">Look4Sat-BA7OPF v%s</string>
<string name="prefs_app_url" translatable="false">https://github.com/atsunatsu/Look4Sat/releases</string>
<string name="prefs_github_title" translatable="false">GitHub</string>
<string name="prefs_github_url" translatable="false">https://github.com/rt-bishop/Look4Sat/</string>
<string name="prefs_github_url" translatable="false">https://github.com/atsunatsu/Look4Sat</string>
<string name="prefs_license_title" translatable="false">GPLv3</string>
<string name="prefs_license_url" translatable="false">https://www.gnu.org/licenses/gpl-3.0.html</string>
<string name="prefs_privacy_title">Gizlilik Politikası</string>
<string name="prefs_privacy_url" translatable="false">https://sites.google.com/view/look4sat-privacy-policy/home</string>
<string name="prefs_upstream_title" translatable="false">Upstream Project</string>
<string name="prefs_upstream_url" translatable="false">https://github.com/rt-bishop/Look4Sat</string>
<string name="prefs_updated_title">Güncellendi: %s</string>
<string name="prefs_updated_time">d MMM yyyy - HH:mm:ss</string>
@@ -83,7 +83,6 @@
<string name="map_visible">Видимий</string>
<!-- Settings screen -->
<string name="prefs_donate_title">Підтримати</string>
<string name="prefs_privacy_title">Політика конфіденційності</string>
<string name="prefs_updated_title">Оновлено: %s</string>
@@ -41,7 +41,7 @@
<string name="pass_welcome_message">请务必在设置中通过GPS、经纬度或QTH定位您的位置\n建议至少每周更新一次数据库,以确保预测结果的准确性</string>
<!-- AMSAT Status screen -->
<string name="pass_whatsnew_message">4.4.6 以来的全部更新(4.4.7 系列):\n\n• 网格模式地图:LoTW 已确认网格绿色填充、漫游网格蓝色条纹、当前网格加粗框;右上角网格/卫星模式开关。\n\n• 首通呼号(VUCC 视图):绿格显示该格最早通联的呼号,非绿格空白。\n\n• worked 网格可按操作网格筛选(含“全部”);点击网格弹出 QSO 明细;每格 QSO 计数。\n\n• 各操作网格独立 VUCC 计数;极区网格标签;反经线修复;漫游条纹密度一致。\n\n• WAPC/WAJA 地图标签随系统语言。\n\n• 网格模式视口与筛选跨页保持。\n\n• LoTW 同步:账号直连、全量/增量模式、实时进度、可取消、启动自动同步(独立开关)、设置页显示上次同步时间。\n\n• 历史过境:过境筛选可回看过去数小时(历史过境时间)。\n\n• 指南针校准:8 字动作精度指示 + 手动航向偏置。\n\n• AMSAT 状态页:连续相同状态报告计数。\n\n• 过境/雷达:UTC 切换重算过境;日出日落与仰角颜色修复;卫星模糊搜索;相互过境页滚动保持、跟随 UTC、雷达快捷入口。\n\n• CW 解码器支持 32/64 位设备;Android 17 (API 37) 适配。</string>
<string name="pass_whatsnew_message">4.4.6 以来的全部更新(4.4.7 / 4.4.8 系列):\n\n• 合并上游 4.4.8:IC-705 读频修复;地图昼夜分界线极性修复(秋分后昼夜阴影反相);白色主题(地图、状态栏、弹窗与自绘网格/奖状图层一并适配);卫星名模糊搜索。\n\n• LoTW 日志本:上传预检按真实 BAND/SATELLITE 给出原因;QSO 全详情编辑;卫星名按 TQSL 官方名录校验(非官方名不予保存);FM 卫星固定记录语音转发器频率(SO-50 下行 436.795);LoTW 确认记录会并入已上传的那条 QSO,不再另起一行(卫星名经 TQSL 别名表比对,报告里的 BAND 按上行解析)。\n\n• 弹窗统一为底部弹出栏同款配色与圆角。\n\n• 设置页链接指向本项目:版本号 → 发布页,GitHub → 本仓库。\n\n• 网格模式地图:LoTW 已确认网格绿色填充、漫游网格蓝色条纹、当前网格加粗框;右上角网格/卫星模式开关。\n\n• 首通呼号(VUCC 视图):绿格显示该格最早通联的呼号,非绿格空白。\n\n• worked 网格可按操作网格筛选(含“全部”);点击网格弹出 QSO 明细;每格 QSO 计数。\n\n• 各操作网格独立 VUCC 计数;极区网格标签;反经线修复;漫游条纹密度一致。\n\n• WAPC/WAJA 地图标签随系统语言。\n\n• 网格模式视口与筛选跨页保持。\n\n• LoTW 同步:账号直连、全量/增量模式、实时进度、可取消、启动自动同步(独立开关)、设置页显示上次同步时间。\n\n• 历史过境:过境筛选可回看过去数小时(历史过境时间)。\n\n• 指南针校准:8 字动作精度指示 + 手动航向偏置。\n\n• AMSAT 状态页:连续相同状态报告计数。\n\n• 过境/雷达:UTC 切换重算过境;日出日落与仰角颜色修复;卫星模糊搜索;相互过境页滚动保持、跟随 UTC、雷达快捷入口。\n\n• CW 解码器支持 32/64 位设备;Android 17 (API 37) 适配。\n\n• v4.4.7-ba7opf.21:日志本卫星名统一按 TQSL 官方名存取(SAUDISAT 1C → SO-50),存量记录自动改名;日志页与通联帖按卫星身份匹配。\n• v4.4.7-ba7opf.22:同步时自动修正旧确认行的频段方向;上传重复判定改为时间+卫星+呼号三者相同。\n• v4.4.7-ba7opf.23:应用更名为 Look4Sat-BA7OPF;致谢页声明为非官方修改版,关于页加入上游项目链接,移除打赏/F-Droid 卡片。</string>
<string name="nav_amsat">AMSAT</string>
<string name="amsat_title">AMSAT 卫星状态</string>
@@ -141,7 +141,6 @@
<string name="map_visible">日照中</string>
<!-- Settings screen -->
<string name="prefs_donate_title">支持</string>
<string name="prefs_privacy_title">隐私政策</string>
<string name="prefs_updated_title">更新:%s</string>
@@ -304,6 +303,7 @@
<string name="prefs_compass_offset">手动航向偏置(°)</string>
<string name="prefs_compass_offset_support">范围 −180° 至 +180°;正值为顺时针偏移。</string>
<string name="prefs_compass_offset_reset">偏置归零</string>
<string name="prefs_other_switch_light_theme">浅色主题</string>
<string name="prefs_other_switch_night_mode">启用红色夜间模式</string>
<string name="prefs_other_switch_map_grid">地图显示 Maidenhead 网格</string>
@@ -312,7 +312,7 @@
<string name="prefs_highlight_mid">中 %1$d-%2$d°</string>
<string name="prefs_highlight_high">高 &gt; %1$d°</string>
<string name="prefs_outro_title">我要感谢:</string>
<string name="prefs_outro_title">致谢与声明</string>
<string name="prefs_outro_thanks" translatable="false">
• Look4Sat 所有用户及贡献者!
\n• David A. B. Johnson (predict4java)
@@ -325,7 +325,9 @@
\n• BH2VSQ BG5JSU(天地图渲染支持)
\n• BG5JSU(LoTW上传支持)
</string>
<string name="prefs_outro_license">该应用程序不提供任何保修.</string>
<string name="prefs_outro_notice">Look4Sat-BA7OPF 是基于 Look4Sat(GPL-3.0)的非官方修改版,由 BA7OPF 独立维护,与原项目无隶属关系,亦未获其背书。
上游项目:https://github.com/rt-bishop/Look4Sat</string>
<string name="prefs_outro_license">基于 GNU GPL v3 许可 · 本程序不提供任何保修</string>
<!-- 匹配页 Mutual match page -->
<string name="mutual_title">过境匹配</string>
@@ -374,4 +376,32 @@
<string name="update_check_download_error">下载失败,请重试。</string>
<string name="update_check_no_apk">该版本未附带 APK 安装包。</string>
<!-- QSO edit dialog -->
<string name="qso_edit_title">编辑通联记录</string>
<string name="qso_edit_uploaded_notice">该通联已上传或已确认。保存将保留原记录不变,并新建一条记录。</string>
<string name="qso_edit_callsign">呼号</string>
<string name="qso_edit_date">日期 (UTC)</string>
<string name="qso_edit_time">时间 (UTC)</string>
<string name="qso_edit_tx">上行频率 (MHz)</string>
<string name="qso_edit_rx">下行频率 (MHz)</string>
<string name="qso_edit_band">频段:%1$s</string>
<string name="qso_edit_mode">模式</string>
<string name="qso_edit_satellite">卫星</string>
<string name="qso_edit_hint_satellite">LoTW 只接受它的官方卫星名</string>
<string name="qso_edit_sent">发送报告</string>
<string name="qso_edit_received">接收报告</string>
<string name="qso_edit_grid">对方网格</string>
<string name="qso_edit_comment">备注</string>
<string name="qso_edit_save">保存</string>
<string name="qso_edit_cancel">取消</string>
<string name="qso_edit_err_callsign">呼号不能为空</string>
<string name="qso_edit_err_datetime">日期或时间格式不对 —— 应为 yyyy-MM-dd 与 HH:mm:ss(UTC)</string>
<string name="qso_edit_err_frequency">频率格式不对 —— 请输入 MHz,例如 145.850</string>
<string name="qso_edit_err_satellite">不是 LoTW 官方卫星名 —— 请从列表中选一个</string>
<string name="qso_edit_satellite_pick">选择</string>
<string name="qso_edit_satellite_pick_title">LoTW 官方卫星名</string>
<string name="qso_edit_satellite_search">搜索卫星名或数据源别名</string>
<string name="qso_edit_satellite_official">保存并签名时使用 %1$s</string>
<string name="qso_edit_warn_no_freq">没有上行频率:LoTW 要求 BAND,补上频率前这条无法上传</string>
</resources>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Commmon -->
<string name="app_name" translatable="false">Look4Sat</string>
<string name="app_name" translatable="false">Look4Sat-BA7OPF</string>
<string name="btn_accept">Accept</string>
<string name="btn_cancel">Cancel</string>
<string name="empty_list_message">No data to show</string>
@@ -52,7 +52,7 @@
Make sure to set your position via GPS, Lat/Lon or QTH in Settings.
\n\nPlease update the database at least weekly to get accurate predictions.</string>
<string name="pass_whatsnew_title" translatable="false">What\'s new in Look4Sat</string>
<string name="pass_whatsnew_message">All changes since 4.4.6 (the 4.4.7 series):\n\n• Grid-mode map: LoTW confirmed grids in green, roamed grids with blue stripes, current grid highlighted; mode switch pill at the map\'s top-right.\n\n• First-call labels (VUCC view): show the first callsign worked in each green grid instead of the grid code; non-worked cells stay empty.\n\n• Worked grids filtered by operated grid (with an All option); tap a grid to see its QSO detail dialog; per-grid QSO counts.\n\n• Per-operated-grid VUCC counting; polar grid labels; antimeridian fixes; consistent roamed-stripe density.\n\n• WAPC/WAJA map labels follow the system language.\n\n• Grid-mode viewport and award filter survive page switches.\n\n• LoTW sync: direct login, full/incremental modes, live progress, cancel button, auto-sync on app start with its own toggle, last-sync time shown.\n\n• Pass history: filter can look back over past passes (Time before).\n\n• Compass calibration: figure-eight accuracy meter plus manual heading offset.\n\n• AMSAT status page: consecutive same-status report counts.\n\n• Passes/radar: UTC toggle recalculates passes; sunrise/sunset and elevation color fixes; satellite fuzzy search; mutual page keeps scroll, follows UTC, radar shortcut.\n\n• CW decoder for 32/64-bit devices; Android 17 (API 37) support.</string>
<string name="pass_whatsnew_message">All changes since 4.4.6 (the 4.4.7 / 4.4.8 series):\n\n• Merged from upstream 4.4.8: IC-705 frequency readout fix; day/night terminator polarity fix (shading was inverted after the September equinox); white/light theme (map, status bar, dialogs and the custom grid/award overlays included); fuzzy satellite search.\n\n• LoTW logbook: upload pre-check reports the real BAND/SATELLITE reasons; full QSO detail editing; satellite names validated against the TQSL list (unofficial names are not saved); FM satellites always log the voice repeater frequency (SO-50 downlink 436.795); a LoTW confirmation now folds into the uploaded QSO instead of appearing as a second row (satellite names are matched through the TQSL aliases, and the report\'s BAND is read as the uplink).\n\n• Dialogs share the bottom-sheet colours and corner radius.\n\n• Settings links point to this project: version number -> releases page, GitHub -> repository.\n\n• Grid-mode map: LoTW confirmed grids in green, roamed grids with blue stripes, current grid highlighted; mode switch pill at the map\'s top-right.\n\n• First-call labels (VUCC view): show the first callsign worked in each green grid instead of the grid code; non-worked cells stay empty.\n\n• Worked grids filtered by operated grid (with an All option); tap a grid to see its QSO detail dialog; per-grid QSO counts.\n\n• Per-operated-grid VUCC counting; polar grid labels; antimeridian fixes; consistent roamed-stripe density.\n\n• WAPC/WAJA map labels follow the system language.\n\n• Grid-mode viewport and award filter survive page switches.\n\n• LoTW sync: direct login, full/incremental modes, live progress, cancel button, auto-sync on app start with its own toggle, last-sync time shown.\n\n• Pass history: filter can look back over past passes (Time before).\n\n• Compass calibration: figure-eight accuracy meter plus manual heading offset.\n\n• AMSAT status page: consecutive same-status report counts.\n\n• Passes/radar: UTC toggle recalculates passes; sunrise/sunset and elevation color fixes; satellite fuzzy search; mutual page keeps scroll, follows UTC, radar shortcut.\n\n• CW decoder for 32/64-bit devices; Android 17 (API 37) support.\n\n• v4.4.7-ba7opf.21: the logbook stores and shows satellites under their TQSL official names (SAUDISAT 1C -> SO-50); existing records are renamed automatically; the log page and QSO post match by satellite identity.\n• v4.4.7-ba7opf.22: stale confirmation rows get their band direction fixed on sync; upload duplicate detection keys on time + satellite + callsign.\n• v4.4.7-ba7opf.23: renamed to Look4Sat-BA7OPF; the credits card states this is an unofficial build, the About screen links the upstream project, and the Donate / F-Droid cards were removed.</string>
<!-- AMSAT Status screen -->
<string name="nav_amsat">AMSAT</string>
@@ -162,18 +162,16 @@
<string name="map_visible">Visible</string>
<!-- Settings screen -->
<string name="prefs_app_title" translatable="false">Look4Sat v%s</string>
<string name="prefs_app_url" translatable="false">https://play.google.com/store/apps/details?id=com.rtbishop.look4sat</string>
<string name="prefs_donate_title">Donate</string>
<string name="prefs_donate_url" translatable="false">https://ko-fi.com/rt_bishop</string>
<string name="prefs_fdroid_title" translatable="false">F-Droid</string>
<string name="prefs_fdroid_url" translatable="false">https://f-droid.org/en/packages/com.rtbishop.look4sat/</string>
<string name="prefs_app_title" translatable="false">Look4Sat-BA7OPF v%s</string>
<string name="prefs_app_url" translatable="false">https://github.com/atsunatsu/Look4Sat/releases</string>
<string name="prefs_github_title" translatable="false">GitHub</string>
<string name="prefs_github_url" translatable="false">https://github.com/rt-bishop/Look4Sat/</string>
<string name="prefs_github_url" translatable="false">https://github.com/atsunatsu/Look4Sat</string>
<string name="prefs_license_title" translatable="false">GPLv3</string>
<string name="prefs_license_url" translatable="false">https://www.gnu.org/licenses/gpl-3.0.html</string>
<string name="prefs_privacy_title">Privacy Policy</string>
<string name="prefs_privacy_url" translatable="false">https://sites.google.com/view/look4sat-privacy-policy/home</string>
<string name="prefs_upstream_title" translatable="false">Upstream Project</string>
<string name="prefs_upstream_url" translatable="false">https://github.com/rt-bishop/Look4Sat</string>
<string name="prefs_updated_title">Updated: %s</string>
<string name="prefs_updated_time">d MMM yyyy - HH:mm:ss</string>
@@ -334,6 +332,7 @@
<string name="prefs_compass_offset">Manual heading offset (°)</string>
<string name="prefs_compass_offset_support">Range −180° to +180°. Positive values rotate clockwise.</string>
<string name="prefs_compass_offset_reset">Reset offset</string>
<string name="prefs_other_switch_light_theme">Light theme</string>
<string name="prefs_other_switch_night_mode">Enable red night mode filter</string>
<string name="prefs_other_switch_map_grid">Show Maidenhead grid on map</string>
@@ -342,7 +341,7 @@
<string name="prefs_highlight_mid">Mid %1$d–%2$d°</string>
<string name="prefs_highlight_high">High &gt; %1$d°</string>
<string name="prefs_outro_title">I would like to say thanks to</string>
<string name="prefs_outro_title">Credits &amp; Notices</string>
<string name="prefs_outro_thanks" translatable="false">
* Look4Sat users and contributors!
\n* David A. B. Johnson (predict4java)
@@ -355,7 +354,9 @@
\n* BH2VSQ BG5JSU (TianDiTu rendering support)
\n* BG5JSU (LoTW upload support)
</string>
<string name="prefs_outro_license">The app comes with no warranty</string>
<string name="prefs_outro_notice">Look4Sat-BA7OPF is an unofficial modified build based on Look4Sat (GPL-3.0), independently maintained by BA7OPF. It is not affiliated with or endorsed by the original Look4Sat project.
Upstream: https://github.com/rt-bishop/Look4Sat</string>
<string name="prefs_outro_license">Licensed under the GNU General Public License v3 · The app comes with no warranty</string>
<!-- Mutual match page -->
<string name="mutual_title">Pass Match</string>
@@ -404,4 +405,32 @@
<string name="update_check_download_error">Download failed. Try again.</string>
<string name="update_check_no_apk">No APK attached to this release.</string>
<!-- QSO edit dialog -->
<string name="qso_edit_title">Edit QSO</string>
<string name="qso_edit_uploaded_notice">This QSO was already uploaded or confirmed. Saving keeps the original record unchanged and creates a new one.</string>
<string name="qso_edit_callsign">Callsign</string>
<string name="qso_edit_date">Date (UTC)</string>
<string name="qso_edit_time">Time (UTC)</string>
<string name="qso_edit_tx">Uplink (MHz)</string>
<string name="qso_edit_rx">Downlink (MHz)</string>
<string name="qso_edit_band">Band: %1$s</string>
<string name="qso_edit_mode">Mode</string>
<string name="qso_edit_satellite">Satellite</string>
<string name="qso_edit_hint_satellite">LoTW only accepts its official satellite name</string>
<string name="qso_edit_sent">Sent RST</string>
<string name="qso_edit_received">Received RST</string>
<string name="qso_edit_grid">Opposite grid</string>
<string name="qso_edit_comment">Comment</string>
<string name="qso_edit_save">Save</string>
<string name="qso_edit_cancel">Cancel</string>
<string name="qso_edit_err_callsign">Callsign is required</string>
<string name="qso_edit_err_datetime">Invalid date/time — use yyyy-MM-dd and HH:mm:ss (UTC)</string>
<string name="qso_edit_err_frequency">Invalid frequency — enter MHz, e.g. 145.850</string>
<string name="qso_edit_err_satellite">Not an ARRL satellite name — pick one from the list</string>
<string name="qso_edit_satellite_pick">Pick</string>
<string name="qso_edit_satellite_pick_title">ARRL satellite name</string>
<string name="qso_edit_satellite_search">Search name or catalogue alias</string>
<string name="qso_edit_satellite_official">Saved and signed as %1$s</string>
<string name="qso_edit_warn_no_freq">No uplink frequency: LoTW requires a band, so this QSO cannot be uploaded yet</string>
</resources>
@@ -1,3 +1,21 @@
/*
* Look4Sat-BA7OPF. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2026 BA7OPF.
* Based on Look4Sat by Arty Bishop and contributors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/*
* CwDecodeScreen.kt — Look4Sat 的 CW 解码页面(Compose 外壳)。
*
@@ -139,6 +139,18 @@ class AwardBoundaryOverlay : Overlay() {
color = Color.argb(90, 76, 217, 100)
}
/**
* Recolor the amber boundary/label paints for the active map theme (dark amber + white halo on
* the light map, light amber + black halo on the dark map). The worked fill reads on both.
*/
fun applyTheme(isLightUi: Boolean) {
val line = if (isLightUi) Color.argb(200, 113, 92, 12) else Color.argb(160, 255, 224, 130)
val label = if (isLightUi) Color.argb(235, 30, 25, 5) else Color.argb(220, 255, 224, 130)
linePaint.color = line
labelPaint.color = label
labelPaint.setShadowLayer(3f, 2f, 2f, if (isLightUi) Color.WHITE else Color.BLACK)
}
/** Boundary regions to render; assigning recomputes culling bounds. */
var regions: List<AwardRegion> = emptyList()
set(value) {
@@ -89,6 +89,24 @@ class MaidenheadGridOverlay : Overlay() {
color = Color.argb(255, 76, 217, 100)
}
/**
* Recolor the amber line/label paints for the active map theme: dark amber with a white halo
* on the light map, light amber with a black halo on the dark map. The worked / marked / roam /
* selected colors stay as they are — they read on both backgrounds.
*/
fun applyTheme(isLightUi: Boolean) {
val line = if (isLightUi) Color.argb(200, 113, 92, 12) else Color.argb(160, 255, 224, 130)
val label = if (isLightUi) Color.argb(235, 30, 25, 5) else Color.argb(220, 255, 224, 130)
val halo = if (isLightUi) Color.WHITE else Color.BLACK
linePaint.color = line
// ownLine stays the same color as the normal grid lines (only thicker).
ownLinePaint.color = line
labelPaint.color = label
labelPaint.setShadowLayer(3f, 2f, 2f, halo)
firstCallPaint.color = label
firstCallPaint.setShadowLayer(3f, 2f, 2f, halo)
}
/** Worked gridsquares (4-char, uppercase) to highlight, e.g. {"OL62", "PM95"}. */
var workedGrids: Set<String> = emptySet()
@@ -1,3 +1,21 @@
/*
* Look4Sat-BA7OPF. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2026 BA7OPF.
* Based on Look4Sat by Arty Bishop and contributors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.feature.map
import androidx.compose.runtime.MutableState
@@ -159,6 +159,71 @@ private val moonIconPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
android.graphics.PorterDuffColorFilter("#E0E0E0".toColorInt(), android.graphics.PorterDuff.Mode.SRC_IN)
}
/**
* Accent used for the station marker, footprint outline and map labels. It is recolored per
* theme (dark amber on light tiles, light amber on dark tiles) so every map element stays
* readable; [applyMapColors] keeps it in sync with the shared paints.
*/
private var mapAccentColor = "#FFE082".toColorInt()
/** Grayscale + invert filter that turns the offline OSM map dark; unused (and off) in light theme. */
private val darkTileFilter by lazy { createColorFilter() }
/** Theme the shared map paints were last colored for; null = not applied yet this process. */
private var mapIsLightUi: Boolean? = null
/** Theme the tile filter / icon caches were last applied for (drives the per-frame theme check). */
private var appliedLightUi: Boolean? = null
/** True while the offline OSM source is active — satellite imagery (Tianditu) is never filtered. */
private var offlineTilesActive = true
/**
* Recolor the shared overlay paints and custom overlays for the current theme. Must run before the
* markers are (re)built each frame so freshly created icons use the new colors; [applyThemeToOverlays]
* and the icon-cache eviction in the update block handle the already created ones.
*/
private fun applyMapColors(isLightUi: Boolean) {
mapIsLightUi = isLightUi
if (isLightUi) {
// Dark amber (lightScheme primary) reads on light tiles; labels get a white halo.
mapAccentColor = "#715C0C".toColorInt()
footprintPaint.color = mapAccentColor
textPaint.color = "#1E1B13".toColorInt()
textPaint.setShadowLayer(3f, 3f, 3f, Color.WHITE)
sunIconPaint.colorFilter =
android.graphics.PorterDuffColorFilter(mapAccentColor, android.graphics.PorterDuff.Mode.SRC_IN)
moonIconPaint.colorFilter = android.graphics.PorterDuffColorFilter(
"#4C4639".toColorInt(), android.graphics.PorterDuff.Mode.SRC_IN
)
} else {
mapAccentColor = "#FFE082".toColorInt()
footprintPaint.color = mapAccentColor
textPaint.color = mapAccentColor
textPaint.setShadowLayer(3f, 3f, 3f, Color.BLACK)
sunIconPaint.colorFilter =
android.graphics.PorterDuffColorFilter(mapAccentColor, android.graphics.PorterDuff.Mode.SRC_IN)
moonIconPaint.colorFilter =
android.graphics.PorterDuffColorFilter("#E0E0E0".toColorInt(), android.graphics.PorterDuff.Mode.SRC_IN)
}
applyThemeToOverlays()
}
/** Push the current theme into the grid / award overlays, which own their own paints. */
private fun applyThemeToOverlays() {
val isLight = mapIsLightUi == true
currentMapView?.overlays?.forEach { overlay ->
when (overlay) {
is MaidenheadGridOverlay -> overlay.applyTheme(isLight)
is AwardBoundaryOverlay -> overlay.applyTheme(isLight)
else -> Unit
}
}
}
/** The MapView currently attached to the map page; used to recolor overlays on a theme change. */
private var currentMapView: MapView? = null
@Composable
fun MapDestination(
mapFilterViewModel: MapFilterViewModel,
@@ -320,8 +385,8 @@ private fun MapScreen(
val firstPos = uiState.track?.firstOrNull()?.firstOrNull() ?: return@LaunchedEffect
mapView.controller.animateTo(GeoPoint(firstPos.latitude, firstPos.longitude))
}
LaunchedEffect(uiState.mapSource, uiState.tiandituKey) {
configureTileSources(mapView, uiState.mapSource, uiState.tiandituKey)
LaunchedEffect(uiState.mapSource, uiState.tiandituKey, uiState.isLightUi) {
configureTileSources(mapView, uiState.mapSource, uiState.tiandituKey, uiState.isLightUi)
}
Column(modifier = Modifier.layoutPadding(), verticalArrangement = Arrangement.spacedBy(6.dp)) {
val isVertical = isVerticalLayout()
@@ -359,6 +424,18 @@ private fun MapScreen(
mutableStateOf(restoredViewport && uiState.isGridMode)
}
AndroidView({ mapView }) { view ->
// Theme change: recolor the paints/tile filter/overlays and drop the icon
// caches, which baked the previous theme's colors into their bitmaps.
if (appliedLightUi != uiState.isLightUi) {
appliedLightUi = uiState.isLightUi
applyMapColors(uiState.isLightUi)
iconCache.evictAll()
dotIcon = null
view.overlayManager.tilesOverlay.setColorFilter(
if (!uiState.isLightUi && offlineTilesActive) darkTileFilter else null
)
requestTileRefresh(view)
}
// Award filter mode: show the selected award's regions with
// worked ones filled green; hide satellite layers like grid mode.
val awardMode = selectedAward?.takeIf { it != AwardType.VUCC }
@@ -393,13 +470,13 @@ private fun MapScreen(
}
}
if (!uiState.isGridMode) {
uiState.stationPosition?.let { setStationPosition(it, view) }
uiState.stationPosition?.let { setStationPosition(it, view, uiState.isLightUi) }
uiState.track?.let { setSatelliteTrack(it, view) }
uiState.footprint?.let { setFootprint(it, view) }
uiState.positions?.let { setPositions(it, view) { item -> onAction(MapAction.SelectItem(item)) } }
setTerminator(uiState.sunLatDeg, uiState.sunLonDeg, view)
setSubSolarPoint(uiState.sunLatDeg, uiState.sunLonDeg, view)
setMoonPosition(uiState.moonLatDeg, uiState.moonLonDeg, view)
setSubSolarPoint(uiState.sunLatDeg, uiState.sunLonDeg, view, uiState.isLightUi)
setMoonPosition(uiState.moonLatDeg, uiState.moonLonDeg, view, uiState.isLightUi)
}
view.invalidate()
}
@@ -634,7 +711,7 @@ private fun MarkedStationRow(
text = marked.call,
style = MaterialTheme.typography.titleMedium,
fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace,
color = ComposeColor(0xFFFFE082),
color = MaterialTheme.colorScheme.primary,
maxLines = 1,
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis,
modifier = Modifier.weight(1f)
@@ -651,7 +728,7 @@ private fun MarkedStationRow(
Icon(
painter = painterResource(R.drawable.ic_pin),
contentDescription = stringResource(R.string.grid_mark_pin),
tint = if (isPinned) ComposeColor(0xFF9E9E9E) else ComposeColor(0xFFFFE082),
tint = if (isPinned) ComposeColor(0xFF9E9E9E) else MaterialTheme.colorScheme.primary,
modifier = Modifier
.size(18.dp)
.clickable(onClick = onPinMark)
@@ -706,7 +783,7 @@ private fun WorkedGridCallRow(
text = call,
style = MaterialTheme.typography.titleMedium,
fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace,
color = ComposeColor(0xFFFFE082),
color = MaterialTheme.colorScheme.primary,
maxLines = 1,
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis,
modifier = Modifier.weight(1f)
@@ -1092,22 +1169,29 @@ private fun FirstCallToggle(
// region Tile sources
private var configuredTileMapView: MapView? = null
private var configuredTileSource: String? = null
private var configuredTileIsLightUi: Boolean? = null
private fun configureTileSources(mapView: MapView, mapSource: String, tiandituKey: String) {
private fun configureTileSources(mapView: MapView, mapSource: String, tiandituKey: String, isLightUi: Boolean) {
val key = tiandituKey.trim()
val layers = tiandituLayers(mapSource)
val sourceKey = if (layers == null || key.isBlank()) MapSource.OSM else "${layers.base}:$key"
if (configuredTileMapView === mapView && configuredTileSource == sourceKey) return
val offlineSource = layers == null || key.isBlank()
offlineTilesActive = offlineSource
val sourceKey = if (offlineSource) MapSource.OSM else "${layers.base}:$key"
if (configuredTileMapView === mapView && configuredTileSource == sourceKey &&
configuredTileIsLightUi == isLightUi
) return
configuredTileMapView = mapView
configuredTileSource = sourceKey
configuredTileIsLightUi = isLightUi
(mapView.overlays.getOrNull(OVERLAY_TDT_LABELS) as? TilesOverlay)?.onDetach(mapView)
if (layers == null || key.isBlank()) {
if (offlineSource) {
mapView.setUseDataConnection(false)
mapView.setTileSource(offlineTileSource)
mapView.maxZoomLevel = OFFLINE_MAX_ZOOM
mapView.overlayManager.tilesOverlay.applyTileOverlayDefaults(createColorFilter())
// The dark-map filter belongs to the dark theme only; the light theme shows plain tiles.
mapView.overlayManager.tilesOverlay.applyTileOverlayDefaults(if (isLightUi) null else darkTileFilter)
mapView.overlays[OVERLAY_TDT_LABELS] = FolderOverlay()
requestTileRefresh(mapView)
return
@@ -1210,6 +1294,7 @@ private fun setAwardMode(award: AwardType, workedCodes: Set<String>, mapView: Ma
isEnabled = true
regions = AwardBoundaryData.load(mapView.context, asset)
this.workedCodes = workedCodes
applyTheme(mapIsLightUi == true)
}
}
for (index in OVERLAY_STATION..OVERLAY_MOON) {
@@ -1252,6 +1337,7 @@ private fun setGridMode(
this.firstCallsByGrid = firstCallsByGrid
this.markedGrids = markedGrids
this.ownGrid = ownGridOf(stationPosition)
applyTheme(mapIsLightUi == true)
}
}
// Satellite-related layers are hidden in grid mode; the grid overlay
@@ -1306,17 +1392,28 @@ private fun gridOfPoint(latitude: Double, longitude: Double): String? {
return "${'A' + fieldLon}${'A' + fieldLat}$subLon$subLat"
}
private fun setStationPosition(stationPos: GeoPos, mapView: MapView) {
private fun setStationPosition(stationPos: GeoPos, mapView: MapView, isLightUi: Boolean) {
try {
val overlay = mapView.overlays[OVERLAY_STATION]
val stationPoint = GeoPoint(stationPos.latitude, stationPos.longitude)
if (overlay is Marker) {
overlay.position = GeoPoint(stationPos.latitude, stationPos.longitude)
overlay.position = stationPoint
// Icon tint is baked into the drawable, so re-tint after a theme change.
if ((overlay.relatedObject as? Boolean) != isLightUi) {
overlay.icon = ContextCompat.getDrawable(mapView.context, R.drawable.ic_position)?.apply {
setTint(mapAccentColor)
}
overlay.relatedObject = isLightUi
}
} else {
mapView.overlays[OVERLAY_STATION] = Marker(mapView).apply {
setInfoWindow(null)
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
icon = ContextCompat.getDrawable(mapView.context, R.drawable.ic_position)
position = GeoPoint(stationPos.latitude, stationPos.longitude)
icon = ContextCompat.getDrawable(mapView.context, R.drawable.ic_position)?.apply {
setTint(mapAccentColor)
}
position = stationPoint
relatedObject = isLightUi
}
}
} catch (e: Exception) {
@@ -1539,26 +1636,37 @@ private fun setTerminator(sunLatDeg: Double, sunLonDeg: Double, mapView: MapView
}
}
/** Build the 48dp sun/moon marker icon, tinted with the theme-dependent paint. */
private fun buildMarkerIcon(mapView: MapView, resId: Int, paint: Paint): Drawable {
val iconSize = 48
val bmp = createBitmap(iconSize, iconSize)
ContextCompat.getDrawable(mapView.context, resId)?.apply {
setBounds(0, 0, iconSize, iconSize)
colorFilter = paint.colorFilter
draw(Canvas(bmp))
}
return bmp.toDrawable(mapView.context.resources)
}
/** Place an ic_sun icon marker at the sub-solar point. */
private fun setSubSolarPoint(sunLatDeg: Double, sunLonDeg: Double, mapView: MapView) {
private fun setSubSolarPoint(sunLatDeg: Double, sunLonDeg: Double, mapView: MapView, isLightUi: Boolean) {
try {
val overlay = mapView.overlays[OVERLAY_SUN]
val sunPos = GeoPoint(sunLatDeg, sunLonDeg)
if (overlay is Marker) {
overlay.position = sunPos
} else {
val iconSize = 48
val bmp = createBitmap(iconSize, iconSize)
ContextCompat.getDrawable(mapView.context, R.drawable.ic_sun)?.apply {
setBounds(0, 0, iconSize, iconSize)
colorFilter = sunIconPaint.colorFilter
draw(Canvas(bmp))
// The icon bitmap bakes in the theme tint, so rebuild it after a theme change.
if ((overlay.relatedObject as? Boolean) != isLightUi) {
overlay.icon = buildMarkerIcon(mapView, R.drawable.ic_sun, sunIconPaint)
overlay.relatedObject = isLightUi
}
} else {
mapView.overlays[OVERLAY_SUN] = Marker(mapView).apply {
setInfoWindow(null)
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER)
icon = bmp.toDrawable(mapView.context.resources)
icon = buildMarkerIcon(mapView, R.drawable.ic_sun, sunIconPaint)
position = sunPos
relatedObject = isLightUi
}
}
} catch (e: Exception) {
@@ -1567,26 +1675,23 @@ private fun setSubSolarPoint(sunLatDeg: Double, sunLonDeg: Double, mapView: MapV
}
/** Place an ic_moon icon marker at the sub-lunar point. */
private fun setMoonPosition(moonLatDeg: Double, moonLonDeg: Double, mapView: MapView) {
private fun setMoonPosition(moonLatDeg: Double, moonLonDeg: Double, mapView: MapView, isLightUi: Boolean) {
try {
val overlay = mapView.overlays[OVERLAY_MOON]
val moonPos = GeoPoint(moonLatDeg, moonLonDeg)
if (overlay is Marker) {
overlay.position = moonPos
} else {
val iconSize = 48
val bmp = createBitmap(iconSize, iconSize)
val c = Canvas(bmp)
ContextCompat.getDrawable(mapView.context, R.drawable.ic_moon)?.apply {
setBounds(0, 0, iconSize, iconSize)
colorFilter = moonIconPaint.colorFilter
draw(c)
if ((overlay.relatedObject as? Boolean) != isLightUi) {
overlay.icon = buildMarkerIcon(mapView, R.drawable.ic_moon, moonIconPaint)
overlay.relatedObject = isLightUi
}
} else {
mapView.overlays[OVERLAY_MOON] = Marker(mapView).apply {
setInfoWindow(null)
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER)
icon = bmp.toDrawable(mapView.context.resources)
icon = buildMarkerIcon(mapView, R.drawable.ic_moon, moonIconPaint)
position = moonPos
relatedObject = isLightUi
}
}
} catch (e: Exception) {
@@ -1631,10 +1736,12 @@ private fun rememberMapViewWithLifecycle(
zoomController.setVisibility(CustomZoomButtonsController.Visibility.NEVER)
overlayManager.tilesOverlay.loadingBackgroundColor = Color.TRANSPARENT
overlayManager.tilesOverlay.loadingLineColor = Color.TRANSPARENT
overlayManager.tilesOverlay.setColorFilter(createColorFilter())
// The dark-map tile filter (or no filter in the light theme) is applied per theme from
// the AndroidView update block, so it also follows a theme switch at runtime.
setScrollableAreaLimitLatitude(maxLat, minLat, 0)
overlays.addAll(Array(OVERLAY_COUNT) { FolderOverlay() })
overlays[OVERLAY_GRID] = MaidenheadGridOverlay()
overlays[OVERLAY_GRID] = MaidenheadGridOverlay().apply { applyTheme(mapIsLightUi == true) }
currentMapView = this
}
}
val lifecycleObserver = rememberMapViewLifecycleObserver(mapView)
@@ -1647,7 +1754,10 @@ private fun rememberMapViewWithLifecycle(
// released with the MapView or they keep the Activity and its bitmaps alive after disposal.
// (The viewport save lives in MapDestination, where the grid/satellite mode is known.)
DisposableEffect(mapView) {
onDispose { clearMapCaches() }
onDispose {
clearMapCaches()
if (currentMapView === mapView) currentMapView = null
}
}
return mapView
}
@@ -84,6 +84,7 @@ class MapViewModel(
_uiState.update {
it.copy(
isUtc = settings.stateOfUtc,
isLightUi = settings.stateOfLightTheme,
mapSource = settings.mapSource,
tiandituKey = settings.tiandituKey,
isGridMode = settings.stateOfMapGrid,
@@ -35,6 +35,7 @@ import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
@@ -264,11 +265,13 @@ private fun SettingsScreen(uiState: SettingsState, onAction: (SettingsAction) ->
if (dialogs.logbook) {
LogbookDialog(
records = uiState.logbookRecords,
satelliteCandidates = uiState.satelliteCatalog,
uploadBusy = uiState.logbookUploadBusy,
uploadMessage = uiState.logbookUploadMessage,
preview = uiState.logbookPreview,
onDismiss = { dialogs.logbook = false },
onDelete = { onAction(SettingsAction.DeleteLogbookRecord(it)) },
onEdit = { onAction(SettingsAction.UpdateLogbookRecord(it)) },
onUpload = { onAction(SettingsAction.PrepareLogbookUpload) },
onConfirmUpload = { onAction(SettingsAction.ConfirmLogbookUpload) },
onDismissPreview = { onAction(SettingsAction.DismissLogbookPreview) },
@@ -292,10 +295,8 @@ private fun SettingsScreen(uiState: SettingsState, onAction: (SettingsAction) ->
// URLs for top bar
val uriHandler = LocalUriHandler.current
val appUrl = stringResource(R.string.prefs_app_url)
val donateUrl = stringResource(R.string.prefs_donate_url)
val fdroidTitle = stringResource(R.string.prefs_fdroid_title)
val fdroidUrl = stringResource(R.string.prefs_fdroid_url)
val upstreamTitle = stringResource(R.string.prefs_upstream_title)
val upstreamUrl = stringResource(R.string.prefs_upstream_url)
val gitHubTitle = stringResource(R.string.prefs_github_title)
val gitHubUrl = stringResource(R.string.prefs_github_url)
val licenseUrl = stringResource(R.string.prefs_license_url)
@@ -309,18 +310,17 @@ private fun SettingsScreen(uiState: SettingsState, onAction: (SettingsAction) ->
if (isVerticalLayout) {
TopBar {
TopCard(
onClick = { safeOpenUri(appUrl) },
onClick = { showUpdateChecker = true },
version = uiState.appVersionName,
modifier = Modifier.weight(1f)
)
PrimaryIconCard(onClick = { safeOpenUri(donateUrl) }, resId = R.drawable.ic_pound)
}
TopBar {
Row(modifier = Modifier.weight(1f), horizontalArrangement = Arrangement.spacedBy(6.dp)) {
BotCard(
onClick = { safeOpenUri(fdroidUrl) },
resId = R.drawable.ic_fdroid,
text = fdroidTitle,
onClick = { safeOpenUri(upstreamUrl) },
resId = R.drawable.ic_github,
text = upstreamTitle,
modifier = Modifier.weight(1f)
)
BotCard(
@@ -335,17 +335,16 @@ private fun SettingsScreen(uiState: SettingsState, onAction: (SettingsAction) ->
}
} else {
TopBar {
PrimaryIconCard(onClick = { safeOpenUri(donateUrl) }, resId = R.drawable.ic_pound)
TopCard(
onClick = { safeOpenUri(appUrl) },
onClick = { showUpdateChecker = true },
version = uiState.appVersionName,
modifier = Modifier.weight(1f)
)
Row(modifier = Modifier.weight(1f), horizontalArrangement = Arrangement.spacedBy(6.dp)) {
BotCard(
onClick = { safeOpenUri(fdroidUrl) },
resId = R.drawable.ic_fdroid,
text = fdroidTitle,
onClick = { safeOpenUri(upstreamUrl) },
resId = R.drawable.ic_github,
text = upstreamTitle,
modifier = Modifier.weight(1f)
)
BotCard(
@@ -639,7 +638,7 @@ private fun OtherCard(
ElevatedCard(
modifier = Modifier
.fillMaxWidth()
.height(370.dp)
.height(420.dp) // 370dp + one switch row (light theme)
) {
Column(modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)) {
Text(
@@ -666,7 +665,16 @@ private fun OtherCard(
text = stringResource(R.string.prefs_compass_calibration_button),
modifier = Modifier.fillMaxWidth()
)
SwitchRow(R.string.prefs_other_switch_night_mode, settings.stateOfNightMode) {
// Appearance: light theme + the red night filter (mutually exclusive —
// the night filter is disabled while the light theme is on).
SwitchRow(R.string.prefs_other_switch_light_theme, settings.stateOfLightTheme) {
onAction(SettingsAction.ToggleLightTheme(it))
}
SwitchRow(
R.string.prefs_other_switch_night_mode,
settings.stateOfNightMode,
enabled = !settings.stateOfLightTheme
) {
onAction(SettingsAction.ToggleNightMode(it))
}
}
@@ -762,14 +770,19 @@ private fun CompassCalibrationDialog(
}
@Composable
private fun SwitchRow(labelResId: Int, checked: Boolean, onCheckedChange: (Boolean) -> Unit) {
private fun SwitchRow(
labelResId: Int,
checked: Boolean,
enabled: Boolean = true,
onCheckedChange: (Boolean) -> Unit
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Text(text = stringResource(id = labelResId))
Switch(checked = checked, onCheckedChange = onCheckedChange)
Switch(checked = checked, enabled = enabled, onCheckedChange = onCheckedChange)
}
}
@@ -845,27 +858,28 @@ private fun CardCreditsPreview() = MainTheme { CardCredits() }
@Composable
private fun CardCredits(modifier: Modifier = Modifier) {
ElevatedCard(
modifier = modifier
.fillMaxWidth()
.height(268.dp)
) {
ElevatedCard(modifier = modifier.fillMaxWidth()) {
Column(
verticalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.padding(horizontal = 8.dp, vertical = 4.dp)
.fillMaxHeight()
verticalArrangement = Arrangement.spacedBy(6.dp),
modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp)
) {
Text(
text = stringResource(id = R.string.prefs_outro_title),
color = MaterialTheme.colorScheme.primary
)
Text(
text = stringResource(id = R.string.prefs_outro_thanks)
text = stringResource(id = R.string.prefs_outro_notice),
fontSize = 12.sp
)
HorizontalDivider()
Text(
text = stringResource(id = R.string.prefs_outro_thanks),
fontSize = 12.sp
)
Text(
text = stringResource(id = R.string.prefs_outro_license),
color = MaterialTheme.colorScheme.primary
color = MaterialTheme.colorScheme.primary,
fontSize = 12.sp
)
}
}
@@ -70,6 +70,8 @@ data class SettingsState(
val lotwLastSyncEpochMs: Long = 0L,
/** Logbook (QSO records + LoTW confirmations), newest first. */
val logbookRecords: List<com.rtbishop.look4sat.core.domain.logbook.QsoRecord> = emptyList(),
/** ARRL satellite names (config.tq6) — the only names a record may be signed with. */
val satelliteCatalog: List<String> = emptyList(),
/** Imported LoTW upload certificate (null when none). */
val lotwCertificate: com.rtbishop.look4sat.core.domain.repository.LoTWCertificate? = null,
/** LoTW upload station location (null when unset). */
@@ -153,6 +155,8 @@ sealed interface SettingsAction {
// Logbook (QSO records + LoTW confirmations)
data object RefreshLogbook : SettingsAction
data class DeleteLogbookRecord(val id: Long) : SettingsAction
/** Persist an edited record (frequency/callsign/time/…) from the logbook dialog. */
data class UpdateLogbookRecord(val record: com.rtbishop.look4sat.core.domain.logbook.QsoRecord) : SettingsAction
data object PrepareLogbookUpload : SettingsAction
data object ConfirmLogbookUpload : SettingsAction
data object DismissLogbookPreview : SettingsAction