Release: dev → main up to 222a300 (iOS donation-link App Store fix)#27
Merged
Conversation
…rent overlay refreshes and syncs, improving performance and stability during rapid state changes.
Syncing Dev to MapLibre_GL
format using `dart format .` manual add missing curly braces in control flow statements with dartls adds check in ci to confirm formatting and remove tests and there are none # Conflicts: # lib/models/user_preferences.dart # lib/widgets/map_widget.dart
clean up small issues
comment out unused _blankStyleJson URL
offline maps
…safeguards - Add "Cancel" button to in-progress download card; discards partial tiles and frees the slot for queued downloads - Queue multiple offline map downloads; starting a second download while one is running enqueues it, and the next begins automatically when the active download finishes or is cancelled. Progress card shows pending count - Check device free space before starting; reject downloads up front if less than 1.5x the estimated region size is available - Label storage usage on Offline Maps screen as an estimate (derived from tile-count heuristics) rather than presenting it as an exact figure - Surface real error causes from the native layer (network, style, IO) instead of the generic "Download error occurred" message - Cap offline zoom slider at z15 (was z18) and set default max zoom to z14; OpenFreeMap vector tiles max out at z14, so higher zooms produced duplicate overzoom tiles and could accidentally explode storage usage - Reject offline regions that cross the antimeridian with a clear message instead of silently producing a 357-degree-wide region - Fix race where the offline download dialog could close before validation errors were shown to the user
- Bump version to 1.3.0 in .build_version. - Modify Build.sh to include export-options-plist for iOS IPA builds. - Enhance MainActivity.kt to support fetching per-region downloaded byte counts. - Update AppDelegate.swift to handle region sizes and improve cache management. - Refactor offline_maps_screen.dart to refresh region sizes and update UI accordingly. - Adjust settings_screen.dart to reflect changes in terminology from "regions" to "areas". - Improve offline_map_service.dart to track actual bytes used by downloaded areas. - Add tile_cache_service.dart method to retrieve per-region sizes from the native SDK. - Modify map_widget.dart to manage coverage overlay based on tile availability. - Create ExportOptions.plist for iOS export settings.
…F). When OFF, Send Ping, Active, and Hybrid controls are hidden across all three ping-control variants (standard, compact, landscape). Passive and Trace stay visible. - MeshMapper now parses `flood_disabled` from the auth response and auto-syncs the toggle on connect and zone change. If the region permits flooding, the toggle flips ON. If the region forbids it, the toggle is forced OFF, greyed out, with an amber "Set by Regional Admin" subtitle. - One-shot popup when a user-enabled flood toggle gets overridden by regional policy on auth or zone change, explaining that Active and Hybrid modes have been disabled for this session.
… startup theme loader was treating a non-fatal open timeout as corruption and deleting the entire user_preferences box, taking every saved setting with it. The early-startup deletion is gone — real corruption is still caught later by AppStateProvider._attemptHiveRecovery with a user-visible error. - Force a box.flush() after every preference write (general preferences, device antenna preferences, device power overrides, audio settings) so settings are persisted to disk immediately instead of relying on Hive's lazy flush — prevents loss on crash or abrupt termination.
…s later by a MapLibre camera animation error. Camera animations now suspend while the app is in the background.
…e (or near-same) GPS coordinates now collapse into a cluster bubble with a count instead of rendering as an overlapping pile with illegible labels. Clustering runs at every zoom level (was previously dropped past zoom 14), so a stack stays as a single tappable target even fully zoomed in. - **Spiderfy on Tap:** Tapping a cluster at max zoom fans the members out in a ring (or spiral past 20 markers) connected by thin leader lines, each independently tappable and opening the repeater detail sheet. Below max zoom, taps zoom in further first so you can separate the stack visually. - Spider auto-collapses when you tap an empty area, tap a different cluster, or zoom past a 0.25-level delta. Panning keeps the spread open since leader lines live in geographic coordinates and follow the map naturally. On iOS, spread markers covered by the GPS dot are now tappable as well.
…reen entirely in heading mode) when the bottom controls panel is expanded with auto-follow enabled. The centring offset was using a 256px tile assumption while MapLibre projects onto a 512px tile grid, so every padding-aware shift was 2x too large.
…the packet travelled (origin → ... → us) as a tappable chain of repeater chips, with the receiving repeater tagged "(heard)". Each hop chip opens the standard repeater info popup so you can resolve names and distances along the path without leaving the sheet. The same chain renders on every RX entry in the Log tab. If your own carpeater was on the tail, it's stripped from the chain just like it's stripped from the displayed last-hop ID. CSV export gains a `path_hops` column on RX rows. Path data is transient and lost on app restart, matching how TX heard-repeater data already works.
…apping a bubble at max zoom. The fan now matches the count shown on the bubble exactly.
…ys are now hidden from the map. They don't render as markers, don't contribute to cluster bubble counts, and don't get pulled into spider expansions. The repeater picker (trace mode) and Log tab still list every repeater the server returns, so nothing is lost from the data view, it's a map-only declutter.
- Auto-follow zoom is now one-shot: zooms to street level once, then only pans. Pinch-zooming while following no longer fights you. - Reduced per-GPS-tick map work for smoother performance Bug Fixes - Coverage overlay no longer flashes on tile refresh - Repeaters without GPS now show a location-off icon instead of dragging focus mode to (0, 0) - Fixed map labels flickering at max zoom during follow mode
…is no longer hardcoded. The app now reads `stale_repeater_hours` from the zone status API response so that it matches the web client.
…X window, discovery window) no longer trigger a full widget-tree rebuild on every 500ms tick. They now self-notify via `ChangeNotifier` so only the ping control buttons redraw. During auto-ping this eliminates 4-6 unnecessary rebuilds per second across 11+ widgets. Noise floor and battery streams also skip redundant notifications when values haven't changed, removing ~840 unnecessary full-tree rebuilds per hour during stable conditions.
setMapStyle and setColorVisionType called plain notifyListeners(), which does not bump mapRevision — so the MapWidget (isolated behind the mapRevision Selector, Critical Rule 9) never rebuilt and the new style/palette never reached the native map. Switch both to _notifyMapNow() so the change actually applies.
…h event) The map re-pushed every accumulated TX/RX/DISC/trace marker to the native layer on every ping event — hundreds of awaited platform round-trips per event late in a session — so marker/ping display lag grew with session length and the coverage tile often repainted before its ping marker showed. Markers now skip the native update when their icon/size is unchanged, bounding per-event work to actually-changed pins. Adds a [MAP] Coverage sync diag line.
…e server to accept the upload session before sending, and never discards pings it couldn't upload (they're kept and retried instead of being lost as a "partial upload").
…at record time for parity with live uploads — model was previously sent as the generic 'Offline Upload' and power/version were derived at upload time instead of when the pings were recorded. Snapshotted fields are sent on upload auth with fallbacks for legacy sessions.
…coordinates All 10 TestFlight crash reports share one root cause: an invalid GPS or stored coordinate (NaN / infinite / out-of-range) reached MapLibre's camera, whose native LatLng constructor throws std::domain_error — uncaught across the C++/Obj-C boundary, so it aborts the app (SIGABRT). iOS can briefly report an invalid CLLocation right after the app resumes from background (the "brought it back up and it crashed" reports), and a corrupted/stale lastKnownPosition from Hive explains the "instacrash on launch" reports. The prior _cameraAnimationReady one-frame delay only addressed GL-surface timing, not the invalid input. Two layers of defense: - Layer A (crash-stopper): validate every coordinate before it reaches the map camera. New shared isValidLatLng() guards the animate helpers, the three center builders feeding initialCameraPosition + style-load zoom, the focus/history fit-bounds (invalid points filtered), and the cluster zoom-ins in map_widget, plus the offline_maps_screen center. - Layer B (stop the bad data at the source): drop invalid fixes at the GPS stream, simulator, getCurrentPosition and getFreshPosition; ignore invalid stored last-known-position on load and never persist invalid coords. This also keeps invalid coordinates out of the API upload payloads. Adds lib/utils/geo_validation.dart + unit test. flutter analyze clean; full suite green (72 tests).
…kdrop (web parity) Replaces the cyan outline box around a tapped cell's 3x3 block with the web front end's spot-click look (highlightSpotCoverage): the block fills as a grid of cells in one dominant colour and the coverage backdrop dims to 0.15 so the footprint pops, restored when the summary sheet closes (honouring ping-focus). The dominant colour is the highest-priority status (green > cyan > orange > purple > grey > red) among only the pings whose blob covers the tapped cell, so a red-dominant block repaints green-looking neighbours that were coloured by a ping outside the blob (the intentional smear). Applies in both grid modes (3x3 Detailed, single cell Simplified). Highlight + dim appear with the fetched pings, driven off the same fetch the summary sheet already uses (no extra network call).
…ze button Both tap popups now match the ping-focus sheets: a transparent modal barrier so the map and markers stay fully bright (the tile footprint's 3x3 highlight + 0.15 coverage dim still pop), plus a minimize button that collapses the sheet to a tappable bottom pill, leaving the map interactive. Adds a generic _MinimizedInfoPopup + pill (kept separate from focus state), extracts _presentCellSummarySheet for reuse on reshow, and threads an optional onMinimize into CellSummarySheet. Opening a new popup supersedes a minimized one (a minimized cell's footprint is cleaned up when superseded by a repeater; cell-to-cell keeps the old footprint until the new data lands, so no flash).
The minimize pill sat underneath the ping control panel. Mirror focus mode's mechanism: add an AppStateProvider.infoPopupMinimized flag and OR it into the same control-panel visibility conditions + map bottom-padding calc in home_screen that isFocusModeActive/viewingHistorySession already use. The pill set/clear is centralized in _setMinimizedInfoPopup/_clearMinimizedInfoPopup so the flag stays in lockstep across every show/reshow/close/supersede path.
…ered bottom bar - audio_service: route TX/RX/alert blips to the Android media stream (usage: media) so they track the media volume slider instead of the ringer/notification stream (#88). - home_screen: add a minimize button to the landscape control panel; the collapsed state shows a compact bar centered along the bottom of the map (#329).
…ormat that hid pings from coverage In broadcast-coords mode the app sent on-air "MM:<tag>:<lat>,<lon>" while still posting the bare wire_tag to /wardrive. The server's observer truncates the on-air body to 16 chars and exact-matches it as the wire tag, so it never joined the app's coverage row — the TX stayed at DEAD and never reached the coverage view (visible only in the analyzer). RX and privacy mode were unaffected. Broadcast-coords now uses the plain coords path: "MM:<lat>,<lon>" on air, no wire_tag/ping_counter to the API (coords still travel via API lat/lon), matching the server's documented contract. No ping counter is consumed in this mode.
…on-air format" Keep the combined on-air format MM:<tag>:<lat>,<lon> for broadcast-coords mode — the keyed region-tag is intentional so cross-region spillover can still be attributed even when a user broadcasts coordinates. The coverage-join breakage is being fixed server-side (api.php normalizes the on-air body back to the bare tag before the join + spillover decode) instead of removing the tag client-side. This reverts commit be1bfea.
Tapping coverage data now draws connection lines from the points the tap flow already fetches (no new API calls), matching the web client's matching/fan-out: - Tap a coverage tile: fan out blue dashed lines from the cell centre to every unique repeater that heard its pings (with distance labels), hiding the rest. - Tap a repeater: draw its coverage cells (status/tile colours) + status-coloured lines to each cell centre, dim the base tiles, and hide the other repeaters. Cap 5000 cells (farthest-first) so the full footprint draws. - Repeater pill: status shown as a coloured tower icon (green online / grey offline) so the stats row fits on one line. New helpers in coverage_summary.dart (fromCoverageWithPoints, repeaterCoverageCells, heardEndpointsForCell) reuse the existing web-parity matching logic.
- Fix dead taps at z>=15 on freshly-pinged cells: _handleFeatureTap now treats the session patch layer (and the footprint highlight / repeater coverage cells) as a coverage-cell tap, not just the base overlay. Fallback hit-test queries the patch layer too, with a small rect so tiny cells stay tappable. - Ignore cell taps below zoom 12 (cells are sub-pixel when zoomed out, so taps are almost always accidental). - Repeater pill: online/offline shown as a coloured tower icon in the title so the stats row fits one line. - Repeater coverage cap raised 250 -> 5000 so the full footprint draws (web parity).
- Shared focus-camera lifecycle (_enterFocusCamera/_exitFocusCamera/ _fitCameraToPoints/_exitFocusCameraIfDone) so ping focus, tile (Feature A) and repeater (Feature B) coverage views share one save/restore of the user's camera, follow and rotation. - Spider spread-marker tap now collapses the spider and focuses that repeater (focus mode + its coverage cells), matching the GPS fall-through path (was: minimized pill only, no coverage). - Detailed Grid Mode: tapping a stacked pile spiders out at any zoom (un-clustered, no cluster-bubble zoom-in path); spider markers render baked chips so the hex label shows instead of an empty box.
The server now routes each offline ping to the region it occurred in and returns
placement_counts + too_far_region per batch. Parse and accumulate these across
all batches of an upload, store them on the uploaded OfflineSession, and show a
summary ("Uploaded - DSA 88 - EMA 157 - too far 3") that is tappable for a
per-region breakdown dialog.
Add a defensive guard in WireTagCodec.encode() that rejects offline-/non-region
session ids (offline is passive-only and is never wire-tag encoded; this also
prevents a RangeError on a malformed id). Session id stays opaque otherwise, so
the upload loop is unchanged and older behaviour is preserved.
…pLibre a non-finite zoom The camera fit-to-bounds paths could hand MapLibre a zero-area box (coincident points) or edge padding larger than the visible map (the tall coverage/repeater bottom sheet), both of which make its projection compute a non-finite zoom and abort the app via an uncaught native LatLng throw (SIGABRT) — the same crash signature as an invalid coordinate. All newLatLngBounds fits now route through a guarded _animateFitBounds helper that centers degenerate boxes (isDegenerateBounds) and clamps padding to the live map size (clampFitPadding). The earlier lat/lon-only guard (813273d) never covered zoom/bounds/padding, so the same crash survived in the coverage focus-camera feature. Adds unit tests for the new helpers.
On a fresh /auth, continue the wire-tag ping counter from the server's resume_counter (sent when the session was reused after a force-close + reconnect) instead of resetting to 0, which re-minted duplicate wire-tags the subscriber + coverage dedup silently dropped. Absent/0 -> reset as before.
MapLibre's transform unprojects against the live viewport. On a launch where the GL surface is degenerate/zero-sized (tiles never render a real frame), the first animated flyTo/setCamera makes unproject return NaN and mbgl::LatLng throws an uncaught C++ std::domain_error -> SIGABRT. That throw crosses the native->Dart boundary and cannot be caught from Dart, so the fix has to live inside the plugin. - Vendor maplibre_gl 0.25.0 under third_party/maplibre_gl, consumed via pubspec.yaml dependency_overrides (no longer from pub.dev). - Native MESHMAPPER GUARD in the camera handlers of MapLibreMapController (.swift / .java): bail out (still completing the method-channel result) when the map view has no usable size. - Dart defense-in-depth: _mapHasRenderedOnce (set on first onMapIdle) is folded into _canAnimateCamera, so no programmatic camera move is even attempted until the map has rendered once; the one-shot initial GPS zoom re-attempts on later ticks instead of burning. - analysis_options.yaml excludes third_party/** from our lints. - DEVELOPMENT.md documents the vendoring and the re-apply-on-upgrade step.
When many RX pins arrived within a few seconds, newer pins could render underneath older ones. The recency sort key was seconds-since-2025-01-01, now ~46M, which exceeds the native float32 symbol-sort-key's exact-integer ceiling (2^24). Float32 quantizes that range to multiples of 4, so any pings within ~4s collided on one sort key and MapLibre ordered the ties arbitrarily. (Even as a true int, .inSeconds tied pings in the same second.) Replace the timestamp-derived zIndex with a monotonic counter assigned once per coverage symbol key (_coverageZIndex / _coverageZCounter): - Stays well under 2^24, so float32 stays exact. - Unique per key, so no ties. - Assign-once, so existing symbols never need re-pushing (the incremental sync skip path is untouched). - Newest key gets the highest value -> always on top and wins the topmost-first tap hit-test. An SNR-updated RX changes its key (new ts) -> fresh top counter, correctly redrawn on top. Drop the counter entry in the symbol cleanup loop (prevents an unbounded leak) and clear the map on style reload (the SymbolManager is rebuilt empty there; keep the counter climbing).
- Add OfflineUploadResult.networkError; a null /auth (timeout) is now a retryable network error, not a misleading auth rejection - Retry /auth on timeout (2s/4s backoff) before giving up, mirroring the batch-upload retry - Success snackbar reads 'Upload Success'; network errors show 'Network error - tap again to retry'. The 'advert your device' message is reserved for genuine auth rejections.
- History map popups: pass pathHops through when reconstructing a TxPing from stored markers so multi-hop repeats render distinctly instead of all appearing as direct repeats - Map: keep the GPS puck above all coverage pings via a fixed z-index - ping_controls: landscape button keeps its color during cooldown - app_state_provider: ping/auto-mode adjustments and added debug logging
…iles) The wire-tag packed only (region, NNNN, counter) with no date. NNNN recycles daily, so a tag re-minted on a later day was byte-identical to the prior day's and the server's idempotency skip dropped the new ping -> DEAD tiles when driving new areas. Encode the session date too: region(15) | date(15: year-2020/month/day) | NNNN(14) | counter(11) = 55 bits -> "MM:"+10 chars. decode() now returns the date and reconstructs the full session_id. Web-safe on dart2js (value > 2^53) by working on two <=28-bit halves with arithmetic only; verified byte-identical to the PHP twin and under flutter test --platform chrome. Canonical vectors regenerated. Also: [COVERAGE] Patched log now includes the cell status histogram, and the API queue logs wire_tag + ping_counter on upload, so collisions self-document.
Wire the Active/Hybrid and Passive buttons (portrait, compact, and landscape layouts) to the new `isAutoPingStarting` flag so they disable the instant a mode-start is tapped, instead of looking dead during the awaited session-check round-trip. Send Ping already disables via `isPingSending`. Replace each control widget's top-level `context.watch` with `context.select(_controlsDepsOf)` + `context.read`, so the controls rebuild only when a control-relevant field changes — not on every GPS / noise-floor / battery notify (~1-2 Hz during wardriving). Countdown values still update via the inner ListenableBuilder(timerListenable). The dependency records cover every appState.* read in the build bodies (incl. isConnected / targetRepeaterId / repeaters for the Trace section) so no button goes stale while idle. Companion to the sendPing()/toggleAutoPing() instant-feedback + re-entrancy guards already landed in app_state_provider.dart.
Bug 1 (GPS puck blink, regression from 764f0d4): the puck shared one MapLibre symbol layer with every coverage pin. Adding a pin rewrote and re-laid-out the whole layer, so for ~1 frame the new pin painted over the puck before the sort-key reordered it on top. New since 764f0d4 because the puck previously sat UNDER the pins. Move the puck into its own dedicated gps-puck-source + gps-puck-layer, installed topmost (no belowLayerId), mirroring _ensureCoverageLinesLayer. Always-on-top is now guaranteed by layer order, not sort-key contention, and a pin can never paint in the puck's isolated layer -> blink gone by construction. enableInteraction:false so taps fall through to repeaters. Re-installs on style reload. Removes the old _gpsZIndex sort-key + _gpsSymbol annotation approach. Bug 2 (intermittent "Send Ping locks out Hybrid/Passive", force-close to clear): NOT a regression from recent commits — the new _isPingSending / _autoPingStarting flags both clear in finally and the disable/RX-window lifecycle was untouched. Latent issue: CountdownTimerService.isRunning is _timer != null with no wall-clock check, so if the 500ms _update() stops firing (iOS suspends timers while backgrounded/driving) the timer never self-cancels; a stuck rxWindowActive then disables Send Ping itself, so the user can't ping to reset it. Add diagnostic logging only (_logStuckTimers): warns [TIMER] <name> isRunning past its deadline when a countdown timer reads isRunning && remainingMs == 0. Runs on app-resume and throttled-5s off the GPS notify (no new timer). Real one-line fix deferred until a debug log confirms the trigger.
External 'Buy us a coffee' (Buy Me a Coffee) link triggered Apple rejection under 3.1.1. Gate the About-section tile behind a platform check so it's omitted on iOS while remaining on Android/Web, where external donation links are permitted.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Merges
devintomainup to commit222a300("Hide donation link on iOS to satisfy App Store guideline 3.1.1").Range:
main(582f175) →release/222a300— 104 commits.mainis the merge-base, so this is a clean fast-forwardable integration with no divergence.Deliberately excluded (the two
devcommits newer than222a300, held back for a later cut):eb74c0eFix landscape nav/connect layout, 3-byte ID clipping, hybrid bubble, boot notification5e36b0fFix repeater search relevance and surface RX-no-GPS silenceHighlights
Location privacy / TX wire-tag
/authon session reuse.New transports
Maps & coverage
maplibre_gl0.25.0 with a native camera-viewport crash guard (SIGABRT on degenerate viewport).Performance / wardrive overheat
notifyListeners()or relayouts on every GPS tick (mapRevisionisolation architecture).iOS / Android stability
std::domain_error).Offline sessions
Other notable
Verification
Run
flutter analyzeand the test suite onrelease/222a300before merge (seetest/services/wire_tag_codec_test.dartfor the wire-tag canonical vectors).