Bulk delete (mass-remove) chats from Google AI Studio in batch.
// ==UserScript==
// @name Google AI Studio Bulk Chat Deleter
// @namespace http://tampermonkey.net/
// @version 2025-11-21-v7-eod2
// @description Bulk delete (mass-remove) chats from Google AI Studio in batch.
// @author Lorenzo Alali
// @match https://aistudio.google.com/*
// @grant none
// @license MIT
// @homepageURL https://github.com/lorenzoalali/Gemini-and-AI-Studio-Mass-Chat-Deleter
// @run-at document-idle
// ==/UserScript==
/*
* =======================================================================
* --- DISCLAIMER & IMPORTANT INFORMATION ---
*
* This tool (and its Gemini equivalent) can be found
* on GitHub https://github.com/lorenzoalali/Google-AI-Studio-Bulk-Delete-UserScript
* and on Greasy Fork https://greasyfork.org/en/scripts/555870-google-ai-studio-bulk-delete
*
* --- USAGE AT YOUR OWN RISK ---
* The author provides no guarantees regarding the performance, safety, or functionality of this script. You assume
* all risks associated with its use. The author offers no support and is not responsible for any potential data
* loss or issues that may arise.
*
* --- FUTURE COMPATIBILITY ---
* This script's operation depends on the current Document Object Model (DOM) of the Google AI Studio platform.
* Modifications to the website by Google are likely to render this script non-functional in the future. While the
* author does not plan on providing proactive updates or support, contributions in the form of GitHub pull requests
* are welcome.
* =======================================================================
*/
(function () {
'use strict';
// --- State Management ---
const aBULK_DELETE_ALL_KEY = 'isAiStudioBulkDeletingAll';
const aBULK_DELETE_SELECTED_KEY = 'isAiStudioBulkDeletingSelected';
const aBULK_DELETE_SUCCESS_KEY = 'aiStudioBulkDeleteSuccessCount';
const aBULK_DELETE_FAIL_KEY = 'aiStudioBulkDeleteFailCount';
const aBULK_DELETE_TOTAL_KEY = 'aiStudioBulkDeleteTotalCount';
let isStopRequested = false;
let isProcessing = false; // Prevent multiple loops
let lastCheckedCheckbox = null; // For Range Selection
// Global UI References to handle re-renders
const uiElements = {
allBtn: null,
selBtn: null,
stopBtn: null
};
// --- UI Helper & Styles ---
const UI = {
injectStyles: () => {
if (document.getElementById('gas-bulk-delete-styles')) return;
const style = document.createElement('style');
style.id = 'gas-bulk-delete-styles';
style.textContent = `
.gas-toast-container {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
z-index: 10000;
display: flex;
flex-direction: column;
gap: 10px;
pointer-events: none;
}
.gas-toast {
background: #333;
color: #fff;
padding: 12px 24px;
border-radius: 8px;
font-family: 'Google Sans', Roboto, Arial, sans-serif;
font-size: 14px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
opacity: 0;
transform: translateY(20px);
transition: opacity 0.3s, transform 0.3s;
pointer-events: auto;
display: flex;
align-items: center;
gap: 8px;
min-width: 300px;
justify-content: center;
}
.gas-toast.visible {
opacity: 1;
transform: translateY(0);
}
.gas-toast.success { background: #0f9d58; }
.gas-toast.error { background: #d93025; }
.gas-toast.warning { background: #f4b400; color: #202124; }
.gas-toast.info { background: #1a73e8; }
.gas-modal-overlay {
position: fixed;
top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.5);
z-index: 10001;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.2s;
}
.gas-modal-overlay.visible { opacity: 1; }
.gas-modal {
background: white;
padding: 24px;
border-radius: 8px;
width: 400px;
max-width: 90%;
box-shadow: 0 1px 3px 0 rgba(60,64,67,0.3), 0 4px 8px 3px rgba(60,64,67,0.15);
font-family: 'Google Sans', Roboto, Arial, sans-serif;
transform: scale(0.95);
transition: transform 0.2s;
}
.gas-modal-overlay.visible .gas-modal { transform: scale(1); }
.gas-modal-title {
font-size: 18px;
font-weight: 500;
margin-bottom: 12px;
color: #202124;
}
.gas-modal-content {
font-size: 14px;
color: #5f6368;
line-height: 1.5;
margin-bottom: 24px;
white-space: pre-wrap;
}
.gas-modal-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
}
.gas-btn {
border: none;
background: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
font-size: 14px;
transition: background 0.2s;
}
.gas-btn:hover { background: rgba(0,0,0,0.04); }
.gas-btn.primary {
background: #1a73e8;
color: white;
}
.gas-btn.primary:hover { background: #1557b0; }
.gas-btn.danger {
background: #d93025;
color: white;
}
.gas-btn.danger:hover { background: #a50e0e; }
.gas-btn.secondary {
background: white;
color: #3c4043;
border: 1px solid #dadce0;
}
.gas-btn.secondary:hover {
background: #f1f3f4;
border-color: #dadce0;
}
/* Progress Bar Styles */
.gas-progress-container {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
z-index: 10000;
background: #333;
color: #fff;
padding: 16px 24px;
border-radius: 12px;
font-family: 'Google Sans', Roboto, Arial, sans-serif;
box-shadow: 0 4px 20px rgba(0,0,0,0.25);
display: flex;
flex-direction: column;
gap: 12px;
min-width: 320px;
opacity: 0;
transform: translate(-50%, 20px);
transition: opacity 0.3s, transform 0.3s;
pointer-events: auto;
}
.gas-progress-container.visible {
opacity: 1;
transform: translate(-50%, 0);
}
.gas-progress-header {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 14px;
font-weight: 500;
}
.gas-progress-bar-bg {
width: 100%;
height: 6px;
background: rgba(255,255,255,0.2);
border-radius: 3px;
overflow: hidden;
}
.gas-progress-bar-fill {
height: 100%;
background: #4285f4; /* Google Blue */
width: 0%;
transition: width 0.3s ease-out;
}
.gas-progress-details {
font-size: 12px;
color: rgba(255,255,255,0.7);
text-align: right;
}
/* Highlight for Dry Run */
.bulk-delete-highlight {
background-color: rgba(242, 153, 0, 0.2) !important;
border: 2px dashed #F29900 !important;
}
`;
document.head.appendChild(style);
},
showToast: (message, type = 'info', duration = 3000) => {
UI.injectStyles();
let container = document.querySelector('.gas-toast-container');
if (!container) {
container = document.createElement('div');
container.className = 'gas-toast-container';
document.body.appendChild(container);
}
const toast = document.createElement('div');
toast.className = `gas-toast ${type}`;
let icon = '';
if (type === 'success') icon = '✅';
if (type === 'error') icon = '❌';
if (type === 'warning') icon = '⚠️';
if (type === 'info') icon = 'ℹ️';
toast.innerHTML = `<span>${icon}</span><span>${message}</span>`;
container.appendChild(toast);
// Trigger reflow
toast.offsetHeight;
toast.classList.add('visible');
if (duration > 0) {
setTimeout(() => {
toast.classList.remove('visible');
setTimeout(() => toast.remove(), 300);
}, duration);
}
return toast;
},
showConfirm: (title, message, confirmText = 'Confirm', confirmType = 'primary') => {
UI.injectStyles();
return new Promise((resolve) => {
const overlay = document.createElement('div');
overlay.className = 'gas-modal-overlay';
const modal = document.createElement('div');
modal.className = 'gas-modal';
modal.innerHTML = `
<div class="gas-modal-title">${title}</div>
<div class="gas-modal-content">${message}</div>
<div class="gas-modal-actions">
<button class="gas-btn secondary cancel-btn">Cancel</button>
<button class="gas-btn ${confirmType} confirm-btn">${confirmText}</button>
</div>
`;
overlay.appendChild(modal);
document.body.appendChild(overlay);
// Trigger reflow
overlay.offsetHeight;
overlay.classList.add('visible');
const close = (result) => {
overlay.classList.remove('visible');
setTimeout(() => overlay.remove(), 200);
resolve(result);
};
modal.querySelector('.cancel-btn').addEventListener('click', () => close(false));
modal.querySelector('.confirm-btn').addEventListener('click', () => close(true));
// Close on click outside
overlay.addEventListener('click', (e) => {
if (e.target === overlay) close(false);
});
});
},
showProgress: (message, total) => {
UI.injectStyles();
// Remove existing if any
UI.hideProgress();
const container = document.createElement('div');
container.className = 'gas-progress-container';
container.id = 'gas-progress-ui';
container.innerHTML = `
<div class="gas-progress-header">
<span id="gas-progress-text">${message}</span>
<span id="gas-progress-percent">0%</span>
</div>
<div class="gas-progress-bar-bg">
<div class="gas-progress-bar-fill" id="gas-progress-fill"></div>
</div>
<div class="gas-progress-details" id="gas-progress-count">0 of ${total}</div>
`;
document.body.appendChild(container);
// Trigger reflow
container.offsetHeight;
container.classList.add('visible');
},
updateProgress: (current, total, textOverride = null) => {
const container = document.getElementById('gas-progress-ui');
if (!container) return;
const percentage = Math.min(100, Math.round((current / total) * 100));
const fill = document.getElementById('gas-progress-fill');
const percentText = document.getElementById('gas-progress-percent');
const countText = document.getElementById('gas-progress-count');
const mainText = document.getElementById('gas-progress-text');
if (fill) fill.style.width = `${percentage}%`;
if (percentText) percentText.textContent = `${percentage}%`;
if (countText) countText.textContent = `${current} of ${total}`;
if (textOverride && mainText) mainText.textContent = textOverride;
},
hideProgress: () => {
const container = document.getElementById('gas-progress-ui');
if (container) {
container.classList.remove('visible');
setTimeout(() => container.remove(), 300);
}
},
showUndoToast: (seconds) => {
UI.injectStyles();
return new Promise((resolve, reject) => {
let container = document.querySelector('.gas-toast-container');
if (!container) {
container = document.createElement('div');
container.className = 'gas-toast-container';
document.body.appendChild(container);
}
const toast = document.createElement('div');
toast.className = 'gas-toast warning';
toast.id = 'gas-undo-toast';
let remaining = seconds;
const updateText = () => {
toast.innerHTML = `
<span>⏳</span>
<span style="flex:1">Starting deletion in ${remaining}s...</span>
<button id="gas-undo-cancel" style="background:transparent;border:1px solid white;color:white;border-radius:4px;padding:4px 8px;cursor:pointer;font-size:12px;">CANCEL</button>
`;
};
updateText();
container.appendChild(toast);
// Trigger reflow
toast.offsetHeight;
toast.classList.add('visible');
const interval = setInterval(() => {
remaining--;
if (remaining <= 0) {
clearInterval(interval);
toast.classList.remove('visible');
setTimeout(() => toast.remove(), 300);
resolve(true); // Timer finished
} else {
updateText();
// Re-attach listener because innerHTML wiped it
document.getElementById('gas-undo-cancel').addEventListener('click', handleCancel);
}
}, 1000);
const handleCancel = () => {
clearInterval(interval);
toast.classList.remove('visible');
setTimeout(() => toast.remove(), 300);
resolve(false); // Cancelled
};
// Initial listener
toast.querySelector('#gas-undo-cancel').addEventListener('click', handleCancel);
});
}
};
// --- Configuration ---
const aDELAY_BETWEEN_ACTIONS = 500;
const aDELAY_AFTER_DELETION = 1500;
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// --- Count Management ---
function getCounts() {
return {
success: parseInt(sessionStorage.getItem(aBULK_DELETE_SUCCESS_KEY) || '0', 10),
fail: parseInt(sessionStorage.getItem(aBULK_DELETE_FAIL_KEY) || '0', 10)
};
}
function incrementSuccess() {
const current = getCounts().success;
sessionStorage.setItem(aBULK_DELETE_SUCCESS_KEY, (current + 1).toString());
}
function incrementFail() {
const current = getCounts().fail;
sessionStorage.setItem(aBULK_DELETE_FAIL_KEY, (current + 1).toString());
}
function resetCounts() {
sessionStorage.setItem(aBULK_DELETE_SUCCESS_KEY, '0');
sessionStorage.setItem(aBULK_DELETE_FAIL_KEY, '0');
}
function clearCounts() {
sessionStorage.removeItem(aBULK_DELETE_SUCCESS_KEY);
sessionStorage.removeItem(aBULK_DELETE_FAIL_KEY);
sessionStorage.removeItem(aBULK_DELETE_TOTAL_KEY);
}
async function findAndClickByText(
selector,
textOptions,
maxRetries = 5,
retryInterval = 250
) {
for (let i = 0; i < maxRetries; i++) {
const elements = document.querySelectorAll(selector);
for (const element of elements) {
const elementText = element.textContent.trim().toLowerCase();
const found = textOptions.some(text =>
elementText.includes(text.toLowerCase())
);
if (found) {
element.click();
return true;
}
}
await sleep(retryInterval);
}
console.error(
`Could not find element with selector "${selector}" ` +
`and text from [${textOptions.join(', ')}]`
);
return false;
}
/**
* Processes a batch of deletions (all visible items) and then reloads.
*/
async function processBatchDeletion(items) {
isProcessing = true;
if (uiElements.allBtn) uiElements.allBtn.disabled = true;
if (uiElements.selBtn) uiElements.selBtn.disabled = true;
if (uiElements.stopBtn) uiElements.stopBtn.style.display = 'inline-flex';
for (let i = 0; i < items.length; i++) {
if (isStopRequested) {
sessionStorage.removeItem(aBULK_DELETE_ALL_KEY);
clearCounts();
UI.showToast("Bulk delete process stopped by user.", 'warning');
isProcessing = false;
break;
}
const item = items[i];
if (!document.body.contains(item)) continue;
const counts = getCounts();
const totalProcessed = counts.success + counts.fail + 1;
const total = parseInt(sessionStorage.getItem(aBULK_DELETE_TOTAL_KEY) || '0');
const progressHTML =
`<span style="font-size: 1.2em;">🔥</span> <span>Deleting... (${totalProcessed})</span>`;
if (uiElements.allBtn) uiElements.allBtn.innerHTML = progressHTML;
// Update Progress Bar
UI.updateProgress(totalProcessed, total, `Deleting ${totalProcessed} of ${total}...`);
try {
item.click();
await sleep(aDELAY_BETWEEN_ACTIONS);
const delMenu = await findAndClickByText(
'button[role="menuitem"]',
['Delete']
);
if (!delMenu) {
document.querySelector('.cdk-overlay-backdrop')?.click();
incrementFail();
continue;
}
await sleep(aDELAY_BETWEEN_ACTIONS);
const delConfirm = await findAndClickByText(
'.mat-mdc-dialog-actions button',
['Delete']
);
if (!delConfirm) {
document.querySelector('.cdk-overlay-backdrop')?.click();
incrementFail();
continue;
}
incrementSuccess();
await sleep(aDELAY_AFTER_DELETION);
} catch (e) {
console.error("Error deleting item:", e);
incrementFail();
}
}
if (!isStopRequested) {
location.reload();
} else {
if (uiElements.allBtn) {
uiElements.allBtn.disabled = false;
uiElements.allBtn.innerHTML =
`<span style="font-size: 1.2em;">🔥</span> <span>Delete All</span>`;
}
if (uiElements.selBtn) uiElements.selBtn.disabled = false;
if (uiElements.stopBtn) uiElements.stopBtn.style.display = 'none';
isProcessing = false;
}
}
/**
* Deletes a single item and triggers a page reload.
*/
async function deleteSingleItemAndReload(itemMenuButton, onComplete) {
isProcessing = true;
if (!document.body.contains(itemMenuButton)) {
incrementFail();
isProcessing = false;
return;
}
try {
itemMenuButton.click();
await sleep(aDELAY_BETWEEN_ACTIONS);
const delMenu = await findAndClickByText(
'button[role="menuitem"]',
['Delete']
);
if (!delMenu) {
document.querySelector('.cdk-overlay-backdrop')?.click();
incrementFail();
isProcessing = false;
return;
}
await sleep(aDELAY_BETWEEN_ACTIONS);
const delConfirm = await findAndClickByText(
'.mat-mdc-dialog-actions button',
['Delete']
);
if (!delConfirm) {
document.querySelector('.cdk-overlay-backdrop')?.click();
incrementFail();
isProcessing = false;
return;
}
incrementSuccess();
// Update Progress Bar
const counts = getCounts();
const total = parseInt(sessionStorage.getItem(aBULK_DELETE_TOTAL_KEY) || '0');
const current = counts.success + counts.fail;
UI.updateProgress(current, total, `Deleting ${current} of ${total}...`);
await sleep(aDELAY_AFTER_DELETION);
if (onComplete) onComplete();
if (!isStopRequested) location.reload();
} catch (e) {
console.error("Error deleting single item:", e);
incrementFail();
// Update Progress Bar on failure too
const counts = getCounts();
const total = parseInt(sessionStorage.getItem(aBULK_DELETE_TOTAL_KEY) || '0');
const current = counts.success + counts.fail;
UI.updateProgress(current, total, `Error on item ${current}...`);
if (onComplete) onComplete(); // Proceed anyway
if (!isStopRequested) location.reload();
}
isProcessing = false;
}
/**
* Starts the 'Bulk Delete All' process.
*/
async function startBulkDeleteAll(isAutoStart = false) {
const query = 'ms-prompt-options-menu button[aria-label="More options"]';
const items = document.querySelectorAll(query);
if (items.length === 0) {
sessionStorage.removeItem(aBULK_DELETE_ALL_KEY);
const counts = getCounts();
clearCounts();
if (counts.success > 0 || counts.fail > 0) {
UI.showToast(`Bulk delete complete. Deleted: ${counts.success}, Failed: ${counts.fail}`, 'success', 5000);
} else {
UI.showToast("Bulk delete complete. Your library is now empty.", 'success', 5000);
}
isProcessing = false;
return;
}
if (!isAutoStart) {
const confirmMsg =
"This will bulk-delete ALL items in your history.\n\n" +
"It will delete items in batches of 5, reload the page, and " +
"continue until finished.\n\nAre you sure you want to proceed?";
const userConfirmation = await UI.showConfirm(
"Delete All Chats?",
confirmMsg,
"Delete All",
"danger"
);
if (!userConfirmation) return;
// Undo Timer
const proceed = await UI.showUndoToast(5);
if (!proceed) {
UI.showToast("Deletion Cancelled.", 'info');
return;
}
sessionStorage.setItem(aBULK_DELETE_ALL_KEY, 'true');
resetCounts();
// Initial Total Set
sessionStorage.setItem(aBULK_DELETE_TOTAL_KEY, items.length.toString());
UI.showProgress("Starting deletion...", items.length);
} else {
// Auto-start (after reload)
// Update total to include new items found
const storedTotal = parseInt(sessionStorage.getItem(aBULK_DELETE_TOTAL_KEY) || '0');
const newTotal = storedTotal + items.length;
sessionStorage.setItem(aBULK_DELETE_TOTAL_KEY, newTotal.toString());
const counts = getCounts();
UI.showProgress("Resuming deletion...", newTotal);
UI.updateProgress(counts.success + counts.fail, newTotal);
}
processBatchDeletion(Array.from(items));
}
/**
* Starts the 'Bulk Delete Selected' process.
*/
async function startBulkDeleteSelected() {
sessionStorage.removeItem(aBULK_DELETE_ALL_KEY); // Safety clear
const selector = '.bulk-delete-checkbox:checked';
const selectedCheckboxes = document.querySelectorAll(selector);
if (selectedCheckboxes.length === 0) {
UI.showToast("No items selected for deletion.", 'warning');
return;
}
const confirmMsg = `Are you sure you want to delete the ${selectedCheckboxes.length} selected item(s)?`;
const userConfirmation = await UI.showConfirm(
"Delete Selected Chats?",
confirmMsg,
"Delete Selected",
"danger"
);
if (!userConfirmation) return;
// Undo Timer
const proceed = await UI.showUndoToast(5);
if (!proceed) {
UI.showToast("Deletion Cancelled.", 'info');
return;
}
const itemHrefs = Array.from(selectedCheckboxes).map(cb => {
const link = cb.closest('tr').querySelector('a.name-btn');
return link ? link.getAttribute('href') : null;
}).filter(Boolean);
const key = aBULK_DELETE_SELECTED_KEY;
sessionStorage.setItem(key, JSON.stringify(itemHrefs));
// Set Total for Selected
sessionStorage.setItem(aBULK_DELETE_TOTAL_KEY, itemHrefs.length.toString());
resetCounts();
location.reload(); // Start the process
}
function addCheckboxesToRows() {
// Only run if we are on the library page
if (!location.href.includes('/library')) return;
const rows = document.querySelectorAll('tbody tr.mat-mdc-row');
rows.forEach(row => {
if (row.querySelector('.bulk-delete-checkbox-cell')) return;
const checkboxCell = document.createElement('td');
checkboxCell.className = 'mat-mdc-cell mdc-data-table__cell cdk-cell bulk-delete-checkbox-cell';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.className = 'bulk-delete-checkbox';
Object.assign(checkbox.style, {
width: '18px',
height: '18px',
cursor: 'pointer'
});
checkboxCell.appendChild(checkbox);
row.prepend(checkboxCell);
// Range Selection Logic
checkbox.addEventListener('click', (e) => {
if (e.shiftKey && lastCheckedCheckbox && lastCheckedCheckbox !== checkbox) {
const allCheckboxes = Array.from(document.querySelectorAll('.bulk-delete-checkbox'));
const start = allCheckboxes.indexOf(lastCheckedCheckbox);
const end = allCheckboxes.indexOf(checkbox);
if (start !== -1 && end !== -1) {
const low = Math.min(start, end);
const high = Math.max(start, end);
for (let i = low; i <= high; i++) {
allCheckboxes[i].checked = lastCheckedCheckbox.checked;
}
}
}
lastCheckedCheckbox = checkbox;
updateButtonState();
});
});
const headerRow = document.querySelector('thead tr');
if (!headerRow) return;
const headerExists = headerRow.querySelector('.bulk-delete-header-cell');
if (headerExists) return;
const th = document.createElement('th');
th.className = 'mat-mdc-header-cell mdc-data-table__header-cell cdk-header-cell bulk-delete-header-cell';
const masterCheckbox = document.createElement('input');
masterCheckbox.type = 'checkbox';
masterCheckbox.id = 'master-bulk-delete-checkbox';
Object.assign(masterCheckbox.style, {
width: '18px',
height: '18px',
cursor: 'pointer'
});
masterCheckbox.addEventListener('change', (event) => {
const isChecked = event.target.checked;
const allCheckboxes = document.querySelectorAll(
'input.bulk-delete-checkbox'
);
allCheckboxes.forEach(checkbox => {
checkbox.checked = isChecked;
});
updateButtonState();
});
th.appendChild(masterCheckbox);
headerRow.prepend(th);
}
function simulateDelete() {
const selector = '.bulk-delete-checkbox:checked';
const selectedCheckboxes = document.querySelectorAll(selector);
let targets = [];
if (selectedCheckboxes.length > 0) {
targets = Array.from(selectedCheckboxes).map(cb => cb.closest('tr'));
} else {
// If nothing selected, maybe simulate ALL?
// For now, let's just simulate selected if any, or warn.
// Actually, user might want to simulate "Delete All".
// But "Delete All" deletes everything, so we can just highlight everything.
// Let's stick to simulating selection if selection exists, otherwise warn.
// Or if user clicks Simulate with nothing selected, maybe we simulate ALL visible?
// Let's simulate ALL visible if nothing selected.
targets = Array.from(document.querySelectorAll('tbody tr.mat-mdc-row'));
}
if (targets.length === 0) {
UI.showToast("No chats found to simulate.", 'warning');
return;
}
// Highlight
targets.forEach(el => {
el.classList.add('bulk-delete-highlight');
});
const mode = selectedCheckboxes.length > 0 ? "Selected" : "All Visible";
UI.showToast(`DRY RUN (${mode}): Would delete ${targets.length} chat(s).`, 'info', 4000);
// Remove highlight after 4 seconds
setTimeout(() => {
targets.forEach(el => {
el.classList.remove('bulk-delete-highlight');
});
}, 4000);
}
function addButtons() {
// Only run if we are on the library page
if (!location.href.includes('/library')) return;
// If buttons exist, update references and return
const existingAll = document.getElementById('bulk-delete-all-button');
if (existingAll) {
uiElements.allBtn = existingAll;
uiElements.selBtn = document.getElementById('bulk-delete-selected-button');
uiElements.stopBtn = document.getElementById('stop-bulk-delete-button');
return;
}
const wrapper = document.querySelector('.lib-header .actions-wrapper');
if (!wrapper) return;
// Use the EXACT classes from the native "Open in Drive" button
const btnClass = 'responsive-button-viewport-medium viewport-small-hidden ms-button-primary';
// Minimal inline styles to position the buttons and override specific colors.
// We rely on the classes above for shape, padding, font, and height.
const commonStyle = {
marginLeft: '10px',
cursor: 'pointer',
color: 'white',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
gap: '8px',
textDecoration: 'none',
border: 'none' // Ensure no border overrides the class
};
// --- Simulate Button ---
const simulateButton = document.createElement('button');
simulateButton.id = 'bulk-delete-simulate-button';
simulateButton.setAttribute('ms-button', '');
simulateButton.innerHTML =
`<span style="font-size: 1.2em;">🧪</span>`;
simulateButton.title = "Simulate Deletion (Dry Run)";
simulateButton.className = btnClass;
Object.assign(simulateButton.style, {
...commonStyle,
backgroundColor: '#f4b400', // Yellow/Orange
color: '#202124',
minWidth: '40px',
padding: '0 12px'
});
simulateButton.onclick = simulateDelete;
// --- Delete All Button ---
const bulkDeleteAllButton = document.createElement('button');
bulkDeleteAllButton.id = 'bulk-delete-all-button';
// Add 'ms-button' attribute as seen in the native anchor tag
bulkDeleteAllButton.setAttribute('ms-button', '');
bulkDeleteAllButton.innerHTML =
`<span style="font-size: 1.2em;">🔥</span> <span>Delete All</span>`;
bulkDeleteAllButton.className = btnClass;
Object.assign(bulkDeleteAllButton.style, {
...commonStyle,
backgroundColor: '#d93025', // Google Red override for 'delete' action
});
// --- Delete Selected Button ---
const bulkDeleteSelectedButton = document.createElement('button');
bulkDeleteSelectedButton.id = 'bulk-delete-selected-button';
bulkDeleteSelectedButton.setAttribute('ms-button', '');
bulkDeleteSelectedButton.innerHTML =
`<span style="font-size: 1.2em;">🗑️</span> <span>Delete Selected</span>`;
bulkDeleteSelectedButton.className = btnClass;
Object.assign(bulkDeleteSelectedButton.style, {
...commonStyle,
backgroundColor: '#e37400', // Google Orange/Warning override
});
// --- Stop Button ---
const stopButton = document.createElement('button');
stopButton.id = 'stop-bulk-delete-button';
stopButton.setAttribute('ms-button', '');
stopButton.innerHTML =
`<span style="font-size: 1.2em;">🛑</span> <span>Stop</span>`;
stopButton.className = btnClass;
Object.assign(stopButton.style, {
...commonStyle,
// No background color override needed here if we want the native Blue (ms-button-primary default)
// But explicit is safer if the class logic changes:
backgroundColor: '#1a73e8', // Google Blue
display: 'none' // Hidden by default
});
// --- Hover Effects ---
// Native buttons typically have a state overlay.
// We simulate this simply by dimming slightly on hover.
const addHover = (btn) => {
btn.addEventListener('mouseenter', () => btn.style.filter = 'brightness(0.95)');
btn.addEventListener('mouseleave', () => btn.style.filter = 'brightness(1)');
};
addHover(bulkDeleteAllButton);
addHover(bulkDeleteSelectedButton);
addHover(stopButton);
addHover(simulateButton);
// --- Event Listeners ---
stopButton.addEventListener('click', () => {
isStopRequested = true;
sessionStorage.removeItem(aBULK_DELETE_ALL_KEY);
sessionStorage.removeItem(aBULK_DELETE_SELECTED_KEY);
clearCounts();
stopButton.innerHTML = `<span style="font-size: 1.2em;">🛑</span> <span>Stopping...</span>`;
stopButton.disabled = true;
UI.showToast(
"Bulk delete will stop. The page will not reload after the current action.",
'info',
5000
);
});
bulkDeleteAllButton.addEventListener('click', () =>
startBulkDeleteAll(false)
);
bulkDeleteSelectedButton.addEventListener('click', () =>
startBulkDeleteSelected()
);
// Append to DOM
wrapper.appendChild(simulateButton);
wrapper.appendChild(bulkDeleteSelectedButton);
wrapper.appendChild(bulkDeleteAllButton);
wrapper.appendChild(stopButton);
// Update Global Refs
uiElements.allBtn = bulkDeleteAllButton;
uiElements.selBtn = bulkDeleteSelectedButton;
uiElements.stopBtn = stopButton;
// If we are processing (e.g. after a re-render), restore UI state
if (isProcessing) {
bulkDeleteAllButton.disabled = true;
bulkDeleteSelectedButton.disabled = true;
stopButton.style.display = 'inline-flex';
// Note: innerHTML for progress is updated by the running loop
} else {
updateButtonState(); // Set initial state
}
setTimeout(() => handleAutoDeletion(), 2000);
}
/**
* Checks session storage on page load and continues any pending
* deletion process.
*/
function handleAutoDeletion() {
if (isProcessing) return; // Already running
if (sessionStorage.getItem(aBULK_DELETE_ALL_KEY) === 'true') {
startBulkDeleteAll(true);
} else if (sessionStorage.getItem(aBULK_DELETE_SELECTED_KEY)) {
const key = aBULK_DELETE_SELECTED_KEY;
let itemHrefs = JSON.parse(sessionStorage.getItem(key));
if (itemHrefs.length > 0) {
// Update UI for selected deletion
if (uiElements.allBtn) uiElements.allBtn.disabled = true;
if (uiElements.selBtn) uiElements.selBtn.disabled = true;
if (uiElements.stopBtn) uiElements.stopBtn.style.display = 'inline-flex';
// Show Progress Bar
const total = parseInt(sessionStorage.getItem(aBULK_DELETE_TOTAL_KEY) || itemHrefs.length.toString());
const counts = getCounts();
const current = counts.success + counts.fail;
UI.showProgress("Deleting selected...", total);
UI.updateProgress(current, total);
const nextHref = itemHrefs[0];
const linkSelector = `a.name-btn[href="${nextHref}"]`;
const nextLink = document.querySelector(linkSelector);
if (nextLink) {
const progressHTML =
`<span style="font-size: 1.2em;">🗑️</span> ` +
`<span>Deleting ${itemHrefs.length} selected...</span>`;
if (uiElements.selBtn) uiElements.selBtn.innerHTML = progressHTML;
const itemMenu = nextLink.closest('tr')
.querySelector('ms-prompt-options-menu button');
deleteSingleItemAndReload(itemMenu, () => {
itemHrefs.shift(); // Remove processed item
// Always save the array, even if empty, so we detect "finished" state on next load
sessionStorage.setItem(key, JSON.stringify(itemHrefs));
});
} else {
console.warn(
`Item with href ${nextHref} not found. Checking next...`
);
itemHrefs.shift();
sessionStorage.setItem(key, JSON.stringify(itemHrefs));
incrementFail();
location.reload();
}
} else {
// List is empty, meaning we finished
sessionStorage.removeItem(key);
const counts = getCounts();
clearCounts();
UI.hideProgress();
UI.showToast(`Selected items have been deleted. Deleted: ${counts.success}, Failed: ${counts.fail}`, 'success', 5000);
if (uiElements.allBtn) uiElements.allBtn.disabled = false;
if (uiElements.selBtn) {
uiElements.selBtn.disabled = false;
updateButtonState(); // Reset text
}
if (uiElements.stopBtn) uiElements.stopBtn.style.display = 'none';
}
}
}
function updateButtonState() {
const selectedCount = document.querySelectorAll('.bulk-delete-checkbox:checked').length;
const btn = uiElements.selBtn;
if (btn) {
if (selectedCount > 0) {
btn.disabled = false;
btn.innerHTML = `<span style="font-size: 1.2em;">🗑️</span> <span>Delete Selected (${selectedCount})</span>`;
} else {
btn.disabled = true; // Optional: disable if nothing selected, like Gemini
btn.innerHTML = `<span style="font-size: 1.2em;">🗑️</span> <span>Delete Selected</span>`;
}
}
}
function manageUI() {
// If we are on the library page, attempt to inject UI
if (location.href.includes('/library')) {
addButtons();
addCheckboxesToRows();
}
}
// --- Observer Setup ---
const observer = new MutationObserver((mutations) => {
manageUI();
});
observer.observe(document.body, {
childList: true,
subtree: true
});
// --- Fallback Interval ---
setInterval(() => {
manageUI();
}, 1000);
// Initial Run
manageUI();
})();