Pixiv图片牌堆生成器

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

安裝腳本?
作者推薦腳本

您可能也會喜歡 Pixiv 增強

安裝腳本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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);

})();