阅图标记 (Visited Image Marker)

为您点击过的图片链接添加一个可自定义样式的醒目标记,以方便您识别已阅内容。

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

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

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

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

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

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

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         阅图标记 (Visited Image Marker)
// @namespace    RANRAN
// @version      1.0.30
// @description  为您点击过的图片链接添加一个可自定义样式的醒目标记,以方便您识别已阅内容。
// @match        http://*/*
// @match        https://*/*
// @exclude      *://tieba.baidu.com/*
// @exclude      *://hi.baidu.com/*
// @exclude      *://blog.sina.com.cn/*
// @exclude      *://*.blog.sina.com.cn/*
// @exclude      *://www.51.la/*
// @exclude      *://bbs.aicbbs.com/*
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    // 默认配置
    const DEFAULTS = {
        style: 'tag',
        position: 'top-left',
        size: '24',
        offsetX: '5',
        offsetY: '5',
        unreadColor: '#FFFFFF',
        readColor: '#FF0000',
        shadow: true,
        minWidth: '40',
        minHeight: '40',
        siteListMode: 'blacklist',
        siteList: [],
        buttonPos: { x: '15px', y: '15px' },
        showFloatingButton: true,
        enableDimming: true,
        dimmingIntensity: '65',
    };

    // --- 1. 配置与存储管理 ---
    let config = {};
    let processImagesTimeout;
    let visitedLinks = new Set();
    const VISITED_LINKS_KEY = 'readimage_visited_links';
    const VISITED_LINKS_CAP = 2000;
    const SYNC_SAVE_KEY = 'readimage_sync_save';

    function canonicalizeUrl(href) {
        if (typeof href !== 'string' || href.length === 0) return null;
        try {
            const url = new URL(href);
            return url.origin + url.pathname + url.search;
        } catch (e) {
            return href.split('#')[0];
        }
    }

    function loadConfig() {
        const savedConfig = GM_getValue('config', {});
        config = { ...DEFAULTS, ...savedConfig };
    }

    function shouldScriptRun() {
        const currentHost = window.location.hostname;
        if (!config.siteList || config.siteList.length === 0) {
            return config.siteListMode === 'blacklist';
        }
        const isOnList = config.siteList.some(site => currentHost.endsWith(site));
        return config.siteListMode === 'blacklist' ? !isOnList : isOnList;
    }

    loadConfig();

    GM_registerMenuCommand('设置标记样式 (UI)', showSettingsPanel);
    GM_registerMenuCommand('重置设置并清空所有记录', resetConfigAndClearData);
    GM_registerMenuCommand('清除当前网站的已读记录', clearCurrentSiteData);

    if (config.showFloatingButton) {
        createSettingsButton();
    }

    if (!shouldScriptRun()) {
        return;
    }

    function saveConfig() {
        const panel = document.getElementById('readimage-settings-panel');
        if (panel) {
            config.style = panel.querySelector('#style').value;
            config.position = panel.querySelector('#position').value;
            config.size = panel.querySelector('#size').value;
            config.offsetX = panel.querySelector('#offsetX').value;
            config.offsetY = panel.querySelector('#offsetY').value;
            config.unreadColor = panel.querySelector('#unreadColor').value;
            config.readColor = panel.querySelector('#readColor').value;
            config.shadow = panel.querySelector('#shadow').checked;
            config.minWidth = panel.querySelector('#minWidth').value;
            config.minHeight = panel.querySelector('#minHeight').value;
            config.siteListMode = panel.querySelector('input[name="siteListMode"]:checked').value;
            const siteListText = panel.querySelector('#siteListArea').value;
            config.siteList = siteListText.split('\n').map(s => s.trim()).filter(Boolean);
            config.showFloatingButton = panel.querySelector('#showFloatingButton').checked;
            config.enableDimming = panel.querySelector('#enableDimming').checked;
            config.dimmingIntensity = panel.querySelector('#dimmingIntensity').value;
        }
        GM_setValue('config', config);
        alert('设置已保存!部分设置(如站点列表)需要刷新页面才能生效。');
    }

    function loadVisitedDb() {
        const storedLinks = GM_getValue(VISITED_LINKS_KEY, []);
        visitedLinks = new Set(storedLinks);
        try {
            const syncSavedUrl = localStorage.getItem(SYNC_SAVE_KEY);
            if (syncSavedUrl) {
                visitedLinks.add(syncSavedUrl);
                localStorage.removeItem(SYNC_SAVE_KEY);
                saveVisitedDb();
            }
        } catch (e) { console.error('[readimage] Error accessing localStorage:', e); }
    }

    function saveVisitedDb() { let linksToSave = Array.from(visitedLinks); if (linksToSave.length > VISITED_LINKS_CAP) { linksToSave = linksToSave.slice(linksToSave.length - VISITED_LINKS_CAP); } GM_setValue(VISITED_LINKS_KEY, linksToSave); }
    function addLinkToVisited(href) { const canonicalUrl = canonicalizeUrl(href); if (!canonicalUrl || visitedLinks.has(canonicalUrl)) { return; } try { localStorage.setItem(SYNC_SAVE_KEY, canonicalUrl); } catch (e) { console.error('[readimage] Error writing to localStorage:', e); } visitedLinks.add(canonicalUrl); saveVisitedDb(); }
    function removeLinkFromVisited(href) { const canonicalUrl = canonicalizeUrl(href); if (canonicalUrl && visitedLinks.has(canonicalUrl)) { visitedLinks.delete(canonicalUrl); saveVisitedDb(); } }

    function clearCurrentSiteData() {
        const currentOrigin = window.location.origin;
        if (confirm(`确定要清除网站 ${currentOrigin} 的所有已读记录吗?\n此操作不可恢复。`)) {
            const oldSize = visitedLinks.size;
            const newLinks = Array.from(visitedLinks).filter(link => !link.startsWith(currentOrigin));
            visitedLinks = new Set(newLinks);
            saveVisitedDb();
            const removedCount = oldSize - visitedLinks.size;
            alert(`清除了 ${removedCount} 条记录。\n页面即将刷新以应用更改。`);
            location.reload();
        }
    }

    function resetConfigAndClearData() {
        if (confirm('确定要重置所有设置并清空全部已读记录吗?此操作不可恢复。')) {
            config = { ...DEFAULTS };
            GM_setValue('config', config);
            visitedLinks.clear();
            GM_setValue(VISITED_LINKS_KEY, []);
            try { localStorage.removeItem(SYNC_SAVE_KEY); } catch (e) {}
            alert('已重置所有设置和已读记录!请刷新页面。');
            location.reload();
        }
    }

    function applyMarker(link) {
        let marker = link.querySelector('.readimage-marker');
        const isNewMarker = !marker;

        if (isNewMarker) {
            marker = document.createElement('span');
            marker.className = `readimage-marker style-${config.style}`;
            link.appendChild(marker);
        }

        const canonicalUrl = canonicalizeUrl(link.href);
        const isRead = canonicalUrl && visitedLinks.has(canonicalUrl);
        marker.classList.toggle('is-read', isRead);
        link.classList.toggle('is-read', isRead);

        if (config.style === 'tag' && isNewMarker) {
            let leaveTimeout = null;

            marker.addEventListener('mouseenter', () => {
                if (leaveTimeout) {
                    clearTimeout(leaveTimeout);
                    leaveTimeout = null;
                }
                if (link.getAttribute('href')) {
                    link.setAttribute('data-vim-href', link.getAttribute('href'));
                    link.removeAttribute('href');
                }
            });

            marker.addEventListener('mouseleave', () => {
                leaveTimeout = setTimeout(() => {
                    if (link.hasAttribute('data-vim-href')) {
                        link.setAttribute('href', link.getAttribute('data-vim-href'));
                        link.removeAttribute('data-vim-href');
                    }
                }, 100);
            });

            // ▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼
            // 【核心修改】在“捕获”阶段监听'click'事件,并彻底阻止其传播
            // ▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲
            marker.addEventListener('click', (event) => {
                // 彻底阻止事件,不让页面上任何其他脚本(如灯箱)响应
                event.preventDefault();
                event.stopImmediatePropagation();

                const href = link.getAttribute('data-vim-href') || link.href;
                const isCurrentlyRead = link.classList.contains('is-read');

                if (isCurrentlyRead) {
                    link.classList.remove('is-read');
                    marker.classList.remove('is-read');
                    removeLinkFromVisited(href);
                } else {
                    link.classList.add('is-read');
                    marker.classList.add('is-read');
                    addLinkToVisited(href);
                }
                return false;
            }, true); // 末尾的 'true' 开启了事件捕获,这是关键
        }
    }


    function debounceProcessImages() { clearTimeout(processImagesTimeout); processImagesTimeout = setTimeout(processImages, 250); }
    function processImages() { const links = document.querySelectorAll('a:has(img):not(.readimage-processed)'); links.forEach(link => { link.classList.add('readimage-processed'); const img = link.querySelector('img'); if (img) { const checkAndApply = (targetImg) => { if (targetImg.naturalWidth >= config.minWidth && targetImg.naturalHeight >= config.minHeight) { applyMarker(link); } }; img.addEventListener('load', () => checkAndApply(img), { once: true }); if (img.complete) { checkAndApply(img); } } }); }

    function updateStyles() {
        const root = document.documentElement;
        root.style.setProperty('--marker-size', `${config.size}px`);
        root.style.setProperty('--marker-offset-x', `${config.offsetX}px`);
        root.style.setProperty('--marker-offset-y', `${config.offsetY}px`);
        root.style.setProperty('--marker-unread-color', config.unreadColor);
        root.style.setProperty('--marker-read-color', config.readColor);
        const brightnessValue = (100 - config.dimmingIntensity) / 100;
        root.style.setProperty('--dimming-brightness', brightnessValue);
        let positionCSS = '';
        switch (config.position) { case 'top-right': positionCSS = `top: var(--marker-offset-y); right: var(--marker-offset-x);`; break; case 'bottom-left': positionCSS = `bottom: var(--marker-offset-y); left: var(--marker-offset-x);`; break; case 'bottom-right': positionCSS = `bottom: var(--marker-offset-y); right: var(--marker-offset-x);`; break; case 'center': positionCSS = `top: 50%; left: 50%; transform: translate(-50%, -50%);`; break; case 'top-left': default: positionCSS = `top: var(--marker-offset-y); left: var(--marker-offset-x);`; break; }
        const shadowStyle = config.shadow ? 'text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000;' : 'text-shadow: none;';
        const dimmingCSS = config.enableDimming ? ` a.is-read img { filter: brightness(var(--dimming-brightness)); transition: filter 0.3s ease-in-out; } ` : '';
        const finalCSS = `
            a:has(> .readimage-marker) { position: relative !important; display: inherit !important; }
            .readimage-marker { position: absolute; ${positionCSS} z-index: 999; pointer-events: none; transition: all 0.2s ease-in-out; line-height: 1; display: grid; place-items: center; font-weight: bold; ${shadowStyle} }
            .readimage-marker.style-star::before { content: '★'; font-size: var(--marker-size); color: var(--marker-unread-color); }
            .readimage-marker.style-star.is-read::before { color: var(--marker-read-color); }
            .readimage-marker.style-circle::before { content: ''; display: block; width: var(--marker-size); height: var(--marker-size); background-color: var(--marker-unread-color); border-radius: 50%; }
            .readimage-marker.style-circle.is-read::before { background-color: var(--marker-read-color); }
            .readimage-marker.style-tag { background-color: rgba(0, 0, 0, 0.6); color: white; font-size: calc(var(--marker-size) / 2); padding: 0.2em 0.5em; border-radius: 4px; border: 1px solid rgba(255, 255, 255, 0.2); }
            .readimage-marker.style-tag::before { content: '未看'; }
            .readimage-marker.style-tag.is-read { background-color: var(--marker-read-color); color: var(--marker-unread-color); }
            .readimage-marker.style-tag.is-read::before { content: '已看'; }
            .readimage-marker.style-tag {
                cursor: pointer;
                pointer-events: auto;
                transition: transform 0.15s ease, background-color 0.2s ease-in-out;
            }
            .readimage-marker.style-tag:hover {
                transform: scale(1.15);
                z-index: 1000;
            }
            ${dimmingCSS}
        `;
        let styleElement = document.getElementById('readimage-style');
        if (!styleElement) { styleElement = document.createElement('style'); styleElement.id = 'readimage-style'; document.head.appendChild(styleElement); }
        styleElement.textContent = finalCSS;
        document.querySelectorAll('.readimage-marker').forEach(marker => { marker.className = 'readimage-marker'; marker.classList.add(`style-${config.style}`); const link = marker.parentElement; const canonicalUrl = canonicalizeUrl(link.href); const isRead = link.classList.contains('is-read') || (canonicalUrl && visitedLinks.has(canonicalUrl)); marker.classList.toggle('is-read', isRead); });
    }

    function showSettingsPanel() {
        if (document.getElementById('readimage-settings-panel')) return;
        const panel = document.createElement('div');
        panel.id = 'readimage-settings-panel';
        panel.innerHTML = ` <div id="readimage-settings-header"><span>标记样式设置</span><button id="readimage-close-btn" title="关闭">✖</button></div> <div id="readimage-settings-body"> <label>样式:</label> <select id="style"> <option value="star" ${config.style === 'star' ? 'selected' : ''}>五角星 ★</option> <option value="circle" ${config.style === 'circle' ? 'selected' : ''}>圆形 ●</option> <option value="tag" ${config.style === 'tag' ? 'selected' : ''}>标签</option> </select> <label>位置:</label> <select id="position"> <option value="top-left" ${config.position === 'top-left' ? 'selected' : ''}>左上</option> <option value="top-right" ${config.position === 'top-right' ? 'selected' : ''}>右上</option> <option value="bottom-left" ${config.position === 'bottom-left' ? 'selected' : ''}>左下</option> <option value="bottom-right" ${config.position === 'bottom-right' ? 'selected' : ''}>右下</option> <option value="center" ${config.position === 'center' ? 'selected' : ''}>居中</option> </select> <label id="size-label">大小 (px):</label> <input type="range" id="size" min="10" max="50" value="${config.size}"><span class="value-display">${config.size}px</span> <label>水平偏移 (px):</label> <input type="range" id="offsetX" min="-20" max="20" value="${config.offsetX}"><span class="value-display">${config.offsetX}px</span> <label>垂直偏移 (px):</label> <input type="range" id="offsetY" min="-20" max="20" value="${config.offsetY}"><span class="value-display">${config.offsetY}px</span> <label>未读颜色/标签文字:</label> <input type="color" id="unreadColor" value="${config.unreadColor}"> <label>已读颜色:</label> <input type="color" id="readColor" value="${config.readColor}"> <label>为星星/标签加描边:</label> <input type="checkbox" id="shadow" ${config.shadow ? 'checked' : ''}> <hr> <label>已读效果:</label> <div><input type="checkbox" id="enableDimming" ${config.enableDimming ? 'checked' : ''}> <label for="enableDimming" style="margin: 0 0 0 4px;">启用压暗效果</label></div> <label for="dimmingIntensity">压暗强度:</label> <input type="range" id="dimmingIntensity" min="0" max="100" value="${config.dimmingIntensity}"><span class="value-display">${config.dimmingIntensity}%</span> <hr> <label>最小宽度 (px):</label> <input type="range" id="minWidth" min="10" max="200" value="${config.minWidth}"><span class="value-display">${config.minWidth}px</span> <label>最小高度 (px):</label> <input type="range" id="minHeight" min="10" max="200" value="${config.minHeight}"><span class="value-display">${config.minHeight}px</span> <hr> <label>站点管理:</label> <div class="radio-group"> <input type="radio" id="blacklist" name="siteListMode" value="blacklist" ${config.siteListMode === 'blacklist' ? 'checked' : ''}> <label for="blacklist">黑名单模式</label> <input type="radio" id="whitelist" name="siteListMode" value="whitelist" ${config.siteListMode === 'whitelist' ? 'checked' : ''}> <label for="whitelist">白名单模式</label> </div> <label for="siteListArea" style="align-self: start; padding-top: 5px;">网站列表:</label> <textarea id="siteListArea" rows="4" placeholder="每行一个域名,例如&#10;google.com&#10;example.org">${config.siteList.join('\n')}</textarea> <label></label> <p class="settings-help">黑名单:脚本在此列表网站上**禁用**。<br>白名单:脚本**仅**在此列表网站上生效。</p> <hr> <label>界面选项:</label> <div><input type="checkbox" id="showFloatingButton" ${config.showFloatingButton ? 'checked' : ''}> <label for="showFloatingButton" style="margin: 0 0 0 4px;">显示悬浮设置按钮</label></div> </div> <div id="readimage-settings-footer"><button id="readimage-save-btn">保存</button></div> `;
        document.body.appendChild(panel);
        GM_addStyle(` #readimage-settings-panel { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 99999; background: #f0f0f0; border: 1px solid #ccc; border-radius: 8px; box-shadow: 0 4px 15px rgba(0,0,0,0.2); font-family: sans-serif; width: 340px; color: #333; } #readimage-settings-body hr { grid-column: 1 / -1; border: none; border-top: 1px solid #ccc; margin: 5px 0; } #readimage-settings-header { padding: 10px; background: #e0e0e0; border-bottom: 1px solid #ccc; cursor: move; display: flex; justify-content: space-between; align-items: center; border-top-left-radius: 8px; border-top-right-radius: 8px; } #readimage-settings-header span { font-weight: bold; } #readimage-close-btn { background: none; border: none; font-size: 16px; cursor: pointer; } #readimage-settings-body { padding: 15px; display: grid; grid-template-columns: auto 1fr; gap: 10px 5px; align-items: center; max-height: 70vh; overflow-y: auto; } #readimage-settings-body label { font-size: 14px; grid-column: 1 / 2; } #readimage-settings-body > *:not(label):not(hr) { grid-column: 2 / 3; } #readimage-settings-body select, #readimage-settings-body textarea { width: 100%; padding: 4px; box-sizing: border-box; } #readimage-settings-body .value-display { font-family: monospace; } #readimage-settings-body div, #readimage-settings-body .radio-group { display: flex; align-items: center; } #readimage-settings-body input[type="range"] { flex: 1; } #readimage-settings-body input[type="color"] { width: 100%; height: 25px; } #readimage-settings-footer { padding: 10px; background: #e0e0e0; text-align: right; border-top: 1px solid #ccc; border-bottom-left-radius: 8px; border-bottom-right-radius: 8px; } #readimage-settings-footer button { margin-left: 10px; padding: 5px 15px; border: 1px solid #999; border-radius: 4px; cursor: pointer; } #readimage-save-btn { background: #4CAF50; color: white; border-color: #4CAF50; } input:disabled { opacity: 0.5; cursor: not-allowed; } .radio-group label { margin: 0 10px 0 2px; } .settings-help { font-size: 12px; color: #666; margin: 0; } `);
        const enableDimmingCheckbox = panel.querySelector('#enableDimming'); const dimmingSlider = panel.querySelector('#dimmingIntensity'); const dimmingValueDisplay = dimmingSlider.nextElementSibling; function updateSliderState() { dimmingSlider.disabled = !enableDimmingCheckbox.checked; dimmingValueDisplay.style.color = enableDimmingCheckbox.checked ? '' : '#aaa'; dimmingSlider.previousElementSibling.style.color = enableDimmingCheckbox.checked ? '' : '#aaa'; } panel.querySelectorAll('input[type="range"]').forEach(range => { const display = range.nextElementSibling; const container = document.createElement('div'); range.parentNode.insertBefore(container, range); container.appendChild(range); container.appendChild(display); }); const inputs = panel.querySelectorAll('input, select, textarea'); inputs.forEach(input => { input.addEventListener('input', () => { const key = input.id || input.name; const value = input.type === 'checkbox' ? input.checked : input.value; if (key) config[key] = value; if (input.type === 'range') { input.nextElementSibling.textContent = `${value}${input.id === 'dimmingIntensity' ? '%' : 'px'}`; } if (key === 'showFloatingButton') { const button = document.getElementById('readimage-settings-button'); if (config.showFloatingButton) { if (!button) createSettingsButton(); } else { if (button) button.remove(); } return; } if (input.id.includes('siteList')) return; updateStyles(); if (key === 'minWidth' || key === 'minHeight' || key === 'style') { document.querySelectorAll('.readimage-processed').forEach(el => { el.classList.remove('readimage-processed'); const marker = el.querySelector('.readimage-marker'); if (marker) marker.remove(); }); debounceProcessImages(); } updatePanelState(); updateSliderState(); }); }); panel.querySelector('#readimage-save-btn').addEventListener('click', () => { saveConfig(); closeSettingsPanel(); }); panel.querySelector('#readimage-close-btn').addEventListener('click', closeSettingsPanel); updatePanelState(); updateSliderState(); makeDraggable(panel.querySelector('#readimage-settings-header'), panel);
    }

    function updatePanelState() { const panel = document.getElementById('readimage-settings-panel'); if (!panel) return; const currentStyle = panel.querySelector('#style').value; const unreadColorInput = panel.querySelector('#unreadColor'); const shadowCheckbox = panel.querySelector('#shadow'); const sizeLabel = panel.querySelector('#size-label'); const unreadColorLabel = unreadColorInput.previousElementSibling; unreadColorInput.disabled = false; shadowCheckbox.disabled = (currentStyle === 'circle'); if (currentStyle === 'tag') { sizeLabel.textContent = '字号基准 (px):'; unreadColorLabel.textContent = '已读标签文字颜色:'; } else if (currentStyle === 'circle') { sizeLabel.textContent = '直径 (px):'; unreadColorLabel.textContent = '未读颜色:'; } else { sizeLabel.textContent = '大小 (px):'; unreadColorLabel.textContent = '未读颜色:'; } }
    function closeSettingsPanel() { const panel = document.getElementById('readimage-settings-panel'); if (panel) panel.remove(); }
    function makeDraggable(header, panel) { let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0; header.onmousedown = (e) => { if (e.target.id === 'readimage-close-btn') return; panel.style.transform = 'none'; e.preventDefault(); pos3 = e.clientX; pos4 = e.clientY; document.onmouseup = () => { document.onmouseup = null; document.onmousemove = null; }; document.onmousemove = (e) => { e.preventDefault(); pos1 = pos3 - e.clientX; pos2 = pos4 - e.clientY; pos3 = e.clientX; pos4 = e.clientY; panel.style.top = (panel.offsetTop - pos2) + "px"; panel.style.left = (panel.offsetLeft - pos1) + "px"; }; }; }
    function createSettingsButton() { if (document.getElementById('readimage-settings-button')) return; const button = document.createElement('div'); button.id = 'readimage-settings-button'; button.innerHTML = '⚙️'; document.body.appendChild(button); GM_addStyle(` #readimage-settings-button { position: fixed; z-index: 99998; width: 40px; height: 40px; background-color: rgba(0, 0, 0, 0.5); color: white; border-radius: 50%; display: flex; justify-content: center; align-items: center; font-size: 24px; cursor: pointer; transition: background-color 0.2s, transform 0.2s; user-select: none; } #readimage-settings-button:hover { background-color: rgba(0, 0, 0, 0.7); transform: rotate(45deg); } `); button.style.right = config.buttonPos.x; button.style.bottom = config.buttonPos.y; let dragState = {}; button.addEventListener('mousedown', (e) => { if (e.button !== 0) return; dragState = { isDragging: false, startX: e.clientX, startY: e.clientY, btnStartX: parseFloat(button.style.right), btnStartY: parseFloat(button.style.bottom) }; document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); }); function onMouseMove(e) { const dx = e.clientX - dragState.startX; const dy = e.clientY - dragState.startY; if (!dragState.isDragging && Math.sqrt(dx*dx + dy*dy) > 5) { dragState.isDragging = true; } if (dragState.isDragging) { let newX = dragState.btnStartX - dx; let newY = dragState.btnStartY - dy; newX = Math.max(0, Math.min(newX, window.innerWidth - button.offsetWidth)); newY = Math.max(0, Math.min(newY, window.innerHeight - button.offsetHeight)); button.style.right = `${newX}px`; button.style.bottom = `${newY}px`; } } function onMouseUp() { document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); if (dragState.isDragging) { config.buttonPos = { x: button.style.right, y: button.style.bottom }; const currentConfig = GM_getValue('config', DEFAULTS); currentConfig.buttonPos = config.buttonPos; GM_setValue('config', currentConfig); } else { const panel = document.getElementById('readimage-settings-panel'); if (panel) { closeSettingsPanel(); } else { showSettingsPanel(); } } } }

    // --- 4. 脚本初始化 ---
    loadVisitedDb();
    updateStyles();
    debounceProcessImages();

    const observer = new MutationObserver(debounceProcessImages);
    observer.observe(document.body, { childList: true, subtree: true });

    document.body.addEventListener("mousedown", function(event) {
        if (event.target.closest('#readimage-settings-button') || event.target.closest('#readimage-settings-panel')) {
            return;
        }

        if (config.style === 'tag' && event.target.closest('.readimage-marker')) {
            return;
        }

        const link = event.target.closest('a');
        if (!link || !link.querySelector('.readimage-marker')) {
            return;
        }

        if (!link.classList.contains('is-read')) {
            link.classList.add('is-read');
            link.querySelector('.readimage-marker').classList.add('is-read');
            addLinkToVisited(link.href);
        }
    }, true);

})();