shots: Mac App Store screenshots (mac size, APP_DESKTOP to the macOS version), a landscape layout (Mac window or bare; Mac, Play Chromebook, Play tablets), safe margins in every template checked by shots check from the render's layout boxes, a contact sheet timeout that scales with images, sizes and load; pitfall 80

This commit is contained in:
CRTkafa 2026-10-20 03:29:19 +07:00
parent 30488c512d
commit e59539fec8
26 changed files with 532 additions and 67 deletions

View file

@ -3,7 +3,9 @@
// storebound apple screenshots [--src store/renders] [--only en-US,tr] [--jobs 4] [--variant b] [--dry]
// Source per language: <renders>/<language id>/ios69/NN.png (6.9", 1320 x 2868 -> APP_IPHONE_67),
// ios65/NN.png (6.5", 1284 x 2778 -> APP_IPHONE_65) and, only with apple.ipad, ipad13/NN.png (13" iPad, 2064 x 2752 ->
// APP_IPAD_PRO_3GEN_129: the API has no "13 inch" value, the 13" display set keeps the 3rd gen 12.9" name). Up to 10
// APP_IPAD_PRO_3GEN_129: the API has no "13 inch" value, the 13" display set keeps the 3rd gen 12.9" name). With
// --platform macos (or when only the macOS version is editable) and apple.mac: mac/NN.png (2880 x 1800 -> APP_DESKTOP)
// to the macOS version. iPhone Duo sets are skipped with the console deep link (no API display type yet). Up to 10
// per set. --variant <id> uploads <renders>/<id>/<lang>/... instead (an A/B variant from shots.variants, e.g. for a
// Product Page Optimization treatment you then set up in App Store Connect). Apple processes uploads asynchronously; failed
// ones are reported at the end. Long: run in the background with a log file.
@ -29,10 +31,13 @@ export const IPAD_DISPLAY: Record<string, string> = { ipad13: 'APP_IPAD_PRO_3GEN
/** 6.3" iPhone (Dynamic Island, medium display), uploaded only when apple.iphoneMedium is true. */
export const MEDIUM_DISPLAY: Record<string, string> = { 'iphone-medium': 'APP_IPHONE_61' };
/** Display sets this project uploads: the iPhone sets, plus the 6.3" set with apple.iphoneMedium and the 13" iPad set
* with apple.ipad. */
export const displaySets = (apple: { ipad?: boolean; iphoneMedium?: boolean; iphoneDuo?: boolean } | undefined): Record<string, string> =>
({ ...DISPLAY, ...(apple?.iphoneMedium ? MEDIUM_DISPLAY : {}), ...(apple?.ipad ? IPAD_DISPLAY : {}),
/** Mac App Store set (APP_DESKTOP, 16:10), uploaded to the macOS version only, with apple.mac. */
export const MAC_DISPLAY: Record<string, string> = { mac: 'APP_DESKTOP' };
/** Display sets this project uploads to a version of `platform`: macOS -> the Mac set (apple.mac); iOS (default) -> the
* iPhone sets, plus the 6.3" set with apple.iphoneMedium and the 13" iPad set with apple.ipad. */
export const displaySets = (apple: { ipad?: boolean; iphoneMedium?: boolean; iphoneDuo?: boolean; mac?: boolean } | undefined, platform: string = 'IOS'): Record<string, string> =>
platform === 'MAC_OS' ? (apple?.mac ? MAC_DISPLAY : {}) : ({ ...DISPLAY, ...(apple?.iphoneMedium ? MEDIUM_DISPLAY : {}), ...(apple?.ipad ? IPAD_DISPLAY : {}),
...(apple?.iphoneDuo ? Object.fromEntries(Object.entries(IPHONE_DUO_DISPLAY).filter((e): e is [string, string] => !!e[1])) : {}) });
/** iPhone Duo size keys `apple screenshots` skips (apple.iphoneDuo on, no API display type yet), with the message. */
@ -89,8 +94,10 @@ export async function run(ctx: Ctx): Promise<void> {
const version = await c.editableVersion(platform);
if (!version) throw new Error(`no editable ${platformLabel(platform)} App Store version (run apple listing --version x.y.z first)`);
log('apple', `screenshots for version ${versionLabel(version)}`);
const duoSkip = skippedDuoSets(config.apple);
const duoSkip = platform === 'MAC_OS' ? null : skippedDuoSets(config.apple);
if (duoSkip) warn('apple', duoSkip.msg);
if (platform === 'MAC_OS' && !config.apple?.mac) throw new Error('macOS version: set apple.mac: true and render the mac size (storebound shots render --size mac) first');
if (platform !== 'MAC_OS' && config.apple?.mac) log('apple', 'Mac screenshots (mac/, APP_DESKTOP) go to the macOS version: run apple screenshots --platform macos');
const langOf = byAppleLocale(config.languages);
const locs = await c.all(`/v1/appStoreVersions/${version.id}/appStoreVersionLocalizations?limit=50`);
@ -105,7 +112,7 @@ export async function run(ctx: Ctx): Promise<void> {
if (!lang) { warn('apple', `${locale}: no language with apple: "${locale}", skipped`); continue; }
tasks.push(async () => {
const sets = await c.all(`/v1/appStoreVersionLocalizations/${loc.id}/appScreenshotSets`);
for (const [dir, type] of Object.entries(displaySets(config.apple))) {
for (const [dir, type] of Object.entries(displaySets(config.apple, platform))) {
const d = join(src, lang.id, dir);
const files = numberedImages(d).filter(f => f.endsWith('.png')).slice(0, 10);
if (!existsSync(d) || !files.length) { warn('apple', `${locale}: nothing in ${d}, skipped`); continue; }