Google Photos Auto Delete

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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
`);