feat: store release setup — EAS config, listing copy, screenshots, new icon
Some checks failed
Build & Release APK / build (push) Has been cancelled
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:
85
store/icons/make.js
Normal file
85
store/icons/make.js
Normal 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();
|
||||
Reference in New Issue
Block a user