App Store Screenshot Resizer

Resize and format screenshots for App Store Connect with custom backgrounds and device presets

Size

20.4 KB

Version

1.0.1

Created

Mar 2, 2026

Updated

about 1 month ago

1// ==UserScript==
2// @name		App Store Screenshot Resizer
3// @description		Resize and format screenshots for App Store Connect with custom backgrounds and device presets
4// @version		1.0.1
5// @match		https://*.appstoreconnect.apple.com/*
6// @icon		https://appstoreconnect.apple.com/favicon.ico
7// ==/UserScript==
8(function() {
9    'use strict';
10
11    console.log('App Store Screenshot Resizer initialized');
12
13    // Device size presets with all supported dimensions
14    const DEVICE_PRESETS = {
15        'iPhone 6.5", 6.7" or 6.9"': [
16            { width: 1260, height: 2736, label: '1260 × 2736px (Portrait)' },
17            { width: 2736, height: 1260, label: '2736 × 1260px (Landscape)' },
18            { width: 1320, height: 2868, label: '1320 × 2868px (Portrait)' },
19            { width: 2868, height: 1320, label: '2868 × 1320px (Landscape)' },
20            { width: 1290, height: 2796, label: '1290 × 2796px (Portrait)' },
21            { width: 2796, height: 1290, label: '2796 × 1290px (Landscape)' }
22        ],
23        'iPhone 6.5" Display': [
24            { width: 1242, height: 2688, label: '1242 × 2688px (Portrait)' },
25            { width: 2688, height: 1242, label: '2688 × 1242px (Landscape)' },
26            { width: 1284, height: 2778, label: '1284 × 2778px (Portrait)' },
27            { width: 2778, height: 1284, label: '2778 × 1284px (Landscape)' }
28        ],
29        'iPhone 6.1" or 6.3"': [
30            { width: 1206, height: 2622, label: '1206 × 2622px (Portrait)' },
31            { width: 2622, height: 1206, label: '2622 × 1206px (Landscape)' },
32            { width: 1179, height: 2556, label: '1179 × 2556px (Portrait)' },
33            { width: 2556, height: 1179, label: '2556 × 1179px (Landscape)' }
34        ],
35        'iPhone 5.4", 5.8" or 6.1"': [
36            { width: 1125, height: 2436, label: '1125 × 2436px (Portrait)' },
37            { width: 2436, height: 1125, label: '2436 × 1125px (Landscape)' },
38            { width: 1080, height: 2340, label: '1080 × 2340px (Portrait)' },
39            { width: 2340, height: 1080, label: '2340 × 1080px (Landscape)' },
40            { width: 2532, height: 1170, label: '2532 × 1170px (Landscape)' },
41            { width: 1170, height: 2532, label: '1170 × 2532px (Portrait)' }
42        ],
43        'iPhone 5.5"': [
44            { width: 1242, height: 2208, label: '1242 × 2208px (Portrait)' },
45            { width: 2208, height: 1242, label: '2208 × 1242px (Landscape)' }
46        ],
47        'iPhone 4.7"': [
48            { width: 750, height: 1334, label: '750 × 1334px (Portrait)' },
49            { width: 1334, height: 750, label: '1334 × 750px (Landscape)' }
50        ],
51        'iPhone 4" or 4.7"': [
52            { width: 640, height: 1096, label: '640 × 1096px (Portrait)' },
53            { width: 640, height: 1136, label: '640 × 1136px (Portrait)' },
54            { width: 1136, height: 600, label: '1136 × 600px (Landscape)' },
55            { width: 1136, height: 640, label: '1136 × 640px (Landscape)' }
56        ],
57        'iPhone 3.5"': [
58            { width: 640, height: 920, label: '640 × 920px (Portrait)' },
59            { width: 640, height: 960, label: '640 × 960px (Portrait)' },
60            { width: 960, height: 600, label: '960 × 600px (Landscape)' },
61            { width: 960, height: 640, label: '960 × 640px (Landscape)' }
62        ]
63    };
64
65    // Add custom styles
66    TM_addStyle(`
67        #screenshot-resizer-panel {
68            position: fixed;
69            top: 80px;
70            right: 20px;
71            width: 420px;
72            max-height: 85vh;
73            background: #ffffff;
74            border: 1px solid #d1d1d6;
75            border-radius: 12px;
76            box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
77            z-index: 999999;
78            font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", sans-serif;
79            overflow: hidden;
80            display: flex;
81            flex-direction: column;
82        }
83
84        #screenshot-resizer-header {
85            background: linear-gradient(135deg, #007aff 0%, #0051d5 100%);
86            color: white;
87            padding: 16px 20px;
88            display: flex;
89            justify-content: space-between;
90            align-items: center;
91            cursor: move;
92        }
93
94        #screenshot-resizer-header h3 {
95            margin: 0;
96            font-size: 17px;
97            font-weight: 600;
98        }
99
100        #screenshot-resizer-close {
101            background: rgba(255, 255, 255, 0.2);
102            border: none;
103            color: white;
104            width: 28px;
105            height: 28px;
106            border-radius: 50%;
107            cursor: pointer;
108            font-size: 18px;
109            display: flex;
110            align-items: center;
111            justify-content: center;
112            transition: background 0.2s;
113        }
114
115        #screenshot-resizer-close:hover {
116            background: rgba(255, 255, 255, 0.3);
117        }
118
119        #screenshot-resizer-content {
120            padding: 20px;
121            overflow-y: auto;
122            flex: 1;
123        }
124
125        .resizer-section {
126            margin-bottom: 24px;
127        }
128
129        .resizer-section-title {
130            font-size: 14px;
131            font-weight: 600;
132            color: #1d1d1f;
133            margin-bottom: 12px;
134            display: block;
135        }
136
137        .device-preset-group {
138            margin-bottom: 16px;
139        }
140
141        .device-preset-label {
142            font-size: 13px;
143            font-weight: 500;
144            color: #6e6e73;
145            margin-bottom: 8px;
146            display: block;
147        }
148
149        .device-preset-buttons {
150            display: flex;
151            flex-wrap: wrap;
152            gap: 8px;
153        }
154
155        .device-preset-btn {
156            background: #f5f5f7;
157            border: 1px solid #d1d1d6;
158            border-radius: 8px;
159            padding: 8px 12px;
160            font-size: 12px;
161            color: #1d1d1f;
162            cursor: pointer;
163            transition: all 0.2s;
164            flex: 1;
165            min-width: 120px;
166            text-align: center;
167        }
168
169        .device-preset-btn:hover {
170            background: #e8e8ed;
171            border-color: #007aff;
172        }
173
174        .device-preset-btn.active {
175            background: #007aff;
176            color: white;
177            border-color: #007aff;
178        }
179
180        .color-picker-container {
181            display: flex;
182            align-items: center;
183            gap: 12px;
184        }
185
186        #background-color-picker {
187            width: 60px;
188            height: 40px;
189            border: 1px solid #d1d1d6;
190            border-radius: 8px;
191            cursor: pointer;
192        }
193
194        #background-color-value {
195            flex: 1;
196            padding: 10px 12px;
197            border: 1px solid #d1d1d6;
198            border-radius: 8px;
199            font-size: 13px;
200            font-family: 'SF Mono', Monaco, monospace;
201        }
202
203        #drop-zone {
204            border: 2px dashed #d1d1d6;
205            border-radius: 12px;
206            padding: 40px 20px;
207            text-align: center;
208            background: #f5f5f7;
209            cursor: pointer;
210            transition: all 0.3s;
211            margin-bottom: 16px;
212        }
213
214        #drop-zone:hover, #drop-zone.drag-over {
215            border-color: #007aff;
216            background: #e8f4ff;
217        }
218
219        #drop-zone-icon {
220            font-size: 48px;
221            margin-bottom: 12px;
222            color: #86868b;
223        }
224
225        #drop-zone-text {
226            font-size: 15px;
227            color: #1d1d1f;
228            font-weight: 500;
229            margin-bottom: 4px;
230        }
231
232        #drop-zone-subtext {
233            font-size: 13px;
234            color: #86868b;
235        }
236
237        #file-input {
238            display: none;
239        }
240
241        #process-btn {
242            width: 100%;
243            background: #007aff;
244            color: white;
245            border: none;
246            border-radius: 10px;
247            padding: 14px;
248            font-size: 15px;
249            font-weight: 600;
250            cursor: pointer;
251            transition: background 0.2s;
252        }
253
254        #process-btn:hover:not(:disabled) {
255            background: #0051d5;
256        }
257
258        #process-btn:disabled {
259            background: #d1d1d6;
260            cursor: not-allowed;
261        }
262
263        #preview-container {
264            margin-top: 20px;
265            display: none;
266        }
267
268        #preview-image {
269            width: 100%;
270            border-radius: 8px;
271            border: 1px solid #d1d1d6;
272        }
273
274        #download-btn {
275            width: 100%;
276            background: #34c759;
277            color: white;
278            border: none;
279            border-radius: 10px;
280            padding: 14px;
281            font-size: 15px;
282            font-weight: 600;
283            cursor: pointer;
284            margin-top: 12px;
285            transition: background 0.2s;
286        }
287
288        #download-btn:hover {
289            background: #2fb350;
290        }
291
292        .toggle-btn {
293            position: fixed;
294            bottom: 30px;
295            right: 30px;
296            width: 60px;
297            height: 60px;
298            background: linear-gradient(135deg, #007aff 0%, #0051d5 100%);
299            color: white;
300            border: none;
301            border-radius: 50%;
302            font-size: 28px;
303            cursor: pointer;
304            box-shadow: 0 4px 20px rgba(0, 122, 255, 0.4);
305            z-index: 999998;
306            transition: transform 0.2s;
307            display: flex;
308            align-items: center;
309            justify-content: center;
310        }
311
312        .toggle-btn:hover {
313            transform: scale(1.1);
314        }
315
316        .status-message {
317            padding: 12px;
318            border-radius: 8px;
319            margin-top: 12px;
320            font-size: 13px;
321            text-align: center;
322        }
323
324        .status-success {
325            background: #d1f4e0;
326            color: #1e7e34;
327        }
328
329        .status-error {
330            background: #ffe5e5;
331            color: #d32f2f;
332        }
333    `);
334
335    let selectedPreset = null;
336    let uploadedImage = null;
337    let backgroundColor = '#ffffff';
338
339    function createUI() {
340        // Create toggle button
341        const toggleBtn = document.createElement('button');
342        toggleBtn.className = 'toggle-btn';
343        toggleBtn.innerHTML = '📱';
344        toggleBtn.title = 'Screenshot Resizer';
345        document.body.appendChild(toggleBtn);
346
347        // Create main panel
348        const panel = document.createElement('div');
349        panel.id = 'screenshot-resizer-panel';
350        panel.style.display = 'none';
351        panel.innerHTML = `
352            <div id="screenshot-resizer-header">
353                <h3>📱 Screenshot Resizer</h3>
354                <button id="screenshot-resizer-close">×</button>
355            </div>
356            <div id="screenshot-resizer-content">
357                <div class="resizer-section">
358                    <label class="resizer-section-title">Device Size Presets</label>
359                    <div id="device-presets-container"></div>
360                </div>
361
362                <div class="resizer-section">
363                    <label class="resizer-section-title">Background Color</label>
364                    <div class="color-picker-container">
365                        <input type="color" id="background-color-picker" value="#ffffff">
366                        <input type="text" id="background-color-value" value="#ffffff" placeholder="#ffffff">
367                    </div>
368                </div>
369
370                <div class="resizer-section">
371                    <label class="resizer-section-title">Upload Screenshot</label>
372                    <div id="drop-zone">
373                        <div id="drop-zone-icon">📸</div>
374                        <div id="drop-zone-text">Drag & Drop or Click to Upload</div>
375                        <div id="drop-zone-subtext">Supports PNG, JPG, JPEG</div>
376                    </div>
377                    <input type="file" id="file-input" accept="image/png,image/jpeg,image/jpg">
378                </div>
379
380                <button id="process-btn" disabled>Process Screenshot</button>
381
382                <div id="preview-container">
383                    <label class="resizer-section-title">Preview</label>
384                    <img id="preview-image" alt="Preview">
385                    <button id="download-btn">Download Screenshot</button>
386                </div>
387
388                <div id="status-container"></div>
389            </div>
390        `;
391        document.body.appendChild(panel);
392
393        // Populate device presets
394        const presetsContainer = document.getElementById('device-presets-container');
395        Object.keys(DEVICE_PRESETS).forEach(deviceName => {
396            const group = document.createElement('div');
397            group.className = 'device-preset-group';
398            
399            const label = document.createElement('label');
400            label.className = 'device-preset-label';
401            label.textContent = deviceName;
402            group.appendChild(label);
403
404            const buttonsContainer = document.createElement('div');
405            buttonsContainer.className = 'device-preset-buttons';
406
407            DEVICE_PRESETS[deviceName].forEach(preset => {
408                const btn = document.createElement('button');
409                btn.className = 'device-preset-btn';
410                btn.textContent = preset.label;
411                btn.onclick = () => selectPreset(btn, preset);
412                buttonsContainer.appendChild(btn);
413            });
414
415            group.appendChild(buttonsContainer);
416            presetsContainer.appendChild(group);
417        });
418
419        // Event listeners
420        toggleBtn.onclick = () => {
421            panel.style.display = panel.style.display === 'none' ? 'flex' : 'none';
422        };
423
424        document.getElementById('screenshot-resizer-close').onclick = () => {
425            panel.style.display = 'none';
426        };
427
428        const colorPicker = document.getElementById('background-color-picker');
429        const colorValue = document.getElementById('background-color-value');
430
431        colorPicker.oninput = (e) => {
432            backgroundColor = e.target.value;
433            colorValue.value = backgroundColor;
434        };
435
436        colorValue.oninput = (e) => {
437            const value = e.target.value;
438            if (/^#[0-9A-F]{6}$/i.test(value)) {
439                backgroundColor = value;
440                colorPicker.value = value;
441            }
442        };
443
444        const dropZone = document.getElementById('drop-zone');
445        const fileInput = document.getElementById('file-input');
446
447        dropZone.onclick = () => fileInput.click();
448
449        dropZone.ondragover = (e) => {
450            e.preventDefault();
451            dropZone.classList.add('drag-over');
452        };
453
454        dropZone.ondragleave = () => {
455            dropZone.classList.remove('drag-over');
456        };
457
458        dropZone.ondrop = (e) => {
459            e.preventDefault();
460            dropZone.classList.remove('drag-over');
461            const files = e.dataTransfer.files;
462            if (files.length > 0) {
463                handleFileUpload(files[0]);
464            }
465        };
466
467        fileInput.onchange = (e) => {
468            if (e.target.files.length > 0) {
469                handleFileUpload(e.target.files[0]);
470            }
471        };
472
473        document.getElementById('process-btn').onclick = processScreenshot;
474        document.getElementById('download-btn').onclick = downloadScreenshot;
475
476        // Make panel draggable
477        makeDraggable(panel);
478
479        console.log('Screenshot Resizer UI created successfully');
480    }
481
482    function selectPreset(button, preset) {
483        // Remove active class from all buttons
484        document.querySelectorAll('.device-preset-btn').forEach(btn => {
485            btn.classList.remove('active');
486        });
487
488        // Add active class to selected button
489        button.classList.add('active');
490        selectedPreset = preset;
491
492        // Enable process button if image is uploaded
493        updateProcessButton();
494        
495        console.log('Selected preset:', preset);
496    }
497
498    function handleFileUpload(file) {
499        if (!file.type.match('image/(png|jpeg|jpg)')) {
500            showStatus('Please upload a PNG or JPG image', 'error');
501            return;
502        }
503
504        const reader = new FileReader();
505        reader.onload = (e) => {
506            const img = new Image();
507            img.onload = () => {
508                uploadedImage = img;
509                document.getElementById('drop-zone-text').textContent = file.name;
510                document.getElementById('drop-zone-icon').textContent = '✓';
511                updateProcessButton();
512                console.log('Image uploaded:', file.name, 'Size:', img.width, 'x', img.height);
513            };
514            img.src = e.target.result;
515        };
516        reader.readAsDataURL(file);
517    }
518
519    function updateProcessButton() {
520        const processBtn = document.getElementById('process-btn');
521        processBtn.disabled = !(selectedPreset && uploadedImage);
522    }
523
524    function processScreenshot() {
525        if (!selectedPreset || !uploadedImage) {
526            showStatus('Please select a device preset and upload an image', 'error');
527            return;
528        }
529
530        console.log('Processing screenshot with preset:', selectedPreset);
531
532        const canvas = document.createElement('canvas');
533        const ctx = canvas.getContext('2d');
534
535        // Set canvas size to target dimensions
536        canvas.width = selectedPreset.width;
537        canvas.height = selectedPreset.height;
538
539        // Fill background
540        ctx.fillStyle = backgroundColor;
541        ctx.fillRect(0, 0, canvas.width, canvas.height);
542
543        // Calculate scaling to fit image within canvas while maintaining aspect ratio
544        const scale = Math.min(
545            canvas.width / uploadedImage.width,
546            canvas.height / uploadedImage.height
547        );
548
549        const scaledWidth = uploadedImage.width * scale;
550        const scaledHeight = uploadedImage.height * scale;
551
552        // Center the image
553        const x = (canvas.width - scaledWidth) / 2;
554        const y = (canvas.height - scaledHeight) / 2;
555
556        // Draw image
557        ctx.drawImage(uploadedImage, x, y, scaledWidth, scaledHeight);
558
559        // Show preview
560        const previewContainer = document.getElementById('preview-container');
561        const previewImage = document.getElementById('preview-image');
562        previewImage.src = canvas.toDataURL('image/png');
563        previewContainer.style.display = 'block';
564
565        // Store canvas for download
566        previewImage.dataset.canvas = canvas.toDataURL('image/png');
567
568        showStatus('Screenshot processed successfully!', 'success');
569        console.log('Screenshot processed successfully');
570    }
571
572    function downloadScreenshot() {
573        const previewImage = document.getElementById('preview-image');
574        const dataUrl = previewImage.dataset.canvas;
575
576        if (!dataUrl) {
577            showStatus('No processed screenshot to download', 'error');
578            return;
579        }
580
581        const link = document.createElement('a');
582        const filename = `screenshot_${selectedPreset.width}x${selectedPreset.height}_${Date.now()}.png`;
583        link.download = filename;
584        link.href = dataUrl;
585        link.click();
586
587        showStatus('Screenshot downloaded!', 'success');
588        console.log('Screenshot downloaded:', filename);
589    }
590
591    function showStatus(message, type) {
592        const container = document.getElementById('status-container');
593        container.innerHTML = `<div class="status-message status-${type}">${message}</div>`;
594        
595        setTimeout(() => {
596            container.innerHTML = '';
597        }, 3000);
598    }
599
600    function makeDraggable(element) {
601        const header = element.querySelector('#screenshot-resizer-header');
602        let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
603
604        header.onmousedown = dragMouseDown;
605
606        function dragMouseDown(e) {
607            e.preventDefault();
608            pos3 = e.clientX;
609            pos4 = e.clientY;
610            document.onmouseup = closeDragElement;
611            document.onmousemove = elementDrag;
612        }
613
614        function elementDrag(e) {
615            e.preventDefault();
616            pos1 = pos3 - e.clientX;
617            pos2 = pos4 - e.clientY;
618            pos3 = e.clientX;
619            pos4 = e.clientY;
620            element.style.top = (element.offsetTop - pos2) + "px";
621            element.style.right = "auto";
622            element.style.left = (element.offsetLeft - pos1) + "px";
623        }
624
625        function closeDragElement() {
626            document.onmouseup = null;
627            document.onmousemove = null;
628        }
629    }
630
631    // Initialize when page is ready
632    TM_runBody(() => {
633        console.log('Initializing Screenshot Resizer...');
634        createUI();
635    });
636
637})();