feat: store release setup — EAS config, listing copy, screenshots, new icon
Some checks failed
Build & Release APK / build (push) Has been cancelled

- eas.json production/simulator profiles; iOS plist flags; EAS project linked
- literal versionCode/versionName in build.gradle so EAS can parse it; Gitea CI
  stamps the run number with sed instead of env-driven gradle expressions
- store/: listing copy + keyword rationale, screenshot composer (headless
  Chrome), raw simulator captures, rendered App Store / Play slides, feature
  graphic, privacy policy source, icon generator
- replace Expo placeholder icon with the session-ring mark; regenerate Android
  launcher/splash res and theme colours

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WontRuPqKcod9XK4ZN9MXv
This commit is contained in:
marcuspaico
2026-09-07 16:10:27 +10:00
parent 6a75db0aae
commit 55999b7458
55 changed files with 421 additions and 12 deletions

85
store/icons/make.js Normal file
View File

@@ -0,0 +1,85 @@
#!/usr/bin/env node
// Generate the app icon set from one SVG mark, rendered through headless Chrome.
//
// node store/icons/make.js
//
// Writes into assets/images/:
// icon.png 1024×1024 opaque (iOS + fallback)
// android-icon-foreground.png 1024×1024 alpha (adaptive foreground, mark in the 66% safe zone)
// android-icon-monochrome.png 1024×1024 alpha (themed icon, white mark)
// splash-icon.png 512×512 alpha (splash, mark only)
//
// The mark is the session ring from the app: a thick accent ring with a filled
// core — "squeeze". Colours from src/lib/theme.ts. After running, regenerate the
// committed Android res with `npx expo prebuild -p android --no-install`.
const fs = require("fs");
const path = require("path");
const { execFileSync } = require("child_process");
const CHROME = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
const OUT = path.join(__dirname, "..", "..", "assets", "images");
const TMP = path.join(__dirname, ".tmp");
const C = { bg: "#160B0B", bg2: "#2A1112", accent: "#E7402E", accentDeep: "#B8271A", text: "#F7ECEA" };
// size: canvas px; scale: fraction of canvas the mark spans; color overrides for monochrome.
function mark(size, scale, { ring = C.accent, core = C.accent, glow = true } = {}) {
const c = size / 2;
const R = (size * scale) / 2; // outer radius of the ring
const stroke = R * 0.3;
const ringR = R - stroke / 2;
const coreR = R * 0.32;
return `
<defs>
<radialGradient id="g" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="${ring}" stop-opacity="0.35"/>
<stop offset="100%" stop-color="${ring}" stop-opacity="0"/>
</radialGradient>
</defs>
${glow ? `<circle cx="${c}" cy="${c}" r="${R * 1.35}" fill="url(#g)"/>` : ""}
<!-- open arc, ~82% of the ring, like the session progress ring mid-hold; gap at the top-right, dash starts at 12 o'clock -->
<circle cx="${c}" cy="${c}" r="${ringR}" fill="none" stroke="${ring}" stroke-width="${stroke}" stroke-linecap="round"
stroke-dasharray="${2 * Math.PI * ringR * 0.82} ${2 * Math.PI * ringR}" transform="rotate(-90 ${c} ${c})"/>
<circle cx="${c}" cy="${c}" r="${coreR}" fill="${core}"/>`;
}
function svg(size, body, bg) {
return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}">
${bg ? `<defs><linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="${C.bg2}"/><stop offset="100%" stop-color="${C.bg}"/></linearGradient></defs>
<rect width="${size}" height="${size}" fill="url(#bg)"/>` : ""}
${body}
</svg>`;
}
function render(svgText, size, out, transparent) {
fs.mkdirSync(TMP, { recursive: true });
const file = path.join(TMP, path.basename(out, ".png") + ".html");
fs.writeFileSync(
file,
`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;width:${size}px;height:${size}px;overflow:hidden;background:${transparent ? "transparent" : C.bg}}svg{display:block}</style></head><body>${svgText}</body></html>`,
);
const args = [
"--headless=new", "--disable-gpu", "--hide-scrollbars", "--force-device-scale-factor=1",
`--window-size=${size},${size}`, `--screenshot=${out}`,
];
if (transparent) args.push("--default-background-color=00000000");
args.push("file://" + file);
execFileSync(CHROME, args, { stdio: "ignore" });
}
function main() {
// iOS: opaque, mark fills ~62% of the canvas (Apple masks the corners itself).
render(svg(1024, mark(1024, 0.62), true), 1024, path.join(OUT, "icon.png"), false);
// Android adaptive foreground: content must sit inside the central 66% safe zone.
render(svg(1024, mark(1024, 0.46, { glow: false })), 1024, path.join(OUT, "android-icon-foreground.png"), true);
// Android monochrome: single-colour silhouette, same safe zone.
render(svg(1024, mark(1024, 0.46, { ring: "#FFFFFF", core: "#FFFFFF", glow: false })), 1024, path.join(OUT, "android-icon-monochrome.png"), true);
// Splash: mark only, on transparent; app.json sets the background colour.
render(svg(512, mark(512, 0.8, { glow: false })), 512, path.join(OUT, "splash-icon.png"), true);
fs.rmSync(TMP, { recursive: true, force: true });
console.log("wrote icon.png, android-icon-foreground.png, android-icon-monochrome.png, splash-icon.png");
}
main();