Greasy Fork

Greasy Fork is available in English.

LightMode - 暗色模式转亮色模式

将特定网站从暗色模式转换为亮色模式,支持记录网站、实时切换、精细化颜色反转

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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
// ==/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 `
      <div class="lm-header">
        <h2><span class="icon">☀️</span> LightMode</h2>
        <button class="lm-close" data-action="close" title="关闭">✕</button>
      </div>
      <div class="lm-body">

        <!-- 当前网站开关 -->
        <div class="lm-section">
          <div class="lm-section-title">当前网站</div>
          <div class="lm-toggle-row">
            <div>
              <div class="label">${host}</div>
              <div class="sublabel">为此域名启用亮色模式</div>
            </div>
            <label class="lm-switch">
              <input type="checkbox" data-action="toggle-current" ${enabled ? 'checked' : ''}>
              <span class="slider"></span>
            </label>
          </div>
        </div>

        <!-- 全局设置 -->
        <div class="lm-section">
          <div class="lm-section-title">全局设置</div>

          <div class="lm-range-row">
            <div class="range-header">
              <span class="label">反转强度</span>
              <span class="value" data-display="intensity">${Math.round(gs.intensity * 100)}%</span>
            </div>
            <input type="range" min="0" max="100" value="${Math.round(gs.intensity * 100)}" data-action="intensity">
          </div>

          <div class="lm-toggle-row">
            <div><div class="label">保留图片原色</div></div>
            <label class="lm-switch">
              <input type="checkbox" data-action="preserve-images" ${gs.preserveImages ? 'checked' : ''}>
              <span class="slider"></span>
            </label>
          </div>

          <div class="lm-toggle-row">
            <div><div class="label">保留视频原色</div></div>
            <label class="lm-switch">
              <input type="checkbox" data-action="preserve-videos" ${gs.preserveVideos ? 'checked' : ''}>
              <span class="slider"></span>
            </label>
          </div>
        </div>

        <!-- 已保存网站列表 -->
        <div class="lm-section">
          <div class="lm-section-title">已保存网站</div>
          <ul class="lm-site-list" data-list="sites">
            ${buildSiteListHTML(data)}
          </ul>
          <div class="lm-add-row">
            <input type="text" placeholder="输入域名,如 example.com" data-input="add-site">
            <button data-action="add-site">添加</button>
          </div>
        </div>

      </div>
      <div class="lm-footer">
        快捷键:<kbd>Alt+L</kbd> 切换 &nbsp;|&nbsp; <kbd>Alt+Shift+L</kbd> 面板
      </div>
    `;
  }

  function buildSiteListHTML(data) {
    const entries = Object.entries(data.sites);
    if (entries.length === 0) {
      return '<li class="lm-empty">尚未添加任何网站</li>';
    }
    return entries
      .map(
        ([site, cfg]) => `
        <li class="lm-site-item">
          <span class="site-name" title="${site}">${site}</span>
          <span class="site-enabled ${cfg.enabled ? '' : 'off'}">${cfg.enabled ? '已启用' : '已禁用'}</span>
          <button class="lm-delete" data-action="delete-site" data-site="${site}" title="删除">✕</button>
        </li>`
      )
      .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();
})();