您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
画像が縦一列表示となっているサイト上で起動後、右上アイコンクリックで見開き表示。
当前为
// ==UserScript== // @name マンガ見開きビューア(自動判別版) // @namespace http://2chan.net/ // @version 2.1 // @description 画像が縦一列表示となっているサイト上で起動後、右上アイコンクリックで見開き表示。 // @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 with auto-detection. // @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, }; // 検出モード設定 const DETECTION_MODES = { 'default': { name: '自動判別', description: 'ページ全体から画像を自動検出' }, 'normal': { name: '通常検出', description: 'ページ全体から画像を自動検出' }, 'iframe': { name: 'iframeモード', description: 'iframe内の画像を検出' }, 'selector:reading-content': { name: 'reading-contentセレクタ', description: '.reading-content img で検出', selector: '.reading-content img', dataSrcSupport: true }, 'selector:chapter-content': { name: 'chapter-contentセレクタ', description: '.chapter-content img で検出', selector: '.chapter-content img', dataSrcSupport: true }, 'selector:manga-reader': { name: 'manga-readerセレクタ', description: '.manga-reader img で検出', selector: '.manga-reader img', dataSrcSupport: false } }; let currentPage = 0; let images = []; let container = null; let imageArea = null; let bgToggleBtn = null; let fullscreenBtn = null; let toggleButton = null; let io = null; const watched = new WeakSet(); let refreshTimer = null; let navTimer = null; let detectedMode = null; // 自動判別で検出されたモード let isFullscreen = 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] || 'hide'; // デフォルトは非表示 } function shouldShowButton() { return getCurrentSiteStatus() === 'show'; } function getCurrentModeDisplay() { const status = getCurrentSiteStatus(); if (status === 'hide') return '非表示'; if (status === 'show') return '表示中'; return 'デフォルト'; } function setSiteMode(hostname, mode) { const settings = getSiteSettings(); settings[hostname] = mode; setSiteSettings(settings); const statusText = mode === 'show' ? 'ボタン表示' : 'ボタン非表示'; showMessage(`${hostname}: ${statusText} に設定しました`, 'rgba(0,100,200,0.8)'); // UI更新(リロードなし) if (mode === 'hide' && toggleButton) { toggleButton.remove(); toggleButton = null; } else if (mode === 'show' && !toggleButton) { setTimeout(addToggleButton, 100); } } // ---------- 検出モード設定 ---------- function getDetectionMode() { const hostname = window.location.hostname; const key = `mangaDetectionMode_${hostname}`; return localStorage.getItem(key) || 'default'; } function setDetectionMode(hostname, mode) { const key = `mangaDetectionMode_${hostname}`; localStorage.setItem(key, mode); const modeInfo = DETECTION_MODES[mode] || { name: mode }; showMessage(`${hostname}: ${modeInfo.name} に設定しました`, 'rgba(0,150,0,0.8)'); } function getCurrentDetectionResultDisplay() { const mode = getDetectionMode(); if (mode === 'default') { // 自動判別モードの場合、実際に検出されたモードを表示 if (detectedMode) { const modeInfo = DETECTION_MODES[detectedMode]; return modeInfo ? modeInfo.name : detectedMode; } else { return '[不明]'; } } else { const modeInfo = DETECTION_MODES[mode]; return modeInfo ? modeInfo.name : mode; } } // ---------- 背景色 ---------- 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 toggleFullscreen() { if (!container) return; if (!isFullscreen) { // 全画面に入る if (container.requestFullscreen) { container.requestFullscreen(); } else if (container.webkitRequestFullscreen) { container.webkitRequestFullscreen(); } else if (container.mozRequestFullScreen) { container.mozRequestFullScreen(); } else if (container.msRequestFullscreen) { container.msRequestFullscreen(); } } else { // 全画面を出る if (document.exitFullscreen) { document.exitFullscreen(); } else if (document.webkitExitFullscreen) { document.webkitExitFullscreen(); } else if (document.mozCancelFullScreen) { document.mozCancelFullScreen(); } else if (document.msExitFullscreen) { document.msExitFullscreen(); } } } // 全画面状態の監視 function setupFullscreenListeners() { const fullscreenEvents = ['fullscreenchange', 'webkitfullscreenchange', 'mozfullscreenchange', 'MSFullscreenChange']; fullscreenEvents.forEach(event => { document.addEventListener(event, () => { isFullscreen = !!(document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement); // ボタンのアイコンは常に⛶で固定 }); }); } // ---------- 画像検出(統合版) ---------- function detectMangaImages() { const detectionMode = getDetectionMode(); console.log('Detection mode:', detectionMode); // 設定済みの場合は、その方法を使用 if (detectionMode !== 'default') { return detectByMode(detectionMode); } // デフォルト(自動判別)の場合:自動判別を試行 return detectWithAutoFallback(); } function detectByMode(mode) { if (mode === 'default') return detectWithAutoFallback(); if (mode === 'normal') return detectMangaImagesFromDocument(); if (mode === 'iframe') return detectMangaImagesFromIframe(); if (mode.startsWith('selector:')) return detectBySelectorMode(mode); // フォールバック return detectMangaImagesFromDocument(); } function detectWithAutoFallback() { console.log('Starting auto-detection...'); // 1. 通常検出を試行 let images = detectMangaImagesFromDocument(); if (images.length >= CONFIG.minMangaImageCount) { console.log('Auto-detected: normal mode'); detectedMode = 'normal'; return images; } // 2. 各セレクタパターンを試行 const selectorModes = ['reading-content', 'chapter-content', 'manga-reader']; for (const selectorName of selectorModes) { images = detectBySelectorConfig(selectorName); if (images.length >= CONFIG.minMangaImageCount) { console.log(`Auto-detected: selector:${selectorName}`); detectedMode = `selector:${selectorName}`; return images; } } // 3. iframe検出を試行 if (document.querySelector('iframe')) { images = detectMangaImagesFromIframe(); if (images.length >= CONFIG.minMangaImageCount) { console.log('Auto-detected: iframe mode'); detectedMode = 'iframe'; return images; } } // すべて失敗 console.log('Auto-detection failed'); detectedMode = null; return []; } 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 detectBySelectorMode(mode) { const selectorName = mode.replace('selector:', ''); return detectBySelectorConfig(selectorName); } function detectBySelectorConfig(configName) { const config = DETECTION_MODES[`selector:${configName}`]; if (!config || !config.selector) { console.warn(`Unknown selector config: ${configName}`); return []; } const images = Array.from(document.querySelectorAll(config.selector)); console.log(`Selector ${config.selector} found ${images.length} images`); // data-src対応 if (config.dataSrcSupport) { images.forEach(img => { if (!img.src && img.dataset.src) { console.log('Converting data-src to src:', img.dataset.src); img.src = img.dataset.src; } }); } // 基本フィルタリング return images.filter(img => { if (!img.complete || img.naturalHeight === 0 || img.naturalWidth === 0) return false; return img.naturalHeight >= CONFIG.minImageHeight && img.naturalWidth >= CONFIG.minImageWidth; }); } 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; return isComplete && hasSize && isLargeEnough; }); console.log('Filtered potential images:', potential.length); // iframe内の画像のsrcをフルURLに変換 potential.forEach(img => { let src = img.src; if (!src.startsWith('http')) { try { const iframeUrl = new URL(iframe.src); src = new URL(src, iframeUrl.origin).href; 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 showDetectionMethodDialog() { const dialog = document.createElement('div'); dialog.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 20px; border-radius: 8px; z-index: 10002; box-shadow: 0 4px 20px rgba(0,0,0,0.5); max-width: 400px; color: black; font-family: sans-serif; `; dialog.innerHTML = ` <h3 style="margin-top:0;">画像検出設定</h3> <p>検出方法を選択してください:</p> <div style="display:flex; flex-direction:column; gap:8px;"> <button data-mode="default" style="padding:8px; cursor:pointer;">🤖 自動判別</button> <button data-mode="selector:reading-content" style="padding:8px; cursor:pointer;">📖 reading-content セレクタ</button> <button data-mode="selector:chapter-content" style="padding:8px; cursor:pointer;">📖 chapter-content セレクタ</button> <button data-mode="selector:manga-reader" style="padding:8px; cursor:pointer;">📖 manga-reader セレクタ</button> <button data-mode="iframe" style="padding:8px; cursor:pointer;">📄 iframe内を検索</button> <button data-mode="normal" style="padding:8px; cursor:pointer;">🔍 通常検出(再試行)</button> <button data-close="true" style="padding:8px; cursor:pointer; background:#ddd;">✖ キャンセル</button> </div> `; dialog.addEventListener('click', (e) => { const mode = e.target.dataset.mode; const close = e.target.dataset.close; if (mode) { // 選択された方法で再試行 + サイト設定に保存 setDetectionMode(window.location.hostname, mode); dialog.remove(); // 即座に再実行 setTimeout(() => { refreshImages(); if (images.length >= CONFIG.minMangaImageCount) { showSpreadPage(0); } else { showMessage('この方法でも画像が見つかりませんでした', 'rgba(200,0,0,0.8)'); } }, 100); } else if (close) { dialog.remove(); } }); document.body.appendChild(dialog); } // ---------- 画像更新処理 ---------- 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(); } // 通常モードでのみ監視を設定 const currentMode = getDetectionMode(); if (currentMode === 'default' || currentMode === 'normal') { 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'; } const currentMode = getDetectionMode(); const isIframeMode = (currentMode === 'iframe'); if (isIframeMode) { loadAllImagesFromIframe(buttonElement); } else { loadAllImagesFromDocument(buttonElement); } } function loadAllImagesFromIframe(buttonElement) { 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(); finishLoadAll(buttonElement); }, 100); } } scrollAndLoad(); } catch (e) { console.log("iframe scroll failed:", e); finishLoadAll(buttonElement); } } function loadAllImagesFromDocument(buttonElement) { 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(); finishLoadAll(buttonElement); }, 100); } } scrollAndLoad(); } function finishLoadAll(buttonElement) { if (buttonElement) { buttonElement.textContent = '🔥全読込'; buttonElement.style.opacity = '0.8'; buttonElement.dataset.loading = '0'; } showMessage(`${images.length}枚の画像を検出しました`); } 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; const targetDocument = document; const targetWindow = 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; left: 50%; transform: translateX(-50%); 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); // --- 全画面ボタン(右下上) --- fullscreenBtn = targetDocument.createElement('button'); fullscreenBtn.setAttribute('data-mv-ui','1'); fullscreenBtn.textContent = '⛶'; fullscreenBtn.style.cssText = ` position:absolute; bottom:80px; right:20px; background:rgba(0,0,0,0.5); color:white; border:none; font-size:14px; padding:4px 8px; border-radius:6px; cursor:pointer; font-family:monospace; `; fullscreenBtn.addEventListener('click', e => { e.stopPropagation(); e.preventDefault(); toggleFullscreen(); }); container.appendChild(fullscreenBtn); // --- ページカウンター(右下下) --- const pageCounter = targetDocument.createElement('div'); pageCounter.id = 'mv-page-counter'; pageCounter.setAttribute('data-mv-ui','1'); pageCounter.style.cssText = ` position:absolute; bottom:40px; 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); // --- 背景切替(左下) --- bgToggleBtn = targetDocument.createElement('button'); bgToggleBtn.setAttribute('data-mv-ui','1'); bgToggleBtn.style.cssText = ` position:absolute; bottom:40px; left:20px; background:rgba(0,0,0,0.5); color:white; border:none; font-size:14px; padding:4px 8px; border-radius:6px; cursor:pointer; font-family:monospace; `; bgToggleBtn.textContent = (getBgColor() === '#F5F5F5') ? '背景:白' : '背景:黒'; bgToggleBtn.addEventListener('click', e => { e.stopPropagation(); e.preventDefault(); toggleBgColor(); }); container.appendChild(bgToggleBtn); 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 pageCounter = document.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; text-align: center; line-height: 32px; vertical-align: middle; 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) { // 成功:そのまま表示 // 自動判別で検出された場合は、設定を保存 if (detectedMode && getDetectionMode() === 'default') { setDetectionMode(window.location.hostname, detectedMode); } showSpreadPage(0); } else { // 失敗:検出方法選択ダイアログを表示 showDetectionMethodDialog(); } }); // 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() { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', addToggleButton); } else { addToggleButton(); } // 全画面監視の設定 setupFullscreenListeners(); // ---------- 動的監視(通常モードのみ) ---------- const detectionMode = getDetectionMode(); if ((detectionMode === 'default' || detectionMode === 'normal') && 'IntersectionObserver' in window) { io = new IntersectionObserver(entries => { if (entries.some(e => e.isIntersecting)) scheduleRefreshImages(); }, { root:null, rootMargin:'200px 0px', threshold:0.01 }); } if (detectionMode === 'default' || detectionMode === 'normal') { 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) { // 自動判別で検出された場合は、設定を保存 if (detectedMode && getDetectionMode() === 'default') { setDetectionMode(window.location.hostname, detectedMode); } showSpreadPage(0); } else { showDetectionMethodDialog(); } }, 300); }); GM_registerMenuCommand("─── 検出モード設定 ───", () => {}); // 現在の検出結果表示 GM_registerMenuCommand(`🔍 判定: ${getCurrentDetectionResultDisplay()}`, () => { const mode = getDetectionMode(); if (mode === 'default') { if (detectedMode) { const modeInfo = DETECTION_MODES[detectedMode]; const description = modeInfo ? modeInfo.description : detectedMode; showMessage(`検出結果: ${description}`, 'rgba(100,100,100,0.8)'); } else { showMessage('検出結果: 不明(画像が見つかりませんでした)', 'rgba(200,100,0,0.8)'); } } else { const modeInfo = DETECTION_MODES[mode]; const description = modeInfo ? modeInfo.description : mode; showMessage(`設定済み: ${description}`, 'rgba(100,100,100,0.8)'); } }); GM_registerMenuCommand("⚙️ 画像検出設定...", () => { showDetectionMethodDialog(); }); GM_registerMenuCommand("─── サイト設定 ───", () => {}); GM_registerMenuCommand("👁️ このサイトでボタンを表示", () => { setSiteMode(window.location.hostname, 'show'); }); GM_registerMenuCommand("🚫 このサイトでボタンを非表示", () => { setSiteMode(window.location.hostname, 'hide'); }); GM_registerMenuCommand("⚠️ 記憶したサイト設定をリセット ⚠️", () => { if (confirm('記憶したサイト設定をリセットしますか?\n(現在のページもリロードされます)')) { localStorage.removeItem('mangaViewerDomains'); localStorage.removeItem('mangaViewerBg'); // 検出モード設定もリセット const hostname = window.location.hostname; const detectionKey = `mangaDetectionMode_${hostname}`; localStorage.removeItem(detectionKey); 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或关注我们的公众号极客氢云获取最新地址