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

19 days 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})();