Google Photos Auto Delete

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

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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
`);