From f3fdf4bf37d801a930f1a0dad26f228ee9f18aad Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Wed, 30 Sep 2026 10:04:34 +0000 Subject: [PATCH] Mobile apps from plugins: a how-to, with Deep Research as the worked example documentation/how-to-build-mobile-apps-from-plugins/README.md explains why Deep Research (doxa-maps-spa, its app preset) is the plugin to wrap first: the seven production maps in one phone-ready shell, token-free on MapLibre. It then walks the whole path: tools, the wiring check, one command, what each step does, another plugin as the app, making it your own, and fixes. The one command needed two small changes; the default app is unchanged: - internal/set-app-start-page.mjs takes APP_START=/[/] and opens that page instead of the UPG-100 list. Unset, it behaves and marks the page exactly as before. A malformed value, a page missing from the build, or links that would not resolve are refused, as before. - package.json: phone:from-tree is sync, start page, verify, assemble, install from the committed tree, with no full rebuild (bun run build refuses without BUILD_ALLOW=1). phone:deep-research is phone:from-tree with Deep Research as the start page. how-to.md links the guide, and two of its lines are corrected: webDir is public/dx-maps/, and apk:debug's first step is the full build, which asks for BUILD_ALLOW=1. Checked, with no Android tools: the start page written for doxa-maps-spa/doxa-maps-spa/app, then the app root served and loaded in the shared browser at 390 and 1280. It showed the gallery element, 15 tabs (the seven maps and the experimental ones) and a drawn map, with 0 console errors and 0 failed requests. --check passed for APP_START and failed for the default until it was restored; the default run still writes the UPG-100 page with its original marker. The Gradle and adb steps are those bun run phone already uses; they were not run here. --- .../README.md | 131 ++++++++++++++++++ documentation/how-to.md | 7 +- internal/set-app-start-page.mjs | 40 ++++-- package.json | 2 + 4 files changed, 165 insertions(+), 15 deletions(-) create mode 100644 documentation/how-to-build-mobile-apps-from-plugins/README.md diff --git a/documentation/how-to-build-mobile-apps-from-plugins/README.md b/documentation/how-to-build-mobile-apps-from-plugins/README.md new file mode 100644 index 000000000..cb008c9f2 --- /dev/null +++ b/documentation/how-to-build-mobile-apps-from-plugins/README.md @@ -0,0 +1,131 @@ +# How to build a mobile app from a plugin + +The plugins in this repo are web pages, and an Android app can wrap them with no new code. This guide +turns one into an app you can install, using **Deep Research** as the worked example. It takes one +command once the tools are installed. + +## Why start with Deep Research + +Deep Research is the plugin that is already a whole app. It is the app shell in +`plugin-designs/doxa-maps-spa/` (its `app` preset). One screen holds all seven production maps: +- Big Picture; +- Simple Research; +- Regions; +- Prayer Progress; +- UPG-100 List; +- Cities with Internet; +- Gospel Resources. + +The experimental maps are there too. It is made for phones: +- a round **☰** at the bottom left switches maps; +- each legend starts folded; +- the ⓘ opens as a sheet over the map; +- posters, packets and cards print from the phone. + +It draws with the free MapLibre engine, so it needs no Mapbox key. And it needs no new code: the app +is the same built folder the web uses, opened on Deep Research instead of on the Plugin Studio. + +## The short version + +```bash +bun install +bun run check:mobile # checks the app is wired up; needs no Android tools +bun run phone:deep-research # makes the app and installs it on your phone or emulator +``` + +Then open **Doxa Maps** on the phone. + +## What you need + +- This repo, cloned, and [Bun](https://bun.sh). The README's quick start covers both. +- **Android Studio**, which brings the Android SDK and `adb`, and **JDK 17**. +- An Android phone plugged in over USB with USB debugging turned on, or an emulator started from + Android Studio. `adb devices` should list it. + +iOS is not set up yet. It needs a Mac with Xcode, and its project would live beside the Android one, +at `deployment/capacitor/ios`. + +## Step by step + +### 1. Check the wiring (no Android tools needed) + +```bash +bun run check:mobile +``` + +This copies the built folder, `public/dx-maps/`, into the Android project (Capacitor's `cap sync`). +It then checks that the copy landed. It should end with `OK webDir copied`. + +### 2. Make the app and install it + +```bash +bun run phone:deep-research +``` + +This runs, in order: + +1. `cap sync android`: copies the built folder into the Android project. +2. The start-page step, `APP_START=doxa-maps-spa/doxa-maps-spa/app`: the app opens Deep Research + instead of the Plugin Studio. +3. `cap:verify`: stops if the copy is empty, which would show as a blank white screen. +4. `./gradlew assembleDebug`: builds `app-debug.apk`. +5. `adb install -r`: installs it on the phone. + +It uses the built folder as it is committed, so nothing is rebuilt. On a fresh clone that folder is +the current build. + +### 3. Open it + +The app is called **Doxa Maps**. On a phone it opens on Simple Research, because Big Picture needs a +larger screen. The round **☰** at the bottom left lists every map. + +## Changed a plugin first? + +Rebuild the one you changed, then run the same command: + +```bash +BUNDLE= bun x vite build && bun run tree && bun run fix:sri +bun run phone:deep-research +``` + +## Any other plugin as an app + +It is the same command with another start page. Name the page's folder inside `public/dx-maps/`, as +`/` or `//`: + +```bash +APP_START=doxa-simple-map/doxa-simple-map bun run phone:from-tree # Prayer Progress +bun run phone:from-tree # unset: the UPG-100 list +``` + +Pick a page that works on a phone; Big Picture, for one, is desktop only. The start-page step +refuses a page that is not in the build, and one whose links would not resolve at the app's root. + +## Make it your own + +- **The name under the icon:** `app_name` in + `deployment/capacitor/android/app/src/main/res/values/strings.xml`. +- **Icon and splash screen:** the images in `deployment/capacitor/android/app/src/main/res/`. +- **Version:** `versionCode` and `versionName` in `deployment/capacitor/android/app/build.gradle`. +- **A store release:** it needs its own app id and a signing key. The key never goes in the repo. + +## If something goes wrong + +- **A blank white screen:** the copy is empty. Run `bun run check:mobile`; it must say + `OK webDir copied`. +- **The app opens the Plugin Studio:** the start-page step did not run. Run + `bun run phone:deep-research` again. +- **`adb` finds no device:** turn on USB debugging and accept the prompt on the phone, or start an + emulator. +- **Maps without their data:** several maps load live data, so the phone needs internet. +- **Using a Mapbox token:** the app's address is `https://localhost`. A token service must allow that + address (CORS). Deep Research's `app` preset uses MapLibre and needs no token. + +## How it works + +The app is a WebView around the same built folder the web serves; `capacitor.config.json` points at +it (`webDir`). The start-page step, `internal/set-app-start-page.mjs`, copies the chosen page to the +folder's root in the app's copy and fixes its links. So the app opens that page, and the web folder +itself is untouched. The Plugin Studio stays inside the app at `studio.html`. + +One build, three outputs: the web folder, this Android app, and the desktop app (see the README). diff --git a/documentation/how-to.md b/documentation/how-to.md index a3462dd52..72d11f83c 100644 --- a/documentation/how-to.md +++ b/documentation/how-to.md @@ -303,13 +303,16 @@ Contracts and gotchas (PROFILES shape, tag-vs-bundle names, `--no-token`): **Mobile is separate from `bun run build`.** The default build produces **only** the `dx-maps` web folder — the embeddable maps (and the SPA, if configured). It never touches Capacitor. The Android/iOS app is an **opt-in** step that *reuses* that web build: Capacitor's `webDir` points at -`public/` (`appId: life.doxa.maps`, `appName: "Doxa Maps"`), so `cap sync` wraps the already-built +`public/dx-maps/` (`appId: life.doxa.maps`, `appName: "Doxa Maps"`), so `cap sync` wraps the already-built folder into a native app. You never need to build mobile to ship the web maps. +**The quickest app is one plugin, with no rebuild:** `bun run phone:deep-research` installs Deep Research +as an app. The whole tutorial: [how to build mobile apps from plugins](how-to-build-mobile-apps-from-plugins/README.md). + **Android** (project is committed; needs Android Studio + a JDK): ```bash -bun run apk:debug # build web → cap sync → verify → assemble a debug APK +bun run apk:debug # build web → cap sync → verify → assemble a debug APK (the full build asks for BUILD_ALLOW=1) bun run apk:install # adb install onto a connected device/emulator ``` diff --git a/internal/set-app-start-page.mjs b/internal/set-app-start-page.mjs index 1ea1f8eb8..f1a95ce46 100644 --- a/internal/set-app-start-page.mjs +++ b/internal/set-app-start-page.mjs @@ -26,7 +26,11 @@ * verified means there is no second copy of that hash to drift. Only the relative depth changes. * * Usage: node internal/set-app-start-page.mjs [--check] - * --check assert the app root is already the UPG-100 page; do not write. Exit 1 if not. + * --check assert the app root is already the start page; do not write. Exit 1 if not. + * APP_START=/[/] start on another plugin page instead of the UPG-100 list, + * as the tree lays it out: APP_START=doxa-maps-spa/doxa-maps-spa/app is Deep Research + * (`bun run phone:deep-research`). Unset, the app opens the UPG-100 list, as before. + * How-to: documentation/how-to-build-mobile-apps-from-plugins/README.md */ import { readFileSync, writeFileSync, existsSync, copyFileSync } from 'node:fs' import { resolve, dirname, join } from 'node:path' @@ -39,13 +43,21 @@ const CHECK = process.argv.includes('--check') // The assets tree `cap sync` writes. NOT public/ — see the header. const APP_PUBLIC = join(ROOT, 'deployment/capacitor/android/app/src/main/assets/public') const APP_ROOT = join(APP_PUBLIC, 'index.html') -// The showcase leaf, inside the served tree. +// The showcase leaf, inside the served tree: the UPG-100 list unless APP_START names another page. const TREE = TREE_NAME -const LEAF_REL = `my-upg-100-list/upg-100-mfe/index.html` // the app's web root IS the tree (webDir public/) +const DEFAULT_START = 'my-upg-100-list/upg-100-mfe' +const START = (process.env.APP_START || DEFAULT_START).trim().replace(/^\/+|\/+$/g, '') +if (!/^[a-z0-9_-]+(\/[a-z0-9_-]+){1,2}$/i.test(START)) { + console.error(`APP-START-PAGE: FAIL — APP_START must be /[/], got "${START}".`) + process.exit(1) +} +const NAME = START === DEFAULT_START ? 'the UPG-100 list' : START +const LEAF_REL = `${START}/index.html` // the app's web root IS the tree (webDir public/) const LEAF = join(APP_PUBLIC, LEAF_REL) // A marker so --check can tell the app page apart from the studio page without parsing HTML. -const MARKER = `` +// The default keeps its original text, so an app built before APP_START existed still checks. +const MARKER = `` if (!existsSync(APP_PUBLIC)) { console.log('APP-START-PAGE: SKIP — no synced assets tree.') @@ -62,23 +74,25 @@ if (!existsSync(LEAF)) { if (CHECK) { const cur = existsSync(APP_ROOT) ? readFileSync(APP_ROOT, 'utf8') : '' if (cur.includes(MARKER)) { - console.log('APP-START-PAGE: PASS — the app opens the UPG-100 list.') + console.log(`APP-START-PAGE: PASS — the app opens ${NAME}.`) process.exit(0) } - console.error('APP-START-PAGE: FAIL — the app root is NOT the UPG-100 page.') + console.error(`APP-START-PAGE: FAIL — the app root is NOT ${NAME}.`) console.error(cur.includes(': a recipient opening the app meets the dev studio.' - : ' No UPG-100 marker found.') + : ` No start-page marker for ${START} found.`) console.error(' Fix: node internal/set-app-start-page.mjs (after cap:sync).') process.exit(1) } // ── REWRITE THE LEAF FOR ONE LEVEL UP ──────────────────────────────────────── -// The leaf lives at /my-upg-100-list/upg-100-mfe/index.html and reaches its bundle with -// `../my-upg-100-list.js`. At the app root that same relative path resolves ABOVE the web root. -// Every leaf-relative reference gains the leaf's own directory prefix; absolute and protocol URLs -// (the Mapbox CDN tags) are left alone. -const LEAF_DIR = `my-upg-100-list/upg-100-mfe/` +// The leaf lives at //index.html (the UPG-100 list: my-upg-100-list/upg-100-mfe/) and +// reaches its bundle with `../.js`. At the app root that same relative path resolves ABOVE +// the web root. Every leaf-relative reference gains the leaf's own directory prefix; absolute and +// protocol URLs (the Mapbox CDN tags) are left alone. An inline "assetsBase":"../../assets" is not +// rewritten and needs no rewrite: a path that climbs past the web root stops AT it, and the tree's +// assets/ is there. +const LEAF_DIR = `${START}/` const html = readFileSync(LEAF, 'utf8') const rebased = html.replace( /\b(src|href)=(["'])(?!https?:|data:|#|\/)([^"']+)\2/g, @@ -110,7 +124,7 @@ const STUDIO_KEEP = join(APP_PUBLIC, 'studio.html') if (existsSync(APP_ROOT) && !existsSync(STUDIO_KEEP)) copyFileSync(APP_ROOT, STUDIO_KEEP) writeFileSync(APP_ROOT, rebased.replace(//i, `\n ${MARKER}`)) -console.log('APP-START-PAGE: the app now opens the UPG-100 list.') +console.log(`APP-START-PAGE: the app now opens ${NAME}.`) console.log(` from ${LEAF_REL}`) console.log(` wrote assets/public/index.html (${referenced.length} reference(s) rebased, all present)`) console.log(' the studio page is kept at assets/public/studio.html') diff --git a/package.json b/package.json index 87174f1bb..6920d87b9 100644 --- a/package.json +++ b/package.json @@ -351,6 +351,8 @@ "packet:ci": "node internal/build-packet-fixtures.mjs", "packets": "cd library/scripts/posters && VITE_API_BASE_URL=https://pray.doxa.life node packet.mjs", "phone": "bun run apk:debug && bun run apk:install", + "phone:deep-research": "APP_START=doxa-maps-spa/doxa-maps-spa/app bun run phone:from-tree", + "phone:from-tree": "bun run cap:sync && bun run app:start-page && bun run cap:verify && cd deployment/capacitor/android && ./gradlew assembleDebug && adb install -r app/build/outputs/apk/debug/app-debug.apk", "posters": "cd library/scripts/posters && SPARSE=1 OFFLINE=1 VITE_API_BASE_URL=https://pray.doxa.life FORMAT=3x3 node generate.mjs", "posters:export": "cd library/scripts/posters && node export-public.mjs", "posters:gallery": "cd library/scripts/posters && node build-gallery.mjs && node build-hd-gallery.mjs",