codeplay-common 4.6.6 → 4.6.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/files/generate-ai-promo-video.js +20 -15
- package/files/take-screen-image.js +387 -23
- package/package.json +1 -1
|
@@ -86,7 +86,8 @@ const analysisFrameDirectory = path.join(temporaryDirectory, 'ai-analysis');
|
|
|
86
86
|
const endCardBannerPath = path.join(outputVideoDirectory, `${appFilePrefix}-ai-end-card-banner.png`);
|
|
87
87
|
const promoCopySchemaPath = path.join(temporaryDirectory, 'promo-copy-schema.json');
|
|
88
88
|
const promoCopyResultPath = path.join(temporaryDirectory, 'promo-copy-result.json');
|
|
89
|
-
const ffmpegFallbackDirectory = String(process.env.FFMPEG_BIN || '').trim()
|
|
89
|
+
const ffmpegFallbackDirectory = String(process.env.FFMPEG_BIN || '').trim()
|
|
90
|
+
|| path.join(projectDirectory, 'tools', 'ffmpeg');
|
|
90
91
|
const minimumSceneCount = 7;
|
|
91
92
|
const minimumSourceDuration = 12;
|
|
92
93
|
let expectedDuration = 34;
|
|
@@ -208,15 +209,15 @@ async function ensureEndCardBanner() {
|
|
|
208
209
|
+ '- Keep all logos and badge text sharp, correctly proportioned, and completely visible.\n'
|
|
209
210
|
+ '- Match the background colors and visual style to the actual app icon.\n'
|
|
210
211
|
+ '- Create a polished, professional background related to the app’s purpose.\n'
|
|
211
|
-
+ '- Keep the background subtle so it does not compete with the icon, title, feature text, or download badges.\n'
|
|
212
|
+
+ '- Keep the background subtle so it does not compete with the icon, title, feature text, or download badges.\n'
|
|
212
213
|
+ '- Use a balanced landscape composition:\n'
|
|
213
214
|
+ ' - Large actual app icon on the left.\n'
|
|
214
215
|
+ ' - App Store badge followed by Google Play badge on the right.\n'
|
|
215
216
|
+ ' - Maintain comfortable margins and clear space around every element.\n'
|
|
216
|
-
+ '- Add the application title prominently.\n'
|
|
217
|
-
+ '- Add concise promotional text describing the application’s actual features, presenting each feature one by one.\n'
|
|
218
|
-
+ '- Keep the title and feature text sharp, readable, accurate, and visually balanced with the icon and download badges.\n'
|
|
219
|
-
+ '- Do not invent unsupported features or add unrelated marketing text.\n'
|
|
217
|
+
+ '- Add the application title prominently.\n'
|
|
218
|
+
+ '- Add concise promotional text describing the application’s actual features, presenting each feature one by one.\n'
|
|
219
|
+
+ '- Keep the title and feature text sharp, readable, accurate, and visually balanced with the icon and download badges.\n'
|
|
220
|
+
+ '- Do not invent unsupported features or add unrelated marketing text.\n'
|
|
220
221
|
+ '- Do not add watermarks, fake UI, generated symbols, or unrelated decorations.\n'
|
|
221
222
|
+ '- Rounded corners and a subtle shadow may be applied around the app icon, but its internal artwork must remain unchanged.\n'
|
|
222
223
|
+ '- Use image generation only for the background.\n'
|
|
@@ -467,12 +468,10 @@ function commandExists(command) {
|
|
|
467
468
|
}
|
|
468
469
|
|
|
469
470
|
function getExecutable(command) {
|
|
470
|
-
if (commandExists(command)) return command;
|
|
471
|
-
|
|
472
|
-
if (!ffmpegFallbackDirectory) return '';
|
|
473
471
|
const executableName = process.platform === 'win32' ? `${command}.exe` : command;
|
|
474
472
|
const fallbackPath = path.join(ffmpegFallbackDirectory, executableName);
|
|
475
|
-
|
|
473
|
+
if (fs.existsSync(fallbackPath)) return fallbackPath;
|
|
474
|
+
return commandExists(command) ? command : '';
|
|
476
475
|
}
|
|
477
476
|
|
|
478
477
|
function detectRedundantStaticRanges(ffmpegCommand, inputPath, sourceDuration) {
|
|
@@ -641,7 +640,7 @@ function resolvePromoLayout(requestedLayout) {
|
|
|
641
640
|
}
|
|
642
641
|
|
|
643
642
|
function getPythonExecutable() {
|
|
644
|
-
for (const candidate of ['py', 'python']) {
|
|
643
|
+
for (const candidate of ['py', 'python', 'python3']) {
|
|
645
644
|
const result = spawnSync(candidate, ['-m', 'edge_tts', '--version'], {
|
|
646
645
|
encoding: 'utf8',
|
|
647
646
|
windowsHide: true,
|
|
@@ -788,7 +787,7 @@ function getCodexCandidates() {
|
|
|
788
787
|
const configuredCodexPath = String(process.env.CODEX_BIN || '').trim();
|
|
789
788
|
if (configuredCodexPath) candidates.unshift(configuredCodexPath);
|
|
790
789
|
|
|
791
|
-
if (process.platform === 'win32') {
|
|
790
|
+
if (process.platform === 'win32' || process.platform === 'darwin' || process.platform === 'linux') {
|
|
792
791
|
const userProfile = process.env.USERPROFILE || process.env.HOME;
|
|
793
792
|
const extensionRoots = [
|
|
794
793
|
path.join(userProfile || '', '.vscode', 'extensions'),
|
|
@@ -796,7 +795,7 @@ function getCodexCandidates() {
|
|
|
796
795
|
path.join(userProfile || '', '.vscode-server', 'extensions'),
|
|
797
796
|
path.join(userProfile || '', '.vscode-server-insiders', 'extensions'),
|
|
798
797
|
];
|
|
799
|
-
const executableNames = ['codex.exe', 'codex'];
|
|
798
|
+
const executableNames = process.platform === 'win32' ? ['codex.exe', 'codex'] : ['codex'];
|
|
800
799
|
|
|
801
800
|
for (const extensionRoot of extensionRoots) {
|
|
802
801
|
if (!fs.existsSync(extensionRoot)) continue;
|
|
@@ -916,7 +915,7 @@ Rules:
|
|
|
916
915
|
- footer_keywords must contain exactly 3 distinct, natural app-search phrases, each 2-24 characters.
|
|
917
916
|
- Generate a YouTube upload title and description from the same verified features.
|
|
918
917
|
- YouTube title: 40-100 characters, include ${JSON.stringify(appName)} and one strong natural search phrase, without clickbait.
|
|
919
|
-
- YouTube description: 350-1500 characters with readable paragraphs. Put the clearest app summary and primary search phrase in the opening two lines, explain the demonstrated features, add a simple call to action, and finish with exactly 3 relevant hashtags on
|
|
918
|
+
- YouTube description: 350-1500 characters with readable paragraphs. Put the clearest app summary and primary search phrase in the opening two lines, explain the demonstrated features, add a simple call to action, then insert a newline and finish with exactly 3 relevant hashtags on that separate final line. The hashtags must not be appended to the preceding paragraph.
|
|
920
919
|
- Do not add download links or URLs; the script will append the verified Google Play URL built from the configured package ID. Do not add chapter timestamps or unsupported availability claims.
|
|
921
920
|
- Keep YouTube text natural and useful; do not repeat keywords unnaturally.
|
|
922
921
|
- Avoid prices, ratings, awards, competitor names, unsupported superlatives, and words such as best, guaranteed, or free.
|
|
@@ -1027,7 +1026,13 @@ function validatePromoCopy(value) {
|
|
|
1027
1026
|
}
|
|
1028
1027
|
|
|
1029
1028
|
const youtubeTitle = cleanGeneratedString(value.youtube.title);
|
|
1030
|
-
|
|
1029
|
+
let youtubeDescription = cleanGeneratedDescription(value.youtube.description);
|
|
1030
|
+
const trailingHashtagGroup = youtubeDescription.match(
|
|
1031
|
+
/(?:^|\s)(#[\p{L}\p{N}_]+\s+#[\p{L}\p{N}_]+\s+#[\p{L}\p{N}_]+)\s*$/u,
|
|
1032
|
+
);
|
|
1033
|
+
if (trailingHashtagGroup) {
|
|
1034
|
+
youtubeDescription = `${youtubeDescription.slice(0, trailingHashtagGroup.index).trimEnd()}\n${trailingHashtagGroup[1]}`;
|
|
1035
|
+
}
|
|
1031
1036
|
if (youtubeTitle.length < 40 || youtubeTitle.length > 100) {
|
|
1032
1037
|
throw new Error('YouTube title must be 40-100 characters.');
|
|
1033
1038
|
}
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
//node take-screen-image.js --delete 3
|
|
2
2
|
//node take-screen-image.js --delete 3,4,5
|
|
3
|
-
|
|
3
|
+
//node take-screen-image.js --profiles vivo-store //only taking screenshot for vivo-store - Not only vivo, we can do this to all of our store images.
|
|
4
|
+
//node take-screen-image.js --vivo-existing //Used if we already took the screenshot and those vivo screenshots are not there then execute this to resize the screenshot for vivo store.
|
|
5
|
+
//node take-screen-image.js --oppo-existing //Used if we already took the screenshot and those oppo screenshots are not there then execute this to resize the screenshot for Oppo store.
|
|
4
6
|
|
|
5
7
|
|
|
6
8
|
const fs = require('node:fs');
|
|
7
9
|
const path = require('node:path');
|
|
8
10
|
const readline = require('node:readline/promises');
|
|
9
11
|
const { spawn, spawnSync } = require('node:child_process');
|
|
12
|
+
const sharp = require('sharp');
|
|
10
13
|
|
|
11
14
|
const screenshotDirectory = path.join(__dirname, 'Auto-Screenshot', 'Store-image-Screenshot');
|
|
12
15
|
const inFrameDirectory = path.join(__dirname, 'Auto-Screenshot', 'Store-image-Frame');
|
|
@@ -32,6 +35,9 @@ const playstoreAssetDefinitions = [
|
|
|
32
35
|
{ fileName: 'feature-graphic-1024x500.png', width: 1024, height: 500 },
|
|
33
36
|
];
|
|
34
37
|
const browserExecutablePaths = [
|
|
38
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
39
|
+
'/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
|
|
40
|
+
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
|
35
41
|
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
|
36
42
|
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
|
|
37
43
|
'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe',
|
|
@@ -50,6 +56,52 @@ const deviceProfiles = [
|
|
|
50
56
|
orientationSensitive: true,
|
|
51
57
|
mobile: true,
|
|
52
58
|
},
|
|
59
|
+
{
|
|
60
|
+
name: 'vivo-store',
|
|
61
|
+
label: 'Vivo Store portrait screenshot',
|
|
62
|
+
outputDirectory: 'Vivo-Store',
|
|
63
|
+
frameType: 'android-phone',
|
|
64
|
+
width: 360,
|
|
65
|
+
height: 720,
|
|
66
|
+
deviceScaleFactor: 3,
|
|
67
|
+
framedWidth: 1080,
|
|
68
|
+
framedHeight: 2160,
|
|
69
|
+
orientationSensitive: false,
|
|
70
|
+
mobile: true,
|
|
71
|
+
storeRequirements: {
|
|
72
|
+
maxFileSizeBytes: 5 * 1024 * 1024,
|
|
73
|
+
minPixels: 320,
|
|
74
|
+
maxPixels: 3840,
|
|
75
|
+
minAspectRatio: 0.5,
|
|
76
|
+
maxAspectRatio: 2,
|
|
77
|
+
minImages: 4,
|
|
78
|
+
maxImages: 8,
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: 'oppo-store',
|
|
83
|
+
label: 'Oppo Store portrait screenshot',
|
|
84
|
+
outputDirectory: 'Oppo-Store',
|
|
85
|
+
frameType: 'android-phone',
|
|
86
|
+
width: 360,
|
|
87
|
+
height: 640,
|
|
88
|
+
deviceScaleFactor: 3,
|
|
89
|
+
framedWidth: 1080,
|
|
90
|
+
framedHeight: 1920,
|
|
91
|
+
orientationSensitive: false,
|
|
92
|
+
mobile: true,
|
|
93
|
+
outputFormat: 'jpeg',
|
|
94
|
+
outputExtension: 'jpg',
|
|
95
|
+
storeRequirements: {
|
|
96
|
+
maxFileSizeBytes: 3 * 1024 * 1024,
|
|
97
|
+
minPixels: 320,
|
|
98
|
+
maxPixels: 1920,
|
|
99
|
+
minAspectRatio: 0.5,
|
|
100
|
+
maxAspectRatio: 0.6,
|
|
101
|
+
minImages: 3,
|
|
102
|
+
maxImages: 5,
|
|
103
|
+
},
|
|
104
|
+
},
|
|
53
105
|
{
|
|
54
106
|
name: 'android-tablet-7-inch',
|
|
55
107
|
aliases: ['android-tablet'],
|
|
@@ -350,31 +402,33 @@ function getSelectedOutputDirectories(selectedProfiles) {
|
|
|
350
402
|
}
|
|
351
403
|
|
|
352
404
|
function getNextNumberedFileName(selectedProfiles) {
|
|
405
|
+
const extension = selectedProfiles[0]?.outputExtension || 'png';
|
|
353
406
|
const highestNumber = getSelectedOutputDirectories(selectedProfiles)
|
|
354
407
|
.filter((directory) => fs.existsSync(directory))
|
|
355
408
|
.reduce((highestAcrossDirectories, directory) => {
|
|
356
409
|
const highestInDirectory = fs.readdirSync(directory, { withFileTypes: true })
|
|
357
410
|
.filter((entry) => entry.isFile())
|
|
358
411
|
.reduce((highest, entry) => {
|
|
359
|
-
const match = entry.name.match(
|
|
412
|
+
const match = entry.name.match(new RegExp(`^(\\d+)\\.${extension}$`, 'i'));
|
|
360
413
|
return match ? Math.max(highest, Number(match[1])) : highest;
|
|
361
414
|
}, 0);
|
|
362
415
|
|
|
363
416
|
return Math.max(highestAcrossDirectories, highestInDirectory);
|
|
364
417
|
}, 0);
|
|
365
418
|
|
|
366
|
-
return `${highestNumber + 1}
|
|
419
|
+
return `${highestNumber + 1}.${extension}`;
|
|
367
420
|
}
|
|
368
421
|
|
|
369
422
|
function getUniqueFileName(prefix, selectedProfiles) {
|
|
423
|
+
const extension = selectedProfiles[0]?.outputExtension || 'png';
|
|
370
424
|
const outputDirectories = getSelectedOutputDirectories(selectedProfiles);
|
|
371
|
-
let fileName = `${prefix}
|
|
425
|
+
let fileName = `${prefix}.${extension}`;
|
|
372
426
|
let duplicateNumber = 2;
|
|
373
427
|
|
|
374
428
|
while (outputDirectories.some((directory) => (
|
|
375
429
|
fs.existsSync(path.join(directory, fileName))
|
|
376
430
|
))) {
|
|
377
|
-
fileName = `${prefix}-${duplicateNumber}
|
|
431
|
+
fileName = `${prefix}-${duplicateNumber}.${extension}`;
|
|
378
432
|
duplicateNumber += 1;
|
|
379
433
|
}
|
|
380
434
|
|
|
@@ -382,7 +436,7 @@ function getUniqueFileName(prefix, selectedProfiles) {
|
|
|
382
436
|
}
|
|
383
437
|
|
|
384
438
|
function getScreenshotFileName(fileNameInput, selectedProfiles) {
|
|
385
|
-
const inputWithoutExtension = fileNameInput.trim().replace(/\.png$/i, '');
|
|
439
|
+
const inputWithoutExtension = fileNameInput.trim().replace(/\.(png|jpe?g)$/i, '');
|
|
386
440
|
|
|
387
441
|
if (!inputWithoutExtension) {
|
|
388
442
|
return getNextNumberedFileName(selectedProfiles);
|
|
@@ -408,6 +462,8 @@ function parseArguments() {
|
|
|
408
462
|
samsungAssets: false,
|
|
409
463
|
dryRun: false,
|
|
410
464
|
frameExisting: false,
|
|
465
|
+
vivoExisting: false,
|
|
466
|
+
oppoExisting: false,
|
|
411
467
|
help: false,
|
|
412
468
|
list: false,
|
|
413
469
|
deleteRequested: false,
|
|
@@ -427,6 +483,10 @@ function parseArguments() {
|
|
|
427
483
|
options.dryRun = true;
|
|
428
484
|
} else if (argument === '--frame-existing') {
|
|
429
485
|
options.frameExisting = true;
|
|
486
|
+
} else if (argument === '--vivo-existing') {
|
|
487
|
+
options.vivoExisting = true;
|
|
488
|
+
} else if (argument === '--oppo-existing') {
|
|
489
|
+
options.oppoExisting = true;
|
|
430
490
|
} else if (argument === '--help' || argument === '-h') {
|
|
431
491
|
options.help = true;
|
|
432
492
|
} else if (argument === '--list') {
|
|
@@ -457,13 +517,20 @@ function printProfiles() {
|
|
|
457
517
|
const outputWidth = profile.width * profile.deviceScaleFactor;
|
|
458
518
|
const outputHeight = profile.height * profile.deviceScaleFactor;
|
|
459
519
|
const framedSize = profile.framedWidth
|
|
460
|
-
? `, framed
|
|
520
|
+
? `, framed ${(profile.outputFormat || 'png').toUpperCase()} ${profile.framedWidth}x${profile.framedHeight}`
|
|
461
521
|
: '';
|
|
462
522
|
console.log(
|
|
463
523
|
`${profile.name.padEnd(15)} ${profile.label} `
|
|
464
524
|
+ `(folder ${profile.outputDirectory}, viewport ${profile.width}x${profile.height}, `
|
|
465
|
-
+
|
|
525
|
+
+ `${(profile.outputFormat || 'png').toUpperCase()} ${outputWidth}x${outputHeight}${framedSize})`,
|
|
466
526
|
);
|
|
527
|
+
if (profile.storeRequirements) {
|
|
528
|
+
console.log(
|
|
529
|
+
` Store limits: ${profile.storeRequirements.minImages}-${profile.storeRequirements.maxImages} images, `
|
|
530
|
+
+ `max ${profile.storeRequirements.maxFileSizeBytes / (1024 * 1024)} MB, `
|
|
531
|
+
+ `width/height ${profile.storeRequirements.minAspectRatio}-${profile.storeRequirements.maxAspectRatio}:1`,
|
|
532
|
+
);
|
|
533
|
+
}
|
|
467
534
|
}
|
|
468
535
|
}
|
|
469
536
|
|
|
@@ -475,6 +542,8 @@ function printHelp() {
|
|
|
475
542
|
console.log(' node take-screen-image.js --profiles iphone,ipad');
|
|
476
543
|
console.log(' node take-screen-image.js --prefix home --profiles android-phone,ipad');
|
|
477
544
|
console.log(' node take-screen-image.js --frame-existing');
|
|
545
|
+
console.log(' node take-screen-image.js --vivo-existing');
|
|
546
|
+
console.log(' node take-screen-image.js --oppo-existing');
|
|
478
547
|
console.log(' node take-screen-image.js --amazon-assets');
|
|
479
548
|
console.log(' node take-screen-image.js --samsung-assets');
|
|
480
549
|
console.log(' node take-screen-image.js --dry-run');
|
|
@@ -1151,9 +1220,84 @@ function getFrameAdornment(frameType) {
|
|
|
1151
1220
|
return '<div class="xr-status"></div>';
|
|
1152
1221
|
}
|
|
1153
1222
|
|
|
1154
|
-
function
|
|
1155
|
-
const screenshotDataUrl = `data:image
|
|
1156
|
-
const
|
|
1223
|
+
function createStoreDeviceFrameHtml(profile, screenshotBuffer, imageWidth, imageHeight, canvasWidth, canvasHeight, imageFormat) {
|
|
1224
|
+
const screenshotDataUrl = `data:image/${imageFormat === 'jpeg' ? 'jpeg' : 'png'};base64,${screenshotBuffer.toString('base64')}`;
|
|
1225
|
+
const screenWidth = Math.floor(Math.min(canvasWidth - 160, (canvasHeight - 180) * imageWidth / imageHeight));
|
|
1226
|
+
const accent = profile.name === 'oppo-store' ? '#76e4c2' : '#9daaff';
|
|
1227
|
+
|
|
1228
|
+
return `<!doctype html>
|
|
1229
|
+
<html>
|
|
1230
|
+
<head>
|
|
1231
|
+
<meta charset="utf-8" />
|
|
1232
|
+
<style>
|
|
1233
|
+
* { box-sizing: border-box; }
|
|
1234
|
+
html, body { width: 100%; height: 100%; margin: 0; overflow: hidden; }
|
|
1235
|
+
body {
|
|
1236
|
+
display: grid;
|
|
1237
|
+
place-items: center;
|
|
1238
|
+
background: radial-gradient(ellipse at 50% 43%, #24344d 0%, #101b2d 47%, #070e1a 100%);
|
|
1239
|
+
}
|
|
1240
|
+
body::before {
|
|
1241
|
+
content: '';
|
|
1242
|
+
position: absolute;
|
|
1243
|
+
width: 82%;
|
|
1244
|
+
height: 62%;
|
|
1245
|
+
border-radius: 50%;
|
|
1246
|
+
background: ${accent};
|
|
1247
|
+
opacity: 0.11;
|
|
1248
|
+
filter: blur(90px);
|
|
1249
|
+
}
|
|
1250
|
+
.device {
|
|
1251
|
+
position: relative;
|
|
1252
|
+
z-index: 1;
|
|
1253
|
+
width: ${screenWidth + 48}px;
|
|
1254
|
+
padding: 34px 18px 20px;
|
|
1255
|
+
border: 6px solid #8d99ac;
|
|
1256
|
+
border-radius: 48px;
|
|
1257
|
+
background: linear-gradient(135deg, #404c60, #111925 35%, #070c15 72%, #59657a);
|
|
1258
|
+
box-shadow: 0 38px 85px rgba(0, 0, 0, 0.58), 0 0 0 2px rgba(255, 255, 255, 0.19) inset;
|
|
1259
|
+
}
|
|
1260
|
+
.screen {
|
|
1261
|
+
width: ${screenWidth}px;
|
|
1262
|
+
background: #fff;
|
|
1263
|
+
box-shadow: 0 0 0 2px #03060c;
|
|
1264
|
+
}
|
|
1265
|
+
.screen img { display: block; width: 100%; height: auto; }
|
|
1266
|
+
.camera {
|
|
1267
|
+
position: absolute;
|
|
1268
|
+
top: 11px;
|
|
1269
|
+
left: 50%;
|
|
1270
|
+
width: 12px;
|
|
1271
|
+
height: 12px;
|
|
1272
|
+
transform: translateX(-50%);
|
|
1273
|
+
border-radius: 50%;
|
|
1274
|
+
background: radial-gradient(circle at 32% 30%, #547496, #070b12 48%, #000 80%);
|
|
1275
|
+
box-shadow: 0 0 0 3px #1b2534;
|
|
1276
|
+
}
|
|
1277
|
+
.side-button {
|
|
1278
|
+
position: absolute;
|
|
1279
|
+
top: 19%;
|
|
1280
|
+
right: -10px;
|
|
1281
|
+
width: 4px;
|
|
1282
|
+
height: 66px;
|
|
1283
|
+
border-radius: 0 3px 3px 0;
|
|
1284
|
+
background: #79869a;
|
|
1285
|
+
}
|
|
1286
|
+
</style>
|
|
1287
|
+
</head>
|
|
1288
|
+
<body>
|
|
1289
|
+
<main class="device">
|
|
1290
|
+
<div class="camera"></div>
|
|
1291
|
+
<div class="screen"><img src="${screenshotDataUrl}" /></div>
|
|
1292
|
+
<div class="side-button"></div>
|
|
1293
|
+
</main>
|
|
1294
|
+
</body>
|
|
1295
|
+
</html>`;
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
function createDeviceFrameHtml(profile, screenshotBuffer, imageWidth = profile.width, imageHeight = profile.height, imageFormat = 'png') {
|
|
1299
|
+
const screenshotDataUrl = `data:image/${imageFormat === 'jpeg' ? 'jpeg' : 'png'};base64,${screenshotBuffer.toString('base64')}`;
|
|
1300
|
+
const aspectRatio = `${imageWidth} / ${imageHeight}`;
|
|
1157
1301
|
|
|
1158
1302
|
return `<!doctype html>
|
|
1159
1303
|
<html>
|
|
@@ -1197,20 +1341,21 @@ function createDeviceFrameHtml(profile, screenshotBuffer) {
|
|
|
1197
1341
|
.screen {
|
|
1198
1342
|
position: relative;
|
|
1199
1343
|
width: 100%;
|
|
1200
|
-
height:
|
|
1344
|
+
height: 87%;
|
|
1201
1345
|
overflow: hidden;
|
|
1202
|
-
background: #
|
|
1346
|
+
background: #f5f7fc;
|
|
1203
1347
|
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.95) inset;
|
|
1204
1348
|
}
|
|
1205
1349
|
.screen img {
|
|
1206
1350
|
display: block;
|
|
1207
1351
|
width: 100%;
|
|
1208
1352
|
height: 100%;
|
|
1209
|
-
object-fit:
|
|
1353
|
+
object-fit: contain;
|
|
1354
|
+
background: #f5f7fc;
|
|
1210
1355
|
}
|
|
1211
1356
|
.device.android-phone,
|
|
1212
1357
|
.device.iphone {
|
|
1213
|
-
height:
|
|
1358
|
+
height: 100%;
|
|
1214
1359
|
padding: 1.25%;
|
|
1215
1360
|
border-radius: clamp(24px, 8vw, 70px);
|
|
1216
1361
|
}
|
|
@@ -2037,9 +2182,15 @@ async function captureFramedScreenshot(
|
|
|
2037
2182
|
return false;
|
|
2038
2183
|
}
|
|
2039
2184
|
|
|
2040
|
-
const
|
|
2041
|
-
|
|
2042
|
-
|
|
2185
|
+
const originalImageMetadata = profile.storeRequirements
|
|
2186
|
+
? await sharp(screenshotBuffer).metadata()
|
|
2187
|
+
: null;
|
|
2188
|
+
const frameDimensions = originalImageMetadata?.width && originalImageMetadata?.height
|
|
2189
|
+
? { width: originalImageMetadata.width, height: originalImageMetadata.height }
|
|
2190
|
+
: null;
|
|
2191
|
+
const hasCustomFrameSize = Boolean(frameDimensions || (profile.framedWidth && profile.framedHeight));
|
|
2192
|
+
const frameWidth = frameDimensions?.width || profile.framedWidth || profile.width;
|
|
2193
|
+
const frameHeight = frameDimensions?.height || profile.framedHeight || profile.height;
|
|
2043
2194
|
const frameScaleFactor = hasCustomFrameSize ? 1 : profile.deviceScaleFactor;
|
|
2044
2195
|
|
|
2045
2196
|
await client.send('Emulation.setDeviceMetricsOverride', {
|
|
@@ -2056,7 +2207,12 @@ async function captureFramedScreenshot(
|
|
|
2056
2207
|
const frameTree = await client.send('Page.getFrameTree');
|
|
2057
2208
|
await client.send('Page.setDocumentContent', {
|
|
2058
2209
|
frameId: frameTree.frameTree.frame.id,
|
|
2059
|
-
html:
|
|
2210
|
+
html: profile.name === 'vivo-store' || profile.name === 'oppo-store'
|
|
2211
|
+
? createStoreDeviceFrameHtml(
|
|
2212
|
+
profile, screenshotBuffer, originalImageMetadata.width, originalImageMetadata.height,
|
|
2213
|
+
frameWidth, frameHeight, originalImageMetadata.format,
|
|
2214
|
+
)
|
|
2215
|
+
: createDeviceFrameHtml(profile, screenshotBuffer, frameWidth, frameHeight),
|
|
2060
2216
|
});
|
|
2061
2217
|
await client.send('Runtime.evaluate', {
|
|
2062
2218
|
expression: `new Promise((resolve) => {
|
|
@@ -2073,21 +2229,52 @@ async function captureFramedScreenshot(
|
|
|
2073
2229
|
});
|
|
2074
2230
|
|
|
2075
2231
|
const framedScreenshot = await client.send('Page.captureScreenshot', {
|
|
2076
|
-
format: 'png',
|
|
2232
|
+
format: profile.outputFormat || 'png',
|
|
2233
|
+
quality: profile.outputFormat === 'jpeg' ? 90 : undefined,
|
|
2077
2234
|
fromSurface: true,
|
|
2078
2235
|
captureBeyondViewport: false,
|
|
2079
2236
|
});
|
|
2080
2237
|
|
|
2238
|
+
const framedScreenshotBuffer = Buffer.from(framedScreenshot.data, 'base64');
|
|
2239
|
+
if (!frameDimensions) {
|
|
2240
|
+
validateStoreImage(profile, frameWidth, frameHeight, framedScreenshotBuffer);
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2081
2243
|
fs.mkdirSync(framedProfileDirectory, { recursive: true });
|
|
2082
2244
|
fs.writeFileSync(
|
|
2083
2245
|
framedScreenshotPath,
|
|
2084
|
-
|
|
2085
|
-
{ flag: 'wx' },
|
|
2246
|
+
framedScreenshotBuffer,
|
|
2247
|
+
{ flag: skipExisting ? 'wx' : 'w' },
|
|
2086
2248
|
);
|
|
2087
2249
|
console.log(`Saved framed ${profile.label}: ${framedScreenshotPath}`);
|
|
2088
2250
|
return true;
|
|
2089
2251
|
}
|
|
2090
2252
|
|
|
2253
|
+
function validateStoreImage(profile, width, height, imageBuffer) {
|
|
2254
|
+
if (!profile.storeRequirements) return;
|
|
2255
|
+
|
|
2256
|
+
const {
|
|
2257
|
+
maxFileSizeBytes, minPixels, maxPixels, minAspectRatio, maxAspectRatio,
|
|
2258
|
+
} = profile.storeRequirements;
|
|
2259
|
+
const aspectRatio = width / height;
|
|
2260
|
+
|
|
2261
|
+
if (
|
|
2262
|
+
width < minPixels || height < minPixels
|
|
2263
|
+
|| width > maxPixels || height > maxPixels
|
|
2264
|
+
|| aspectRatio < minAspectRatio || aspectRatio > maxAspectRatio
|
|
2265
|
+
) {
|
|
2266
|
+
throw new Error(
|
|
2267
|
+
`${profile.label} must be between ${minPixels}px and ${maxPixels}px with aspect ratio <= ${maxAspectRatio}:1; received ${width}x${height}.`,
|
|
2268
|
+
);
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
if (imageBuffer.length > maxFileSizeBytes) {
|
|
2272
|
+
throw new Error(
|
|
2273
|
+
`${profile.label} exceeds ${maxFileSizeBytes / (1024 * 1024)} MB: ${Math.ceil(imageBuffer.length / (1024 * 1024))} MB.`,
|
|
2274
|
+
);
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2091
2278
|
async function captureProfile(client, snapshot, profile, screenshotFileName) {
|
|
2092
2279
|
await client.send('Emulation.setDeviceMetricsOverride', {
|
|
2093
2280
|
width: profile.width,
|
|
@@ -2115,7 +2302,8 @@ async function captureProfile(client, snapshot, profile, screenshotFileName) {
|
|
|
2115
2302
|
}
|
|
2116
2303
|
|
|
2117
2304
|
const screenshot = await client.send('Page.captureScreenshot', {
|
|
2118
|
-
format: 'png',
|
|
2305
|
+
format: profile.outputFormat || 'png',
|
|
2306
|
+
quality: profile.outputFormat === 'jpeg' ? 90 : undefined,
|
|
2119
2307
|
fromSurface: true,
|
|
2120
2308
|
captureBeyondViewport: false,
|
|
2121
2309
|
});
|
|
@@ -2123,6 +2311,12 @@ async function captureProfile(client, snapshot, profile, screenshotFileName) {
|
|
|
2123
2311
|
const profileDirectory = path.join(screenshotDirectory, profile.outputDirectory);
|
|
2124
2312
|
const screenshotPath = path.join(profileDirectory, screenshotFileName);
|
|
2125
2313
|
const screenshotBuffer = Buffer.from(screenshot.data, 'base64');
|
|
2314
|
+
validateStoreImage(
|
|
2315
|
+
profile,
|
|
2316
|
+
profile.width * profile.deviceScaleFactor,
|
|
2317
|
+
profile.height * profile.deviceScaleFactor,
|
|
2318
|
+
screenshotBuffer,
|
|
2319
|
+
);
|
|
2126
2320
|
|
|
2127
2321
|
fs.mkdirSync(profileDirectory, { recursive: true });
|
|
2128
2322
|
fs.writeFileSync(screenshotPath, screenshotBuffer, { flag: 'wx' });
|
|
@@ -2142,6 +2336,22 @@ async function captureScreenshotSet(
|
|
|
2142
2336
|
selectedProfiles,
|
|
2143
2337
|
fileNameInput,
|
|
2144
2338
|
) {
|
|
2339
|
+
const storeProfile = selectedProfiles.find((profile) => profile.storeRequirements);
|
|
2340
|
+
if (storeProfile) {
|
|
2341
|
+
const profileDirectory = path.join(screenshotDirectory, storeProfile.outputDirectory);
|
|
2342
|
+
const existingImageCount = fs.existsSync(profileDirectory)
|
|
2343
|
+
? fs.readdirSync(profileDirectory, { withFileTypes: true })
|
|
2344
|
+
.filter((entry) => entry.isFile() && /\.(png|jpe?g|webp)$/i.test(entry.name))
|
|
2345
|
+
.length
|
|
2346
|
+
: 0;
|
|
2347
|
+
|
|
2348
|
+
if (existingImageCount >= storeProfile.storeRequirements.maxImages) {
|
|
2349
|
+
throw new Error(
|
|
2350
|
+
`${storeProfile.label} already contains ${existingImageCount} images; the store allows a maximum of ${storeProfile.storeRequirements.maxImages}.`,
|
|
2351
|
+
);
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2145
2355
|
console.log('Capturing the current rendered page and resolved native data...');
|
|
2146
2356
|
const snapshot = await capturePageSnapshot(deviceClient);
|
|
2147
2357
|
const screenshotFileName = getScreenshotFileName(fileNameInput, selectedProfiles);
|
|
@@ -2210,6 +2420,148 @@ async function frameExistingScreenshots(selectedProfiles) {
|
|
|
2210
2420
|
}
|
|
2211
2421
|
}
|
|
2212
2422
|
|
|
2423
|
+
async function createVivoAssetsFromExistingScreenshots() {
|
|
2424
|
+
const vivoProfile = deviceProfiles.find((profile) => profile.name === 'vivo-store');
|
|
2425
|
+
const sourceDirectory = path.join(screenshotDirectory, 'Android');
|
|
2426
|
+
const outputDirectory = path.join(screenshotDirectory, vivoProfile.outputDirectory);
|
|
2427
|
+
const sourceFiles = fs.existsSync(sourceDirectory)
|
|
2428
|
+
? fs.readdirSync(sourceDirectory, { withFileTypes: true })
|
|
2429
|
+
.filter((entry) => entry.isFile() && /\.png$/i.test(entry.name))
|
|
2430
|
+
.map((entry) => entry.name)
|
|
2431
|
+
.sort((first, second) => first.localeCompare(second, undefined, { numeric: true }))
|
|
2432
|
+
: [];
|
|
2433
|
+
|
|
2434
|
+
if (!sourceFiles.length) {
|
|
2435
|
+
throw new Error(`No existing PNG screenshots found in ${sourceDirectory}.`);
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
const { maxImages } = vivoProfile.storeRequirements;
|
|
2439
|
+
const selectedSourceFiles = sourceFiles.slice(0, maxImages);
|
|
2440
|
+
if (sourceFiles.length > maxImages) {
|
|
2441
|
+
console.warn(
|
|
2442
|
+
`Vivo Store allows a maximum of ${maxImages} images; using the first ${maxImages} of ${sourceFiles.length} existing Android screenshots.`,
|
|
2443
|
+
);
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2446
|
+
let replayBrowser;
|
|
2447
|
+
let replayClient;
|
|
2448
|
+
let createdCount = 0;
|
|
2449
|
+
|
|
2450
|
+
try {
|
|
2451
|
+
replayBrowser = await launchReplayBrowser();
|
|
2452
|
+
replayClient = await createReplayPage(replayBrowser);
|
|
2453
|
+
fs.mkdirSync(outputDirectory, { recursive: true });
|
|
2454
|
+
|
|
2455
|
+
for (const screenshotFileName of selectedSourceFiles) {
|
|
2456
|
+
const sourcePath = path.join(sourceDirectory, screenshotFileName);
|
|
2457
|
+
const outputPath = path.join(outputDirectory, screenshotFileName);
|
|
2458
|
+
const sourceBuffer = fs.readFileSync(sourcePath);
|
|
2459
|
+
const vivoBuffer = await sharp(sourceBuffer)
|
|
2460
|
+
.resize(vivoProfile.width * vivoProfile.deviceScaleFactor, vivoProfile.height * vivoProfile.deviceScaleFactor, {
|
|
2461
|
+
fit: 'contain',
|
|
2462
|
+
background: '#f5f7fc',
|
|
2463
|
+
})
|
|
2464
|
+
.png()
|
|
2465
|
+
.toBuffer();
|
|
2466
|
+
|
|
2467
|
+
validateStoreImage(
|
|
2468
|
+
vivoProfile,
|
|
2469
|
+
vivoProfile.width * vivoProfile.deviceScaleFactor,
|
|
2470
|
+
vivoProfile.height * vivoProfile.deviceScaleFactor,
|
|
2471
|
+
vivoBuffer,
|
|
2472
|
+
);
|
|
2473
|
+
|
|
2474
|
+
fs.writeFileSync(outputPath, vivoBuffer);
|
|
2475
|
+
createdCount += 1;
|
|
2476
|
+
console.log(`Saved Vivo frameless screenshot: ${outputPath}`);
|
|
2477
|
+
|
|
2478
|
+
await captureFramedScreenshot(
|
|
2479
|
+
replayClient,
|
|
2480
|
+
sourceBuffer,
|
|
2481
|
+
vivoProfile,
|
|
2482
|
+
screenshotFileName,
|
|
2483
|
+
false,
|
|
2484
|
+
);
|
|
2485
|
+
}
|
|
2486
|
+
} finally {
|
|
2487
|
+
if (replayClient) replayClient.close();
|
|
2488
|
+
if (replayBrowser) await closeReplayBrowser(replayBrowser);
|
|
2489
|
+
}
|
|
2490
|
+
|
|
2491
|
+
console.log(`Vivo assets created from existing screenshots: ${createdCount} frameless image(s).`);
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
async function createOppoAssetsFromExistingScreenshots() {
|
|
2495
|
+
const oppoProfile = deviceProfiles.find((profile) => profile.name === 'oppo-store');
|
|
2496
|
+
const sourceDirectory = path.join(screenshotDirectory, 'Android');
|
|
2497
|
+
const outputDirectory = path.join(screenshotDirectory, oppoProfile.outputDirectory);
|
|
2498
|
+
const sourceFiles = fs.existsSync(sourceDirectory)
|
|
2499
|
+
? fs.readdirSync(sourceDirectory, { withFileTypes: true })
|
|
2500
|
+
.filter((entry) => entry.isFile() && /\.png$/i.test(entry.name))
|
|
2501
|
+
.map((entry) => entry.name)
|
|
2502
|
+
.sort((first, second) => first.localeCompare(second, undefined, { numeric: true }))
|
|
2503
|
+
: [];
|
|
2504
|
+
|
|
2505
|
+
if (!sourceFiles.length) {
|
|
2506
|
+
throw new Error(`No existing PNG screenshots found in ${sourceDirectory}.`);
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2509
|
+
const { maxImages } = oppoProfile.storeRequirements;
|
|
2510
|
+
const selectedSourceFiles = sourceFiles.slice(0, maxImages);
|
|
2511
|
+
if (sourceFiles.length > maxImages) {
|
|
2512
|
+
console.warn(
|
|
2513
|
+
`Oppo Store allows a maximum of ${maxImages} images; using the first ${maxImages} of ${sourceFiles.length} existing Android screenshots.`,
|
|
2514
|
+
);
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
let replayBrowser;
|
|
2518
|
+
let replayClient;
|
|
2519
|
+
let createdCount = 0;
|
|
2520
|
+
|
|
2521
|
+
try {
|
|
2522
|
+
replayBrowser = await launchReplayBrowser();
|
|
2523
|
+
replayClient = await createReplayPage(replayBrowser);
|
|
2524
|
+
fs.mkdirSync(outputDirectory, { recursive: true });
|
|
2525
|
+
|
|
2526
|
+
for (const sourceFileName of selectedSourceFiles) {
|
|
2527
|
+
const outputFileName = `${path.basename(sourceFileName, path.extname(sourceFileName))}.jpg`;
|
|
2528
|
+
const sourceBuffer = fs.readFileSync(path.join(sourceDirectory, sourceFileName));
|
|
2529
|
+
const oppoBuffer = await sharp(sourceBuffer)
|
|
2530
|
+
.resize(oppoProfile.width * oppoProfile.deviceScaleFactor, oppoProfile.height * oppoProfile.deviceScaleFactor, {
|
|
2531
|
+
fit: 'contain',
|
|
2532
|
+
background: '#f5f7fc',
|
|
2533
|
+
})
|
|
2534
|
+
.jpeg({ quality: 90 })
|
|
2535
|
+
.toBuffer();
|
|
2536
|
+
const outputPath = path.join(outputDirectory, outputFileName);
|
|
2537
|
+
|
|
2538
|
+
validateStoreImage(
|
|
2539
|
+
oppoProfile,
|
|
2540
|
+
oppoProfile.width * oppoProfile.deviceScaleFactor,
|
|
2541
|
+
oppoProfile.height * oppoProfile.deviceScaleFactor,
|
|
2542
|
+
oppoBuffer,
|
|
2543
|
+
);
|
|
2544
|
+
|
|
2545
|
+
fs.writeFileSync(outputPath, oppoBuffer);
|
|
2546
|
+
createdCount += 1;
|
|
2547
|
+
console.log(`Saved Oppo frameless screenshot: ${outputPath}`);
|
|
2548
|
+
|
|
2549
|
+
await captureFramedScreenshot(
|
|
2550
|
+
replayClient,
|
|
2551
|
+
sourceBuffer,
|
|
2552
|
+
oppoProfile,
|
|
2553
|
+
outputFileName,
|
|
2554
|
+
false,
|
|
2555
|
+
);
|
|
2556
|
+
}
|
|
2557
|
+
} finally {
|
|
2558
|
+
if (replayClient) replayClient.close();
|
|
2559
|
+
if (replayBrowser) await closeReplayBrowser(replayBrowser);
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2562
|
+
console.log(`Oppo assets created from existing screenshots: ${createdCount} frameless image(s).`);
|
|
2563
|
+
}
|
|
2564
|
+
|
|
2213
2565
|
async function runInteractiveCaptureLoop(
|
|
2214
2566
|
deviceClient,
|
|
2215
2567
|
replayClient,
|
|
@@ -2251,6 +2603,8 @@ async function getInteractiveOptions(options) {
|
|
|
2251
2603
|
|| options.dryRun
|
|
2252
2604
|
|| options.amazonAssets
|
|
2253
2605
|
|| options.samsungAssets
|
|
2606
|
+
|| options.vivoExisting
|
|
2607
|
+
|| options.oppoExisting
|
|
2254
2608
|
) {
|
|
2255
2609
|
return options;
|
|
2256
2610
|
}
|
|
@@ -2329,6 +2683,16 @@ async function main() {
|
|
|
2329
2683
|
return;
|
|
2330
2684
|
}
|
|
2331
2685
|
|
|
2686
|
+
if (options.vivoExisting) {
|
|
2687
|
+
await createVivoAssetsFromExistingScreenshots();
|
|
2688
|
+
return;
|
|
2689
|
+
}
|
|
2690
|
+
|
|
2691
|
+
if (options.oppoExisting) {
|
|
2692
|
+
await createOppoAssetsFromExistingScreenshots();
|
|
2693
|
+
return;
|
|
2694
|
+
}
|
|
2695
|
+
|
|
2332
2696
|
if (!options.dryRun) {
|
|
2333
2697
|
await createMissingStoreAssets();
|
|
2334
2698
|
}
|