codeplay-common 4.7.3 → 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 → scripts-screenshot/screenshot}/frame-samples.js +117 -117
- 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 +1029 -93
- package/package.json +1 -1
|
@@ -5,15 +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');
|
|
13
|
-
const { frameStyles, createFrameSamples } = require('./scripts/frame-samples.js');
|
|
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');
|
|
14
32
|
|
|
15
33
|
const screenshotDirectory = path.join(__dirname, 'Auto-Screenshot', 'Store-image-Screenshot');
|
|
16
34
|
const inFrameDirectory = path.join(__dirname, 'Auto-Screenshot', 'Store-image-Frame');
|
|
35
|
+
const uploadDirectory = path.join(__dirname, 'Auto-Screenshot', 'Store-upload');
|
|
17
36
|
const autoScreenshotDirectory = path.join(__dirname, 'Auto-Screenshot');
|
|
18
37
|
const aiAssetsDirectory = path.join(__dirname, 'AI-Assets');
|
|
19
38
|
const amazonAssetsDirectory = path.join(aiAssetsDirectory, 'Amazon-Assets');
|
|
@@ -22,8 +41,12 @@ const playstoreAssetsDirectory = path.join(aiAssetsDirectory, 'Playstore-Assets'
|
|
|
22
41
|
const capacitorConfigPath = path.join(__dirname, 'capacitor.config.json');
|
|
23
42
|
const amazonIconPath = path.join(__dirname, 'resources', 'icon-only.png');
|
|
24
43
|
const temporaryDirectory = path.join(__dirname, 'agent-temp');
|
|
25
|
-
|
|
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' };
|
|
26
47
|
let frameCaptureSequence = 0;
|
|
48
|
+
let screenshotCopyConfig;
|
|
49
|
+
const missingScreenshotCopyWarnings = new Set();
|
|
27
50
|
const frameTimeBase = Date.now();
|
|
28
51
|
const amazonAssetDefinitions = [
|
|
29
52
|
{ fileName: 'icon-1280x720.png', type: 'icon', width: 1280, height: 720 },
|
|
@@ -150,7 +173,7 @@ const deviceProfiles = [
|
|
|
150
173
|
name: 'ipad',
|
|
151
174
|
label: 'iPad 13-inch App Store viewport',
|
|
152
175
|
outputDirectory: 'iPad',
|
|
153
|
-
frameType: '
|
|
176
|
+
frameType: 'ipad',
|
|
154
177
|
width: 1032,
|
|
155
178
|
height: 1376,
|
|
156
179
|
deviceScaleFactor: 2,
|
|
@@ -467,6 +490,7 @@ function parseArguments() {
|
|
|
467
490
|
dryRun: false,
|
|
468
491
|
frameExisting: false,
|
|
469
492
|
reframeExisting: false,
|
|
493
|
+
refreshInfo: '',
|
|
470
494
|
vivoExisting: false,
|
|
471
495
|
oppoExisting: false,
|
|
472
496
|
help: false,
|
|
@@ -475,6 +499,7 @@ function parseArguments() {
|
|
|
475
499
|
deleteNames: '',
|
|
476
500
|
prefix: '',
|
|
477
501
|
profiles: '',
|
|
502
|
+
screenshots: '',
|
|
478
503
|
androidFrame: '',
|
|
479
504
|
iosFrame: '',
|
|
480
505
|
};
|
|
@@ -492,6 +517,9 @@ function parseArguments() {
|
|
|
492
517
|
options.frameExisting = true;
|
|
493
518
|
} else if (argument === '--reframe-existing') {
|
|
494
519
|
options.reframeExisting = true;
|
|
520
|
+
} else if (argument === '--refresh-info') {
|
|
521
|
+
options.refreshInfo = args[index + 1] || '';
|
|
522
|
+
index += 1;
|
|
495
523
|
} else if (argument === '--vivo-existing') {
|
|
496
524
|
options.vivoExisting = true;
|
|
497
525
|
} else if (argument === '--oppo-existing') {
|
|
@@ -510,6 +538,9 @@ function parseArguments() {
|
|
|
510
538
|
} else if (argument === '--profiles') {
|
|
511
539
|
options.profiles = args[index + 1] || '';
|
|
512
540
|
index += 1;
|
|
541
|
+
} else if (argument === '--screenshots') {
|
|
542
|
+
options.screenshots = args[index + 1] || '';
|
|
543
|
+
index += 1;
|
|
513
544
|
} else if (argument === '--android-frame') {
|
|
514
545
|
options.androidFrame = args[index + 1] || '';
|
|
515
546
|
index += 1;
|
|
@@ -559,6 +590,8 @@ function printHelp() {
|
|
|
559
590
|
console.log(' node take-screen-image.js --prefix home --profiles android-phone,ipad');
|
|
560
591
|
console.log(' node take-screen-image.js --frame-existing');
|
|
561
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');
|
|
562
595
|
console.log(' node take-screen-image.js --vivo-existing');
|
|
563
596
|
console.log(' node take-screen-image.js --oppo-existing');
|
|
564
597
|
console.log(' node take-screen-image.js --amazon-assets');
|
|
@@ -568,12 +601,12 @@ function printHelp() {
|
|
|
568
601
|
console.log(' node take-screen-image.js --delete 3,4,5');
|
|
569
602
|
console.log('');
|
|
570
603
|
console.log('A normal capture automatically creates any missing Amazon, Samsung, and Play Store assets.');
|
|
571
|
-
console.log('Frame defaults: Android
|
|
604
|
+
console.log('Frame defaults: Android graphite (A), iPhone midnight (D).');
|
|
572
605
|
}
|
|
573
606
|
|
|
574
607
|
async function chooseFrameStyles(options) {
|
|
575
608
|
const choices = { android: options.androidFrame, ios: options.iosFrame };
|
|
576
|
-
const defaults = { android: '
|
|
609
|
+
const defaults = { android: 'graphite', ios: 'midnight' };
|
|
577
610
|
const aliases = {
|
|
578
611
|
android: { a: 'graphite', b: 'cobalt', 1: 'graphite', 2: 'cobalt' },
|
|
579
612
|
ios: { c: 'titanium', d: 'midnight', 1: 'titanium', 2: 'midnight' },
|
|
@@ -604,18 +637,40 @@ async function chooseFrameStyles(options) {
|
|
|
604
637
|
}
|
|
605
638
|
|
|
606
639
|
async function prepareFrameSelection(options) {
|
|
607
|
-
|
|
608
|
-
|
|
640
|
+
if (process.stdin.isTTY) {
|
|
641
|
+
const samplePaths = await createFrameSamples(autoScreenshotDirectory);
|
|
642
|
+
for (const samplePath of samplePaths) console.log(`Frame choices: ${samplePath}`);
|
|
643
|
+
}
|
|
609
644
|
selectedFrameStyles = await chooseFrameStyles(options);
|
|
610
645
|
}
|
|
611
646
|
|
|
612
647
|
function getFrameDisplayTime(platform) {
|
|
613
648
|
const time = new Date(frameTimeBase + frameCaptureSequence * 60000);
|
|
614
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
|
+
}
|
|
615
656
|
const hour = platform === 'ios' ? (time.getHours() % 12 || 12) : time.getHours();
|
|
616
657
|
return `${String(hour).padStart(2, '0')}:${String(time.getMinutes()).padStart(2, '0')}`;
|
|
617
658
|
}
|
|
618
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;
|
|
672
|
+
}
|
|
673
|
+
|
|
619
674
|
function deleteMatchingImages(input) {
|
|
620
675
|
const names = [...new Set(input.split(',').map((name) => name.trim()).filter(Boolean))];
|
|
621
676
|
|
|
@@ -648,10 +703,92 @@ function deleteMatchingImages(input) {
|
|
|
648
703
|
}
|
|
649
704
|
|
|
650
705
|
scanDirectory(autoScreenshotDirectory);
|
|
706
|
+
removeScreenshotRecords(nameSet);
|
|
651
707
|
console.log(`Deleted ${deletedFiles.length} matching image(s) from ${autoScreenshotDirectory}.`);
|
|
652
708
|
for (const filePath of deletedFiles) console.log(`Deleted: ${filePath}`);
|
|
653
709
|
}
|
|
654
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
|
+
|
|
655
792
|
function selectProfiles(input) {
|
|
656
793
|
const orientation = getConfiguredOrientation();
|
|
657
794
|
const names = input
|
|
@@ -813,6 +950,266 @@ async function capturePageSnapshot(client) {
|
|
|
813
950
|
return canvas.toDataURL('image/png');
|
|
814
951
|
};
|
|
815
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
|
+
|
|
816
1213
|
const serializeDocument = async (sourceDocument) => {
|
|
817
1214
|
const clonedRoot = sourceDocument.documentElement.cloneNode(true);
|
|
818
1215
|
const originalElements = [
|
|
@@ -955,6 +1352,7 @@ async function capturePageSnapshot(client) {
|
|
|
955
1352
|
return {
|
|
956
1353
|
html: '<!doctype html>' + html,
|
|
957
1354
|
scrollY,
|
|
1355
|
+
screenInfo,
|
|
958
1356
|
};
|
|
959
1357
|
})()`,
|
|
960
1358
|
awaitPromise: true,
|
|
@@ -1264,6 +1662,16 @@ function getFrameAdornment(frameType) {
|
|
|
1264
1662
|
return '<div class="tablet-camera"></div>';
|
|
1265
1663
|
}
|
|
1266
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
|
+
|
|
1267
1675
|
if (frameType === 'tv' || frameType === 'apple-tv') {
|
|
1268
1676
|
return '<div class="tv-stand"></div>';
|
|
1269
1677
|
}
|
|
@@ -1283,10 +1691,166 @@ function getFrameAdornment(frameType) {
|
|
|
1283
1691
|
return '<div class="xr-status"></div>';
|
|
1284
1692
|
}
|
|
1285
1693
|
|
|
1286
|
-
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
|
+
) {
|
|
1287
1851
|
const screenshotDataUrl = `data:image/${imageFormat === 'jpeg' ? 'jpeg' : 'png'};base64,${screenshotBuffer.toString('base64')}`;
|
|
1288
|
-
const
|
|
1289
|
-
const
|
|
1852
|
+
const availableDeviceHeight = screenshotCopy ? canvasHeight * 0.64 : canvasHeight - 180;
|
|
1853
|
+
const screenWidth = Math.floor(Math.min(canvasWidth - 160, availableDeviceHeight * imageWidth / imageHeight));
|
|
1290
1854
|
const frameStyle = frameStyles.android[selectedFrameStyles.android];
|
|
1291
1855
|
|
|
1292
1856
|
return `<!doctype html>
|
|
@@ -1299,18 +1863,40 @@ function createStoreDeviceFrameHtml(profile, screenshotBuffer, imageWidth, image
|
|
|
1299
1863
|
body {
|
|
1300
1864
|
display: grid;
|
|
1301
1865
|
place-items: center;
|
|
1302
|
-
|
|
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});
|
|
1303
1871
|
}
|
|
1304
1872
|
body::before {
|
|
1305
1873
|
content: '';
|
|
1306
1874
|
position: absolute;
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
background:
|
|
1311
|
-
|
|
1312
|
-
|
|
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);
|
|
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;
|
|
1313
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%); }
|
|
1314
1900
|
.device {
|
|
1315
1901
|
position: relative;
|
|
1316
1902
|
z-index: 1;
|
|
@@ -1331,12 +1917,13 @@ function createStoreDeviceFrameHtml(profile, screenshotBuffer, imageWidth, image
|
|
|
1331
1917
|
box-shadow: 0 0 0 2px #03060c;
|
|
1332
1918
|
}
|
|
1333
1919
|
.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:
|
|
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; }
|
|
1335
1921
|
.status-time { margin-left: 5%; font-size: ${Math.max(11, Math.round(screenWidth * 0.03))}px; white-space: nowrap; }
|
|
1336
|
-
.android-
|
|
1337
|
-
.android-
|
|
1338
|
-
.android-
|
|
1339
|
-
.
|
|
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}; }
|
|
1340
1927
|
.camera {
|
|
1341
1928
|
position: absolute;
|
|
1342
1929
|
top: 11px;
|
|
@@ -1359,61 +1946,112 @@ function createStoreDeviceFrameHtml(profile, screenshotBuffer, imageWidth, image
|
|
|
1359
1946
|
}
|
|
1360
1947
|
</style>
|
|
1361
1948
|
</head>
|
|
1362
|
-
<body>
|
|
1949
|
+
<body class="${screenshotCopy ? 'has-copy' : ''}">
|
|
1950
|
+
${createPromotionalDecorations(palette)}
|
|
1951
|
+
${createScreenshotCopyMarkup(screenshotCopy)}
|
|
1363
1952
|
<main class="device">
|
|
1364
1953
|
<div class="camera"></div>
|
|
1365
|
-
<div class="screen">${getPhoneStatusBar('android', displayTime)}<img src="${screenshotDataUrl}" /><div class="android-navigation"><div class="gesture-pill"></div></div></div>
|
|
1954
|
+
<div class="screen">${getPhoneStatusBar('android', displayTime, batteryPercentage)}<img src="${screenshotDataUrl}" /><div class="android-navigation"><div class="gesture-pill"></div></div></div>
|
|
1366
1955
|
<div class="side-button"></div>
|
|
1367
1956
|
</main>
|
|
1368
1957
|
</body>
|
|
1369
1958
|
</html>`;
|
|
1370
1959
|
}
|
|
1371
1960
|
|
|
1372
|
-
function getPhoneStatusBar(platform, time) {
|
|
1961
|
+
function getPhoneStatusBar(platform, time, batteryPercentage = null) {
|
|
1962
|
+
const batteryLevel = Number.isInteger(batteryPercentage) ? batteryPercentage : 75;
|
|
1963
|
+
|
|
1373
1964
|
if (platform === 'ios') {
|
|
1374
1965
|
return `<div class="phone-statusbar ios-statusbar">
|
|
1375
1966
|
<span class="status-time">${time}</span>
|
|
1376
1967
|
<div class="dynamic-island"></div>
|
|
1377
1968
|
<svg viewBox="0 0 90 28" aria-hidden="true">
|
|
1378
|
-
<circle cx="5" cy="14" r="2" fill="
|
|
1379
|
-
<circle cx="21" cy="14" r="2" fill="
|
|
1380
|
-
<path d="M36 10 Q46 2 56 10 M39 14 Q46 8 53 14 M44 18 Q46 16 48 18" fill="none" stroke="
|
|
1381
|
-
<rect x="61" y="7" width="22" height="15" rx="4" fill="none" stroke="
|
|
1382
|
-
<rect x="64" y="10" width="16" height="9" rx="2" fill="
|
|
1383
|
-
<rect x="85" y="12" width="2" height="5" rx="1" fill="
|
|
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>
|
|
1384
1975
|
</svg>
|
|
1385
1976
|
</div>`;
|
|
1386
1977
|
}
|
|
1387
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
|
+
|
|
1388
2002
|
return `<div class="phone-statusbar android-statusbar">
|
|
1389
2003
|
<span class="status-time">${time}</span>
|
|
1390
|
-
<svg class="android-notification" viewBox="0 0
|
|
1391
|
-
<circle cx="
|
|
1392
|
-
<circle cx="
|
|
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>
|
|
1393
2010
|
</svg>
|
|
1394
|
-
<svg class="android-status-icons" viewBox="0 0
|
|
1395
|
-
<
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
<
|
|
1402
|
-
<
|
|
1403
|
-
<text x="
|
|
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>
|
|
1404
2025
|
</svg>
|
|
1405
2026
|
</div>`;
|
|
1406
2027
|
}
|
|
1407
2028
|
|
|
1408
|
-
function createDeviceFrameHtml(
|
|
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
|
+
) {
|
|
1409
2042
|
const screenshotDataUrl = `data:image/${imageFormat === 'jpeg' ? 'jpeg' : 'png'};base64,${screenshotBuffer.toString('base64')}`;
|
|
1410
2043
|
const aspectRatio = `${imageWidth} / ${imageHeight}`;
|
|
1411
2044
|
const phonePlatform = profile.frameType === 'iphone' ? 'ios' : profile.frameType === 'android-phone' ? 'android' : null;
|
|
2045
|
+
const statusBarPlatform = profile.frameType === 'ipad' ? 'ipad' : phonePlatform;
|
|
1412
2046
|
const phoneStyle = phonePlatform && frameStyles[phonePlatform][selectedFrameStyles[phonePlatform]];
|
|
1413
|
-
const phoneStatusBar =
|
|
2047
|
+
const phoneStatusBar = statusBarPlatform
|
|
2048
|
+
? getPhoneStatusBar(statusBarPlatform, displayTime, batteryPercentage)
|
|
2049
|
+
: '';
|
|
1414
2050
|
const phoneNavigation = phonePlatform === 'android'
|
|
1415
2051
|
? '<div class="android-navigation"><div class="gesture-pill"></div></div>'
|
|
1416
|
-
: ''
|
|
2052
|
+
: statusBarPlatform === 'ios' || statusBarPlatform === 'ipad'
|
|
2053
|
+
? `<div class="ios-navigation${statusBarPlatform === 'ipad' ? ' ipad-navigation' : ''}"><div class="ios-gesture-pill"></div></div>`
|
|
2054
|
+
: '';
|
|
1417
2055
|
|
|
1418
2056
|
return `<!doctype html>
|
|
1419
2057
|
<html>
|
|
@@ -1427,24 +2065,33 @@ function createDeviceFrameHtml(profile, screenshotBuffer, imageWidth = profile.w
|
|
|
1427
2065
|
place-items: center;
|
|
1428
2066
|
position: relative;
|
|
1429
2067
|
background:
|
|
1430
|
-
radial-gradient(circle at
|
|
1431
|
-
radial-gradient(circle at
|
|
1432
|
-
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});
|
|
1433
2071
|
font-family: Arial, sans-serif;
|
|
1434
2072
|
}
|
|
1435
|
-
body.phone-frame { background: #eef2f9; }
|
|
1436
|
-
body.phone-frame::before { display: none; }
|
|
1437
2073
|
body::before {
|
|
1438
2074
|
content: '';
|
|
1439
2075
|
position: absolute;
|
|
1440
2076
|
inset: 0;
|
|
1441
|
-
opacity: 0.
|
|
1442
|
-
background-image:
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
background-size: 7vw 7vw;
|
|
1446
|
-
mask-image: linear-gradient(to bottom, transparent, black 28%, black 72%, transparent);
|
|
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);
|
|
1447
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;
|
|
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; }
|
|
1448
2095
|
.device {
|
|
1449
2096
|
position: relative;
|
|
1450
2097
|
z-index: 1;
|
|
@@ -1456,6 +2103,9 @@ function createDeviceFrameHtml(profile, screenshotBuffer, imageWidth = profile.w
|
|
|
1456
2103
|
0 0 0 1px rgba(255, 255, 255, 0.18) inset,
|
|
1457
2104
|
0 0 5vh rgba(45, 213, 255, 0.12);
|
|
1458
2105
|
}
|
|
2106
|
+
.device:not(.android-phone):not(.iphone) {
|
|
2107
|
+
box-sizing: content-box;
|
|
2108
|
+
}
|
|
1459
2109
|
.screen {
|
|
1460
2110
|
position: relative;
|
|
1461
2111
|
width: 100%;
|
|
@@ -1509,7 +2159,7 @@ function createDeviceFrameHtml(profile, screenshotBuffer, imageWidth = profile.w
|
|
|
1509
2159
|
font-size: clamp(11px, 3vw, 40px);
|
|
1510
2160
|
white-space: nowrap;
|
|
1511
2161
|
}
|
|
1512
|
-
.ios-statusbar { background:
|
|
2162
|
+
.ios-statusbar { background: ${statusBarStyle?.background || 'transparent'}; color: ${statusBarStyle?.foreground || '#050505'}; font-weight: 700; }
|
|
1513
2163
|
.ios-statusbar svg { width: 27%; height: 62%; margin-left: auto; margin-right: 3%; }
|
|
1514
2164
|
.ios-statusbar .dynamic-island {
|
|
1515
2165
|
position: absolute;
|
|
@@ -1521,17 +2171,38 @@ function createDeviceFrameHtml(profile, screenshotBuffer, imageWidth = profile.w
|
|
|
1521
2171
|
border-radius: 999px;
|
|
1522
2172
|
background: #000;
|
|
1523
2173
|
}
|
|
1524
|
-
.android-statusbar { background:
|
|
1525
|
-
.android-notification { width:
|
|
1526
|
-
.android-status-icons { width:
|
|
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%; }
|
|
1527
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;
|
|
1528
2192
|
flex: none;
|
|
1529
2193
|
height: 2.2%;
|
|
1530
2194
|
display: grid;
|
|
1531
2195
|
place-items: center;
|
|
1532
|
-
background:
|
|
2196
|
+
background: ${navigationBarStyle?.background || 'transparent'};
|
|
1533
2197
|
}
|
|
1534
|
-
.gesture-pill {
|
|
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%; }
|
|
1535
2206
|
.camera-dot {
|
|
1536
2207
|
position: absolute;
|
|
1537
2208
|
z-index: 3;
|
|
@@ -1564,6 +2235,85 @@ function createDeviceFrameHtml(profile, screenshotBuffer, imageWidth = profile.w
|
|
|
1564
2235
|
.device.tablet .screen {
|
|
1565
2236
|
border-radius: clamp(13px, 2.5vw, 38px);
|
|
1566
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
|
+
}
|
|
1567
2317
|
.device.apple-watch {
|
|
1568
2318
|
height: 82%;
|
|
1569
2319
|
padding: 2.2%;
|
|
@@ -1695,9 +2445,49 @@ function createDeviceFrameHtml(profile, screenshotBuffer, imageWidth = profile.w
|
|
|
1695
2445
|
background: #6fe8ff;
|
|
1696
2446
|
box-shadow: 0 0 1.8vh #2dd5ff;
|
|
1697
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
|
+
}
|
|
1698
2486
|
</style>
|
|
1699
2487
|
</head>
|
|
1700
|
-
<body class="${phonePlatform ? 'phone-frame' : ''}">
|
|
2488
|
+
<body class="${phonePlatform ? 'phone-frame' : ''} ${screenshotCopy ? 'has-copy' : ''}">
|
|
2489
|
+
${createPromotionalDecorations(palette)}
|
|
2490
|
+
${createScreenshotCopyMarkup(screenshotCopy)}
|
|
1701
2491
|
<main class="device ${profile.frameType}">
|
|
1702
2492
|
<div class="screen">${phoneStatusBar}<img src="${screenshotDataUrl}" />${phoneNavigation}</div>
|
|
1703
2493
|
${getFrameAdornment(profile.frameType)}
|
|
@@ -1739,6 +2529,88 @@ function readJsonAssetSource(filePath) {
|
|
|
1739
2529
|
}
|
|
1740
2530
|
}
|
|
1741
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
|
+
|
|
1742
2614
|
function collectProjectUiPhrases() {
|
|
1743
2615
|
const phrases = [];
|
|
1744
2616
|
const pageDirectory = path.join(__dirname, 'src', 'pages');
|
|
@@ -2340,26 +3212,48 @@ async function captureFramedScreenshot(
|
|
|
2340
3212
|
profile,
|
|
2341
3213
|
screenshotFileName,
|
|
2342
3214
|
skipExisting = false,
|
|
3215
|
+
outputRootDirectory = inFrameDirectory,
|
|
2343
3216
|
) {
|
|
2344
3217
|
if (profile.screenshotOnly) return false;
|
|
2345
3218
|
|
|
2346
|
-
const
|
|
2347
|
-
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);
|
|
2348
3223
|
|
|
2349
3224
|
if (skipExisting && fs.existsSync(framedScreenshotPath)) {
|
|
2350
3225
|
console.log(`Skipped existing framed screenshot: ${framedScreenshotPath}`);
|
|
2351
3226
|
return false;
|
|
2352
3227
|
}
|
|
2353
3228
|
|
|
2354
|
-
const originalImageMetadata =
|
|
2355
|
-
? await sharp(screenshotBuffer).metadata()
|
|
2356
|
-
: null;
|
|
3229
|
+
const originalImageMetadata = await sharp(screenshotBuffer).metadata();
|
|
2357
3230
|
const hasCustomFrameSize = Boolean(profile.framedWidth && profile.framedHeight);
|
|
2358
3231
|
const frameWidth = profile.framedWidth || profile.width;
|
|
2359
3232
|
const frameHeight = profile.framedHeight || profile.height;
|
|
2360
3233
|
const frameScaleFactor = hasCustomFrameSize ? 1 : profile.deviceScaleFactor;
|
|
2361
3234
|
const phonePlatform = profile.frameType === 'iphone' ? 'ios' : profile.frameType === 'android-phone' ? 'android' : null;
|
|
2362
|
-
const
|
|
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
|
+
}
|
|
2363
3257
|
|
|
2364
3258
|
await client.send('Emulation.setDeviceMetricsOverride', {
|
|
2365
3259
|
width: frameWidth,
|
|
@@ -2379,8 +3273,15 @@ async function captureFramedScreenshot(
|
|
|
2379
3273
|
? createStoreDeviceFrameHtml(
|
|
2380
3274
|
profile, screenshotBuffer, originalImageMetadata.width, originalImageMetadata.height,
|
|
2381
3275
|
frameWidth, frameHeight, originalImageMetadata.format, displayTime,
|
|
3276
|
+
batteryPercentage, statusBarStyle, navigationBarStyle,
|
|
3277
|
+
screenshotCopy, promotionalPalette,
|
|
2382
3278
|
)
|
|
2383
|
-
: createDeviceFrameHtml(
|
|
3279
|
+
: createDeviceFrameHtml(
|
|
3280
|
+
profile, screenshotBuffer, originalImageMetadata.width, originalImageMetadata.height,
|
|
3281
|
+
originalImageMetadata.format,
|
|
3282
|
+
displayTime, batteryPercentage, statusBarStyle, navigationBarStyle,
|
|
3283
|
+
screenshotCopy, promotionalPalette,
|
|
3284
|
+
),
|
|
2384
3285
|
});
|
|
2385
3286
|
await client.send('Runtime.evaluate', {
|
|
2386
3287
|
expression: `new Promise((resolve) => {
|
|
@@ -2502,25 +3403,10 @@ async function captureScreenshotSet(
|
|
|
2502
3403
|
selectedProfiles,
|
|
2503
3404
|
fileNameInput,
|
|
2504
3405
|
) {
|
|
2505
|
-
const storeProfile = selectedProfiles.find((profile) => profile.storeRequirements);
|
|
2506
|
-
if (storeProfile) {
|
|
2507
|
-
const profileDirectory = path.join(screenshotDirectory, storeProfile.outputDirectory);
|
|
2508
|
-
const existingImageCount = fs.existsSync(profileDirectory)
|
|
2509
|
-
? fs.readdirSync(profileDirectory, { withFileTypes: true })
|
|
2510
|
-
.filter((entry) => entry.isFile() && /\.(png|jpe?g|webp)$/i.test(entry.name))
|
|
2511
|
-
.length
|
|
2512
|
-
: 0;
|
|
2513
|
-
|
|
2514
|
-
if (existingImageCount >= storeProfile.storeRequirements.maxImages) {
|
|
2515
|
-
throw new Error(
|
|
2516
|
-
`${storeProfile.label} already contains ${existingImageCount} images; the store allows a maximum of ${storeProfile.storeRequirements.maxImages}.`,
|
|
2517
|
-
);
|
|
2518
|
-
}
|
|
2519
|
-
}
|
|
2520
|
-
|
|
2521
3406
|
console.log('Capturing the current rendered page and resolved native data...');
|
|
2522
3407
|
const snapshot = await capturePageSnapshot(deviceClient);
|
|
2523
3408
|
const screenshotFileName = getScreenshotFileName(fileNameInput, selectedProfiles);
|
|
3409
|
+
saveScreenshotInfo(screenshotFileName, snapshot.screenInfo);
|
|
2524
3410
|
|
|
2525
3411
|
for (const profile of selectedProfiles) {
|
|
2526
3412
|
await captureProfile(replayClient, snapshot, profile, screenshotFileName);
|
|
@@ -2529,11 +3415,23 @@ async function captureScreenshotSet(
|
|
|
2529
3415
|
console.log(`Responsive screenshot set completed: ${screenshotFileName}`);
|
|
2530
3416
|
}
|
|
2531
3417
|
|
|
2532
|
-
async function frameExistingScreenshots(
|
|
3418
|
+
async function frameExistingScreenshots(
|
|
3419
|
+
selectedProfiles,
|
|
3420
|
+
overwriteExisting = false,
|
|
3421
|
+
screenshotNames = '',
|
|
3422
|
+
outputRootDirectory = inFrameDirectory,
|
|
3423
|
+
) {
|
|
2533
3424
|
let replayBrowser;
|
|
2534
3425
|
let replayClient;
|
|
2535
3426
|
let framedCount = 0;
|
|
2536
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();
|
|
2537
3435
|
|
|
2538
3436
|
try {
|
|
2539
3437
|
replayBrowser = await launchReplayBrowser();
|
|
@@ -2547,12 +3445,17 @@ async function frameExistingScreenshots(selectedProfiles, overwriteExisting = fa
|
|
|
2547
3445
|
}
|
|
2548
3446
|
|
|
2549
3447
|
const screenshotFiles = fs.readdirSync(profileDirectory, { withFileTypes: true })
|
|
2550
|
-
.filter((entry) => entry.isFile() && /\.png$/i.test(entry.name))
|
|
3448
|
+
.filter((entry) => entry.isFile() && /\.(?:png|jpe?g|webp)$/i.test(entry.name))
|
|
2551
3449
|
.map((entry) => entry.name)
|
|
3450
|
+
.filter((fileName) => (
|
|
3451
|
+
!selectedScreenshotNames.size
|
|
3452
|
+
|| selectedScreenshotNames.has(path.parse(fileName).name)
|
|
3453
|
+
))
|
|
2552
3454
|
.sort((first, second) => first.localeCompare(second, undefined, { numeric: true }));
|
|
2553
3455
|
|
|
2554
3456
|
for (const screenshotFileName of screenshotFiles) {
|
|
2555
3457
|
const screenshotPath = path.join(profileDirectory, screenshotFileName);
|
|
3458
|
+
foundScreenshotNames.add(path.parse(screenshotFileName).name);
|
|
2556
3459
|
|
|
2557
3460
|
try {
|
|
2558
3461
|
const wasCreated = await captureFramedScreenshot(
|
|
@@ -2561,6 +3464,7 @@ async function frameExistingScreenshots(selectedProfiles, overwriteExisting = fa
|
|
|
2561
3464
|
profile,
|
|
2562
3465
|
screenshotFileName,
|
|
2563
3466
|
!overwriteExisting,
|
|
3467
|
+
outputRootDirectory,
|
|
2564
3468
|
);
|
|
2565
3469
|
if (wasCreated) framedCount += 1;
|
|
2566
3470
|
} catch (error) {
|
|
@@ -2581,6 +3485,14 @@ async function frameExistingScreenshots(selectedProfiles, overwriteExisting = fa
|
|
|
2581
3485
|
|
|
2582
3486
|
console.log(`Existing screenshot framing completed: ${framedCount} created.`);
|
|
2583
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
|
+
|
|
2584
3496
|
if (failedCount) {
|
|
2585
3497
|
throw new Error(`${failedCount} existing screenshot(s) could not be framed.`);
|
|
2586
3498
|
}
|
|
@@ -2769,6 +3681,7 @@ async function getInteractiveOptions(options) {
|
|
|
2769
3681
|
|| options.dryRun
|
|
2770
3682
|
|| options.amazonAssets
|
|
2771
3683
|
|| options.samsungAssets
|
|
3684
|
+
|| options.refreshInfo
|
|
2772
3685
|
|| options.vivoExisting
|
|
2773
3686
|
|| options.oppoExisting
|
|
2774
3687
|
) {
|
|
@@ -2815,6 +3728,10 @@ async function main() {
|
|
|
2815
3728
|
|
|
2816
3729
|
options = await getInteractiveOptions(options);
|
|
2817
3730
|
|
|
3731
|
+
if (!options.dryRun && !options.refreshInfo) {
|
|
3732
|
+
reconcileScreenshotRecords();
|
|
3733
|
+
}
|
|
3734
|
+
|
|
2818
3735
|
if (options.amazonAssets) {
|
|
2819
3736
|
let amazonBrowser;
|
|
2820
3737
|
let amazonClient;
|
|
@@ -2865,16 +3782,21 @@ async function main() {
|
|
|
2865
3782
|
|
|
2866
3783
|
const selectedProfiles = selectProfiles(options.profiles);
|
|
2867
3784
|
|
|
2868
|
-
if (!options.dryRun) {
|
|
3785
|
+
if (!options.dryRun && !options.refreshInfo) {
|
|
2869
3786
|
await prepareFrameSelection(options);
|
|
2870
3787
|
}
|
|
2871
3788
|
|
|
2872
3789
|
if (options.frameExisting || options.reframeExisting) {
|
|
2873
|
-
await frameExistingScreenshots(
|
|
3790
|
+
await frameExistingScreenshots(
|
|
3791
|
+
selectedProfiles,
|
|
3792
|
+
options.reframeExisting,
|
|
3793
|
+
options.screenshots,
|
|
3794
|
+
options.reframeExisting ? uploadDirectory : inFrameDirectory,
|
|
3795
|
+
);
|
|
2874
3796
|
return;
|
|
2875
3797
|
}
|
|
2876
3798
|
|
|
2877
|
-
if (!options.dryRun) {
|
|
3799
|
+
if (!options.dryRun && !options.refreshInfo) {
|
|
2878
3800
|
await createMissingStoreAssets();
|
|
2879
3801
|
}
|
|
2880
3802
|
|
|
@@ -2916,6 +3838,20 @@ async function main() {
|
|
|
2916
3838
|
return;
|
|
2917
3839
|
}
|
|
2918
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
|
+
|
|
2919
3855
|
fs.mkdirSync(screenshotDirectory, { recursive: true });
|
|
2920
3856
|
replayBrowser = await launchReplayBrowser();
|
|
2921
3857
|
replayClient = await createReplayPage(replayBrowser);
|