Google Photos Bulk Delete Tool

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