// ==UserScript== // @name desuarchive manga reader // @namespace http://tampermonkey.net/ // @version 1.0 // @description Dual-page manga reading mode for desuarchive threads with navigation controls and backlinks display // @author sakanon // @match *://desuarchive.org/a/thread/* // @grant none // @license MIT // @downloadURL none // ==/UserScript== (function() { 'use strict'; let active = false; let currentIndex = 0; let images = []; document.querySelector('.post_is_op').classList.add('post'); // Add toggle button const toggleButton = document.createElement('button'); toggleButton.innerText = '📖'; toggleButton.style.position = 'fixed'; toggleButton.style.bottom = '10px'; toggleButton.style.right = '10px'; toggleButton.style.zIndex = 9999; document.body.appendChild(toggleButton); toggleButton.addEventListener('click', () => { active = !active; if (active) { enterReadingMode(); } else { exitReadingMode(); } }); async function enterReadingMode() { document.body.style.overflowY = 'hidden'; toggleButton.style.display = 'none'; images = Array.from(document.querySelectorAll('.thread_image_link')).map(a => { return { src: a.href.endsWith('.webm') ? a.href.replace('/thumb/', '/image/').replace('.webm', 's.jpg') : a.href, postId: a.closest('.post').id, backlinks: a.closest('.post').querySelector('.backlink_list')?.innerHTML || "" }; }); currentIndex = 0; await showImages(); document.addEventListener('keydown', navigateImages); } function exitReadingMode() { document.body.style.overflowY = 'auto'; toggleButton.style.display = 'block'; const overlay = document.getElementById('reading-overlay'); if (overlay) overlay.remove(); document.removeEventListener('keydown', navigateImages); } async function showImages() { const overlay = document.getElementById('reading-overlay') || createOverlay(); overlay.innerHTML = ''; // Clear previous images // Create elements for the first image const img1 = await createImage(images[currentIndex].src); const img1Backlinks = createBacklinks(images[currentIndex].backlinks, true); // Create elements for the second image (if it exists and both images are portrait) const img2 = ((currentIndex != 0) && (currentIndex + 1 < images.length) && await isPortrait(images[currentIndex].src) && await isPortrait(images[currentIndex + 1].src)) ? await createImage(images[currentIndex + 1].src) : null; const img2Backlinks = img2 ? createBacklinks(images[currentIndex + 1].backlinks, false) : null; const pageWrapper = document.createElement('div'); pageWrapper.style.display = 'flex'; pageWrapper.style.justifyContent = 'center'; pageWrapper.style.flexDirection = 'row-reverse'; // For right to left reading if (img1) pageWrapper.appendChild(img1); if (img2) pageWrapper.appendChild(img2); overlay.appendChild(pageWrapper); if (img1Backlinks) overlay.appendChild(img1Backlinks); if (img2Backlinks) overlay.appendChild(img2Backlinks); document.body.appendChild(overlay); } async function navigateImages(e) { if (e.key === 'ArrowLeft' && currentIndex + 1 < images.length) { currentIndex += (await isPortrait(images[currentIndex].src) && currentIndex != 0 && await isPortrait(images[currentIndex + 1].src)) + 1; await showImages(); } else if (e.key === 'ArrowRight' && currentIndex > 0) { currentIndex -= (await isPortrait(images[currentIndex].src) && (currentIndex - 1) > 0 && await isPortrait(images[currentIndex - 1].src)) + 1; await showImages(); } else if (e.key === 'Escape') { exitReadingMode(); } else if (e.key === 'o') { offsetPages(); } } function createOverlay() { const overlay = document.createElement('div'); overlay.id = 'reading-overlay'; overlay.style.position = 'fixed'; overlay.style.top = '0'; overlay.style.left = '0'; overlay.style.width = '100%'; overlay.style.height = '100%'; overlay.style.backgroundColor = 'rgba(0, 0, 0, 0.9)'; overlay.style.zIndex = 9998; overlay.style.overflowY = 'hidden'; overlay.style.display = 'flex'; overlay.style.alignItems = 'center'; overlay.style.justifyContent = 'center'; return overlay; } async function createImage(src) { return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => { img.style.maxHeight = '100vh'; img.style.maxWidth = (img.width < img.height) ? '50vw' : '100vw'; resolve(img); }; img.onerror = () => { reject(new Error('Failed to load image')); }; img.src = src; }); } function createBacklinks(backlinksHtml, isRight) { if (!backlinksHtml) return null; const backlinksDiv = document.createElement('div'); backlinksDiv.innerHTML = backlinksHtml; backlinksDiv.style.margin = '5px'; backlinksDiv.style.cursor = 'pointer'; backlinksDiv.className = 'backlink_list'; backlinksDiv.style.position = 'fixed'; backlinksDiv.style.top = '0'; backlinksDiv.style.width = '50%'; backlinksDiv.style.fontSize = '11px'; if (isRight) { backlinksDiv.style.right = '0'; backlinksDiv.style.textAlign = 'right'; } else { backlinksDiv.style.left = '0'; } // Add hover event to display post content backlinksDiv.querySelectorAll('.backlink').forEach(link => { link.addEventListener('mouseenter', () => { const postId = link.href.split('#')[1]; const post = document.getElementById(postId); if (post) { const tooltip = createTooltip(post.innerHTML); if (isRight) { tooltip.style.right = '0'; } else { tooltip.style.left = '0'; } link.appendChild(tooltip); } }); link.addEventListener('mouseleave', () => { const tooltip = link.querySelector('.replytooltip'); if (tooltip) tooltip.remove(); }); }); return backlinksDiv; } function createTooltip(content) { const tooltip = document.createElement('div'); tooltip.className = 'replytooltip'; tooltip.innerHTML = content; tooltip.style.position = 'absolute'; tooltip.style.color = 'white'; tooltip.style.padding = '5px'; tooltip.style.zIndex = '10000'; tooltip.style.fontSize = '10pt'; tooltip.style.wordBreak = 'break-word'; return tooltip; } async function isPortrait(src) { return new Promise((resolve, reject) => { const img = new Image(); img.src = src; img.onload = () => { resolve(img.width < img.height); }; img.onerror = () => { reject(new Error('Failed to load image')); }; }); } function offsetPages() { currentIndex += 1; showImages(); } })();