Commit Graph
100 Commits
Author SHA1 Message Date
atsunatsu fa51fb2b44 build: bump version to 4.4.7-ba7opf.15.4 (501) 2026-09-24 09:17:55 +08:00
atsunatsu 75a251eb98 fix: keep last good AMSAT live lists on fetch failures; whitelist mirror
- Failed whitelist fetch (raw.githubusercontent.com unreachable on some
  networks) no longer wipes the persisted whitelist or admits every ISS
  module alias: fall back to the persisted whitelist, and only persist
  when a stream was actually fetched.
- Multi-match collapse without any whitelist now keeps the primary entry
  (smallest catnum, ISS ZARYA=25544) instead of every alias (DESTINY etc.).
- Failed FM/Linear page fetch keeps the previous lists instead of
  clearing the filters.
- Amateur whitelist gets a jsDelivr CDN mirror as fallback source.
2026-09-24 09:17:17 +08:00
atsunatsu 91b5269740 build: bump version to 4.4.7-ba7opf.15.3 (500) 2026-09-24 08:49:53 +08:00
atsunatsu 522cac25e8 fix: correct AMSAT live filter satellite lists
- Resolve ISS station modules (ZARYA/UNITY/ZVEZDA/DESTINY/NAUKA) to the
  primary 25544 instead of whichever module appears first in the TLE source
- Match OSCAR designators to local names spelled 'OSCAR N'/'AMSAT-OSCAR N'
  (AO-7 -> OSCAR 7) so Linear lists keep AO-7 on SatNOGS-sourced data
- SSTV virtual type now uses the transceiver service class (Amateur only)
  plus launcher-debris name exclusion, dropping retired weather birds
  (TIROS) and rocket bodies (ARIANE 6 R/B) while keeping ISS/CSS/TEIKYOSAT
- Persist the AMSAT active-frequency whitelist (palewire mirror) on sync;
  used only to disambiguate multi-match names, never as a hard filter
2026-09-24 08:49:53 +08:00
atsunatsu 732412deb2 build: bump version to 4.4.7-ba7opf.15.2 (499) 2026-09-24 02:53:53 +08:00
atsunatsu 30000a913e fix: AMSAT name matching token-based; SSTV filters to in-orbit only
- matchesAmSatName: exact token match instead of substring contains,
  so 'ISS' no longer matches AISSAT-1 and 'AO-7' no longer matches
  AO-73 (FUNCUBE-1); resolves missing ISS in FM and missing AO-7 in
  Linear lists
- resolveTypeIds: intersect SSTV catnums with current in-orbit TLE set,
  excluding retired satellites (e.g. TIROS) whose radios are tagged SSTV
- add regression tests for token matching
2026-09-24 02:53:34 +08:00
atsunatsu e7754a4c9e build: bump version to 4.4.7-ba7opf.15.1 (498) 2026-09-24 00:51:21 +08:00
atsunatsu cf1e2b99b4 fix: transponder filters - mutual exclusion and beacon-free fallback
Two correctness fixes for the FM/Linear/SSTV filters:

1. Satellite type picker: virtual transponder types (AMSAT Live FM,
   AMSAT Live Linear, Live SSTV) are now mutually exclusive with the
   regular TLE-source types - ticking a virtual type clears Amateur/
   CubeSat/etc and vice versa, so selecting 'AMSAT Live FM' no longer
   unions in hundreds of non-transponder satellites. Virtual types
   still combine with each other. Also: an empty virtual-type resolve
   now yields an empty list instead of falling back to 'show all'.

2. Mutual-match fallback: when the AMSAT lists are empty (first run /
   sync failure) the DB mode filter used downlinkMode alone, so any
   downlink-only beacon sharing a mode label was misclassified
   (ISS/IO-86 CW beacons into Linear, FO-29's FM Digitalker into FM).
   New getIdsWithModesAndUplink() requires uplinkLow IS NOT NULL,
   matching real transponders only. SSTV filter keeps the old query
   (SSTV sats are often downlink-only).

Shared virtual-type names moved to Sources.virtualTypeNames.
2026-09-24 00:51:13 +08:00
atsunatsu b295500d14 build: bump version to 4.4.7-ba7opf.15 (497) 2026-09-23 23:26:03 +08:00
atsunatsu 0163c7b217 feat: satellite page transponder/activity filters as virtual types
The satellite selection screen's 'Select satellite type' dialog now lists
three virtual types first (AMSAT Live FM, AMSAT Live Linear, Live SSTV)
before the TLE-source types. Selecting one filters the satellite list to
that transponder/activity set; multiple selections union. 'All' is kept.

- SelectionRepo.getTypesList: virtual types prepended (All preserved)
- SelectionRepo.resolveTypeIds: FM/Linear -> AMSAT catnum sets,
  Live SSTV -> mode=SSTV radios; other types via persisted IDs
- Tests: virtual-type filtering, union, clear, list-order assertion
2026-09-23 23:25:52 +08:00
atsunatsu 85a4b9cfa3 feat: mutual pass FM/Linear transponder filters (AMSAT Live source)
Mutual match page gains FM/Linear transponder-type filters. The
authoritative source is AMSAT's human-maintained Live FM/Linear
Satellites pages, fetched during data sync and stored as catnum sets;
filters resolve to those sets (falling back to DB mode filter when
empty), and unchecking both shows a prompt instead of querying.

- DataParser.parseAmSatLivePage: table rows to satellite names,
  TEVEL2-1 thru TEVEL2-9 range expansion, bracket-alias matching
- SettingsRepo: AMSAT FM/Linear catnum sets persisted in prefs
- DatabaseRepo: fetch AMSAT pages in updateFromRemote, best-effort
  (failure keeps previous list, never blocks the update)
- getIdsWithModes: AND isAlive = 1
- MutualScreen: FilterChip row inside MatchSearchCard
- Tests: parser (4) + viewmodel (3) + repo fakes
2026-09-23 23:25:37 +08:00
atsunatsu c4c4dd682d build: bump to 4.4.7-ba7opf.14.2 2026-09-23 15:54:59 +08:00
atsunatsu 62ad995aeb feat(map): multiple marked stations per grid with pin-to-top
- MarkedStation storage upgraded to an ordered list per grid
  (Map<String, List<MarkedStation>>); JSON array shape with legacy
  v14.1 single-object migration on read
- MapAction: SetMarkedStation appends (duplicate callsign ignored),
  RemoveMarkedStation deletes by callsign, PinMarkedStation moves a
  mark to the top of the list
- MaidenheadGridOverlay: red fill for any marked grid unchanged;
  first-call label mode shows the FIRST (pinned) marked callsign
- MapScreen: one row per mark (pale-yellow callsign), pin icon
  (冖 + stroke ↑, drawn in-app) left of the Marked label — shown only
  when the grid has 2+ marks; pinned row's icon grey, others pale
  yellow; delete icon per row
- New ic_pin.xml vector drawable; strings grid_mark_pin
2026-09-23 15:47:17 +08:00
atsunatsu c8fdd2e93e build: bump to 4.4.7-ba7opf.14.1 2026-09-23 15:15:47 +08:00
atsunatsu dc013f920d feat(map,mutual): uppercase callsign input; mutual match honors historical pass window
- MapScreen: mark-a-station callsign input uppercases as you type
- MutualViewModel: read passesSettings.hoursBefore, shift the mutual
  match query start to now-hoursBefore so historical passes are
  included when the main Passes page has a lookback window set;
  endTime stays at now+hoursAhead
2026-09-23 15:08:46 +08:00
atsunatsu f36a426855 build: bump to 4.4.7-ba7opf.14 2026-09-23 13:26:00 +08:00
atsunatsu 4da59bdd17 feat(map): mark a station you want to contact in unworked grids
- New MarkedStation model + ISettingsRepo/SettingsRepo persistence
  (single JSON key markedGridStations, org.json only)
- MapState/MapViewModel: markedGrids state, Set/RemoveMarkedStation
  actions; marks auto-clear once the grid becomes worked
- MaidenheadGridOverlay: red fill (alpha 90, same as green worked
  fill) for marked grids at both zoom levels; first-call label mode
  shows the marked callsign in pale yellow on red cells
- MapScreen: unworked-grid dialog gains a 'mark a station you want
  to contact' button + callsign input dialog; marked row shows red
  callsign, 'Marked' in the count column, marking date, delete icon
- i18n: values/ + values-zh/ (grid_mark_*)
2026-09-23 13:14:38 +08:00
atsunatsu b8138c948b build: bump to 4.4.7-ba7opf.13 2026-09-22 16:41:59 +08:00
atsunatsu 0e28231c4a feat(map): run the Match query before navigating to the mutual page
Tapping the grid-QSO dialog "Match" button now starts the pass query on
the map page and only navigates to the match page once it finishes, so the
page's first frame already contains the results — the time-range card at
the top and the pass curves right below, with no scroll flicker. The dialog
button shows a spinner and is disabled while calculating. A uiState
collector in MainScreen performs the actual navigation (still a push, so
the back gesture returns to the map). Failures/exceptions still navigate so
the user is never stuck on the map page.
2026-09-22 16:41:55 +08:00
atsunatsu d6bdf5ecb6 fix(mutual): keep scroll position when re-querying from the match page
queryMutualPasses() reset listScrollIndex to 0 on every query, so tapping
"query" on the match page jumped the list back to the top. Now the default
keeps the current position; the stale results stay on screen while the new
query runs (clearing them shrank the list below a screen and clamped the
position away) and are replaced when the new results arrive. New VM test
covers position + stale-results behaviour through a settled re-query.
2026-09-22 16:41:51 +08:00
atsunatsu 6962a4bfa1 fix(mutual): retry prefill scroll until it really lands
On a device the first frame of the match page only contains the station
cards and the time-range card, which can be shorter than the viewport —
scrollToItem(1) then has no scroll range and silently does nothing, and
by the time the async query results grow the list past one screen the
one-shot flag is already consumed. Retry every 100ms (up to 2s) until
firstVisibleItemIndex reaches the time-range card. Verified with a new
Robolectric test that grows the content after a delay. Bump 12.6.
2026-09-22 15:50:31 +08:00
atsunatsu 94a1a4f7b7 diagnostics(mutual): log prefill scroll path under tag Look4SatMutual
Temporary Log.d probes at prefill, list-layout gate, scroll effect entry,
scrollToItem outcome and consume, to locate why the map Match prefill
scroll does not land on the device (Robolectric covers it). Bump 12.5.
2026-09-22 15:13:47 +08:00
atsunatsu dbf2229774 chore: bump version to 4.4.7-ba7opf.12.4 (versionCode 490) 2026-09-22 14:49:32 +08:00
atsunatsu f2fc1b7b28 test(mutual): portrait-size Robolectric + NavDisplay entry scenario
Run the scroll tests at 411x891dp portrait (device-like layout) and add
a NavDisplay entryProvider scenario that composes MutualScreen the way
the app does, so the prefill scroll is exercised end-to-end.
2026-09-22 14:49:04 +08:00
atsunatsu d600836e19 fix(mutual): gate prefill scroll on real list layout (onGloballyPositioned)
On a device the Mutual entry is composed inside the NavDisplay transition;
LaunchedEffect could run before the LazyColumn is measurable and the
scrollToItem then never lands, leaving the page at the top. Only scroll
once onGloballyPositioned confirms the list has actually been laid out.
2026-09-22 14:49:00 +08:00
atsunatsu 94bffe80b0 chore: bump version to 4.4.7-ba7opf.12.3 (versionCode 489) 2026-09-22 13:13:11 +08:00
atsunatsu 4260fbb096 fix(nav): map Match prefill pushes Mutual onto the stack
The grid-QSO dialog Match button now adds Mutual on top of the Map entry
instead of clearing the stack, so the system back gesture pops back to
the map page (the page the user came from). Bottom-nav tab switches keep
their replace semantics.
2026-09-22 13:13:08 +08:00
atsunatsu 3d5b55faa3 test(mutual): Robolectric Compose UI tests for prefill scroll; bump 4.4.7-ba7opf.12.2
MutualMatchPrefillScrollTest verifies the map grid-QSO Match prefill:
time-range card lands at the top after scrollToItem, default open stays
at station inputs, and the query auto-starts with grid+24h pre-filled.
Adds Robolectric + compose ui-test-junit4 to the mutual test classpath.
2026-09-22 12:23:55 +08:00
atsunatsu 466f2f6088 fix(mutual): map Match prefill scrolls to time-range card via scrollToItem
Compose ui 1.11.x (BOM 2026.06.01) ignores the LazyListState
firstVisibleItemIndex constructor parameter - the list always starts at
item 0. Restore the scrollToItem approach (verified effective by
Robolectric Compose UI tests) and keep the auto-started query.
2026-09-22 12:23:50 +08:00
atsunatsu 14bd984327 chore: bump version to 4.4.7-ba7opf.12.1 (versionCode 487) 2026-09-22 11:05:50 +08:00
atsunatsu d5c100903b fix(mutual,passes): map Match prefill opens at time-range card; whats-new dialog scrollable
- Match prefill now positions the LazyColumn via its initial state
  (queryMutualPasses(initialScrollIndex = 1)) instead of a post-layout
  scrollToItem, so the page reliably opens with the time-range card at the
  top while the 24h query runs.
- The install-time what's new dialog text now scrolls inside a bounded box
  (max 360dp) instead of overflowing the fixed dialog frame.
2026-09-22 11:05:47 +08:00
atsunatsu 1c6388f3bc chore: bump version to 4.4.7-ba7opf.12 (versionCode 486) 2026-09-22 10:47:00 +08:00
atsunatsu 3e0874e6a8 feat(map): grid QSO dialog Match button prefills the match page
- Grid-QSO dialog gains a Match action (radio-tower icon + label) in its
  header; tapping it navigates to the match page pre-filled with the tapped
  grid as the opposite station and a 24h time range, auto-starts the query,
  and scrolls the page straight to the time-range card.
- Every grid is now tappable (previously only worked grids opened the
  dialog); unworked grids show an empty QSO list instead of a hint.
2026-09-22 10:46:56 +08:00
atsunatsu 6beaeb3e1a chore: bump version to 4.4.7-ba7opf.11.4 (versionCode 485) 2026-09-21 16:51:50 +08:00
atsunatsu 91f0cb1c56 fix(map): grid labels match first-call font size and display zoom 2026-09-21 16:51:13 +08:00
atsunatsu 3b72f8d68a fix(res): rename pass-history slider label 先前时间 to 历史过境时间 (values-zh) 2026-09-21 16:51:09 +08:00
atsunatsu 3bc2983d26 fix(passes): expire finished passes beyond history window, refresh keeps lookback 2026-09-21 16:51:06 +08:00
atsunatsu 0ec4c5dc6c chore: bump version to 4.4.7-ba7opf.11.3 (versionCode 484) 2026-09-21 11:35:33 +08:00
atsunatsu 8e8a3849df fix(map): first-call labels now appear exactly when grid lines do
The 11.2 gate only relaxed the pre-return zoom check, but the showLabels
gate below it still required LABEL_ZOOM_SUB (6.5), so first-call labels
stayed hidden in the 6.0..6.5 band where the 4-char grid lines render.
- Gate showLabels on GRID_ZOOM_SUB (6.0) when showFirstCallLabels is on.
- Relax MIN_LABEL_CELL_PX (48) to 0.6x for first-call mode so 1° cells
  (~45 px on the equator at zoom 6.0) are not filtered out.
Normal grid-code labels are unchanged (still LABEL_ZOOM_SUB).
2026-09-21 11:35:10 +08:00
atsunatsu cd65e6c077 fix(res): single-line what's new string, unformatted qso count, si typo
- pass_whatsnew_message rewritten as a single line with escaped \n bullets:
  real newlines in the value are invalid for aapt2 ('not a valid string').
- Escape the apostrophe in "map's"; mark grid_qso_calls_count
  formatted=false (two %d + '·' triggered multiple-substitutions).
- values-si: remove stray comma after map_altitude </string>.
All were latent resource errors surfaced by the forced recompile.
2026-09-21 11:11:52 +08:00
atsunatsu a3d370b1fa chore: bump version to 4.4.7-ba7opf.11.2 (versionCode 483) 2026-09-21 10:52:35 +08:00
atsunatsu bf492e3f8b feat(passes/radar): port pass history + compass calibration, cumulative what's new
- Pass history (Iu-yang1 fork): PassesSettings.hoursBefore look-back window;
  second 'Time before' slider in the pass filter; completed passes stay
  listed (progress clamped to [0,1]) in the history window.
- Compass calibration (Iu-yang1 fork): OtherSettings.compassOffsetDegrees,
  CompassAccuracy from the magnetic sensor, calibration dialog with
  figure-eight accuracy meter, corrected heading and manual offset field;
  RadarViewModel applies the offset.
- What's New dialog now shows the cumulative 4.4.6->4.4.7 changelog and
  follows the system language (values/ + values-zh/).
- pass_filter_history_hours localized in es/ru/si/tr/uk.
2026-09-21 10:42:45 +08:00
atsunatsu 3591960d55 feat(map): show first-call labels as soon as grid lines appear
Move the first-call label gate from LABEL_ZOOM_SUB (6.5) to GRID_ZOOM_SUB
(6.0): worked (green) cells are labelled with their first callsign as soon
as the 4-char grid lines render, not only at the name-label zoom.
2026-09-21 10:42:37 +08:00
atsunatsu 3f7a087938 chore: bump version to 4.4.7-ba7opf.11.1 (versionCode 482) 2026-09-21 09:44:46 +08:00
atsunatsu 4b82332133 feat(map): first-call callsign label toggle in VUCC grid mode
Add a 'first call' pill toggle below the grid-mode switch (map top-right,
visible only in grid mode + VUCC award). When on, worked (green) cells are
labelled with the first callsign worked in that grid (earliest confirmed
QSO by time) instead of the Maidenhead code; non-worked cells get no label
at all. Persisted via OtherSettings.stateOfMapFirstCall (full MVI chain).
The feature is VUCC-only: All and other award overlays never draw it.
2026-09-21 09:44:43 +08:00
atsunatsu dcd16a0196 chore: bump version to 4.4.7-ba7opf.11 (versionCode 481) 2026-09-19 04:27:55 +08:00
atsunatsu 9e88609efc feat(amsat): show consecutive same-status report count in status cells
Day-cell numbers now count the most recent consecutive reports sharing the
newest status (counting back from the newest report, stopping at the first
different status), instead of the total report count of the day. Empty gaps
do not break the streak; statuses compare by exact report text; counting
does not cross day boundaries.

- SatDay gains streakCount (computed in AmSatRepository.buildStatuses)
- DayCell renders day.streakCount instead of slot count sum
- Add real org.json to core:data unit tests (android.jar stub cannot parse)
- Add 2 streak tests: mixed-status slot breaks, all-same-status counts all
2026-09-19 04:27:19 +08:00
atsunatsu 322becdda8 feat(satellites): fuzzy name search ignoring separators
The satellite search only matched exact continuous substrings, so names with
dashes, spaces or brackets (AO-7, ISS (ZARYA), FO-29) could not be found by
queries without those separators. Now the query is normalized (lowercased,
non-alphanumeric separators stripped) and split into space-separated tokens,
and every token must appear in the normalized satellite name. This makes
'ao7' match 'AO-7 (AMSAT-OSCAR 7)' and 'iss zarya' match 'ISS (ZARYA)'.
Numeric queries still match catnum exactly. Covered by 8 new unit tests.
2026-09-18 14:49:41 +08:00
atsunatsu 7f93a10556 chore: bump version to 4.4.7-ba7opf.9 (versionCode 479) 2026-09-18 02:04:09 +08:00
atsunatsu 3cd5b49f98 feat(mutual): keep pass list scroll position across page switches
The mutual pass list LazyColumn reset to the top on every return to the page.
Scroll position is now stored in the Activity-scoped MutualViewModel and
restored on re-entry; a new query (queryGeneration bump) rebuilds the list at
the top so stale positions never leak into fresh results.
2026-09-18 02:04:04 +08:00
atsunatsu 4dbe896124 feat(map): keep grid-mode viewport across page switches
The osmdroid map was recreated on every return to the Map page, resetting
center/zoom to the default and forcing grid mode back onto the station grid.
Now the viewport (center + zoom) is saved in the Activity-scoped
MapFilterViewModel only in grid mode and restored on re-entry, so the user
keeps exactly where they left off. Satellite mode keeps its original
follow-the-satellite behavior; cold-start entries and in-page grid toggles
still center on the station grid as before.
2026-09-18 02:03:57 +08:00
atsunatsu b47bd78ecd feat(map): keep award filter selection across page switches
The award chip (VUCC/DXCC/WAJA/...) now lives in an Activity-scoped
MapFilterViewModel instead of a per-composition remember, so switching
to another page and back keeps the last selection; it resets to VUCC
only once per process (cold start). Removes the effect that forced
VUCC on every grid-mode entry.
2026-09-17 12:57:36 +08:00
atsunatsu c89e79e28f chore: bump version to 4.4.7-ba7opf.8 (versionCode 478) 2026-09-17 11:53:44 +08:00
atsunatsu b40a79e3c6 docs: credit BG5JSU with CW decoding feature 2026-09-17 11:38:19 +08:00
atsunatsu 6c23696e22 docs: add BG5JSU to acknowledgments (after BG7NTA) 2026-09-17 11:25:44 +08:00
atsunatsu f997d95cd4 chore: bump version to 4.4.7-ba7opf.7 (versionCode 477) 2026-09-17 10:41:11 +08:00
atsunatsu e0a1d279ce i18n(zh): localize update time to yyyy年M月d日 HH:mm:ss 2026-09-17 10:20:26 +08:00
atsunatsu 059c816075 feat(lotw): show last sync time on settings, precise incremental cursor
The stored sync cursor was 'yyyyMMdd', which LoTW's qso_qslsince silently
ignores (measured live: it falls back to the system-supplied default).
Write a full UTC 'yyyy-MM-dd HH:mm:ss' cursor instead — the documented
format LoTW honors — so incremental pulls are exact (second precision)
and the settings page can show the last successful sync time on the LoTW
card, formatted like the ephemeris update time (prefs_updated_title).
Legacy yyyyMMdd cursors are normalized for the API, the gate and the
display (lotwCursorApi/lotwCursorDate/lotwCursorEpochMs).
2026-09-17 10:13:52 +08:00
atsunatsu 60dfa5d600 feat(lotw): auto-sync grids on app start with separate toggle
Mirrors the ephemeris auto-update check: on every app start, if LoTW
credentials are configured, the new 'auto-sync LoTW grids' toggle is on,
at least one manual sync was done before, and today has not synced yet
(ARRL rate limit, ~once a day), pull confirmations since the last sync
and merge them. Same callsign -> incremental merge; callsign change ->
full replace. Manual dialog and automatic path share the orchestration
(mode resolution, gate, merge/persist) moved to core:domain
LoTWGridSync.kt; LoTWSyncMode moved to core:domain. 8 unit tests.
2026-09-16 20:18:22 +08:00
atsunatsu 5f1eb239ed feat(lotw): cancel in-flight sync, honest progress bar, drop stale hint
- Cancel: the dialog's cancel/back while syncing now aborts the download job
  (cooperative cancellation in the stream reader, CancellationException
  propagated, nothing written to storage). An accidental full sync can be
  stopped immediately.
- Progress: the size estimate is conservative (900 B/record vs measured 720)
  so the bar never hits 100% before the download truly ends; storage writes
  moved to the IO dispatcher (multi-MB per-grid QSO JSON used to freeze the
  main thread after download); when the bar caps, the UI switches to
  'Processing downloaded data…' instead of looking stuck.
- Removed the outdated '3-10 seconds' hint (misleading for large accounts).
2026-09-16 18:17:52 +08:00
atsunatsu 01ca06b43f chore: bump version to 4.4.7-ba7opf.6 (versionCode 476) 2026-09-16 16:58:05 +08:00
atsunatsu 22363c2549 fix(map): roamed-grid stripes keep geographic density
Stripe spacing now scales as 44px * 2^(zoom-7) instead of a fixed 44px on
screen: at the map's max zoom (7.0) the stripes keep the established look,
and at lower zooms they shrink with the map, so stripes stay pinned to the
map (same stripes-per-cell at every level) — denser on screen when zoomed
out, sparser when zoomed in.
2026-09-16 16:53:54 +08:00
atsunatsu 45fe755470 ui(dialogs): pad title away from cancel; LoTW buttons bottom corners
- SharedDialog title gets start padding when only a cancel button is shown
  (was flush against it); dialogs with both buttons unchanged.
- LoTW dialog: full-sync button bottom-left, incremental-merge bottom-right
  (SpaceBetween instead of side-by-side).
2026-09-16 16:53:50 +08:00
atsunatsu 0ce177cfbf fix(mutual/radar): bigger radar shortcut; no bounce-back on finished pass
- Pass card header: radar icon button enlarged to 48dp touch target with a
  24dp icon, spanning both header rows vertically (name/time + elevations);
  the pass info column narrows so the Opposite elevation shifts left.
- RadarDestination no longer auto-returns instantly when the mutual pass has
  already ended — the auto-return now only runs while the pass is actually in
  progress. Previously the collapsed-card radar shortcut appeared broken
  ('flash back to Mutual') whenever the computed pass had finished.
- Fakes: add the 4 last-sync bookkeeping members missed by the LoTW commit.
2026-09-16 16:53:46 +08:00
atsunatsu f3fdf61784 feat(mutual): radar shortcut icon on collapsed pass card
Icon-only radar button at the right end of the collapsed pass header (always
visible, not only when expanded); the pass info shifts left. The button's tap
is consumed and does not toggle the card expansion.
2026-09-16 14:34:04 +08:00
atsunatsu fc028f852c feat(lotw): full/incremental sync modes with live progress and time estimate
Two explicit sync buttons in the LoTW dialog:
- Full sync: qso_qslsince=2000-01-01, fresh report replaces all stored data.
- Incremental merge: since=last successful sync date, merged into the stored
  set (grids union, QSOs deduped by call + time). First sync / callsign change
  automatically falls back to full replace so no stale grids from another
  account linger.

Progress & estimates:
- Report body is now streamed; the header's <APP_LoTW_NUMREC> gives the real
  QSL count, total size is estimated as NUMREC * 720 B (measured), and the UI
  shows a determinate bar with 'N QSOs, ~X s remaining' once known.
- Pre-sync hint shows the measured rate (~8 s per 100 QSOs).
- readTimeout raised to 600 s (stall timeout, not a total cap — huge accounts
  legitimately take 30+ min at ARRL's ~10 KB/s stream rate); connectTimeout
  raised to 60 s (a busy server took 36.5 s to accept a connection).

Also: ISettingsRepo gains last-sync date/callsign bookkeeping, and the mutual
test fake implements the new members.
2026-09-16 14:34:00 +08:00
atsunatsu 7a865fffd7 chore: bump version to 4.4.7-ba7opf.5 (versionCode 475) 2026-09-16 11:57:27 +08:00
atsunatsu 89220a0b4b fix(status): day tiles show total daily report count
DayCell displayed the count of the newest non-gray slot only; it now sums the
counts of all 12 slots of the day. The tile color still reflects the newest
reported status.
2026-09-16 11:57:24 +08:00
atsunatsu 03d0055ce2 refactor(settings): drop redundant map-grid toggle
Grid mode is toggled from the map page pill (MapAction.ToggleGridMode writes
stateOfMapGrid); the settings-page switch was a duplicate entry point. Removes
the SwitchRow, the ToggleMapGrid action and its ViewModel branch, and trims
the OtherCard height by one row.
2026-09-16 11:57:18 +08:00
atsunatsu afb1e434e8 fix(map): constrain VUCC grid selector width to its content
The expanded option rows used fillMaxWidth inside a full-screen Box, which
stretched the selector's dark surface across the whole map. The Column now
sizes to its widest child (IntrinsicSize.Max) so the box is just wide enough
for the label, count and expand arrow.
2026-09-16 11:57:11 +08:00
atsunatsu 1bed7849a3 feat(mutual): mutual page times follow the UTC setting
The pass card time range and the elevation-curve X-axis labels were hardcoded
to the device timezone. MutualUiState now carries isUtc, initialized and kept
in sync with OtherSettings.stateOfUtc (same pattern as PassesViewModel), and
both the card timeFormat and the chart axis use UTC when the setting is on.

Also fixes pre-existing test-fake compile breakage: FakeSettingsRepo built
OtherSettings with positional args missing shouldSeeWhatsNew, and never
implemented the wavelogSettings/lotwSettings/worked-grids members added by
later interface extensions.
2026-09-16 11:57:03 +08:00
atsunatsu 06f00e1601 chore: bump version to 4.4.7-ba7opf.4 (versionCode 474) 2026-09-16 04:10:03 +08:00
atsunatsu 462c3de155 fix(radar): elevation color thresholds not applied on radar page
Root cause: LocalElevationThresholds was provided inside MainScreen, but
RadarDestination is a root-level sibling entry in NavRoot's NavDisplay, so
it fell outside the CompositionLocal scope and always used the default
15/45 thresholds.

Fix: hoist the CompositionLocalProvider to NavRoot so both the tab content
and the root-level RadarDestination read the user's low/high elevation
thresholds. Also affects upstream (same navigation structure).
2026-09-16 02:41:53 +08:00
atsunatsu 4020381b03 chore: bump version to 4.4.7-ba7opf.3 (versionCode 473) 2026-09-15 11:16:33 +08:00
atsunatsu b8389f63fe fix(awards): close antimeridian-straddling rings so shared zone/country boundaries render
WAZ/DXCC award rings are stored with the closing edge implicit (the renderer
close()s single-segment rings). Rings that straddle ±180° are split into
multiple segments, and multi-segment rings must not be close()d (each close()
would draw a spurious chord across the map), so the implicit closing edge
was never drawn -> the shared boundary between WAZ zones 31/32 (165E,-8 ->
180,-12), and the closing edges of Russia's main ring / Fiji / Russia's
Chukotka tip in DXCC, stayed open.

Fix: store every ring explicitly closed (append first point). splitRingAt
Antimeridian then cuts the closing edge at ±180 exactly like any other edge,
so each segment ends on the correct side and the boundary lines render.
2026-09-15 11:16:32 +08:00
atsunatsu d7d4a752ed chore: bump version to 4.4.7-ba7opf.2 (versionCode 472) 2026-09-15 01:49:33 +08:00
atsunatsu 6a3ab54226 fix(map): '1 QSO' singular never matched on zh systems
Android plurals have no 'one' category in Chinese (zh plural rules only
contain 'other'), so a single QSO rendered as '1 QSOs' on Chinese-locale
devices. Replace the plurals resource with explicit singular/plural
strings selected in code.
2026-09-15 01:34:04 +08:00
atsunatsu 3b3579696b fix(map): worked grids follow the selected operated grid; add All option
Switching the operated-grid selector now re-filters the green worked
cells on the map (the overlay receives the per-grid worked set instead
of the global one, on every recomposition via setGridMode). Previously
only the VUCC progress count changed while the map stayed green for all
operated grids.

The selector gains an 'All (n)' entry at the bottom of the list: it
selects no operated grid (null), showing every worked grid combined and
restoring the global VUCC count.
2026-09-15 00:25:12 +08:00
atsunatsu 826f44829b fix(map): polar field-row grid lines vanish at field zoom
Maidenhead row R (80-90N) and row A (-90..-80S) vertical lines were drawn
with projectionToY(±90). Web Mercator diverges there: osmdroid's
TileSystemWebMercator yields y01=±Infinity -> Long.MIN/MAX_VALUE pixel
coordinates (verified via JVM test: lat=90 -> -9.2e18, lat=-90 -> +9.2e18).
The canvas cannot rasterize such coordinates, so the polar rows' grid
lines silently disappeared at field zoom (2-char labels).

Fix: clamp the projected latitude to the Mercator limit ±85.0511 in
projectionToY(). The polar rows then project onto the screen edge and
their lines render (the sliver beyond 85.05 is off-screen anyway). Row A
kept its symmetric handling; row R now uses the same path.
2026-09-14 21:24:35 +08:00
atsunatsu b979524b37 chore: bump version to 4.4.7-ba7opf.1 (versionCode 471) 2026-09-14 20:45:38 +08:00
atsunatsu 9e748f5534 feat(map): worked-grid QSO counts and operated-grid selector
- QSO dialog: each callsign row shows 'n QSO'/'n QSOs' (plurals) centered
  between callsign and date, styled like the date.
- Grid mode + VUCC filter: top-left selector picks the operated grid the
  VUCC count is computed from (defaults to the most-worked grid), with a
  collapsible list of all operated grids; arrow style matches the QSO
  dialog rows.
2026-09-14 20:45:25 +08:00
atsunatsu 25b982ac59 feat(vucc): per operated-grid VUCC counting
GridQso gains myGrid (ADIF MY_GRIDSQUARE) captured per QSO during LoTW
parsing (handles alphabetical field order + EOR reset) and persisted as
JSON key 'mg' (absent in pre-myGrid data -> null). Legacy syncs keep the
global VUCC count; one resync unlocks per-grid counting.
2026-09-14 20:45:20 +08:00
atsunatsu 6a6399b606 fix(passes): recalculate passes when the UTC toggle changes
Port of upstream PR #252 (wty2019wty): combine otherSettings.stateOfUtc
into initRepository so an existing AOS time window is reapplied in the
newly selected timezone right after toggling UTC, instead of waiting for
the next selection/position change.
2026-09-14 18:54:22 +08:00
atsunatsu c1cc992abd chore: bump version to 4.4.7-ba7opf.0 2026-09-14 14:30:27 +08:00
atsunatsu 212dced3c4 feat(data): persist ndot and protect manually imported transceivers
- DB v2 migration: entries.ndot column so hasDecayed() actually works after
  reload (parsed ndot was dropped on write, making the decay detection dead
  code); radios.isCustom column to mark manually imported transceivers.
- updateTransceiversFromFile marks imported radios isCustom=true; remote
  updates now deleteManagedRadios() then insert the fresh snapshot, keeping
  manual imports (no source can refresh them, so nothing would bring them back).
- Wire MIGRATION_1_2 into MainContainer (fallbackToDestructiveMigration=false
  would otherwise crash on upgrade).
2026-09-14 14:30:23 +08:00
atsunatsu 7ef555e377 fix(time): respect UTC toggle for AOS filter window and sunrise/sunset day anchor
- PassesViewModel: use the DEVICE timezone for the natural-day anchor in
  findTodaySunRiseSet instead of the display timezone. With UTC enabled the
  anchor was UTC midnight = 08:00 local for UTC+8 stations, when the sun is
  already up — tripping the white-night guard and rendering '--:--'.
- SatelliteRepo.isAosInRange: follow the UTC display toggle so the pass time
  window is interpreted in UTC when UTC mode is on. Previously the list
  showed UTC times while the filter silently used local time, off by the
  timezone offset (8h for China).
2026-09-14 14:13:16 +08:00
atsunatsu 2773145e0c chore(upstream): v4.4.7 low-risk backports — night overlay alloc-free, radar inset padding, sunrise/sunset tests 2026-09-14 13:41:15 +08:00
atsunatsu d8d3d81648 perf(map): viewport culling, label limit, scroll refresh, SetVisible gating 2026-09-14 13:41:11 +08:00
atsunatsu 25a41def35 i18n: localize WAPC/WAJA map labels to English
- wapc.json: add name_en for all 34 provinces (Beijing/Guangdong/...)
- waja.json: add name_en for all 47 prefectures (Gifu/Kyoto/...)
- AwardBoundaryOverlay: render English label on non-Chinese UI,
  fall back to source name when name_en is absent
- bump version to 4.4.6-ba7opf.10.4 (469)
2026-09-13 01:25:36 +08:00
atsunatsu d9dc0a1735 docs: reword ADIF parsing description in README 2026-09-12 05:56:41 +08:00
atsunatsu 692e917893 docs: drop boundary-claim wording and Wavelog from README (LoTW sync only) 2026-09-12 05:54:03 +08:00
atsunatsu 6371bb114d docs: list major fork features in README — grid mode, award boundaries, LoTW/Wavelog sync, in-app updates, custom TLE sources 2026-09-12 03:05:47 +08:00
atsunatsu 2d76aeda23 chore: bump version to v4.4.6-ba7opf.10.3 2026-09-12 01:51:13 +08:00
atsunatsu ecc24fd836 fix(radar): CW decoder text hidden behind system nav bar on gesture-nav devices
The radar page is a full-screen destination; the transceiver list had no
bottom inset padding, so on gesture navigation the translucent system bar
floated over the CW decoder's decoded-text output at the end of the list.
Add navigationBars height as LazyColumn contentPadding (no-op on
three-button nav where the window is already resized).
2026-09-12 01:22:02 +08:00
atsunatsu 689f08881c fix(map): dxcc boundary aligns with Chinese official claim — Zangnan + Karakoram corridor
- Zangnan (Arunachal Pradesh): China ring south edge moved from the de-facto
  line to the official claim (DataV Tibet south arc); India ring's Arunachal
  protrusion removed so the region shows as China (both rings share the new
  boundary). Verified: Zangnan points in China ring only, Assam stays India.
- Karakoram corridor / Shaksgam valley (China-controlled since 1963): the
  coarse 13-pt west segment (Karakoram Pass -> Afghan/Tajik corner) replaced
  with the official Xinjiang south arc (232 pts) — the valley is now inside
  the China ring. Aksai Chin was already correct; no other entity deviates
  from the official map (SCS islands follow their own DXCC entities).
2026-09-12 01:08:43 +08:00
atsunatsu d467200a45 style(map): roamed stripes — 44f spacing, 14f width, alpha 90 (match green fill)
User picked the spacing-44/width-14 GridMaster-style zebra from the preview
grid; opacity lowered to the same alpha as the worked green fill (90).
2026-09-12 00:32:59 +08:00
atsunatsu 6277e36a96 feat(map): own 4-char grid bold outline also at field zoom; bump v4.4.6-ba7opf.10.2
Field zoom (2-char labels) now draws the station's own 2°x1° square outline
on top of the field grid, so the operator always sees their exact grid.
Polar labels + order-independent LoTW parsing fixes ride along in this release.
2026-09-11 23:58:57 +08:00
atsunatsu 08a5c8c460 fix(lotw): order-independent ADIF parsing — roamed/work grids dropped by sequential PROP_MODE gate
parseConfirmedGrids and parseRoamedGrids gated grid fields on propMode == SAT
at the moment the field line was read. LoTW emits ADIF fields alphabetically
(GRIDSQUARE/MY_GRIDSQUARE before PROP_MODE), so the gate was never satisfied
on real reports: the worked set AND the roamed set silently came back empty.
Both parsers now buffer the record and decide at <EOR>, matching the proven
parseConfirmedGridQsos pattern. Regression tests cover the alphabetical order.
2026-09-11 23:47:08 +08:00
atsunatsu 1ffb5ed9e8 fix(map): polar 2-char labels visible + own-grid bold only at sub-square zoom
- Polar bands (80..90 / -90..-80) extend past the map's latitude limit, so
  their label center fell off-screen and the labels never rendered. Labels are
  now centered in the VISIBLE part of the cell; fully off-screen cells are
  still skipped. Verified with a projection-accurate simulation render: Arctic
  band shows IR/JR, Antarctic band shows IA/JA, ±80 lines intact.
- Own-grid bold outline is drawn only at sub-square zoom (zoom >= 6): at field
  zoom (2-char labels) the current grid is no longer marked (user request).
  The ownGrid-set-every-frame fix stays, so the outline still survives map
  refreshes at sub-square zoom.
2026-09-11 23:12:33 +08:00
atsunatsu cc684b6e3a fix(map): own-grid outline in grid-line color (bold only); passes top bar aligned to upstream; v10.1
- Bold outline now uses the same yellow as the regular grid lines
  (argb(160,255,224,130)) at 6f stroke — no color change, just thicker.
- Passes top bar matches upstream exactly: left = radios dialog
  (ic_radios -> ToggleRadiosDialog), right = passes filter dialog
  (ic_filter -> TogglePassesDialog). The calendar button stays only on the
  radar page (upstream behavior); removes the now-unreachable
  PassesAction.AddToCalendar wiring.
- Version bumped to 4.4.6-ba7opf.10.1.
2026-09-11 22:48:53 +08:00