codeplay-common 4.7.3 → 4.7.5
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 +1041 -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,178 @@ 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
|
+
.toColourspace('srgb')
|
|
1777
|
+
.removeAlpha()
|
|
1778
|
+
.raw()
|
|
1779
|
+
.toBuffer({ resolveWithObject: true });
|
|
1780
|
+
const colorBuckets = new Map();
|
|
1781
|
+
const neutralBuckets = new Map();
|
|
1782
|
+
|
|
1783
|
+
for (let index = 0; index < data.length; index += info.channels) {
|
|
1784
|
+
const red = data[index];
|
|
1785
|
+
const green = data[index + 1];
|
|
1786
|
+
const blue = data[index + 2];
|
|
1787
|
+
const hsl = rgbToHsl(red, green, blue);
|
|
1788
|
+
const isColorful = hsl.lightness >= 0.1 && hsl.lightness <= 0.94
|
|
1789
|
+
&& hsl.saturation >= 0.12 && Math.max(red, green, blue) - Math.min(red, green, blue) >= 12;
|
|
1790
|
+
const buckets = isColorful ? colorBuckets : neutralBuckets;
|
|
1791
|
+
// Combine shades of the same hue so large tinted surfaces and their controls count together.
|
|
1792
|
+
const key = isColorful
|
|
1793
|
+
? Math.round(hsl.hue / 15) % 24
|
|
1794
|
+
: `${Math.round(red / 24)},${Math.round(green / 24)},${Math.round(blue / 24)}`;
|
|
1795
|
+
const bucket = buckets.get(key) || {
|
|
1796
|
+
count: 0, red: 0, green: 0, blue: 0, saturation: 0, lightness: 0,
|
|
1797
|
+
};
|
|
1798
|
+
bucket.count += 1;
|
|
1799
|
+
bucket.red += red;
|
|
1800
|
+
bucket.green += green;
|
|
1801
|
+
bucket.blue += blue;
|
|
1802
|
+
bucket.saturation += hsl.saturation;
|
|
1803
|
+
bucket.lightness += hsl.lightness;
|
|
1804
|
+
buckets.set(key, bucket);
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
let selectedColor;
|
|
1808
|
+
let selectedScore = -1;
|
|
1809
|
+
for (const bucket of (colorBuckets.size ? colorBuckets : neutralBuckets).values()) {
|
|
1810
|
+
const saturation = bucket.saturation / bucket.count;
|
|
1811
|
+
const lightness = bucket.lightness / bucket.count;
|
|
1812
|
+
const score = colorBuckets.size
|
|
1813
|
+
? bucket.count * (0.55 + saturation * 1.8)
|
|
1814
|
+
* (1 - Math.abs(lightness - 0.55) * 0.7)
|
|
1815
|
+
: bucket.count;
|
|
1816
|
+
if (score <= selectedScore) continue;
|
|
1817
|
+
selectedScore = score;
|
|
1818
|
+
selectedColor = {
|
|
1819
|
+
red: Math.round(bucket.red / bucket.count),
|
|
1820
|
+
green: Math.round(bucket.green / bucket.count),
|
|
1821
|
+
blue: Math.round(bucket.blue / bucket.count),
|
|
1822
|
+
};
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
const seed = getDecorationSeed(path.parse(screenshotFileName).name);
|
|
1826
|
+
const sourceHsl = rgbToHsl(selectedColor.red, selectedColor.green, selectedColor.blue);
|
|
1827
|
+
const hue = Math.round(sourceHsl.hue) % 360;
|
|
1828
|
+
const secondaryHue = (hue + 18) % 360;
|
|
1829
|
+
const saturation = colorBuckets.size
|
|
1830
|
+
? Math.min(75, Math.round(sourceHsl.saturation * 100))
|
|
1831
|
+
: 0;
|
|
1832
|
+
const backgroundSaturation = Math.round(saturation * 0.75);
|
|
1833
|
+
|
|
1834
|
+
return {
|
|
1835
|
+
seed,
|
|
1836
|
+
accent: `hsl(${hue} ${saturation}% 46%)`,
|
|
1837
|
+
accentSoft: `hsla(${hue} ${saturation}% 55% / 0.2)`,
|
|
1838
|
+
secondary: `hsl(${secondaryHue} ${saturation}% 54%)`,
|
|
1839
|
+
secondarySoft: `hsla(${secondaryHue} ${saturation}% 58% / 0.18)`,
|
|
1840
|
+
backgroundStart: `hsl(${hue} ${backgroundSaturation}% 97%)`,
|
|
1841
|
+
backgroundMiddle: `hsl(${hue} ${backgroundSaturation}% 93%)`,
|
|
1842
|
+
backgroundEnd: `hsl(${secondaryHue} ${backgroundSaturation}% 88%)`,
|
|
1843
|
+
text: `hsl(${hue} ${Math.min(35, saturation)}% 17%)`,
|
|
1844
|
+
mutedText: `hsl(${hue} ${Math.min(18, saturation)}% 38%)`,
|
|
1845
|
+
};
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
function createStoreDeviceFrameHtml(
|
|
1849
|
+
profile,
|
|
1850
|
+
screenshotBuffer,
|
|
1851
|
+
imageWidth,
|
|
1852
|
+
imageHeight,
|
|
1853
|
+
canvasWidth,
|
|
1854
|
+
canvasHeight,
|
|
1855
|
+
imageFormat,
|
|
1856
|
+
displayTime,
|
|
1857
|
+
batteryPercentage,
|
|
1858
|
+
statusBarStyle,
|
|
1859
|
+
navigationBarStyle,
|
|
1860
|
+
screenshotCopy,
|
|
1861
|
+
palette,
|
|
1862
|
+
) {
|
|
1287
1863
|
const screenshotDataUrl = `data:image/${imageFormat === 'jpeg' ? 'jpeg' : 'png'};base64,${screenshotBuffer.toString('base64')}`;
|
|
1288
|
-
const
|
|
1289
|
-
const
|
|
1864
|
+
const availableDeviceHeight = screenshotCopy ? canvasHeight * 0.64 : canvasHeight - 180;
|
|
1865
|
+
const screenWidth = Math.floor(Math.min(canvasWidth - 160, availableDeviceHeight * imageWidth / imageHeight));
|
|
1290
1866
|
const frameStyle = frameStyles.android[selectedFrameStyles.android];
|
|
1291
1867
|
|
|
1292
1868
|
return `<!doctype html>
|
|
@@ -1299,18 +1875,40 @@ function createStoreDeviceFrameHtml(profile, screenshotBuffer, imageWidth, image
|
|
|
1299
1875
|
body {
|
|
1300
1876
|
display: grid;
|
|
1301
1877
|
place-items: center;
|
|
1302
|
-
|
|
1878
|
+
position: relative;
|
|
1879
|
+
background:
|
|
1880
|
+
radial-gradient(circle at 12% 16%, ${palette.accentSoft}, transparent 32%),
|
|
1881
|
+
radial-gradient(circle at 88% 84%, ${palette.secondarySoft}, transparent 34%),
|
|
1882
|
+
linear-gradient(145deg, ${palette.backgroundStart}, ${palette.backgroundMiddle} 52%, ${palette.backgroundEnd});
|
|
1303
1883
|
}
|
|
1304
1884
|
body::before {
|
|
1305
1885
|
content: '';
|
|
1306
1886
|
position: absolute;
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
background:
|
|
1311
|
-
|
|
1312
|
-
|
|
1887
|
+
inset: 0;
|
|
1888
|
+
opacity: 0.16;
|
|
1889
|
+
background-image: radial-gradient(${palette.accent} 1px, transparent 1px);
|
|
1890
|
+
background-size: 28px 28px;
|
|
1891
|
+
mask-image: linear-gradient(135deg, black, transparent 42%, transparent 64%, black);
|
|
1892
|
+
}
|
|
1893
|
+
${getPromotionalDecorationStyles()}
|
|
1894
|
+
.promo-copy {
|
|
1895
|
+
position: absolute;
|
|
1896
|
+
z-index: 2;
|
|
1897
|
+
top: 4.5%;
|
|
1898
|
+
right: 7%;
|
|
1899
|
+
left: 7%;
|
|
1900
|
+
display: flex;
|
|
1901
|
+
flex-direction: column;
|
|
1902
|
+
align-items: center;
|
|
1903
|
+
color: ${palette.text};
|
|
1904
|
+
text-align: center;
|
|
1905
|
+
font-family: Arial, sans-serif;
|
|
1313
1906
|
}
|
|
1907
|
+
.promo-copy h1 { max-width: 900px; margin: 0; font-size: clamp(42px, 5.6vw, 72px); line-height: 1.02; letter-spacing: -0.035em; }
|
|
1908
|
+
.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; }
|
|
1909
|
+
.promo-features { display: flex; flex-wrap: wrap; justify-content: center; gap: 12px; margin-top: 2.4%; }
|
|
1910
|
+
.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; }
|
|
1911
|
+
body.has-copy .device { position: absolute; bottom: 3.5%; left: 50%; transform: translateX(-50%); }
|
|
1314
1912
|
.device {
|
|
1315
1913
|
position: relative;
|
|
1316
1914
|
z-index: 1;
|
|
@@ -1331,12 +1929,13 @@ function createStoreDeviceFrameHtml(profile, screenshotBuffer, imageWidth, image
|
|
|
1331
1929
|
box-shadow: 0 0 0 2px #03060c;
|
|
1332
1930
|
}
|
|
1333
1931
|
.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:
|
|
1932
|
+
.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
1933
|
.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
|
-
.
|
|
1934
|
+
.android-statusbar { height: 4%; font-family: Roboto, Arial, sans-serif; }
|
|
1935
|
+
.android-notification { width: 3.4%; height: 46%; margin-left: 2%; }
|
|
1936
|
+
.android-status-icons { width: 32%; height: 58%; margin-left: auto; margin-right: 4%; }
|
|
1937
|
+
.android-navigation { position: absolute; z-index: 2; right: 0; bottom: 0; left: 0; height: 2.2%; display: grid; place-items: center; background: transparent; }
|
|
1938
|
+
.gesture-pill { width: 40%; height: 14%; border-radius: 999px; background: ${navigationBarStyle.foreground}; }
|
|
1340
1939
|
.camera {
|
|
1341
1940
|
position: absolute;
|
|
1342
1941
|
top: 11px;
|
|
@@ -1359,61 +1958,112 @@ function createStoreDeviceFrameHtml(profile, screenshotBuffer, imageWidth, image
|
|
|
1359
1958
|
}
|
|
1360
1959
|
</style>
|
|
1361
1960
|
</head>
|
|
1362
|
-
<body>
|
|
1961
|
+
<body class="${screenshotCopy ? 'has-copy' : ''}">
|
|
1962
|
+
${createPromotionalDecorations(palette)}
|
|
1963
|
+
${createScreenshotCopyMarkup(screenshotCopy)}
|
|
1363
1964
|
<main class="device">
|
|
1364
1965
|
<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>
|
|
1966
|
+
<div class="screen">${getPhoneStatusBar('android', displayTime, batteryPercentage)}<img src="${screenshotDataUrl}" /><div class="android-navigation"><div class="gesture-pill"></div></div></div>
|
|
1366
1967
|
<div class="side-button"></div>
|
|
1367
1968
|
</main>
|
|
1368
1969
|
</body>
|
|
1369
1970
|
</html>`;
|
|
1370
1971
|
}
|
|
1371
1972
|
|
|
1372
|
-
function getPhoneStatusBar(platform, time) {
|
|
1973
|
+
function getPhoneStatusBar(platform, time, batteryPercentage = null) {
|
|
1974
|
+
const batteryLevel = Number.isInteger(batteryPercentage) ? batteryPercentage : 75;
|
|
1975
|
+
|
|
1373
1976
|
if (platform === 'ios') {
|
|
1374
1977
|
return `<div class="phone-statusbar ios-statusbar">
|
|
1375
1978
|
<span class="status-time">${time}</span>
|
|
1376
1979
|
<div class="dynamic-island"></div>
|
|
1377
1980
|
<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="
|
|
1981
|
+
<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>
|
|
1982
|
+
<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>
|
|
1983
|
+
<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>
|
|
1984
|
+
<rect x="61" y="7" width="22" height="15" rx="4" fill="none" stroke="currentColor" stroke-width="1.5" opacity="0.65"></rect>
|
|
1985
|
+
<rect x="64" y="10" width="16" height="9" rx="2" fill="currentColor"></rect>
|
|
1986
|
+
<rect x="85" y="12" width="2" height="5" rx="1" fill="currentColor" opacity="0.65"></rect>
|
|
1384
1987
|
</svg>
|
|
1385
1988
|
</div>`;
|
|
1386
1989
|
}
|
|
1387
1990
|
|
|
1991
|
+
if (platform === 'ipad') {
|
|
1992
|
+
const batteryFillWidth = Math.max(2, Math.round(24 * batteryLevel / 100));
|
|
1993
|
+
return `<div class="phone-statusbar ipad-statusbar">
|
|
1994
|
+
<span class="status-time">${time}</span>
|
|
1995
|
+
<svg class="ipad-status-icons" viewBox="0 0 130 28" aria-hidden="true">
|
|
1996
|
+
<g fill="currentColor">
|
|
1997
|
+
<rect x="2" y="19" width="3" height="5" rx="1"></rect>
|
|
1998
|
+
<rect x="7" y="15" width="3" height="9" rx="1"></rect>
|
|
1999
|
+
<rect x="12" y="11" width="3" height="13" rx="1"></rect>
|
|
2000
|
+
<rect x="17" y="7" width="3" height="17" rx="1"></rect>
|
|
2001
|
+
</g>
|
|
2002
|
+
<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>
|
|
2003
|
+
<circle cx="38" cy="21" r="1.7" fill="currentColor"></circle>
|
|
2004
|
+
<text x="54" y="20" font-size="12" font-weight="600" fill="currentColor">${batteryLevel}%</text>
|
|
2005
|
+
<rect x="88" y="6" width="32" height="17" rx="4" fill="none" stroke="currentColor" stroke-width="1.6"></rect>
|
|
2006
|
+
<rect x="91" y="9" width="${batteryFillWidth}" height="11" rx="2" fill="currentColor" opacity="0.72"></rect>
|
|
2007
|
+
<rect x="122" y="11" width="3" height="7" rx="1.5" fill="currentColor"></rect>
|
|
2008
|
+
</svg>
|
|
2009
|
+
</div>`;
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
const batteryFillWidth = Math.max(2, Math.round(30 * batteryLevel / 100));
|
|
2013
|
+
|
|
1388
2014
|
return `<div class="phone-statusbar android-statusbar">
|
|
1389
2015
|
<span class="status-time">${time}</span>
|
|
1390
|
-
<svg class="android-notification" viewBox="0 0
|
|
1391
|
-
<circle cx="
|
|
1392
|
-
<circle cx="
|
|
2016
|
+
<svg class="android-notification" viewBox="0 0 24 24" aria-hidden="true">
|
|
2017
|
+
<circle cx="12" cy="12" r="9.5" fill="none" stroke="currentColor" stroke-width="1.8"></circle>
|
|
2018
|
+
<circle cx="8" cy="10" r="1.3" fill="currentColor"></circle>
|
|
2019
|
+
<circle cx="12" cy="8" r="1.3" fill="currentColor"></circle>
|
|
2020
|
+
<circle cx="16" cy="10" r="1.3" fill="currentColor"></circle>
|
|
2021
|
+
<path d="M7.5 14 Q12 18 16.5 14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"></path>
|
|
1393
2022
|
</svg>
|
|
1394
|
-
<svg class="android-status-icons" viewBox="0 0
|
|
1395
|
-
<
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
<
|
|
1402
|
-
<
|
|
1403
|
-
<text x="
|
|
2023
|
+
<svg class="android-status-icons" viewBox="0 0 200 28" aria-hidden="true">
|
|
2024
|
+
<g fill="currentColor">
|
|
2025
|
+
<rect x="1" y="20" width="5" height="5" rx="1"></rect><rect x="8" y="16" width="5" height="9" rx="1"></rect>
|
|
2026
|
+
<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>
|
|
2027
|
+
<rect x="43" y="20" width="5" height="5" rx="1"></rect><rect x="50" y="16" width="5" height="9" rx="1"></rect>
|
|
2028
|
+
<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>
|
|
2029
|
+
</g>
|
|
2030
|
+
<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>
|
|
2031
|
+
<circle cx="99" cy="22" r="2" fill="currentColor"></circle>
|
|
2032
|
+
<text x="108" y="24" font-size="7" font-weight="700" fill="currentColor">6</text>
|
|
2033
|
+
<rect x="121" y="4" width="39" height="21" rx="5" fill="none" stroke="currentColor" stroke-width="2"></rect>
|
|
2034
|
+
<rect x="125" y="8" width="${batteryFillWidth}" height="13" rx="2" fill="currentColor" opacity="0.65"></rect>
|
|
2035
|
+
<rect x="162" y="10" width="3" height="9" rx="1.5" fill="currentColor"></rect>
|
|
2036
|
+
<text x="169" y="21" font-size="13" font-weight="500" fill="currentColor">${batteryLevel}%</text>
|
|
1404
2037
|
</svg>
|
|
1405
2038
|
</div>`;
|
|
1406
2039
|
}
|
|
1407
2040
|
|
|
1408
|
-
function createDeviceFrameHtml(
|
|
2041
|
+
function createDeviceFrameHtml(
|
|
2042
|
+
profile,
|
|
2043
|
+
screenshotBuffer,
|
|
2044
|
+
imageWidth = profile.width,
|
|
2045
|
+
imageHeight = profile.height,
|
|
2046
|
+
imageFormat = 'png',
|
|
2047
|
+
displayTime = '',
|
|
2048
|
+
batteryPercentage = null,
|
|
2049
|
+
statusBarStyle = null,
|
|
2050
|
+
navigationBarStyle = null,
|
|
2051
|
+
screenshotCopy = null,
|
|
2052
|
+
palette,
|
|
2053
|
+
) {
|
|
1409
2054
|
const screenshotDataUrl = `data:image/${imageFormat === 'jpeg' ? 'jpeg' : 'png'};base64,${screenshotBuffer.toString('base64')}`;
|
|
1410
2055
|
const aspectRatio = `${imageWidth} / ${imageHeight}`;
|
|
1411
2056
|
const phonePlatform = profile.frameType === 'iphone' ? 'ios' : profile.frameType === 'android-phone' ? 'android' : null;
|
|
2057
|
+
const statusBarPlatform = profile.frameType === 'ipad' ? 'ipad' : phonePlatform;
|
|
1412
2058
|
const phoneStyle = phonePlatform && frameStyles[phonePlatform][selectedFrameStyles[phonePlatform]];
|
|
1413
|
-
const phoneStatusBar =
|
|
2059
|
+
const phoneStatusBar = statusBarPlatform
|
|
2060
|
+
? getPhoneStatusBar(statusBarPlatform, displayTime, batteryPercentage)
|
|
2061
|
+
: '';
|
|
1414
2062
|
const phoneNavigation = phonePlatform === 'android'
|
|
1415
2063
|
? '<div class="android-navigation"><div class="gesture-pill"></div></div>'
|
|
1416
|
-
: ''
|
|
2064
|
+
: statusBarPlatform === 'ios' || statusBarPlatform === 'ipad'
|
|
2065
|
+
? `<div class="ios-navigation${statusBarPlatform === 'ipad' ? ' ipad-navigation' : ''}"><div class="ios-gesture-pill"></div></div>`
|
|
2066
|
+
: '';
|
|
1417
2067
|
|
|
1418
2068
|
return `<!doctype html>
|
|
1419
2069
|
<html>
|
|
@@ -1427,24 +2077,33 @@ function createDeviceFrameHtml(profile, screenshotBuffer, imageWidth = profile.w
|
|
|
1427
2077
|
place-items: center;
|
|
1428
2078
|
position: relative;
|
|
1429
2079
|
background:
|
|
1430
|
-
radial-gradient(circle at
|
|
1431
|
-
radial-gradient(circle at
|
|
1432
|
-
linear-gradient(145deg,
|
|
2080
|
+
radial-gradient(circle at 12% 16%, ${palette.accentSoft}, transparent 32%),
|
|
2081
|
+
radial-gradient(circle at 88% 84%, ${palette.secondarySoft}, transparent 34%),
|
|
2082
|
+
linear-gradient(145deg, ${palette.backgroundStart}, ${palette.backgroundMiddle} 52%, ${palette.backgroundEnd});
|
|
1433
2083
|
font-family: Arial, sans-serif;
|
|
1434
2084
|
}
|
|
1435
|
-
body.phone-frame { background: #eef2f9; }
|
|
1436
|
-
body.phone-frame::before { display: none; }
|
|
1437
2085
|
body::before {
|
|
1438
2086
|
content: '';
|
|
1439
2087
|
position: absolute;
|
|
1440
2088
|
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);
|
|
2089
|
+
opacity: 0.16;
|
|
2090
|
+
background-image: radial-gradient(${palette.accent} 0.8px, transparent 0.8px);
|
|
2091
|
+
background-size: clamp(18px, 2.2vw, 34px) clamp(18px, 2.2vw, 34px);
|
|
2092
|
+
mask-image: linear-gradient(135deg, black, transparent 42%, transparent 64%, black);
|
|
1447
2093
|
}
|
|
2094
|
+
${getPromotionalDecorationStyles()}
|
|
2095
|
+
.promo-copy {
|
|
2096
|
+
position: absolute;
|
|
2097
|
+
z-index: 4;
|
|
2098
|
+
display: flex;
|
|
2099
|
+
flex-direction: column;
|
|
2100
|
+
color: ${palette.text};
|
|
2101
|
+
font-family: Arial, sans-serif;
|
|
2102
|
+
}
|
|
2103
|
+
.promo-copy h1 { margin: 0; font-size: clamp(24px, 5.4vw, 82px); line-height: 1.02; letter-spacing: -0.04em; }
|
|
2104
|
+
.promo-copy p { margin: 1.8vh 0 0; color: ${palette.mutedText}; font-size: clamp(14px, 2.1vw, 34px); line-height: 1.35; }
|
|
2105
|
+
.promo-features { display: flex; flex-wrap: wrap; gap: clamp(6px, 0.9vw, 16px); margin-top: 2vh; }
|
|
2106
|
+
.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
2107
|
.device {
|
|
1449
2108
|
position: relative;
|
|
1450
2109
|
z-index: 1;
|
|
@@ -1456,6 +2115,9 @@ function createDeviceFrameHtml(profile, screenshotBuffer, imageWidth = profile.w
|
|
|
1456
2115
|
0 0 0 1px rgba(255, 255, 255, 0.18) inset,
|
|
1457
2116
|
0 0 5vh rgba(45, 213, 255, 0.12);
|
|
1458
2117
|
}
|
|
2118
|
+
.device:not(.android-phone):not(.iphone) {
|
|
2119
|
+
box-sizing: content-box;
|
|
2120
|
+
}
|
|
1459
2121
|
.screen {
|
|
1460
2122
|
position: relative;
|
|
1461
2123
|
width: 100%;
|
|
@@ -1509,7 +2171,7 @@ function createDeviceFrameHtml(profile, screenshotBuffer, imageWidth = profile.w
|
|
|
1509
2171
|
font-size: clamp(11px, 3vw, 40px);
|
|
1510
2172
|
white-space: nowrap;
|
|
1511
2173
|
}
|
|
1512
|
-
.ios-statusbar { background:
|
|
2174
|
+
.ios-statusbar { background: ${statusBarStyle?.background || 'transparent'}; color: ${statusBarStyle?.foreground || '#050505'}; font-weight: 700; }
|
|
1513
2175
|
.ios-statusbar svg { width: 27%; height: 62%; margin-left: auto; margin-right: 3%; }
|
|
1514
2176
|
.ios-statusbar .dynamic-island {
|
|
1515
2177
|
position: absolute;
|
|
@@ -1521,17 +2183,38 @@ function createDeviceFrameHtml(profile, screenshotBuffer, imageWidth = profile.w
|
|
|
1521
2183
|
border-radius: 999px;
|
|
1522
2184
|
background: #000;
|
|
1523
2185
|
}
|
|
1524
|
-
.android-statusbar { background:
|
|
1525
|
-
.android-notification { width:
|
|
1526
|
-
.android-status-icons { width:
|
|
2186
|
+
.android-statusbar { height: 4%; background: ${statusBarStyle?.background || 'transparent'}; color: ${statusBarStyle?.foreground || '#050505'}; font-family: Roboto, Arial, sans-serif; }
|
|
2187
|
+
.android-notification { width: 3.4%; height: 46%; margin-left: 2%; }
|
|
2188
|
+
.android-status-icons { width: 32%; height: 58%; margin-left: auto; margin-right: 4%; }
|
|
1527
2189
|
.android-navigation {
|
|
2190
|
+
position: absolute;
|
|
2191
|
+
z-index: 2;
|
|
2192
|
+
right: 0;
|
|
2193
|
+
bottom: 0;
|
|
2194
|
+
left: 0;
|
|
2195
|
+
height: 2.2%;
|
|
2196
|
+
display: grid;
|
|
2197
|
+
place-items: center;
|
|
2198
|
+
background: transparent;
|
|
2199
|
+
}
|
|
2200
|
+
.gesture-pill { width: 40%; height: 14%; border-radius: 999px; background: ${navigationBarStyle?.foreground || '#7a7b7c'}; }
|
|
2201
|
+
.ios-navigation {
|
|
2202
|
+
position: relative;
|
|
2203
|
+
z-index: 2;
|
|
1528
2204
|
flex: none;
|
|
1529
2205
|
height: 2.2%;
|
|
1530
2206
|
display: grid;
|
|
1531
2207
|
place-items: center;
|
|
1532
|
-
background:
|
|
2208
|
+
background: ${navigationBarStyle?.background || 'transparent'};
|
|
1533
2209
|
}
|
|
1534
|
-
.gesture-pill {
|
|
2210
|
+
.ios-gesture-pill {
|
|
2211
|
+
width: 36%;
|
|
2212
|
+
height: clamp(3px, 0.42vw, 8px);
|
|
2213
|
+
border-radius: 999px;
|
|
2214
|
+
background: ${navigationBarStyle?.foreground || '#050505'};
|
|
2215
|
+
}
|
|
2216
|
+
.ipad-navigation { height: 1.8%; }
|
|
2217
|
+
.ipad-navigation .ios-gesture-pill { width: 24%; }
|
|
1535
2218
|
.camera-dot {
|
|
1536
2219
|
position: absolute;
|
|
1537
2220
|
z-index: 3;
|
|
@@ -1564,6 +2247,85 @@ function createDeviceFrameHtml(profile, screenshotBuffer, imageWidth = profile.w
|
|
|
1564
2247
|
.device.tablet .screen {
|
|
1565
2248
|
border-radius: clamp(13px, 2.5vw, 38px);
|
|
1566
2249
|
}
|
|
2250
|
+
.device.ipad {
|
|
2251
|
+
height: 86%;
|
|
2252
|
+
padding: 4.1%;
|
|
2253
|
+
border: clamp(2px, 0.34vw, 7px) solid #9da3ad;
|
|
2254
|
+
border-radius: clamp(32px, 5.2vw, 84px);
|
|
2255
|
+
background: linear-gradient(145deg, #24272c 0%, #0b0c0e 22%, #050506 70%, #17191c 100%);
|
|
2256
|
+
box-shadow:
|
|
2257
|
+
0 4vh 9vh rgba(0, 0, 0, 0.62),
|
|
2258
|
+
0 0 0 clamp(1px, 0.16vw, 3px) rgba(230, 233, 238, 0.42) inset,
|
|
2259
|
+
0 0 0 clamp(3px, 0.45vw, 9px) rgba(0, 0, 0, 0.82) inset;
|
|
2260
|
+
}
|
|
2261
|
+
.device.ipad .screen {
|
|
2262
|
+
display: flex;
|
|
2263
|
+
flex-direction: column;
|
|
2264
|
+
border: clamp(2px, 0.25vw, 5px) solid #181a1e;
|
|
2265
|
+
border-radius: clamp(20px, 3.4vw, 55px);
|
|
2266
|
+
background: #000;
|
|
2267
|
+
box-shadow: 0 0 0 1px rgba(255,255,255,0.05), 0 0 1.4vh rgba(0,0,0,0.9) inset;
|
|
2268
|
+
}
|
|
2269
|
+
.device.ipad .screen img {
|
|
2270
|
+
flex: 1 1 0;
|
|
2271
|
+
min-height: 0;
|
|
2272
|
+
height: 0;
|
|
2273
|
+
object-fit: fill;
|
|
2274
|
+
}
|
|
2275
|
+
.ipad-statusbar {
|
|
2276
|
+
height: 3.2%;
|
|
2277
|
+
background: ${statusBarStyle?.background || 'transparent'};
|
|
2278
|
+
color: ${statusBarStyle?.foreground || '#050505'};
|
|
2279
|
+
font-weight: 700;
|
|
2280
|
+
}
|
|
2281
|
+
.ipad-statusbar .status-time {
|
|
2282
|
+
margin-left: 2.2%;
|
|
2283
|
+
font-size: clamp(9px, 1.18vw, 22px);
|
|
2284
|
+
}
|
|
2285
|
+
.ipad-status-icons {
|
|
2286
|
+
width: 17%;
|
|
2287
|
+
height: 58%;
|
|
2288
|
+
margin-left: auto;
|
|
2289
|
+
margin-right: 1.8%;
|
|
2290
|
+
}
|
|
2291
|
+
.ipad-camera-array {
|
|
2292
|
+
position: absolute;
|
|
2293
|
+
z-index: 3;
|
|
2294
|
+
top: 1.05%;
|
|
2295
|
+
left: 50%;
|
|
2296
|
+
display: flex;
|
|
2297
|
+
align-items: center;
|
|
2298
|
+
justify-content: center;
|
|
2299
|
+
gap: clamp(4px, 0.62vw, 12px);
|
|
2300
|
+
width: 18%;
|
|
2301
|
+
height: 1.65%;
|
|
2302
|
+
transform: translateX(-50%);
|
|
2303
|
+
}
|
|
2304
|
+
.ipad-sensor {
|
|
2305
|
+
display: block;
|
|
2306
|
+
flex: none;
|
|
2307
|
+
border-radius: 50%;
|
|
2308
|
+
background: #020304;
|
|
2309
|
+
box-shadow: 0 0 0 1px rgba(29,33,39,0.9), 0 0 0.35vh rgba(0,0,0,0.85) inset;
|
|
2310
|
+
}
|
|
2311
|
+
.ipad-sensor-large { width: clamp(5px, 1vw, 17px); aspect-ratio: 1; }
|
|
2312
|
+
.ipad-sensor-small { width: clamp(3px, 0.38vw, 7px); aspect-ratio: 1; }
|
|
2313
|
+
.ipad-sensor-lens {
|
|
2314
|
+
width: clamp(4px, 0.62vw, 11px);
|
|
2315
|
+
aspect-ratio: 1;
|
|
2316
|
+
background: radial-gradient(circle at 38% 35%, #355e8a, #07101d 42%, #010205 75%);
|
|
2317
|
+
box-shadow: 0 0 0 1px #121a26, 0 0 0.45vh rgba(42,110,202,0.45);
|
|
2318
|
+
}
|
|
2319
|
+
.ipad-side-button {
|
|
2320
|
+
position: absolute;
|
|
2321
|
+
top: 7.6%;
|
|
2322
|
+
right: -0.45%;
|
|
2323
|
+
width: 0.45%;
|
|
2324
|
+
height: 7.8%;
|
|
2325
|
+
border-radius: 0 999px 999px 0;
|
|
2326
|
+
background: linear-gradient(90deg, #4c5159, #a9afb7 58%, #555b64);
|
|
2327
|
+
box-shadow: 0 0.3vh 0.7vh rgba(0,0,0,0.5);
|
|
2328
|
+
}
|
|
1567
2329
|
.device.apple-watch {
|
|
1568
2330
|
height: 82%;
|
|
1569
2331
|
padding: 2.2%;
|
|
@@ -1695,9 +2457,49 @@ function createDeviceFrameHtml(profile, screenshotBuffer, imageWidth = profile.w
|
|
|
1695
2457
|
background: #6fe8ff;
|
|
1696
2458
|
box-shadow: 0 0 1.8vh #2dd5ff;
|
|
1697
2459
|
}
|
|
2460
|
+
@media (orientation: portrait) {
|
|
2461
|
+
body.has-copy .promo-copy { top: 4%; right: 7%; left: 7%; align-items: center; text-align: center; }
|
|
2462
|
+
body.has-copy .promo-copy h1 { max-width: 90%; }
|
|
2463
|
+
body.has-copy .promo-copy p { max-width: 86%; }
|
|
2464
|
+
body.has-copy .promo-features { justify-content: center; }
|
|
2465
|
+
body.has-copy .device,
|
|
2466
|
+
body.has-copy .device.android-phone,
|
|
2467
|
+
body.has-copy .device.iphone,
|
|
2468
|
+
body.has-copy .device.tablet,
|
|
2469
|
+
body.has-copy .device.apple-watch {
|
|
2470
|
+
position: absolute;
|
|
2471
|
+
bottom: 3%;
|
|
2472
|
+
left: 50%;
|
|
2473
|
+
width: auto;
|
|
2474
|
+
height: 68%;
|
|
2475
|
+
transform: translateX(-50%);
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
@media (orientation: landscape) {
|
|
2479
|
+
body.has-copy .promo-copy { top: 50%; left: 5%; width: 31%; transform: translateY(-50%); }
|
|
2480
|
+
body.has-copy .promo-copy h1 { font-size: clamp(28px, 4.4vw, 84px); }
|
|
2481
|
+
body.has-copy .promo-copy p { font-size: clamp(16px, 1.7vw, 32px); }
|
|
2482
|
+
body.has-copy .device,
|
|
2483
|
+
body.has-copy .device.tv,
|
|
2484
|
+
body.has-copy .device.apple-tv,
|
|
2485
|
+
body.has-copy .device.laptop,
|
|
2486
|
+
body.has-copy .device.xr,
|
|
2487
|
+
body.has-copy .device.apple-vision-pro {
|
|
2488
|
+
position: absolute;
|
|
2489
|
+
top: 50%;
|
|
2490
|
+
right: 3%;
|
|
2491
|
+
left: auto;
|
|
2492
|
+
width: 60%;
|
|
2493
|
+
height: auto;
|
|
2494
|
+
max-height: 84%;
|
|
2495
|
+
transform: translateY(-50%);
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
1698
2498
|
</style>
|
|
1699
2499
|
</head>
|
|
1700
|
-
<body class="${phonePlatform ? 'phone-frame' : ''}">
|
|
2500
|
+
<body class="${phonePlatform ? 'phone-frame' : ''} ${screenshotCopy ? 'has-copy' : ''}">
|
|
2501
|
+
${createPromotionalDecorations(palette)}
|
|
2502
|
+
${createScreenshotCopyMarkup(screenshotCopy)}
|
|
1701
2503
|
<main class="device ${profile.frameType}">
|
|
1702
2504
|
<div class="screen">${phoneStatusBar}<img src="${screenshotDataUrl}" />${phoneNavigation}</div>
|
|
1703
2505
|
${getFrameAdornment(profile.frameType)}
|
|
@@ -1739,6 +2541,88 @@ function readJsonAssetSource(filePath) {
|
|
|
1739
2541
|
}
|
|
1740
2542
|
}
|
|
1741
2543
|
|
|
2544
|
+
function getScreenshotSourceDetails(screenInfo) {
|
|
2545
|
+
const pageDirectory = path.join(__dirname, 'src', 'pages');
|
|
2546
|
+
const routesPath = path.join(__dirname, 'src', 'js', 'routes.js');
|
|
2547
|
+
const pageName = String(screenInfo?.page?.dataName || '').trim();
|
|
2548
|
+
const pageId = String(screenInfo?.page?.id || '').trim();
|
|
2549
|
+
const escapePattern = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
2550
|
+
const pageFiles = [];
|
|
2551
|
+
|
|
2552
|
+
if (fs.existsSync(pageDirectory) && (pageName || pageId)) {
|
|
2553
|
+
for (const fileName of fs.readdirSync(pageDirectory).filter((name) => /\.(?:f7|html)$/i.test(name))) {
|
|
2554
|
+
const filePath = path.join(pageDirectory, fileName);
|
|
2555
|
+
const source = fs.readFileSync(filePath, 'utf8');
|
|
2556
|
+
const hasPageName = pageName
|
|
2557
|
+
&& new RegExp(`data-name=["']${escapePattern(pageName)}["']`).test(source);
|
|
2558
|
+
const hasPageId = pageId
|
|
2559
|
+
&& new RegExp(`id=["']${escapePattern(pageId)}["']`).test(source);
|
|
2560
|
+
if (hasPageName || hasPageId) {
|
|
2561
|
+
pageFiles.push(path.relative(__dirname, filePath).replace(/\\/g, '/'));
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
const routeCandidates = [];
|
|
2567
|
+
if (fs.existsSync(routesPath)) {
|
|
2568
|
+
const routesSource = fs.readFileSync(routesPath, 'utf8');
|
|
2569
|
+
const staticImports = new Map();
|
|
2570
|
+
const staticImportPattern = /import\s+([A-Za-z_$][\w$]*)\s+from\s+["']\.\.\/pages\/([^"']+)["']/g;
|
|
2571
|
+
let match;
|
|
2572
|
+
while ((match = staticImportPattern.exec(routesSource))) {
|
|
2573
|
+
staticImports.set(match[1], `src/pages/${match[2]}`);
|
|
2574
|
+
}
|
|
2575
|
+
|
|
2576
|
+
const routeBlockPattern = /\{\s*path:\s*["']([^"']+)["']([\s\S]*?)\n\s*\},/g;
|
|
2577
|
+
while ((match = routeBlockPattern.exec(routesSource))) {
|
|
2578
|
+
const routePath = match[1];
|
|
2579
|
+
const block = match[2];
|
|
2580
|
+
const dynamicPage = block.match(/import\(["']\.\.\/pages\/([^"']+)["']\)/)?.[1];
|
|
2581
|
+
const componentName = block.match(/component:\s*([A-Za-z_$][\w$]*)/)?.[1];
|
|
2582
|
+
const pageFile = dynamicPage
|
|
2583
|
+
? `src/pages/${dynamicPage}`
|
|
2584
|
+
: staticImports.get(componentName) || '';
|
|
2585
|
+
const normalizedPageFile = pageFile.replace(/\\/g, '/');
|
|
2586
|
+
const routeMatchesPage = normalizedPageFile
|
|
2587
|
+
&& pageFiles.some((candidate) => candidate.toLowerCase() === normalizedPageFile.toLowerCase());
|
|
2588
|
+
const framework7Path = screenInfo?.route?.framework7?.path;
|
|
2589
|
+
const framework7Url = screenInfo?.route?.framework7?.url;
|
|
2590
|
+
if (routeMatchesPage || routePath === framework7Path || routePath === framework7Url) {
|
|
2591
|
+
routeCandidates.push({ path: routePath, pageFile: normalizedPageFile });
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2596
|
+
return {
|
|
2597
|
+
pageFiles,
|
|
2598
|
+
routesFile: fs.existsSync(routesPath) ? 'src/js/routes.js' : '',
|
|
2599
|
+
routeCandidates,
|
|
2600
|
+
};
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2603
|
+
function saveScreenshotInfo(screenshotFileName, screenInfo) {
|
|
2604
|
+
const screenshotKey = path.parse(screenshotFileName).name;
|
|
2605
|
+
const source = readJsonAssetSource(screenshotInfoPath);
|
|
2606
|
+
const screenshots = source.screenshots && typeof source.screenshots === 'object'
|
|
2607
|
+
? source.screenshots
|
|
2608
|
+
: {};
|
|
2609
|
+
|
|
2610
|
+
screenshots[screenshotKey] = {
|
|
2611
|
+
schemaVersion: 2,
|
|
2612
|
+
fileName: screenshotFileName,
|
|
2613
|
+
capturedAt: new Date().toISOString(),
|
|
2614
|
+
...screenInfo,
|
|
2615
|
+
source: getScreenshotSourceDetails(screenInfo),
|
|
2616
|
+
};
|
|
2617
|
+
|
|
2618
|
+
fs.mkdirSync(path.dirname(screenshotInfoPath), { recursive: true });
|
|
2619
|
+
fs.writeFileSync(
|
|
2620
|
+
screenshotInfoPath,
|
|
2621
|
+
`${JSON.stringify({ version: 2, screenshots }, null, 2)}\n`,
|
|
2622
|
+
);
|
|
2623
|
+
console.log(`Saved screenshot information: ${screenshotInfoPath} (${screenshotKey})`);
|
|
2624
|
+
}
|
|
2625
|
+
|
|
1742
2626
|
function collectProjectUiPhrases() {
|
|
1743
2627
|
const phrases = [];
|
|
1744
2628
|
const pageDirectory = path.join(__dirname, 'src', 'pages');
|
|
@@ -2340,26 +3224,48 @@ async function captureFramedScreenshot(
|
|
|
2340
3224
|
profile,
|
|
2341
3225
|
screenshotFileName,
|
|
2342
3226
|
skipExisting = false,
|
|
3227
|
+
outputRootDirectory = inFrameDirectory,
|
|
2343
3228
|
) {
|
|
2344
3229
|
if (profile.screenshotOnly) return false;
|
|
2345
3230
|
|
|
2346
|
-
const
|
|
2347
|
-
const
|
|
3231
|
+
const outputExtension = profile.outputExtension || path.extname(screenshotFileName).slice(1) || 'png';
|
|
3232
|
+
const outputFileName = `${path.parse(screenshotFileName).name}.${outputExtension}`;
|
|
3233
|
+
const framedProfileDirectory = path.join(outputRootDirectory, profile.outputDirectory);
|
|
3234
|
+
const framedScreenshotPath = path.join(framedProfileDirectory, outputFileName);
|
|
2348
3235
|
|
|
2349
3236
|
if (skipExisting && fs.existsSync(framedScreenshotPath)) {
|
|
2350
3237
|
console.log(`Skipped existing framed screenshot: ${framedScreenshotPath}`);
|
|
2351
3238
|
return false;
|
|
2352
3239
|
}
|
|
2353
3240
|
|
|
2354
|
-
const originalImageMetadata =
|
|
2355
|
-
? await sharp(screenshotBuffer).metadata()
|
|
2356
|
-
: null;
|
|
3241
|
+
const originalImageMetadata = await sharp(screenshotBuffer).metadata();
|
|
2357
3242
|
const hasCustomFrameSize = Boolean(profile.framedWidth && profile.framedHeight);
|
|
2358
3243
|
const frameWidth = profile.framedWidth || profile.width;
|
|
2359
3244
|
const frameHeight = profile.framedHeight || profile.height;
|
|
2360
3245
|
const frameScaleFactor = hasCustomFrameSize ? 1 : profile.deviceScaleFactor;
|
|
2361
3246
|
const phonePlatform = profile.frameType === 'iphone' ? 'ios' : profile.frameType === 'android-phone' ? 'android' : null;
|
|
2362
|
-
const
|
|
3247
|
+
const statusBarPlatform = profile.frameType === 'ipad' ? 'ipad' : phonePlatform;
|
|
3248
|
+
const displayTime = statusBarPlatform ? getFrameDisplayTime(statusBarPlatform) : '';
|
|
3249
|
+
const batteryPercentage = statusBarPlatform === 'android' || statusBarPlatform === 'ipad'
|
|
3250
|
+
? getFrameBatteryPercentage()
|
|
3251
|
+
: null;
|
|
3252
|
+
const statusBarStyle = statusBarPlatform
|
|
3253
|
+
? await getScreenshotBarStyle(screenshotBuffer)
|
|
3254
|
+
: null;
|
|
3255
|
+
const navigationBarStyle = statusBarPlatform
|
|
3256
|
+
? await getScreenshotBarStyle(screenshotBuffer, 'bottom')
|
|
3257
|
+
: null;
|
|
3258
|
+
const screenshotCopy = getScreenshotCopy(screenshotFileName);
|
|
3259
|
+
const promotionalPalette = await getPromotionalPalette(screenshotBuffer, screenshotFileName);
|
|
3260
|
+
const screenshotKey = path.parse(screenshotFileName).name;
|
|
3261
|
+
|
|
3262
|
+
if (!screenshotCopy && !missingScreenshotCopyWarnings.has(screenshotKey)) {
|
|
3263
|
+
missingScreenshotCopyWarnings.add(screenshotKey);
|
|
3264
|
+
console.warn(
|
|
3265
|
+
`No promotional copy configured for screenshot ${screenshotKey}; `
|
|
3266
|
+
+ `add it to ${screenshotCopyPath}.`,
|
|
3267
|
+
);
|
|
3268
|
+
}
|
|
2363
3269
|
|
|
2364
3270
|
await client.send('Emulation.setDeviceMetricsOverride', {
|
|
2365
3271
|
width: frameWidth,
|
|
@@ -2379,8 +3285,15 @@ async function captureFramedScreenshot(
|
|
|
2379
3285
|
? createStoreDeviceFrameHtml(
|
|
2380
3286
|
profile, screenshotBuffer, originalImageMetadata.width, originalImageMetadata.height,
|
|
2381
3287
|
frameWidth, frameHeight, originalImageMetadata.format, displayTime,
|
|
3288
|
+
batteryPercentage, statusBarStyle, navigationBarStyle,
|
|
3289
|
+
screenshotCopy, promotionalPalette,
|
|
2382
3290
|
)
|
|
2383
|
-
: createDeviceFrameHtml(
|
|
3291
|
+
: createDeviceFrameHtml(
|
|
3292
|
+
profile, screenshotBuffer, originalImageMetadata.width, originalImageMetadata.height,
|
|
3293
|
+
originalImageMetadata.format,
|
|
3294
|
+
displayTime, batteryPercentage, statusBarStyle, navigationBarStyle,
|
|
3295
|
+
screenshotCopy, promotionalPalette,
|
|
3296
|
+
),
|
|
2384
3297
|
});
|
|
2385
3298
|
await client.send('Runtime.evaluate', {
|
|
2386
3299
|
expression: `new Promise((resolve) => {
|
|
@@ -2502,25 +3415,10 @@ async function captureScreenshotSet(
|
|
|
2502
3415
|
selectedProfiles,
|
|
2503
3416
|
fileNameInput,
|
|
2504
3417
|
) {
|
|
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
3418
|
console.log('Capturing the current rendered page and resolved native data...');
|
|
2522
3419
|
const snapshot = await capturePageSnapshot(deviceClient);
|
|
2523
3420
|
const screenshotFileName = getScreenshotFileName(fileNameInput, selectedProfiles);
|
|
3421
|
+
saveScreenshotInfo(screenshotFileName, snapshot.screenInfo);
|
|
2524
3422
|
|
|
2525
3423
|
for (const profile of selectedProfiles) {
|
|
2526
3424
|
await captureProfile(replayClient, snapshot, profile, screenshotFileName);
|
|
@@ -2529,11 +3427,23 @@ async function captureScreenshotSet(
|
|
|
2529
3427
|
console.log(`Responsive screenshot set completed: ${screenshotFileName}`);
|
|
2530
3428
|
}
|
|
2531
3429
|
|
|
2532
|
-
async function frameExistingScreenshots(
|
|
3430
|
+
async function frameExistingScreenshots(
|
|
3431
|
+
selectedProfiles,
|
|
3432
|
+
overwriteExisting = false,
|
|
3433
|
+
screenshotNames = '',
|
|
3434
|
+
outputRootDirectory = inFrameDirectory,
|
|
3435
|
+
) {
|
|
2533
3436
|
let replayBrowser;
|
|
2534
3437
|
let replayClient;
|
|
2535
3438
|
let framedCount = 0;
|
|
2536
3439
|
let failedCount = 0;
|
|
3440
|
+
const selectedScreenshotNames = new Set(
|
|
3441
|
+
screenshotNames
|
|
3442
|
+
.split(',')
|
|
3443
|
+
.map((name) => path.parse(name.trim()).name)
|
|
3444
|
+
.filter(Boolean),
|
|
3445
|
+
);
|
|
3446
|
+
const foundScreenshotNames = new Set();
|
|
2537
3447
|
|
|
2538
3448
|
try {
|
|
2539
3449
|
replayBrowser = await launchReplayBrowser();
|
|
@@ -2547,12 +3457,17 @@ async function frameExistingScreenshots(selectedProfiles, overwriteExisting = fa
|
|
|
2547
3457
|
}
|
|
2548
3458
|
|
|
2549
3459
|
const screenshotFiles = fs.readdirSync(profileDirectory, { withFileTypes: true })
|
|
2550
|
-
.filter((entry) => entry.isFile() && /\.png$/i.test(entry.name))
|
|
3460
|
+
.filter((entry) => entry.isFile() && /\.(?:png|jpe?g|webp)$/i.test(entry.name))
|
|
2551
3461
|
.map((entry) => entry.name)
|
|
3462
|
+
.filter((fileName) => (
|
|
3463
|
+
!selectedScreenshotNames.size
|
|
3464
|
+
|| selectedScreenshotNames.has(path.parse(fileName).name)
|
|
3465
|
+
))
|
|
2552
3466
|
.sort((first, second) => first.localeCompare(second, undefined, { numeric: true }));
|
|
2553
3467
|
|
|
2554
3468
|
for (const screenshotFileName of screenshotFiles) {
|
|
2555
3469
|
const screenshotPath = path.join(profileDirectory, screenshotFileName);
|
|
3470
|
+
foundScreenshotNames.add(path.parse(screenshotFileName).name);
|
|
2556
3471
|
|
|
2557
3472
|
try {
|
|
2558
3473
|
const wasCreated = await captureFramedScreenshot(
|
|
@@ -2561,6 +3476,7 @@ async function frameExistingScreenshots(selectedProfiles, overwriteExisting = fa
|
|
|
2561
3476
|
profile,
|
|
2562
3477
|
screenshotFileName,
|
|
2563
3478
|
!overwriteExisting,
|
|
3479
|
+
outputRootDirectory,
|
|
2564
3480
|
);
|
|
2565
3481
|
if (wasCreated) framedCount += 1;
|
|
2566
3482
|
} catch (error) {
|
|
@@ -2581,6 +3497,14 @@ async function frameExistingScreenshots(selectedProfiles, overwriteExisting = fa
|
|
|
2581
3497
|
|
|
2582
3498
|
console.log(`Existing screenshot framing completed: ${framedCount} created.`);
|
|
2583
3499
|
|
|
3500
|
+
const missingScreenshotNames = [...selectedScreenshotNames]
|
|
3501
|
+
.filter((name) => !foundScreenshotNames.has(name));
|
|
3502
|
+
if (missingScreenshotNames.length) {
|
|
3503
|
+
throw new Error(
|
|
3504
|
+
`Requested screenshot(s) were not found for the selected profiles: ${missingScreenshotNames.join(', ')}.`,
|
|
3505
|
+
);
|
|
3506
|
+
}
|
|
3507
|
+
|
|
2584
3508
|
if (failedCount) {
|
|
2585
3509
|
throw new Error(`${failedCount} existing screenshot(s) could not be framed.`);
|
|
2586
3510
|
}
|
|
@@ -2769,6 +3693,7 @@ async function getInteractiveOptions(options) {
|
|
|
2769
3693
|
|| options.dryRun
|
|
2770
3694
|
|| options.amazonAssets
|
|
2771
3695
|
|| options.samsungAssets
|
|
3696
|
+
|| options.refreshInfo
|
|
2772
3697
|
|| options.vivoExisting
|
|
2773
3698
|
|| options.oppoExisting
|
|
2774
3699
|
) {
|
|
@@ -2815,6 +3740,10 @@ async function main() {
|
|
|
2815
3740
|
|
|
2816
3741
|
options = await getInteractiveOptions(options);
|
|
2817
3742
|
|
|
3743
|
+
if (!options.dryRun && !options.refreshInfo) {
|
|
3744
|
+
reconcileScreenshotRecords();
|
|
3745
|
+
}
|
|
3746
|
+
|
|
2818
3747
|
if (options.amazonAssets) {
|
|
2819
3748
|
let amazonBrowser;
|
|
2820
3749
|
let amazonClient;
|
|
@@ -2865,16 +3794,21 @@ async function main() {
|
|
|
2865
3794
|
|
|
2866
3795
|
const selectedProfiles = selectProfiles(options.profiles);
|
|
2867
3796
|
|
|
2868
|
-
if (!options.dryRun) {
|
|
3797
|
+
if (!options.dryRun && !options.refreshInfo) {
|
|
2869
3798
|
await prepareFrameSelection(options);
|
|
2870
3799
|
}
|
|
2871
3800
|
|
|
2872
3801
|
if (options.frameExisting || options.reframeExisting) {
|
|
2873
|
-
await frameExistingScreenshots(
|
|
3802
|
+
await frameExistingScreenshots(
|
|
3803
|
+
selectedProfiles,
|
|
3804
|
+
options.reframeExisting,
|
|
3805
|
+
options.screenshots,
|
|
3806
|
+
options.reframeExisting ? uploadDirectory : inFrameDirectory,
|
|
3807
|
+
);
|
|
2874
3808
|
return;
|
|
2875
3809
|
}
|
|
2876
3810
|
|
|
2877
|
-
if (!options.dryRun) {
|
|
3811
|
+
if (!options.dryRun && !options.refreshInfo) {
|
|
2878
3812
|
await createMissingStoreAssets();
|
|
2879
3813
|
}
|
|
2880
3814
|
|
|
@@ -2916,6 +3850,20 @@ async function main() {
|
|
|
2916
3850
|
return;
|
|
2917
3851
|
}
|
|
2918
3852
|
|
|
3853
|
+
if (options.refreshInfo) {
|
|
3854
|
+
const screenshotKey = sanitizeName(
|
|
3855
|
+
options.refreshInfo.trim().replace(/\.(png|jpe?g)$/i, ''),
|
|
3856
|
+
'',
|
|
3857
|
+
);
|
|
3858
|
+
if (!screenshotKey) {
|
|
3859
|
+
throw new Error('Provide a screenshot name after --refresh-info, for example: --refresh-info 1');
|
|
3860
|
+
}
|
|
3861
|
+
const snapshot = await capturePageSnapshot(deviceClient);
|
|
3862
|
+
saveScreenshotInfo(`${screenshotKey}.png`, snapshot.screenInfo);
|
|
3863
|
+
console.log(`Screenshot information refreshed without creating images: ${screenshotKey}`);
|
|
3864
|
+
return;
|
|
3865
|
+
}
|
|
3866
|
+
|
|
2919
3867
|
fs.mkdirSync(screenshotDirectory, { recursive: true });
|
|
2920
3868
|
replayBrowser = await launchReplayBrowser();
|
|
2921
3869
|
replayClient = await createReplayPage(replayBrowser);
|