您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
画像が縦一列表示となっているサイト上で起動後、右上アイコンクリックで見開き表示に切り替える(遅延読み込み対応)
当前为
// ==UserScript== // @name マンガ見開きビューア // @namespace http://2chan.net/ // @version 1.2 // @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. // @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, containerMaxWidth: '95vw', imageMaxHeight: '90vh', enableKeyControls: true, enableMouseWheel: true, minMangaImageCount: 2, defaultBg: '#333333', // Dark 初期 refreshDebounceMs: 250, }; let currentPage = 0; // 0-based, 表示は [currentPage, currentPage+1] let images = []; // 検出済み <img> の配列(縦位置順) let container = null; let imageArea = null; let pageInfo = null; let bgToggleBtn = null; let toggleButton = null; // 見開き表示ボタンの参照を保持 let io = null; // IntersectionObserver const watched = new WeakSet(); // 監視済みimg let refreshTimer = null; let currentDomain = window.location.hostname; // 現在のドメイン // ---------- ドメイン学習システム ---------- function getDomainSettings() { const saved = localStorage.getItem('mangaViewerDomains'); return saved ? JSON.parse(saved) : {}; } function saveDomainSettings(domains) { localStorage.setItem('mangaViewerDomains', JSON.stringify(domains)); } function setDomainStatus(domain, status) { const domains = getDomainSettings(); domains[domain] = status; saveDomainSettings(domains); } function getDomainStatus(domain) { const domains = getDomainSettings(); return domains[domain] || 'unknown'; // unknown, show, hide } function shouldShowButton() { const status = getDomainStatus(currentDomain); return status === 'show'; // showのみ表示(ホワイトリスト方式) } // ---------- 背景色 ---------- function getBgColor() { return localStorage.getItem('mangaViewerBg') || CONFIG.defaultBg; } function modeFromColor(color) { return color === '#ffffff' ? '背景:白' : '背景:黒'; } function toggleBgColor() { const newColor = getBgColor() === '#333333' ? '#ffffff' : '#333333'; setBgColor(newColor); } function setBgColor(color) { localStorage.setItem('mangaViewerBg', color); if (container) container.style.background = color; if (bgToggleBtn) bgToggleBtn.textContent = modeFromColor(color); } // ---------- 画像検出 ---------- function detectMangaImages() { 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' ]; // 1. 基本条件でフィルタリング 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; const area = img.naturalWidth * img.naturalHeight; if (area > 15000000 || area < 50000) return false; return true; }); if (potential.length < CONFIG.minMangaImageCount) return []; // 2. 重複除去(同じsrcの画像は除外) const uniqueImages = []; const seenSrcs = new Set(); for (const img of potential) { if (!seenSrcs.has(img.src)) { seenSrcs.add(img.src); uniqueImages.push(img); } } // 3. 連番ソート(ファイル名に数字が含まれる場合) const sortedImages = uniqueImages.sort((a, b) => { // まずY座標でソート(基本位置) const ra = a.getBoundingClientRect(); const rb = b.getBoundingClientRect(); const yDiff = ra.top - rb.top; // Y座標の差が50px以内なら、ファイル名の数字で比較 if (Math.abs(yDiff) <= 50) { const aNumbers = extractNumbers(a.src); const bNumbers = extractNumbers(b.src); // 両方に数字がある場合、数字順でソート if (aNumbers.length > 0 && bNumbers.length > 0) { for (let i = 0; i < Math.min(aNumbers.length, bNumbers.length); i++) { const diff = aNumbers[i] - bNumbers[i]; if (diff !== 0) return diff; } } } // デフォルトはY座標順 return yDiff; }); return sortedImages; } // ファイル名から数字を抽出する関数 function extractNumbers(url) { // URLからファイル名部分を抽出 const filename = url.split('/').pop().split('?')[0]; // 数字の連続を全て抽出 const matches = filename.match(/\d+/g); return matches ? matches.map(num => parseInt(num, 10)) : []; } // 検出リスト更新(デバウンス) 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(); } } // 新しいimgに監視を付与 newImages.forEach(img => attachWatchers(img)); } // 遅延読み込み対策:intersect/load を監視 function attachWatchers(img) { if (watched.has(img)) return; watched.add(img); // 画像ロード完了時に更新 img.addEventListener('load', scheduleRefreshImages, { once: true }); // まだ読み込まれていない && IO が使えるなら監視 if (io && !img.complete) { io.observe(img); } } // ---------- ビューア生成(1回だけ) ---------- function createSpreadViewerOnce() { if (container) return; container = document.createElement('div'); container.id = 'manga-spread-viewer'; 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 = document.createElement('div'); imageArea.style.cssText = ` display:flex; flex-direction:row-reverse; /* 右→左で表示 */ justify-content:center; align-items:center; max-width:${CONFIG.containerMaxWidth}; max-height:${CONFIG.imageMaxHeight}; gap:2px; margin-top: -15px; `; // --- ナビゲーション --- const nav = document.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; `; const mkBtn = (label, onClick) => { const b = document.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; }; // 表記:←次 / 前→(RTLの文脈で「次」は左方向) const btnNextSpread = mkBtn('←次', () => nextPage(2)); // 2ページ進む const btnNextSingle = mkBtn('←単', () => nextPage(1)); // 1ページ進む pageInfo = document.createElement('span'); pageInfo.setAttribute('data-mv-ui', '1'); pageInfo.style.cssText = 'min-width: 120px; text-align: center; font-family: monospace;'; // 固定幅フォントで位置安定化 const btnPrevSingle = mkBtn('単→', () => prevPage(1)); // 1ページ戻る const btnPrevSpread = mkBtn('前→', () => prevPage(2)); // 2ページ戻る nav.appendChild(btnNextSpread); nav.appendChild(btnNextSingle); nav.appendChild(pageInfo); nav.appendChild(btnPrevSingle); nav.appendChild(btnPrevSpread); // --- 閉じるボタン --- const closeBtn = document.createElement('button'); closeBtn.type = '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'; }); // --- 右上の全画像読み込みボタン --- const loadAllBtnTop = document.createElement('button'); loadAllBtnTop.type = 'button'; loadAllBtnTop.setAttribute('data-mv-ui', '1'); loadAllBtnTop.textContent = '🔥全読込'; loadAllBtnTop.title = 'ページ全体をスクロールして全ての画像を読み込み'; 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); }); // --- 右下の全画像読み込みボタンを削除 --- // --- 背景トグル --- bgToggleBtn = document.createElement('button'); bgToggleBtn.type = '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 = modeFromColor(getBgColor()); bgToggleBtn.addEventListener('click', (e) => { e.stopPropagation(); e.preventDefault(); toggleBgColor(); }); container.appendChild(imageArea); container.appendChild(nav); container.appendChild(closeBtn); container.appendChild(loadAllBtnTop); container.appendChild(bgToggleBtn); document.body.appendChild(container); // --- コンテナ内クリック(画像/背景のみで有効) --- container.addEventListener('click', (e) => { // UI要素上のクリックは無視 if (e.target.closest('[data-mv-ui="1"]')) return; const rect = container.getBoundingClientRect(); const x = e.clientX - rect.left; const center = rect.width / 2; if (x > center) { // 右半分:前(2ページ戻る) prevPage(2); } else { // 左半分:次(2ページ進む) nextPage(2); } }); // マウスホイール if (CONFIG.enableMouseWheel) { container.addEventListener('wheel', (e) => { e.preventDefault(); if (e.deltaY > 0) nextPage(2); else prevPage(2); }, { passive: false }); } } // ---------- 全画像読み込み機能 ---------- function loadAllImages(buttonElement) { if (buttonElement) { buttonElement.textContent = '🔥読込中...'; buttonElement.style.opacity = '0.5'; } 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); // 画面1個分ずつスクロール(高速化) function scrollAndLoad() { // 高速スクロール currentScroll += scrollStep; window.scrollTo(0, currentScroll); // 画像検出を更新 scheduleRefreshImages(); if (currentScroll < documentHeight - viewportHeight) { // まだスクロールが必要(間隔を短縮:10msに) setTimeout(scrollAndLoad, 10); } else { // スクロール完了 setTimeout(() => { // 元の位置に戻す window.scrollTo(0, originalScrollTop); // 最終的な画像検出 refreshImages(); // ボタンを元に戻す if (buttonElement) { buttonElement.textContent = '🔥全読込'; buttonElement.style.opacity = '0.8'; } // 完了メッセージ const msg = document.createElement('div'); msg.textContent = `${images.length}枚の画像を検出しました`; msg.style.cssText = ` position: fixed; top: 80px; left: 50%; transform: translateX(-50%); z-index: 10001; background: rgba(0,150,0,0.8); color: white; padding: 8px 12px; border-radius: 4px; font-size: 12px; pointer-events: none; `; document.body.appendChild(msg); setTimeout(() => msg.remove(), 2000); }, 100); // 少し待ってから元の位置に戻す } } scrollAndLoad(); } 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 = ''; // row-reverse なので pageNum が右、pageNum+1 が左に並ぶ for (let i = 0; i < 2; i++) { const idx = pageNum + i; if (idx < images.length) { const img = document.createElement('img'); img.src = images[idx].src; img.style.cssText = ` max-height:${CONFIG.imageMaxHeight}; max-width:50vw; object-fit:contain; `; imageArea.appendChild(img); } } currentPage = pageNum; updatePageInfoOnly(); container.style.display = 'flex'; } function updatePageInfoOnly() { if (!pageInfo) return; const start = Math.min(currentPage + 1, images.length); const end = Math.min(currentPage + 2, images.length); // 3桁まで固定幅で表示(例:001-002 / 123) const startStr = start.toString().padStart(3, '0'); const endStr = end.toString().padStart(3, '0'); const totalStr = images.length.toString().padStart(3, '0'); pageInfo.textContent = `${startStr}-${endStr} / ${totalStr}`; } function nextPage(step = 2) { const target = currentPage + step; if (target < images.length) showSpreadPage(target); } function prevPage(step = 2) { const target = currentPage - step; if (target >= 0) showSpreadPage(target); } // ---------- キー操作 ---------- if (CONFIG.enableKeyControls) { document.addEventListener('keydown', (e) => { if (!container || container.style.display !== 'flex') return; switch (e.key) { case 'ArrowLeft': // 画面左:次(2ページ進む) case ' ': e.preventDefault(); nextPage(2); break; case 'ArrowRight': // 画面右:前(2ページ戻る) 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.type = 'button'; toggleButton.textContent = '📖'; // 本の絵文字で目立たな toggleButton.title = '見開き表示'; // ツールチップで機能説明 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; `; toggleButton.onmouseenter = () => toggleButton.style.opacity = '1'; toggleButton.onmouseleave = () => toggleButton.style.opacity = '0.7'; toggleButton.addEventListener('click', () => { // クリック時に改めて画像を検出 refreshImages(); if (images.length >= CONFIG.minMangaImageCount) { // ビューア起動 = 記憶として、サイトを記憶 setDomainStatus(currentDomain, 'show'); showSpreadPage(0); } else { // ボタンクリック時は簡潔なメッセージ表示(アラートなし) const msg = document.createElement('div'); msg.textContent = '漫画画像が見つかりません'; msg.style.cssText = ` position: fixed; top: 60px; right: 40px; z-index: 10000; background: rgba(0,0,0,0.8); color: white; padding: 8px 12px; border-radius: 4px; font-size: 14px; pointer-events: none; `; document.body.appendChild(msg); setTimeout(() => msg.remove(), 2000); } }); document.body.appendChild(toggleButton); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', addToggleButton); } else { addToggleButton(); } // ---------- Tampermonkey 右クリックメニュー ---------- if (typeof GM_registerMenuCommand !== 'undefined') { // 見開きビューアを手動起動 GM_registerMenuCommand("📖 見開きビューアを起動", () => { // 300ms待機してから画像検出を実行し、遅延読み込みに対応 setTimeout(() => { refreshImages(); if (images.length >= CONFIG.minMangaImageCount) { setDomainStatus(currentDomain, 'show'); showSpreadPage(0); } else { // アラートの代わりにメッセージチップを表示 const msg = document.createElement('div'); msg.textContent = '漫画画像が見つかりません'; msg.style.cssText = ` position: fixed; top: 20px; left: 50%; transform: translateX(-50%); z-index: 10001; background: rgba(0,0,0,0.8); color: white; padding: 10px 16px; border-radius: 6px; font-size: 14px; pointer-events: none; `; document.body.appendChild(msg); setTimeout(() => msg.remove(), 2500); } }, 300); }); // 現在のドメインでの表示状態に応じてメニューを切り替え const status = getDomainStatus(currentDomain); GM_registerMenuCommand("👁️ このサイトでボタンを表示", () => { setDomainStatus(currentDomain, 'show'); location.reload(); // ページをリロードして反映 }); if (status === 'show') { GM_registerMenuCommand("🚫 このサイトでボタンを非表示", () => { setDomainStatus(currentDomain, 'hide'); if (toggleButton) { toggleButton.remove(); toggleButton = null; } const msg = document.createElement('div'); msg.textContent = `${currentDomain} で見開きボタンを非表示にしました`; msg.style.cssText = ` position: fixed; top: 20px; left: 50%; transform: translateX(-50%); z-index: 10000; background: rgba(0,0,0,0.8); color: white; padding: 12px 20px; border-radius: 6px; font-size: 14px; `; document.body.appendChild(msg); setTimeout(() => msg.remove(), 3000); }); } // --- 設定リセット GM_registerMenuCommand("⚠️ 記憶したサイトを全リセット ⚠️", () => { if (confirm('記憶したすべてのサイト設定をリセットしますか?\n(現在のページもリロードされます)')) { // LocalStorageから設定を削除 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: 10000; background: rgba(255,0,0,0.8); color: white; padding: 12px 20px; border-radius: 6px; font-size: 14px; `; document.body.appendChild(msg); // 1秒後にリロード setTimeout(() => { location.reload(); }, 1000); } }); } // ---------- 動的コンテンツ対応 ---------- // IO: 画面に入ったら更新(読み込み契機) if ('IntersectionObserver' in window) { io = new IntersectionObserver((entries) => { let hit = false; entries.forEach(entry => { if (entry.isIntersecting) hit = true; }); if (hit) scheduleRefreshImages(); }, { root: null, rootMargin: '200px 0px', threshold: 0.01 }); } // Mutation: 新規imgを監視対象に const mo = new MutationObserver((mutations) => { let foundImg = false; for (const m of mutations) { m.addedNodes && m.addedNodes.forEach(node => { if (node && node.nodeType === 1) { if (node.tagName === 'IMG') { attachWatchers(node); foundImg = true; } else { // 子孫にIMGがある可能性 node.querySelectorAll && node.querySelectorAll('img').forEach(attachWatchers); } } }); } if (foundImg) scheduleRefreshImages(); }); mo.observe(document.body, { childList: true, subtree: true}); // 初期検出 refreshImages(); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址