Last active
March 15, 2025 03:38
-
-
Save tranphuquy19/f8eeb02c7ca4b10f3baf02093eb80085 to your computer and use it in GitHub Desktop.
Google Photos Auto Delete Script
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==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 | |
`); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Preview