任务列表 + 番茄钟 (Ctrl+T)

一个带有番茄钟的任务列表,浮动在所有页面。使用 Ctrl+T 切换管理面板。

当前为 2025-05-13 提交的版本,查看 最新版本

// ==UserScript==
// @name         ToDo List + Pomodoro Timer (Ctrl+T)
// @name:zh-CN   任务列表 + 番茄钟 (Ctrl+T)
// @namespace    http://tampermonkey.net/
// @version      1.4
// @description  A ToDo List with a Pomodoro Timer that floats on all pages. Use Ctrl+T to toggle manager.
// @description:zh-CN 一个带有番茄钟的任务列表,浮动在所有页面。使用 Ctrl+T 切换管理面板。
// @author       kq
// @license      MIT
// @match        *://*/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @grant        GM_registerMenuCommand
// @grant        GM_addValueChangeListener
// @grant        GM_removeValueChangeListener
// @grant        GM_notification
// ==/UserScript==

(function() {
    'use strict';

    const STORAGE_KEY_TASKS = 'todoPomodoroTasks_v1_kq'; // Added suffix to avoid conflict with old versions
    const STORAGE_KEY_ACTIVE_TIMER = 'todoPomodoroActiveTimer_v1_kq';
    let tasks = [];
    let activeTimerInterval = null;
    let activeTimerChangeListenerId = null;
    let audioContext;

    // --- HTML Elements ---
    let taskManagerPanel, taskNameInput, taskDurationInput, taskListContainer, showExpiredCheckbox;
    let floatingDisplay, floatingTaskName, floatingTimerText, countdownCircle;

    // --- Sound ---
    function getAudioContext() {
        if (!audioContext && (window.AudioContext || window.webkitAudioContext)) {
            audioContext = new (window.AudioContext || window.webkitAudioContext)();
        }
        return audioContext;
    }

    function playAlarm() {
        const ctx = getAudioContext();
        if (!ctx) {
            GM_notification({
                text: "时间到!任务已完成或需要休息。",
                title: "番茄钟提醒",
                silent: false,
                timeout: 7000
            });
            console.warn("AudioContext not supported. Using GM_notification.");
            return;
        }
        const oscillator = ctx.createOscillator();
        const gainNode = ctx.createGain();
        oscillator.connect(gainNode);
        gainNode.connect(ctx.destination);
        oscillator.type = 'sine';
        oscillator.frequency.setValueAtTime(660, ctx.currentTime); // E5
        gainNode.gain.setValueAtTime(0.3, ctx.currentTime);
        oscillator.start();
        setTimeout(() => {
            oscillator.stop();
        }, 700);
         GM_notification({
            text: "时间到!任务已完成或需要休息。",
            title: "番茄钟提醒",
            silent: true,
            timeout: 7000
        });
    }

    // --- Data Management ---
    async function loadTasks() {
        tasks = await GM_getValue(STORAGE_KEY_TASKS, []);
        await renderTaskList(); // Ensure list is rendered after loading
    }

    async function saveTasks() {
        await GM_setValue(STORAGE_KEY_TASKS, tasks);
        await renderTaskList(); // Re-render after saving
    }

    function generateId() {
        return Date.now().toString(36) + Math.random().toString(36).substr(2);
    }

    async function addTask() {
        const name = taskNameInput.value.trim();
        const duration = parseInt(taskDurationInput.value, 10);

        if (name && duration > 0) {
            tasks.push({ id: generateId(), name, duration, done: false, expired: false });
            taskNameInput.value = '';
            taskDurationInput.value = '25';
            await saveTasks();
        } else {
            alert('请输入有效的任务名称和时长(分钟)!');
        }
    }

    async function deleteTask(taskId) {
        tasks = tasks.filter(task => task.id !== taskId);
        const activeTimer = await GM_getValue(STORAGE_KEY_ACTIVE_TIMER);
        if (activeTimer && activeTimer.taskId === taskId) {
            await stopCurrentTimer(false); // Stop but don't mark as expired if deleted task was active
        }
        await saveTasks();
    }

    async function toggleTaskDone(taskId) {
        const task = tasks.find(t => t.id === taskId);
        if (task) {
            task.done = !task.done;
            if (task.done) { // If marking as done
                task.expired = false; // A completed task is not considered expired for display purposes
            }
            await saveTasks();
        }
    }

    // --- UI Rendering ---
    function createTaskManagerUI() {
        taskManagerPanel = document.createElement('div');
        taskManagerPanel.id = 'todo-pomodoro-manager';
        taskManagerPanel.innerHTML = `
            <div class="tp-header">
                <h2>任务管理 & 番茄钟</h2>
                <button id="tp-close-panel" class="tp-close-btn">&times;</button>
            </div>
            <div class="tp-input-group">
                <input type="text" id="tp-task-name" placeholder="任务名称">
                <input type="number" id="tp-task-duration" value="25" min="1" placeholder="时长 (分钟)">
                <button id="tp-add-task-btn">添加任务</button>
            </div>
            <div class="tp-task-list-controls">
                <label><input type="checkbox" id="tp-show-expired-checkbox"> 显示已过期任务</label>
            </div>
            <div id="tp-task-list"></div>
            <p class="tp-shortcut-info">快捷键: Ctrl + T</p>
        `;
        document.body.appendChild(taskManagerPanel);
        taskManagerPanel.style.display = 'none';

        taskNameInput = document.getElementById('tp-task-name');
        taskDurationInput = document.getElementById('tp-task-duration');
        taskListContainer = document.getElementById('tp-task-list');
        showExpiredCheckbox = document.getElementById('tp-show-expired-checkbox');

        document.getElementById('tp-add-task-btn').addEventListener('click', addTask);
        document.getElementById('tp-close-panel').addEventListener('click', toggleTaskManager);
        showExpiredCheckbox.addEventListener('change', renderTaskList);

        makeDraggable(taskManagerPanel, taskManagerPanel.querySelector('.tp-header'));

        // Event delegation for task actions
        taskListContainer.addEventListener('click', async (e) => {
            const target = e.target.closest('button'); // Get the button element
            if (!target || !target.dataset.id) return;

            const taskId = target.dataset.id;

            if (target.classList.contains('tp-start-pomodoro-btn')) {
                await startPomodoroForTask(taskId);
            } else if (target.classList.contains('tp-stop-pomodoro-btn')) {
                await stopCurrentTimer(true); // Mark as expired if stopped manually
            } else if (target.classList.contains('tp-done-btn')) {
                await toggleTaskDone(taskId);
            } else if (target.classList.contains('tp-delete-btn')) {
                await deleteTask(taskId);
            }
        });
    }
    
    function makeDraggable(element, handle) {
        let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
        handle.onmousedown = dragMouseDown;

        function dragMouseDown(e) {
            e = e || window.event;
            e.preventDefault();
            pos3 = e.clientX;
            pos4 = e.clientY;
            document.onmouseup = closeDragElement;
            document.onmousemove = elementDrag;
        }

        function elementDrag(e) {
            e = e || window.event;
            e.preventDefault();
            pos1 = pos3 - e.clientX;
            pos2 = pos4 - e.clientY;
            pos3 = e.clientX;
            pos4 = e.clientY;
            element.style.top = (element.offsetTop - pos2) + "px";
            element.style.left = (element.offsetLeft - pos1) + "px";
        }

        function closeDragElement() {
            document.onmouseup = null;
            document.onmousemove = null;
        }
    }


    async function renderTaskList() {
        if (!taskListContainer) return;

        const shouldShowExpired = showExpiredCheckbox && showExpiredCheckbox.checked;

        const tasksToDisplay = tasks.filter(task => {
            if (task.done) return true;
            if (!task.expired) return true;
            return shouldShowExpired; // If expired and not done, show only if checkbox is checked
        });

        taskListContainer.innerHTML = '';
        if (tasksToDisplay.length === 0) {
            let message = '暂无任务';
            if (tasks.length > 0 && !shouldShowExpired) { // Has tasks, but they are all expired and hidden
                message = '暂无进行中或已完成任务 (可勾选"显示已过期任务")';
            } else if (tasks.length > 0 && shouldShowExpired) { // Has tasks, showing expired, but still list is empty (should not happen if tasks.length > 0)
                 message = '没有符合条件的任务';
            }
            taskListContainer.innerHTML = `<p class="tp-no-tasks">${message}</p>`;
            return;
        }

        const ul = document.createElement('ul');
        const activeTimer = await GM_getValue(STORAGE_KEY_ACTIVE_TIMER);

        tasksToDisplay.forEach(task => {
            const li = document.createElement('li');
            let liClass = '';
            if (task.done) liClass = 'tp-task-done';
            else if (task.expired) liClass = 'tp-task-expired';
            li.className = liClass;

            let timerButtonHTML;
            if (activeTimer && activeTimer.taskId === task.id) {
                timerButtonHTML = `<button class="tp-control-btn tp-stop-pomodoro-btn" data-id="${task.id}">停止计时</button>`;
            } else {
                timerButtonHTML = `<button class="tp-control-btn tp-start-pomodoro-btn" data-id="${task.id}" ${task.done ? 'disabled' : ''}>开始番茄钟</button>`;
            }

            li.innerHTML = `
                <span class="tp-task-name-display">${task.name} (${task.duration} 分钟)</span>
                <div class="tp-task-actions">
                    ${timerButtonHTML}
                    <button class="tp-done-btn" data-id="${task.id}">${task.done ? '撤销完成' : '标记完成'}</button>
                    <button class="tp-delete-btn" data-id="${task.id}">删除</button>
                </div>
            `;
            ul.appendChild(li);
        });
        taskListContainer.appendChild(ul);
    }

    function createFloatingDisplay() {
        floatingDisplay = document.createElement('div');
        floatingDisplay.id = 'tp-floating-display';
        floatingDisplay.innerHTML = `
            <div id="tp-countdown-circle-container">
                 <div id="tp-countdown-circle">
                    <span id="tp-floating-timer-text">--:--</span>
                </div>
            </div>
            <div id="tp-floating-task-details">
                <p id="tp-floating-task-name">无任务</p>
            </div>
        `;
        document.body.appendChild(floatingDisplay);

        floatingTaskName = document.getElementById('tp-floating-task-name');
        floatingTimerText = document.getElementById('tp-floating-timer-text');
        countdownCircle = document.getElementById('tp-countdown-circle');

        updateFloatingDisplayFromStorage();
    }

    function updateFloatingDisplay(taskName, timeLeftFormatted, progressPercent, isRunning) {
        if (!floatingDisplay) return;

        floatingTaskName.textContent = taskName || '无任务';
        floatingTimerText.textContent = timeLeftFormatted || '--:--';

        if (countdownCircle) {
             if (isRunning && progressPercent !== undefined) {
                countdownCircle.style.background = `conic-gradient(#4CAF50 ${progressPercent}%, #ddd ${progressPercent}% 100%)`;
            } else {
                countdownCircle.style.background = `conic-gradient(#ddd 0% 100%)`;
            }
        }
        floatingDisplay.style.display = 'flex'; // Always visible to show "No Task" or timer
    }


    // --- Timer Logic ---
    async function startPomodoroForTask(taskId) {
        const task = tasks.find(t => t.id === taskId);
        if (!task || task.done) return;

        await stopCurrentTimer(false); // Stop any existing timer, don't mark as expired

        const durationSeconds = task.duration * 60;
        const startTimeEpoch = Date.now();
        const activeTimerData = {
            taskId: task.id,
            taskName: task.name,
            startTimeEpoch: startTimeEpoch,
            durationSeconds: durationSeconds,
            originalDurationSeconds: durationSeconds
        };

        await GM_setValue(STORAGE_KEY_ACTIVE_TIMER, activeTimerData);
        // Value change listener will handle UI updates
    }

    function runTimerInterval(timerData) {
        if (activeTimerInterval) clearInterval(activeTimerInterval);

        const { taskId, taskName, startTimeEpoch, durationSeconds, originalDurationSeconds } = timerData;

        activeTimerInterval = setInterval(async () => {
            const now = Date.now();
            const elapsedSeconds = Math.floor((now - startTimeEpoch) / 1000);
            const remainingSeconds = durationSeconds - elapsedSeconds;

            if (remainingSeconds <= 0) {
                clearInterval(activeTimerInterval);
                activeTimerInterval = null;
                playAlarm();

                const task = tasks.find(t => t.id === taskId);
                if (task && !task.done) { // If not already marked done
                    task.expired = true;
                    await saveTasks(); // This will re-render list
                }
                await GM_deleteValue(STORAGE_KEY_ACTIVE_TIMER);
                // updateFloatingDisplay will be handled by GM_addValueChangeListener
            } else {
                const minutes = Math.floor(remainingSeconds / 60);
                const seconds = remainingSeconds % 60;
                const timeLeftFormatted = `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
                const progressPercent = ((originalDurationSeconds - remainingSeconds) / originalDurationSeconds) * 100;
                updateFloatingDisplay(taskName, timeLeftFormatted, progressPercent, true);
            }
        }, 1000);
        // Initial display for the new timer
        updateFloatingDisplay(taskName, formatTime(durationSeconds), 0, true);
    }

    async function stopCurrentTimer(markAsExpired = false) {
        if (activeTimerInterval) {
            clearInterval(activeTimerInterval);
            activeTimerInterval = null;
        }
        const activeTimer = await GM_getValue(STORAGE_KEY_ACTIVE_TIMER);
        if (activeTimer) {
            if (markAsExpired) {
                const task = tasks.find(t => t.id === activeTimer.taskId);
                if (task && !task.done) {
                    task.expired = true;
                    // Don't call saveTasks here directly, GM_deleteValue will trigger update cycle
                    // which will re-render task list with the current 'tasks' array state.
                    // So, ensure 'tasks' array modification is picked up.
                    // We need to save the 'tasks' array state if we modify it here.
                    await GM_setValue(STORAGE_KEY_TASKS, tasks); // Save the modified tasks array
                }
            }
            await GM_deleteValue(STORAGE_KEY_ACTIVE_TIMER);
            // The change listener on STORAGE_KEY_ACTIVE_TIMER will handle UI updates.
            // If task was marked expired and tasks saved, ensure renderTaskList picks it up.
            // The GM_deleteValue will trigger handleActiveTimerChange, which calls renderTaskList.
            // renderTaskList uses the global 'tasks' array, which has been updated if expired.
        }
    }


    function formatTime(totalSeconds) {
        const minutes = Math.floor(totalSeconds / 60);
        const seconds = totalSeconds % 60;
        return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
    }

    async function updateFloatingDisplayFromStorage() {
        const activeTimer = await GM_getValue(STORAGE_KEY_ACTIVE_TIMER);
        if (activeTimer && activeTimer.startTimeEpoch && activeTimer.durationSeconds) {
            const now = Date.now();
            const elapsedSeconds = Math.floor((now - activeTimer.startTimeEpoch) / 1000);
            const remainingSeconds = activeTimer.durationSeconds - elapsedSeconds;

            if (remainingSeconds > 0) {
                const progressPercent = ((activeTimer.originalDurationSeconds - remainingSeconds) / activeTimer.originalDurationSeconds) * 100;
                updateFloatingDisplay(activeTimer.taskName, formatTime(remainingSeconds), progressPercent, true);
                if (!activeTimerInterval) {
                    runTimerInterval(activeTimer);
                }
            } else { // Timer expired while tab might have been inactive
                updateFloatingDisplay(activeTimer.taskName, '00:00', 100, false);
                const task = tasks.find(t => t.id === activeTimer.taskId);
                if (task && !task.done && !task.expired) {
                     task.expired = true;
                     await saveTasks(); // Save and re-render
                }
                await GM_deleteValue(STORAGE_KEY_ACTIVE_TIMER); // Clean up
            }
        } else {
            updateFloatingDisplay(null, null, 0, false);
             if (activeTimerInterval) {
                clearInterval(activeTimerInterval);
                activeTimerInterval = null;
            }
        }
    }


    // --- Event Handlers & Initialization ---
    function toggleTaskManager() {
        if (!taskManagerPanel) createTaskManagerUI();
        const isVisible = taskManagerPanel.style.display === 'block';
        taskManagerPanel.style.display = isVisible ? 'none' : 'block';
        if (!isVisible) {
            loadTasks(); // Refresh task list when opening
        }
    }

    function handleKeyDown(e) {
        // Check if focused on an input field or contentEditable element,
        // but allow if it's our own panel's input fields.
        const activeEl = document.activeElement;
        const isTypingInExternalField = activeEl &&
            (activeEl.tagName === 'INPUT' || activeEl.tagName === 'TEXTAREA' || activeEl.isContentEditable) &&
            activeEl !== taskNameInput && activeEl !== taskDurationInput;

        if (e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey && e.key.toLowerCase() === 't') {
            if (isTypingInExternalField && taskManagerPanel && taskManagerPanel.style.display === 'none') {
                // If panel is hidden and user is typing in a general field,
                // they might not intend to open our panel.
                // However, for simplicity and specific request for Ctrl+T, we'll allow it.
                // Could add a setting later to disable shortcut in input fields.
            }
            e.preventDefault();
            toggleTaskManager();
        }
    }

    async function handleActiveTimerChange(name, oldValue, newValue) {
        if (name === STORAGE_KEY_ACTIVE_TIMER) {
            if (activeTimerInterval) {
                clearInterval(activeTimerInterval);
                activeTimerInterval = null;
            }
            if (newValue && newValue.startTimeEpoch && newValue.durationSeconds) {
                runTimerInterval(newValue);
            } else {
                updateFloatingDisplay(null, null, 0, false); // Clear floating display
            }
            if (taskManagerPanel && taskManagerPanel.style.display === 'block') {
                 await renderTaskList(); // Update buttons in manager panel if open
            }
            // Also ensure floating display is up-to-date based on the new value (or lack thereof)
            await updateFloatingDisplayFromStorage();
        }
    }


    async function init() {
        addStyles();
        createFloatingDisplay(); // Create floating UI first

        // Create task manager UI but keep it hidden initially.
        // This ensures its elements (like showExpiredCheckbox) exist before loadTasks tries to access them for filtering.
        if (!taskManagerPanel) createTaskManagerUI();
        
        await loadTasks(); // Load tasks, which will also call renderTaskList

        document.addEventListener('keydown', handleKeyDown);
        GM_registerMenuCommand("打开/关闭任务番茄钟面板 (Ctrl+T)", toggleTaskManager, "t");
        activeTimerChangeListenerId = GM_addValueChangeListener(STORAGE_KEY_ACTIVE_TIMER, handleActiveTimerChange);
        await updateFloatingDisplayFromStorage(); // Initial sync of floating display

        window.addEventListener('unload', () => {
            if (activeTimerChangeListenerId) {
                GM_removeValueChangeListener(activeTimerChangeListenerId);
            }
            if (activeTimerInterval) {
                clearInterval(activeTimerInterval);
            }
        });
    }

    function addStyles() {
        GM_addStyle(`
            #todo-pomodoro-manager {
                position: fixed;
                top: 50px;
                left: 50%;
                transform: translateX(-50%);
                width: 480px; /* Slightly wider for new checkbox area */
                max-height: 80vh;
                background-color: #f9f9f9;
                border: 1px solid #ccc;
                box-shadow: 0 4px 12px rgba(0,0,0,0.2);
                z-index: 2147483640; /* High z-index */
                font-family: Arial, sans-serif;
                color: #333;
                border-radius: 8px;
                display: flex;
                flex-direction: column;
            }
            #todo-pomodoro-manager .tp-header {
                padding: 10px 15px;
                background-color: #4CAF50;
                color: white;
                border-top-left-radius: 8px;
                border-top-right-radius: 8px;
                display: flex;
                justify-content: space-between;
                align-items: center;
                cursor: move;
            }
            #todo-pomodoro-manager .tp-header h2 { margin: 0; font-size: 1.2em; }
            #todo-pomodoro-manager .tp-close-btn { background: none; border: none; color: white; font-size: 1.5em; cursor: pointer; }
            #todo-pomodoro-manager .tp-input-group { padding: 15px; border-bottom: 1px solid #eee; display: flex; gap: 10px; }
            #todo-pomodoro-manager .tp-input-group input[type="text"] { flex-grow: 1; padding: 8px; border: 1px solid #ddd; border-radius: 4px; }
            #todo-pomodoro-manager .tp-input-group input[type="number"] { width: 80px; padding: 8px; border: 1px solid #ddd; border-radius: 4px; }
            #todo-pomodoro-manager .tp-input-group button { padding: 8px 12px; background-color: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; transition: background-color 0.2s; }
            #todo-pomodoro-manager .tp-input-group button:hover { background-color: #45a049; }
            
            .tp-task-list-controls {
                padding: 10px 15px 5px;
                border-bottom: 1px solid #eee;
                font-size: 0.9em;
            }
            .tp-task-list-controls label { display: flex; align-items: center; gap: 5px; cursor: pointer; }

            #tp-task-list { padding: 0px 15px 15px 15px; overflow-y: auto; flex-grow: 1; min-height: 100px; } /* Added min-height */
            #tp-task-list ul { list-style: none; padding: 0; margin: 0; }
            #tp-task-list li { padding: 10px 5px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; }
            #tp-task-list li:last-child { border-bottom: none; }
            #tp-task-list .tp-task-name-display { flex-grow: 1; font-size: 0.95em; word-break: break-all; }
            #tp-task-list .tp-task-actions button { margin-left: 5px; padding: 5px 8px; font-size: 0.8em; border: 1px solid #ddd; border-radius: 3px; cursor: pointer; white-space: nowrap;}
            #tp-task-list .tp-task-actions .tp-start-pomodoro-btn { background-color: #2196F3; color: white; border-color: #2196F3;}
            #tp-task-list .tp-task-actions .tp-start-pomodoro-btn:disabled { background-color: #aaa; border-color: #aaa; cursor: not-allowed; }
            #tp-task-list .tp-task-actions .tp-stop-pomodoro-btn { background-color: #ff9800; color: white; border-color: #ff9800;}
            #tp-task-list .tp-task-actions .tp-done-btn { background-color: #67c23a; color: white; border-color: #67c23a;}
            #tp-task-list .tp-task-actions .tp-delete-btn { background-color: #f44336; color: white; border-color: #f44336;}
            
            #tp-task-list .tp-task-done .tp-task-name-display { text-decoration: line-through; color: #888; }
            #tp-task-list .tp-task-expired .tp-task-name-display { color: #e74c3c; /*font-style: italic;*/ }
            #tp-task-list .tp-task-expired .tp-task-name-display::after { content: " (已超时)"; color: #e74c3c; font-size: 0.9em; margin-left: 4px;}
            
            #tp-task-list .tp-no-tasks { text-align: center; color: #777; padding: 20px; font-size: 0.9em; }
            .tp-shortcut-info { font-size: 0.8em; color: #777; text-align: center; padding: 5px 15px 10px; border-top: 1px solid #eee; margin-top: auto; }

            #tp-floating-display {
                position: fixed;
                bottom: 20px;
                right: 20px;
                background-color: rgba(255, 255, 255, 0.95);
                border: 1px solid #ccc;
                box-shadow: 0 2px 8px rgba(0,0,0,0.15);
                border-radius: 8px;
                padding: 10px 15px;
                z-index: 2147483639; /* Slightly lower than panel */
                display: flex;
                align-items: center;
                gap: 15px;
                font-family: Arial, sans-serif;
                color: #333;
            }
            #tp-countdown-circle {
                width: 50px;
                height: 50px;
                border-radius: 50%;
                background: conic-gradient(#ddd 0% 100%);
                display: flex;
                justify-content: center;
                align-items: center;
                font-size: 0.9em;
                font-weight: bold;
                color: #333;
                transition: background 0.2s linear;
            }
            #tp-floating-task-details { display: flex; flex-direction: column; align-items: flex-start; }
            #tp-floating-task-name {
                margin: 0;
                font-size: 0.95em;
                font-weight: bold;
                max-width: 180px;
                white-space: nowrap;
                overflow: hidden;
                text-overflow: ellipsis;
            }
        `);
    }

    // --- Start Script ---
    if (window.self === window.top) { // Attempt to run only in top frame to avoid multiple instances in iframes
        init();
    } else {
        // For iframes, potentially only show the floating display if a timer is active globally.
        // For simplicity, current script runs fully in all frames.
        // A more complex setup would be needed for selective iframe operation.
        // The @match *://*/* will run it in all frames.
        // This basic check is a common pattern, though not foolproof.
        // Let's assume user wants it on all pages including iframes for now.
        // If you want to restrict to top frame only for the *main* logic:
        // if (window.self === window.top) { init(); } else { createFloatingDisplay(); /* and sync logic */ }
        // For now, keep it simple as the prompt implies "all pages".
        init();
    }


})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址