Greasy Fork

desuarchive manga reader

Dual-page manga reading mode for desuarchive threads with navigation controls and backlinks display

目前为 2024-08-29 提交的版本。查看 最新版本

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