// ==UserScript== // @name LightMode - 暗色模式转亮色模式 // @namespace lightmode-atseiunsky // @version 1.0.0 // @description 将特定网站从暗色模式转换为亮色模式,支持记录网站、实时切换、精细化颜色反转 // @author atSeiunSky // @match *://*/* // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @grant GM_addStyle // @run-at document-start // @noframes // @license MIT // @downloadURL https://update.greasyfork.icu/scripts/566453/LightMode%20-%20%E6%9A%97%E8%89%B2%E6%A8%A1%E5%BC%8F%E8%BD%AC%E4%BA%AE%E8%89%B2%E6%A8%A1%E5%BC%8F.user.js // @updateURL https://update.greasyfork.icu/scripts/566453/LightMode%20-%20%E6%9A%97%E8%89%B2%E6%A8%A1%E5%BC%8F%E8%BD%AC%E4%BA%AE%E8%89%B2%E6%A8%A1%E5%BC%8F.meta.js // ==/UserScript== (function () { 'use strict'; /* ============================================================ * 常量 & 默认配置 * ============================================================ */ const STORAGE_KEY = 'lightmode_data'; const DEFAULT_DATA = { sites: {}, globalSettings: { intensity: 1.0, preserveImages: true, preserveVideos: true, }, }; /* ============================================================ * 存储工具 * ============================================================ */ function loadData() { try { const raw = GM_getValue(STORAGE_KEY, null); if (!raw) return structuredClone(DEFAULT_DATA); const d = typeof raw === 'string' ? JSON.parse(raw) : raw; return { ...DEFAULT_DATA, ...d }; } catch { return structuredClone(DEFAULT_DATA); } } function saveData(data) { GM_setValue(STORAGE_KEY, JSON.stringify(data)); } function currentHost() { return location.hostname; } function isSiteEnabled(data, host) { // 精确匹配 if (data.sites[host]?.enabled) return true; // 通配符匹配(*.example.com) for (const [pattern, cfg] of Object.entries(data.sites)) { if (!cfg.enabled) continue; if (pattern.startsWith('*.')) { const suffix = pattern.slice(1); // .example.com if (host.endsWith(suffix) || host === pattern.slice(2)) return true; } } return false; } /* ============================================================ * CSS Filter 引擎 * ============================================================ */ const LIGHT_MODE_CSS_ID = 'lightmode-global-css'; const EXEMPT_SELECTOR = [ 'img', 'video', 'canvas', 'picture', 'svg image', '[style*="background-image"]', 'iframe', '.lightmode-panel', // 控制面板本身 '#lightmode-panel-container', ].join(', '); function buildCSS(intensity, preserveImages, preserveVideos) { const inv = intensity.toFixed(2); let exemptParts = ['canvas', 'iframe', '.lightmode-panel', '#lightmode-panel-container']; if (preserveImages) exemptParts.push('img', 'picture', 'svg image', 'video[poster]', '[style*="background-image"]'); if (preserveVideos) exemptParts.push('video'); const exemptSelector = exemptParts.join(', '); return ` html.lightmode-active { filter: invert(${inv}) hue-rotate(180deg) !important; -webkit-filter: invert(${inv}) hue-rotate(180deg) !important; background-color: #fff !important; } html.lightmode-active ${exemptSelector} { filter: invert(${inv}) hue-rotate(180deg) !important; -webkit-filter: invert(${inv}) hue-rotate(180deg) !important; } /* 保证嵌套的豁免元素不会被双重反转 */ html.lightmode-active img img, html.lightmode-active video video { filter: none !important; } `; } function injectCSS(cssText) { let el = document.getElementById(LIGHT_MODE_CSS_ID); if (!el) { el = document.createElement('style'); el.id = LIGHT_MODE_CSS_ID; (document.head || document.documentElement).appendChild(el); } el.textContent = cssText; } function removeCSS() { const el = document.getElementById(LIGHT_MODE_CSS_ID); if (el) el.remove(); document.documentElement.classList.remove('lightmode-active'); } function applyLightMode(data) { const { intensity, preserveImages, preserveVideos } = data.globalSettings; injectCSS(buildCSS(intensity, preserveImages, preserveVideos)); document.documentElement.classList.add('lightmode-active'); } /* ============================================================ * MutationObserver — 动态内容监听 * ============================================================ */ let observer = null; function startObserver() { if (observer) return; observer = new MutationObserver((mutations) => { // 仅在 lightmode-active 状态下才需要处理 if (!document.documentElement.classList.contains('lightmode-active')) return; for (const m of mutations) { if (m.type === 'childList') { // 确保新插入的 style 标签不会覆盖我们的样式 for (const node of m.addedNodes) { if (node.id === LIGHT_MODE_CSS_ID) continue; if (node.nodeName === 'STYLE' || node.nodeName === 'LINK') { // 重新注入我们的样式到末尾以保持优先级 const ourEl = document.getElementById(LIGHT_MODE_CSS_ID); if (ourEl && ourEl.parentNode) { ourEl.parentNode.appendChild(ourEl); } } } } } }); observer.observe(document.documentElement, { childList: true, subtree: true }); } function stopObserver() { if (observer) { observer.disconnect(); observer = null; } } /* ============================================================ * 控制面板 UI(Shadow DOM 隔离) * ============================================================ */ let panelVisible = false; let panelHost = null; function createPanel() { if (panelHost) return; panelHost = document.createElement('div'); panelHost.id = 'lightmode-panel-container'; // 让面板不受 invert 影响 panelHost.style.cssText = 'position:fixed;top:0;right:0;z-index:2147483647;'; document.body.appendChild(panelHost); const shadow = panelHost.attachShadow({ mode: 'closed' }); const style = document.createElement('style'); style.textContent = getPanelCSS(); shadow.appendChild(style); const wrapper = document.createElement('div'); wrapper.className = 'lm-panel'; wrapper.innerHTML = getPanelHTML(); shadow.appendChild(wrapper); // 绑定事件 bindPanelEvents(shadow, wrapper); } function getPanelCSS() { return ` * { box-sizing: border-box; margin: 0; padding: 0; } @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); .lm-panel { font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; position: fixed; top: 16px; right: 16px; width: 360px; max-height: 80vh; background: linear-gradient(135deg, #ffffff 0%, #f8f9ff 100%); border-radius: 16px; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.08), 0 20px 40px -4px rgba(0, 0, 0, 0.14), 0 0 0 1px rgba(0, 0, 0, 0.05); overflow: hidden; display: none; flex-direction: column; color: #1a1a2e; animation: lm-slideIn 0.3s cubic-bezier(0.16, 1, 0.3, 1); backdrop-filter: blur(20px); } .lm-panel.visible { display: flex; } @keyframes lm-slideIn { from { opacity: 0; transform: translateY(-12px) scale(0.96); } to { opacity: 1; transform: translateY(0) scale(1); } } /* Header */ .lm-header { display: flex; align-items: center; justify-content: space-between; padding: 18px 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: #fff; } .lm-header h2 { font-size: 16px; font-weight: 700; letter-spacing: -0.3px; display: flex; align-items: center; gap: 8px; } .lm-header h2 .icon { font-size: 20px; } .lm-close { background: rgba(255,255,255,0.2); border: none; color: #fff; width: 32px; height: 32px; border-radius: 8px; cursor: pointer; font-size: 18px; display: flex; align-items: center; justify-content: center; transition: background 0.2s; } .lm-close:hover { background: rgba(255,255,255,0.35); } /* Body */ .lm-body { padding: 16px 20px; overflow-y: auto; flex: 1; } /* Section */ .lm-section { margin-bottom: 20px; } .lm-section:last-child { margin-bottom: 0; } .lm-section-title { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.8px; color: #8b8fa3; margin-bottom: 10px; } /* Toggle Row */ .lm-toggle-row { display: flex; align-items: center; justify-content: space-between; padding: 12px 14px; background: #f0f1f8; border-radius: 12px; margin-bottom: 8px; transition: background 0.15s; } .lm-toggle-row:hover { background: #e8eaf4; } .lm-toggle-row .label { font-size: 14px; font-weight: 500; color: #2d2d44; } .lm-toggle-row .sublabel { font-size: 12px; color: #8b8fa3; margin-top: 2px; } /* Toggle Switch */ .lm-switch { position: relative; width: 44px; height: 24px; flex-shrink: 0; } .lm-switch input { opacity: 0; width: 0; height: 0; position: absolute; } .lm-switch .slider { position: absolute; inset: 0; background: #c7c9d9; border-radius: 24px; cursor: pointer; transition: background 0.25s; } .lm-switch .slider::before { content: ''; position: absolute; width: 18px; height: 18px; left: 3px; bottom: 3px; background: #fff; border-radius: 50%; transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1); box-shadow: 0 1px 3px rgba(0,0,0,0.15); } .lm-switch input:checked + .slider { background: linear-gradient(135deg, #667eea, #764ba2); } .lm-switch input:checked + .slider::before { transform: translateX(20px); } /* Slider Range */ .lm-range-row { padding: 10px 14px; background: #f0f1f8; border-radius: 12px; margin-bottom: 8px; } .lm-range-row .range-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; } .lm-range-row .label { font-size: 14px; font-weight: 500; color: #2d2d44; } .lm-range-row .value { font-size: 13px; font-weight: 600; color: #667eea; } .lm-range-row input[type=range] { -webkit-appearance: none; width: 100%; height: 6px; border-radius: 3px; background: linear-gradient(90deg, #667eea, #764ba2); outline: none; } .lm-range-row input[type=range]::-webkit-slider-thumb { -webkit-appearance: none; width: 18px; height: 18px; border-radius: 50%; background: #fff; box-shadow: 0 1px 4px rgba(0,0,0,0.2); cursor: pointer; } /* Site List */ .lm-site-list { list-style: none; max-height: 200px; overflow-y: auto; } .lm-site-list::-webkit-scrollbar { width: 4px; } .lm-site-list::-webkit-scrollbar-track { background: transparent; } .lm-site-list::-webkit-scrollbar-thumb { background: #c7c9d9; border-radius: 2px; } .lm-site-item { display: flex; align-items: center; justify-content: space-between; padding: 8px 12px; border-radius: 8px; transition: background 0.15s; gap: 8px; } .lm-site-item:hover { background: #f0f1f8; } .lm-site-item .site-name { font-size: 13px; font-weight: 500; color: #2d2d44; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .lm-site-item .site-enabled { font-size: 10px; padding: 2px 8px; border-radius: 6px; font-weight: 600; background: #e8f5e9; color: #2e7d32; } .lm-site-item .site-enabled.off { background: #fce4ec; color: #c62828; } .lm-site-item .lm-delete { background: none; border: none; color: #c7c9d9; cursor: pointer; font-size: 16px; padding: 4px; border-radius: 6px; transition: color 0.2s, background 0.2s; line-height: 1; } .lm-site-item .lm-delete:hover { color: #ef5350; background: #fce4ec; } .lm-empty { text-align: center; padding: 20px; color: #b0b3c6; font-size: 13px; } /* Add Site */ .lm-add-row { display: flex; gap: 8px; margin-top: 10px; } .lm-add-row input { flex: 1; padding: 8px 12px; border: 2px solid #e8eaf4; border-radius: 10px; font-size: 13px; outline: none; font-family: inherit; transition: border-color 0.2s; color: #2d2d44; background: #fff; } .lm-add-row input::placeholder { color: #b0b3c6; } .lm-add-row input:focus { border-color: #667eea; } .lm-add-row button { padding: 8px 16px; background: linear-gradient(135deg, #667eea, #764ba2); color: #fff; border: none; border-radius: 10px; font-size: 13px; font-weight: 600; cursor: pointer; transition: opacity 0.2s, transform 0.15s; white-space: nowrap; } .lm-add-row button:hover { opacity: 0.9; transform: scale(1.02); } .lm-add-row button:active { transform: scale(0.98); } /* Footer */ .lm-footer { padding: 12px 20px; border-top: 1px solid #f0f1f8; text-align: center; font-size: 11px; color: #b0b3c6; } .lm-footer kbd { padding: 2px 6px; background: #f0f1f8; border-radius: 4px; font-family: inherit; font-size: 11px; color: #667eea; font-weight: 600; } `; } function getPanelHTML() { const data = loadData(); const host = currentHost(); const enabled = isSiteEnabled(data, host); const gs = data.globalSettings; return `