Pixiv图片牌堆生成器

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

安装此脚本
作者推荐脚本

您可能也喜欢Pixiv 增强

安装此脚本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name Pixiv图片牌堆生成器
// @version 1.37
// @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 handleButtonMouseenter(event) {
        // 阻止事件冒泡,尽管在这里可能不那么必要,但保持一致性
        event.stopPropagation();
        const button = event.currentTarget;
        const img = button.imgElement;
        const linkKey = button.linkKey;
        const illustId = button.illustId;
        const pageIndex = button.pageIndex;

        // 直接调用显示菜单的函数
        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('mouseenter', handleButtonMouseenter);
                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);

})();