Letterboxd Batch Mark as Unwatched

Marks all visible "watched" films as "unwatched" from a list/grid view.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Letterboxd Batch Mark as Unwatched
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Marks all visible "watched" films as "unwatched" from a list/grid view.
// @author       0x00a
// @match        https://letterboxd.com/*/films/*
// @match        https://letterboxd.com/*/likes/films/*
// @match        https://letterboxd.com/*/diary/*
// @grant        GM_addStyle
// @license MIT 
// ==/UserScript==

(function() {
    'use strict';

    // --- Configuration ---
    const DELAY_BETWEEN_CLICKS = 500; // 0.5 second delay between each click.

    let isRunning = false;

    // --- Helper function for delays ---
    const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));

    // --- UI Control Panel ---
    function setupControlPanel() {
        if (document.getElementById('unmarker-panel')) return;

        const panelHtml = `
            <div id="unmarker-panel">
                <h3>Batch Mark as Unwatched</h3>
                <p style="font-size: 12px; color: #c25; font-weight: bold;">Important: Scroll down to load all films before starting!</p>
                <div id="unmarker-status">Status: Idle</div>
                <button id="start-unmarker-btn">Start Un-marking</button>
                <button id="stop-unmarker-btn">Stop</button>
            </div>
        `;

        GM_addStyle(`
            #unmarker-panel { position: fixed; top: 70px; right: 20px; z-index: 9999; background: #2C3440; color: #FFF; border: 1px solid #445; padding: 15px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.5); font-family: 'Letterboxd', 'Graphik', sans-serif; width: 250px; }
            #unmarker-panel h3 { margin-top: 0; color: #FFF; }
            #unmarker-panel button { margin-top: 10px; width: 100%; padding: 8px; cursor: pointer; background-color: #E9A100; color: white; border: none; border-radius: 4px; font-weight: bold; }
            #unmarker-panel #stop-unmarker-btn { background-color: #9A231F; }
            #unmarker-status { font-weight: bold; margin-bottom: 10px; padding: 5px; background-color: #1A1F26; border-radius: 3px;}
        `);

        document.body.insertAdjacentHTML('beforeend', panelHtml);
        document.getElementById('start-unmarker-btn').addEventListener('click', startProcess);
        document.getElementById('stop-unmarker-btn').addEventListener('click', stopProcess);
    }

    function updateStatus(message) {
        const statusEl = document.getElementById('unmarker-status');
        if (statusEl) statusEl.textContent = `Status: ${message}`;
    }

    // --- Core Logic ---
    function stopProcess() {
        isRunning = false;
        console.log("Process stopped by user.");
    }

    async function startProcess() {
        if (isRunning) {
            alert("Process is already running.");
            return;
        }
        isRunning = true;

        // Find all "watched" films and then the "eye" icon within them.
        const watchedIcons = document.querySelectorAll('li.film-watched .icon-watched');
        const totalToProcess = watchedIcons.length;

        if (totalToProcess === 0) {
            updateStatus("No watched films found on the page.");
            isRunning = false;
            return;
        }

        for (let i = 0; i < totalToProcess; i++) {
            if (!isRunning) {
                updateStatus(`Stopped by user at item ${i + 1}.`);
                break;
            }

            const icon = watchedIcons[i];
            const filmContainer = icon.closest('li.poster-container');
            const filmName = filmContainer.querySelector('img')?.alt || `Item ${i + 1}`;

            // Scroll the poster into view so we can see the action
            filmContainer.scrollIntoView({ behavior: 'auto', block: 'center' });
            await sleep(100); // Wait briefly for scroll to finish

            updateStatus(`Un-marking "${filmName}" (${i + 1}/${totalToProcess})`);
            icon.click();

            // The main delay between each action
            await sleep(DELAY_BETWEEN_CLICKS);
        }

        if (isRunning) {
             updateStatus(`Finished! Processed ${totalToProcess} films.`);
        }
        isRunning = false;
    }

    // --- Initialization ---
    window.addEventListener('load', () => {
        setupControlPanel();
    });

})();