Greasy Fork

desuarchive manga reader

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

目前为 2025-01-03 提交的版本。查看 最新版本

// ==UserScript==
// @name         desuarchive manga reader
// @namespace    http://tampermonkey.net/
// @version      1.2
// @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 = [];
    let loadedImages = [];
    let isLoading = false; // Track loading state

    document.addEventListener('keydown', (e) => {
        if (e.key === '`') {
            active = !active;
            active ? enterReadingMode() : exitReadingMode();
        }
    });

    async function enterReadingMode() {
        active = true;
        document.body.style.overflowY = 'hidden';

        images = Array.from(document.querySelectorAll('.thread_image_link')).map(a => {
            const post = a.closest('.post') || a.closest('.post_is_op');
            let originalMessageLink = "";
            if (post.querySelector('.text').innerHTML.trim() !== "") {
                originalMessageLink = `<span><a href="#${post.id}" class="backlink">>>O </a></span>`;
            }
            return {
                src: a.href.endsWith('.webm') ? a.href.replace('/thumb/', '/image/').replace('.webm', 's.jpg') : a.href,
                postId: post.id,
                backlinks: originalMessageLink + (post.querySelector('.backlink_list')?.innerHTML || "")
            };
        });

        if (loadedImages.length === 0) {
            loadedImages = new Array(images.length).fill(false);
        }

        currentIndex = 0;
        await showImages();
        document.addEventListener('keydown', navigateImages);
    }

    function exitReadingMode() {
        active = false;
        document.body.style.overflowY = 'auto';

        const overlay = document.getElementById('reading-overlay');
        if (overlay) overlay.remove();
        document.removeEventListener('keydown', navigateImages);
    }

    async function showImages() {
        isLoading = true; // Set loading state to true

        const overlay = document.getElementById('reading-overlay') || createOverlay();
        overlay.innerHTML = ''; // Clear previous images

        // Show loading text
        const loadingText = document.createElement('div');
        loadingText.innerText = 'Loading...';
        loadingText.style.color = 'white';
        loadingText.style.fontSize = '24px';
        loadingText.style.position = 'absolute';
        loadingText.style.top = '50%';
        loadingText.style.left = '50%';
        loadingText.style.transform = 'translate(-50%, -50%)';
        overlay.appendChild(loadingText);

        document.body.appendChild(overlay);

        try {
            // Create elements for the first image
            const img1 = await createImage(images[currentIndex].src);
            const img1Backlinks = createBacklinks(images[currentIndex].backlinks, true);
            loadedImages[currentIndex] = 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;
            if (img2) loadedImages[currentIndex + 1] = true;

            // Remove loading text once images are loaded
            overlay.innerHTML = '';

            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);

            overlay.appendChild(createPageNumber(currentIndex, true));
            if (img2) overlay.appendChild(createPageNumber(currentIndex + 1, false));

        } catch (error) {
            console.error('Error loading images:', error);
        } finally {
            isLoading = false; // Set loading state to false after images are loaded
        }
    }

    async function navigateImages(e) {
        if (e.key === 'ArrowLeft' && currentIndex + 1 < images.length && (!isLoading || loadedImages[currentIndex + 1])) {
            currentIndex += (await isPortrait(images[currentIndex].src) && currentIndex != 0 && await isPortrait(images[currentIndex + 1].src)) + 1;
            await showImages();
        } else if (e.key === 'ArrowRight' && currentIndex > 0 && (!isLoading || loadedImages[currentIndex - 1])) {
            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' && !isLoading) {
            offsetPages();
        } else if (e.key === 'f') {
            if (document.fullscreenElement) {
                document.exitFullscreen();
            } else {
                document.documentElement.requestFullscreen();
            }
        } else if (e.key === 'g') {
            const index = prompt('Enter page number:');
            if (index) {
                goToIndex(parseInt(index) - 1);
            }
        }
    }

    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('article');
        backlinksDiv.innerHTML = backlinksHtml;
        backlinksDiv.style.padding = '0';
        backlinksDiv.style.margin = '5px';
        backlinksDiv.style.cursor = 'pointer';
        backlinksDiv.className = 'thread';
        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', (e) => {
                const postId = link.href.split('#')[1];
                const post = document.getElementById(postId);
                if (post) {
                    const tooltip = createTooltip(post.innerHTML);
                    if (isRight) {
                        tooltip.style.right = `${window.innerWidth - e.clientX}px`;
                    } else {
                        tooltip.style.left = `${e.clientX}px`;
                    }
                    link.appendChild(tooltip);
                }
            });
            link.addEventListener('mouseleave', () => {
                const tooltip = link.querySelector('.replytooltip');
                if (tooltip) tooltip.remove();
            });
        });

        return backlinksDiv;
    }

    function createPageNumber(index, isRight) {
        const pageNumber = document.createElement('div');
        pageNumber.innerText = `${index + 1}`;
        pageNumber.style.position = 'fixed';
        pageNumber.style.bottom = '0';
        pageNumber.style.fontSize = '0.8em';
        pageNumber.style.color = 'white';
        pageNumber.style.padding = '5px';
        if (isRight) {
            pageNumber.style.right = '0';
        } else {
            pageNumber.style.left = '0';
        }
        return pageNumber;
    }

    function createTooltip(content) {
        const tooltip = document.createElement('div');
        tooltip.className = 'replytooltip post';
        tooltip.innerHTML = content;
        tooltip.style.position = 'absolute';
        tooltip.style.color = 'white';
        tooltip.style.zIndex = '10000';
        tooltip.style.fontSize = '10pt';
        tooltip.style.wordBreak = 'break-word';
        tooltip.style.minWidth = '300px';
        tooltip.style.backgroundColor = '#282a2e';
        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();
    }

    function goToIndex(index) {
        currentIndex = index;
        showImages();
    }
})();