feat(android): Capacitor wrapper bundling the Fabric web frontend

Greenfield Android client built as a Capacitor shell (mirrors the
Desktop strategy — reuses the React SPA 100%):
- appId ai.hangman.fabric, appName Fabric, androidScheme http +
  usesCleartextTraffic (talks to the http Center/Guild backends; the
  login screen sets the Center base, so a phone points at a LAN/host
  URL instead of localhost).
- scripts/build-web.mjs builds Fabric.Frontend and copies dist -> www;
  npm run sync / apk:debug wire it to Gradle.
- Native android/ project (cap add android) committed; build outputs,
  www/, node_modules, local.properties gitignored.
- Launcher icons = the Fabric mark (adaptive: green mark on black,
  legacy + round) at all densities.

Verified: gradlew assembleDebug -> app-debug.apk (4.2MB) containing
assets/public/index.html + the SPA bundle + Fabric icons.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
h z
2026-05-16 12:39:08 +01:00
parent f06855c37f
commit c09506b6ea
58 changed files with 2226 additions and 0 deletions

37
scripts/build-web.mjs Normal file
View File

@@ -0,0 +1,37 @@
#!/usr/bin/env node
/**
* Build the Fabric web frontend and copy the static bundle into www/ so the
* Capacitor Android app ships it. Capacitor serves www/ from http://localhost
* with SPA index.html fallback, so the standard (absolute-base) build +
* BrowserRouter work as-is.
*
* Fabric.Frontend is the sibling submodule under the parent Fabric repo.
*/
import { execSync } from 'node:child_process'
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const ROOT = path.resolve(__dirname, '..')
const FRONTEND = path.resolve(ROOT, '..', 'Fabric.Frontend')
const SRC = path.join(FRONTEND, 'dist')
const DEST = path.join(ROOT, 'www')
if (!fs.existsSync(FRONTEND)) {
console.error(`[build-web] Fabric.Frontend not found at ${FRONTEND}`)
process.exit(1)
}
console.log('[build-web] building frontend…')
execSync('npm install --no-audit --no-fund', { cwd: FRONTEND, stdio: 'inherit' })
execSync('npm run build', { cwd: FRONTEND, stdio: 'inherit' })
if (!fs.existsSync(path.join(SRC, 'index.html'))) {
console.error(`[build-web] expected ${SRC}/index.html after build`)
process.exit(1)
}
fs.rmSync(DEST, { recursive: true, force: true })
fs.cpSync(SRC, DEST, { recursive: true })
console.log(`[build-web] copied ${SRC} -> ${DEST}`)