trilium/apps/mobile
Elian Doran 8b10c77c86
feat(geomap): show the device's location with MapLibre's locate button
Adds MapLibre's own GeolocateControl to the geo map, in the bottom-right
corner above the camera group, in tracking mode: the first press asks the
browser for the device's position, frames it to the accuracy of the fix and
follows it; dragging the map keeps the dot and frees the camera; a second
press turns the watch off and removes the dot.

The control is reused rather than rewritten. Its behaviour is what a
Trilium implementation would have had to copy: fitBounds on the accuracy
radius with a zoom cap, the active/background state machine driven by
movestart, the stale-dot treatment on error, and a permission probe that
notices an insecure origin and disables the button. Only its white button
clashes with the map's own controls, and that is a later step: the plan is
to hide the stock button and drive trigger() from an OverlayControlButton
on MapToolbar, mirroring the control's events. The dot and accuracy circle
are DOM markers, so unlike the layer-drawn pins they need no carrying
across a style switch.

The stock button stands one inset above MapToolbar's group, placed through
the existing --geo-map-foot variable, so the two do not overlap and the
button follows the group's safe-area insets in fullscreen.

Electron denied geolocation on purpose in the session permission
allowlist; it is now granted to the trilium-app://app shell only, gated by
the same origin check as clipboard-write and notifications, so a remote
embed in the default session does not learn where the device is. MapLibre
probes the permission through the synchronous check handler before it
enables its button, which is why the allowlist entry is needed for the
button to be usable at all on desktop.

Android declares coarse and fine location so Capacitor's WebChromeClient
can prompt for them, and marks the location hardware features as optional:
ACCESS_FINE_LOCATION otherwise implies GPS as a required feature, which
Google Play uses to hide the app from GPS-less devices. iOS declares the
when-in-use usage description, without which WebKit refuses the API.

Verified: the MapToolbar spec asserts the control is added in tracking
mode, removed on unmount and not removed from a map that already tore down;
the desktop spec asserts the app shell is granted geolocation and embeds
and guests are not. Both fail with the respective change reverted. Not
verified: the running app, the mobile builds, and the Linux desktop path,
where Chromium's network location provider may need a Google API key.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-05 19:32:26 +02:00
..
android feat(geomap): show the device's location with MapLibre's locate button 2026-09-05 19:32:26 +02:00
ios feat(geomap): show the device's location with MapLibre's locate button 2026-09-05 19:32:26 +02:00
.gitignore chore(mobile): start with an empty capacitor app 2026-04-20 15:09:52 +03:00
capacitor.config.json chore(mobile/ios): drop the no-op iosScheme: "https" 2026-07-05 18:50:34 +03:00
package.json Update capacitor monorepo to v8.5.1 2026-09-04 02:04:35 +00:00
README.md docs(mobile): document the iOS capacitor:// request-routing gotcha 2026-07-05 18:52:52 +03:00

@triliumnext/mobile

Capacitor shell that wraps the @triliumnext/standalone PWA build as a native mobile app. This package does not ship its own web assets — webDir in capacitor.config.json points directly at ../standalone/dist.

Prerequisites

  • Android SDK + an emulator or attached device (set up ANDROID_HOME / ANDROID_SDK_ROOT).
  • JDK 17+.
  • The monorepo installed: corepack enable && pnpm install at the repo root.

First-time setup

# 1. Build the standalone web app into apps/standalone/dist
pnpm --filter @triliumnext/mobile build

# 2. Generate the native Android project (one-off — commits as apps/mobile/android/)
pnpm --filter @triliumnext/mobile exec cap add android

Everyday loop

pnpm --filter @triliumnext/mobile build          # rebuild standalone dist
pnpm --filter @triliumnext/mobile sync           # copy dist into android/
pnpm --filter @triliumnext/mobile run:android    # launch on emulator/device
# or
pnpm --filter @triliumnext/mobile open:android   # open Android Studio

How web requests reach the local server (Android vs iOS)

There is no network backend — the whole server runs in-process as WASM in a web worker. How the client's API/sync calls (/api, /sync, /bootstrap, /search) reach that worker differs by platform, because the two WebViews resolve *Scheme: "https" differently:

  • AndroidandroidScheme: "https" works: the app loads at https://localhost (a real secure origin), so the service worker (apps/standalone/src/sw.ts) intercepts those requests and forwards them to the worker.
  • iOS — the app loads at capacitor://localhost and uses fetch / XHR / image interceptors (apps/standalone/src/main.ts, gated on location.protocol === "capacitor:") instead, because a service worker cannot register on capacitor:// (navigator.serviceWorker.register() throws — the scheme is not HTTP/HTTPS).

Why iOS is on capacitor:// and not https: Capacitor ignores iosScheme: "https". WKWebView reserves the http/https schemes, so CAPInstanceDescriptor.normalize() (WKWebView.handlesURLScheme("https") == true) rejects it and resets the scheme to the default capacitor. That's why iosScheme is intentionally not set here — it would be a no-op that falsely implies iOS runs on an https origin.

⚠️ Do not remove the iOS interceptor path as "dead code." It is the only working request path on iOS. And do not re-add iosScheme: "https" — it does nothing on iOS and is misleading.