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