Phase 1: wrap the reference web app in Capacitor for Android

Wraps reference/sp404-layers as a sideloadable APK without editing the
reference, which stays the frozen spec for the Phase 2 React port.

- sync-www.sh builds www/ from reference/ + native/, asserting each patch
  anchor so a reference change fails loudly rather than shipping broken
- drops the service worker (would serve a stale app after an APK update)
- haptics via a navigator.vibrate override, so buzz() needs no change
- localStorage mirrored into Preferences; sync load()/save() untouched
- hardware back: sheet -> Explore tab -> exit
- edge-to-edge handled in MainActivity: Android 16 + targetSdk 36 removes
  the opt-out, and a WebView never fills env(safe-area-inset-*), so insets
  are pushed into CSS custom properties instead

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-16 00:26:35 +03:00
commit 1a8a12f7f3
75 changed files with 3542 additions and 0 deletions

View File

@@ -0,0 +1,96 @@
#!/usr/bin/env bash
# Installs the Android toolchain needed to build a Capacitor APK on Pop!_OS 24.04.
# Headless: JDK 21 + cmdline-tools + platform-tools (adb). No Android Studio.
# Idempotent - safe to re-run.
set -euo pipefail
# Versions below are verified against Capacitor 8.5.2's capacitor/build.gradle
# (compileSdk 36, targetSdk 36, minSdk 24, Java 21) - do not bump blind.
SDK_ROOT="${ANDROID_HOME:-$HOME/Android/Sdk}"
CMDLINE_BUILD="15859902" # newest per developer.android.com/studio
BUILD_TOOLS="36.1.0" # pairs with compileSdk 36
SHELL_RC="$HOME/.zshrc"
MARKER="# >>> android sdk (sp404-layers) >>>"
[ "$(id -u)" -ne 0 ] || { echo "Run as your normal user, not root. It will sudo when needed."; exit 1; }
echo "==> 1/6 apt packages (needs sudo)"
sudo apt-get update -qq
# android-sdk-platform-tools-common ships the udev rules that let adb see the phone.
sudo apt-get install -y openjdk-21-jdk android-sdk-platform-tools-common unzip curl
echo "==> 2/6 command-line tools -> $SDK_ROOT"
if [ ! -x "$SDK_ROOT/cmdline-tools/latest/bin/sdkmanager" ]; then
tmp="$(mktemp -d)"; trap 'rm -rf "$tmp"' EXIT
url="https://dl.google.com/android/repository/commandlinetools-linux-${CMDLINE_BUILD}_latest.zip"
echo " downloading $url"
curl -fL --progress-bar -o "$tmp/cmdline.zip" "$url"
unzip -q "$tmp/cmdline.zip" -d "$tmp"
mkdir -p "$SDK_ROOT/cmdline-tools"
rm -rf "$SDK_ROOT/cmdline-tools/latest"
# the zip unpacks to cmdline-tools/, sdkmanager insists on being at cmdline-tools/latest/
mv "$tmp/cmdline-tools" "$SDK_ROOT/cmdline-tools/latest"
else
echo " already present, skipping"
fi
# Derive JAVA_HOME from the javac we just installed rather than hardcoding a path.
JAVA_HOME="$(dirname "$(dirname "$(readlink -f "$(command -v javac)")")")"
[ -x "$JAVA_HOME/bin/javac" ] || { echo "Could not locate JAVA_HOME (got '$JAVA_HOME')"; exit 1; }
case "$("$JAVA_HOME/bin/javac" -version 2>&1)" in
*"javac 21."*) : ;;
*) echo "WARNING: Capacitor 8 wants Java 21, found: $("$JAVA_HOME/bin/javac" -version 2>&1)" ;;
esac
export JAVA_HOME
export ANDROID_HOME="$SDK_ROOT"
export ANDROID_SDK_ROOT="$SDK_ROOT"
export PATH="$SDK_ROOT/cmdline-tools/latest/bin:$SDK_ROOT/platform-tools:$JAVA_HOME/bin:$PATH"
echo "==> 3/6 accepting SDK licenses"
yes 2>/dev/null | sdkmanager --licenses >/dev/null || true
echo "==> 4/6 SDK packages (this is the slow part, ~1-2 GB)"
sdkmanager --install \
"platform-tools" \
"platforms;android-35" \
"platforms;android-36" \
"build-tools;${BUILD_TOOLS}"
echo "==> 5/6 persisting env to $SHELL_RC"
if ! grep -qF "$MARKER" "$SHELL_RC" 2>/dev/null; then
cat >> "$SHELL_RC" <<EOF
$MARKER
export JAVA_HOME=$JAVA_HOME
export ANDROID_HOME=$SDK_ROOT
export ANDROID_SDK_ROOT=$SDK_ROOT
export PATH=\$ANDROID_HOME/cmdline-tools/latest/bin:\$ANDROID_HOME/platform-tools:\$JAVA_HOME/bin:\$PATH
# <<< android sdk (sp404-layers) <<<
EOF
echo " added"
else
echo " already there, skipping"
fi
echo "==> 6/6 usb device access"
if ! id -nG "$USER" | grep -qw plugdev; then
sudo usermod -aG plugdev "$USER"
echo " added $USER to plugdev - LOG OUT AND BACK IN for this to take effect"
else
echo " already in plugdev"
fi
sudo udevadm control --reload-rules && sudo udevadm trigger || true
cat <<'EOF'
------------------------------------------------------------
Done. Open a NEW terminal, then verify:
java -version # expect 21.x
sdkmanager --list_installed
adb devices # plug the phone in, tap "Allow USB debugging"
If adb shows "no permissions" or nothing, log out and back in
(the plugdev group change needs a fresh session).
------------------------------------------------------------
EOF

58
scripts/sync-www.sh Executable file
View File

@@ -0,0 +1,58 @@
#!/usr/bin/env bash
# Builds www/ from reference/ for the Capacitor wrapper.
#
# reference/ is the frozen spec and is NEVER edited. Everything native-specific
# is either an additive file (native.css, native.js) or one of the three
# removals below. Each patch asserts its anchor exists, so if the reference
# ever changes shape this fails loudly instead of silently shipping a broken app.
set -euo pipefail
cd "$(dirname "$0")/.."
SRC="reference"
OUT="www"
[ -f "$SRC/index.html" ] || { echo "missing $SRC/index.html"; exit 1; }
rm -rf "$OUT"
mkdir -p "$OUT"
# sw.js and README.md are deliberately not copied:
# - the service worker would cache index.html and serve a stale app after an
# APK update; assets are already local so it buys nothing.
# - README documents the web deployment, irrelevant inside the APK.
cp -r "$SRC/fonts" "$SRC/icons" "$OUT/"
cp "$SRC/manifest.webmanifest" "$OUT/" # harmless, keeps icon refs valid
cp "$SRC/index.html" "$OUT/index.html"
cp native/native.css native/native.js "$OUT/"
python3 - "$OUT/index.html" <<'PY'
import sys, re
path = sys.argv[1]
html = open(path, encoding="utf-8").read()
def patch(html, anchor, replacement, label):
if anchor not in html:
sys.exit(f"sync-www: anchor not found for '{label}':\n {anchor[:90]}")
return html.replace(anchor, replacement, 1)
# 1. drop the service worker registration
sw = '<script>if("serviceWorker" in navigator&&location.protocol!=="file:")addEventListener("load",()=>navigator.serviceWorker.register("sw.js"));</script>\n'
html = patch(html, sw, "", "service worker registration")
# 2. drop the web manifest link (the APK's own manifest governs the app)
html = patch(html, '<link rel="manifest" href="manifest.webmanifest">', "", "manifest link")
# 3. native stylesheet last in <head> so its overrides win on equal specificity
html = patch(html, "</head>",
'<link rel="stylesheet" href="native.css">\n</head>', "head close")
# 4. native script after the app script, so its globals already exist
html = patch(html, "</body>",
'<script src="native.js"></script>\n</body>', "body close")
open(path, "w", encoding="utf-8").write(html)
print("sync-www: patched index.html (4/4 anchors matched)")
PY
echo "sync-www: $OUT/ built from $SRC/"