// ==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 `

☀️ LightMode

当前网站
${host}
为此域名启用亮色模式
全局设置
反转强度 ${Math.round(gs.intensity * 100)}%
保留图片原色
保留视频原色
已保存网站
`; } function buildSiteListHTML(data) { const entries = Object.entries(data.sites); if (entries.length === 0) { return '
  • 尚未添加任何网站
  • '; } return entries .map( ([site, cfg]) => `
  • ${site} ${cfg.enabled ? '已启用' : '已禁用'}
  • ` ) .join(''); } function bindPanelEvents(shadow, wrapper) { // 使用事件代理 wrapper.addEventListener('click', (e) => { const action = e.target.closest('[data-action]')?.dataset.action; if (!action) return; if (action === 'close') { togglePanel(wrapper); } else if (action === 'delete-site') { const site = e.target.closest('[data-site]').dataset.site; deleteSite(site, shadow, wrapper); } else if (action === 'add-site') { const input = shadow.querySelector('[data-input="add-site"]'); const site = input.value.trim(); if (site) { addSite(site, shadow, wrapper); input.value = ''; } } }); wrapper.addEventListener('change', (e) => { const action = e.target.closest('[data-action]')?.dataset.action; if (!action) return; const data = loadData(); if (action === 'toggle-current') { const host = currentHost(); if (!data.sites[host]) data.sites[host] = { enabled: false, customRules: [] }; data.sites[host].enabled = e.target.checked; saveData(data); refreshLightMode(data); refreshSiteList(shadow, wrapper, data); } else if (action === 'preserve-images') { data.globalSettings.preserveImages = e.target.checked; saveData(data); refreshLightMode(data); } else if (action === 'preserve-videos') { data.globalSettings.preserveVideos = e.target.checked; saveData(data); refreshLightMode(data); } }); wrapper.addEventListener('input', (e) => { const action = e.target.closest('[data-action]')?.dataset.action; if (action === 'intensity') { const val = parseInt(e.target.value, 10); const display = shadow.querySelector('[data-display="intensity"]'); if (display) display.textContent = val + '%'; const data = loadData(); data.globalSettings.intensity = val / 100; saveData(data); refreshLightMode(data); } }); // Enter 键添加网站 wrapper.addEventListener('keydown', (e) => { if (e.key === 'Enter' && e.target.matches('[data-input="add-site"]')) { const site = e.target.value.trim(); if (site) { addSite(site, shadow, wrapper); e.target.value = ''; } } }); } function addSite(site, shadow, wrapper) { // 清理输入 site = site.replace(/^https?:\/\//, '').replace(/\/.*$/, '').trim().toLowerCase(); if (!site) return; const data = loadData(); data.sites[site] = { enabled: true, customRules: [] }; saveData(data); refreshLightMode(data); refreshSiteList(shadow, wrapper, data); } function deleteSite(site, shadow, wrapper) { const data = loadData(); delete data.sites[site]; saveData(data); refreshLightMode(data); refreshSiteList(shadow, wrapper, data); // 更新当前网站的 toggle 状态 const toggle = shadow.querySelector('[data-action="toggle-current"]'); if (toggle) toggle.checked = isSiteEnabled(data, currentHost()); } function refreshSiteList(shadow, wrapper, data) { const list = shadow.querySelector('[data-list="sites"]'); if (list) list.innerHTML = buildSiteListHTML(data); } function togglePanel(wrapper) { panelVisible = !panelVisible; if (wrapper) { wrapper.classList.toggle('visible', panelVisible); } } function showPanel() { if (!panelHost) createPanel(); const shadow = panelHost.shadowRoot || panelHost; if (!panelHost._shadow) { // 重建面板以刷新数据 panelHost.remove(); panelHost = null; panelVisible = false; createPanel(); } panelVisible = true; } let _shadowRef = null; let _wrapperRef = null; function createPanelV2() { if (panelHost) { panelHost.remove(); panelHost = null; } panelHost = document.createElement('div'); panelHost.id = 'lightmode-panel-container'; panelHost.style.cssText = 'position:fixed;top:0;right:0;z-index:2147483647;pointer-events:none;width:0;height:0;'; document.body.appendChild(panelHost); const shadow = panelHost.attachShadow({ mode: 'open' }); _shadowRef = shadow; const style = document.createElement('style'); style.textContent = getPanelCSS(); shadow.appendChild(style); const wrapper = document.createElement('div'); wrapper.className = 'lm-panel'; wrapper.style.pointerEvents = 'auto'; wrapper.innerHTML = getPanelHTML(); shadow.appendChild(wrapper); _wrapperRef = wrapper; bindPanelEvents(shadow, wrapper); } function togglePanelV2() { if (!panelHost) createPanelV2(); panelVisible = !panelVisible; if (_wrapperRef) { _wrapperRef.classList.toggle('visible', panelVisible); } // 每次打开时刷新面板内容 if (panelVisible && _wrapperRef && _shadowRef) { _wrapperRef.innerHTML = getPanelHTML(); bindPanelEvents(_shadowRef, _wrapperRef); } } /* ============================================================ * 刷新亮色模式状态 * ============================================================ */ function refreshLightMode(data) { if (!data) data = loadData(); const host = currentHost(); if (isSiteEnabled(data, host)) { applyLightMode(data); startObserver(); } else { removeCSS(); stopObserver(); } } /* ============================================================ * 快捷键 * ============================================================ */ function handleKeydown(e) { // Alt+L: 切换当前网站 if (e.altKey && !e.shiftKey && !e.ctrlKey && !e.metaKey && e.key.toLowerCase() === 'l') { e.preventDefault(); const data = loadData(); const host = currentHost(); if (!data.sites[host]) data.sites[host] = { enabled: false, customRules: [] }; data.sites[host].enabled = !data.sites[host].enabled; saveData(data); refreshLightMode(data); } // Alt+Shift+L: 开关面板 if (e.altKey && e.shiftKey && !e.ctrlKey && !e.metaKey && e.key.toLowerCase() === 'l') { e.preventDefault(); togglePanelV2(); } } /* ============================================================ * Tampermonkey 菜单命令 * ============================================================ */ function registerMenuCommands() { const data = loadData(); const host = currentHost(); const enabled = isSiteEnabled(data, host); GM_registerMenuCommand( enabled ? `✅ 已启用 — ${host}(点击关闭)` : `☀️ 启用亮色模式 — ${host}`, () => { const d = loadData(); if (!d.sites[host]) d.sites[host] = { enabled: false, customRules: [] }; d.sites[host].enabled = !d.sites[host].enabled; saveData(d); refreshLightMode(d); } ); GM_registerMenuCommand('⚙️ 打开 LightMode 面板', () => { if (!panelVisible) togglePanelV2(); }); } /* ============================================================ * 初始化 * ============================================================ */ function init() { // 1. 尽早注入 CSS(document-start 阶段) const data = loadData(); const host = currentHost(); if (isSiteEnabled(data, host)) { applyLightMode(data); } // 2. DOM 就绪后初始化其他功能 const onReady = () => { if (isSiteEnabled(data, host)) { startObserver(); } document.addEventListener('keydown', handleKeydown); registerMenuCommands(); }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', onReady); } else { onReady(); } } init(); })();