feat(android,pwa): native Android build + offline caching + region download#377
Conversation
✅ Deploy Preview for geolibre-app ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThis PR introduces an offline map tile pre-download feature via a new ChangesOffline Map Region Download
Android Port and Mobile Polish
Sequence Diagram(s)sequenceDiagram
rect rgba(100, 149, 237, 0.5)
note over User,ServiceWorker: Offline Region Download Flow
end
participant User
participant OfflineRegionDialog
participant offline_tiles as offline-tiles
participant MapLibreStyle as MapLibre Style
participant TileJSON
participant ServiceWorker
User->>OfflineRegionDialog: click "Download Offline Region"
OfflineRegionDialog->>offline_tiles: collectOfflineUrls(map, bbox, minZoom, maxZoom)
offline_tiles->>MapLibreStyle: read sources (vector/raster), sprites, glyphs
offline_tiles->>TileJSON: fetch TileJSON (if tiles[] missing)
TileJSON-->>offline_tiles: tile URL templates
offline_tiles-->>OfflineRegionDialog: deduplicated absolute URL[]
OfflineRegionDialog->>offline_tiles: warmUrls(urls, { signal, onProgress })
loop bounded concurrency workers
offline_tiles->>ServiceWorker: fetch(url) → cache response
offline_tiles-->>OfflineRegionDialog: onProgress({ done, total, failed })
OfflineRegionDialog-->>User: update progress bar
end
offline_tiles-->>OfflineRegionDialog: WarmProgress final
OfflineRegionDialog-->>User: show completion status
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/android.yml:
- Around line 54-56: The `android-actions/setup-android@v3` action on line 56
uses a mutable version tag which weakens supply-chain integrity. Replace the
`@v3` tag reference with a pinned commit SHA (for example, `@abc123def...`) to
ensure the exact version of this third-party action is locked and cannot change
unexpectedly. Check the action's repository or releases page to identify the
appropriate commit SHA for the desired version.
In `@apps/geolibre-desktop/src/components/layout/OfflineRegionDialog.tsx`:
- Around line 86-104: The uncacheableHosts useMemo hook currently checks
spec.tiles?.[0] ?? spec.url for host detection, but this approach misses actual
tile hosts when sources use TileJSON indirection (spec.url pointing to a
TileJSON endpoint). For sources lacking a tiles array but having a url field,
fetch and parse the TileJSON file to extract the real tile templates from the
resolved TileJSON's tiles array, then validate those hosts against
CACHED_TILE_HOST instead of just checking the TileJSON endpoint's host. This
ensures all potential tile hosts are properly detected and validated for the
offline warning.
In `@apps/geolibre-desktop/src/lib/offline-tiles.ts`:
- Around line 177-204: The fetch call in the collectOfflineUrls function
performs a network request without an AbortSignal, making it impossible to
cancel the operation if the TileJSON endpoint is slow or unreachable. Add an
optional options parameter to the collectOfflineUrls function signature to
accept an AbortSignal, then pass this signal to the fetch() call within the try
block so that cancellations can properly abort the TileJSON discovery. Update
the call site to pass the controller.signal through the new options parameter.
- Around line 281-307: The concurrency parameter is not validated before being
used to determine the number of workers in the Array.from call. If concurrency
is 0 or negative, Math.min(concurrency, urls.length) will result in zero workers
being created, causing the warming function to silently return without
processing any URLs. Add a guard to clamp concurrency to a minimum of 1 before
calculating the array length. This ensures at least one worker is always spawned
to process the URLs, even if an invalid concurrency value is passed.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 6a98731b-b0af-413c-957e-2215622a7d03
📒 Files selected for processing (10)
.github/workflows/android.ymlapps/geolibre-desktop/src-tauri/src/lib.rsapps/geolibre-desktop/src/components/layout/OfflineRegionDialog.tsxapps/geolibre-desktop/src/components/layout/TopToolbar.tsxapps/geolibre-desktop/src/components/layout/toolbar/ProjectMenu.tsxapps/geolibre-desktop/src/i18n/locales/en.jsonapps/geolibre-desktop/src/lib/offline-tiles.tsapps/geolibre-desktop/vite.config.tsdocs/android-port-spike.mdtests/offline-tiles.test.ts
Code reviewReviewed: Security
Bugs
Performance
Quality / Nits
CLAUDE.mdNo violations. i18n keys added to Overall: The Android cfg-gate is a clean, minimal fix. The PWA offline feature is well-designed (indirect-fetch strategy, bounded concurrency, size estimate preview, good tile-math test coverage). Primary items to address before merging: pin the third-party CI action, fix the two |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/geolibre-desktop/src/components/layout/DesktopShell.tsx`:
- Around line 922-925: The onPointerUp handler in startLayerPanelResize and
startStylePanelResize only detaches event listeners when a pointer up event
occurs. If a pointercancel event fires instead or the component unmounts during
a resize, the global event listeners remain attached and document.body styles
stay locked with cursor and userSelect values. Add a pointercancel event
listener that performs the same cleanup as onPointerUp, and ensure proper
cleanup when the component unmounts. Apply this same pattern to both
startLayerPanelResize and startStylePanelResize functions to handle all teardown
scenarios safely.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 216c0216-ee28-43fa-8362-cc919ea52057
📒 Files selected for processing (4)
apps/geolibre-desktop/src/components/layout/DesktopShell.tsxapps/geolibre-desktop/src/components/panels/LayerPanel.tsxapps/geolibre-desktop/src/components/panels/StylePanel.tsxapps/geolibre-desktop/src/hooks/useIsMobileViewport.ts
Code reviewReviewed: Android CI workflow, Rust Bugs
Security
Performance / Storage
Quality
What looks good
|
Code reviewReviewed: Android/PWA support, mobile feature-gating, pointer-event resize migration, offline tile warming, and CI workflow. Bugs
Security
Quality
What looks good
|
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/geolibre-desktop/src/lib/is-mobile.ts`:
- Around line 18-26: The MOBILE_UA_PATTERN regex in is-mobile.ts misses iPadOS
devices that report desktop-style user agents (containing "Macintosh" with
"Mobile/" string). Update the MOBILE_UA_PATTERN constant to also match these
desktop-class iPad user agent strings in addition to the existing Android,
iPhone, iPad, and iPod patterns. Then add a regression test case in
tests/is-mobile.test.ts that verifies isMobile() correctly returns true for a
desktop-class iPad user agent string (one containing "Macintosh" and "Mobile/").
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 0b20cd3e-baea-4faa-ab65-c6462c0c139d
📒 Files selected for processing (4)
apps/geolibre-desktop/src/components/layout/toolbar/AddDataMenu.tsxapps/geolibre-desktop/src/components/layout/toolbar/ProcessingMenu.tsxapps/geolibre-desktop/src/lib/is-mobile.tstests/is-mobile.test.ts
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/android.yml:
- Around line 118-124: Add validation checks in the branch where KEYSTORE_BASE64
is checked to ensure that KEYSTORE_PASSWORD and KEY_ALIAS environment variables
are also provided. Add explicit precheck conditions after verifying
KEYSTORE_BASE64 is set, and emit a clear CI error message (using echo with exit
status) if either KEYSTORE_PASSWORD or KEY_ALIAS is missing. This allows the
workflow to fail fast with a clear error message before attempting to use
incomplete keystore configuration in apksigner.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 3bc9d6db-8cd6-40c8-9806-b7110aca1c1f
📒 Files selected for processing (2)
.github/workflows/android.ymlapps/geolibre-desktop/src-tauri/tauri.android.conf.json
Override productName to "GeoLibre" in tauri.android.conf.json so the Android launcher label and activity title read "GeoLibre". The desktop build keeps "GeoLibre Desktop" (base tauri.conf.json). Verified a clean `tauri android init` generates app_name=GeoLibre in strings.xml.
The foreground filled the full 108dp canvas, so the launcher's adaptive mask cropped the globe's edges. Scale the foreground to ~64% centered (all densities) so the whole icon stays within the ~66dp safe zone and is never clipped.
628b314 to
e0b7347
Compare
Code reviewReviewed: CI workflow, Rust mobile gating, offline tile download, responsive layout, Security
Bugs
Performance
Quality
CLAUDE.mdNo violations found. All new user-facing strings go through |
- README: native Android app + Download Offline Area + engine offline caching in the v1.3 feature list, an Android build/run section, and a Reference link. - New docs/android.md: toolchain setup, build, signing, sideload, and emulator guide (added to mkdocs nav). - roadmap: a "Mobile, offline, and Android" subsection under v1.3. - index: mention Android/mobile/offline in the v1.3 status summary. Also: build the Android APK only on published releases (and on demand), not on every PR — the Rust cross-compile + Gradle build is slow.
On Android the dropdown max-height used 100vh / Radix's available-height, both based on the layout viewport, which extends under the system status/navigation bars. Long menus (e.g. Add Data) then ran their last items under the nav bar where they were clipped and unreachable. Cap the max-height with dvh minus the top/bottom safe-area insets so the menu (and its scroll area) stays within the visible region and scrolls to the end. Desktop/web are unchanged (dvh == vh and insets are 0).
| // would otherwise evict its own freshly-cached tiles past 600. | ||
| cacheName: "geolibre-basemaps", | ||
| expiration: { maxEntries: 600, maxAgeSeconds: 60 * 60 * 24 * 7 }, | ||
| expiration: { maxEntries: 8000, maxAgeSeconds: 60 * 60 * 24 * 30 }, |
There was a problem hiding this comment.
Design concern: 8 000-entry / 30-day cap applies to every PWA user, not just offline-download users
The previous limits (600 entries, 7 days) were reasonable for the "tiles you happen to scroll past" ambient caching use case. The new limits are sized for the "Download Offline Area" download — but they apply to all geolibre-basemaps traffic, meaning every PWA user now gets up to 8 000 tiles and 30-day retention whether or not they ever use the offline download feature.
Two risks:
- Self-eviction: A moderate offline area at zoom 15–17 easily exceeds 8 000 tiles. Workbox's LRU eviction will start purging the oldest entries — potentially the tiles that were just downloaded — long before the area is fully cached.
- Storage pressure: 8 000 × ~30 KB average ≈ 240 MB of IndexedDB for every PWA install, on top of the CDN-engines cache.
Consider giving the offline-download tiles their own Workbox cache bucket (e.g. geolibre-offline-region) with a higher cap, while keeping the ambient basemap cache smaller. The warmUrls fetch could send a custom header or use a dedicated fetch() path that the SW matches to the offline bucket.
There was a problem hiding this comment.
Good point — leaving this open for @giswqs to decide. Note the cap is a ceiling, not a preallocation: CacheFirst only stores tiles actually fetched, so ambient PWA users who scroll a little still cache only a few tiles (not 240 MB). The self-eviction risk for large downloads is now surfaced by the new offline.tooManyTiles warning. A dedicated geolibre-offline-region Workbox bucket (separate cap) is a reasonable follow-up if you'd like ambient vs offline-download caches sized independently.
Code reviewReviewed the full diff (~1 400 additions): Android CI workflow, Rust Bugs
Performance / Storage
Quality / CLAUDE.md
What I checked and found clean
|
offline-tiles.ts:
- collectOfflineUrls: accept an AbortSignal (Cancel now interrupts TileJSON
discovery), thread source `subdomains` into expandTileUrl, and handle the
MapLibre array `sprite` format so multi-sprite styles cache their icons.
- expandTileUrl: drop `{s}` together with its trailing dot when no subdomains,
avoiding invalid `https://.host/...` URLs.
- Glyphs: stop encodeURIComponent-ing the fontstack so the warmed URL matches
MapLibre's actual (browser-normalized) glyph request and the SW cache key hits.
- countTiles/enumerateTiles: split antimeridian-crossing bboxes (west > east).
- warmUrls: clamp concurrency to >=1, and don't increment `done` on aborted
fetches (finally ran after the catch `return`).
OfflineRegionDialog.tsx: catch collectOfflineUrls errors (no false "done"),
abort the in-flight download when the dialog closes, only clear abortRef if it
still points to this run, disable Download when no service worker is active, and
warn when the area exceeds the SW cache cap.
DesktopShell.tsx: panel resize handles now setPointerCapture, listen for
pointercancel, and tear down on unmount, so a canceled/interrupted drag can't
leave listeners attached or document.body styles stuck.
is-mobile.ts: detect iPadOS 13+ desktop-UA via maxTouchPoints (+ tests).
AddDataMenu/ProcessingMenu: memoize the isMobile() check (stable per session).
android.yml: pin android-actions/setup-android to a commit SHA, rename the job
to "(release)", fail fast when keystore secrets are partially set, and pass
apksigner passwords via files instead of CLI args.
| try { | ||
| // CacheFirst means already-cached URLs return instantly; new ones hit | ||
| // the network and are stored by the SW. We discard the body. | ||
| const res = await fetch(url, { signal, cache: "no-cache" }); |
There was a problem hiding this comment.
Performance – cache: "no-cache" works against the HTTP cache without benefit
The cache: "no-cache" option affects only the browser's HTTP cache, not the Service Worker's Cache Storage. The SW's CacheFirst handler intercepts the request first; when the SW cache hits, the response is returned immediately regardless of this flag. When the SW cache misses, the request is forwarded to the network carrying Cache-Control: no-cache, which tells the tile server to revalidate even for version-pinned, effectively-immutable tile URLs — wasting one RTT per cache miss.
For a large offline pre-fetch where thousands of tiles need to be fetched fresh, this is a meaningful overhead. Dropping the cache: option (or using "default") lets the browser's HTTP cache serve tiles that were recently loaded by the map viewer, while still letting the SW store the response for offline use.
| const res = await fetch(url, { signal, cache: "no-cache" }); | |
| const res = await fetch(url, { signal }); |
Confidence: medium — the scenario only matters when the user is downloading a region whose tiles are partly already in the HTTP cache (i.e., recently panned through). The correctness of SW caching is unaffected.
| } | ||
| if (!templates || templates.length === 0) continue; | ||
|
|
||
| const template = templates[0]; |
There was a problem hiding this comment.
Quality – only the first tile URL template is used for warming
templates[0] concentrates every warming request to a single endpoint. For sources that list multiple tile URLs (load-balanced CDN shards, e.g. ["https://a.tile.openstreetmap.org/{z}/{x}/{y}.png", "https://b.tile…", "https://c.tile…"]), this can hit one server far harder than normal map use would, potentially triggering per-IP rate limits on large offline regions.
A simple round-robin over all templates would spread the load the same way the map viewer does:
| const template = templates[0]; | |
| for (const [i, tile] of [...enumerateTiles(bbox, minZoom, maxZoom)].entries()) { | |
| const template = templates[i % templates.length]; | |
| urls.add(absolute(expandTileUrl(template, tile, spec.subdomains))); | |
| } |
Confidence: low-medium — the default OpenFreeMap/CARTO basemaps likely don't hit per-shard rate limits in practice, but the helper is a general utility and the pattern is fragile for other sources.
| * vite.config.ts). Beyond this, Workbox evicts the oldest tiles as new ones | ||
| * arrive, so a region larger than this can't be fully retained — warn the user. | ||
| */ | ||
| const MAX_CACHE_ENTRIES = 8000; |
There was a problem hiding this comment.
Quality – magic constant duplicated from vite.config.ts
MAX_CACHE_ENTRIES = 8000 must equal the Workbox maxEntries: 8000 in vite.config.ts:587. Currently the comment says "kept in sync", but if someone bumps the Workbox limit (or the dialog constant) in isolation, the eviction warning will silently lie to users about when their tiles will be evicted.
Consider re-exporting the value from a shared constants file (or at minimum add a test that reads vite.config.ts and asserts the two values match). Confidence: high.
| const verticalResizeGuideRef = useRef<HTMLDivElement>(null); | ||
| // Teardown for an in-progress panel resize, so a pointercancel or an unmount | ||
| // mid-drag still detaches the global listeners and restores document.body. | ||
| const activeResizeCleanupRef = useRef<(() => void) | null>(null); |
There was a problem hiding this comment.
Bug (edge case) – single shared cleanup ref for two independent resize handlers
activeResizeCleanupRef is overwritten by whichever resize handler runs last. On a touch device a user can theoretically place two fingers simultaneously on the Layer-panel handle and the Style-panel handle. In that case:
startLayerPanelResizefires → stores itsonPointerUpin the ref, addswindowlisteners.startStylePanelResizefires → overwrites the ref with its ownonPointerUp, adds its ownwindowlisteners.- Component unmounts mid-drag → only the Style handler's listeners are removed; the Layer handler's
pointermove/pointeruplisteners onwindoware permanently retained (memory leak + stale handler).
A Map<number, () => void> keyed by pointerId would handle N concurrent drags cleanly:
| const activeResizeCleanupRef = useRef<(() => void) | null>(null); | |
| const activeResizeCleanupMap = useRef<Map<number, () => void>>(new Map()); | |
| useEffect( | |
| () => () => activeResizeCleanupMap.current.forEach((fn) => fn()), | |
| [], | |
| ); |
Then replace activeResizeCleanupRef.current = onPointerUp with activeResizeCleanupMap.current.set(event.pointerId, onPointerUp) and activeResizeCleanupRef.current = null with activeResizeCleanupMap.current.delete(pointerId) (capture the id in a const at the top of each handler).
Confidence: medium — simultaneous two-panel resize is rare, but the listener leak is real and the fix is low-risk.
| echo "signed $signed ($(du -h "$signed" | cut -f1))" | ||
| done < <(find apps/geolibre-desktop/src-tauri/gen/android \ | ||
| -name '*release-unsigned.apk') | ||
| rm -f "$store_pass_file" "$key_pass_file" |
There was a problem hiding this comment.
Security – decoded keystore file left on disk after signing
The password temp-files (ks.pass, key.pass) are deleted here, but $RUNNER_TEMP/release.jks written earlier is not. GitHub-hosted runners are ephemeral, so this is low-risk in practice, but defense-in-depth suggests wiping the keystore bytes as well. Consider:
| rm -f "$store_pass_file" "$key_pass_file" | |
| rm -f "$store_pass_file" "$key_pass_file" "$RUNNER_TEMP/release.jks" |
Confidence: low (ephemeral runners make exploitation unlikely, but trivial to fix).
| maxZoom: number, | ||
| options: { glyphRanges?: string[]; signal?: AbortSignal } = {}, | ||
| ): Promise<string[]> { | ||
| const { glyphRanges = ["0-255", "256-511"], signal } = options; |
There was a problem hiding this comment.
Quality – default glyph ranges cover only basic Latin
The default ["0-255", "256-511"] warms Unicode blocks 0x0000–0x01FF (Basic Latin + Latin Extended). Styles that label in Arabic, Hebrew, CJK, Devanagari, Thai, etc. require higher Unicode blocks (e.g. "1024-1279" for Arabic, "4096-4351" for CJK). Users running "Download Offline Area" on a non-Latin basemap will find place labels absent offline even after a complete tile download.
This is the right default for the common OpenFreeMap/CARTO Latin-label case, but it's worth a note in the JSDoc (or in the dialog UI) that non-Latin basemaps need a broader glyphRanges list passed in. Confidence: high (the limitation is real; whether to address it in this PR is a scope question).
Code reviewReviewed: Android CI workflow, Rust cfg(desktop) gating, offline tile download logic (offline-tiles.ts + OfflineRegionDialog), PWA Workbox cache rules, pointer-event resize migration, mobile feature-gating (isMobile / useIsMobileViewport), and the responsive layout changes. See inline comments for details and suggestions. Bugs
Security
Performance
Quality
CLAUDE.md: nothing to flag — Rust changes are correctly desktop-gated, new i18n strings use Overall: The core logic (tile enumeration, antimeridian splitting, quadkey, SW warming loop, abort plumbing, pointer-event migration, Rust mobile gating) is solid and well-tested. All six findings are non-blocking; the most actionable are the |
- Upstream: TopToolbar god-component refactored into menus/hooks/toolbar/ - Upstream: AddDataDialog split into per-source components (add-data/) - Upstream: geocoding multi-provider + batch/reverse (opengeos#346, opengeos#318, opengeos#363) - Upstream: real-time collaboration MVP (opengeos#307) - Upstream: Python console + automation API (opengeos#327) - Upstream: storymap builder (opengeos#299), print layout (opengeos#303) - Upstream: network analysis (isochrones, OD matrices) (opengeos#308) - Upstream: spatial statistics toolbox (opengeos#342) - Upstream: AI segmentation (SamGeo/SAM3) (opengeos#301) - Upstream: GIS assistant (Strands agent) (opengeos#331) - Upstream: client-side vector tiling (opengeos#356) - Upstream: layer groups/folders (opengeos#311) - Upstream: shapefile/GeoPackage export (opengeos#310) - Upstream: raster pseudocolor + band combination (opengeos#319) - Upstream: attribute table virtualization + statistics (opengeos#338, opengeos#315) - Upstream: i18n framework (opengeos#273), a11y pass (opengeos#279) - Upstream: undo/redo for layers (opengeos#269), command palette (opengeos#278) - Upstream: Android PWA + offline caching (opengeos#377) - Upstream: CDN CereusDB wasm, PGlite CDN loader MilGeo customizations preserved: - MilSymb/Analysis/Sillages toolbar buttons + aside panels - milsymbolPlugin in vite.config.ts (APP-6D dev server middleware) - MilSymbolRenderer in DesktopShell - PrivacyNoticeDialog (startup gate) - ui.analysisOpen / ui.sillagesOpen in store - MGRS status bar display - Switzerland default map center (zoom 3) - mil-symbol / mil-graphic LayerTypes - MilGeo rebrand (title, favicon, URLs) - Traccar dev-proxy in vite.config.ts - openTopographyApiKey / demSource / localDtmPath settings - milsymbol@^3.0.4 dependency
Summary
Adds native Android support (Tauri v2 mobile), a responsive/touch layout pass, mobile feature-gating, slim signed APKs, and two PWA offline improvements — plus CI to build the Android APK.
Android (Tauri v2 mobile)
WebviewWindowBuilder::{on_new_window, window_features}don't exist on Tauri's mobile runtime — now behind#[cfg(desktop)]. Desktop behavior is unchanged (check:rustpasses)..sowas 210 MB).tauri.android.conf.jsonalso drops the bundled Python backend (unusable on Android)..github/workflows/android.ymlbuilds the release APK in CI (JDK 21, Android SDK + NDK r27 LTS, Rust android targets) and signs it: with release-keystore secrets (ANDROID_KEYSTORE_BASE64/ANDROID_KEYSTORE_PASSWORD/ANDROID_KEY_ALIAS/ANDROID_KEY_PASSWORD) when set, otherwise a throwaway debug keystore so the artifact is still installable for testing.Mobile feature-gating
isTauri()istrueon Android, so sidecar-backed tools previously appeared then failed. New UA-basedisMobile()helper hides what can't run on a mobile OS:Responsive / touch layout
max-md), expanded Layers/Style panels overlay the map as slide-over sheets instead of squeezing it; collapsed bars stay in-flow. Resize handles are touch-capable (Pointer Events). Shared reactiveuseIsMobileViewporthook.PWA / offline
geolibre-cdn-enginesCacheFirstrule caches Pyodide + PGlite/PostGIS for offline use after first load.lib/offline-tiles.ts+OfflineRegionDialog, with unit tests.Testing
npm run build,npm run check:rust— pass.tests/offline-tiles.test.ts(15) +tests/is-mobile.test.ts(4).pre-commit run --files(eslint + npm build) — passes on every change.Notes / follow-ups
ANDROID_KEYSTORE_*repo secrets so CI signs with a real upload key (the debug-keystore fallback is test-only).android-actions/setup-androidaction to a commit SHA before relying on it on protected branches.Summary by CodeRabbit
Release Notes