Select and delete all Google Photos at once with a floating control panel
Size
23.8 KB
Version
1.1.13
Created
Mar 9, 2026
Updated
12 days ago
1// ==UserScript==
2// @name Google Photos Bulk Delete Tool
3// @description Select and delete all Google Photos at once with a floating control panel
4// @version 1.1.13
5// @match https://*.photos.google.com/*
6// @icon https://photos.google.com/favicon.ico
7// @grant GM.getValue
8// @grant GM.setValue
9// ==/UserScript==
10(function() {
11 'use strict';
12
13 console.log('Google Photos Bulk Delete Tool initialized');
14
15 // Debounce function to prevent excessive calls
16 function debounce(func, wait) {
17 let timeout;
18 return function executedFunction(...args) {
19 const later = () => {
20 clearTimeout(timeout);
21 func(...args);
22 };
23 clearTimeout(timeout);
24 timeout = setTimeout(later, wait);
25 };
26 }
27
28 // Create floating control panel
29 function createControlPanel() {
30 const panel = document.createElement('div');
31 panel.id = 'bulk-delete-panel';
32 panel.innerHTML = `
33 <div class="panel-header">
34 <h3>Bulk Delete Photos</h3>
35 <button id="close-panel" title="Close">×</button>
36 </div>
37 <div class="panel-content">
38 <div class="stats">
39 <div class="stat-item">
40 <span class="stat-label">Photos on page:</span>
41 <span id="photo-count">0</span>
42 </div>
43 <div class="stat-item">
44 <span class="stat-label">Selected:</span>
45 <span id="selected-count">0</span>
46 </div>
47 </div>
48 <div class="button-group">
49 <button id="select-all-btn" class="action-btn primary">
50 Select All Visible
51 </button>
52 <button id="select-and-scroll-btn" class="action-btn primary">
53 Select All + Auto-scroll
54 </button>
55 <button id="deselect-all-btn" class="action-btn secondary">
56 Deselect All
57 </button>
58 <button id="delete-all-auto-btn" class="action-btn danger">
59 🔥 Delete All (Auto Mode)
60 </button>
61 <button id="delete-selected-btn" class="action-btn danger">
62 Delete Selected
63 </button>
64 </div>
65 <div id="status-message"></div>
66 <div class="progress-container" id="progress-container" style="display: none;">
67 <div class="progress-bar" id="progress-bar"></div>
68 <div class="progress-text" id="progress-text">0%</div>
69 </div>
70 </div>
71 `;
72
73 // Add styles
74 const style = document.createElement('style');
75 style.textContent = `
76 #bulk-delete-panel {
77 position: fixed;
78 top: 20px;
79 right: 20px;
80 width: 320px;
81 background: #ffffff;
82 border-radius: 12px;
83 box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
84 z-index: 999999;
85 font-family: 'Google Sans', Roboto, Arial, sans-serif;
86 overflow: hidden;
87 }
88
89 #bulk-delete-panel .panel-header {
90 background: linear-gradient(135deg, #1a73e8 0%, #174ea6 100%);
91 color: white;
92 padding: 16px 20px;
93 display: flex;
94 justify-content: space-between;
95 align-items: center;
96 cursor: move;
97 }
98
99 #bulk-delete-panel .panel-header h3 {
100 margin: 0;
101 font-size: 16px;
102 font-weight: 500;
103 }
104
105 #bulk-delete-panel #close-panel {
106 background: none;
107 border: none;
108 color: white;
109 font-size: 28px;
110 cursor: pointer;
111 padding: 0;
112 width: 28px;
113 height: 28px;
114 line-height: 28px;
115 text-align: center;
116 border-radius: 50%;
117 transition: background 0.2s;
118 }
119
120 #bulk-delete-panel #close-panel:hover {
121 background: rgba(255, 255, 255, 0.2);
122 }
123
124 #bulk-delete-panel .panel-content {
125 padding: 20px;
126 }
127
128 #bulk-delete-panel .stats {
129 background: #f8f9fa;
130 border-radius: 8px;
131 padding: 12px;
132 margin-bottom: 16px;
133 }
134
135 #bulk-delete-panel .stat-item {
136 display: flex;
137 justify-content: space-between;
138 margin-bottom: 8px;
139 font-size: 14px;
140 }
141
142 #bulk-delete-panel .stat-item:last-child {
143 margin-bottom: 0;
144 }
145
146 #bulk-delete-panel .stat-label {
147 color: #5f6368;
148 }
149
150 #bulk-delete-panel .stat-item span:last-child {
151 font-weight: 600;
152 color: #1a73e8;
153 }
154
155 #bulk-delete-panel .button-group {
156 display: flex;
157 flex-direction: column;
158 gap: 10px;
159 }
160
161 #bulk-delete-panel .action-btn {
162 padding: 12px 16px;
163 border: none;
164 border-radius: 8px;
165 font-size: 14px;
166 font-weight: 500;
167 cursor: pointer;
168 transition: all 0.2s;
169 text-align: center;
170 }
171
172 #bulk-delete-panel .action-btn:disabled {
173 opacity: 0.5;
174 cursor: not-allowed;
175 }
176
177 #bulk-delete-panel .action-btn.primary {
178 background: #1a73e8;
179 color: white;
180 }
181
182 #bulk-delete-panel .action-btn.primary:hover:not(:disabled) {
183 background: #1557b0;
184 box-shadow: 0 2px 8px rgba(26, 115, 232, 0.4);
185 }
186
187 #bulk-delete-panel .action-btn.secondary {
188 background: #f1f3f4;
189 color: #5f6368;
190 }
191
192 #bulk-delete-panel .action-btn.secondary:hover:not(:disabled) {
193 background: #e8eaed;
194 }
195
196 #bulk-delete-panel .action-btn.danger {
197 background: #d93025;
198 color: white;
199 }
200
201 #bulk-delete-panel .action-btn.danger:hover:not(:disabled) {
202 background: #b31412;
203 box-shadow: 0 2px 8px rgba(217, 48, 37, 0.4);
204 }
205
206 #bulk-delete-panel #status-message {
207 margin-top: 12px;
208 padding: 10px;
209 border-radius: 6px;
210 font-size: 13px;
211 text-align: center;
212 display: none;
213 }
214
215 #bulk-delete-panel #status-message.success {
216 background: #e6f4ea;
217 color: #137333;
218 display: block;
219 }
220
221 #bulk-delete-panel #status-message.error {
222 background: #fce8e6;
223 color: #c5221f;
224 display: block;
225 }
226
227 #bulk-delete-panel #status-message.info {
228 background: #e8f0fe;
229 color: #1967d2;
230 display: block;
231 }
232
233 #bulk-delete-panel .progress-container {
234 margin-top: 12px;
235 background: #f1f3f4;
236 border-radius: 8px;
237 height: 32px;
238 position: relative;
239 overflow: hidden;
240 }
241
242 #bulk-delete-panel .progress-bar {
243 height: 100%;
244 background: linear-gradient(90deg, #1a73e8 0%, #4285f4 100%);
245 transition: width 0.3s ease;
246 width: 0%;
247 }
248
249 #bulk-delete-panel .progress-text {
250 position: absolute;
251 top: 50%;
252 left: 50%;
253 transform: translate(-50%, -50%);
254 font-size: 12px;
255 font-weight: 600;
256 color: #202124;
257 }
258 `;
259
260 document.head.appendChild(style);
261 document.body.appendChild(panel);
262
263 // Make panel draggable
264 makeDraggable(panel);
265
266 return panel;
267 }
268
269 // Make panel draggable
270 function makeDraggable(element) {
271 const header = element.querySelector('.panel-header');
272 let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
273
274 header.onmousedown = dragMouseDown;
275
276 function dragMouseDown(e) {
277 e.preventDefault();
278 pos3 = e.clientX;
279 pos4 = e.clientY;
280 document.onmouseup = closeDragElement;
281 document.onmousemove = elementDrag;
282 }
283
284 function elementDrag(e) {
285 e.preventDefault();
286 pos1 = pos3 - e.clientX;
287 pos2 = pos4 - e.clientY;
288 pos3 = e.clientX;
289 pos4 = e.clientY;
290 element.style.top = (element.offsetTop - pos2) + 'px';
291 element.style.left = (element.offsetLeft - pos1) + 'px';
292 element.style.right = 'auto';
293 }
294
295 function closeDragElement() {
296 document.onmouseup = null;
297 document.onmousemove = null;
298 }
299 }
300
301 // Get all photo checkboxes
302 function getPhotoCheckboxes() {
303 return document.querySelectorAll('div[role="checkbox"][aria-label*="Photo"], div[role="checkbox"][aria-label*="photo"], div[role="checkbox"][aria-label*="Collage"]');
304 }
305
306 // Get selected count
307 function getSelectedCount() {
308 const checkboxes = getPhotoCheckboxes();
309 let count = 0;
310 checkboxes.forEach(cb => {
311 if (cb.getAttribute('aria-checked') === 'true') {
312 count++;
313 }
314 });
315 return count;
316 }
317
318 // Update stats
319 function updateStats() {
320 const photoCount = getPhotoCheckboxes().length;
321 const selectedCount = getSelectedCount();
322
323 document.getElementById('photo-count').textContent = photoCount;
324 document.getElementById('selected-count').textContent = selectedCount;
325 }
326
327 // Show status message
328 function showStatus(message, type = 'info') {
329 const statusEl = document.getElementById('status-message');
330 statusEl.textContent = message;
331 statusEl.className = type;
332 statusEl.style.display = 'block';
333
334 setTimeout(() => {
335 statusEl.style.display = 'none';
336 }, 5000);
337 }
338
339 // Show/hide progress bar
340 function showProgress(show = true) {
341 const progressContainer = document.getElementById('progress-container');
342 progressContainer.style.display = show ? 'block' : 'none';
343 if (!show) {
344 updateProgress(0);
345 }
346 }
347
348 // Update progress bar
349 function updateProgress(percent) {
350 const progressBar = document.getElementById('progress-bar');
351 const progressText = document.getElementById('progress-text');
352 progressBar.style.width = percent + '%';
353 progressText.textContent = Math.round(percent) + '%';
354 }
355
356 // Select all visible photos
357 function selectAllPhotos() {
358 console.log('Selecting all visible photos...');
359 const checkboxes = getPhotoCheckboxes();
360 let selected = 0;
361
362 checkboxes.forEach(checkbox => {
363 if (checkbox.getAttribute('aria-checked') !== 'true') {
364 // Use proper mouse events instead of simple click
365 checkbox.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true, view: window }));
366 checkbox.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true, view: window }));
367 checkbox.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }));
368 selected++;
369 }
370 });
371
372 console.log(`Clicked ${selected} checkboxes`);
373
374 // Wait a moment for selections to register, then update stats
375 setTimeout(() => {
376 const actualSelected = getSelectedCount();
377 console.log(`Actually selected: ${actualSelected}`);
378 updateStats();
379 showStatus(`Selected ${actualSelected} photos`, 'success');
380 }, 500);
381
382 console.log(`Selected ${selected} photos`);
383 }
384
385 // Deselect all photos
386 function deselectAllPhotos() {
387 console.log('Deselecting all photos...');
388 const checkboxes = getPhotoCheckboxes();
389 let deselected = 0;
390
391 checkboxes.forEach(checkbox => {
392 if (checkbox.getAttribute('aria-checked') === 'true') {
393 checkbox.click();
394 deselected++;
395 }
396 });
397
398 updateStats();
399 showStatus(`Deselected ${deselected} photos`, 'info');
400 console.log(`Deselected ${deselected} photos`);
401 }
402
403 // Auto-scroll and select
404 let isAutoScrolling = false;
405 let scrollInterval;
406
407 async function selectAllWithScroll() {
408 if (isAutoScrolling) {
409 stopAutoScroll();
410 return;
411 }
412
413 console.log('Starting auto-scroll and select...');
414 isAutoScrolling = true;
415 const btn = document.getElementById('select-and-scroll-btn');
416 btn.textContent = 'Stop Auto-scroll';
417 btn.classList.add('danger');
418 btn.classList.remove('primary');
419
420 showProgress(true);
421 showStatus('Auto-scrolling and selecting all photos...', 'info');
422
423 // Scroll to top first
424 window.scrollTo(0, 0);
425 await new Promise(resolve => setTimeout(resolve, 1000));
426
427 let lastPhotoCount = 0;
428 let stableCount = 0;
429 let totalSelected = 0;
430 let iterationCount = 0;
431
432 scrollInterval = setInterval(() => {
433 if (!isAutoScrolling) return;
434
435 iterationCount++;
436
437 // Select all currently visible photos
438 const checkboxes = getPhotoCheckboxes();
439 let selectedThisRound = 0;
440 checkboxes.forEach(checkbox => {
441 if (checkbox.getAttribute('aria-checked') !== 'true') {
442 checkbox.click();
443 selectedThisRound++;
444 totalSelected++;
445 }
446 });
447
448 console.log(`Iteration ${iterationCount}: Selected ${selectedThisRound} new photos. Total selected: ${totalSelected}. Total on page: ${checkboxes.length}`);
449
450 // Scroll down to load more photos
451 window.scrollBy(0, 2000);
452
453 // Check if we've reached the end
454 const currentPhotoCount = checkboxes.length;
455 const isAtBottom = (window.innerHeight + window.scrollY) >= document.body.scrollHeight - 200;
456
457 if (currentPhotoCount === lastPhotoCount) {
458 stableCount++;
459 console.log(`No new photos loaded. Stable count: ${stableCount}, At bottom: ${isAtBottom}`);
460 if (stableCount >= 8) {
461 // No new photos loaded after 8 checks, we're done
462 stopAutoScroll();
463 showStatus(`Completed! Selected ${totalSelected} photos total`, 'success');
464 console.log(`Auto-scroll completed. Selected ${totalSelected} photos total`);
465 }
466 } else {
467 stableCount = 0;
468 lastPhotoCount = currentPhotoCount;
469 }
470
471 updateStats();
472
473 // Update progress (estimate based on scroll position)
474 const scrollHeight = document.body.scrollHeight - window.innerHeight;
475 const scrollPercent = scrollHeight > 0 ? Math.min(95, (window.scrollY / scrollHeight) * 100) : 0;
476 updateProgress(scrollPercent);
477 }, 1500);
478 }
479
480 function stopAutoScroll() {
481 isAutoScrolling = false;
482 if (scrollInterval) {
483 clearInterval(scrollInterval);
484 }
485 const btn = document.getElementById('select-and-scroll-btn');
486 btn.textContent = 'Select All + Auto-scroll';
487 btn.classList.remove('danger');
488 btn.classList.add('primary');
489 showProgress(false);
490 }
491
492 // Delete selected photos
493 function deleteSelectedPhotos() {
494 const selectedCount = getSelectedCount();
495
496 if (selectedCount === 0) {
497 showStatus('No photos selected', 'error');
498 return;
499 }
500
501 if (!confirm(`Are you sure you want to delete ${selectedCount} selected photos? This will move them to trash.`)) {
502 return;
503 }
504
505 console.log(`Deleting ${selectedCount} selected photos...`);
506 showStatus(`Deleting ${selectedCount} photos...`, 'info');
507
508 // Look for the delete button - Google Photos shows it when items are selected
509 setTimeout(() => {
510 const deleteBtn = document.querySelector('button[aria-label="Move to trash"]');
511
512 if (deleteBtn) {
513 console.log('Found delete button, clicking...');
514 deleteBtn.click();
515
516 // Wait for confirmation dialog and click confirm
517 setTimeout(() => {
518 const confirmBtn = document.querySelector('button[data-mdc-dialog-action="EBS5u"]');
519
520 if (confirmBtn) {
521 console.log('Found confirm button, clicking...');
522 confirmBtn.click();
523 showStatus(`${selectedCount} photos moved to trash!`, 'success');
524 setTimeout(updateStats, 2000);
525 } else {
526 console.log('Confirm button not found');
527 showStatus('Please confirm deletion manually', 'info');
528 }
529 }, 1000);
530 } else {
531 console.error('Delete button not found');
532 showStatus('Delete button not found. Make sure photos are selected.', 'error');
533 }
534 }, 500);
535 }
536
537 // Auto delete all photos in batches
538 let isAutoDeleting = false;
539 let autoDeleteInterval;
540 let totalDeleted = 0;
541
542 async function autoDeleteAll() {
543 if (isAutoDeleting) {
544 stopAutoDelete();
545 return;
546 }
547
548 if (!confirm('This will automatically delete ALL photos in batches. Are you sure you want to continue?')) {
549 return;
550 }
551
552 console.log('Starting auto-delete mode...');
553 isAutoDeleting = true;
554 totalDeleted = 0;
555
556 const btn = document.getElementById('delete-all-auto-btn');
557 btn.textContent = '⏸️ Stop Auto Delete';
558
559 showProgress(true);
560 showStatus('Auto-deleting photos in batches...', 'info');
561
562 autoDeleteBatch();
563 }
564
565 async function autoDeleteBatch() {
566 if (!isAutoDeleting) return;
567
568 // Scroll to top to start fresh
569 window.scrollTo(0, 0);
570 await new Promise(resolve => setTimeout(resolve, 1000));
571
572 // Get current photos
573 const checkboxes = getPhotoCheckboxes();
574
575 if (checkboxes.length === 0) {
576 // No more photos!
577 stopAutoDelete();
578 showStatus(`Completed! Deleted ${totalDeleted} photos total`, 'success');
579 console.log(`Auto-delete completed. Deleted ${totalDeleted} photos total`);
580 return;
581 }
582
583 console.log(`Found ${checkboxes.length} photos to delete`);
584
585 // Select all visible photos
586 let selected = 0;
587 checkboxes.forEach(checkbox => {
588 if (checkbox.getAttribute('aria-checked') !== 'true') {
589 // Use proper mouse events instead of simple click
590 checkbox.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true, view: window }));
591 checkbox.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true, view: window }));
592 checkbox.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }));
593 selected++;
594 }
595 });
596
597 console.log(`Selected ${selected} photos for deletion`);
598 await new Promise(resolve => setTimeout(resolve, 1000));
599
600 // Get actual selected count
601 const selectedCount = getSelectedCount();
602 console.log(`Actually selected: ${selectedCount} photos`);
603
604 if (selectedCount === 0) {
605 console.log('No photos selected, retrying...');
606 setTimeout(autoDeleteBatch, 2000);
607 return;
608 }
609
610 updateStats();
611 showStatus(`Deleting batch of ${selectedCount} photos...`, 'info');
612
613 // Click delete button
614 const deleteBtn = document.querySelector('button[aria-label="Move to trash"]');
615 if (!deleteBtn) {
616 console.error('Delete button not found');
617 showStatus('Delete button not found, retrying...', 'error');
618 setTimeout(autoDeleteBatch, 2000);
619 return;
620 }
621
622 deleteBtn.click();
623 await new Promise(resolve => setTimeout(resolve, 1000));
624
625 // Click confirm button
626 const confirmBtn = document.querySelector('button[data-mdc-dialog-action="EBS5u"]');
627 if (!confirmBtn) {
628 console.error('Confirm button not found');
629 showStatus('Confirm button not found, retrying...', 'error');
630 setTimeout(autoDeleteBatch, 2000);
631 return;
632 }
633
634 confirmBtn.click();
635 totalDeleted += selectedCount;
636 console.log(`Deleted ${selectedCount} photos. Total deleted: ${totalDeleted}`);
637
638 updateProgress(Math.min(95, (totalDeleted / 1000) * 100));
639
640 // Wait for deletion to complete, then do next batch
641 await new Promise(resolve => setTimeout(resolve, 3000));
642
643 if (isAutoDeleting) {
644 autoDeleteBatch();
645 }
646 }
647
648 function stopAutoDelete() {
649 isAutoDeleting = false;
650 if (autoDeleteInterval) {
651 clearInterval(autoDeleteInterval);
652 }
653 const btn = document.getElementById('delete-all-auto-btn');
654 btn.textContent = '🔥 Delete All (Auto Mode)';
655 showProgress(false);
656 console.log(`Auto-delete stopped. Total deleted: ${totalDeleted}`);
657 }
658
659 // Initialize
660 function init() {
661 console.log('Initializing Bulk Delete Tool...');
662
663 // Wait for page to load
664 if (document.readyState === 'loading') {
665 document.addEventListener('DOMContentLoaded', init);
666 return;
667 }
668
669 // Create control panel
670 const panel = createControlPanel();
671
672 // Set up event listeners
673 document.getElementById('close-panel').addEventListener('click', () => {
674 panel.style.display = 'none';
675 });
676
677 document.getElementById('select-all-btn').addEventListener('click', selectAllPhotos);
678 document.getElementById('select-and-scroll-btn').addEventListener('click', selectAllWithScroll);
679 document.getElementById('deselect-all-btn').addEventListener('click', deselectAllPhotos);
680 document.getElementById('delete-all-auto-btn').addEventListener('click', autoDeleteAll);
681 document.getElementById('delete-selected-btn').addEventListener('click', deleteSelectedPhotos);
682
683 // Update stats periodically
684 setInterval(updateStats, 2000);
685 updateStats();
686
687 // Observe DOM changes to update stats
688 const observer = new MutationObserver(debounce(() => {
689 updateStats();
690 }, 500));
691
692 observer.observe(document.body, {
693 childList: true,
694 subtree: true
695 });
696
697 console.log('Bulk Delete Tool ready!');
698 showStatus('Ready to select and delete photos', 'success');
699 }
700
701 // Start the extension
702 init();
703})();