diff --git a/apps/readest-app/extensions/send-to-readest/.gitignore b/apps/readest-app/extensions/send-to-readest/.gitignore index ae9c88d4..d370a05c 100644 --- a/apps/readest-app/extensions/send-to-readest/.gitignore +++ b/apps/readest-app/extensions/send-to-readest/.gitignore @@ -6,6 +6,9 @@ dist/ # Versioned upload archives produced by `pnpm zip`. *.zip +# Generated store screenshots (`pnpm store:screenshots`). +store/out/ + # Per-package node_modules (the workspace hoists most of it to the root, # but pnpm still drops a `.pnpm` symlink tree here). node_modules/ diff --git a/apps/readest-app/extensions/send-to-readest/package.json b/apps/readest-app/extensions/send-to-readest/package.json index 91caeff9..600cbb25 100644 --- a/apps/readest-app/extensions/send-to-readest/package.json +++ b/apps/readest-app/extensions/send-to-readest/package.json @@ -8,6 +8,7 @@ "dev": "webpack --watch --mode development", "clean": "rimraf dist", "zip": "pnpm build && node scripts/zip.mjs", + "store:screenshots": "node store/generate.mjs", "i18n:extract": "node scripts/extract-i18n.mjs", "i18n:check": "node scripts/extract-i18n.mjs --check" }, diff --git a/apps/readest-app/extensions/send-to-readest/store/README.md b/apps/readest-app/extensions/send-to-readest/store/README.md new file mode 100644 index 00000000..a18d30f8 --- /dev/null +++ b/apps/readest-app/extensions/send-to-readest/store/README.md @@ -0,0 +1,41 @@ +# Store screenshots + +Generates the Chrome Web Store listing screenshots for the **Send to Readest** +extension — the popup composited onto an on-brand background (matching +readest.com) with a headline. + +## Run + +```bash +pnpm store:screenshots # from extensions/send-to-readest/ +``` + +Outputs to `store/out/` (gitignored): + +- `send-to-readest-1280x800.png` +- `send-to-readest-640x400.png` + +Both are 24-bit PNG, sRGB, **no alpha** — what the Web Store requires for the +"Global screenshots" slot. + +## Requirements + +- **@playwright/test** — already a monorepo dev dependency (provides headless + Chromium). +- **ImageMagick** `magick` on PATH — `brew install imagemagick`. + +## Customising + +Edit `CONFIG` at the top of `generate.mjs`: + +- `headline` — the left-hand headline, one array entry per line. +- `status` — the popup success line. **Keep `status.text` in sync with the + success message in `src/popup/popup.ts`.** +- `popupDisplayW`, `bg` — popup size and brand background. + +## The popup asset + +`popup.png` is a raw capture of the extension popup, cropped to its dark card +(700×508). The generator blanks its status strip and overlays +`CONFIG.status.text`, so re-capturing the popup only means replacing `popup.png` +with a new crop of the same size/layout — no image editing needed. diff --git a/apps/readest-app/extensions/send-to-readest/store/generate.mjs b/apps/readest-app/extensions/send-to-readest/store/generate.mjs new file mode 100644 index 00000000..0c71a526 --- /dev/null +++ b/apps/readest-app/extensions/send-to-readest/store/generate.mjs @@ -0,0 +1,143 @@ +#!/usr/bin/env node +/** + * Generate Chrome Web Store screenshots for the Send to Readest extension. + * + * Composites the popup capture (store/popup.png) onto an on-brand background + * (matching readest.com) with a headline, renders it with headless Chromium, + * and writes 24-bit PNGs (no alpha) at the two Web Store sizes into store/out/. + * + * pnpm store:screenshots # from extensions/send-to-readest/ + * + * Requires: + * - @playwright/test (already a monorepo dev dependency) + * - ImageMagick `magick` on PATH (brew install imagemagick) + * + * To tweak the result, edit CONFIG below. Keep CONFIG.status.text in sync with + * the success message in src/popup/popup.ts. + */ +import { execFileSync } from 'node:child_process'; +import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { chromium } from '@playwright/test'; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const OUT = join(HERE, 'out'); +const POPUP = join(HERE, 'popup.png'); + +const CONFIG = { + bg: '#fdf9f3', + // Left-hand headline, one array entry per rendered line. + headline: ['Send any page', 'to your Readest', 'library.'], + // Popup success status (keep in sync with src/popup/popup.ts). + status: { text: 'Saved to your library.', color: '#78db88' }, + // popup.png is a 700x508 capture; these source coords place the status line + // and define the dark strip that is blanked, then re-lettered as an overlay. + popupSrc: { w: 700, h: 508, statusLeft: 33, statusTop: 420, statusFontPx: 27, coverTop: 400 }, + popupDisplayW: 560, // how wide the popup renders inside the 1280x800 canvas +}; + +const SIZES = [ + { w: 1280, h: 800 }, + { w: 640, h: 400 }, +]; + +function magick(args) { + execFileSync('magick', args, { stdio: ['ignore', 'ignore', 'inherit'] }); +} + +function ensureMagick() { + try { + execFileSync('magick', ['-version'], { stdio: 'ignore' }); + } catch { + console.error('ImageMagick `magick` not found on PATH. Install it: brew install imagemagick'); + process.exit(1); + } +} + +function buildHtml(blankedPopupPath) { + const s = CONFIG.popupDisplayW / CONFIG.popupSrc.w; + const left = Math.round(CONFIG.popupSrc.statusLeft * s); + const top = Math.round(CONFIG.popupSrc.statusTop * s); + const font = (CONFIG.popupSrc.statusFontPx * s).toFixed(1); + return ` +
+
${CONFIG.headline.join('
')}
+ +
+ `; +} + +async function main() { + ensureMagick(); + if (!existsSync(POPUP)) { + console.error(`missing popup asset: ${POPUP}`); + process.exit(1); + } + rmSync(OUT, { recursive: true, force: true }); + mkdirSync(OUT, { recursive: true }); + + // 1. Blank the popup's status strip so the configured status can be overlaid. + const blanked = join(OUT, '_popup-blanked.png'); + const { w, h, coverTop } = CONFIG.popupSrc; + magick([POPUP, '-fill', '#1a1a1a', '-draw', `rectangle 0,${coverTop} ${w},${h}`, blanked]); + + // 2. Render the composite at 2x for crisp downscaling. + const htmlPath = join(OUT, '_composite.html'); + writeFileSync(htmlPath, buildHtml(blanked)); + const raw = join(OUT, '_render@2x.png'); + const browser = await chromium.launch(); + const page = await browser.newPage({ viewport: { width: 1280, height: 800 }, deviceScaleFactor: 2 }); + await page.goto('file://' + htmlPath); + await page.waitForTimeout(300); + await page.screenshot({ path: raw }); + await browser.close(); + + // 3. Downscale + flatten to each Web Store size (24-bit, no alpha). + for (const { w: ow, h: oh } of SIZES) { + const out = join(OUT, `send-to-readest-${ow}x${oh}.png`); + magick([raw, '-resize', `${ow}x${oh}`, '-background', CONFIG.bg, '-flatten', + '-alpha', 'remove', '-alpha', 'off', '-strip', out]); + console.log(`wrote ${out}`); + } + + for (const f of [blanked, htmlPath, raw]) rmSync(f, { force: true }); +} + +main().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/apps/readest-app/extensions/send-to-readest/store/popup.png b/apps/readest-app/extensions/send-to-readest/store/popup.png new file mode 100644 index 00000000..031c6011 Binary files /dev/null and b/apps/readest-app/extensions/send-to-readest/store/popup.png differ