Pixiv图片牌堆生成器

缓存链接时,同时抓取插画的来源链接和标题,并在牌堆导出时包含所有信息,用 \n 分隔。小键盘可以实现翻页,有更换需求请自行处理。可以打tag!理论上适配所有pixiv的界面(包括背景图)

当前为 2025-11-04 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name Pixiv图片牌堆生成器
// @version 1.36
// @description 缓存链接时,同时抓取插画的来源链接和标题,并在牌堆导出时包含所有信息,用 \n 分隔。小键盘可以实现翻页,有更换需求请自行处理。可以打tag!理论上适配所有pixiv的界面(包括背景图)
// @author Yog-Sothoth
// @match *://www.pixiv.net/*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_registerMenuCommand
// @grant GM_addStyle
// @grant GM_xmlhttpRequest
// @license MIT
// @run-at document-idle
// @namespace https://greasyfork.org/users/1397928
// ==/UserScript==

(function() {
    'use strict';

    const STORAGE_KEY = 'pixivCatTaggedLinks_JSON';
    const CACHE_NAME = 'PixivCat_Tagged_Export.json';
    const PIXIV_BASE_URL = 'https://www.pixiv.net';

    const IMG_SELECTOR_LIST = 'img[src*="i.pximg.net/c/"]:not([data-tag-processed])';
    const IMG_SELECTOR_ARTWORK = 'img[src*="i.pximg.net/img-original/img"]:not([data-tag-processed])';
    const ANCESTOR_LEVEL = 6;

    const IS_ARTWORK_PAGE = window.location.pathname.includes('/artworks/');
    const IS_TAG_SEARCH_PAGE = window.location.pathname.startsWith('/tags/') && window.location.pathname.includes('/artworks');

    let linkCacheIndex = new Map();

    function parseImgSrc(src) {
        const regex = /img\/(\d{4}\/\d{2}\/\d{2}\/\d{2}\/\d{2}\/\d{2})\/(\d+)_p(\d+)/;
        const match = src.match(regex);

        if (match) {
            return {
                timestamp: match[1],
                illustId: match[2],
                pageIndex: parseInt(match[3], 10),
                pageString: `p${match[3]}`,
                linkKey: `${match[2]}_p${match[3]}`
            };
        }

        if (IS_ARTWORK_PAGE) {
             const parts = src.match(/img\/(\d{4}\/\d{2}\/\d{2}\/\d{2}\/\d{2}\/\d{2})\/(\d+)_p(\d+)/);
             const artworkMatch = window.location.pathname.match(/\/artworks\/(\d+)/);

             if (parts && artworkMatch) {
                 const illustId = artworkMatch[1];
                 const pageString = `p${parts[3]}`;
                 return {
                    timestamp: parts[1],
                    illustId: illustId,
                    pageIndex: parseInt(parts[3], 10),
                    pageString: pageString,
                    linkKey: `${illustId}_${pageString}`
                 };
             }
        }

        return null;
    }

    function buildCacheIndex() {
        linkCacheIndex.clear();
        const data = getCachedData();
        for (const tag in data) {
            for (const item of data[tag]) {
                try {
                    const parsedItem = JSON.parse(item);
                    const realUrl = parsedItem.catLink;

                    const parsedInfo = parseImgSrc(realUrl);
                    if (!parsedInfo) continue;

                    const linkKey = parsedInfo.linkKey;

                    if (!linkCacheIndex.has(linkKey)) {
                        linkCacheIndex.set(linkKey, { realUrl: realUrl, tags: [] });
                    }
                    linkCacheIndex.get(linkKey).tags.push(tag);

                } catch (e) {
                }
            }
        }
    }

    function formatToCQImage(cachedItem) {
        try {
            const data = JSON.parse(cachedItem);
            return (
                `[CQ:image,file=${data.catLink}]` +
                `${data.sourceLink}` +
                `\n${data.title}`
            );
        } catch (e) {
            return `[CQ:image,file=N/A] (Error reading source info for: ${cachedItem})`;
        }
    }

function getIllustrationSourceInfo(imgElement) {
        if (IS_ARTWORK_PAGE) {
            const urlMatch = window.location.pathname.match(/\/artworks\/(\d+)/);
            const sourceLink = urlMatch ? PIXIV_BASE_URL + urlMatch[0] : 'Source: N/A (URL)';

            let title = document.title.replace(/ | - pixiv$/, '').trim();
            if (!title || title.includes('pixiv')) {
                title = imgElement.alt.trim() || 'No Title Found (Alt)';
            }

            return { sourceLink, title };

        } else {
            try {
            const aElement = imgElement.closest('li')
                ?.querySelector(':scope > div:first-child > div:nth-of-type(2) > a[href*="/artworks/"]');
            if (!aElement) {
                return { sourceLink: 'Source: N/A (Selector)', title: 'Title: N/A (Selector)' };
            }
            const relativeHref = aElement.getAttribute('href') || '';
            const sourceLink = relativeHref.startsWith(PIXIV_BASE_URL) ? relativeHref : PIXIV_BASE_URL + relativeHref;
            const title = aElement.textContent.trim() || imgElement.alt || 'No Title Found';

                return { sourceLink, title };

            } catch (e) {
                console.error("Error retrieving list source info:", e);
            }
            return { sourceLink: 'Source: Error', title: 'Title: Error' };
        }
    }
    function getCachedData() {
        try {
            return JSON.parse(GM_getValue(STORAGE_KEY, '{}'));
        } catch (e) {
            return {};
        }
    }

    function setCachedData(data) {
        GM_setValue(STORAGE_KEY, JSON.stringify(data));
    }

    function getLinkTags(linkKey) {
        return linkCacheIndex.get(linkKey)?.tags || [];
    }

    function isLinkCached(linkKey) {
        return linkCacheIndex.has(linkKey);
    }

    function addLinkToTag(itemJsonString, realUrl, tag) {
        const data = getCachedData();
        const finalTag = tag || '未分类';
        let isAdded = false;

        const parsedInfo = parseImgSrc(realUrl);
        if (!parsedInfo) {
            console.error("Could not parse realUrl in addLinkToTag", realUrl);
            return false;
        }
        const linkKey = parsedInfo.linkKey;

        if (!data[finalTag]) {
            data[finalTag] = [];
        }

        const existingInTag = data[finalTag].some(item => {
            try {
                return JSON.parse(item).catLink === realUrl;
            } catch (e) {
                return false;
            }
        });

        if (!existingInTag) {
            data[finalTag].push(itemJsonString);
            setCachedData(data);
            isAdded = true;

            if (!linkCacheIndex.has(linkKey)) {
                linkCacheIndex.set(linkKey, { realUrl: realUrl, tags: [] });
            }
            const tags = linkCacheIndex.get(linkKey).tags;
            if (!tags.includes(finalTag)) {
                tags.push(finalTag);
            }
        }
        return isAdded;
    }

    function removeLinkFromAllCache(linkKey) {
        const cacheEntry = linkCacheIndex.get(linkKey);
        if (!cacheEntry) return false;

        const realUrl = cacheEntry.realUrl;
        const data = getCachedData();
        let removed = false;

        for (const tag in data) {
            const originalLength = data[tag].length;

            data[tag] = data[tag].filter(item => {
                try {
                    return JSON.parse(item).catLink !== realUrl;
                } catch (e) {
                    return true;
                }
            });

            if (data[tag].length < originalLength) {
                removed = true;
            }

            if (data[tag].length === 0) {
                delete data[tag];
            }
        }

        if (removed) {
            setCachedData(data);
            linkCacheIndex.delete(linkKey);
        }
        return removed;
    }

    function clearCache() {
        if (confirm('确定要清空所有缓存的 Pixiv Cat 链接和标签吗?')) {
            GM_deleteValue(STORAGE_KEY);
            linkCacheIndex.clear();
            alert('缓存已清空。页面需要刷新以清除按钮状态。');
            location.reload();
        }
    }

    function createTagButton(isCached) {
        const button = document.createElement('div');
        button.style.cssText = 'position:absolute;top:50%;right:0;transform:translateY(-50%);width:24px;height:24px;border-radius:4px;display:flex;align-items:center;justify-content:center;color:white;font-weight:bold;cursor:pointer;z-index:1000;transition:background-color .2s,transform .1s;font-family:sans-serif;font-size:16px;';

        updateButtonState(button, isCached);
        return button;
    }

    function updateButtonState(button, isCached) {
        button.style.backgroundColor = isCached ? '#4CAF50' : '#2196F3';
        button.innerHTML = '&#43;';
    }

    function updateButtonTitle(button) {
        const linkKey = button.linkKey;
        if (!linkKey) {
             button.title = '无法解析图片ID';
             button.style.backgroundColor = '#FF0000';
             return;
        }

        const tags = getLinkTags(linkKey);
        let title = '';
        if (tags.length === 0) {
            title = '未被任何标签缓存 | 单击添加 | 双击取消所有标签';
        } else {
            title = `已缓存到: ${tags.join(', ')} | 单击添加 | 双击取消所有标签`;
        }
        button.title = title;
        updateButtonState(button, tags.length > 0);
    }

function createTagInputMenu(button, imgElement, linkKey, illustId, pageIndex) {
        const cachedData = getCachedData();
        const existingTags = Object.keys(cachedData);
        const sourceInfo = getIllustrationSourceInfo(imgElement);

        const menu = document.createElement('div');
        menu.id = 'tag-select-menu';

        menu.style.width = '280px';
        menu.style.background = '#333333';
        menu.style.border = '1px solid #222222';
        menu.style.boxShadow = '0 4px 8px rgba(0,0,0,0.5)';
        menu.style.padding = '10px';
        menu.style.borderRadius = '4px';
        menu.style.zIndex = '10001';
        menu.style.color = 'white';
        menu.style.boxSizing = 'border-box';
        menu.style.textAlign = 'left';
        menu.style.fontSize = '13px';

        const currentTags = linkKey ? getLinkTags(linkKey).join(', ') : '无';
        const infoText = linkKey ? `(已存在标签: ${currentTags || '无'})` : '(新图片, 未缓存)';

        const info = document.createElement('div');
        info.innerHTML = `
            <strong style="color: #f0f0f0;">来源:</strong> <span style="word-break: break-all;">${sourceInfo.sourceLink}</span><br>
            <strong style="color: #f0f0f0;">标题:</strong> ${sourceInfo.title}
            <hr style="border-top: 1px solid #999; margin: 8px 0;">
            <i style="font-size: 11px;">${infoText}</i>
        `;
        menu.appendChild(info);

        const input = document.createElement('input');
        input.type = 'text';
        input.placeholder = '选择或输入新标签名';
        input.setAttribute('list', 'pixiv-tag-list');
        input.style.width = '100%';
        input.style.padding = '4px';
        input.style.marginTop = '10px';
        input.style.marginBottom = '8px';
        input.style.border = '1px solid #495057';
        input.style.backgroundColor = '#f8f9fa';
        input.style.color = '#212529';
        input.style.borderRadius = '3px';
        input.style.boxSizing = 'border-box';

        const datalist = document.createElement('datalist');
        datalist.id = 'pixiv-tag-list';
        existingTags.forEach(tag => {
            const option = document.createElement('option');
            option.value = tag;
            datalist.appendChild(option);
        });

        const confirmButton = document.createElement('button');
        confirmButton.textContent = '添加并缓存到标签';

        const baseButtonColor = '#4CAF50';
        const hoverButtonColor = '#45a049';
        const disabledButtonColor = '#555';

        confirmButton.style.width = '100%';
        confirmButton.style.background = baseButtonColor;
        confirmButton.style.color = 'white';
        confirmButton.style.border = 'none';
        confirmButton.style.padding = '5px 10px';
        confirmButton.style.cursor = 'pointer';
        confirmButton.style.borderRadius = '3px';
        confirmButton.style.transition = 'background-color .2s';
        confirmButton.style.marginTop = '9px';

        confirmButton.onmouseover = () => {
            if (!confirmButton.disabled) {
                confirmButton.style.background = hoverButtonColor;
            }
        };
        confirmButton.onmouseout = () => {
            if (!confirmButton.disabled) {
                confirmButton.style.background = baseButtonColor;
            }
        };

        menu.appendChild(datalist);
        menu.appendChild(input);
        menu.appendChild(confirmButton);

        confirmButton.onclick = async () => {
            input.style.borderColor = '#495057';
            const tag = input.value.trim();

            if (!tag) {
                input.style.borderColor = 'red';
                return;
            }

            if (!illustId) {
                console.error("没有 illustId, 无法获取真实链接。");
                alert("错误:无法从此图片解析 illustId。");
                return;
            }

            confirmButton.disabled = true;
            confirmButton.style.background = disabledButtonColor;
            confirmButton.style.cursor = 'wait';
            confirmButton.textContent = '正在获取真实链接...';

            let finalUrlToCache;

            try {
                const response = await new Promise((resolve, reject) => {
                    GM_xmlhttpRequest({
                        method: "GET",
                        url: `/ajax/illust/${illustId}/pages`,
                        onload: (res) => resolve(res),
                        onerror: (err) => reject(err)
                    });
                });

                const json = JSON.parse(response.responseText);

                if (json.error || !json.body) throw new Error(json.message || 'API error');

                const pageData = json.body[pageIndex];
                if (!pageData) throw new Error(`Page index ${pageIndex} not found in API response`);

                const realOriginalUrl = pageData.urls.original;
                finalUrlToCache = realOriginalUrl.replace('i.pximg.net', 'i.pixiv.cat');

            } catch (e) {
                console.error('Failed to fetch real URL', e);
                alert(`获取真实链接失败: ${e.message}\n请稍后再试。`);

                confirmButton.disabled = false;
                confirmButton.style.background = baseButtonColor;
                confirmButton.style.cursor = 'pointer';
                confirmButton.textContent = '添加并缓存到标签';
                return;
            }

            confirmButton.disabled = false;
            confirmButton.style.background = baseButtonColor;
            confirmButton.style.cursor = 'pointer';
            confirmButton.textContent = '添加并缓存到标签';

            const itemToCache = JSON.stringify({
                catLink: finalUrlToCache,
                sourceLink: sourceInfo.sourceLink,
                title: sourceInfo.title
            });

            addLinkToTag(itemToCache, finalUrlToCache, tag);

            const newParsedInfo = parseImgSrc(finalUrlToCache);
            if (newParsedInfo) {
                 button.linkKey = newParsedInfo.linkKey;
            }

            updateButtonTitle(button);

            button.innerHTML = '&#10003;';
            setTimeout(() => {
                button.innerHTML = '&#43;';
            }, 1000);

            menu.remove();
        };

        setTimeout(() => {
            const clickHandler = (e) => {
                if (!menu.contains(e.target) && e.target !== button && e.target !== input) {
                    menu.remove();
                    document.removeEventListener('click', clickHandler);
                }
            };
            document.addEventListener('click', clickHandler);
        }, 10);

        return menu;
    }
    function showTagInputMenu(button, imgElement, linkKey, illustId, pageIndex) {
        if (document.getElementById('tag-select-menu')) {
            document.getElementById('tag-select-menu').remove();
        }

        const menu = createTagInputMenu(button, imgElement, linkKey, illustId, pageIndex);

        const rect = button.getBoundingClientRect();
        document.body.appendChild(menu);

        const menuWidth = menu.offsetWidth;
        let left = rect.left - menuWidth - 10;
        if (left < 10) {
            left = rect.right + 10;
        }

        menu.style.position = 'fixed';
        menu.style.top = `${rect.top + rect.height / 2}px`;
        menu.style.left = `${left}px`;
        menu.style.right = 'auto';
            menu.style.background = '#333333';

        menu.querySelector('input').focus();
    }

    function handleButtonClick(event) {
        event.stopPropagation();
        const button = event.currentTarget;
        const img = button.imgElement;
        const linkKey = button.linkKey;
        const illustId = button.illustId;
        const pageIndex = button.pageIndex;

        if (event.detail === 1) {
            showTagInputMenu(button, img, linkKey, illustId, pageIndex);
        }
    }

    function handleButtonDoubleClick(event) {
        event.stopPropagation();
        const button = event.currentTarget;
        const linkKey = button.linkKey;
        if (!linkKey) return;

        const realUrl = linkCacheIndex.get(linkKey)?.realUrl || "未知链接 (Key: " + linkKey + ")";

        if (confirm(`确定要从所有标签中移除该链接吗?\n(链接: ${realUrl})\n当前标签: ${getLinkTags(linkKey).join(', ') || '无'}`)) {
            if (removeLinkFromAllCache(linkKey)) {
            } else {
            }
            updateButtonTitle(button);
        }
    }

    function handleButtonMouseOver(event) {
        const button = event.currentTarget;
        updateButtonTitle(button);
    }

    function findAncestor(element, n) {
        let ancestor = element;
        for (let i = 0; i < n; i++) {
            if (ancestor) {
                ancestor = ancestor.parentElement;
            } else {
                return null;
            }
        }
        return ancestor;
    }

    function getIllustIdFromImg(img) {
         if (IS_ARTWORK_PAGE) {
            const match = window.location.pathname.match(/\/artworks\/(\d+)/);
            if (match) return match[1];
        }

        try {
            let ancestor = img.closest('li') || img.closest('a[href*="/artworks/"]');
            if (ancestor && !ancestor.href) {
                 ancestor = ancestor.querySelector('a[href*="/artworks/"]');
            }

            if (ancestor && ancestor.href) {
                 const match = ancestor.href.match(/\/artworks\/(\d+)/);
                 if (match) return match[1];
            }

            let parent = img.parentElement;
            for (let i = 0; i < 6; i++) {
                if (!parent) break;
                let aTag = parent.querySelector('a[href*="/artworks/"]');
                if (aTag && aTag.href) {
                    const match = aTag.href.match(/\/artworks\/(\d+)/);
                    if (match) return match[1];
                }
                parent = parent.parentElement;
            }

        } catch(e) {}

        const parsedInfo = parseImgSrc(img.src);
        if (parsedInfo) return parsedInfo.illustId;

        return null;
    }

    function processImages() {
        let selectors = [IMG_SELECTOR_LIST];
        if (IS_ARTWORK_PAGE) {
            selectors.push(IMG_SELECTOR_ARTWORK);
        }

        let images = [];
        selectors.forEach(selector => {
            images.push(...document.querySelectorAll(selector));
        });

        images.forEach(img => {
            img.setAttribute('data-tag-processed', 'true');

            let targetAncestor;

            if (IS_ARTWORK_PAGE) {
                let aParent = img.closest('a');
                if (aParent) {
                    targetAncestor = findAncestor(aParent, 2);
                }

                if (!targetAncestor) {
                    targetAncestor = img.parentElement;
                    if (targetAncestor && window.getComputedStyle(targetAncestor).position === 'static') {
                        targetAncestor = targetAncestor.parentElement;
                    }
                }

            } else {
                targetAncestor = findAncestor(img, ANCESTOR_LEVEL);
            }

            if (!targetAncestor) {
                return;
            }

            const originalSrc = img.src;
            const parsedInfo = parseImgSrc(originalSrc);

            const illustId = (parsedInfo ? parsedInfo.illustId : null) || getIllustIdFromImg(img);
            const pageIndex = parsedInfo ? parsedInfo.pageIndex : 0;
            const linkKey = parsedInfo ? parsedInfo.linkKey : (illustId ? `${illustId}_p${pageIndex}` : null);

            const isCached = linkKey ? isLinkCached(linkKey) : false;

            const button = createTagButton(isCached);
            button.imgElement = img;
            button.linkKey = linkKey;
            button.illustId = illustId;
            button.pageIndex = pageIndex;

            if (IS_ARTWORK_PAGE) {
                button.style.width = '34px';
                button.style.height = '34px';
                button.style.fontSize = '20px';
            }

            updateButtonTitle(button);

            if (window.getComputedStyle(targetAncestor).position === 'static') {
                targetAncestor.style.position = 'relative';
          }

            if (!targetAncestor.querySelector('div[data-state]')) {
                button.addEventListener('click', handleButtonClick);
                button.addEventListener('dblclick', handleButtonDoubleClick);
                button.addEventListener('mouseover', handleButtonMouseOver);
                targetAncestor.appendChild(button);
            }
        });
    }

    function exportTagToJson(tagToExport) {
        const data = getCachedData();
        const linksData = data[tagToExport];

        if (!linksData || linksData.length === 0) {
            alert(`标签 "${tagToExport}" 下没有缓存链接可导出。`);
            return;
        }

        const jsonContent = {
            "_title": ["pixiv图片转载"],
            "_author": ["Yog-Sothoth"],
            "_date": [new Date().toISOString().slice(0, 10).replace(/-/g, '/')],
            "_version": ["1.0"],
        };

        jsonContent[tagToExport] = linksData.map(formatToCQImage);
        const content = JSON.stringify(jsonContent, null, 2);

        const blob = new Blob([content], { type: 'application/json' });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = `${tagToExport}_${CACHE_NAME}`;
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(url);

        const menu = document.getElementById('tag-export-menu');
        if (menu) menu.remove();
        alert(`标签 "${tagToExport}" 的 JSON 文件已生成并开始下载。`);
    }

    function exportAllTagsToJson() {
        const data = getCachedData();
        const allTags = Object.keys(data);

        if (allTags.length === 0) {
            alert('缓存中没有任何数据可导出。');
            return;
        }

        const jsonContent = {
            "_title": ["pixiv图片转载"],
            "_author": ["Yog-Sothoth"],
            "_date": [new Date().toISOString().slice(0, 10).replace(/-/g, '/')],
            "_version": ["1.0"],
        };

        allTags.forEach(tag => {
            const linksData = data[tag];
            if (linksData && linksData.length > 0) {
                jsonContent[tag] = linksData.map(formatToCQImage);
            }
        });

        const content = JSON.stringify(jsonContent, null, 2);

        const blob = new Blob([content], { type: 'application/json' });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = `All_Tags_${CACHE_NAME}`;
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(url);

        const menu = document.getElementById('tag-export-menu');
        if (menu) menu.remove();
        alert('包含所有标签的 JSON 文件已生成并开始下载。');
    }

    function createExportMenuUI() {
        const existingMenu = document.getElementById('tag-export-menu');
        if (existingMenu) {
            existingMenu.remove();
            return;
        }

        const data = getCachedData();
        const tags = Object.keys(data).filter(tag => data[tag].length > 0);

        if (Object.keys(data).length === 0) {
            alert('缓存中没有可导出的标签。');
            return;
        }

        const menu = document.createElement('div');
        menu.id = 'tag-export-menu';
        menu.style.cssText = 'position: fixed; top: 10%; left: 50%; transform: translateX(-50%); background: #f9f9f9; border: 1px solid #ccc; box-shadow: 0 5px 15px rgba(0,0,0,0.3); padding: 15px; border-radius: 8px; z-index: 9999; max-height: 80vh; overflow-y: auto;';

        const title = document.createElement('h3');
        title.textContent = '⬇️ 选择要导出的标签 (JSON)';
        title.style.cssText = 'margin-top: 0; border-bottom: 1px solid #ddd; padding-bottom: 5px; color: #333;';
        menu.appendChild(title);

        const exportAllButton = document.createElement('button');
        exportAllButton.textContent = '➡️ 导出所有标签 (JSON)';
        exportAllButton.style.cssText = 'display: block; width: 100%; padding: 10px; margin: 10px 0; background-color: #28a745; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; transition: background-color 0.2s;';

        exportAllButton.onmouseover = () => exportAllButton.style.backgroundColor = '#218838';
        exportAllButton.onmouseout = () => exportAllButton.style.backgroundColor = '#28a745';
        exportAllButton.onclick = exportAllTagsToJson;

        menu.appendChild(exportAllButton);

        const separator = document.createElement('hr');
        separator.style.cssText = 'border: 0; border-top: 1px solid #ddd; margin: 10px 0;';
        menu.appendChild(separator);

        if (tags.length > 0) {
            tags.forEach(tag => {
                const count = data[tag].length;
                const button = document.createElement('button');
                button.textContent = `${tag} (${count} links)`;
                button.style.cssText = 'display: block; width: 100%; padding: 8px; margin: 5px 0; background-color: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; transition: background-color 0.2s;';

                button.onmouseover = () => button.style.backgroundColor = '#0056b3';
                button.onmouseout = () => button.style.backgroundColor = '#007bff';
                button.onclick = () => exportTagToJson(tag);

                menu.appendChild(button);
            });
        } else {
            const noTagsText = document.createElement('p');
            noTagsText.textContent = '没有找到单独的标签。';
            noTagsText.style.cssText = 'color: #555; font-style: italic; text-align: center;';
            menu.appendChild(noTagsText);
        }


        const closeButton = document.createElement('button');
        closeButton.textContent = '关闭';
        closeButton.style.cssText = 'display: block; width: 100%; padding: 8px; margin-top: 15px; background-color: #6c757d; color: white; border: none; border-radius: 4px; cursor: pointer;';
        closeButton.onclick = () => menu.remove();
        menu.appendChild(closeButton);

        document.body.appendChild(menu);
    }

    function registerMenuCommands() {
        GM_registerMenuCommand('⬇️ 导出标签链接为 JSON', createExportMenuUI);
        GM_registerMenuCommand('🗑️ 清空所有缓存链接和标签', clearCache);
    }

    function quickPageTurn(step) {
        const url = new URL(window.location.href);
        const params = url.searchParams;
        let currentPage = parseInt(params.get('p'), 10);
        if (isNaN(currentPage) || currentPage < 1) {
            currentPage = 1;
        }
        let newPage = currentPage + step;
        if (newPage < 1) {
            newPage = 1;
        }
        params.set('p', newPage);
        const newUrl = url.origin + url.pathname + '?' + params.toString() + url.hash;
        window.location.href = newUrl;
    }

    function handleKeydown(event) {
        const activeElement = document.activeElement;
        if (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA') {
            return;
        }
        switch (event.code) {
            case 'Numpad4':
                event.preventDefault();
                quickPageTurn(-1);
                break;
            case 'Numpad6':
                event.preventDefault();
                quickPageTurn(1);
                break;
        }
    }

    function observeDOMChanges() {
        processImages();

        setTimeout(processImages, 2000);

        const observer = new MutationObserver((mutationsList, observer) => {
            for (const mutation of mutationsList) {
                if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                    setTimeout(processImages, 100);
                    break;
                }
            }
        });
        observer.observe(document.body, { childList: true, subtree: true });
    }

    buildCacheIndex();
    registerMenuCommands();
    observeDOMChanges();
    window.addEventListener('keydown', handleKeydown);

})();