// ==UserScript==
// @name Google Photos Auto Delete
// @namespace https://github.com/tranphuquy19
// @version 1.0.1
// @description Automatically delete multiple images from Google Photos. Source: https://gist.github.com/tranphuquy19/f8eeb02c7ca4b10f3baf02093eb80085
// @author Quy (Christian) P. TRAN
// @match https://photos.google.com/*
// @grant none
// @run-at document-end
// ==/UserScript==
if (window.trustedTypes && window.trustedTypes.createPolicy) {
window.trustedTypes.createPolicy('default', {
createHTML: (string, sink) => string
});
}
class AutoDeleter {
constructor(config = {}) {
this.config = {
MAX_RETRIES: 3,
SCROLL_STEP: 1000,
DELAY: 2000,
SELECTORS: {
checkboxes: '.QcpS9c.ckGgle',
trashIcon: 'button[aria-label="Move to trash"]',
confirmButton: 'button'
},
...config
};
this.isRunning = false;
this.currentIteration = 0;
this.totalIterations = 0;
}
async smoothScroll() {
return new Promise((resolve) => {
const scrollHeight = document.documentElement.scrollHeight;
let currentPosition = window.pageYOffset;
const scrollStep = Math.max(scrollHeight / 10, this.config.SCROLL_STEP);
const scroll = () => {
currentPosition += scrollStep;
window.scrollTo(0, currentPosition);
if (currentPosition < scrollHeight) {
setTimeout(scroll, 100);
} else {
resolve();
}
};
scroll();
});
}
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async findAndClick(selector, description) {
const elements = selector === this.config.SELECTORS.confirmButton
? Array.from(document.querySelectorAll(selector)).filter(btn => btn.textContent === 'Move to trash')
: document.querySelectorAll(selector);
if (elements.length === 0) {
console.log(`Not found: ${description}`);
return false;
}
if (elements.length > 1) {
elements.forEach(el => el.click());
} else {
elements[0].click();
}
return true;
}
async performSingleDeletion() {
try {
await this.smoothScroll();
await this.delay(this.config.DELAY);
const checkboxesClicked = await this.findAndClick(
this.config.SELECTORS.checkboxes,
'checkboxes'
);
if (!checkboxesClicked) return false;
await this.delay(this.config.DELAY);
const trashIconClicked = await this.findAndClick(
this.config.SELECTORS.trashIcon,
'trash icon'
);
if (!trashIconClicked) return false;
await this.delay(this.config.DELAY);
const confirmButtonClicked = await this.findAndClick(
this.config.SELECTORS.confirmButton,
'confirm button'
);
if (!confirmButtonClicked) return false;
return true;
} catch (error) {
console.error('Error during deletion:', error);
return false;
}
}
async start(times) {
if (this.isRunning) {
console.log('Already running!');
return;
}
this.isRunning = true;
this.currentIteration = 0;
this.totalIterations = times;
await this.runIteration();
}
stop() {
this.isRunning = false;
console.log('Stopping after current iteration...');
}
async runIteration(retryCount = 0) {
if (!this.isRunning || this.currentIteration >= this.totalIterations) {
this.isRunning = false;
this.updateUI('complete');
return;
}
if (retryCount >= this.config.MAX_RETRIES) {
console.log(`Failed after ${this.config.MAX_RETRIES} retries, moving to next iteration`);
this.currentIteration++;
this.updateUI('running');
await this.runIteration(0);
return;
}
if (retryCount === 0) {
this.currentIteration++;
console.log(`Iteration ${this.currentIteration}/${this.totalIterations}`);
} else {
console.log(`Retry ${retryCount + 1} for iteration ${this.currentIteration}`);
}
const success = await this.performSingleDeletion();
if (!success) {
await this.delay(this.config.DELAY);
await this.runIteration(retryCount + 1);
return;
}
this.updateUI('running');
await this.delay(this.config.DELAY);
await this.runIteration(0);
}
updateUI(status) {
const event = new CustomEvent('autoDeleterUpdate', {
detail: {
status,
current: this.currentIteration,
total: this.totalIterations
}
});
window.dispatchEvent(event);
}
}
class UIController {
constructor() {
this.autoDeleter = new AutoDeleter();
this.setupUI();
this.setupEventListeners();
this.setupDraggable();
}
setupUI() {
const container = document.createElement('div');
Object.assign(container.style, {
position: 'fixed',
top: '20px',
right: '20px',
backgroundColor: 'white',
padding: '20px',
borderRadius: '8px',
boxShadow: '0 2px 10px rgba(0,0,0,0.1)',
zIndex: '9999',
width: '300px',
fontFamily: 'Arial, sans-serif',
cursor: 'move', // Thêm cursor move
userSelect: 'none' // Prevent text selection while dragging
});
container.innerHTML = `
<div id="dragHandle" style="
padding: 10px;
margin: -20px -20px 15px -20px;
background: #f5f5f5;
border-radius: 8px 8px 0 0;
display: flex;
justify-content: space-between;
align-items: center;
cursor: move;
">
<h3 style="margin: 0; font-size: 16px;">Google Photos Auto Delete</h3>
<div style="display: flex; gap: 10px;">
<button id="minimizeButton" style="
padding: 4px 8px;
background: #ddd;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
">_</button>
<button id="closeButton" style="
padding: 4px 8px;
background: #ff4444;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
">×</button>
</div>
</div>
<div id="contentPanel">
<input type="number" id="iterationCount" min="1" value="5"
style="width: 100%; padding: 8px; margin-bottom: 10px; box-sizing: border-box; border: 1px solid #ddd; border-radius: 4px;">
<div style="display: flex; gap: 10px; margin-bottom: 15px;">
<button id="startButton" style="
flex: 1;
padding: 8px;
background: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background 0.3s;
">Start</button>
<button id="stopButton" style="
flex: 1;
padding: 8px;
background: #f44336;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background 0.3s;
" disabled>Stop</button>
</div>
<div style="
background: #f5f5f5;
padding: 10px;
border-radius: 4px;
font-size: 14px;
">
<div>Status: <span id="status">Ready</span></div>
<div>Progress: <span id="progress">0/0</span></div>
</div>
</div>
`;
document.body.appendChild(container);
this.container = container;
}
setupDraggable() {
const container = this.container;
const dragHandle = container.querySelector('#dragHandle');
let isDragging = false;
let currentX;
let currentY;
let initialX;
let initialY;
let xOffset = 0;
let yOffset = 0;
// Lưu vị trí vào localStorage
const savePosition = () => {
const position = {
x: xOffset,
y: yOffset
};
localStorage.setItem('autoDeleterPosition', JSON.stringify(position));
};
// Khôi phục vị trí từ localStorage
const loadPosition = () => {
const savedPosition = localStorage.getItem('autoDeleterPosition');
if (savedPosition) {
const position = JSON.parse(savedPosition);
xOffset = position.x;
yOffset = position.y;
setTranslate(xOffset, yOffset, container);
}
};
const dragStart = (e) => {
if (e.type === "touchstart") {
initialX = e.touches[0].clientX - xOffset;
initialY = e.touches[0].clientY - yOffset;
} else {
initialX = e.clientX - xOffset;
initialY = e.clientY - yOffset;
}
if (e.target === dragHandle || e.target.parentElement === dragHandle) {
isDragging = true;
}
};
const dragEnd = () => {
isDragging = false;
savePosition(); // Lưu vị trí khi kết thúc kéo
};
const drag = (e) => {
if (isDragging) {
e.preventDefault();
if (e.type === "touchmove") {
currentX = e.touches[0].clientX - initialX;
currentY = e.touches[0].clientY - initialY;
} else {
currentX = e.clientX - initialX;
currentY = e.clientY - initialY;
}
xOffset = currentX;
yOffset = currentY;
setTranslate(currentX, currentY, container);
}
};
const setTranslate = (xPos, yPos, el) => {
el.style.transform = `translate3d(${xPos}px, ${yPos}px, 0)`;
};
// Mouse events
dragHandle.addEventListener('mousedown', dragStart);
document.addEventListener('mousemove', drag);
document.addEventListener('mouseup', dragEnd);
// Touch events
dragHandle.addEventListener('touchstart', dragStart);
document.addEventListener('touchmove', drag);
document.addEventListener('touchend', dragEnd);
// Minimize/Maximize functionality
const minimizeButton = container.querySelector('#minimizeButton');
const contentPanel = container.querySelector('#contentPanel');
let isMinimized = false;
minimizeButton.addEventListener('click', () => {
if (isMinimized) {
contentPanel.style.display = 'block';
minimizeButton.textContent = '_';
} else {
contentPanel.style.display = 'none';
minimizeButton.textContent = '□';
}
isMinimized = !isMinimized;
});
// Close functionality
const closeButton = container.querySelector('#closeButton');
closeButton.addEventListener('click', () => {
container.remove();
});
// Load saved position when initializing
loadPosition();
}
setupEventListeners() {
const startButton = this.container.querySelector('#startButton');
const stopButton = this.container.querySelector('#stopButton');
const iterationInput = this.container.querySelector('#iterationCount');
startButton.addEventListener('click', () => {
const count = parseInt(iterationInput.value);
if (count > 0) {
startButton.disabled = true;
stopButton.disabled = false;
this.autoDeleter.start(count);
}
});
stopButton.addEventListener('click', () => {
this.autoDeleter.stop();
stopButton.disabled = true;
});
window.addEventListener('autoDeleterUpdate', (e) => {
const statusElem = this.container.querySelector('#status');
const progressElem = this.container.querySelector('#progress');
const startButton = this.container.querySelector('#startButton');
const stopButton = this.container.querySelector('#stopButton');
progressElem.textContent = `${e.detail.current}/${e.detail.total}`;
switch (e.detail.status) {
case 'running':
statusElem.textContent = 'Running';
statusElem.style.color = '#4CAF50';
break;
case 'complete':
statusElem.textContent = 'Complete';
statusElem.style.color = '#2196F3';
startButton.disabled = false;
stopButton.disabled = true;
break;
}
});
}
}
// Initialize the UI
const controller = new UIController();
console.log(`
Google Photos Auto Delete Script
------------------------------
UI Controls have been added to the page.
You can:
1. Set the number of iterations
2. Click Start to begin
3. Click Stop to pause after current iteration
4. Monitor progress in the UI panel
`);