// ==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();
})();