Super Luogu Task Plan

超级洛谷任务计划:替换主页原生任务计划模块,支持主页按题号添加/删除,题目页加入/移出,JSON 导入导出,无上限 IndexedDB 存储

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

Advertisement:

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

Advertisement:

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         Super Luogu Task Plan
// @namespace    https://luogu.com.cn/
// @version      2026.4
// @description  超级洛谷任务计划:替换主页原生任务计划模块,支持主页按题号添加/删除,题目页加入/移出,JSON 导入导出,无上限 IndexedDB 存储
// @author       aaa_Pigeon & ChatGPT
// @match        *://www.luogu.com.cn/*
// @match        *://www.luogu.org/*
// @grant        GM_setClipboard
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    /****************************************************************
     * 配置
     ****************************************************************/
    const DB_NAME = 'super_luogu_task_plan_db';
    const DB_VERSION = 4;
    const STORE_NAME = 'tasks';

    const STATUS = {
        TODO: 'todo',
        DOING: 'doing',
        DONE: 'done'
    };

    const PRIORITY = {
        HIGH: 'high',
        NORMAL: 'normal',
        LOW: 'low'
    };

    let db = null;
    let currentPath = location.pathname;

    /****************************************************************
     * IndexedDB
     ****************************************************************/
    function openDB() {
        return new Promise((resolve, reject) => {
            const request = indexedDB.open(DB_NAME, DB_VERSION);

            request.onerror = () => reject(request.error);

            request.onsuccess = () => {
                db = request.result;
                resolve(db);
            };

            request.onupgradeneeded = (e) => {
                const database = e.target.result;
                let store;

                if (!database.objectStoreNames.contains(STORE_NAME)) {
                    store = database.createObjectStore(STORE_NAME, {
                        keyPath: 'id'
                    });
                } else {
                    store = e.target.transaction.objectStore(STORE_NAME);
                }

                if (!store.indexNames.contains('pid')) {
                    store.createIndex('pid', 'pid', { unique: false });
                }

                if (!store.indexNames.contains('pidKey')) {
                    store.createIndex('pidKey', 'pidKey', { unique: false });
                }

                if (!store.indexNames.contains('status')) {
                    store.createIndex('status', 'status', { unique: false });
                }

                if (!store.indexNames.contains('updatedAt')) {
                    store.createIndex('updatedAt', 'updatedAt', { unique: false });
                }
            };
        });
    }

    function getStore(mode = 'readonly') {
        return db.transaction(STORE_NAME, mode).objectStore(STORE_NAME);
    }

    function putTask(task) {
        task.pidKey = normalizePid(task.pid);

        return new Promise((resolve, reject) => {
            const req = getStore('readwrite').put(task);
            req.onsuccess = () => resolve();
            req.onerror = () => reject(req.error);
        });
    }

    function deleteTaskById(id) {
        return new Promise((resolve, reject) => {
            const req = getStore('readwrite').delete(id);
            req.onsuccess = () => resolve();
            req.onerror = () => reject(req.error);
        });
    }

    function getAllTasks() {
        return new Promise((resolve, reject) => {
            const req = getStore().getAll();
            req.onsuccess = () => resolve(req.result || []);
            req.onerror = () => reject(req.error);
        });
    }

    async function getTaskByPid(pid) {
        const pidKey = normalizePid(pid);
        const tasks = await getAllTasks();

        return tasks.find(task => normalizePid(task.pid) === pidKey) || null;
    }

    async function deleteTasksByPid(pid) {
        const pidKey = normalizePid(pid);
        const tasks = await getAllTasks();
        const matched = tasks.filter(task => normalizePid(task.pid) === pidKey);

        for (const task of matched) {
            await deleteTaskById(task.id);
        }

        return matched.length;
    }

    /****************************************************************
     * 工具函数
     ****************************************************************/
    function uuid() {
        return crypto.randomUUID
            ? crypto.randomUUID()
            : String(Date.now()) + Math.random().toString(36).slice(2);
    }

    function now() {
        return Date.now();
    }

    function normalizePid(pid) {
        return String(pid || '').trim().toLowerCase();
    }

    function formatTime(t) {
        if (!t) return '';
        return new Date(t).toLocaleString();
    }

    function escapeHTML(str) {
        return String(str || '')
            .replaceAll('&', '&')
            .replaceAll('<', '&lt;')
            .replaceAll('>', '&gt;')
            .replaceAll('"', '&quot;');
    }

    function decodeHTML(str) {
        const textarea = document.createElement('textarea');
        textarea.innerHTML = str;
        return textarea.value;
    }

    function escapeRegExp(str) {
        return String(str).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    }

    function removeAll(selector) {
        document.querySelectorAll(selector).forEach(el => el.remove());
    }

    function download(filename, content) {
        const blob = new Blob([content], {
            type: 'application/json;charset=utf-8'
        });

        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');

        a.href = url;
        a.download = filename;

        document.body.appendChild(a);
        a.click();
        a.remove();

        setTimeout(() => URL.revokeObjectURL(url), 1000);
    }

    function isHomePage() {
        return location.pathname === '/' || location.pathname === '';
    }

    function isProblemPage() {
        return /^\/problem\/[A-Za-z0-9_]+/i.test(location.pathname);
    }

    function getProblemUrl(pid) {
        return location.origin + '/problem/' + encodeURIComponent(String(pid).trim());
    }

    function parseProblemLines(text) {
        return String(text || '')
            .split(/\n|,|,|;|;/)
            .map(line => line.trim().replace(/^[—–\-•\s]+/, ''))
            .filter(Boolean)
            .map(line => {
                const match = line.match(/^([A-Za-z0-9_]+)(?:\s+(.+))?$/);
                if (!match) return null;

                return {
                    pid: match[1].trim(),
                    title: (match[2] || '').trim()
                };
            })
            .filter(Boolean);
    }

    function cleanProblemTitle(raw, pid) {
        return String(raw || '')
            .replace(/\s*-\s*洛谷.*$/i, '')
            .replace(/\s*洛谷.*$/i, '')
            .replace(new RegExp('^' + escapeRegExp(pid) + '\\s*[-—::]?\\s*', 'i'), '')
            .trim();
    }

    async function tryFetchProblemTitle(pid) {
        try {
            const res = await fetch(getProblemUrl(pid), {
                credentials: 'include'
            });

            if (!res.ok) return '';

            const html = await res.text();
            const titleMatch = html.match(/<title>([\s\S]*?)<\/title>/i);

            if (!titleMatch) return '';

            const title = cleanProblemTitle(decodeHTML(titleMatch[1]), pid);

            return title || '';
        } catch (err) {
            console.warn('[Super Luogu Task Plan] 获取题目标题失败:', pid, err);
            return '';
        }
    }

    /****************************************************************
     * 当前题目识别
     ****************************************************************/
    function getCurrentProblemInfo() {
        const match = location.pathname.match(/\/problem\/([A-Za-z0-9_]+)/i);
        if (!match) return null;

        const pid = match[1];
        let title = document.title || pid;

        const h1 = document.querySelector('h1');
        if (h1 && h1.innerText.trim()) {
            title = h1.innerText.trim();
        }

        title = cleanProblemTitle(title, pid);

        return {
            pid,
            title: title || pid,
            url: getProblemUrl(pid)
        };
    }

    /****************************************************************
     * 样式
     ****************************************************************/
    const style = document.createElement('style');

    style.textContent = `
#sltp-home-panel {
    width: 100%;
    color: #222;
    font-size: 14px;
    background: #fff;
}

#sltp-home-panel * {
    box-sizing: border-box;
}

#sltp-home-panel.sltp-floating-fallback {
    position: fixed;
    right: 24px;
    bottom: 24px;
    width: 330px;
    max-height: 560px;
    z-index: 999999;
    border-radius: 8px;
    box-shadow: 0 4px 22px rgba(0, 0, 0, .18);
    border: 1px solid #e5e5e5;
    overflow: hidden;
}

#sltp-home-header {
    padding: 10px 0 8px 0;
    border-bottom: 1px solid #e5e5e5;
    display: flex;
    justify-content: space-between;
    align-items: center;
    font-weight: bold;
    font-size: 15px;
}

#sltp-home-tools {
    padding: 8px 0;
    border-bottom: 1px solid #e5e5e5;
    background: #fff;
}

#sltp-home-body {
    padding: 6px 0;
    overflow: visible;
    max-height: none;
}

#sltp-home-panel.sltp-floating-fallback #sltp-home-body {
    max-height: 300px;
    overflow-y: auto;
    padding: 8px 10px;
}

#sltp-home-panel.sltp-floating-fallback #sltp-home-header {
    padding: 10px 12px;
    background: #f6f8fa;
}

#sltp-home-panel.sltp-floating-fallback #sltp-home-tools {
    padding: 8px 10px;
    background: #fafafa;
}

.sltp-home-task {
    padding: 5px 0;
    line-height: 1.55;
}

.sltp-home-task a {
    color: #3498db;
    text-decoration: none;
    font-weight: bold;
}

.sltp-home-task a:hover {
    text-decoration: underline;
}

.sltp-home-title {
    display: inline;
    color: #3498db;
    word-break: break-word;
}

.sltp-home-meta {
    margin-left: 2px;
    color: #999;
    font-size: 12px;
}

.sltp-row {
    display: flex;
    gap: 6px;
    margin-bottom: 6px;
}

.sltp-row:last-child {
    margin-bottom: 0;
}

.sltp-btn {
    border: none;
    border-radius: 4px;
    padding: 6px 9px;
    cursor: pointer;
    background: #3498db;
    color: #fff;
    font-size: 12px;
    white-space: nowrap;
}

.sltp-btn:hover {
    opacity: .88;
}

.sltp-btn-gray { background: #666; }
.sltp-btn-green { background: #2e7d32; }
.sltp-btn-red { background: #d32f2f; }

#sltp-id-text,
#sltp-import-text {
    width: 100%;
    resize: vertical;
    font-size: 12px;
    padding: 6px;
    border: 1px solid #ddd;
    border-radius: 4px;
    margin-bottom: 6px;
}

#sltp-id-text {
    height: 52px;
}

#sltp-import-text {
    height: 72px;
}

#sltp-problem-btn {
    display: inline-block;
    margin-left: 8px;
    padding: 8px 16px;
    border-radius: 3px;
    border: none;
    cursor: pointer;
    color: #fff;
    font-size: 14px;
    line-height: 1;
    vertical-align: middle;
}

#sltp-problem-btn.sltp-add {
    background: #2e7d32;
}

#sltp-problem-btn.sltp-remove {
    background: #d32f2f;
}

#sltp-problem-float {
    position: fixed;
    right: 24px;
    bottom: 24px;
    z-index: 999999;
    background: #ffffff;
    border: 1px solid #e5e5e5;
    box-shadow: 0 4px 20px rgba(0,0,0,.18);
    border-radius: 8px;
    padding: 12px;
    width: 230px;
    color: #222;
}

#sltp-problem-float-title {
    font-weight: bold;
    margin-bottom: 8px;
}
`;

    document.head.appendChild(style);

    /****************************************************************
     * 寻找洛谷主页原生“任务计划”模块
     ****************************************************************/
    function findNativeHomeTaskPlanBox() {
        const marked = document.querySelector('[data-sltp-native-task-plan="1"]');

        if (marked && document.body.contains(marked)) {
            return marked;
        }

        const all = [
            ...document.querySelectorAll('div, section, aside, article')
        ];

        const candidates = all.filter(el => {
            if (!el || el.id === 'sltp-home-panel') return false;
            if (el.closest('#sltp-home-panel')) return false;

            const text = normalizeText(el.innerText || el.textContent || '');

            if (!text.includes('任务计划')) return false;

            const rect = el.getBoundingClientRect();

            if (rect.width < 120 || rect.width > 520) return false;
            if (rect.height < 40 || rect.height > 500) return false;

            const looksLikeNativeTaskPlan =
                text.includes('任务计划') &&
                (
                    text.includes('编辑') ||
                    text.includes('随机') ||
                    text.length < 260
                );

            return looksLikeNativeTaskPlan;
        });

        if (!candidates.length) return null;

        candidates.sort((a, b) => {
            const ar = a.getBoundingClientRect();
            const br = b.getBoundingClientRect();

            return ar.width * ar.height - br.width * br.height;
        });

        const target = candidates[0];
        target.dataset.sltpNativeTaskPlan = '1';

        return target;
    }

    function normalizeText(text) {
        return String(text || '').replace(/\s+/g, '');
    }

    /****************************************************************
     * 主页任务计划:替换原生模块
     ****************************************************************/
    async function renderHomePanel() {
        removeAll('#sltp-problem-btn, #sltp-problem-float');
        removeAll('#sltp-home-panel');

        if (!isHomePage()) return;

        const tasks = await getAllTasks();

        tasks.sort((a, b) => {
            const pa = priorityValue(a.priority);
            const pb = priorityValue(b.priority);

            if (pa !== pb) return pb - pa;

            return (b.updatedAt || 0) - (a.updatedAt || 0);
        });

        const panel = document.createElement('div');
        panel.id = 'sltp-home-panel';

        panel.innerHTML = `
<div id="sltp-home-header">
    <span>任务计划</span>
    <span>${tasks.length}</span>
</div>

<div id="sltp-home-tools">
    <textarea id="sltp-id-text" placeholder="按题号添加/删除;支持:P1001 或 P1001 A+B Problem;一行一个"></textarea>

    <div class="sltp-row">
        <button class="sltp-btn sltp-btn-green" id="sltp-home-add-by-id">按题号加入</button>
        <button class="sltp-btn sltp-btn-red" id="sltp-home-delete-by-id">按题号删除</button>
    </div>

    <details>
        <summary style="cursor:pointer;color:#666;margin:4px 0;">JSON 导入 / 导出</summary>

        <div class="sltp-row" style="margin-top:6px;">
            <button class="sltp-btn sltp-btn-gray" id="sltp-export-json">导出</button>
            <button class="sltp-btn sltp-btn-gray" id="sltp-copy-json">复制</button>
            <button class="sltp-btn sltp-btn-green" id="sltp-import-json">导入</button>
        </div>

        <textarea id="sltp-import-text" placeholder="粘贴 JSON 后点导入"></textarea>
    </details>
</div>

<div id="sltp-home-body">
    ${
        tasks.length
            ? tasks.map(task => renderHomeTask(task)).join('')
            : `<div style="color:#888;padding:8px 0;">暂无题目。可以在主页输入题号加入,也可以进入题目页加入。</div>`
    }
</div>
`;

        const nativeBox = findNativeHomeTaskPlanBox();

        if (nativeBox) {
            nativeBox.dataset.sltpNativeTaskPlan = '1';
            nativeBox.innerHTML = '';
            nativeBox.appendChild(panel);
        } else {
            panel.classList.add('sltp-floating-fallback');
            document.body.appendChild(panel);
        }

        document.getElementById('sltp-home-add-by-id').onclick = addByPidFromHome;
        document.getElementById('sltp-home-delete-by-id').onclick = deleteByPidFromHome;
        document.getElementById('sltp-export-json').onclick = exportJSON;
        document.getElementById('sltp-copy-json').onclick = copyJSON;
        document.getElementById('sltp-import-json').onclick = importJSON;
    }

    function renderHomeTask(task) {
        const url = task.url || getProblemUrl(task.pid);

        return `
<div class="sltp-home-task">
    —
    <a href="${escapeHTML(url)}" target="_blank">${escapeHTML(task.pid)}</a>
    <span class="sltp-home-title">${escapeHTML(task.title || task.pid)}</span>
</div>
`;
    }

    async function addByPidFromHome() {
        const text = document.getElementById('sltp-id-text').value;
        const entries = parseProblemLines(text);

        if (!entries.length) {
            alert('请输入题号,例如:P1001 或 CF1738F Connectivity Addicts');
            return;
        }

        let added = 0;
        let skipped = 0;

        for (const entry of entries) {
            const existed = await getTaskByPid(entry.pid);

            if (existed) {
                skipped++;
                continue;
            }

            const fetchedTitle = entry.title ? '' : await tryFetchProblemTitle(entry.pid);
            const title = entry.title || fetchedTitle || entry.pid;

            await putTask({
                id: uuid(),
                pid: entry.pid,
                title,
                url: getProblemUrl(entry.pid),
                status: STATUS.TODO,
                priority: PRIORITY.NORMAL,
                note: '',
                createdAt: now(),
                updatedAt: now()
            });

            added++;
        }

        document.getElementById('sltp-id-text').value = '';
        alert(`加入完成:新增 ${added} 道,已存在 ${skipped} 道`);

        renderHomePanel();
    }

    async function deleteByPidFromHome() {
        const text = document.getElementById('sltp-id-text').value;
        const entries = parseProblemLines(text);

        if (!entries.length) {
            alert('请输入要删除的题号,例如:P1001');
            return;
        }

        const ok = confirm(`确定从任务计划中删除这 ${entries.length} 个题号吗?`);

        if (!ok) return;

        let removed = 0;

        for (const entry of entries) {
            removed += await deleteTasksByPid(entry.pid);
        }

        document.getElementById('sltp-id-text').value = '';
        alert(`删除完成:移出 ${removed} 条任务`);

        renderHomePanel();
    }

    /****************************************************************
     * JSON 导入导出
     ****************************************************************/
    async function exportJSON() {
        const all = await getAllTasks();

        const data = {
            schema: 'super-luogu-task-plan',
            version: '2026.4',
            exportedAt: new Date().toISOString(),
            total: all.length,
            tasks: all
        };

        download(
            `luogu-task-plan-${Date.now()}.json`,
            JSON.stringify(data, null, 2)
        );
    }

    async function copyJSON() {
        const all = await getAllTasks();

        const data = {
            schema: 'super-luogu-task-plan',
            version: '2026.4',
            exportedAt: new Date().toISOString(),
            total: all.length,
            tasks: all
        };

        const text = JSON.stringify(data, null, 2);

        if (typeof GM_setClipboard !== 'undefined') {
            GM_setClipboard(text);
        } else {
            await navigator.clipboard.writeText(text);
        }

        alert('已复制 JSON');
    }

    async function importJSON() {
        const textarea = document.getElementById('sltp-import-text');
        const text = textarea.value.trim();

        if (!text) {
            alert('请先粘贴 JSON');
            return;
        }

        try {
            const parsed = JSON.parse(text);

            const importedTasks = Array.isArray(parsed)
                ? parsed
                : Array.isArray(parsed.tasks)
                    ? parsed.tasks
                    : null;

            if (!importedTasks) {
                alert('JSON 格式错误');
                return;
            }

            const oldTasks = await getAllTasks();
            const oldPidSet = new Set(oldTasks.map(task => normalizePid(task.pid)));

            let added = 0;
            let skipped = 0;

            for (const raw of importedTasks) {
                if (!raw || !raw.pid) continue;

                const pid = String(raw.pid).trim();
                const pidKey = normalizePid(pid);

                if (oldPidSet.has(pidKey)) {
                    skipped++;
                    continue;
                }

                await putTask({
                    id: raw.id || uuid(),
                    pid,
                    title: raw.title || pid,
                    url: raw.url || getProblemUrl(pid),
                    status: raw.status || STATUS.TODO,
                    priority: raw.priority || PRIORITY.NORMAL,
                    note: raw.note || '',
                    createdAt: raw.createdAt || now(),
                    updatedAt: raw.updatedAt || now()
                });

                oldPidSet.add(pidKey);
                added++;
            }

            textarea.value = '';
            alert(`导入成功:新增 ${added} 道,跳过重复 ${skipped} 道`);

            renderHomePanel();
        } catch (err) {
            console.error(err);
            alert('JSON 导入失败');
        }
    }

    /****************************************************************
     * 题目页:加入 / 移出计划
     ****************************************************************/
    async function renderProblemPageButton() {
        removeAll('#sltp-home-panel, #sltp-problem-btn, #sltp-problem-float');

        if (!isProblemPage()) return;

        const problem = getCurrentProblemInfo();

        if (!problem) return;

        const existed = await getTaskByPid(problem.pid);

        const btn = document.createElement('button');

        btn.id = 'sltp-problem-btn';
        btn.className = existed ? 'sltp-remove' : 'sltp-add';
        btn.textContent = existed ? '移出计划' : '加入计划';

        btn.onclick = async () => {
            if (existed) {
                const ok = confirm(`确定把 ${problem.pid} 从任务计划中移出吗?`);

                if (!ok) return;

                await deleteTasksByPid(problem.pid);
                alert('已移出任务计划');
            } else {
                await putTask({
                    id: uuid(),
                    pid: problem.pid,
                    title: problem.title,
                    url: problem.url,
                    status: STATUS.TODO,
                    priority: PRIORITY.NORMAL,
                    note: '',
                    createdAt: now(),
                    updatedAt: now()
                });

                alert('已加入任务计划');
            }

            renderProblemPageButton();
        };

        const insertTarget = findProblemButtonArea();

        if (insertTarget) {
            insertTarget.appendChild(btn);
        } else {
            renderProblemFloat(problem, existed);
        }
    }

    function findProblemButtonArea() {
        const buttons = [...document.querySelectorAll('button, a')];

        const submitBtn = buttons.find(el => {
            const text = (el.innerText || '').trim();
            return text === '提交答案' || text === '提交';
        });

        if (submitBtn && submitBtn.parentElement) {
            return submitBtn.parentElement;
        }

        const addBtn = buttons.find(el => {
            const text = (el.innerText || '').trim();
            return text.includes('加入题单') || text.includes('复制题目');
        });

        if (addBtn && addBtn.parentElement) {
            return addBtn.parentElement;
        }

        const h1 = document.querySelector('h1');

        if (h1 && h1.parentElement) {
            return h1.parentElement;
        }

        return null;
    }

    function renderProblemFloat(problem, existed) {
        const box = document.createElement('div');
        box.id = 'sltp-problem-float';

        box.innerHTML = `
<div id="sltp-problem-float-title">任务计划</div>

<div style="margin-bottom:8px;color:#555;">
    ${escapeHTML(problem.pid)}
</div>

<button class="sltp-btn ${existed ? 'sltp-btn-red' : 'sltp-btn-green'}" id="sltp-problem-float-btn">
    ${existed ? '移出计划' : '加入计划'}
</button>
`;

        document.body.appendChild(box);

        document.getElementById('sltp-problem-float-btn').onclick = async () => {
            if (existed) {
                const ok = confirm(`确定把 ${problem.pid} 从任务计划中移出吗?`);

                if (!ok) return;

                await deleteTasksByPid(problem.pid);
                alert('已移出任务计划');
            } else {
                await putTask({
                    id: uuid(),
                    pid: problem.pid,
                    title: problem.title,
                    url: problem.url,
                    status: STATUS.TODO,
                    priority: PRIORITY.NORMAL,
                    note: '',
                    createdAt: now(),
                    updatedAt: now()
                });

                alert('已加入任务计划');
            }

            renderProblemPageButton();
        };
    }

    /****************************************************************
     * 状态文字
     ****************************************************************/
    function getStatusText(status) {
        if (status === STATUS.DOING) return '进行中';
        if (status === STATUS.DONE) return '已完成';
        return '待做';
    }

    function getPriorityText(priority) {
        if (priority === PRIORITY.HIGH) return '高';
        if (priority === PRIORITY.LOW) return '低';
        return '普通';
    }

    function priorityValue(priority) {
        if (priority === PRIORITY.HIGH) return 3;
        if (priority === PRIORITY.NORMAL) return 2;
        if (priority === PRIORITY.LOW) return 1;
        return 0;
    }

    /****************************************************************
     * 页面刷新与 SPA 路由适配
     ****************************************************************/
    async function refreshUI() {
        if (!db) return;

        if (isHomePage()) {
            await renderHomePanel();
        } else if (isProblemPage()) {
            await renderProblemPageButton();
        } else {
            removeAll('#sltp-home-panel, #sltp-problem-btn, #sltp-problem-float');
        }
    }

    function hookHistory() {
        const rawPushState = history.pushState;
        const rawReplaceState = history.replaceState;

        history.pushState = function () {
            const ret = rawPushState.apply(this, arguments);
            setTimeout(checkRouteChange, 300);
            return ret;
        };

        history.replaceState = function () {
            const ret = rawReplaceState.apply(this, arguments);
            setTimeout(checkRouteChange, 300);
            return ret;
        };

        window.addEventListener('popstate', () => {
            setTimeout(checkRouteChange, 300);
        });

        setInterval(checkRouteChange, 1000);
    }

    function checkRouteChange() {
        if (location.pathname !== currentPath) {
            currentPath = location.pathname;

            setTimeout(refreshUI, 400);
            setTimeout(refreshUI, 1200);
            setTimeout(refreshUI, 2500);
        }
    }

    /****************************************************************
     * 初始化
     ****************************************************************/
    async function init() {
        await openDB();

        hookHistory();

        setTimeout(refreshUI, 500);
        setTimeout(refreshUI, 1500);
        setTimeout(refreshUI, 3000);
        setTimeout(refreshUI, 5000);

        console.log('[Super Luogu Task Plan] Loaded');
    }

    init();
})();