diff --git a/README.md b/README.md
index 61466aee..198b5a0d 100644
--- a/README.md
+++ b/README.md
@@ -4,6 +4,8 @@
**BA7OPF 定制版** — 基于 [rt-bishop/Look4Sat](https://github.com/rt-bishop/Look4Sat) 的业余无线电卫星追踪器,增加了线性卫星频率计算器等功能。
+非官方修改版(fork),独立维护;首次发布 2026-09,持续修改中。
+
## 本仓库特色功能
- **网格地图模式(Grid Mode)** — 地图页内置 VUCC 网格覆盖图:已通联网格绿色填充、漫游网格蓝色 45° 斜纹(疏朗半透明样式)、当前所在方格淡黄加粗框;点击已通联网格弹出该网格内通联明细弹窗;进入网格模式默认 VUCC 视图
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 58745973..87c49991 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -44,7 +44,7 @@
+ android:value="cn.ba7opf.look4sat" />
.
+ */
package com.rtbishop.look4sat.core.data.repository
import com.rtbishop.look4sat.core.domain.model.LatestRelease
diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/usecase/AddToCalendar.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/usecase/AddToCalendar.kt
index 3dbbca35..2db784a8 100644
--- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/usecase/AddToCalendar.kt
+++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/usecase/AddToCalendar.kt
@@ -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)
}
diff --git a/core/presentation/src/main/java/com/rtbishop/look4sat/core/presentation/GridTargetChip.kt b/core/presentation/src/main/java/com/rtbishop/look4sat/core/presentation/GridTargetChip.kt
index de56f9ff..f3e531b7 100644
--- a/core/presentation/src/main/java/com/rtbishop/look4sat/core/presentation/GridTargetChip.kt
+++ b/core/presentation/src/main/java/com/rtbishop/look4sat/core/presentation/GridTargetChip.kt
@@ -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 .
+ */
package com.rtbishop.look4sat.core.presentation
import androidx.compose.foundation.Canvas
diff --git a/core/presentation/src/main/java/com/rtbishop/look4sat/core/presentation/MainTheme.kt b/core/presentation/src/main/java/com/rtbishop/look4sat/core/presentation/MainTheme.kt
index 80f4ade9..64979673 100644
--- a/core/presentation/src/main/java/com/rtbishop/look4sat/core/presentation/MainTheme.kt
+++ b/core/presentation/src/main/java/com/rtbishop/look4sat/core/presentation/MainTheme.kt
@@ -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
}
diff --git a/core/presentation/src/main/java/com/rtbishop/look4sat/core/presentation/SwipeableItem.kt b/core/presentation/src/main/java/com/rtbishop/look4sat/core/presentation/SwipeableItem.kt
index 5e6f448e..f63ac50b 100644
--- a/core/presentation/src/main/java/com/rtbishop/look4sat/core/presentation/SwipeableItem.kt
+++ b/core/presentation/src/main/java/com/rtbishop/look4sat/core/presentation/SwipeableItem.kt
@@ -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 .
+ */
package com.rtbishop.look4sat.core.presentation
import androidx.compose.animation.core.animateDpAsState
diff --git a/core/presentation/src/main/res/values-es/strings.xml b/core/presentation/src/main/res/values-es/strings.xml
index 61e3a2c3..81501d38 100644
--- a/core/presentation/src/main/res/values-es/strings.xml
+++ b/core/presentation/src/main/res/values-es/strings.xml
@@ -83,7 +83,6 @@
Visible
- Soporte
Política de privacidad
Actualizado: %s
diff --git a/core/presentation/src/main/res/values-ru/strings.xml b/core/presentation/src/main/res/values-ru/strings.xml
index a0b44882..e3b5b867 100644
--- a/core/presentation/src/main/res/values-ru/strings.xml
+++ b/core/presentation/src/main/res/values-ru/strings.xml
@@ -82,7 +82,6 @@
Видимый
- Поддержать
Политика конфиденциальности
Обновлено: %s
diff --git a/core/presentation/src/main/res/values-si/strings.xml b/core/presentation/src/main/res/values-si/strings.xml
index fd8acb18..ffd7b92c 100644
--- a/core/presentation/src/main/res/values-si/strings.xml
+++ b/core/presentation/src/main/res/values-si/strings.xml
@@ -83,7 +83,6 @@
දෘශ්ය
- දායකත්ව
රහස්යතා ප්රතිපත්තිය
යාවත්කාලීන කරන ලදී: %s
diff --git a/core/presentation/src/main/res/values-tr/strings.xml b/core/presentation/src/main/res/values-tr/strings.xml
index f3de6b20..d0d65250 100644
--- a/core/presentation/src/main/res/values-tr/strings.xml
+++ b/core/presentation/src/main/res/values-tr/strings.xml
@@ -1,7 +1,7 @@
- Look4Sat
+ Look4Sat-BA7OPF
Kabul et
İptal
Gösterilecek veri bulunamadı
@@ -104,18 +104,16 @@
Görünür
- Look4Sat v%s
- https://play.google.com/store/apps/details?id=com.rtbishop.look4sat
- Bağış yap
- https://ko-fi.com/rt_bishop
- F-Droid
- https://f-droid.org/en/packages/com.rtbishop.look4sat/
+ Look4Sat-BA7OPF v%s
+ https://github.com/atsunatsu/Look4Sat/releases
GitHub
- https://github.com/rt-bishop/Look4Sat/
+ https://github.com/atsunatsu/Look4Sat
GPLv3
https://www.gnu.org/licenses/gpl-3.0.html
Gizlilik Politikası
https://sites.google.com/view/look4sat-privacy-policy/home
+ Upstream Project
+ https://github.com/rt-bishop/Look4Sat
Güncellendi: %s
d MMM yyyy - HH:mm:ss
diff --git a/core/presentation/src/main/res/values-uk/strings.xml b/core/presentation/src/main/res/values-uk/strings.xml
index fa878bd0..761a5178 100644
--- a/core/presentation/src/main/res/values-uk/strings.xml
+++ b/core/presentation/src/main/res/values-uk/strings.xml
@@ -83,7 +83,6 @@
Видимий
- Підтримати
Політика конфіденційності
Оновлено: %s
diff --git a/core/presentation/src/main/res/values-zh/strings.xml b/core/presentation/src/main/res/values-zh/strings.xml
index e8f0ca91..ae1d2fdf 100644
--- a/core/presentation/src/main/res/values-zh/strings.xml
+++ b/core/presentation/src/main/res/values-zh/strings.xml
@@ -41,7 +41,7 @@
请务必在设置中通过GPS、经纬度或QTH定位您的位置\n建议至少每周更新一次数据库,以确保预测结果的准确性
- 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) 适配。
+ 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 卡片。
AMSAT
AMSAT 卫星状态
@@ -141,7 +141,6 @@
日照中
- 支持
隐私政策
更新:%s
@@ -304,6 +303,7 @@
手动航向偏置(°)
范围 −180° 至 +180°;正值为顺时针偏移。
偏置归零
+ 浅色主题
启用红色夜间模式
地图显示 Maidenhead 网格
@@ -312,7 +312,7 @@
中 %1$d-%2$d°
高 > %1$d°
- 我要感谢:
+ 致谢与声明
• Look4Sat 所有用户及贡献者!
\n• David A. B. Johnson (predict4java)
@@ -325,7 +325,9 @@
\n• BH2VSQ BG5JSU(天地图渲染支持)
\n• BG5JSU(LoTW上传支持)
- 该应用程序不提供任何保修.
+ Look4Sat-BA7OPF 是基于 Look4Sat(GPL-3.0)的非官方修改版,由 BA7OPF 独立维护,与原项目无隶属关系,亦未获其背书。
+上游项目:https://github.com/rt-bishop/Look4Sat
+基于 GNU GPL v3 许可 · 本程序不提供任何保修
过境匹配
@@ -374,4 +376,32 @@
下载失败,请重试。
该版本未附带 APK 安装包。
+
+ 编辑通联记录
+ 该通联已上传或已确认。保存将保留原记录不变,并新建一条记录。
+ 呼号
+ 日期 (UTC)
+ 时间 (UTC)
+ 上行频率 (MHz)
+ 下行频率 (MHz)
+ 频段:%1$s
+ 模式
+ 卫星
+ LoTW 只接受它的官方卫星名
+ 发送报告
+ 接收报告
+ 对方网格
+ 备注
+ 保存
+ 取消
+ 呼号不能为空
+ 日期或时间格式不对 —— 应为 yyyy-MM-dd 与 HH:mm:ss(UTC)
+ 频率格式不对 —— 请输入 MHz,例如 145.850
+ 不是 LoTW 官方卫星名 —— 请从列表中选一个
+ 选择
+ LoTW 官方卫星名
+ 搜索卫星名或数据源别名
+ 保存并签名时使用 %1$s
+ 没有上行频率:LoTW 要求 BAND,补上频率前这条无法上传
+
diff --git a/core/presentation/src/main/res/values/strings.xml b/core/presentation/src/main/res/values/strings.xml
index a76797d4..a6176ff8 100644
--- a/core/presentation/src/main/res/values/strings.xml
+++ b/core/presentation/src/main/res/values/strings.xml
@@ -1,7 +1,7 @@
- Look4Sat
+ Look4Sat-BA7OPF
Accept
Cancel
No data to show
@@ -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.
What\'s new in Look4Sat
- 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.
+ 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.
AMSAT
@@ -162,18 +162,16 @@
Visible
- Look4Sat v%s
- https://play.google.com/store/apps/details?id=com.rtbishop.look4sat
- Donate
- https://ko-fi.com/rt_bishop
- F-Droid
- https://f-droid.org/en/packages/com.rtbishop.look4sat/
+ Look4Sat-BA7OPF v%s
+ https://github.com/atsunatsu/Look4Sat/releases
GitHub
- https://github.com/rt-bishop/Look4Sat/
+ https://github.com/atsunatsu/Look4Sat
GPLv3
https://www.gnu.org/licenses/gpl-3.0.html
Privacy Policy
https://sites.google.com/view/look4sat-privacy-policy/home
+ Upstream Project
+ https://github.com/rt-bishop/Look4Sat
Updated: %s
d MMM yyyy - HH:mm:ss
@@ -334,6 +332,7 @@
Manual heading offset (°)
Range −180° to +180°. Positive values rotate clockwise.
Reset offset
+ Light theme
Enable red night mode filter
Show Maidenhead grid on map
@@ -342,7 +341,7 @@
Mid %1$d–%2$d°
High > %1$d°
- I would like to say thanks to
+ Credits & Notices
* 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)
- The app comes with no warranty
+ 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
+Licensed under the GNU General Public License v3 · The app comes with no warranty
Pass Match
@@ -404,4 +405,32 @@
Download failed. Try again.
No APK attached to this release.
+
+ Edit QSO
+ This QSO was already uploaded or confirmed. Saving keeps the original record unchanged and creates a new one.
+ Callsign
+ Date (UTC)
+ Time (UTC)
+ Uplink (MHz)
+ Downlink (MHz)
+ Band: %1$s
+ Mode
+ Satellite
+ LoTW only accepts its official satellite name
+ Sent RST
+ Received RST
+ Opposite grid
+ Comment
+ Save
+ Cancel
+ Callsign is required
+ Invalid date/time — use yyyy-MM-dd and HH:mm:ss (UTC)
+ Invalid frequency — enter MHz, e.g. 145.850
+ Not an ARRL satellite name — pick one from the list
+ Pick
+ ARRL satellite name
+ Search name or catalogue alias
+ Saved and signed as %1$s
+ No uplink frequency: LoTW requires a band, so this QSO cannot be uploaded yet
+
diff --git a/feature/cw/src/main/java/com/rtbishop/look4sat/feature/cw/CwDecodeScreen.kt b/feature/cw/src/main/java/com/rtbishop/look4sat/feature/cw/CwDecodeScreen.kt
index c2387315..ec897c3c 100644
--- a/feature/cw/src/main/java/com/rtbishop/look4sat/feature/cw/CwDecodeScreen.kt
+++ b/feature/cw/src/main/java/com/rtbishop/look4sat/feature/cw/CwDecodeScreen.kt
@@ -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 .
+ */
/*
* CwDecodeScreen.kt — Look4Sat 的 CW 解码页面(Compose 外壳)。
*
diff --git a/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/AwardBoundaryOverlay.kt b/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/AwardBoundaryOverlay.kt
index beb2e517..bc952271 100644
--- a/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/AwardBoundaryOverlay.kt
+++ b/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/AwardBoundaryOverlay.kt
@@ -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 = emptyList()
set(value) {
diff --git a/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MaidenheadGridOverlay.kt b/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MaidenheadGridOverlay.kt
index 66aa8dc3..c715efde 100644
--- a/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MaidenheadGridOverlay.kt
+++ b/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MaidenheadGridOverlay.kt
@@ -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 = emptySet()
diff --git a/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapFilterViewModel.kt b/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapFilterViewModel.kt
index ecb4c4a1..9d2af440 100644
--- a/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapFilterViewModel.kt
+++ b/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapFilterViewModel.kt
@@ -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 .
+ */
package com.rtbishop.look4sat.feature.map
import androidx.compose.runtime.MutableState
diff --git a/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapScreen.kt b/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapScreen.kt
index a66f2f47..9dc17002 100644
--- a/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapScreen.kt
+++ b/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapScreen.kt
@@ -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, 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
}
diff --git a/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapViewModel.kt b/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapViewModel.kt
index 179149d2..4c21261f 100644
--- a/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapViewModel.kt
+++ b/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapViewModel.kt
@@ -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,
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 e5390daa..70c58b0d 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
@@ -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
)
}
}
diff --git a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsState.kt b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsState.kt
index c19f4f3b..46086d80 100644
--- a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsState.kt
+++ b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsState.kt
@@ -70,6 +70,8 @@ data class SettingsState(
val lotwLastSyncEpochMs: Long = 0L,
/** Logbook (QSO records + LoTW confirmations), newest first. */
val logbookRecords: List = emptyList(),
+ /** ARRL satellite names (config.tq6) — the only names a record may be signed with. */
+ val satelliteCatalog: List = 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