codeplay-common 4.7.2 → 4.7.4
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/scripts-screenshot/screenshot/background-decorations.js +120 -0
- package/files/scripts-screenshot/screenshot/frame-samples.js +117 -0
- package/files/scripts-screenshot/screenshot/screenshot-copy.json +44 -0
- package/files/scripts-screenshot/screenshot/screenshot-info.json +1737 -0
- package/files/take-screen-image.js +1183 -68
- package/package.json +1 -1
|
@@ -5,14 +5,34 @@
|
|
|
5
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.
|
|
6
6
|
|
|
7
7
|
|
|
8
|
+
/*
|
|
9
|
+
1) run this "node take-screen-image.js"
|
|
10
|
+
|
|
11
|
+
2) After take all screenshot do the following
|
|
12
|
+
Execute the following in codex
|
|
13
|
+
Execute this AGENTS\Commands\AI-Screenshot text generation.txt
|
|
14
|
+
|
|
15
|
+
3) Execute the following
|
|
16
|
+
node take-screen-image.js --reframe-existing
|
|
17
|
+
node take-screen-image.js --reframe-existing --screenshots 6,7
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
|
|
8
21
|
const fs = require('node:fs');
|
|
9
22
|
const path = require('node:path');
|
|
10
23
|
const readline = require('node:readline/promises');
|
|
11
24
|
const { spawn, spawnSync } = require('node:child_process');
|
|
12
25
|
const sharp = require('sharp');
|
|
26
|
+
const { frameStyles, createFrameSamples } = require('./scripts-screenshot/screenshot/frame-samples.js');
|
|
27
|
+
const {
|
|
28
|
+
getDecorationSeed,
|
|
29
|
+
createPromotionalDecorations,
|
|
30
|
+
getPromotionalDecorationStyles,
|
|
31
|
+
} = require('./scripts-screenshot/screenshot/background-decorations.js');
|
|
13
32
|
|
|
14
33
|
const screenshotDirectory = path.join(__dirname, 'Auto-Screenshot', 'Store-image-Screenshot');
|
|
15
34
|
const inFrameDirectory = path.join(__dirname, 'Auto-Screenshot', 'Store-image-Frame');
|
|
35
|
+
const uploadDirectory = path.join(__dirname, 'Auto-Screenshot', 'Store-upload');
|
|
16
36
|
const autoScreenshotDirectory = path.join(__dirname, 'Auto-Screenshot');
|
|
17
37
|
const aiAssetsDirectory = path.join(__dirname, 'AI-Assets');
|
|
18
38
|
const amazonAssetsDirectory = path.join(aiAssetsDirectory, 'Amazon-Assets');
|
|
@@ -21,6 +41,13 @@ const playstoreAssetsDirectory = path.join(aiAssetsDirectory, 'Playstore-Assets'
|
|
|
21
41
|
const capacitorConfigPath = path.join(__dirname, 'capacitor.config.json');
|
|
22
42
|
const amazonIconPath = path.join(__dirname, 'resources', 'icon-only.png');
|
|
23
43
|
const temporaryDirectory = path.join(__dirname, 'agent-temp');
|
|
44
|
+
const screenshotCopyPath = path.join(__dirname, 'scripts-screenshot', 'screenshot', 'screenshot-copy.json');
|
|
45
|
+
const screenshotInfoPath = path.join(__dirname, 'scripts-screenshot', 'screenshot', 'screenshot-info.json');
|
|
46
|
+
let selectedFrameStyles = { android: 'graphite', ios: 'midnight' };
|
|
47
|
+
let frameCaptureSequence = 0;
|
|
48
|
+
let screenshotCopyConfig;
|
|
49
|
+
const missingScreenshotCopyWarnings = new Set();
|
|
50
|
+
const frameTimeBase = Date.now();
|
|
24
51
|
const amazonAssetDefinitions = [
|
|
25
52
|
{ fileName: 'icon-1280x720.png', type: 'icon', width: 1280, height: 720 },
|
|
26
53
|
{ fileName: 'background-image-1920x1080.png', type: 'background-image', width: 1920, height: 1080 },
|
|
@@ -146,7 +173,7 @@ const deviceProfiles = [
|
|
|
146
173
|
name: 'ipad',
|
|
147
174
|
label: 'iPad 13-inch App Store viewport',
|
|
148
175
|
outputDirectory: 'iPad',
|
|
149
|
-
frameType: '
|
|
176
|
+
frameType: 'ipad',
|
|
150
177
|
width: 1032,
|
|
151
178
|
height: 1376,
|
|
152
179
|
deviceScaleFactor: 2,
|
|
@@ -462,6 +489,8 @@ function parseArguments() {
|
|
|
462
489
|
samsungAssets: false,
|
|
463
490
|
dryRun: false,
|
|
464
491
|
frameExisting: false,
|
|
492
|
+
reframeExisting: false,
|
|
493
|
+
refreshInfo: '',
|
|
465
494
|
vivoExisting: false,
|
|
466
495
|
oppoExisting: false,
|
|
467
496
|
help: false,
|
|
@@ -470,6 +499,9 @@ function parseArguments() {
|
|
|
470
499
|
deleteNames: '',
|
|
471
500
|
prefix: '',
|
|
472
501
|
profiles: '',
|
|
502
|
+
screenshots: '',
|
|
503
|
+
androidFrame: '',
|
|
504
|
+
iosFrame: '',
|
|
473
505
|
};
|
|
474
506
|
|
|
475
507
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -483,6 +515,11 @@ function parseArguments() {
|
|
|
483
515
|
options.dryRun = true;
|
|
484
516
|
} else if (argument === '--frame-existing') {
|
|
485
517
|
options.frameExisting = true;
|
|
518
|
+
} else if (argument === '--reframe-existing') {
|
|
519
|
+
options.reframeExisting = true;
|
|
520
|
+
} else if (argument === '--refresh-info') {
|
|
521
|
+
options.refreshInfo = args[index + 1] || '';
|
|
522
|
+
index += 1;
|
|
486
523
|
} else if (argument === '--vivo-existing') {
|
|
487
524
|
options.vivoExisting = true;
|
|
488
525
|
} else if (argument === '--oppo-existing') {
|
|
@@ -501,6 +538,15 @@ function parseArguments() {
|
|
|
501
538
|
} else if (argument === '--profiles') {
|
|
502
539
|
options.profiles = args[index + 1] || '';
|
|
503
540
|
index += 1;
|
|
541
|
+
} else if (argument === '--screenshots') {
|
|
542
|
+
options.screenshots = args[index + 1] || '';
|
|
543
|
+
index += 1;
|
|
544
|
+
} else if (argument === '--android-frame') {
|
|
545
|
+
options.androidFrame = args[index + 1] || '';
|
|
546
|
+
index += 1;
|
|
547
|
+
} else if (argument === '--ios-frame') {
|
|
548
|
+
options.iosFrame = args[index + 1] || '';
|
|
549
|
+
index += 1;
|
|
504
550
|
} else {
|
|
505
551
|
throw new Error(`Unknown argument: ${argument}`);
|
|
506
552
|
}
|
|
@@ -540,8 +586,12 @@ function printHelp() {
|
|
|
540
586
|
console.log('Usage:');
|
|
541
587
|
console.log(' node take-screen-image.js');
|
|
542
588
|
console.log(' node take-screen-image.js --profiles iphone,ipad');
|
|
589
|
+
console.log(' node take-screen-image.js --android-frame graphite --ios-frame titanium');
|
|
543
590
|
console.log(' node take-screen-image.js --prefix home --profiles android-phone,ipad');
|
|
544
591
|
console.log(' node take-screen-image.js --frame-existing');
|
|
592
|
+
console.log(' node take-screen-image.js --reframe-existing --profiles android-phone,iphone');
|
|
593
|
+
console.log(' node take-screen-image.js --reframe-existing --screenshots 1,2');
|
|
594
|
+
console.log(' node take-screen-image.js --refresh-info 1');
|
|
545
595
|
console.log(' node take-screen-image.js --vivo-existing');
|
|
546
596
|
console.log(' node take-screen-image.js --oppo-existing');
|
|
547
597
|
console.log(' node take-screen-image.js --amazon-assets');
|
|
@@ -551,6 +601,74 @@ function printHelp() {
|
|
|
551
601
|
console.log(' node take-screen-image.js --delete 3,4,5');
|
|
552
602
|
console.log('');
|
|
553
603
|
console.log('A normal capture automatically creates any missing Amazon, Samsung, and Play Store assets.');
|
|
604
|
+
console.log('Frame defaults: Android graphite (A), iPhone midnight (D).');
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
async function chooseFrameStyles(options) {
|
|
608
|
+
const choices = { android: options.androidFrame, ios: options.iosFrame };
|
|
609
|
+
const defaults = { android: 'graphite', ios: 'midnight' };
|
|
610
|
+
const aliases = {
|
|
611
|
+
android: { a: 'graphite', b: 'cobalt', 1: 'graphite', 2: 'cobalt' },
|
|
612
|
+
ios: { c: 'titanium', d: 'midnight', 1: 'titanium', 2: 'midnight' },
|
|
613
|
+
};
|
|
614
|
+
const canPrompt = process.stdin.isTTY && !options.prefix;
|
|
615
|
+
const terminal = canPrompt ? readline.createInterface({ input: process.stdin, output: process.stdout }) : null;
|
|
616
|
+
|
|
617
|
+
try {
|
|
618
|
+
for (const platform of ['android', 'ios']) {
|
|
619
|
+
if (!choices[platform] && terminal) {
|
|
620
|
+
const names = Object.values(frameStyles[platform]).map((style) => style.label);
|
|
621
|
+
choices[platform] = await terminal.question(
|
|
622
|
+
`${platform === 'ios' ? 'iPhone' : 'Android'} frame (${names.join(' / ')}), Enter for ${defaults[platform]}: `,
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
choices[platform] = (choices[platform] || defaults[platform]).trim().toLowerCase();
|
|
626
|
+
choices[platform] = aliases[platform][choices[platform]] || choices[platform];
|
|
627
|
+
if (!frameStyles[platform][choices[platform]]) {
|
|
628
|
+
throw new Error(`Unknown ${platform} frame: ${choices[platform]}. Choose ${Object.keys(frameStyles[platform]).join(' or ')}.`);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
} finally {
|
|
632
|
+
if (terminal) terminal.close();
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
console.log(`Frame selection: Android ${choices.android}, iPhone ${choices.ios}.`);
|
|
636
|
+
return choices;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
async function prepareFrameSelection(options) {
|
|
640
|
+
if (process.stdin.isTTY) {
|
|
641
|
+
const samplePaths = await createFrameSamples(autoScreenshotDirectory);
|
|
642
|
+
for (const samplePath of samplePaths) console.log(`Frame choices: ${samplePath}`);
|
|
643
|
+
}
|
|
644
|
+
selectedFrameStyles = await chooseFrameStyles(options);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function getFrameDisplayTime(platform) {
|
|
648
|
+
const time = new Date(frameTimeBase + frameCaptureSequence * 60000);
|
|
649
|
+
frameCaptureSequence += 1;
|
|
650
|
+
if (platform === 'ipad') {
|
|
651
|
+
const weekdays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
652
|
+
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
653
|
+
return `${String(time.getHours()).padStart(2, '0')}:${String(time.getMinutes()).padStart(2, '0')} `
|
|
654
|
+
+ `${weekdays[time.getDay()]} ${time.getDate()} ${months[time.getMonth()]}`;
|
|
655
|
+
}
|
|
656
|
+
const hour = platform === 'ios' ? (time.getHours() % 12 || 12) : time.getHours();
|
|
657
|
+
return `${String(hour).padStart(2, '0')}:${String(time.getMinutes()).padStart(2, '0')}`;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function getFrameBatteryPercentage() {
|
|
661
|
+
try {
|
|
662
|
+
const deviceId = getConnectedDevice();
|
|
663
|
+
const batteryDetails = runAdb(['-s', deviceId, 'shell', 'dumpsys', 'battery']);
|
|
664
|
+
const level = Number(batteryDetails.match(/^\s*level:\s*(\d+)/m)?.[1]);
|
|
665
|
+
|
|
666
|
+
if (Number.isInteger(level) && level >= 0 && level <= 100) return level;
|
|
667
|
+
} catch {
|
|
668
|
+
// Existing screenshots can still be framed when no Android device is connected.
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
return 75;
|
|
554
672
|
}
|
|
555
673
|
|
|
556
674
|
function deleteMatchingImages(input) {
|
|
@@ -585,10 +703,92 @@ function deleteMatchingImages(input) {
|
|
|
585
703
|
}
|
|
586
704
|
|
|
587
705
|
scanDirectory(autoScreenshotDirectory);
|
|
706
|
+
removeScreenshotRecords(nameSet);
|
|
588
707
|
console.log(`Deleted ${deletedFiles.length} matching image(s) from ${autoScreenshotDirectory}.`);
|
|
589
708
|
for (const filePath of deletedFiles) console.log(`Deleted: ${filePath}`);
|
|
590
709
|
}
|
|
591
710
|
|
|
711
|
+
function readScreenshotRecordSource(filePath) {
|
|
712
|
+
if (!fs.existsSync(filePath)) return { screenshots: {} };
|
|
713
|
+
|
|
714
|
+
try {
|
|
715
|
+
const source = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
716
|
+
return source && typeof source === 'object' ? source : { screenshots: {} };
|
|
717
|
+
} catch (error) {
|
|
718
|
+
throw new Error(`Cannot reconcile invalid JSON file ${filePath}: ${error.message}`);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function removeScreenshotRecords(names) {
|
|
723
|
+
for (const [filePath, label] of [
|
|
724
|
+
[screenshotCopyPath, 'screenshot copy'],
|
|
725
|
+
[screenshotInfoPath, 'screenshot information'],
|
|
726
|
+
]) {
|
|
727
|
+
const source = readScreenshotRecordSource(filePath);
|
|
728
|
+
const screenshots = source.screenshots && typeof source.screenshots === 'object'
|
|
729
|
+
? source.screenshots
|
|
730
|
+
: {};
|
|
731
|
+
const removedNames = [];
|
|
732
|
+
|
|
733
|
+
for (const name of names) {
|
|
734
|
+
if (!Object.hasOwn(screenshots, name)) continue;
|
|
735
|
+
delete screenshots[name];
|
|
736
|
+
removedNames.push(name);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
if (!removedNames.length) continue;
|
|
740
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
741
|
+
fs.writeFileSync(filePath, `${JSON.stringify({ ...source, screenshots }, null, 2)}\n`);
|
|
742
|
+
console.log(
|
|
743
|
+
`Removed stale ${label} record(s) ${removedNames.join(', ')} from ${filePath}.`,
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
screenshotCopyConfig = undefined;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function getCapturedScreenshotNames() {
|
|
751
|
+
const capturedNames = new Set();
|
|
752
|
+
|
|
753
|
+
if (!fs.existsSync(screenshotDirectory)) return capturedNames;
|
|
754
|
+
|
|
755
|
+
const scanDirectory = (directory) => {
|
|
756
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
757
|
+
const entryPath = path.join(directory, entry.name);
|
|
758
|
+
if (entry.isDirectory()) {
|
|
759
|
+
scanDirectory(entryPath);
|
|
760
|
+
} else if (/\.(?:png|jpe?g|webp)$/i.test(entry.name)) {
|
|
761
|
+
capturedNames.add(path.parse(entry.name).name);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
};
|
|
765
|
+
|
|
766
|
+
scanDirectory(screenshotDirectory);
|
|
767
|
+
return capturedNames;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function reconcileScreenshotRecords() {
|
|
771
|
+
const capturedNames = getCapturedScreenshotNames();
|
|
772
|
+
const configuredNames = new Set();
|
|
773
|
+
|
|
774
|
+
for (const filePath of [screenshotCopyPath, screenshotInfoPath]) {
|
|
775
|
+
const source = readScreenshotRecordSource(filePath);
|
|
776
|
+
const screenshots = source.screenshots && typeof source.screenshots === 'object'
|
|
777
|
+
? source.screenshots
|
|
778
|
+
: {};
|
|
779
|
+
Object.keys(screenshots).forEach((name) => configuredNames.add(name));
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
const staleNames = new Set(
|
|
783
|
+
[...configuredNames].filter((name) => !capturedNames.has(name)),
|
|
784
|
+
);
|
|
785
|
+
removeScreenshotRecords(staleNames);
|
|
786
|
+
|
|
787
|
+
if (!staleNames.size) {
|
|
788
|
+
console.log('Screenshot metadata and copy records match the captured screenshots.');
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
592
792
|
function selectProfiles(input) {
|
|
593
793
|
const orientation = getConfiguredOrientation();
|
|
594
794
|
const names = input
|
|
@@ -750,6 +950,266 @@ async function capturePageSnapshot(client) {
|
|
|
750
950
|
return canvas.toDataURL('image/png');
|
|
751
951
|
};
|
|
752
952
|
|
|
953
|
+
const normalizeUiText = (value) => String(value || '')
|
|
954
|
+
.replace(/\\s+/g, ' ')
|
|
955
|
+
.trim();
|
|
956
|
+
|
|
957
|
+
const isVisible = (element) => {
|
|
958
|
+
if (!element) return false;
|
|
959
|
+
const style = getComputedStyle(element);
|
|
960
|
+
const bounds = element.getBoundingClientRect();
|
|
961
|
+
return style.display !== 'none'
|
|
962
|
+
&& style.visibility !== 'hidden'
|
|
963
|
+
&& Number(style.opacity || 1) > 0
|
|
964
|
+
&& bounds.width > 0
|
|
965
|
+
&& bounds.height > 0
|
|
966
|
+
&& bounds.bottom > 0
|
|
967
|
+
&& bounds.right > 0
|
|
968
|
+
&& bounds.top < innerHeight
|
|
969
|
+
&& bounds.left < innerWidth;
|
|
970
|
+
};
|
|
971
|
+
|
|
972
|
+
const isSafeUiText = (value) => {
|
|
973
|
+
const text = normalizeUiText(value);
|
|
974
|
+
if (text.length < 2 || text.length > 110) return false;
|
|
975
|
+
if (/(?:https?:\\/\\/|www\\.|@|(?:^|\\s)[a-z]:\\\\)/i.test(text)) return false;
|
|
976
|
+
if (/\\.(?:jpe?g|png|webp|gif|heic|zip|pdf|mp4|mov)(?:\\s|$)/i.test(text)) return false;
|
|
977
|
+
const digitCount = (text.match(/\\d/g) || []).length;
|
|
978
|
+
return digitCount <= Math.max(4, Math.floor(text.length * 0.25));
|
|
979
|
+
};
|
|
980
|
+
|
|
981
|
+
const collectUiText = (root, selector, limit) => {
|
|
982
|
+
if (!root) return [];
|
|
983
|
+
const values = [];
|
|
984
|
+
const seen = new Set();
|
|
985
|
+
for (const element of root.querySelectorAll(selector)) {
|
|
986
|
+
if (!isVisible(element)) continue;
|
|
987
|
+
const value = normalizeUiText(
|
|
988
|
+
element.getAttribute('aria-label')
|
|
989
|
+
|| element.getAttribute('title')
|
|
990
|
+
|| element.textContent,
|
|
991
|
+
);
|
|
992
|
+
const key = value.toLowerCase();
|
|
993
|
+
if (!isSafeUiText(value) || seen.has(key)) continue;
|
|
994
|
+
seen.add(key);
|
|
995
|
+
values.push(value);
|
|
996
|
+
if (values.length >= limit) break;
|
|
997
|
+
}
|
|
998
|
+
return values;
|
|
999
|
+
};
|
|
1000
|
+
|
|
1001
|
+
const getElementLabel = (element) => {
|
|
1002
|
+
const explicitLabel = normalizeUiText(
|
|
1003
|
+
element.getAttribute('aria-label') || element.getAttribute('title'),
|
|
1004
|
+
);
|
|
1005
|
+
if (isSafeUiText(explicitLabel)) return explicitLabel;
|
|
1006
|
+
|
|
1007
|
+
const labelRoot = element.matches('input, select, textarea')
|
|
1008
|
+
? element.closest('label, .item-content, .input')
|
|
1009
|
+
: element;
|
|
1010
|
+
const preferredLabel = labelRoot?.querySelector(
|
|
1011
|
+
'.item-title, .button-label, strong, .dialog-button-text',
|
|
1012
|
+
);
|
|
1013
|
+
const textRoot = preferredLabel || labelRoot || element;
|
|
1014
|
+
const clone = textRoot.cloneNode(true);
|
|
1015
|
+
clone.querySelectorAll('small, .item-subtitle, svg, img').forEach((child) => child.remove());
|
|
1016
|
+
const text = normalizeUiText(clone.textContent);
|
|
1017
|
+
return isSafeUiText(text) ? text : '';
|
|
1018
|
+
};
|
|
1019
|
+
|
|
1020
|
+
const getElementContext = (element) => {
|
|
1021
|
+
if (element.closest('.dialog, .popup, .sheet-modal, .popover, .actions-modal')) return 'overlay';
|
|
1022
|
+
if (element.closest('.navbar')) return 'navbar';
|
|
1023
|
+
if (element.closest('.toolbar')) return 'toolbar';
|
|
1024
|
+
return 'content';
|
|
1025
|
+
};
|
|
1026
|
+
|
|
1027
|
+
const getStableClasses = (element) => [...element.classList]
|
|
1028
|
+
.filter((className) => !/^(?:active-state|ripple-wave|focus-visible)$/.test(className))
|
|
1029
|
+
.slice(0, 12);
|
|
1030
|
+
|
|
1031
|
+
const collectButtons = (root, limit) => {
|
|
1032
|
+
if (!root) return [];
|
|
1033
|
+
const records = [];
|
|
1034
|
+
const seen = new Set();
|
|
1035
|
+
const selector = 'button, a.button, [role="button"], .list-button, .dialog-button';
|
|
1036
|
+
for (const element of root.querySelectorAll(selector)) {
|
|
1037
|
+
if (!isVisible(element)) continue;
|
|
1038
|
+
const label = getElementLabel(element);
|
|
1039
|
+
if (!label) continue;
|
|
1040
|
+
const context = getElementContext(element);
|
|
1041
|
+
const key = [label.toLowerCase(), context, element.id].join('|');
|
|
1042
|
+
if (seen.has(key)) continue;
|
|
1043
|
+
seen.add(key);
|
|
1044
|
+
const classes = getStableClasses(element);
|
|
1045
|
+
const classText = classes.join(' ');
|
|
1046
|
+
const prominence = /(?:button-fill|primary|main-action|save-all)/i.test(classText)
|
|
1047
|
+
? 'primary'
|
|
1048
|
+
: /(?:button-large|dialog-button-bold)/i.test(classText)
|
|
1049
|
+
? 'major'
|
|
1050
|
+
: /(?:button-outline|secondary)/i.test(classText)
|
|
1051
|
+
? 'secondary'
|
|
1052
|
+
: 'standard';
|
|
1053
|
+
records.push({
|
|
1054
|
+
label,
|
|
1055
|
+
id: element.id || '',
|
|
1056
|
+
context,
|
|
1057
|
+
prominence,
|
|
1058
|
+
disabled: Boolean(element.disabled || element.getAttribute('aria-disabled') === 'true'),
|
|
1059
|
+
selected: element.getAttribute('aria-pressed') === 'true'
|
|
1060
|
+
|| element.getAttribute('aria-selected') === 'true'
|
|
1061
|
+
|| element.classList.contains('tab-link-active'),
|
|
1062
|
+
classes,
|
|
1063
|
+
});
|
|
1064
|
+
if (records.length >= limit) break;
|
|
1065
|
+
}
|
|
1066
|
+
return records;
|
|
1067
|
+
};
|
|
1068
|
+
|
|
1069
|
+
const collectInputs = (root, limit) => {
|
|
1070
|
+
if (!root) return [];
|
|
1071
|
+
const records = [];
|
|
1072
|
+
for (const element of root.querySelectorAll('input, select, textarea')) {
|
|
1073
|
+
if (!isVisible(element) || element.type === 'hidden' || element.type === 'file') continue;
|
|
1074
|
+
const labelRoot = element.closest('label, .item-content, .input');
|
|
1075
|
+
const description = collectUiText(
|
|
1076
|
+
labelRoot,
|
|
1077
|
+
'small, .item-subtitle, .item-after',
|
|
1078
|
+
1,
|
|
1079
|
+
)[0] || '';
|
|
1080
|
+
const record = {
|
|
1081
|
+
label: getElementLabel(element),
|
|
1082
|
+
id: element.id || '',
|
|
1083
|
+
name: normalizeUiText(element.name),
|
|
1084
|
+
type: element.tagName.toLowerCase() === 'select' ? 'select' : element.type || 'text',
|
|
1085
|
+
context: getElementContext(element),
|
|
1086
|
+
disabled: Boolean(element.disabled),
|
|
1087
|
+
description,
|
|
1088
|
+
};
|
|
1089
|
+
if (element.type === 'checkbox' || element.type === 'radio') {
|
|
1090
|
+
record.checked = Boolean(element.checked);
|
|
1091
|
+
const safeValue = normalizeUiText(element.value);
|
|
1092
|
+
if (isSafeUiText(safeValue)) record.value = safeValue;
|
|
1093
|
+
} else if (element.tagName.toLowerCase() === 'select') {
|
|
1094
|
+
const selectedText = normalizeUiText(element.selectedOptions?.[0]?.textContent);
|
|
1095
|
+
if (isSafeUiText(selectedText)) record.selectedOption = selectedText;
|
|
1096
|
+
}
|
|
1097
|
+
records.push(record);
|
|
1098
|
+
if (records.length >= limit) break;
|
|
1099
|
+
}
|
|
1100
|
+
return records;
|
|
1101
|
+
};
|
|
1102
|
+
|
|
1103
|
+
const visiblePages = [...document.querySelectorAll('.page')].filter(isVisible);
|
|
1104
|
+
const activePage = visiblePages.find((page) => page.classList.contains('page-current'))
|
|
1105
|
+
|| visiblePages[visiblePages.length - 1]
|
|
1106
|
+
|| null;
|
|
1107
|
+
const overlaySelector = [
|
|
1108
|
+
'.dialog.modal-in',
|
|
1109
|
+
'.popup.modal-in',
|
|
1110
|
+
'.sheet-modal.modal-in',
|
|
1111
|
+
'.popover.modal-in',
|
|
1112
|
+
'.actions-modal.modal-in',
|
|
1113
|
+
'.login-screen.modal-in',
|
|
1114
|
+
].join(',');
|
|
1115
|
+
const visibleOverlays = [...document.querySelectorAll(overlaySelector)].filter(isVisible);
|
|
1116
|
+
const activeOverlay = visibleOverlays[visibleOverlays.length - 1] || null;
|
|
1117
|
+
const overlayType = activeOverlay
|
|
1118
|
+
? ['dialog', 'popup', 'sheet-modal', 'popover', 'actions-modal', 'login-screen']
|
|
1119
|
+
.find((className) => activeOverlay.classList.contains(className)) || 'overlay'
|
|
1120
|
+
: '';
|
|
1121
|
+
const textSelector = [
|
|
1122
|
+
'h1', 'h2', 'h3',
|
|
1123
|
+
'.title', '.dialog-title', '.dialog-text',
|
|
1124
|
+
'legend', 'strong', '.t-output-choice-heading',
|
|
1125
|
+
'.item-title', '.item-subtitle', '.item-after',
|
|
1126
|
+
'label', 'button', '[role="button"]',
|
|
1127
|
+
'p', 'small', 'option:checked',
|
|
1128
|
+
].join(',');
|
|
1129
|
+
const pageHeadings = collectUiText(
|
|
1130
|
+
activePage,
|
|
1131
|
+
'h1, h2, h3, .navbar .title, .t-section-heading, .headTitle',
|
|
1132
|
+
10,
|
|
1133
|
+
);
|
|
1134
|
+
const pageControls = collectUiText(
|
|
1135
|
+
activePage,
|
|
1136
|
+
'button, [role="button"], label, .item-title, .item-subtitle, .list-button, option:checked',
|
|
1137
|
+
30,
|
|
1138
|
+
);
|
|
1139
|
+
const overlayText = collectUiText(activeOverlay, textSelector, 35);
|
|
1140
|
+
const pageButtons = collectButtons(activePage, 40);
|
|
1141
|
+
const overlayButtons = collectButtons(activeOverlay, 20);
|
|
1142
|
+
const pageInputs = collectInputs(activePage, 30);
|
|
1143
|
+
const overlayInputs = collectInputs(activeOverlay, 25);
|
|
1144
|
+
const f7Route = activePage?.f7Page?.route
|
|
1145
|
+
|| activePage?.f7Page?.router?.currentRoute
|
|
1146
|
+
|| null;
|
|
1147
|
+
const framework7Route = f7Route ? {
|
|
1148
|
+
url: normalizeUiText(f7Route.url),
|
|
1149
|
+
path: normalizeUiText(f7Route.path || f7Route.route?.path),
|
|
1150
|
+
name: normalizeUiText(f7Route.name || f7Route.route?.name),
|
|
1151
|
+
params: Object.keys(f7Route.params || {}),
|
|
1152
|
+
query: Object.keys(f7Route.query || {}),
|
|
1153
|
+
} : null;
|
|
1154
|
+
const screenInfo = {
|
|
1155
|
+
documentTitle: normalizeUiText(document.title),
|
|
1156
|
+
route: {
|
|
1157
|
+
pathname: location.pathname,
|
|
1158
|
+
hash: location.hash,
|
|
1159
|
+
framework7: framework7Route,
|
|
1160
|
+
},
|
|
1161
|
+
page: activePage ? {
|
|
1162
|
+
dataName: activePage.getAttribute('data-name') || '',
|
|
1163
|
+
id: activePage.id || '',
|
|
1164
|
+
classes: getStableClasses(activePage),
|
|
1165
|
+
navbarTitle: collectUiText(activePage, '.navbar .title', 1)[0] || '',
|
|
1166
|
+
headings: pageHeadings,
|
|
1167
|
+
sections: collectUiText(
|
|
1168
|
+
activePage,
|
|
1169
|
+
'h1, h2, h3, legend, .t-section-heading, .t-output-choice-heading, .headTitle',
|
|
1170
|
+
20,
|
|
1171
|
+
),
|
|
1172
|
+
descriptions: collectUiText(
|
|
1173
|
+
activePage,
|
|
1174
|
+
'p, small, .item-subtitle, .item-after, .t-settings-footnote',
|
|
1175
|
+
30,
|
|
1176
|
+
),
|
|
1177
|
+
controls: pageControls,
|
|
1178
|
+
buttons: pageButtons,
|
|
1179
|
+
primaryButtons: pageButtons.filter((button) => (
|
|
1180
|
+
button.prominence === 'primary' || button.prominence === 'major'
|
|
1181
|
+
)),
|
|
1182
|
+
inputs: pageInputs,
|
|
1183
|
+
obscuredByOverlay: Boolean(activeOverlay),
|
|
1184
|
+
} : null,
|
|
1185
|
+
overlay: activeOverlay ? {
|
|
1186
|
+
type: overlayType,
|
|
1187
|
+
id: activeOverlay.id || '',
|
|
1188
|
+
classes: getStableClasses(activeOverlay),
|
|
1189
|
+
title: collectUiText(activeOverlay, '.dialog-title, .navbar .title, h1, h2, h3', 1)[0] || '',
|
|
1190
|
+
sections: collectUiText(
|
|
1191
|
+
activeOverlay,
|
|
1192
|
+
'h1, h2, h3, legend, strong, .t-section-heading, .t-output-choice-heading',
|
|
1193
|
+
20,
|
|
1194
|
+
),
|
|
1195
|
+
visibleText: overlayText,
|
|
1196
|
+
buttons: overlayButtons,
|
|
1197
|
+
primaryButtons: overlayButtons.filter((button) => (
|
|
1198
|
+
button.prominence === 'primary' || button.prominence === 'major'
|
|
1199
|
+
)),
|
|
1200
|
+
inputs: overlayInputs,
|
|
1201
|
+
} : null,
|
|
1202
|
+
theme: document.documentElement.classList.contains('theme-dark')
|
|
1203
|
+
|| document.body.classList.contains('theme-dark')
|
|
1204
|
+
? 'dark'
|
|
1205
|
+
: 'light',
|
|
1206
|
+
viewport: {
|
|
1207
|
+
width: innerWidth,
|
|
1208
|
+
height: innerHeight,
|
|
1209
|
+
devicePixelRatio,
|
|
1210
|
+
},
|
|
1211
|
+
};
|
|
1212
|
+
|
|
753
1213
|
const serializeDocument = async (sourceDocument) => {
|
|
754
1214
|
const clonedRoot = sourceDocument.documentElement.cloneNode(true);
|
|
755
1215
|
const originalElements = [
|
|
@@ -892,6 +1352,7 @@ async function capturePageSnapshot(client) {
|
|
|
892
1352
|
return {
|
|
893
1353
|
html: '<!doctype html>' + html,
|
|
894
1354
|
scrollY,
|
|
1355
|
+
screenInfo,
|
|
895
1356
|
};
|
|
896
1357
|
})()`,
|
|
897
1358
|
awaitPromise: true,
|
|
@@ -1190,17 +1651,27 @@ async function waitForReplayLayout(client, snapshot) {
|
|
|
1190
1651
|
|
|
1191
1652
|
function getFrameAdornment(frameType) {
|
|
1192
1653
|
if (frameType === 'iphone') {
|
|
1193
|
-
return '
|
|
1654
|
+
return '';
|
|
1194
1655
|
}
|
|
1195
1656
|
|
|
1196
1657
|
if (frameType === 'android-phone') {
|
|
1197
|
-
return '
|
|
1658
|
+
return '';
|
|
1198
1659
|
}
|
|
1199
1660
|
|
|
1200
1661
|
if (frameType === 'tablet') {
|
|
1201
1662
|
return '<div class="tablet-camera"></div>';
|
|
1202
1663
|
}
|
|
1203
1664
|
|
|
1665
|
+
if (frameType === 'ipad') {
|
|
1666
|
+
return `<div class="ipad-camera-array" aria-hidden="true">
|
|
1667
|
+
<span class="ipad-sensor ipad-sensor-large"></span>
|
|
1668
|
+
<span class="ipad-sensor ipad-sensor-large"></span>
|
|
1669
|
+
<span class="ipad-sensor ipad-sensor-lens"></span>
|
|
1670
|
+
<span class="ipad-sensor ipad-sensor-small"></span>
|
|
1671
|
+
<span class="ipad-sensor ipad-sensor-large"></span>
|
|
1672
|
+
</div><div class="ipad-side-button" aria-hidden="true"></div>`;
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1204
1675
|
if (frameType === 'tv' || frameType === 'apple-tv') {
|
|
1205
1676
|
return '<div class="tv-stand"></div>';
|
|
1206
1677
|
}
|
|
@@ -1220,10 +1691,167 @@ function getFrameAdornment(frameType) {
|
|
|
1220
1691
|
return '<div class="xr-status"></div>';
|
|
1221
1692
|
}
|
|
1222
1693
|
|
|
1223
|
-
function
|
|
1694
|
+
async function getScreenshotBarStyle(screenshotBuffer, edge = 'top') {
|
|
1695
|
+
const metadata = await sharp(screenshotBuffer).metadata();
|
|
1696
|
+
const { data, info } = await sharp(screenshotBuffer)
|
|
1697
|
+
.extract({
|
|
1698
|
+
left: 0,
|
|
1699
|
+
top: edge === 'bottom' ? metadata.height - 1 : 0,
|
|
1700
|
+
width: 1,
|
|
1701
|
+
height: 1,
|
|
1702
|
+
})
|
|
1703
|
+
.removeAlpha()
|
|
1704
|
+
.raw()
|
|
1705
|
+
.toBuffer({ resolveWithObject: true });
|
|
1706
|
+
const color = [data[0], data[1], data[2]];
|
|
1707
|
+
const luminance = color
|
|
1708
|
+
.map((value) => value / 255)
|
|
1709
|
+
.map((value) => (value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4))
|
|
1710
|
+
.reduce((total, value, index) => total + value * [0.2126, 0.7152, 0.0722][index], 0);
|
|
1711
|
+
|
|
1712
|
+
if (info.channels < 3) {
|
|
1713
|
+
throw new Error('Unable to read the screenshot starting pixel color.');
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
return {
|
|
1717
|
+
background: `rgb(${color.join(', ')})`,
|
|
1718
|
+
foreground: luminance > 0.179 ? '#050505' : '#ffffff',
|
|
1719
|
+
};
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
function getScreenshotCopy(screenshotFileName) {
|
|
1723
|
+
screenshotCopyConfig ||= readJsonAssetSource(screenshotCopyPath);
|
|
1724
|
+
const screenshotKey = path.parse(screenshotFileName).name;
|
|
1725
|
+
const configuredCopy = screenshotCopyConfig.screenshots?.[screenshotKey];
|
|
1726
|
+
|
|
1727
|
+
if (!configuredCopy || typeof configuredCopy !== 'object') return null;
|
|
1728
|
+
|
|
1729
|
+
const title = cleanAssetCopy(configuredCopy.title, 56);
|
|
1730
|
+
const subtitle = cleanAssetCopy(configuredCopy.subtitle, 110);
|
|
1731
|
+
const features = Array.isArray(configuredCopy.features)
|
|
1732
|
+
? configuredCopy.features
|
|
1733
|
+
.map((feature) => cleanAssetCopy(feature, 28))
|
|
1734
|
+
.filter(Boolean)
|
|
1735
|
+
.slice(0, 3)
|
|
1736
|
+
: [];
|
|
1737
|
+
|
|
1738
|
+
if (!title && !subtitle && !features.length) return null;
|
|
1739
|
+
|
|
1740
|
+
return { title, subtitle, features };
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
function createScreenshotCopyMarkup(screenshotCopy) {
|
|
1744
|
+
if (!screenshotCopy) return '';
|
|
1745
|
+
|
|
1746
|
+
const featureMarkup = screenshotCopy.features
|
|
1747
|
+
.map((feature) => `<span>${escapeHtml(feature)}</span>`)
|
|
1748
|
+
.join('');
|
|
1749
|
+
|
|
1750
|
+
return `<section class="promo-copy">
|
|
1751
|
+
${screenshotCopy.title ? `<h1>${escapeHtml(screenshotCopy.title)}</h1>` : ''}
|
|
1752
|
+
${screenshotCopy.subtitle ? `<p>${escapeHtml(screenshotCopy.subtitle)}</p>` : ''}
|
|
1753
|
+
${featureMarkup ? `<div class="promo-features">${featureMarkup}</div>` : ''}
|
|
1754
|
+
</section>`;
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
function rgbToHsl(red, green, blue) {
|
|
1758
|
+
const values = [red, green, blue].map((value) => value / 255);
|
|
1759
|
+
const maximum = Math.max(...values);
|
|
1760
|
+
const minimum = Math.min(...values);
|
|
1761
|
+
const lightness = (maximum + minimum) / 2;
|
|
1762
|
+
const delta = maximum - minimum;
|
|
1763
|
+
if (!delta) return { hue: 230, saturation: 0, lightness };
|
|
1764
|
+
|
|
1765
|
+
const saturation = delta / (1 - Math.abs(2 * lightness - 1));
|
|
1766
|
+
let hue;
|
|
1767
|
+
if (maximum === values[0]) hue = 60 * (((values[1] - values[2]) / delta) % 6);
|
|
1768
|
+
else if (maximum === values[1]) hue = 60 * ((values[2] - values[0]) / delta + 2);
|
|
1769
|
+
else hue = 60 * ((values[0] - values[1]) / delta + 4);
|
|
1770
|
+
return { hue: (hue + 360) % 360, saturation, lightness };
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
async function getPromotionalPalette(screenshotBuffer, screenshotFileName) {
|
|
1774
|
+
const { data, info } = await sharp(screenshotBuffer)
|
|
1775
|
+
.resize(36, 36, { fit: 'fill' })
|
|
1776
|
+
.removeAlpha()
|
|
1777
|
+
.raw()
|
|
1778
|
+
.toBuffer({ resolveWithObject: true });
|
|
1779
|
+
const colorBuckets = new Map();
|
|
1780
|
+
|
|
1781
|
+
for (let index = 0; index < data.length; index += info.channels) {
|
|
1782
|
+
const red = data[index];
|
|
1783
|
+
const green = data[index + 1];
|
|
1784
|
+
const blue = data[index + 2];
|
|
1785
|
+
const hsl = rgbToHsl(red, green, blue);
|
|
1786
|
+
if (hsl.lightness < 0.1 || hsl.lightness > 0.94 || hsl.saturation < 0.12) continue;
|
|
1787
|
+
const key = `${Math.round(red / 24)},${Math.round(green / 24)},${Math.round(blue / 24)}`;
|
|
1788
|
+
const bucket = colorBuckets.get(key) || {
|
|
1789
|
+
count: 0, red: 0, green: 0, blue: 0, saturation: 0, lightness: 0,
|
|
1790
|
+
};
|
|
1791
|
+
bucket.count += 1;
|
|
1792
|
+
bucket.red += red;
|
|
1793
|
+
bucket.green += green;
|
|
1794
|
+
bucket.blue += blue;
|
|
1795
|
+
bucket.saturation += hsl.saturation;
|
|
1796
|
+
bucket.lightness += hsl.lightness;
|
|
1797
|
+
colorBuckets.set(key, bucket);
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
let selectedColor = { red: 85, green: 70, blue: 232 };
|
|
1801
|
+
let selectedScore = -1;
|
|
1802
|
+
for (const bucket of colorBuckets.values()) {
|
|
1803
|
+
const saturation = bucket.saturation / bucket.count;
|
|
1804
|
+
const lightness = bucket.lightness / bucket.count;
|
|
1805
|
+
const score = bucket.count * (0.55 + saturation * 1.8)
|
|
1806
|
+
* (1 - Math.abs(lightness - 0.55) * 0.7);
|
|
1807
|
+
if (score <= selectedScore) continue;
|
|
1808
|
+
selectedScore = score;
|
|
1809
|
+
selectedColor = {
|
|
1810
|
+
red: Math.round(bucket.red / bucket.count),
|
|
1811
|
+
green: Math.round(bucket.green / bucket.count),
|
|
1812
|
+
blue: Math.round(bucket.blue / bucket.count),
|
|
1813
|
+
};
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
const seed = getDecorationSeed(path.parse(screenshotFileName).name);
|
|
1817
|
+
const sourceHsl = rgbToHsl(selectedColor.red, selectedColor.green, selectedColor.blue);
|
|
1818
|
+
const hue = Math.round((sourceHsl.hue + (seed % 25) - 12 + 360) % 360);
|
|
1819
|
+
const secondaryHue = (hue + [38, 56, 142, 205][seed % 4]) % 360;
|
|
1820
|
+
const saturation = Math.max(58, Math.min(82, Math.round(sourceHsl.saturation * 100)));
|
|
1821
|
+
|
|
1822
|
+
return {
|
|
1823
|
+
seed,
|
|
1824
|
+
accent: `hsl(${hue} ${saturation}% 46%)`,
|
|
1825
|
+
accentSoft: `hsla(${hue} ${saturation}% 55% / 0.2)`,
|
|
1826
|
+
secondary: `hsl(${secondaryHue} 70% 54%)`,
|
|
1827
|
+
secondarySoft: `hsla(${secondaryHue} 72% 58% / 0.18)`,
|
|
1828
|
+
backgroundStart: `hsl(${hue} 68% 97%)`,
|
|
1829
|
+
backgroundMiddle: `hsl(${(hue + 14) % 360} 58% 93%)`,
|
|
1830
|
+
backgroundEnd: `hsl(${secondaryHue} 62% 88%)`,
|
|
1831
|
+
text: `hsl(${hue} 35% 17%)`,
|
|
1832
|
+
mutedText: `hsl(${hue} 18% 38%)`,
|
|
1833
|
+
};
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
function createStoreDeviceFrameHtml(
|
|
1837
|
+
profile,
|
|
1838
|
+
screenshotBuffer,
|
|
1839
|
+
imageWidth,
|
|
1840
|
+
imageHeight,
|
|
1841
|
+
canvasWidth,
|
|
1842
|
+
canvasHeight,
|
|
1843
|
+
imageFormat,
|
|
1844
|
+
displayTime,
|
|
1845
|
+
batteryPercentage,
|
|
1846
|
+
statusBarStyle,
|
|
1847
|
+
navigationBarStyle,
|
|
1848
|
+
screenshotCopy,
|
|
1849
|
+
palette,
|
|
1850
|
+
) {
|
|
1224
1851
|
const screenshotDataUrl = `data:image/${imageFormat === 'jpeg' ? 'jpeg' : 'png'};base64,${screenshotBuffer.toString('base64')}`;
|
|
1225
|
-
const
|
|
1226
|
-
const
|
|
1852
|
+
const availableDeviceHeight = screenshotCopy ? canvasHeight * 0.64 : canvasHeight - 180;
|
|
1853
|
+
const screenWidth = Math.floor(Math.min(canvasWidth - 160, availableDeviceHeight * imageWidth / imageHeight));
|
|
1854
|
+
const frameStyle = frameStyles.android[selectedFrameStyles.android];
|
|
1227
1855
|
|
|
1228
1856
|
return `<!doctype html>
|
|
1229
1857
|
<html>
|
|
@@ -1235,34 +1863,67 @@ function createStoreDeviceFrameHtml(profile, screenshotBuffer, imageWidth, image
|
|
|
1235
1863
|
body {
|
|
1236
1864
|
display: grid;
|
|
1237
1865
|
place-items: center;
|
|
1238
|
-
|
|
1866
|
+
position: relative;
|
|
1867
|
+
background:
|
|
1868
|
+
radial-gradient(circle at 12% 16%, ${palette.accentSoft}, transparent 32%),
|
|
1869
|
+
radial-gradient(circle at 88% 84%, ${palette.secondarySoft}, transparent 34%),
|
|
1870
|
+
linear-gradient(145deg, ${palette.backgroundStart}, ${palette.backgroundMiddle} 52%, ${palette.backgroundEnd});
|
|
1239
1871
|
}
|
|
1240
1872
|
body::before {
|
|
1241
1873
|
content: '';
|
|
1242
1874
|
position: absolute;
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
background:
|
|
1247
|
-
|
|
1248
|
-
filter: blur(90px);
|
|
1875
|
+
inset: 0;
|
|
1876
|
+
opacity: 0.16;
|
|
1877
|
+
background-image: radial-gradient(${palette.accent} 1px, transparent 1px);
|
|
1878
|
+
background-size: 28px 28px;
|
|
1879
|
+
mask-image: linear-gradient(135deg, black, transparent 42%, transparent 64%, black);
|
|
1249
1880
|
}
|
|
1881
|
+
${getPromotionalDecorationStyles()}
|
|
1882
|
+
.promo-copy {
|
|
1883
|
+
position: absolute;
|
|
1884
|
+
z-index: 2;
|
|
1885
|
+
top: 4.5%;
|
|
1886
|
+
right: 7%;
|
|
1887
|
+
left: 7%;
|
|
1888
|
+
display: flex;
|
|
1889
|
+
flex-direction: column;
|
|
1890
|
+
align-items: center;
|
|
1891
|
+
color: ${palette.text};
|
|
1892
|
+
text-align: center;
|
|
1893
|
+
font-family: Arial, sans-serif;
|
|
1894
|
+
}
|
|
1895
|
+
.promo-copy h1 { max-width: 900px; margin: 0; font-size: clamp(42px, 5.6vw, 72px); line-height: 1.02; letter-spacing: -0.035em; }
|
|
1896
|
+
.promo-copy p { max-width: 850px; margin: 2.1% 0 0; color: ${palette.mutedText}; font-size: clamp(22px, 2.5vw, 32px); line-height: 1.35; }
|
|
1897
|
+
.promo-features { display: flex; flex-wrap: wrap; justify-content: center; gap: 12px; margin-top: 2.4%; }
|
|
1898
|
+
.promo-features span { padding: 10px 17px; border: 1px solid ${palette.accentSoft}; border-radius: 999px; background: rgba(255,255,255,0.72); color: ${palette.accent}; box-shadow: 0 8px 24px rgba(15,23,42,0.08); font: 700 clamp(16px, 1.8vw, 23px)/1 Arial, sans-serif; }
|
|
1899
|
+
body.has-copy .device { position: absolute; bottom: 3.5%; left: 50%; transform: translateX(-50%); }
|
|
1250
1900
|
.device {
|
|
1251
1901
|
position: relative;
|
|
1252
1902
|
z-index: 1;
|
|
1253
1903
|
width: ${screenWidth + 48}px;
|
|
1254
1904
|
padding: 34px 18px 20px;
|
|
1255
|
-
border: 6px solid
|
|
1905
|
+
border: 6px solid ${frameStyle.edge};
|
|
1256
1906
|
border-radius: 48px;
|
|
1257
|
-
background:
|
|
1907
|
+
background: ${frameStyle.outer};
|
|
1258
1908
|
box-shadow: 0 38px 85px rgba(0, 0, 0, 0.58), 0 0 0 2px rgba(255, 255, 255, 0.19) inset;
|
|
1259
1909
|
}
|
|
1260
1910
|
.screen {
|
|
1911
|
+
display: flex;
|
|
1912
|
+
flex-direction: column;
|
|
1261
1913
|
width: ${screenWidth}px;
|
|
1914
|
+
aspect-ratio: ${imageWidth} / ${imageHeight};
|
|
1915
|
+
overflow: hidden;
|
|
1262
1916
|
background: #fff;
|
|
1263
1917
|
box-shadow: 0 0 0 2px #03060c;
|
|
1264
1918
|
}
|
|
1265
|
-
.screen img { display: block; width: 100%; height:
|
|
1919
|
+
.screen img { display: block; width: 100%; height: 0; min-height: 0; flex: 1 1 0; object-fit: fill; }
|
|
1920
|
+
.phone-statusbar { position: relative; flex: none; height: 5%; display: flex; align-items: center; background: ${statusBarStyle.background}; color: ${statusBarStyle.foreground}; font-family: Arial, sans-serif; }
|
|
1921
|
+
.status-time { margin-left: 5%; font-size: ${Math.max(11, Math.round(screenWidth * 0.03))}px; white-space: nowrap; }
|
|
1922
|
+
.android-statusbar { height: 4%; font-family: Roboto, Arial, sans-serif; }
|
|
1923
|
+
.android-notification { width: 3.4%; height: 46%; margin-left: 2%; }
|
|
1924
|
+
.android-status-icons { width: 32%; height: 58%; margin-left: auto; margin-right: 4%; }
|
|
1925
|
+
.android-navigation { position: absolute; z-index: 2; right: 0; bottom: 0; left: 0; height: 2.2%; display: grid; place-items: center; background: transparent; }
|
|
1926
|
+
.gesture-pill { width: 40%; height: 14%; border-radius: 999px; background: ${navigationBarStyle.foreground}; }
|
|
1266
1927
|
.camera {
|
|
1267
1928
|
position: absolute;
|
|
1268
1929
|
top: 11px;
|
|
@@ -1285,19 +1946,112 @@ function createStoreDeviceFrameHtml(profile, screenshotBuffer, imageWidth, image
|
|
|
1285
1946
|
}
|
|
1286
1947
|
</style>
|
|
1287
1948
|
</head>
|
|
1288
|
-
<body>
|
|
1949
|
+
<body class="${screenshotCopy ? 'has-copy' : ''}">
|
|
1950
|
+
${createPromotionalDecorations(palette)}
|
|
1951
|
+
${createScreenshotCopyMarkup(screenshotCopy)}
|
|
1289
1952
|
<main class="device">
|
|
1290
1953
|
<div class="camera"></div>
|
|
1291
|
-
<div class="screen"
|
|
1954
|
+
<div class="screen">${getPhoneStatusBar('android', displayTime, batteryPercentage)}<img src="${screenshotDataUrl}" /><div class="android-navigation"><div class="gesture-pill"></div></div></div>
|
|
1292
1955
|
<div class="side-button"></div>
|
|
1293
1956
|
</main>
|
|
1294
1957
|
</body>
|
|
1295
1958
|
</html>`;
|
|
1296
1959
|
}
|
|
1297
1960
|
|
|
1298
|
-
function
|
|
1961
|
+
function getPhoneStatusBar(platform, time, batteryPercentage = null) {
|
|
1962
|
+
const batteryLevel = Number.isInteger(batteryPercentage) ? batteryPercentage : 75;
|
|
1963
|
+
|
|
1964
|
+
if (platform === 'ios') {
|
|
1965
|
+
return `<div class="phone-statusbar ios-statusbar">
|
|
1966
|
+
<span class="status-time">${time}</span>
|
|
1967
|
+
<div class="dynamic-island"></div>
|
|
1968
|
+
<svg viewBox="0 0 90 28" aria-hidden="true">
|
|
1969
|
+
<circle cx="5" cy="14" r="2" fill="currentColor" opacity="0.35"></circle><circle cx="13" cy="14" r="2" fill="currentColor" opacity="0.35"></circle>
|
|
1970
|
+
<circle cx="21" cy="14" r="2" fill="currentColor" opacity="0.35"></circle><circle cx="29" cy="14" r="2" fill="currentColor" opacity="0.35"></circle>
|
|
1971
|
+
<path d="M36 10 Q46 2 56 10 M39 14 Q46 8 53 14 M44 18 Q46 16 48 18" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"></path>
|
|
1972
|
+
<rect x="61" y="7" width="22" height="15" rx="4" fill="none" stroke="currentColor" stroke-width="1.5" opacity="0.65"></rect>
|
|
1973
|
+
<rect x="64" y="10" width="16" height="9" rx="2" fill="currentColor"></rect>
|
|
1974
|
+
<rect x="85" y="12" width="2" height="5" rx="1" fill="currentColor" opacity="0.65"></rect>
|
|
1975
|
+
</svg>
|
|
1976
|
+
</div>`;
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
if (platform === 'ipad') {
|
|
1980
|
+
const batteryFillWidth = Math.max(2, Math.round(24 * batteryLevel / 100));
|
|
1981
|
+
return `<div class="phone-statusbar ipad-statusbar">
|
|
1982
|
+
<span class="status-time">${time}</span>
|
|
1983
|
+
<svg class="ipad-status-icons" viewBox="0 0 130 28" aria-hidden="true">
|
|
1984
|
+
<g fill="currentColor">
|
|
1985
|
+
<rect x="2" y="19" width="3" height="5" rx="1"></rect>
|
|
1986
|
+
<rect x="7" y="15" width="3" height="9" rx="1"></rect>
|
|
1987
|
+
<rect x="12" y="11" width="3" height="13" rx="1"></rect>
|
|
1988
|
+
<rect x="17" y="7" width="3" height="17" rx="1"></rect>
|
|
1989
|
+
</g>
|
|
1990
|
+
<path d="M28 10 Q38 2 48 10 M31 14 Q38 8 45 14 M36 18 Q38 16 40 18" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"></path>
|
|
1991
|
+
<circle cx="38" cy="21" r="1.7" fill="currentColor"></circle>
|
|
1992
|
+
<text x="54" y="20" font-size="12" font-weight="600" fill="currentColor">${batteryLevel}%</text>
|
|
1993
|
+
<rect x="88" y="6" width="32" height="17" rx="4" fill="none" stroke="currentColor" stroke-width="1.6"></rect>
|
|
1994
|
+
<rect x="91" y="9" width="${batteryFillWidth}" height="11" rx="2" fill="currentColor" opacity="0.72"></rect>
|
|
1995
|
+
<rect x="122" y="11" width="3" height="7" rx="1.5" fill="currentColor"></rect>
|
|
1996
|
+
</svg>
|
|
1997
|
+
</div>`;
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
const batteryFillWidth = Math.max(2, Math.round(30 * batteryLevel / 100));
|
|
2001
|
+
|
|
2002
|
+
return `<div class="phone-statusbar android-statusbar">
|
|
2003
|
+
<span class="status-time">${time}</span>
|
|
2004
|
+
<svg class="android-notification" viewBox="0 0 24 24" aria-hidden="true">
|
|
2005
|
+
<circle cx="12" cy="12" r="9.5" fill="none" stroke="currentColor" stroke-width="1.8"></circle>
|
|
2006
|
+
<circle cx="8" cy="10" r="1.3" fill="currentColor"></circle>
|
|
2007
|
+
<circle cx="12" cy="8" r="1.3" fill="currentColor"></circle>
|
|
2008
|
+
<circle cx="16" cy="10" r="1.3" fill="currentColor"></circle>
|
|
2009
|
+
<path d="M7.5 14 Q12 18 16.5 14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"></path>
|
|
2010
|
+
</svg>
|
|
2011
|
+
<svg class="android-status-icons" viewBox="0 0 200 28" aria-hidden="true">
|
|
2012
|
+
<g fill="currentColor">
|
|
2013
|
+
<rect x="1" y="20" width="5" height="5" rx="1"></rect><rect x="8" y="16" width="5" height="9" rx="1"></rect>
|
|
2014
|
+
<rect x="15" y="12" width="5" height="13" rx="1"></rect><rect x="22" y="8" width="5" height="17" rx="1"></rect><rect x="29" y="4" width="5" height="21" rx="1"></rect>
|
|
2015
|
+
<rect x="43" y="20" width="5" height="5" rx="1"></rect><rect x="50" y="16" width="5" height="9" rx="1"></rect>
|
|
2016
|
+
<rect x="57" y="12" width="5" height="13" rx="1"></rect><rect x="64" y="8" width="5" height="17" rx="1"></rect><rect x="71" y="4" width="5" height="21" rx="1"></rect>
|
|
2017
|
+
</g>
|
|
2018
|
+
<path d="M86 9 Q99 -1 112 9 M90 14 Q99 7 108 14 M95 19 Q99 16 103 19" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round"></path>
|
|
2019
|
+
<circle cx="99" cy="22" r="2" fill="currentColor"></circle>
|
|
2020
|
+
<text x="108" y="24" font-size="7" font-weight="700" fill="currentColor">6</text>
|
|
2021
|
+
<rect x="121" y="4" width="39" height="21" rx="5" fill="none" stroke="currentColor" stroke-width="2"></rect>
|
|
2022
|
+
<rect x="125" y="8" width="${batteryFillWidth}" height="13" rx="2" fill="currentColor" opacity="0.65"></rect>
|
|
2023
|
+
<rect x="162" y="10" width="3" height="9" rx="1.5" fill="currentColor"></rect>
|
|
2024
|
+
<text x="169" y="21" font-size="13" font-weight="500" fill="currentColor">${batteryLevel}%</text>
|
|
2025
|
+
</svg>
|
|
2026
|
+
</div>`;
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
function createDeviceFrameHtml(
|
|
2030
|
+
profile,
|
|
2031
|
+
screenshotBuffer,
|
|
2032
|
+
imageWidth = profile.width,
|
|
2033
|
+
imageHeight = profile.height,
|
|
2034
|
+
imageFormat = 'png',
|
|
2035
|
+
displayTime = '',
|
|
2036
|
+
batteryPercentage = null,
|
|
2037
|
+
statusBarStyle = null,
|
|
2038
|
+
navigationBarStyle = null,
|
|
2039
|
+
screenshotCopy = null,
|
|
2040
|
+
palette,
|
|
2041
|
+
) {
|
|
1299
2042
|
const screenshotDataUrl = `data:image/${imageFormat === 'jpeg' ? 'jpeg' : 'png'};base64,${screenshotBuffer.toString('base64')}`;
|
|
1300
2043
|
const aspectRatio = `${imageWidth} / ${imageHeight}`;
|
|
2044
|
+
const phonePlatform = profile.frameType === 'iphone' ? 'ios' : profile.frameType === 'android-phone' ? 'android' : null;
|
|
2045
|
+
const statusBarPlatform = profile.frameType === 'ipad' ? 'ipad' : phonePlatform;
|
|
2046
|
+
const phoneStyle = phonePlatform && frameStyles[phonePlatform][selectedFrameStyles[phonePlatform]];
|
|
2047
|
+
const phoneStatusBar = statusBarPlatform
|
|
2048
|
+
? getPhoneStatusBar(statusBarPlatform, displayTime, batteryPercentage)
|
|
2049
|
+
: '';
|
|
2050
|
+
const phoneNavigation = phonePlatform === 'android'
|
|
2051
|
+
? '<div class="android-navigation"><div class="gesture-pill"></div></div>'
|
|
2052
|
+
: statusBarPlatform === 'ios' || statusBarPlatform === 'ipad'
|
|
2053
|
+
? `<div class="ios-navigation${statusBarPlatform === 'ipad' ? ' ipad-navigation' : ''}"><div class="ios-gesture-pill"></div></div>`
|
|
2054
|
+
: '';
|
|
1301
2055
|
|
|
1302
2056
|
return `<!doctype html>
|
|
1303
2057
|
<html>
|
|
@@ -1311,22 +2065,33 @@ function createDeviceFrameHtml(profile, screenshotBuffer, imageWidth = profile.w
|
|
|
1311
2065
|
place-items: center;
|
|
1312
2066
|
position: relative;
|
|
1313
2067
|
background:
|
|
1314
|
-
radial-gradient(circle at
|
|
1315
|
-
radial-gradient(circle at
|
|
1316
|
-
linear-gradient(145deg,
|
|
2068
|
+
radial-gradient(circle at 12% 16%, ${palette.accentSoft}, transparent 32%),
|
|
2069
|
+
radial-gradient(circle at 88% 84%, ${palette.secondarySoft}, transparent 34%),
|
|
2070
|
+
linear-gradient(145deg, ${palette.backgroundStart}, ${palette.backgroundMiddle} 52%, ${palette.backgroundEnd});
|
|
1317
2071
|
font-family: Arial, sans-serif;
|
|
1318
2072
|
}
|
|
1319
2073
|
body::before {
|
|
1320
2074
|
content: '';
|
|
1321
2075
|
position: absolute;
|
|
1322
2076
|
inset: 0;
|
|
1323
|
-
opacity: 0.
|
|
1324
|
-
background-image:
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
2077
|
+
opacity: 0.16;
|
|
2078
|
+
background-image: radial-gradient(${palette.accent} 0.8px, transparent 0.8px);
|
|
2079
|
+
background-size: clamp(18px, 2.2vw, 34px) clamp(18px, 2.2vw, 34px);
|
|
2080
|
+
mask-image: linear-gradient(135deg, black, transparent 42%, transparent 64%, black);
|
|
2081
|
+
}
|
|
2082
|
+
${getPromotionalDecorationStyles()}
|
|
2083
|
+
.promo-copy {
|
|
2084
|
+
position: absolute;
|
|
2085
|
+
z-index: 4;
|
|
2086
|
+
display: flex;
|
|
2087
|
+
flex-direction: column;
|
|
2088
|
+
color: ${palette.text};
|
|
2089
|
+
font-family: Arial, sans-serif;
|
|
1329
2090
|
}
|
|
2091
|
+
.promo-copy h1 { margin: 0; font-size: clamp(24px, 5.4vw, 82px); line-height: 1.02; letter-spacing: -0.04em; }
|
|
2092
|
+
.promo-copy p { margin: 1.8vh 0 0; color: ${palette.mutedText}; font-size: clamp(14px, 2.1vw, 34px); line-height: 1.35; }
|
|
2093
|
+
.promo-features { display: flex; flex-wrap: wrap; gap: clamp(6px, 0.9vw, 16px); margin-top: 2vh; }
|
|
2094
|
+
.promo-features span { padding: clamp(6px, 0.75vw, 13px) clamp(9px, 1.2vw, 20px); border: 1px solid ${palette.accentSoft}; border-radius: 999px; background: rgba(255,255,255,0.72); color: ${palette.accent}; box-shadow: 0 0.5vh 1.8vh rgba(15,23,42,0.08); font-size: clamp(11px, 1.45vw, 23px); font-weight: 700; line-height: 1; }
|
|
1330
2095
|
.device {
|
|
1331
2096
|
position: relative;
|
|
1332
2097
|
z-index: 1;
|
|
@@ -1338,10 +2103,13 @@ function createDeviceFrameHtml(profile, screenshotBuffer, imageWidth = profile.w
|
|
|
1338
2103
|
0 0 0 1px rgba(255, 255, 255, 0.18) inset,
|
|
1339
2104
|
0 0 5vh rgba(45, 213, 255, 0.12);
|
|
1340
2105
|
}
|
|
2106
|
+
.device:not(.android-phone):not(.iphone) {
|
|
2107
|
+
box-sizing: content-box;
|
|
2108
|
+
}
|
|
1341
2109
|
.screen {
|
|
1342
2110
|
position: relative;
|
|
1343
2111
|
width: 100%;
|
|
1344
|
-
height:
|
|
2112
|
+
height: 100%;
|
|
1345
2113
|
overflow: hidden;
|
|
1346
2114
|
background: #f5f7fc;
|
|
1347
2115
|
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.95) inset;
|
|
@@ -1355,14 +2123,86 @@ function createDeviceFrameHtml(profile, screenshotBuffer, imageWidth = profile.w
|
|
|
1355
2123
|
}
|
|
1356
2124
|
.device.android-phone,
|
|
1357
2125
|
.device.iphone {
|
|
1358
|
-
height:
|
|
1359
|
-
padding:
|
|
2126
|
+
height: 84%;
|
|
2127
|
+
padding: 2.5%;
|
|
1360
2128
|
border-radius: clamp(24px, 8vw, 70px);
|
|
1361
2129
|
}
|
|
1362
|
-
|
|
1363
|
-
.device.
|
|
1364
|
-
|
|
2130
|
+
${phoneStyle ? `
|
|
2131
|
+
.device.${profile.frameType} {
|
|
2132
|
+
background: ${phoneStyle.outer};
|
|
2133
|
+
border-color: ${phoneStyle.edge};
|
|
2134
|
+
border-radius: calc(84vh * ${imageWidth / imageHeight * phoneStyle.radius / 340});
|
|
2135
|
+
box-shadow: 0 4vh 9vh rgba(0, 0, 0, 0.48), 0 0 0 0.55vw ${phoneStyle.inner} inset;
|
|
2136
|
+
}
|
|
2137
|
+
.device.${profile.frameType} .screen {
|
|
2138
|
+
display: flex;
|
|
2139
|
+
flex-direction: column;
|
|
2140
|
+
height: 100%;
|
|
2141
|
+
border-radius: calc(84vh * ${imageWidth / imageHeight * phoneStyle.radius / 340} - 2.5vw - clamp(2px, 0.45vw, 9px));
|
|
1365
2142
|
}
|
|
2143
|
+
.device.${profile.frameType} .screen img {
|
|
2144
|
+
flex: 1 1 0;
|
|
2145
|
+
min-height: 0;
|
|
2146
|
+
object-fit: fill;
|
|
2147
|
+
}` : ''}
|
|
2148
|
+
.phone-statusbar {
|
|
2149
|
+
position: relative;
|
|
2150
|
+
z-index: 2;
|
|
2151
|
+
flex: none;
|
|
2152
|
+
height: 5%;
|
|
2153
|
+
display: flex;
|
|
2154
|
+
align-items: center;
|
|
2155
|
+
font-family: Arial, sans-serif;
|
|
2156
|
+
}
|
|
2157
|
+
.status-time {
|
|
2158
|
+
margin-left: 5%;
|
|
2159
|
+
font-size: clamp(11px, 3vw, 40px);
|
|
2160
|
+
white-space: nowrap;
|
|
2161
|
+
}
|
|
2162
|
+
.ios-statusbar { background: ${statusBarStyle?.background || 'transparent'}; color: ${statusBarStyle?.foreground || '#050505'}; font-weight: 700; }
|
|
2163
|
+
.ios-statusbar svg { width: 27%; height: 62%; margin-left: auto; margin-right: 3%; }
|
|
2164
|
+
.ios-statusbar .dynamic-island {
|
|
2165
|
+
position: absolute;
|
|
2166
|
+
top: 22%;
|
|
2167
|
+
left: 50%;
|
|
2168
|
+
width: 25%;
|
|
2169
|
+
height: 56%;
|
|
2170
|
+
transform: translateX(-50%);
|
|
2171
|
+
border-radius: 999px;
|
|
2172
|
+
background: #000;
|
|
2173
|
+
}
|
|
2174
|
+
.android-statusbar { height: 4%; background: ${statusBarStyle?.background || 'transparent'}; color: ${statusBarStyle?.foreground || '#050505'}; font-family: Roboto, Arial, sans-serif; }
|
|
2175
|
+
.android-notification { width: 3.4%; height: 46%; margin-left: 2%; }
|
|
2176
|
+
.android-status-icons { width: 32%; height: 58%; margin-left: auto; margin-right: 4%; }
|
|
2177
|
+
.android-navigation {
|
|
2178
|
+
position: absolute;
|
|
2179
|
+
z-index: 2;
|
|
2180
|
+
right: 0;
|
|
2181
|
+
bottom: 0;
|
|
2182
|
+
left: 0;
|
|
2183
|
+
height: 2.2%;
|
|
2184
|
+
display: grid;
|
|
2185
|
+
place-items: center;
|
|
2186
|
+
background: transparent;
|
|
2187
|
+
}
|
|
2188
|
+
.gesture-pill { width: 40%; height: 14%; border-radius: 999px; background: ${navigationBarStyle?.foreground || '#7a7b7c'}; }
|
|
2189
|
+
.ios-navigation {
|
|
2190
|
+
position: relative;
|
|
2191
|
+
z-index: 2;
|
|
2192
|
+
flex: none;
|
|
2193
|
+
height: 2.2%;
|
|
2194
|
+
display: grid;
|
|
2195
|
+
place-items: center;
|
|
2196
|
+
background: ${navigationBarStyle?.background || 'transparent'};
|
|
2197
|
+
}
|
|
2198
|
+
.ios-gesture-pill {
|
|
2199
|
+
width: 36%;
|
|
2200
|
+
height: clamp(3px, 0.42vw, 8px);
|
|
2201
|
+
border-radius: 999px;
|
|
2202
|
+
background: ${navigationBarStyle?.foreground || '#050505'};
|
|
2203
|
+
}
|
|
2204
|
+
.ipad-navigation { height: 1.8%; }
|
|
2205
|
+
.ipad-navigation .ios-gesture-pill { width: 24%; }
|
|
1366
2206
|
.camera-dot {
|
|
1367
2207
|
position: absolute;
|
|
1368
2208
|
z-index: 3;
|
|
@@ -1395,6 +2235,85 @@ function createDeviceFrameHtml(profile, screenshotBuffer, imageWidth = profile.w
|
|
|
1395
2235
|
.device.tablet .screen {
|
|
1396
2236
|
border-radius: clamp(13px, 2.5vw, 38px);
|
|
1397
2237
|
}
|
|
2238
|
+
.device.ipad {
|
|
2239
|
+
height: 86%;
|
|
2240
|
+
padding: 4.1%;
|
|
2241
|
+
border: clamp(2px, 0.34vw, 7px) solid #9da3ad;
|
|
2242
|
+
border-radius: clamp(32px, 5.2vw, 84px);
|
|
2243
|
+
background: linear-gradient(145deg, #24272c 0%, #0b0c0e 22%, #050506 70%, #17191c 100%);
|
|
2244
|
+
box-shadow:
|
|
2245
|
+
0 4vh 9vh rgba(0, 0, 0, 0.62),
|
|
2246
|
+
0 0 0 clamp(1px, 0.16vw, 3px) rgba(230, 233, 238, 0.42) inset,
|
|
2247
|
+
0 0 0 clamp(3px, 0.45vw, 9px) rgba(0, 0, 0, 0.82) inset;
|
|
2248
|
+
}
|
|
2249
|
+
.device.ipad .screen {
|
|
2250
|
+
display: flex;
|
|
2251
|
+
flex-direction: column;
|
|
2252
|
+
border: clamp(2px, 0.25vw, 5px) solid #181a1e;
|
|
2253
|
+
border-radius: clamp(20px, 3.4vw, 55px);
|
|
2254
|
+
background: #000;
|
|
2255
|
+
box-shadow: 0 0 0 1px rgba(255,255,255,0.05), 0 0 1.4vh rgba(0,0,0,0.9) inset;
|
|
2256
|
+
}
|
|
2257
|
+
.device.ipad .screen img {
|
|
2258
|
+
flex: 1 1 0;
|
|
2259
|
+
min-height: 0;
|
|
2260
|
+
height: 0;
|
|
2261
|
+
object-fit: fill;
|
|
2262
|
+
}
|
|
2263
|
+
.ipad-statusbar {
|
|
2264
|
+
height: 3.2%;
|
|
2265
|
+
background: ${statusBarStyle?.background || 'transparent'};
|
|
2266
|
+
color: ${statusBarStyle?.foreground || '#050505'};
|
|
2267
|
+
font-weight: 700;
|
|
2268
|
+
}
|
|
2269
|
+
.ipad-statusbar .status-time {
|
|
2270
|
+
margin-left: 2.2%;
|
|
2271
|
+
font-size: clamp(9px, 1.18vw, 22px);
|
|
2272
|
+
}
|
|
2273
|
+
.ipad-status-icons {
|
|
2274
|
+
width: 17%;
|
|
2275
|
+
height: 58%;
|
|
2276
|
+
margin-left: auto;
|
|
2277
|
+
margin-right: 1.8%;
|
|
2278
|
+
}
|
|
2279
|
+
.ipad-camera-array {
|
|
2280
|
+
position: absolute;
|
|
2281
|
+
z-index: 3;
|
|
2282
|
+
top: 1.05%;
|
|
2283
|
+
left: 50%;
|
|
2284
|
+
display: flex;
|
|
2285
|
+
align-items: center;
|
|
2286
|
+
justify-content: center;
|
|
2287
|
+
gap: clamp(4px, 0.62vw, 12px);
|
|
2288
|
+
width: 18%;
|
|
2289
|
+
height: 1.65%;
|
|
2290
|
+
transform: translateX(-50%);
|
|
2291
|
+
}
|
|
2292
|
+
.ipad-sensor {
|
|
2293
|
+
display: block;
|
|
2294
|
+
flex: none;
|
|
2295
|
+
border-radius: 50%;
|
|
2296
|
+
background: #020304;
|
|
2297
|
+
box-shadow: 0 0 0 1px rgba(29,33,39,0.9), 0 0 0.35vh rgba(0,0,0,0.85) inset;
|
|
2298
|
+
}
|
|
2299
|
+
.ipad-sensor-large { width: clamp(5px, 1vw, 17px); aspect-ratio: 1; }
|
|
2300
|
+
.ipad-sensor-small { width: clamp(3px, 0.38vw, 7px); aspect-ratio: 1; }
|
|
2301
|
+
.ipad-sensor-lens {
|
|
2302
|
+
width: clamp(4px, 0.62vw, 11px);
|
|
2303
|
+
aspect-ratio: 1;
|
|
2304
|
+
background: radial-gradient(circle at 38% 35%, #355e8a, #07101d 42%, #010205 75%);
|
|
2305
|
+
box-shadow: 0 0 0 1px #121a26, 0 0 0.45vh rgba(42,110,202,0.45);
|
|
2306
|
+
}
|
|
2307
|
+
.ipad-side-button {
|
|
2308
|
+
position: absolute;
|
|
2309
|
+
top: 7.6%;
|
|
2310
|
+
right: -0.45%;
|
|
2311
|
+
width: 0.45%;
|
|
2312
|
+
height: 7.8%;
|
|
2313
|
+
border-radius: 0 999px 999px 0;
|
|
2314
|
+
background: linear-gradient(90deg, #4c5159, #a9afb7 58%, #555b64);
|
|
2315
|
+
box-shadow: 0 0.3vh 0.7vh rgba(0,0,0,0.5);
|
|
2316
|
+
}
|
|
1398
2317
|
.device.apple-watch {
|
|
1399
2318
|
height: 82%;
|
|
1400
2319
|
padding: 2.2%;
|
|
@@ -1526,11 +2445,51 @@ function createDeviceFrameHtml(profile, screenshotBuffer, imageWidth = profile.w
|
|
|
1526
2445
|
background: #6fe8ff;
|
|
1527
2446
|
box-shadow: 0 0 1.8vh #2dd5ff;
|
|
1528
2447
|
}
|
|
2448
|
+
@media (orientation: portrait) {
|
|
2449
|
+
body.has-copy .promo-copy { top: 4%; right: 7%; left: 7%; align-items: center; text-align: center; }
|
|
2450
|
+
body.has-copy .promo-copy h1 { max-width: 90%; }
|
|
2451
|
+
body.has-copy .promo-copy p { max-width: 86%; }
|
|
2452
|
+
body.has-copy .promo-features { justify-content: center; }
|
|
2453
|
+
body.has-copy .device,
|
|
2454
|
+
body.has-copy .device.android-phone,
|
|
2455
|
+
body.has-copy .device.iphone,
|
|
2456
|
+
body.has-copy .device.tablet,
|
|
2457
|
+
body.has-copy .device.apple-watch {
|
|
2458
|
+
position: absolute;
|
|
2459
|
+
bottom: 3%;
|
|
2460
|
+
left: 50%;
|
|
2461
|
+
width: auto;
|
|
2462
|
+
height: 68%;
|
|
2463
|
+
transform: translateX(-50%);
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
@media (orientation: landscape) {
|
|
2467
|
+
body.has-copy .promo-copy { top: 50%; left: 5%; width: 31%; transform: translateY(-50%); }
|
|
2468
|
+
body.has-copy .promo-copy h1 { font-size: clamp(28px, 4.4vw, 84px); }
|
|
2469
|
+
body.has-copy .promo-copy p { font-size: clamp(16px, 1.7vw, 32px); }
|
|
2470
|
+
body.has-copy .device,
|
|
2471
|
+
body.has-copy .device.tv,
|
|
2472
|
+
body.has-copy .device.apple-tv,
|
|
2473
|
+
body.has-copy .device.laptop,
|
|
2474
|
+
body.has-copy .device.xr,
|
|
2475
|
+
body.has-copy .device.apple-vision-pro {
|
|
2476
|
+
position: absolute;
|
|
2477
|
+
top: 50%;
|
|
2478
|
+
right: 3%;
|
|
2479
|
+
left: auto;
|
|
2480
|
+
width: 60%;
|
|
2481
|
+
height: auto;
|
|
2482
|
+
max-height: 84%;
|
|
2483
|
+
transform: translateY(-50%);
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
1529
2486
|
</style>
|
|
1530
2487
|
</head>
|
|
1531
|
-
<body>
|
|
2488
|
+
<body class="${phonePlatform ? 'phone-frame' : ''} ${screenshotCopy ? 'has-copy' : ''}">
|
|
2489
|
+
${createPromotionalDecorations(palette)}
|
|
2490
|
+
${createScreenshotCopyMarkup(screenshotCopy)}
|
|
1532
2491
|
<main class="device ${profile.frameType}">
|
|
1533
|
-
<div class="screen"
|
|
2492
|
+
<div class="screen">${phoneStatusBar}<img src="${screenshotDataUrl}" />${phoneNavigation}</div>
|
|
1534
2493
|
${getFrameAdornment(profile.frameType)}
|
|
1535
2494
|
</main>
|
|
1536
2495
|
</body>
|
|
@@ -1570,6 +2529,88 @@ function readJsonAssetSource(filePath) {
|
|
|
1570
2529
|
}
|
|
1571
2530
|
}
|
|
1572
2531
|
|
|
2532
|
+
function getScreenshotSourceDetails(screenInfo) {
|
|
2533
|
+
const pageDirectory = path.join(__dirname, 'src', 'pages');
|
|
2534
|
+
const routesPath = path.join(__dirname, 'src', 'js', 'routes.js');
|
|
2535
|
+
const pageName = String(screenInfo?.page?.dataName || '').trim();
|
|
2536
|
+
const pageId = String(screenInfo?.page?.id || '').trim();
|
|
2537
|
+
const escapePattern = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
2538
|
+
const pageFiles = [];
|
|
2539
|
+
|
|
2540
|
+
if (fs.existsSync(pageDirectory) && (pageName || pageId)) {
|
|
2541
|
+
for (const fileName of fs.readdirSync(pageDirectory).filter((name) => /\.(?:f7|html)$/i.test(name))) {
|
|
2542
|
+
const filePath = path.join(pageDirectory, fileName);
|
|
2543
|
+
const source = fs.readFileSync(filePath, 'utf8');
|
|
2544
|
+
const hasPageName = pageName
|
|
2545
|
+
&& new RegExp(`data-name=["']${escapePattern(pageName)}["']`).test(source);
|
|
2546
|
+
const hasPageId = pageId
|
|
2547
|
+
&& new RegExp(`id=["']${escapePattern(pageId)}["']`).test(source);
|
|
2548
|
+
if (hasPageName || hasPageId) {
|
|
2549
|
+
pageFiles.push(path.relative(__dirname, filePath).replace(/\\/g, '/'));
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
const routeCandidates = [];
|
|
2555
|
+
if (fs.existsSync(routesPath)) {
|
|
2556
|
+
const routesSource = fs.readFileSync(routesPath, 'utf8');
|
|
2557
|
+
const staticImports = new Map();
|
|
2558
|
+
const staticImportPattern = /import\s+([A-Za-z_$][\w$]*)\s+from\s+["']\.\.\/pages\/([^"']+)["']/g;
|
|
2559
|
+
let match;
|
|
2560
|
+
while ((match = staticImportPattern.exec(routesSource))) {
|
|
2561
|
+
staticImports.set(match[1], `src/pages/${match[2]}`);
|
|
2562
|
+
}
|
|
2563
|
+
|
|
2564
|
+
const routeBlockPattern = /\{\s*path:\s*["']([^"']+)["']([\s\S]*?)\n\s*\},/g;
|
|
2565
|
+
while ((match = routeBlockPattern.exec(routesSource))) {
|
|
2566
|
+
const routePath = match[1];
|
|
2567
|
+
const block = match[2];
|
|
2568
|
+
const dynamicPage = block.match(/import\(["']\.\.\/pages\/([^"']+)["']\)/)?.[1];
|
|
2569
|
+
const componentName = block.match(/component:\s*([A-Za-z_$][\w$]*)/)?.[1];
|
|
2570
|
+
const pageFile = dynamicPage
|
|
2571
|
+
? `src/pages/${dynamicPage}`
|
|
2572
|
+
: staticImports.get(componentName) || '';
|
|
2573
|
+
const normalizedPageFile = pageFile.replace(/\\/g, '/');
|
|
2574
|
+
const routeMatchesPage = normalizedPageFile
|
|
2575
|
+
&& pageFiles.some((candidate) => candidate.toLowerCase() === normalizedPageFile.toLowerCase());
|
|
2576
|
+
const framework7Path = screenInfo?.route?.framework7?.path;
|
|
2577
|
+
const framework7Url = screenInfo?.route?.framework7?.url;
|
|
2578
|
+
if (routeMatchesPage || routePath === framework7Path || routePath === framework7Url) {
|
|
2579
|
+
routeCandidates.push({ path: routePath, pageFile: normalizedPageFile });
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2584
|
+
return {
|
|
2585
|
+
pageFiles,
|
|
2586
|
+
routesFile: fs.existsSync(routesPath) ? 'src/js/routes.js' : '',
|
|
2587
|
+
routeCandidates,
|
|
2588
|
+
};
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2591
|
+
function saveScreenshotInfo(screenshotFileName, screenInfo) {
|
|
2592
|
+
const screenshotKey = path.parse(screenshotFileName).name;
|
|
2593
|
+
const source = readJsonAssetSource(screenshotInfoPath);
|
|
2594
|
+
const screenshots = source.screenshots && typeof source.screenshots === 'object'
|
|
2595
|
+
? source.screenshots
|
|
2596
|
+
: {};
|
|
2597
|
+
|
|
2598
|
+
screenshots[screenshotKey] = {
|
|
2599
|
+
schemaVersion: 2,
|
|
2600
|
+
fileName: screenshotFileName,
|
|
2601
|
+
capturedAt: new Date().toISOString(),
|
|
2602
|
+
...screenInfo,
|
|
2603
|
+
source: getScreenshotSourceDetails(screenInfo),
|
|
2604
|
+
};
|
|
2605
|
+
|
|
2606
|
+
fs.mkdirSync(path.dirname(screenshotInfoPath), { recursive: true });
|
|
2607
|
+
fs.writeFileSync(
|
|
2608
|
+
screenshotInfoPath,
|
|
2609
|
+
`${JSON.stringify({ version: 2, screenshots }, null, 2)}\n`,
|
|
2610
|
+
);
|
|
2611
|
+
console.log(`Saved screenshot information: ${screenshotInfoPath} (${screenshotKey})`);
|
|
2612
|
+
}
|
|
2613
|
+
|
|
1573
2614
|
function collectProjectUiPhrases() {
|
|
1574
2615
|
const phrases = [];
|
|
1575
2616
|
const pageDirectory = path.join(__dirname, 'src', 'pages');
|
|
@@ -2171,24 +3212,48 @@ async function captureFramedScreenshot(
|
|
|
2171
3212
|
profile,
|
|
2172
3213
|
screenshotFileName,
|
|
2173
3214
|
skipExisting = false,
|
|
3215
|
+
outputRootDirectory = inFrameDirectory,
|
|
2174
3216
|
) {
|
|
2175
3217
|
if (profile.screenshotOnly) return false;
|
|
2176
3218
|
|
|
2177
|
-
const
|
|
2178
|
-
const
|
|
3219
|
+
const outputExtension = profile.outputExtension || path.extname(screenshotFileName).slice(1) || 'png';
|
|
3220
|
+
const outputFileName = `${path.parse(screenshotFileName).name}.${outputExtension}`;
|
|
3221
|
+
const framedProfileDirectory = path.join(outputRootDirectory, profile.outputDirectory);
|
|
3222
|
+
const framedScreenshotPath = path.join(framedProfileDirectory, outputFileName);
|
|
2179
3223
|
|
|
2180
3224
|
if (skipExisting && fs.existsSync(framedScreenshotPath)) {
|
|
2181
3225
|
console.log(`Skipped existing framed screenshot: ${framedScreenshotPath}`);
|
|
2182
3226
|
return false;
|
|
2183
3227
|
}
|
|
2184
3228
|
|
|
2185
|
-
const originalImageMetadata =
|
|
2186
|
-
? await sharp(screenshotBuffer).metadata()
|
|
2187
|
-
: null;
|
|
3229
|
+
const originalImageMetadata = await sharp(screenshotBuffer).metadata();
|
|
2188
3230
|
const hasCustomFrameSize = Boolean(profile.framedWidth && profile.framedHeight);
|
|
2189
3231
|
const frameWidth = profile.framedWidth || profile.width;
|
|
2190
3232
|
const frameHeight = profile.framedHeight || profile.height;
|
|
2191
3233
|
const frameScaleFactor = hasCustomFrameSize ? 1 : profile.deviceScaleFactor;
|
|
3234
|
+
const phonePlatform = profile.frameType === 'iphone' ? 'ios' : profile.frameType === 'android-phone' ? 'android' : null;
|
|
3235
|
+
const statusBarPlatform = profile.frameType === 'ipad' ? 'ipad' : phonePlatform;
|
|
3236
|
+
const displayTime = statusBarPlatform ? getFrameDisplayTime(statusBarPlatform) : '';
|
|
3237
|
+
const batteryPercentage = statusBarPlatform === 'android' || statusBarPlatform === 'ipad'
|
|
3238
|
+
? getFrameBatteryPercentage()
|
|
3239
|
+
: null;
|
|
3240
|
+
const statusBarStyle = statusBarPlatform
|
|
3241
|
+
? await getScreenshotBarStyle(screenshotBuffer)
|
|
3242
|
+
: null;
|
|
3243
|
+
const navigationBarStyle = statusBarPlatform
|
|
3244
|
+
? await getScreenshotBarStyle(screenshotBuffer, 'bottom')
|
|
3245
|
+
: null;
|
|
3246
|
+
const screenshotCopy = getScreenshotCopy(screenshotFileName);
|
|
3247
|
+
const promotionalPalette = await getPromotionalPalette(screenshotBuffer, screenshotFileName);
|
|
3248
|
+
const screenshotKey = path.parse(screenshotFileName).name;
|
|
3249
|
+
|
|
3250
|
+
if (!screenshotCopy && !missingScreenshotCopyWarnings.has(screenshotKey)) {
|
|
3251
|
+
missingScreenshotCopyWarnings.add(screenshotKey);
|
|
3252
|
+
console.warn(
|
|
3253
|
+
`No promotional copy configured for screenshot ${screenshotKey}; `
|
|
3254
|
+
+ `add it to ${screenshotCopyPath}.`,
|
|
3255
|
+
);
|
|
3256
|
+
}
|
|
2192
3257
|
|
|
2193
3258
|
await client.send('Emulation.setDeviceMetricsOverride', {
|
|
2194
3259
|
width: frameWidth,
|
|
@@ -2207,9 +3272,16 @@ async function captureFramedScreenshot(
|
|
|
2207
3272
|
html: profile.name === 'vivo-store' || profile.name === 'oppo-store'
|
|
2208
3273
|
? createStoreDeviceFrameHtml(
|
|
2209
3274
|
profile, screenshotBuffer, originalImageMetadata.width, originalImageMetadata.height,
|
|
2210
|
-
frameWidth, frameHeight, originalImageMetadata.format,
|
|
3275
|
+
frameWidth, frameHeight, originalImageMetadata.format, displayTime,
|
|
3276
|
+
batteryPercentage, statusBarStyle, navigationBarStyle,
|
|
3277
|
+
screenshotCopy, promotionalPalette,
|
|
2211
3278
|
)
|
|
2212
|
-
: createDeviceFrameHtml(
|
|
3279
|
+
: createDeviceFrameHtml(
|
|
3280
|
+
profile, screenshotBuffer, originalImageMetadata.width, originalImageMetadata.height,
|
|
3281
|
+
originalImageMetadata.format,
|
|
3282
|
+
displayTime, batteryPercentage, statusBarStyle, navigationBarStyle,
|
|
3283
|
+
screenshotCopy, promotionalPalette,
|
|
3284
|
+
),
|
|
2213
3285
|
});
|
|
2214
3286
|
await client.send('Runtime.evaluate', {
|
|
2215
3287
|
expression: `new Promise((resolve) => {
|
|
@@ -2331,25 +3403,10 @@ async function captureScreenshotSet(
|
|
|
2331
3403
|
selectedProfiles,
|
|
2332
3404
|
fileNameInput,
|
|
2333
3405
|
) {
|
|
2334
|
-
const storeProfile = selectedProfiles.find((profile) => profile.storeRequirements);
|
|
2335
|
-
if (storeProfile) {
|
|
2336
|
-
const profileDirectory = path.join(screenshotDirectory, storeProfile.outputDirectory);
|
|
2337
|
-
const existingImageCount = fs.existsSync(profileDirectory)
|
|
2338
|
-
? fs.readdirSync(profileDirectory, { withFileTypes: true })
|
|
2339
|
-
.filter((entry) => entry.isFile() && /\.(png|jpe?g|webp)$/i.test(entry.name))
|
|
2340
|
-
.length
|
|
2341
|
-
: 0;
|
|
2342
|
-
|
|
2343
|
-
if (existingImageCount >= storeProfile.storeRequirements.maxImages) {
|
|
2344
|
-
throw new Error(
|
|
2345
|
-
`${storeProfile.label} already contains ${existingImageCount} images; the store allows a maximum of ${storeProfile.storeRequirements.maxImages}.`,
|
|
2346
|
-
);
|
|
2347
|
-
}
|
|
2348
|
-
}
|
|
2349
|
-
|
|
2350
3406
|
console.log('Capturing the current rendered page and resolved native data...');
|
|
2351
3407
|
const snapshot = await capturePageSnapshot(deviceClient);
|
|
2352
3408
|
const screenshotFileName = getScreenshotFileName(fileNameInput, selectedProfiles);
|
|
3409
|
+
saveScreenshotInfo(screenshotFileName, snapshot.screenInfo);
|
|
2353
3410
|
|
|
2354
3411
|
for (const profile of selectedProfiles) {
|
|
2355
3412
|
await captureProfile(replayClient, snapshot, profile, screenshotFileName);
|
|
@@ -2358,11 +3415,23 @@ async function captureScreenshotSet(
|
|
|
2358
3415
|
console.log(`Responsive screenshot set completed: ${screenshotFileName}`);
|
|
2359
3416
|
}
|
|
2360
3417
|
|
|
2361
|
-
async function frameExistingScreenshots(
|
|
3418
|
+
async function frameExistingScreenshots(
|
|
3419
|
+
selectedProfiles,
|
|
3420
|
+
overwriteExisting = false,
|
|
3421
|
+
screenshotNames = '',
|
|
3422
|
+
outputRootDirectory = inFrameDirectory,
|
|
3423
|
+
) {
|
|
2362
3424
|
let replayBrowser;
|
|
2363
3425
|
let replayClient;
|
|
2364
3426
|
let framedCount = 0;
|
|
2365
3427
|
let failedCount = 0;
|
|
3428
|
+
const selectedScreenshotNames = new Set(
|
|
3429
|
+
screenshotNames
|
|
3430
|
+
.split(',')
|
|
3431
|
+
.map((name) => path.parse(name.trim()).name)
|
|
3432
|
+
.filter(Boolean),
|
|
3433
|
+
);
|
|
3434
|
+
const foundScreenshotNames = new Set();
|
|
2366
3435
|
|
|
2367
3436
|
try {
|
|
2368
3437
|
replayBrowser = await launchReplayBrowser();
|
|
@@ -2376,12 +3445,17 @@ async function frameExistingScreenshots(selectedProfiles) {
|
|
|
2376
3445
|
}
|
|
2377
3446
|
|
|
2378
3447
|
const screenshotFiles = fs.readdirSync(profileDirectory, { withFileTypes: true })
|
|
2379
|
-
.filter((entry) => entry.isFile() && /\.png$/i.test(entry.name))
|
|
3448
|
+
.filter((entry) => entry.isFile() && /\.(?:png|jpe?g|webp)$/i.test(entry.name))
|
|
2380
3449
|
.map((entry) => entry.name)
|
|
3450
|
+
.filter((fileName) => (
|
|
3451
|
+
!selectedScreenshotNames.size
|
|
3452
|
+
|| selectedScreenshotNames.has(path.parse(fileName).name)
|
|
3453
|
+
))
|
|
2381
3454
|
.sort((first, second) => first.localeCompare(second, undefined, { numeric: true }));
|
|
2382
3455
|
|
|
2383
3456
|
for (const screenshotFileName of screenshotFiles) {
|
|
2384
3457
|
const screenshotPath = path.join(profileDirectory, screenshotFileName);
|
|
3458
|
+
foundScreenshotNames.add(path.parse(screenshotFileName).name);
|
|
2385
3459
|
|
|
2386
3460
|
try {
|
|
2387
3461
|
const wasCreated = await captureFramedScreenshot(
|
|
@@ -2389,7 +3463,8 @@ async function frameExistingScreenshots(selectedProfiles) {
|
|
|
2389
3463
|
fs.readFileSync(screenshotPath),
|
|
2390
3464
|
profile,
|
|
2391
3465
|
screenshotFileName,
|
|
2392
|
-
|
|
3466
|
+
!overwriteExisting,
|
|
3467
|
+
outputRootDirectory,
|
|
2393
3468
|
);
|
|
2394
3469
|
if (wasCreated) framedCount += 1;
|
|
2395
3470
|
} catch (error) {
|
|
@@ -2410,6 +3485,14 @@ async function frameExistingScreenshots(selectedProfiles) {
|
|
|
2410
3485
|
|
|
2411
3486
|
console.log(`Existing screenshot framing completed: ${framedCount} created.`);
|
|
2412
3487
|
|
|
3488
|
+
const missingScreenshotNames = [...selectedScreenshotNames]
|
|
3489
|
+
.filter((name) => !foundScreenshotNames.has(name));
|
|
3490
|
+
if (missingScreenshotNames.length) {
|
|
3491
|
+
throw new Error(
|
|
3492
|
+
`Requested screenshot(s) were not found for the selected profiles: ${missingScreenshotNames.join(', ')}.`,
|
|
3493
|
+
);
|
|
3494
|
+
}
|
|
3495
|
+
|
|
2413
3496
|
if (failedCount) {
|
|
2414
3497
|
throw new Error(`${failedCount} existing screenshot(s) could not be framed.`);
|
|
2415
3498
|
}
|
|
@@ -2598,6 +3681,7 @@ async function getInteractiveOptions(options) {
|
|
|
2598
3681
|
|| options.dryRun
|
|
2599
3682
|
|| options.amazonAssets
|
|
2600
3683
|
|| options.samsungAssets
|
|
3684
|
+
|| options.refreshInfo
|
|
2601
3685
|
|| options.vivoExisting
|
|
2602
3686
|
|| options.oppoExisting
|
|
2603
3687
|
) {
|
|
@@ -2644,6 +3728,10 @@ async function main() {
|
|
|
2644
3728
|
|
|
2645
3729
|
options = await getInteractiveOptions(options);
|
|
2646
3730
|
|
|
3731
|
+
if (!options.dryRun && !options.refreshInfo) {
|
|
3732
|
+
reconcileScreenshotRecords();
|
|
3733
|
+
}
|
|
3734
|
+
|
|
2647
3735
|
if (options.amazonAssets) {
|
|
2648
3736
|
let amazonBrowser;
|
|
2649
3737
|
let amazonClient;
|
|
@@ -2678,6 +3766,10 @@ async function main() {
|
|
|
2678
3766
|
return;
|
|
2679
3767
|
}
|
|
2680
3768
|
|
|
3769
|
+
if (options.vivoExisting || options.oppoExisting) {
|
|
3770
|
+
await prepareFrameSelection(options);
|
|
3771
|
+
}
|
|
3772
|
+
|
|
2681
3773
|
if (options.vivoExisting) {
|
|
2682
3774
|
await createVivoAssetsFromExistingScreenshots();
|
|
2683
3775
|
return;
|
|
@@ -2688,17 +3780,26 @@ async function main() {
|
|
|
2688
3780
|
return;
|
|
2689
3781
|
}
|
|
2690
3782
|
|
|
2691
|
-
if (!options.dryRun) {
|
|
2692
|
-
await createMissingStoreAssets();
|
|
2693
|
-
}
|
|
2694
|
-
|
|
2695
3783
|
const selectedProfiles = selectProfiles(options.profiles);
|
|
2696
3784
|
|
|
2697
|
-
if (options.
|
|
2698
|
-
await
|
|
3785
|
+
if (!options.dryRun && !options.refreshInfo) {
|
|
3786
|
+
await prepareFrameSelection(options);
|
|
3787
|
+
}
|
|
3788
|
+
|
|
3789
|
+
if (options.frameExisting || options.reframeExisting) {
|
|
3790
|
+
await frameExistingScreenshots(
|
|
3791
|
+
selectedProfiles,
|
|
3792
|
+
options.reframeExisting,
|
|
3793
|
+
options.screenshots,
|
|
3794
|
+
options.reframeExisting ? uploadDirectory : inFrameDirectory,
|
|
3795
|
+
);
|
|
2699
3796
|
return;
|
|
2700
3797
|
}
|
|
2701
3798
|
|
|
3799
|
+
if (!options.dryRun && !options.refreshInfo) {
|
|
3800
|
+
await createMissingStoreAssets();
|
|
3801
|
+
}
|
|
3802
|
+
|
|
2702
3803
|
const interactiveMode = process.stdin.isTTY && !options.prefix && !options.dryRun;
|
|
2703
3804
|
const deviceId = getConnectedDevice();
|
|
2704
3805
|
const appId = getAppId();
|
|
@@ -2737,6 +3838,20 @@ async function main() {
|
|
|
2737
3838
|
return;
|
|
2738
3839
|
}
|
|
2739
3840
|
|
|
3841
|
+
if (options.refreshInfo) {
|
|
3842
|
+
const screenshotKey = sanitizeName(
|
|
3843
|
+
options.refreshInfo.trim().replace(/\.(png|jpe?g)$/i, ''),
|
|
3844
|
+
'',
|
|
3845
|
+
);
|
|
3846
|
+
if (!screenshotKey) {
|
|
3847
|
+
throw new Error('Provide a screenshot name after --refresh-info, for example: --refresh-info 1');
|
|
3848
|
+
}
|
|
3849
|
+
const snapshot = await capturePageSnapshot(deviceClient);
|
|
3850
|
+
saveScreenshotInfo(`${screenshotKey}.png`, snapshot.screenInfo);
|
|
3851
|
+
console.log(`Screenshot information refreshed without creating images: ${screenshotKey}`);
|
|
3852
|
+
return;
|
|
3853
|
+
}
|
|
3854
|
+
|
|
2740
3855
|
fs.mkdirSync(screenshotDirectory, { recursive: true });
|
|
2741
3856
|
replayBrowser = await launchReplayBrowser();
|
|
2742
3857
|
replayClient = await createReplayPage(replayBrowser);
|