codeplay-common 4.7.2 → 4.7.3

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.
@@ -0,0 +1,117 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+ const sharp = require('sharp');
4
+
5
+ const frameStyles = {
6
+ android: {
7
+ graphite: { label: 'A · Graphite Android', detail: 'Slim dark bezel', outer: '#151a24', edge: '#626d7f', inner: '#080b12', radius: 47 },
8
+ cobalt: { label: 'B · Cobalt Android', detail: 'Blue accent bezel', outer: '#1945bc', edge: '#8aa9ff', inner: '#07102f', radius: 43 },
9
+ },
10
+ ios: {
11
+ titanium: { label: 'C · Titanium iPhone', detail: 'Light metal edge', outer: '#c7ced6', edge: '#eef2f6', inner: '#222936', radius: 53 },
12
+ midnight: { label: 'D · Midnight iPhone', detail: 'Dark rounded edge', outer: '#202333', edge: '#586076', inner: '#080b12', radius: 53 },
13
+ },
14
+ };
15
+
16
+ function getStatusBarSvg(platform, time) {
17
+ if (platform === 'ios') {
18
+ return `<rect x="12" y="22" width="316" height="40" fill="#f5f7fc"/>
19
+ <text x="29" y="47" font-family="Arial,sans-serif" font-size="14" font-weight="700" fill="#050505">${time}</text>
20
+ <circle cx="246" cy="40" r="2" fill="#c5c7cb"/><circle cx="254" cy="40" r="2" fill="#c5c7cb"/>
21
+ <circle cx="262" cy="40" r="2" fill="#c5c7cb"/><circle cx="270" cy="40" r="2" fill="#c5c7cb"/>
22
+ <path d="M275 35 Q284 27 293 35 M278 39 Q284 34 290 39 M282 43 Q284 41 286 43" fill="none" stroke="#050505" stroke-width="2.5" stroke-linecap="round"/>
23
+ <rect x="295" y="33" width="17" height="13" rx="3" fill="none" stroke="#a7aaae" stroke-width="1.5"/>
24
+ <rect x="297" y="35" width="13" height="9" rx="2" fill="#050505"/>
25
+ <rect x="313" y="37" width="2" height="5" rx="1" fill="#a7aaae"/>`;
26
+ }
27
+
28
+ return `<rect x="12" y="22" width="316" height="40" fill="#9075ed"/>
29
+ <text x="29" y="47" font-family="Arial,sans-serif" font-size="14" fill="#ffffff">${time}</text>
30
+ <circle cx="86" cy="41" r="5" fill="none" stroke="#ffffff" stroke-width="1.2"/>
31
+ <circle cx="84" cy="40" r="0.8" fill="#ffffff"/><circle cx="88" cy="40" r="0.8" fill="#ffffff"/>
32
+ <g transform="translate(-15,0)">
33
+ <text x="200" y="36" font-family="Arial,sans-serif" font-size="7" font-weight="700" fill="#ffffff">0.26</text>
34
+ <text x="200" y="44" font-family="Arial,sans-serif" font-size="6" font-weight="700" fill="#ffffff">KB/s</text>
35
+ <path d="M222 43 L235 33 L235 43 Z M242 43 L254 33 L254 43 Z" fill="#ffffff"/>
36
+ <text x="257" y="39" font-family="Arial,sans-serif" font-size="7" font-weight="700" fill="#ffffff">Vo</text>
37
+ <text x="257" y="46" font-family="Arial,sans-serif" font-size="6" font-weight="700" fill="#ffffff">WiFi</text>
38
+ <path d="M274 35 Q283 27 292 35 M277 39 Q283 34 289 39 M281 43 Q283 41 285 43" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round"/>
39
+ <rect x="296" y="34" width="19" height="11" rx="2" fill="none" stroke="#ffffff" stroke-width="1.4"/>
40
+ <path d="M305 33 L301 40 L306 40 L304 46 L312 38 L307 38 L310 33 Z" fill="#ffffff"/>
41
+ <text x="317" y="43" font-family="Arial,sans-serif" font-size="7" fill="#ffffff">87%</text>
42
+ </g>`;
43
+ }
44
+
45
+ function createFallbackScreenshot(platform) {
46
+ const width = platform === 'ios' ? 1320 : 1080;
47
+ const height = platform === 'ios' ? 2868 : 2400;
48
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">
49
+ <rect width="100%" height="100%" fill="#f5f7fc"/>
50
+ <rect x="8%" y="26%" width="84%" height="20%" rx="60" fill="#d7ddff"/>
51
+ <text x="50%" y="37%" text-anchor="middle" font-family="Arial,sans-serif" font-size="62" font-weight="700" fill="#4446c6">Image Compressor</text>
52
+ <rect x="8%" y="62%" width="84%" height="8%" rx="55" fill="#5145e7"/>
53
+ <text x="50%" y="67%" text-anchor="middle" font-family="Arial,sans-serif" font-size="42" font-weight="700" fill="#ffffff">Select images</text>
54
+ </svg>`;
55
+ return sharp(Buffer.from(svg)).png().toBuffer();
56
+ }
57
+
58
+ async function getSampleScreenshot(autoScreenshotDirectory, platform) {
59
+ const directory = path.join(autoScreenshotDirectory, 'Store-image-Screenshot', platform === 'ios' ? 'iPhone' : 'Android');
60
+ if (fs.existsSync(directory)) {
61
+ const screenshot = fs.readdirSync(directory).filter((name) => /\.png$/i.test(name)).sort()[0];
62
+ if (screenshot) return fs.readFileSync(path.join(directory, screenshot));
63
+ }
64
+ return createFallbackScreenshot(platform);
65
+ }
66
+
67
+ function createSampleCard(style, platform, screenshotUrl, index) {
68
+ const screenRadius = platform === 'ios' ? 40 : 34;
69
+ const sampleTime = platform === 'ios' ? (index ? '12:21' : '12:20') : (index ? '10:56' : '10:55');
70
+ const camera = platform === 'ios'
71
+ ? '<rect x="130" y="27" width="80" height="24" rx="12" fill="#05070b"/>'
72
+ : '';
73
+ const navigation = platform === 'android'
74
+ ? '<rect x="12" y="717" width="316" height="16" fill="#f4f7f8"/><rect x="107" y="725" width="126" height="3" rx="1.5" fill="#7a7b7c"/>'
75
+ : '';
76
+
77
+ return `<g transform="translate(${index * 510},0)">
78
+ <rect x="12" y="12" width="486" height="926" rx="25" fill="#ffffff" stroke="#dce2ee"/>
79
+ <text x="35" y="52" fill="#172137" font-family="Arial,sans-serif" font-size="24" font-weight="700">${style.label}</text>
80
+ <text x="35" y="80" fill="#6c7890" font-family="Arial,sans-serif" font-size="14">${style.detail} · ${platform === 'ios' ? 'iOS' : 'Android'} status bar</text>
81
+ <g transform="translate(85,115)">
82
+ <rect x="1" y="7" width="338" height="753" rx="47" fill="#0a1225" opacity="0.2"/>
83
+ <rect x="1" y="1" width="338" height="753" rx="${style.radius}" fill="${style.outer}" stroke="${style.edge}" stroke-width="2"/>
84
+ <rect x="7" y="7" width="326" height="741" rx="${style.radius - 5}" fill="${style.inner}"/>
85
+ <clipPath id="screen-${index}"><rect x="12" y="22" width="316" height="711" rx="${screenRadius}"/></clipPath>
86
+ <g clip-path="url(#screen-${index})">
87
+ <rect x="12" y="22" width="316" height="711" fill="#f5f7fc"/>
88
+ <image href="${screenshotUrl}" x="12" y="62" width="316" height="671" preserveAspectRatio="xMidYMin slice"/>
89
+ ${getStatusBarSvg(platform, sampleTime)}
90
+ ${navigation}
91
+ </g>
92
+ ${camera}
93
+ <rect x="338" y="177" width="4" height="66" rx="2" fill="${style.edge}"/>
94
+ </g>
95
+ </g>`;
96
+ }
97
+
98
+ async function createFrameSamples(autoScreenshotDirectory) {
99
+ fs.mkdirSync(autoScreenshotDirectory, { recursive: true });
100
+ const paths = [];
101
+
102
+ for (const platform of ['android', 'ios']) {
103
+ const screenshot = await getSampleScreenshot(autoScreenshotDirectory, platform);
104
+ const screenshotUrl = `data:image/png;base64,${screenshot.toString('base64')}`;
105
+ const cards = Object.values(frameStyles[platform])
106
+ .map((style, index) => createSampleCard(style, platform, screenshotUrl, index))
107
+ .join('');
108
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="1020" height="950" viewBox="0 0 1020 950"><rect width="1020" height="950" fill="#eef2f9"/>${cards}</svg>`;
109
+ const output = path.join(autoScreenshotDirectory, `${platform}-frame-options.png`);
110
+ await sharp(Buffer.from(svg)).png().toFile(output);
111
+ paths.push(output);
112
+ }
113
+
114
+ return paths;
115
+ }
116
+
117
+ module.exports = { frameStyles, createFrameSamples };
@@ -10,6 +10,7 @@ const path = require('node:path');
10
10
  const readline = require('node:readline/promises');
11
11
  const { spawn, spawnSync } = require('node:child_process');
12
12
  const sharp = require('sharp');
13
+ const { frameStyles, createFrameSamples } = require('./scripts/frame-samples.js');
13
14
 
14
15
  const screenshotDirectory = path.join(__dirname, 'Auto-Screenshot', 'Store-image-Screenshot');
15
16
  const inFrameDirectory = path.join(__dirname, 'Auto-Screenshot', 'Store-image-Frame');
@@ -21,6 +22,9 @@ const playstoreAssetsDirectory = path.join(aiAssetsDirectory, 'Playstore-Assets'
21
22
  const capacitorConfigPath = path.join(__dirname, 'capacitor.config.json');
22
23
  const amazonIconPath = path.join(__dirname, 'resources', 'icon-only.png');
23
24
  const temporaryDirectory = path.join(__dirname, 'agent-temp');
25
+ let selectedFrameStyles = { android: 'cobalt', ios: 'midnight' };
26
+ let frameCaptureSequence = 0;
27
+ const frameTimeBase = Date.now();
24
28
  const amazonAssetDefinitions = [
25
29
  { fileName: 'icon-1280x720.png', type: 'icon', width: 1280, height: 720 },
26
30
  { fileName: 'background-image-1920x1080.png', type: 'background-image', width: 1920, height: 1080 },
@@ -462,6 +466,7 @@ function parseArguments() {
462
466
  samsungAssets: false,
463
467
  dryRun: false,
464
468
  frameExisting: false,
469
+ reframeExisting: false,
465
470
  vivoExisting: false,
466
471
  oppoExisting: false,
467
472
  help: false,
@@ -470,6 +475,8 @@ function parseArguments() {
470
475
  deleteNames: '',
471
476
  prefix: '',
472
477
  profiles: '',
478
+ androidFrame: '',
479
+ iosFrame: '',
473
480
  };
474
481
 
475
482
  for (let index = 0; index < args.length; index += 1) {
@@ -483,6 +490,8 @@ function parseArguments() {
483
490
  options.dryRun = true;
484
491
  } else if (argument === '--frame-existing') {
485
492
  options.frameExisting = true;
493
+ } else if (argument === '--reframe-existing') {
494
+ options.reframeExisting = true;
486
495
  } else if (argument === '--vivo-existing') {
487
496
  options.vivoExisting = true;
488
497
  } else if (argument === '--oppo-existing') {
@@ -501,6 +510,12 @@ function parseArguments() {
501
510
  } else if (argument === '--profiles') {
502
511
  options.profiles = args[index + 1] || '';
503
512
  index += 1;
513
+ } else if (argument === '--android-frame') {
514
+ options.androidFrame = args[index + 1] || '';
515
+ index += 1;
516
+ } else if (argument === '--ios-frame') {
517
+ options.iosFrame = args[index + 1] || '';
518
+ index += 1;
504
519
  } else {
505
520
  throw new Error(`Unknown argument: ${argument}`);
506
521
  }
@@ -540,8 +555,10 @@ function printHelp() {
540
555
  console.log('Usage:');
541
556
  console.log(' node take-screen-image.js');
542
557
  console.log(' node take-screen-image.js --profiles iphone,ipad');
558
+ console.log(' node take-screen-image.js --android-frame graphite --ios-frame titanium');
543
559
  console.log(' node take-screen-image.js --prefix home --profiles android-phone,ipad');
544
560
  console.log(' node take-screen-image.js --frame-existing');
561
+ console.log(' node take-screen-image.js --reframe-existing --profiles android-phone,iphone');
545
562
  console.log(' node take-screen-image.js --vivo-existing');
546
563
  console.log(' node take-screen-image.js --oppo-existing');
547
564
  console.log(' node take-screen-image.js --amazon-assets');
@@ -551,6 +568,52 @@ function printHelp() {
551
568
  console.log(' node take-screen-image.js --delete 3,4,5');
552
569
  console.log('');
553
570
  console.log('A normal capture automatically creates any missing Amazon, Samsung, and Play Store assets.');
571
+ console.log('Frame defaults: Android cobalt (B), iPhone midnight (D).');
572
+ }
573
+
574
+ async function chooseFrameStyles(options) {
575
+ const choices = { android: options.androidFrame, ios: options.iosFrame };
576
+ const defaults = { android: 'cobalt', ios: 'midnight' };
577
+ const aliases = {
578
+ android: { a: 'graphite', b: 'cobalt', 1: 'graphite', 2: 'cobalt' },
579
+ ios: { c: 'titanium', d: 'midnight', 1: 'titanium', 2: 'midnight' },
580
+ };
581
+ const canPrompt = process.stdin.isTTY && !options.prefix;
582
+ const terminal = canPrompt ? readline.createInterface({ input: process.stdin, output: process.stdout }) : null;
583
+
584
+ try {
585
+ for (const platform of ['android', 'ios']) {
586
+ if (!choices[platform] && terminal) {
587
+ const names = Object.values(frameStyles[platform]).map((style) => style.label);
588
+ choices[platform] = await terminal.question(
589
+ `${platform === 'ios' ? 'iPhone' : 'Android'} frame (${names.join(' / ')}), Enter for ${defaults[platform]}: `,
590
+ );
591
+ }
592
+ choices[platform] = (choices[platform] || defaults[platform]).trim().toLowerCase();
593
+ choices[platform] = aliases[platform][choices[platform]] || choices[platform];
594
+ if (!frameStyles[platform][choices[platform]]) {
595
+ throw new Error(`Unknown ${platform} frame: ${choices[platform]}. Choose ${Object.keys(frameStyles[platform]).join(' or ')}.`);
596
+ }
597
+ }
598
+ } finally {
599
+ if (terminal) terminal.close();
600
+ }
601
+
602
+ console.log(`Frame selection: Android ${choices.android}, iPhone ${choices.ios}.`);
603
+ return choices;
604
+ }
605
+
606
+ async function prepareFrameSelection(options) {
607
+ const samplePaths = await createFrameSamples(autoScreenshotDirectory);
608
+ for (const samplePath of samplePaths) console.log(`Frame choices: ${samplePath}`);
609
+ selectedFrameStyles = await chooseFrameStyles(options);
610
+ }
611
+
612
+ function getFrameDisplayTime(platform) {
613
+ const time = new Date(frameTimeBase + frameCaptureSequence * 60000);
614
+ frameCaptureSequence += 1;
615
+ const hour = platform === 'ios' ? (time.getHours() % 12 || 12) : time.getHours();
616
+ return `${String(hour).padStart(2, '0')}:${String(time.getMinutes()).padStart(2, '0')}`;
554
617
  }
555
618
 
556
619
  function deleteMatchingImages(input) {
@@ -1190,11 +1253,11 @@ async function waitForReplayLayout(client, snapshot) {
1190
1253
 
1191
1254
  function getFrameAdornment(frameType) {
1192
1255
  if (frameType === 'iphone') {
1193
- return '<div class="dynamic-island"></div>';
1256
+ return '';
1194
1257
  }
1195
1258
 
1196
1259
  if (frameType === 'android-phone') {
1197
- return '<div class="camera-dot"></div>';
1260
+ return '';
1198
1261
  }
1199
1262
 
1200
1263
  if (frameType === 'tablet') {
@@ -1220,10 +1283,11 @@ function getFrameAdornment(frameType) {
1220
1283
  return '<div class="xr-status"></div>';
1221
1284
  }
1222
1285
 
1223
- function createStoreDeviceFrameHtml(profile, screenshotBuffer, imageWidth, imageHeight, canvasWidth, canvasHeight, imageFormat) {
1286
+ function createStoreDeviceFrameHtml(profile, screenshotBuffer, imageWidth, imageHeight, canvasWidth, canvasHeight, imageFormat, displayTime) {
1224
1287
  const screenshotDataUrl = `data:image/${imageFormat === 'jpeg' ? 'jpeg' : 'png'};base64,${screenshotBuffer.toString('base64')}`;
1225
1288
  const screenWidth = Math.floor(Math.min(canvasWidth - 160, (canvasHeight - 180) * imageWidth / imageHeight));
1226
1289
  const accent = profile.name === 'oppo-store' ? '#76e4c2' : '#9daaff';
1290
+ const frameStyle = frameStyles.android[selectedFrameStyles.android];
1227
1291
 
1228
1292
  return `<!doctype html>
1229
1293
  <html>
@@ -1252,17 +1316,27 @@ function createStoreDeviceFrameHtml(profile, screenshotBuffer, imageWidth, image
1252
1316
  z-index: 1;
1253
1317
  width: ${screenWidth + 48}px;
1254
1318
  padding: 34px 18px 20px;
1255
- border: 6px solid #8d99ac;
1319
+ border: 6px solid ${frameStyle.edge};
1256
1320
  border-radius: 48px;
1257
- background: linear-gradient(135deg, #404c60, #111925 35%, #070c15 72%, #59657a);
1321
+ background: ${frameStyle.outer};
1258
1322
  box-shadow: 0 38px 85px rgba(0, 0, 0, 0.58), 0 0 0 2px rgba(255, 255, 255, 0.19) inset;
1259
1323
  }
1260
1324
  .screen {
1325
+ display: flex;
1326
+ flex-direction: column;
1261
1327
  width: ${screenWidth}px;
1328
+ aspect-ratio: ${imageWidth} / ${imageHeight};
1329
+ overflow: hidden;
1262
1330
  background: #fff;
1263
1331
  box-shadow: 0 0 0 2px #03060c;
1264
1332
  }
1265
- .screen img { display: block; width: 100%; height: auto; }
1333
+ .screen img { display: block; width: 100%; height: 0; min-height: 0; flex: 1 1 0; object-fit: fill; }
1334
+ .phone-statusbar { position: relative; flex: none; height: 5%; display: flex; align-items: center; background: #9075ed; color: #fff; font-family: Arial, sans-serif; }
1335
+ .status-time { margin-left: 5%; font-size: ${Math.max(11, Math.round(screenWidth * 0.03))}px; white-space: nowrap; }
1336
+ .android-notification { width: 5%; height: 45%; margin-left: 2%; }
1337
+ .android-status-icons { width: 42%; height: 72%; margin-left: auto; margin-right: 2%; }
1338
+ .android-navigation { flex: none; height: 2.2%; display: grid; place-items: center; background: #f4f7f8; }
1339
+ .gesture-pill { width: 40%; height: 14%; border-radius: 999px; background: #7a7b7c; }
1266
1340
  .camera {
1267
1341
  position: absolute;
1268
1342
  top: 11px;
@@ -1288,16 +1362,58 @@ function createStoreDeviceFrameHtml(profile, screenshotBuffer, imageWidth, image
1288
1362
  <body>
1289
1363
  <main class="device">
1290
1364
  <div class="camera"></div>
1291
- <div class="screen"><img src="${screenshotDataUrl}" /></div>
1365
+ <div class="screen">${getPhoneStatusBar('android', displayTime)}<img src="${screenshotDataUrl}" /><div class="android-navigation"><div class="gesture-pill"></div></div></div>
1292
1366
  <div class="side-button"></div>
1293
1367
  </main>
1294
1368
  </body>
1295
1369
  </html>`;
1296
1370
  }
1297
1371
 
1298
- function createDeviceFrameHtml(profile, screenshotBuffer, imageWidth = profile.width, imageHeight = profile.height, imageFormat = 'png') {
1372
+ function getPhoneStatusBar(platform, time) {
1373
+ if (platform === 'ios') {
1374
+ return `<div class="phone-statusbar ios-statusbar">
1375
+ <span class="status-time">${time}</span>
1376
+ <div class="dynamic-island"></div>
1377
+ <svg viewBox="0 0 90 28" aria-hidden="true">
1378
+ <circle cx="5" cy="14" r="2" fill="#c5c7cb"></circle><circle cx="13" cy="14" r="2" fill="#c5c7cb"></circle>
1379
+ <circle cx="21" cy="14" r="2" fill="#c5c7cb"></circle><circle cx="29" cy="14" r="2" fill="#c5c7cb"></circle>
1380
+ <path d="M36 10 Q46 2 56 10 M39 14 Q46 8 53 14 M44 18 Q46 16 48 18" fill="none" stroke="#050505" stroke-width="2.5" stroke-linecap="round"></path>
1381
+ <rect x="61" y="7" width="22" height="15" rx="4" fill="none" stroke="#a7aaae" stroke-width="1.5"></rect>
1382
+ <rect x="64" y="10" width="16" height="9" rx="2" fill="#050505"></rect>
1383
+ <rect x="85" y="12" width="2" height="5" rx="1" fill="#a7aaae"></rect>
1384
+ </svg>
1385
+ </div>`;
1386
+ }
1387
+
1388
+ return `<div class="phone-statusbar android-statusbar">
1389
+ <span class="status-time">${time}</span>
1390
+ <svg class="android-notification" viewBox="0 0 18 18" aria-hidden="true">
1391
+ <circle cx="9" cy="9" r="6" fill="none" stroke="#ffffff" stroke-width="1.5"></circle>
1392
+ <circle cx="7" cy="8" r="1" fill="#ffffff"></circle><circle cx="11" cy="8" r="1" fill="#ffffff"></circle>
1393
+ </svg>
1394
+ <svg class="android-status-icons" viewBox="0 0 132 28" aria-hidden="true">
1395
+ <text x="1" y="11" font-size="8" font-weight="700" fill="#ffffff">0.26</text>
1396
+ <text x="1" y="19" font-size="7" font-weight="700" fill="#ffffff">KB/s</text>
1397
+ <path d="M24 20 L37 9 L37 20 Z M42 20 L54 9 L54 20 Z" fill="#ffffff"></path>
1398
+ <text x="57" y="12" font-size="8" font-weight="700" fill="#ffffff">Vo</text>
1399
+ <text x="57" y="20" font-size="7" font-weight="700" fill="#ffffff">WiFi</text>
1400
+ <path d="M76 11 Q85 3 94 11 M79 15 Q85 9 91 15 M83 19 Q85 17 87 19" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round"></path>
1401
+ <rect x="97" y="8" width="18" height="13" rx="2" fill="none" stroke="#ffffff" stroke-width="1.4"></rect>
1402
+ <path d="M106 7 L102 14 L107 14 L105 22 L113 13 L108 13 L111 7 Z" fill="#ffffff"></path>
1403
+ <text x="117" y="18" font-size="8" fill="#ffffff">87%</text>
1404
+ </svg>
1405
+ </div>`;
1406
+ }
1407
+
1408
+ function createDeviceFrameHtml(profile, screenshotBuffer, imageWidth = profile.width, imageHeight = profile.height, imageFormat = 'png', displayTime = '') {
1299
1409
  const screenshotDataUrl = `data:image/${imageFormat === 'jpeg' ? 'jpeg' : 'png'};base64,${screenshotBuffer.toString('base64')}`;
1300
1410
  const aspectRatio = `${imageWidth} / ${imageHeight}`;
1411
+ const phonePlatform = profile.frameType === 'iphone' ? 'ios' : profile.frameType === 'android-phone' ? 'android' : null;
1412
+ const phoneStyle = phonePlatform && frameStyles[phonePlatform][selectedFrameStyles[phonePlatform]];
1413
+ const phoneStatusBar = phonePlatform ? getPhoneStatusBar(phonePlatform, displayTime) : '';
1414
+ const phoneNavigation = phonePlatform === 'android'
1415
+ ? '<div class="android-navigation"><div class="gesture-pill"></div></div>'
1416
+ : '';
1301
1417
 
1302
1418
  return `<!doctype html>
1303
1419
  <html>
@@ -1316,6 +1432,8 @@ function createDeviceFrameHtml(profile, screenshotBuffer, imageWidth = profile.w
1316
1432
  linear-gradient(145deg, #111827 0%, #070b12 52%, #020409 100%);
1317
1433
  font-family: Arial, sans-serif;
1318
1434
  }
1435
+ body.phone-frame { background: #eef2f9; }
1436
+ body.phone-frame::before { display: none; }
1319
1437
  body::before {
1320
1438
  content: '';
1321
1439
  position: absolute;
@@ -1341,7 +1459,7 @@ function createDeviceFrameHtml(profile, screenshotBuffer, imageWidth = profile.w
1341
1459
  .screen {
1342
1460
  position: relative;
1343
1461
  width: 100%;
1344
- height: 87%;
1462
+ height: 100%;
1345
1463
  overflow: hidden;
1346
1464
  background: #f5f7fc;
1347
1465
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.95) inset;
@@ -1355,14 +1473,65 @@ function createDeviceFrameHtml(profile, screenshotBuffer, imageWidth = profile.w
1355
1473
  }
1356
1474
  .device.android-phone,
1357
1475
  .device.iphone {
1358
- height: 100%;
1359
- padding: 1.25%;
1476
+ height: 84%;
1477
+ padding: 2.5%;
1360
1478
  border-radius: clamp(24px, 8vw, 70px);
1361
1479
  }
1362
- .device.android-phone .screen,
1363
- .device.iphone .screen {
1364
- border-radius: clamp(19px, 6.7vw, 60px);
1480
+ ${phoneStyle ? `
1481
+ .device.${profile.frameType} {
1482
+ background: ${phoneStyle.outer};
1483
+ border-color: ${phoneStyle.edge};
1484
+ border-radius: calc(84vh * ${imageWidth / imageHeight * phoneStyle.radius / 340});
1485
+ box-shadow: 0 4vh 9vh rgba(0, 0, 0, 0.48), 0 0 0 0.55vw ${phoneStyle.inner} inset;
1486
+ }
1487
+ .device.${profile.frameType} .screen {
1488
+ display: flex;
1489
+ flex-direction: column;
1490
+ height: 100%;
1491
+ border-radius: calc(84vh * ${imageWidth / imageHeight * phoneStyle.radius / 340} - 2.5vw - clamp(2px, 0.45vw, 9px));
1492
+ }
1493
+ .device.${profile.frameType} .screen img {
1494
+ flex: 1 1 0;
1495
+ min-height: 0;
1496
+ object-fit: fill;
1497
+ }` : ''}
1498
+ .phone-statusbar {
1499
+ position: relative;
1500
+ z-index: 2;
1501
+ flex: none;
1502
+ height: 5%;
1503
+ display: flex;
1504
+ align-items: center;
1505
+ font-family: Arial, sans-serif;
1506
+ }
1507
+ .status-time {
1508
+ margin-left: 5%;
1509
+ font-size: clamp(11px, 3vw, 40px);
1510
+ white-space: nowrap;
1511
+ }
1512
+ .ios-statusbar { background: #f5f7fc; color: #050505; font-weight: 700; }
1513
+ .ios-statusbar svg { width: 27%; height: 62%; margin-left: auto; margin-right: 3%; }
1514
+ .ios-statusbar .dynamic-island {
1515
+ position: absolute;
1516
+ top: 22%;
1517
+ left: 50%;
1518
+ width: 25%;
1519
+ height: 56%;
1520
+ transform: translateX(-50%);
1521
+ border-radius: 999px;
1522
+ background: #000;
1523
+ }
1524
+ .android-statusbar { background: #9075ed; color: #fff; }
1525
+ .android-notification { width: 5%; height: 45%; margin-left: 2%; }
1526
+ .android-status-icons { width: 42%; height: 72%; margin-left: auto; margin-right: 2%; }
1527
+ .android-navigation {
1528
+ flex: none;
1529
+ height: 2.2%;
1530
+ display: grid;
1531
+ place-items: center;
1532
+ background: #f4f7f8;
1365
1533
  }
1534
+ .gesture-pill { width: 40%; height: 14%; border-radius: 999px; background: #7a7b7c; }
1366
1535
  .camera-dot {
1367
1536
  position: absolute;
1368
1537
  z-index: 3;
@@ -1528,9 +1697,9 @@ function createDeviceFrameHtml(profile, screenshotBuffer, imageWidth = profile.w
1528
1697
  }
1529
1698
  </style>
1530
1699
  </head>
1531
- <body>
1700
+ <body class="${phonePlatform ? 'phone-frame' : ''}">
1532
1701
  <main class="device ${profile.frameType}">
1533
- <div class="screen"><img src="${screenshotDataUrl}" /></div>
1702
+ <div class="screen">${phoneStatusBar}<img src="${screenshotDataUrl}" />${phoneNavigation}</div>
1534
1703
  ${getFrameAdornment(profile.frameType)}
1535
1704
  </main>
1536
1705
  </body>
@@ -2189,6 +2358,8 @@ async function captureFramedScreenshot(
2189
2358
  const frameWidth = profile.framedWidth || profile.width;
2190
2359
  const frameHeight = profile.framedHeight || profile.height;
2191
2360
  const frameScaleFactor = hasCustomFrameSize ? 1 : profile.deviceScaleFactor;
2361
+ const phonePlatform = profile.frameType === 'iphone' ? 'ios' : profile.frameType === 'android-phone' ? 'android' : null;
2362
+ const displayTime = phonePlatform ? getFrameDisplayTime(phonePlatform) : '';
2192
2363
 
2193
2364
  await client.send('Emulation.setDeviceMetricsOverride', {
2194
2365
  width: frameWidth,
@@ -2207,9 +2378,9 @@ async function captureFramedScreenshot(
2207
2378
  html: profile.name === 'vivo-store' || profile.name === 'oppo-store'
2208
2379
  ? createStoreDeviceFrameHtml(
2209
2380
  profile, screenshotBuffer, originalImageMetadata.width, originalImageMetadata.height,
2210
- frameWidth, frameHeight, originalImageMetadata.format,
2381
+ frameWidth, frameHeight, originalImageMetadata.format, displayTime,
2211
2382
  )
2212
- : createDeviceFrameHtml(profile, screenshotBuffer, frameWidth, frameHeight),
2383
+ : createDeviceFrameHtml(profile, screenshotBuffer, frameWidth, frameHeight, 'png', displayTime),
2213
2384
  });
2214
2385
  await client.send('Runtime.evaluate', {
2215
2386
  expression: `new Promise((resolve) => {
@@ -2358,7 +2529,7 @@ async function captureScreenshotSet(
2358
2529
  console.log(`Responsive screenshot set completed: ${screenshotFileName}`);
2359
2530
  }
2360
2531
 
2361
- async function frameExistingScreenshots(selectedProfiles) {
2532
+ async function frameExistingScreenshots(selectedProfiles, overwriteExisting = false) {
2362
2533
  let replayBrowser;
2363
2534
  let replayClient;
2364
2535
  let framedCount = 0;
@@ -2389,7 +2560,7 @@ async function frameExistingScreenshots(selectedProfiles) {
2389
2560
  fs.readFileSync(screenshotPath),
2390
2561
  profile,
2391
2562
  screenshotFileName,
2392
- true,
2563
+ !overwriteExisting,
2393
2564
  );
2394
2565
  if (wasCreated) framedCount += 1;
2395
2566
  } catch (error) {
@@ -2678,6 +2849,10 @@ async function main() {
2678
2849
  return;
2679
2850
  }
2680
2851
 
2852
+ if (options.vivoExisting || options.oppoExisting) {
2853
+ await prepareFrameSelection(options);
2854
+ }
2855
+
2681
2856
  if (options.vivoExisting) {
2682
2857
  await createVivoAssetsFromExistingScreenshots();
2683
2858
  return;
@@ -2688,17 +2863,21 @@ async function main() {
2688
2863
  return;
2689
2864
  }
2690
2865
 
2866
+ const selectedProfiles = selectProfiles(options.profiles);
2867
+
2691
2868
  if (!options.dryRun) {
2692
- await createMissingStoreAssets();
2869
+ await prepareFrameSelection(options);
2693
2870
  }
2694
2871
 
2695
- const selectedProfiles = selectProfiles(options.profiles);
2696
-
2697
- if (options.frameExisting) {
2698
- await frameExistingScreenshots(selectedProfiles);
2872
+ if (options.frameExisting || options.reframeExisting) {
2873
+ await frameExistingScreenshots(selectedProfiles, options.reframeExisting);
2699
2874
  return;
2700
2875
  }
2701
2876
 
2877
+ if (!options.dryRun) {
2878
+ await createMissingStoreAssets();
2879
+ }
2880
+
2702
2881
  const interactiveMode = process.stdin.isTTY && !options.prefix && !options.dryRun;
2703
2882
  const deviceId = getConnectedDevice();
2704
2883
  const appId = getAppId();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeplay-common",
3
- "version": "4.7.2",
3
+ "version": "4.7.3",
4
4
  "description": "Common build scripts and files",
5
5
  "scripts": {
6
6
  "postinstall": "node scripts/sync-files.js",