play images: tablet, TV and Wear sets are optional; play.tablets switch (default: tablet renders exist); a missing optional set never deletes Play's images without --prune
A phone-only project aborted with 'no sevenInchScreenshots'. Play requires the 512 icon, the 1024x500 feature graphic and at least 2 screenshots (Play Console Help answer 9866151). play images uploads what exists and prints an info line for missing optional sets; play.tablets true makes missing tablet sets an error in shots render/check/gallery, validate and play images. Pitfall 19.
This commit is contained in:
parent
18fde37b9b
commit
a9261a8052
18 changed files with 364 additions and 83 deletions
|
|
@ -3,7 +3,8 @@
|
|||
// Per image: pixel size for its store type (Apple: a listed size, portrait or landscape; Play screenshots: 320-3840 px,
|
||||
// at most 2:1; feature graphic exactly 1024x500), no alpha (both stores), Play: JPEG or 24-bit PNG, 8 MB per screenshot.
|
||||
// --variants also checks the A/B variant folders (<renders>/<variant>/<lang>/...).
|
||||
// Per set: Play 2-8 phone screenshots, Apple 1-10 per display type, 13" iPad set when apple.ipad, feature graphic.
|
||||
// Per set: Play 2-8 phone screenshots, Apple 1-10 per display type, 13" iPad set when apple.ipad, feature graphic,
|
||||
// Play 7" / 10" tablet sets only when play.tablets (default: tablet renders exist; optional on Play).
|
||||
// Per app preview: 886x1920 / 1200x1600, H.264, <= 30 fps, 15-30 s, stereo AAC track, <= 500 MB, <= 3 per set.
|
||||
// Text overflow flags come from the last `shots render` (report.json). Frames: with capture.source "folder" every frame
|
||||
// the scenes use must exist with the right aspect. Local font files (shots.fonts paths) without a license note: warn.
|
||||
|
|
@ -16,43 +17,45 @@ import { log, printJson } from '../lib/log';
|
|||
import { checkFrames, type FrameIssue } from './devices';
|
||||
import { defaultSizes, frameDims, SIZES } from './lib';
|
||||
import type { LocalIssue, RendersCheck } from '../lib/validate-rules';
|
||||
import { allIssues, requiredSets, type RendersScan, scanRenders, storeOf } from './report';
|
||||
import { allIssues, requiredSets, type RendersScan, resolvedTablets, scanRenders, storeOf } from './report';
|
||||
import { imageInfo } from './spec';
|
||||
import { fontsDirOf, readManifest } from './fonts';
|
||||
import { localFontIssues } from './local-fonts';
|
||||
|
||||
/** Frame files the scenes (and the feature graphic) use, per language and device class. */
|
||||
export function framesNeeded(config: ShipConfig, langs: string[]): { lang: string; device: 'phone' | 'tablet'; name: string }[] {
|
||||
export function framesNeeded(config: ShipConfig, langs: string[], tablets?: boolean): { lang: string; device: 'phone' | 'tablet'; name: string }[] {
|
||||
const shots = config.shots!;
|
||||
const sizes = defaultSizes(config);
|
||||
const sizes = defaultSizes(config, tablets);
|
||||
const devices: ('phone' | 'tablet')[] = ['phone', ...(sizes.some(s => SIZES[s]!.device === 'tablet') ? ['tablet' as const] : [])];
|
||||
const scene = [...new Set(shots.scenes.flatMap(s => s.frames.map(f => f.src)))];
|
||||
const phone = [...new Set([...scene, ...(shots.feature?.frame && sizes.includes('feature') ? [shots.feature.frame] : [])])];
|
||||
return langs.flatMap(lang => devices.flatMap(device => (device === 'phone' ? phone : scene).map(name => ({ lang, device, name }))));
|
||||
}
|
||||
|
||||
export function checkFrameFolder(config: ShipConfig, dir: string, langs: string[]): FrameIssue[] {
|
||||
export function checkFrameFolder(config: ShipConfig, dir: string, langs: string[], tablets?: boolean): FrameIssue[] {
|
||||
const found = new Map<string, { w: number; h: number }>();
|
||||
for (const lang of langs) for (const device of ['phone', 'tablet']) {
|
||||
const d = join(dir, lang, device);
|
||||
if (!existsSync(d)) continue;
|
||||
for (const f of readdirSync(d).filter(x => /\.png$/i.test(x))) { const i = imageInfo(readFileSync(join(d, f))); found.set(`${lang}/${device}/${f}`, i); }
|
||||
}
|
||||
return checkFrames(found, framesNeeded(config, langs), frameDims(config));
|
||||
return checkFrames(found, framesNeeded(config, langs, tablets), frameDims(config));
|
||||
}
|
||||
|
||||
export function scanProject(ctx: Ctx): RendersScan {
|
||||
const { config, resolve } = ctx.loaded;
|
||||
const langs = ctx.args.list('lang').length ? ctx.args.list('lang') : config.languages.map(l => l.id);
|
||||
const dir = resolve(ctx.args.get('src') ?? config.renders ?? config.shots?.out ?? 'store/renders');
|
||||
return scanRenders(dir, { langs, variants: ctx.args.has('variants') ? Object.keys(config.shots?.variants ?? {}) : [], required: lang => requiredSets(config, lang) });
|
||||
const tablets = resolvedTablets(config, dir);
|
||||
return scanRenders(dir, { langs, variants: ctx.args.has('variants') ? Object.keys(config.shots?.variants ?? {}) : [], required: lang => requiredSets(config, lang, tablets) });
|
||||
}
|
||||
|
||||
/** Local renders check split per store, for `validate` (base renders only, no variants). null = no renders folder. */
|
||||
export function rendersCheck(config: ShipConfig, dir: string): Record<'apple' | 'play', RendersCheck> | null {
|
||||
if (!existsSync(dir)) return null;
|
||||
const langs = config.languages.map(l => l.id);
|
||||
const scan = scanRenders(dir, { langs, required: lang => requiredSets(config, lang) });
|
||||
const tablets = resolvedTablets(config, dir);
|
||||
const scan = scanRenders(dir, { langs, required: lang => requiredSets(config, lang, tablets) });
|
||||
const out = { apple: { dir, images: 0, issues: [] as LocalIssue[] }, play: { dir, images: 0, issues: [] as LocalIssue[] } };
|
||||
for (const img of scan.images) out[storeOf(img.rel)].images++;
|
||||
for (const v of scan.videos) out[storeOf(v.rel)].images++;
|
||||
|
|
@ -65,7 +68,8 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
const scan = scanProject(ctx);
|
||||
const issues = [...allIssues(scan), ...localFontIssues(readManifest(fontsDirOf(root))).map(issue => ({ where: '.storebound/fonts/fonts.json', issue }))];
|
||||
const cap = config.shots?.capture;
|
||||
const frameIssues = cap?.source === 'folder' ? checkFrameFolder(config, resolve(cap.folder ?? config.shots!.frames), scan.langs) : [];
|
||||
const tablets = resolvedTablets(config, resolve(ctx.args.get('src') ?? config.renders ?? config.shots?.out ?? 'store/renders'));
|
||||
const frameIssues = cap?.source === 'folder' ? checkFrameFolder(config, resolve(cap.folder ?? config.shots!.frames), scan.langs, tablets) : [];
|
||||
if (ctx.args.has('json')) {
|
||||
printJson({ images: scan.images.length, videos: scan.videos.length, issues, frames: frameIssues });
|
||||
} else {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue