マンガ見開きビューア

画像が縦一列表示となっているサイト上で起動後、右上アイコンクリックで見開き表示(iframe系のサイト対応メニュー追加)

当前为 2025-08-20 提交的版本,查看 最新版本

// ==UserScript==
// @name         マンガ見開きビューア
// @namespace    http://2chan.net/
// @version      1.4
// @description  画像が縦一列表示となっているサイト上で起動後、右上アイコンクリックで見開き表示(iframe系のサイト対応メニュー追加)
// @description  When launched on a website where images are displayed in a single vertical column, click the icon in the upper right corner to switch to a double-page display.
// @author       futaba
// @match        *://*/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=2chan.net
// @license      MIT
// @grant        GM_registerMenuCommand
// ==/UserScript==

(function () {
  'use strict';

  const CONFIG = {
    minImageHeight: 400,
    minImageWidth: 200,
    enableKeyControls: true,
    enableMouseWheel: true,
    minMangaImageCount: 2,
    defaultBg: '#333333',
    refreshDebounceMs: 250,
  };

  let currentPage = 0;
  let images = [];
  let container = null;
  let imageArea = null;
  let bgToggleBtn = null;
  let toggleButton = null;
  let io = null;
  const watched = new WeakSet();
  let refreshTimer = null;
  let navTimer = null;
  let isIframeMode = false;

  // ---------- 設定管理 ----------
  function getSiteSettings() {
    try {
      return JSON.parse(localStorage.getItem('mangaViewerDomains') || '{}');
    } catch {
      return {};
    }
  }

  function setSiteSettings(settings) {
    localStorage.setItem('mangaViewerDomains', JSON.stringify(settings));
  }

  function getCurrentSiteStatus() {
    const settings = getSiteSettings();
    const hostname = window.location.hostname;
    return settings[hostname] || 'default'; // 'show', 'hide', 'iframe', 'default'
  }

  function shouldShowButton() {
    const status = getCurrentSiteStatus();
    return status === 'show' || status === 'iframe';
  }

  function isCurrentSiteIframeMode() {
    const status = getCurrentSiteStatus();
    return status === 'iframe';
  }

  function setSiteIframeMode(hostname, enabled) {
    const settings = getSiteSettings();
    if (enabled) {
      settings[hostname] = 'iframe';
    } else {
      // iframe解除時は通常の表示モードに変更
      settings[hostname] = 'show';
    }
    setSiteSettings(settings);
    // 即座にisIframeModeフラグも更新
    isIframeMode = enabled;
  }

  // ---------- 背景色 ----------
  function getBgColor() {
    return localStorage.getItem('mangaViewerBg') || CONFIG.defaultBg;
  }
  function toggleBgColor() {
    const newColor = getBgColor() === '#333333' ? '#F5F5F5' : '#333333';
    localStorage.setItem('mangaViewerBg', newColor);
    if (container) container.style.background = newColor;
    if (bgToggleBtn) bgToggleBtn.textContent = (newColor === '#F5F5F5') ? '背景:白' : '背景:黒';
  }

  // ---------- 画像検出 ----------
  function detectMangaImages() {
    console.log('detectMangaImages called, isIframeMode:', isIframeMode);
    if (isIframeMode) {
      const result = detectMangaImagesFromIframe();
      console.log('iframe images detected:', result.length);
      return result;
    }
    const result = detectMangaImagesFromDocument();
    console.log('normal images detected:', result.length);
    return result;
  }

  function detectMangaImagesFromDocument() {
    const allImages = document.querySelectorAll('img');
    const excludePatterns = ['icon','logo','avatar','banner','header','footer','thumb','thumbnail','profile','menu','button','bg','background','nav','sidebar','ad','advertisement','favicon','sprite'];

    const potential = Array.from(allImages).filter(img => {
      if (!img.complete || img.naturalHeight === 0 || img.naturalWidth === 0) return false;
      if (img.naturalHeight < CONFIG.minImageHeight || img.naturalWidth < CONFIG.minImageWidth) return false;
      const src = (img.src || '').toLowerCase();
      if (excludePatterns.some(p => src.includes(p))) return false;
      return true;
    });

    if (potential.length < CONFIG.minMangaImageCount) return [];

    const seenSrcs = new Set();
    return potential.filter(img => {
      if (seenSrcs.has(img.src)) return false;
      seenSrcs.add(img.src);
      return true;
    }).sort((a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top);
  }

  function detectMangaImagesFromIframe() {
    console.log('detectMangaImagesFromIframe called');
    let iframe = document.querySelector("iframe");
    if (!iframe) {
      console.log('No iframe found');
      return [];
    }
    
    let doc;
    try {
      doc = iframe.contentDocument || iframe.contentWindow.document;
    } catch (e) {
      console.log("iframe access denied:", e);
      return [];
    }
    
    if (!doc) {
      console.log('No iframe document found');
      return [];
    }

    const allImages = doc.querySelectorAll('img');
    console.log('Total images in iframe:', allImages.length);
    
    const potential = Array.from(allImages).filter(img => {
      const isComplete = img.complete;
      const hasSize = img.naturalHeight > 0 && img.naturalWidth > 0;
      const isLargeEnough = img.naturalWidth >= 500;
      
      console.log('Image check:', {
        src: img.src.substring(0, 50) + '...',
        complete: isComplete,
        size: `${img.naturalWidth}x${img.naturalHeight}`,
        largeEnough: isLargeEnough
      });
      
      return isComplete && hasSize && isLargeEnough;
    });

    console.log('Filtered potential images:', potential.length);

    // iframe内の画像のsrcをフルURLに変換(元のオブジェクトを保持)
    potential.forEach(img => {
      let src = img.src;
      // 相対URLの場合、iframe内のベースURLを使用
      if (!src.startsWith('http')) {
        try {
          const iframeUrl = new URL(iframe.src);
          src = new URL(src, iframeUrl.origin).href;
          // 元の画像オブジェクトのsrcを更新
          Object.defineProperty(img, 'src', { value: src, writable: false });
        } catch (e) {
          console.log("URL conversion failed:", e);
        }
      }
    });

    return potential.sort((a, b) => {
      const rectA = a.getBoundingClientRect();
      const rectB = b.getBoundingClientRect();
      return rectA.top - rectB.top;
    });
  }

  function scheduleRefreshImages() {
    if (refreshTimer) clearTimeout(refreshTimer);
    refreshTimer = setTimeout(refreshImages, CONFIG.refreshDebounceMs);
  }
  
  function refreshImages() {
    refreshTimer = null;
    const newImages = detectMangaImages();
    if (newImages.length !== images.length || newImages.some((img, i) => images[i] !== img)) {
      images = newImages;
      if (container && container.style.display === 'flex') updatePageInfoOnly();
    }
    if (!isIframeMode) {
      newImages.forEach(img => attachWatchers(img));
    }
  }
  
  function attachWatchers(img) {
    if (watched.has(img)) return;
    watched.add(img);
    img.addEventListener('load', scheduleRefreshImages, { once: true });
    if (io && !img.complete) io.observe(img);
  }

  // ---------- 全画像読み込み機能 ----------
  function loadAllImages(buttonElement) {
    if (buttonElement) {
      if (buttonElement.dataset.loading === '1') return;
      buttonElement.dataset.loading = '1';
      buttonElement.textContent = '🔥読込中...';
      buttonElement.style.opacity = '0.5';
    }
    
    if (isIframeMode) {
      // iframe内でのスクロール処理
      let iframe = document.querySelector("iframe");
      if (!iframe) return;
      
      try {
        const iframeWindow = iframe.contentWindow;
        const iframeDoc = iframe.contentDocument || iframeWindow.document;
        const originalScrollTop = iframeWindow.pageYOffset || iframeDoc.documentElement.scrollTop;
        
        let currentScroll = 0;
        const documentHeight = Math.max(
          iframeDoc.body.scrollHeight,
          iframeDoc.body.offsetHeight,
          iframeDoc.documentElement.clientHeight,
          iframeDoc.documentElement.scrollHeight,
          iframeDoc.documentElement.offsetHeight
        );
        const viewportHeight = iframeWindow.innerHeight;
        const scrollStep = Math.max(500, viewportHeight);

        function scrollAndLoad() {
          currentScroll += scrollStep;
          iframeWindow.scrollTo(0, currentScroll);
          scheduleRefreshImages();
          
          if (currentScroll < documentHeight - viewportHeight) {
            setTimeout(scrollAndLoad, 10);
          } else {
            setTimeout(() => {
              iframeWindow.scrollTo(0, originalScrollTop);
              refreshImages();
              if (buttonElement) {
                buttonElement.textContent = '🔥全読込';
                buttonElement.style.opacity = '0.8';
                buttonElement.dataset.loading = '0';
              }
              showMessage(`${images.length}枚の画像を検出しました`);
            }, 100);
          }
        }
        scrollAndLoad();
      } catch (e) {
        console.log("iframe scroll failed:", e);
        if (buttonElement) {
          buttonElement.textContent = '🔥全読込';
          buttonElement.style.opacity = '0.8';
          buttonElement.dataset.loading = '0';
        }
      }
    } else {
      // 通常のスクロール処理
      const originalScrollTop = window.pageYOffset;
      let currentScroll = 0;
      const documentHeight = Math.max(
        document.body.scrollHeight,
        document.body.offsetHeight,
        document.documentElement.clientHeight,
        document.documentElement.scrollHeight,
        document.documentElement.offsetHeight
      );
      const viewportHeight = window.innerHeight;
      const scrollStep = Math.max(500, viewportHeight);

      function scrollAndLoad() {
        currentScroll += scrollStep;
        window.scrollTo(0, currentScroll);
        scheduleRefreshImages();
        if (currentScroll < documentHeight - viewportHeight) {
          setTimeout(scrollAndLoad, 10);
        } else {
          setTimeout(() => {
            window.scrollTo(0, originalScrollTop);
            refreshImages();
            if (buttonElement) {
              buttonElement.textContent = '🔥全読込';
              buttonElement.style.opacity = '0.8';
              buttonElement.dataset.loading = '0';
            }
            showMessage(`${images.length}枚の画像を検出しました`);
          }, 100);
        }
      }
      scrollAndLoad();
    }
  }

  function showMessage(text, color = 'rgba(0,150,0,0.8)') {
    const msg = document.createElement('div');
    msg.textContent = text;
    msg.style.cssText = `
      position: fixed; top: 20px; left: 50%; transform: translateX(-50%);
      z-index: 10001; background: ${color}; color: white;
      padding: 8px 12px; border-radius: 4px; font-size: 12px; pointer-events: none;
    `;
    document.body.appendChild(msg);
    setTimeout(() => msg.remove(), 2500);
  }

  // ---------- ビューア生成 ----------
  function createSpreadViewerOnce() {
    if (container) return;
    
    // iframe対応時は、親ウィンドウではなくトップレベルウィンドウに作成
    const targetDocument = (isIframeMode && window !== window.top) ? window.top.document : document;
    const targetWindow = (isIframeMode && window !== window.top) ? window.top : window;
    
    container = targetDocument.createElement('div');
    container.style.cssText = `
      position: fixed; top:0; left:0; width:100vw; height:100vh;
      background:${getBgColor()}; z-index:10000; display:none;
      justify-content:center; align-items:center; flex-direction:column;
    `;

    imageArea = targetDocument.createElement('div');
    imageArea.style.cssText = `
      display:flex; flex-direction:row-reverse;
      justify-content:center; align-items:center;
      max-width:calc(100vw - 10px); max-height:calc(100vh - 10px);
      gap:2px; padding:5px; box-sizing:border-box;
    `;

    // --- ナビゲーション ---
    const nav = targetDocument.createElement('div');
    nav.setAttribute('data-mv-ui', '1');
    nav.style.cssText = `
      position:absolute; bottom:20px; color:white;
      font-size:16px; background:rgba(0,0,0,0.7);
      padding:10px 20px; border-radius:20px;
      display:flex; align-items:center; gap:12px;
      opacity:1; transition:opacity 0.5s;
      pointer-events:auto;
    `;
    const mkBtn = (label, onClick) => {
      const b = targetDocument.createElement('button');
      b.type = 'button'; b.textContent = label;
      b.setAttribute('data-mv-ui', '1');
      b.style.cssText = `
        background: rgba(255,255,255,0.2); color: white;
        border:none; padding:6px 10px; border-radius:4px; cursor:pointer;
      `;
      b.addEventListener('click', e => { e.stopPropagation(); e.preventDefault(); onClick(); });
      return b;
    };
    const btnNextSpread = mkBtn('←次', () => nextPage(2));
    const btnNextSingle = mkBtn('←単', () => nextPage(1));
    const btnPrevSingle = mkBtn('単→', () => prevPage(1));
    const btnPrevSpread = mkBtn('前→', () => prevPage(2));

    // 中央は進捗バー
    const progress = targetDocument.createElement('progress');
    progress.setAttribute('data-mv-ui', '1');
    progress.max = 100;
    progress.value = 0;
    progress.style.cssText = `
      width:160px; height:8px;
      appearance:none; -webkit-appearance:none;
      direction: rtl;
    `;

    nav.append(btnNextSpread, btnNextSingle, progress, btnPrevSingle, btnPrevSpread);
    container.appendChild(nav);

    // --- ナビフェードアウト ---
    function scheduleNavFade() {
      clearTimeout(navTimer);
      navTimer = setTimeout(() => nav.style.opacity = '0', 3000);
    }
    nav.addEventListener('mouseenter', () => { nav.style.opacity = '1'; clearTimeout(navTimer); });
    nav.addEventListener('mouseleave', scheduleNavFade);
    scheduleNavFade();

    // --- 閉じるボタン ---
    const closeBtn = targetDocument.createElement('button');
    closeBtn.setAttribute('data-mv-ui','1');
    closeBtn.textContent = '×';
    closeBtn.style.cssText = `
      position:absolute; top:20px; right:20px;
      background:rgba(0,0,0,0.5); color:white;
      border:none; font-size:24px; width:40px; height:40px;
      border-radius:50%; cursor:pointer;
    `;
    closeBtn.addEventListener('click', e => { e.stopPropagation(); e.preventDefault(); container.style.display = 'none'; });
    container.appendChild(closeBtn);

    // --- 全読込ボタン ---
    const loadAllBtnTop = targetDocument.createElement('button');
    loadAllBtnTop.setAttribute('data-mv-ui','1');
    loadAllBtnTop.textContent = '🔥全読込';
    loadAllBtnTop.style.cssText = `
      position:absolute; top:70px; right:20px;
      background:rgba(0,0,0,0.5); color:white;
      border:none; font-size:12px; padding:6px 8px;
      border-radius:4px; cursor:pointer; opacity:0.8;
    `;
    loadAllBtnTop.addEventListener('click', e => { e.stopPropagation(); e.preventDefault(); loadAllImages(loadAllBtnTop); });
    container.appendChild(loadAllBtnTop);

    // --- 背景切替 ---
    bgToggleBtn = targetDocument.createElement('button');
    bgToggleBtn.setAttribute('data-mv-ui','1');
    bgToggleBtn.style.cssText = `
      position:absolute; bottom:20px; right:20px;
      background:rgba(0,0,0,0.5); color:white;
      border:none; font-size:14px; padding:6px 10px;
      border-radius:6px; cursor:pointer;
    `;
    bgToggleBtn.textContent = (getBgColor() === '#F5F5F5') ? '背景:白' : '背景:黒';
    bgToggleBtn.addEventListener('click', e => { e.stopPropagation(); e.preventDefault(); toggleBgColor(); });
    container.appendChild(bgToggleBtn);

    // --- 常時表示のページカウンター ---
    const pageCounter = targetDocument.createElement('div');
    pageCounter.id = 'mv-page-counter';
    pageCounter.setAttribute('data-mv-ui','1');
    pageCounter.style.cssText = `
      position:absolute; bottom:60px; right:20px;
      background:rgba(0,0,0,0.5); color:white;
      font-size:14px; padding:4px 8px;
      border-radius:6px; font-family:monospace;
      pointer-events:none;
    `;
    container.appendChild(pageCounter);

    container.appendChild(imageArea);
    targetDocument.body.appendChild(container);

    container.addEventListener('click', e => {
      if (e.target.closest('[data-mv-ui="1"]')) return;
      const rect = container.getBoundingClientRect();
      if ((e.clientX - rect.left) > rect.width/2) prevPage(2); else nextPage(2);
    });

    if (CONFIG.enableMouseWheel) {
      container.addEventListener('wheel', e => {
        e.preventDefault();
        if (e.deltaY > 0) nextPage(2); else prevPage(2);
      }, { passive: false });
    }
  }

  function showSpreadPage(pageNum) {
    if (!images.length) return;
    createSpreadViewerOnce();
    if (pageNum < 0) pageNum = 0;
    if (pageNum >= images.length) pageNum = Math.max(0, images.length - 1);
    imageArea.innerHTML = '';

    for (let i = 0; i < 2; i++) {
      const idx = pageNum + i;
      if (idx < images.length) {
        const wrapper = document.createElement('div');
        wrapper.className = 'image-wrapper';
        wrapper.style.cssText = 'pointer-events:none;';
        const img = document.createElement('img');
        img.src = images[idx].src;
        img.style.cssText = `
          max-height:calc(100vh - 10px);
          max-width:calc(50vw - 10px);
          object-fit:contain;
          display:block;
        `;
        wrapper.appendChild(img);
        imageArea.appendChild(wrapper);
      }
    }
    currentPage = pageNum;
    updatePageInfoOnly();
    container.style.display = 'flex';
  }

  function updatePageInfoOnly() {
    const targetDocument = (isIframeMode && window !== window.top) ? window.top.document : document;
    const pageCounter = targetDocument.getElementById('mv-page-counter');
    const progress = container ? container.querySelector('progress[data-mv-ui]') : null;
    if (!pageCounter || !progress) return;

    const current = currentPage + 1;
    const total = images.length;
    pageCounter.textContent = `${String(current).padStart(3,'0')}/${String(total).padStart(3,'0')}`;
    progress.value = Math.floor((current / total) * 100);
  }
  function nextPage(step=2){ const t=currentPage+step; if(t<images.length) showSpreadPage(t);}
  function prevPage(step=2){ const t=currentPage-step; if(t>=0) showSpreadPage(t);}

  // ---------- キー操作 ----------
  if (CONFIG.enableKeyControls) {
    document.addEventListener('keydown', e => {
      if (!container || container.style.display !== 'flex') return;
      switch(e.key){
        case 'ArrowLeft': case ' ': e.preventDefault(); nextPage(2); break;
        case 'ArrowRight': e.preventDefault(); prevPage(2); break;
        case 'ArrowDown': e.preventDefault(); nextPage(1); break;
        case 'ArrowUp': e.preventDefault(); prevPage(1); break;
        case 'Escape': e.preventDefault(); container.style.display='none'; break;
      }
    });
  }

  // ---------- 起動ボタン ----------
  function addToggleButton() {
    if (!shouldShowButton()) return;
    
    toggleButton = document.createElement('button');
    toggleButton.textContent = '📖';
    toggleButton.title = '見開き表示';
    // PageExpand拡張機能対策 - 複数の除外属性を設定
    toggleButton.setAttribute('data-pageexpand-ignore', 'true');
    toggleButton.setAttribute('data-no-zoom', 'true');
    toggleButton.setAttribute('data-skip-pageexpand', 'true');
    toggleButton.setAttribute('data-manga-viewer-button', 'true');
    toggleButton.className = 'pageexpand-ignore no-zoom manga-viewer-btn';
    
    toggleButton.style.cssText = `
      position:fixed; top:50px; right:40px; z-index:9999;
      background:rgba(0,0,0,0.6); color:white; border:none;
      width:32px; height:32px; border-radius:6px; cursor:pointer;
      font-size:16px; opacity:0.7; transition:opacity 0.2s;
      display: flex; align-items: center; justify-content: center;
      pointer-events: auto;
      transform: none !important;
      zoom: 1 !important;
      scale: 1 !important;
    `;
    
    // CSS でも拡大を防ぐ
    toggleButton.style.setProperty('transform', 'none', 'important');
    toggleButton.style.setProperty('zoom', '1', 'important');
    toggleButton.style.setProperty('scale', '1', 'important');
    
    toggleButton.onmouseenter = () => toggleButton.style.opacity='1';
    toggleButton.onmouseleave = () => toggleButton.style.opacity='0.7';
    toggleButton.addEventListener('click', () => {
      refreshImages();
      if (images.length >= CONFIG.minMangaImageCount) {
        showSpreadPage(0);
      } else {
        showMessage('漫画画像が見つかりません', 'rgba(0,0,0,0.8)');
      }
    });
    
    // PageExpand拡張機能の動的変更を阻止
    toggleButton.addEventListener('mouseenter', (e) => {
      e.stopImmediatePropagation();
      toggleButton.style.opacity = '1';
      // 拡大を強制的に元に戻す
      toggleButton.style.transform = 'none';
      toggleButton.style.zoom = '1';
    });
    
    toggleButton.addEventListener('mouseleave', (e) => {
      e.stopImmediatePropagation();
      toggleButton.style.opacity = '0.7';
      toggleButton.style.transform = 'none';
      toggleButton.style.zoom = '1';
    });
    
    document.body.appendChild(toggleButton);
    
    // ボタンが追加された後に、PageExpandによる変更を監視・阻止
    const observer = new MutationObserver((mutations) => {
      mutations.forEach((mutation) => {
        if (mutation.target === toggleButton && mutation.type === 'attributes') {
          if (mutation.attributeName === 'style') {
            // スタイルが勝手に変更された場合、元に戻す
            if (toggleButton.style.transform !== 'none') {
              toggleButton.style.transform = 'none';
            }
            if (toggleButton.style.zoom !== '1' && toggleButton.style.zoom !== '') {
              toggleButton.style.zoom = '1';
            }
          }
        }
      });
    });
    
    observer.observe(toggleButton, { attributes: true, attributeFilter: ['style', 'class'] });
  }

  // ---------- 初期化 ----------
  function initialize() {
    isIframeMode = isCurrentSiteIframeMode();
    
    if (document.readyState === 'loading') {
      document.addEventListener('DOMContentLoaded', addToggleButton);
    } else {
      addToggleButton();
    }

    // ---------- 動的監視 ----------
    if (!isIframeMode && 'IntersectionObserver' in window) {
      io = new IntersectionObserver(entries => {
        if (entries.some(e => e.isIntersecting)) scheduleRefreshImages();
      }, { root:null, rootMargin:'200px 0px', threshold:0.01 });
    }
    
    if (!isIframeMode) {
      const mo = new MutationObserver(mutations => {
        let found=false;
        for(const m of mutations){
          m.addedNodes && m.addedNodes.forEach(node=>{
            if(node.nodeType===1){
              if(node.tagName==='IMG'){ attachWatchers(node); found=true;}
              else node.querySelectorAll && node.querySelectorAll('img').forEach(attachWatchers);
            }
          });
        }
        if(found) scheduleRefreshImages();
      });
      mo.observe(document.body,{childList:true,subtree:true});
    }
    
    refreshImages();
  }

  // ---------- Tampermonkey 右クリックメニュー ----------
  if (typeof GM_registerMenuCommand !== 'undefined') {
    GM_registerMenuCommand("📖 見開きビューアを起動", () => {
      setTimeout(() => {
        refreshImages();
        if (images.length >= CONFIG.minMangaImageCount) {
          showSpreadPage(0);
        } else {
          showMessage('漫画画像が見つかりません', 'rgba(0,0,0,0.8)');
        }
      }, 300);
    });

    GM_registerMenuCommand("🔄 iframe対応モード切替", () => {
      const hostname = window.location.hostname;
      const currentMode = isCurrentSiteIframeMode();
      setSiteIframeMode(hostname, !currentMode);
      showMessage(`${hostname}: iframe対応 ${!currentMode ? 'ON' : 'OFF'}`, 'rgba(0,100,200,0.8)');
      // リロードしない - 即座に反映
      if (toggleButton) {
        toggleButton.remove();
        addToggleButton();
      }
    });

    GM_registerMenuCommand("👁️ このサイトでボタンを表示", () => {
      const settings = getSiteSettings();
      const hostname = window.location.hostname;
      const currentMode = isCurrentSiteIframeMode();
      settings[hostname] = currentMode ? 'iframe' : 'show';
      setSiteSettings(settings);
      showMessage(`${hostname} でボタンを表示します`, 'rgba(0,150,0,0.8)');
      setTimeout(() => location.reload(), 1500);
    });

    GM_registerMenuCommand("🚫 このサイトでボタンを非表示", () => {
      const settings = getSiteSettings();
      const hostname = window.location.hostname;
      settings[hostname] = 'hide';
      setSiteSettings(settings);
      if (toggleButton) toggleButton.remove();
      showMessage(`${hostname} で見開きボタンを非表示にしました`, 'rgba(200,100,0,0.8)');
    });

    GM_registerMenuCommand("⚠️ 記憶したサイト設定をリセット ⚠️", () => {
      if (confirm('記憶したサイト設定をリセットしますか?\n(現在のページもリロードされます)')) {
        localStorage.removeItem('mangaViewerDomains');
        localStorage.removeItem('mangaViewerBg');
        const msg = document.createElement('div');
        msg.textContent = 'すべての設定をリセットしました';
        msg.style.cssText = `
          position: fixed; top: 20px; left: 50%; transform: translateX(-50%);
          z-index: 10001; background: rgba(255,0,0,0.8); color: white;
          padding: 12px 20px; border-radius: 6px; font-size: 14px; pointer-events: none;
        `;
        document.body.appendChild(msg);
        setTimeout(() => location.reload(), 1000);
      }
    });
  }

  initialize();
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址