// ==UserScript== // @name 阅图标记 (Visited Image Marker) // @namespace RANRAN // @version 1.0.27 // @description 帮助你分辨哪些图片已经点击过;已读标识可自定义样式(五角星\点\标签\压暗);支持黑白名单功能;可自定义调整(px)生效范围以取消过小图片内容应用该标记效果。可配合【阅图标记 (边框标记版)】使用,安装脚本后页面右下角添加齿轮悬浮按钮点击打开功能界面 // @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 // @downloadURL https://update.greasyfork.icu/scripts/543777/%E9%98%85%E5%9B%BE%E6%A0%87%E8%AE%B0%20%28Visited%20Image%20Marker%29.user.js // @updateURL https://update.greasyfork.icu/scripts/543777/%E9%98%85%E5%9B%BE%E6%A0%87%E8%AE%B0%20%28Visited%20Image%20Marker%29.meta.js // ==/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', // [新增] 压暗强度 (0-100) }; // --- 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 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); if (config.showFloatingButton) { createSettingsButton(); } if (!shouldScriptRun()) { return; } function saveConfig() { const panel = document.getElementById('readimage-settings-panel'); if (panel) { // 从UI面板同步所有设置到config对象 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 canonicalizeUrl(href) { if (typeof href !== 'string' || href.length === 0) return null; try { const url = new URL(href); return url.origin + url.pathname; } catch (e) { return href.split('?')[0].split('#')[0]; } } 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 resetConfigAndClearData() { if (confirm('确定要重置所有设置并清空已读记录吗?此操作不可恢复。')) { config = { ...DEFAULTS }; GM_setValue('config', config); visitedLinks.clear(); GM_setValue(VISITED_LINKS_KEY, []); try { localStorage.removeItem(SYNC_SAVE_KEY); } catch (e) {} document.querySelectorAll('.readimage-marker').forEach(m => m.remove()); document.querySelectorAll('.readimage-processed, .is-read').forEach(el => el.classList.remove('readimage-processed', 'is-read')); updateStyles(); processImages(); let button = document.getElementById('readimage-settings-button'); if (DEFAULTS.showFloatingButton) { if (!button) createSettingsButton(); button = document.getElementById('readimage-settings-button'); button.style.right = DEFAULTS.buttonPos.x; button.style.bottom = DEFAULTS.buttonPos.y; } else { if (button) button.remove(); } if (document.getElementById('readimage-settings-panel')) { closeSettingsPanel(); showSettingsPanel(); } alert('已重置所有设置和已读记录!请刷新页面。'); } } function debounceProcessImages() { clearTimeout(processImagesTimeout); processImagesTimeout = setTimeout(processImages, 250); } function applyMarker(link) { if (link.querySelector('.readimage-marker')) { const marker = link.querySelector('.readimage-marker'); const canonicalUrl = canonicalizeUrl(link.href); const isRead = canonicalUrl && visitedLinks.has(canonicalUrl); marker.classList.toggle('is-read', isRead); link.classList.toggle('is-read', isRead); // 同样为父级更新is-read状态 return; } const marker = document.createElement('span'); marker.className = `readimage-marker style-${config.style}`; link.appendChild(marker); const canonicalUrl = canonicalizeUrl(link.href); if (canonicalUrl && visitedLinks.has(canonicalUrl)) { marker.classList.add('is-read'); link.classList.add('is-read'); // 为父级添加is-read状态 } } 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); // [新增] 计算并设置压暗效果的CSS变量 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;'; // [新增] 压暗效果的CSS规则 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: '已看'; } ${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 = `
标记样式设置
${config.size}px ${config.offsetX}px ${config.offsetY}px
${config.dimmingIntensity}%
${config.minWidth}px ${config.minHeight}px

黑名单:脚本在此列表网站上**禁用**。
白名单:脚本**仅**在此列表网站上生效。


`; 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; } #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; } #readimage-reset-btn { background: #f44336; color: white; border-color: #f44336; } 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-reset-btn').addEventListener('click', resetConfigAndClearData); 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; const link = event.target.closest('a'); if (!link || !link.querySelector('.readimage-marker')) return; // 为父级
和标记都添加is-read,确保压暗和标记样式同时生效 link.classList.add('is-read'); const marker = link.querySelector('.readimage-marker'); if(marker) marker.classList.add('is-read'); addLinkToVisited(link.href); }, true); })();