Greasy Fork

Greasy Fork is available in English.

Jellyfin/Emby 显示剧照 (Show Jellyfin/Emby images under the extrafanart folder)

Jellyfin/Emby 显示剧照

当前为 2024-03-13 提交的版本,查看 最新版本

// ==UserScript==
// @name         Jellyfin/Emby 显示剧照 (Show Jellyfin/Emby images under the extrafanart folder)
// @namespace    http://tampermonkey.net/
// @version      1.0.9
// @description  Jellyfin/Emby 显示剧照
// @author       Squirtle
// @match        *://*/web/index.html*
// @match        *://*/*/web/index.html*
// @icon         data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @grant        GM_addStyle
// @license MIT
// ==/UserScript==

(function () {
    'use strict';

    let currentImageIndex = 0;
    let totalImageCount = 0;

    const imageContainer = createImageContainer();
    const zoomedMask = createZoomedMask();

    const zoomedImage = zoomedMask.querySelector('#jv-zoom-img');
    const zoomedImageWrapper = zoomedMask.querySelector('#jv-zoom-img-wrapper');
    const zoomedImageDescription = zoomedMask.querySelector('#jv-zoom-img-desc');
    const leftButton = zoomedMask.querySelector('.jv-left-btn');
    const rightButton = zoomedMask.querySelector('.jv-right-btn');

    function getBackgroundImageSrc(index) {
        const itemId = location.hash.match(/id\=(\w+)/)?.[1];
        return itemId && `${location.origin}/Items/${itemId}/Images/Backdrop/${index}?maxWidth=1280`;
    }

    function createImageContainer() {
        const container = document.createElement('div');
        container.id = 'jv-image-container';
        return container;
    }

    function createZoomedMask() {
        const mask = document.createElement('div')
        mask.id = 'jv-zoom-mask'
        mask.innerHTML = `
        <button class="jv-zoom-btn jv-left-btn"></button>
        <div id="jv-zoom-img-wrapper"><img id="jv-zoom-img" /><div id="jv-zoom-img-desc">abc</div></div>
        <button class="jv-zoom-btn jv-right-btn" /></button>
       `
        return mask
    }

    function showZoomedMask(index) {
        const imageSrc = getBackgroundImageSrc(index)
        if (!imageSrc) return
        zoomedMask.style.display = 'flex';
        zoomedImage.src = imageSrc
        setTimeout(() => {
            zoomedImageWrapper.classList.add('do-zoom');
        })
        zoomedImageDescription.innerHTML = `${currentImageIndex - 1} of ${totalImageCount - 1}`
    }

    function hideZoomedMask() {
        zoomedImageWrapper.classList.remove('do-zoom');
        currentImageIndex = 0
        zoomedImageDescription.innerHTML = ''
        setTimeout(() => {
            zoomedMask.style.display = 'none'
        }, 400)
    }

    function createImageElement(index) {
        const imageSrc = getBackgroundImageSrc(index);
        const imageElement = document.createElement('img');
        imageElement.src = imageSrc;
        imageElement.className = 'jv-image';
        imageElement.onclick = function () {
            currentImageIndex = index;
            showZoomedMask(index);
        };
        return imageElement;
    }

    function appendImagesToContainer(imageCount) {
        const imageFragment = document.createDocumentFragment();
        for (let index = 2; index <= imageCount; index++) {
            const imageElement = createImageElement(index);
            imageFragment.appendChild(imageElement);
        }
        imageContainer.appendChild(imageFragment);
    }

    function getVisibleElement(querySet) {
        for (const item of querySet) {
            if (parseInt(getComputedStyle(item).height) > 0) {
                return item;
            }
        }
        return null;
    }

    function showContainer(imageCount) {
        if (imageCount > 0) {
            const primaryDiv = getVisibleElement(document.querySelectorAll('#castCollapsible')) ||
                getVisibleElement(document.querySelectorAll('.peopleSection'));
            if (primaryDiv) {
                imageContainer.style.display = 'block';
                primaryDiv.insertAdjacentElement('afterend', imageContainer);
            }
        }
    }

    function isDetailsPage() {
        return location.hash.startsWith('#!/details?id=') || location.hash.startsWith('#!/item?id=');
    }

    async function getImageCount() {
        let left = 2
        let right = 22
        let found = false
        while (left <= right) {
            let mid = Math.floor((left + right) / 2)
            const newSrc = getBackgroundImageSrc(mid)
            try {
                const response = await fetch(newSrc, { method: 'HEAD' })
                if (!response.ok) throw new Error('Image not found.')
                found = true
                left = mid + 1
            } catch (error) {
                right = mid - 1
            }
        }
        return found ? right : 0
    }

    function debounce(fn, delay = 500, scope) {
        let timer = null
        return function () {
            const args = arguments
            const context = scope || this
            if (timer) {
                clearTimeout(timer)
            }
            timer = setTimeout(() => {
                fn.apply(context, args)
            }, delay)
        }
    }

    async function loadImages() {
        if (!isDetailsPage()) return;
        imageContainer.innerHTML = '';
        const imageCount = await getImageCount();
        totalImageCount = imageCount;
        appendImagesToContainer(imageCount);
        showContainer(imageCount);
    }

    const debounceLoadImages = debounce(loadImages)

    function handleLeftButtonClick(e) {
        e.stopPropagation();
        if (currentImageIndex === 0) return
        if (currentImageIndex > 2) {
            currentImageIndex--
        } else {
            currentImageIndex = totalImageCount
        }
        showZoomedMask(currentImageIndex)
    }

    function handleRightButtonClick(e) {
        e.stopPropagation();
        if (currentImageIndex === 0) return
        if (currentImageIndex >= totalImageCount) {
            currentImageIndex = 2
        } else {
            currentImageIndex++
        }
        showZoomedMask(currentImageIndex)
    }

    function handleKeydown(e) {
        if (currentImageIndex === 0) return
        e.stopPropagation()
        if (e.key === 'ArrowLeft') {
            handleLeftButtonClick(e)
        } else if (e.key === 'ArrowRight') {
            handleRightButtonClick(e)
        } else if (e.key === 'Escape') {
            hideZoomedMask()
        }
    }

    function registerEventListeners() {
        window.addEventListener('load', () => setTimeout(debounceLoadImages, 500));
        document.addEventListener('viewshow', () => setTimeout(debounceLoadImages, 500));
        document.addEventListener('keydown', handleKeydown);
        leftButton.addEventListener('click', handleLeftButtonClick);
        rightButton.addEventListener('click', handleRightButtonClick);
        zoomedMask.addEventListener('click', hideZoomedMask)
        zoomedImageWrapper.addEventListener('click', handleRightButtonClick)
    }

    function start() {
        document.body.appendChild(zoomedMask);
        registerEventListeners();
    }

    start();

    // 注入css
    const css = `
        #jv-image-container {
            display: none;
            backdrop-filter: blur(10px);
            box-shadow: 0 0 20px rgba(0, 0, 0, 0.2);
            padding: 10px;
            border-radius: 25px;
        }

        .jv-image {
            max-width: 200px;
            max-height: 200px;
            margin: 10px;
            cursor: zoom-in;
            user-select: none;
        }

        #jv-zoom-mask {
            position: fixed;
            left: 0;
            right: 0;
            top: 0;
            bottom: 0;
            background: rgba(0, 0, 0, 0.7);
            display: none;
            justify-content: space-between;
            align-items: center;
            padding: 20px;
            z-index: 1100;
            overflow: hidden;
            cursor: zoom-out;
        }

        #jv-zoom-img-wrapper {
            display: flex;
            flex-flow: column wrap;
            align-items: flex-end;
            user-select: none;
            cursor: pointer;
            transform: scale(0);
            transition: transform 0.4s ease-in-out;
        }

        #jv-zoom-img-wrapper.do-zoom {
            transform: scale(1);
        }

        #jv-zoom-img {
            width: 100%;
        }

        #jv-zoom-img-desc {
            color: #cccccc;
            font-size: 12px;
            margin-top: 4px;
        }

        .jv-zoom-btn {
            padding: 20px;
            cursor: pointer;
            background: transparent;
            border: 0;
            outline: none;
            box-shadow: none;
            opacity: 0.7;
            display: flex;
            justify-content: center;
            align-items: center
        }

        .jv-zoom-btn:hover {
            opacity: 1;
        }

        .jv-zoom-btn:before {
            content: '';
            display: block;
            width: 0;
            height: 0;
            border: medium inset transparent;
            border-top-width: 21px;
            border-bottom-width: 21px;
        }

        .jv-zoom-btn.jv-left-btn:before {
            border-right: 27px solid white;
        }

        .jv-zoom-btn.jv-right-btn:before {
            border-left: 27px solid white;
        }
    `
    GM_addStyle(css)
}());