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