您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
画像が縦一列表示となっているサイト上で起動後、右上アイコンクリックで見開き表示(iframe系のサイト対応メニュー追加)
当前为
// ==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或关注我们的公众号极客氢云获取最新地址