Google Photos Auto Delete

Automatically delete multiple images from Google Photos. Source: https://gist.github.com/tranphuquy19/f8eeb02c7ca4b10f3baf02093eb80085

当前为 2025-03-15 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Google Photos Auto Delete
  3. // @namespace https://github.com/tranphuquy19
  4. // @version 1.0.1
  5. // @description Automatically delete multiple images from Google Photos. Source: https://gist.github.com/tranphuquy19/f8eeb02c7ca4b10f3baf02093eb80085
  6. // @author Quy (Christian) P. TRAN
  7. // @match https://photos.google.com/*
  8. // @grant none
  9. // @run-at document-end
  10. // ==/UserScript==
  11.  
  12. if (window.trustedTypes && window.trustedTypes.createPolicy) {
  13. window.trustedTypes.createPolicy('default', {
  14. createHTML: (string, sink) => string
  15. });
  16. }
  17.  
  18. class AutoDeleter {
  19. constructor(config = {}) {
  20. this.config = {
  21. MAX_RETRIES: 3,
  22. SCROLL_STEP: 1000,
  23. DELAY: 2000,
  24. SELECTORS: {
  25. checkboxes: '.QcpS9c.ckGgle',
  26. trashIcon: 'button[aria-label="Move to trash"]',
  27. confirmButton: 'button'
  28. },
  29. ...config
  30. };
  31. this.isRunning = false;
  32. this.currentIteration = 0;
  33. this.totalIterations = 0;
  34. }
  35.  
  36. async smoothScroll() {
  37. return new Promise((resolve) => {
  38. const scrollHeight = document.documentElement.scrollHeight;
  39. let currentPosition = window.pageYOffset;
  40. const scrollStep = Math.max(scrollHeight / 10, this.config.SCROLL_STEP);
  41.  
  42. const scroll = () => {
  43. currentPosition += scrollStep;
  44. window.scrollTo(0, currentPosition);
  45.  
  46. if (currentPosition < scrollHeight) {
  47. setTimeout(scroll, 100);
  48. } else {
  49. resolve();
  50. }
  51. };
  52.  
  53. scroll();
  54. });
  55. }
  56.  
  57. delay(ms) {
  58. return new Promise(resolve => setTimeout(resolve, ms));
  59. }
  60.  
  61. async findAndClick(selector, description) {
  62. const elements = selector === this.config.SELECTORS.confirmButton
  63. ? Array.from(document.querySelectorAll(selector)).filter(btn => btn.textContent === 'Move to trash')
  64. : document.querySelectorAll(selector);
  65.  
  66. if (elements.length === 0) {
  67. console.log(`Not found: ${description}`);
  68. return false;
  69. }
  70.  
  71. if (elements.length > 1) {
  72. elements.forEach(el => el.click());
  73. } else {
  74. elements[0].click();
  75. }
  76.  
  77. return true;
  78. }
  79.  
  80. async performSingleDeletion() {
  81. try {
  82. await this.smoothScroll();
  83. await this.delay(this.config.DELAY);
  84.  
  85. const checkboxesClicked = await this.findAndClick(
  86. this.config.SELECTORS.checkboxes,
  87. 'checkboxes'
  88. );
  89. if (!checkboxesClicked) return false;
  90. await this.delay(this.config.DELAY);
  91.  
  92. const trashIconClicked = await this.findAndClick(
  93. this.config.SELECTORS.trashIcon,
  94. 'trash icon'
  95. );
  96. if (!trashIconClicked) return false;
  97. await this.delay(this.config.DELAY);
  98.  
  99. const confirmButtonClicked = await this.findAndClick(
  100. this.config.SELECTORS.confirmButton,
  101. 'confirm button'
  102. );
  103. if (!confirmButtonClicked) return false;
  104.  
  105. return true;
  106. } catch (error) {
  107. console.error('Error during deletion:', error);
  108. return false;
  109. }
  110. }
  111.  
  112. async start(times) {
  113. if (this.isRunning) {
  114. console.log('Already running!');
  115. return;
  116. }
  117.  
  118. this.isRunning = true;
  119. this.currentIteration = 0;
  120. this.totalIterations = times;
  121. await this.runIteration();
  122. }
  123.  
  124. stop() {
  125. this.isRunning = false;
  126. console.log('Stopping after current iteration...');
  127. }
  128.  
  129. async runIteration(retryCount = 0) {
  130. if (!this.isRunning || this.currentIteration >= this.totalIterations) {
  131. this.isRunning = false;
  132. this.updateUI('complete');
  133. return;
  134. }
  135.  
  136. if (retryCount >= this.config.MAX_RETRIES) {
  137. console.log(`Failed after ${this.config.MAX_RETRIES} retries, moving to next iteration`);
  138. this.currentIteration++;
  139. this.updateUI('running');
  140. await this.runIteration(0);
  141. return;
  142. }
  143.  
  144. if (retryCount === 0) {
  145. this.currentIteration++;
  146. console.log(`Iteration ${this.currentIteration}/${this.totalIterations}`);
  147. } else {
  148. console.log(`Retry ${retryCount + 1} for iteration ${this.currentIteration}`);
  149. }
  150.  
  151. const success = await this.performSingleDeletion();
  152.  
  153. if (!success) {
  154. await this.delay(this.config.DELAY);
  155. await this.runIteration(retryCount + 1);
  156. return;
  157. }
  158.  
  159. this.updateUI('running');
  160. await this.delay(this.config.DELAY);
  161. await this.runIteration(0);
  162. }
  163.  
  164. updateUI(status) {
  165. const event = new CustomEvent('autoDeleterUpdate', {
  166. detail: {
  167. status,
  168. current: this.currentIteration,
  169. total: this.totalIterations
  170. }
  171. });
  172. window.dispatchEvent(event);
  173. }
  174. }
  175.  
  176. class UIController {
  177. constructor() {
  178. this.autoDeleter = new AutoDeleter();
  179. this.setupUI();
  180. this.setupEventListeners();
  181. this.setupDraggable();
  182. }
  183.  
  184. setupUI() {
  185. const container = document.createElement('div');
  186. Object.assign(container.style, {
  187. position: 'fixed',
  188. top: '20px',
  189. right: '20px',
  190. backgroundColor: 'white',
  191. padding: '20px',
  192. borderRadius: '8px',
  193. boxShadow: '0 2px 10px rgba(0,0,0,0.1)',
  194. zIndex: '9999',
  195. width: '300px',
  196. fontFamily: 'Arial, sans-serif',
  197. cursor: 'move', // Thêm cursor move
  198. userSelect: 'none' // Prevent text selection while dragging
  199. });
  200.  
  201. container.innerHTML = `
  202. <div id="dragHandle" style="
  203. padding: 10px;
  204. margin: -20px -20px 15px -20px;
  205. background: #f5f5f5;
  206. border-radius: 8px 8px 0 0;
  207. display: flex;
  208. justify-content: space-between;
  209. align-items: center;
  210. cursor: move;
  211. ">
  212. <h3 style="margin: 0; font-size: 16px;">Google Photos Auto Delete</h3>
  213. <div style="display: flex; gap: 10px;">
  214. <button id="minimizeButton" style="
  215. padding: 4px 8px;
  216. background: #ddd;
  217. border: none;
  218. border-radius: 4px;
  219. cursor: pointer;
  220. font-size: 14px;
  221. ">_</button>
  222. <button id="closeButton" style="
  223. padding: 4px 8px;
  224. background: #ff4444;
  225. color: white;
  226. border: none;
  227. border-radius: 4px;
  228. cursor: pointer;
  229. font-size: 14px;
  230. ">×</button>
  231. </div>
  232. </div>
  233. <div id="contentPanel">
  234. <input type="number" id="iterationCount" min="1" value="5"
  235. style="width: 100%; padding: 8px; margin-bottom: 10px; box-sizing: border-box; border: 1px solid #ddd; border-radius: 4px;">
  236. <div style="display: flex; gap: 10px; margin-bottom: 15px;">
  237. <button id="startButton" style="
  238. flex: 1;
  239. padding: 8px;
  240. background: #4CAF50;
  241. color: white;
  242. border: none;
  243. border-radius: 4px;
  244. cursor: pointer;
  245. transition: background 0.3s;
  246. ">Start</button>
  247. <button id="stopButton" style="
  248. flex: 1;
  249. padding: 8px;
  250. background: #f44336;
  251. color: white;
  252. border: none;
  253. border-radius: 4px;
  254. cursor: pointer;
  255. transition: background 0.3s;
  256. " disabled>Stop</button>
  257. </div>
  258. <div style="
  259. background: #f5f5f5;
  260. padding: 10px;
  261. border-radius: 4px;
  262. font-size: 14px;
  263. ">
  264. <div>Status: <span id="status">Ready</span></div>
  265. <div>Progress: <span id="progress">0/0</span></div>
  266. </div>
  267. </div>
  268. `;
  269.  
  270. document.body.appendChild(container);
  271. this.container = container;
  272. }
  273.  
  274. setupDraggable() {
  275. const container = this.container;
  276. const dragHandle = container.querySelector('#dragHandle');
  277. let isDragging = false;
  278. let currentX;
  279. let currentY;
  280. let initialX;
  281. let initialY;
  282. let xOffset = 0;
  283. let yOffset = 0;
  284.  
  285. // Lưu vị trí vào localStorage
  286. const savePosition = () => {
  287. const position = {
  288. x: xOffset,
  289. y: yOffset
  290. };
  291. localStorage.setItem('autoDeleterPosition', JSON.stringify(position));
  292. };
  293.  
  294. // Khôi phục vị trí từ localStorage
  295. const loadPosition = () => {
  296. const savedPosition = localStorage.getItem('autoDeleterPosition');
  297. if (savedPosition) {
  298. const position = JSON.parse(savedPosition);
  299. xOffset = position.x;
  300. yOffset = position.y;
  301. setTranslate(xOffset, yOffset, container);
  302. }
  303. };
  304.  
  305. const dragStart = (e) => {
  306. if (e.type === "touchstart") {
  307. initialX = e.touches[0].clientX - xOffset;
  308. initialY = e.touches[0].clientY - yOffset;
  309. } else {
  310. initialX = e.clientX - xOffset;
  311. initialY = e.clientY - yOffset;
  312. }
  313.  
  314. if (e.target === dragHandle || e.target.parentElement === dragHandle) {
  315. isDragging = true;
  316. }
  317. };
  318.  
  319. const dragEnd = () => {
  320. isDragging = false;
  321. savePosition(); // Lưu vị trí khi kết thúc kéo
  322. };
  323.  
  324. const drag = (e) => {
  325. if (isDragging) {
  326. e.preventDefault();
  327.  
  328. if (e.type === "touchmove") {
  329. currentX = e.touches[0].clientX - initialX;
  330. currentY = e.touches[0].clientY - initialY;
  331. } else {
  332. currentX = e.clientX - initialX;
  333. currentY = e.clientY - initialY;
  334. }
  335.  
  336. xOffset = currentX;
  337. yOffset = currentY;
  338.  
  339. setTranslate(currentX, currentY, container);
  340. }
  341. };
  342.  
  343. const setTranslate = (xPos, yPos, el) => {
  344. el.style.transform = `translate3d(${xPos}px, ${yPos}px, 0)`;
  345. };
  346.  
  347. // Mouse events
  348. dragHandle.addEventListener('mousedown', dragStart);
  349. document.addEventListener('mousemove', drag);
  350. document.addEventListener('mouseup', dragEnd);
  351.  
  352. // Touch events
  353. dragHandle.addEventListener('touchstart', dragStart);
  354. document.addEventListener('touchmove', drag);
  355. document.addEventListener('touchend', dragEnd);
  356.  
  357. // Minimize/Maximize functionality
  358. const minimizeButton = container.querySelector('#minimizeButton');
  359. const contentPanel = container.querySelector('#contentPanel');
  360. let isMinimized = false;
  361.  
  362. minimizeButton.addEventListener('click', () => {
  363. if (isMinimized) {
  364. contentPanel.style.display = 'block';
  365. minimizeButton.textContent = '_';
  366. } else {
  367. contentPanel.style.display = 'none';
  368. minimizeButton.textContent = '□';
  369. }
  370. isMinimized = !isMinimized;
  371. });
  372.  
  373. // Close functionality
  374. const closeButton = container.querySelector('#closeButton');
  375. closeButton.addEventListener('click', () => {
  376. container.remove();
  377. });
  378.  
  379. // Load saved position when initializing
  380. loadPosition();
  381. }
  382.  
  383. setupEventListeners() {
  384. const startButton = this.container.querySelector('#startButton');
  385. const stopButton = this.container.querySelector('#stopButton');
  386. const iterationInput = this.container.querySelector('#iterationCount');
  387.  
  388. startButton.addEventListener('click', () => {
  389. const count = parseInt(iterationInput.value);
  390. if (count > 0) {
  391. startButton.disabled = true;
  392. stopButton.disabled = false;
  393. this.autoDeleter.start(count);
  394. }
  395. });
  396.  
  397. stopButton.addEventListener('click', () => {
  398. this.autoDeleter.stop();
  399. stopButton.disabled = true;
  400. });
  401.  
  402. window.addEventListener('autoDeleterUpdate', (e) => {
  403. const statusElem = this.container.querySelector('#status');
  404. const progressElem = this.container.querySelector('#progress');
  405. const startButton = this.container.querySelector('#startButton');
  406. const stopButton = this.container.querySelector('#stopButton');
  407.  
  408. progressElem.textContent = `${e.detail.current}/${e.detail.total}`;
  409.  
  410. switch (e.detail.status) {
  411. case 'running':
  412. statusElem.textContent = 'Running';
  413. statusElem.style.color = '#4CAF50';
  414. break;
  415. case 'complete':
  416. statusElem.textContent = 'Complete';
  417. statusElem.style.color = '#2196F3';
  418. startButton.disabled = false;
  419. stopButton.disabled = true;
  420. break;
  421. }
  422. });
  423. }
  424. }
  425.  
  426. // Initialize the UI
  427. const controller = new UIController();
  428.  
  429. console.log(`
  430. Google Photos Auto Delete Script
  431. ------------------------------
  432. UI Controls have been added to the page.
  433. You can:
  434. 1. Set the number of iterations
  435. 2. Click Start to begin
  436. 3. Click Stop to pause after current iteration
  437. 4. Monitor progress in the UI panel
  438. `);