shots: feature rotate, per-language frame fallback, layout-aware capture selectors
This commit is contained in:
parent
2e1c7cdc3c
commit
61b5fa0623
15 changed files with 355 additions and 17 deletions
|
|
@ -1,6 +1,6 @@
|
|||
// Raw frames from a running build of your app or game, from any engine.
|
||||
// storebound shots capture [--lang en,tr] [--only hero,shop] [--device phone|tablet] [--source web|ios-sim|android-emu|folder]
|
||||
// [--dry] [--keep-booted] [--concurrency N] [--only-missing | --force]
|
||||
// [--dry [--probe]] [--keep-booted] [--concurrency N] [--only-missing | --force]
|
||||
// Frames go to <shots.frames>/<lang>/<device>/<name>.png: the screen BELOW the status bar (the template draws one).
|
||||
// shots.capture.source (default web):
|
||||
// web headless Chromium (playwright), mobile viewport + touch at phone / tablet size. Each shot:
|
||||
|
|
@ -18,7 +18,9 @@
|
|||
// folder your own frames (any tool, any engine): only checks that every frame the scenes use exists with the
|
||||
// right aspect ratio and resolution.
|
||||
// A simulator storebound booted itself is shut down at the end (--keep-booted keeps it); one that was already running is
|
||||
// left running with its status bar restored. --dry prints the simctl / adb commands without running them.
|
||||
// left running with its status bar restored. Web shots may list waitSelector / click candidates per layout
|
||||
// ({ phone, tablet, any }, data attributes first, text= fallback); --dry prints them per size, --dry --probe opens the
|
||||
// pages and reports which candidate matched or MISSed on which size. --dry prints the simctl / adb commands without running them.
|
||||
// Simulator / emulator screenshots are full screen: cropTop rows (the status bar) are cut so the frame fits the template
|
||||
// (default per device class from the frame dims: phone 162 px of a 6.9" iPhone, tablet 48 px of a 13" iPad with apple.ipad).
|
||||
// The app or game needs switches for this: language, scene, fixed seed, no ads / tutorial, freeze. See the store-screenshots skill.
|
||||
|
|
@ -26,7 +28,8 @@ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node
|
|||
import { homedir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import type { ShipConfig, ShotsCapture } from '../lib/config';
|
||||
import type { ShipConfig, ShotSelector, ShotsCapture } from '../lib/config';
|
||||
import { candidates } from './selectors';
|
||||
import type { Ctx } from '../lib/ctx';
|
||||
import { need } from '../lib/ctx';
|
||||
import { done, log, step, warn } from '../lib/log';
|
||||
|
|
@ -36,7 +39,7 @@ import { adbAppLocaleArgs, adbDemoExitArgs, adbDemoModeArgs, adbForceStopArgs, a
|
|||
cropArgs, cropTopFor, fill, pickSimulator, shotsFor, simBootArgs, simBootStatusArgs, simInstallArgs, simLaunchArgs, simOpenUrlArgs,
|
||||
simScreenshotArgs, simShutdownArgs, simStatusBarArgs, simStatusBarClearArgs, simTarget, SOURCES, type Source, TOOL_HINTS, validateCapture } from './devices';
|
||||
import { type CopyFile, copyText, type FrameDims, frameDims } from './lib';
|
||||
import { captureWeb, defaultConcurrency, hashPaths, hashServed, planWeb, readCache, selectJobs, summaryLines, writeCaptureState } from './capture-web';
|
||||
import { captureWeb, defaultConcurrency, probeSelectors, hashPaths, hashServed, planWeb, readCache, selectJobs, summaryLines, writeCaptureState } from './capture-web';
|
||||
import { imageInfo } from './spec';
|
||||
import { authPlan, safeUrl } from './auth';
|
||||
import * as rt from '../lib/rt';
|
||||
|
|
@ -122,7 +125,21 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
|
||||
if (source === 'web') {
|
||||
if (dry) {
|
||||
for (const lang of langs) for (const device of devices) for (const [name, s] of shotsFor(cap, device, only)) progress(`${lang}/${device}/${name}.png <- ${cap.baseUrl}${(s.path ?? '/').replaceAll('{lang}', lang)}`);
|
||||
for (const lang of langs) for (const device of devices) for (const [name, s] of shotsFor(cap, device, only)) {
|
||||
progress(`${lang}/${device}/${name}.png <- ${cap.baseUrl}${(s.path ?? '/').replaceAll('{lang}', lang)}`);
|
||||
const sel: [string, string[]][] = [['waitSelector', candidates(s.waitSelector, device)], ...(Array.isArray(s.click) && s.click.some(c => typeof c !== 'string') ? s.click : s.click === undefined ? [] : [s.click]).map((c, k) => [`click[${k}]`, candidates(c as ShotSelector, device)] as [string, string[]])];
|
||||
for (const [what, list] of sel) if (list.length) log('shots', ` ${what} on ${device} ${dims[device].w}x${dims[device].h}: ${list.join(' | ')}`);
|
||||
}
|
||||
if (a.has('probe')) { // open the pages and test every candidate on each size (needs the app running at baseUrl)
|
||||
need(cap.baseUrl, 'shots.capture.baseUrl');
|
||||
const auth = cap.auth ? authPlan(cap.auth, { root, baseUrl: cap.baseUrl! }) : undefined;
|
||||
const jobs = planWeb(cap, { langs, devices, only, dims, framesDir, build: '', fonts: '' });
|
||||
const browser = await launch(root, { headless: true, args: ['--mute-audio'] });
|
||||
let r;
|
||||
try { r = await probeSelectors(browser, jobs, { dims, ...(auth ? { auth } : {}) }); } finally { await closeBrowser(browser); }
|
||||
for (const l of r.lines) log('shots', l);
|
||||
if (r.missed) throw new Error(`${r.missed} selector lists matched nothing (see MISS lines above: the size and every candidate with its match count)`);
|
||||
}
|
||||
if (cap.auth) log('shots', cap.auth.storageState ? `would load the signed-in state from ${cap.auth.storageState} into every context` : `would sign every context in first: ${(cap.auth.request?.method ?? 'POST').toUpperCase()} ${safeUrl(cap.auth.request?.url ?? '')} (values from .storebound/credentials.json / env, never shown)`);
|
||||
return done('shots', `capture planned (dry run): ${total} frames from web`);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue