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.5
// @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 curZoomIndex = 0
    let maxImageCount = 0

    const imageContainer = document.createElement('div')
    imageContainer.id = 'jv-image-container'

    const zoomedMask = document.createElement('div')
    zoomedMask.id = 'jv-zoom-mask'

    zoomedMask.innerHTML = `
     <button class="jv-zoom-btn jv-left-btn"></button>
     <img id="jv-zoom-img" />
     <button class="jv-zoom-btn jv-right-btn" /></button>
    `

    const zoomedImage = zoomedMask.querySelector('#jv-zoom-img')

    zoomedImage.addEventListener('click', e => e.stopPropagation())

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

    function hideZoomedMask() {
        zoomedMask.style.display = 'none'
        curZoomIndex = 0
    }
    zoomedMask.onclick = hideZoomedMask

    function showZoomedMask(index) {
        const newSrc = getBgImageSrc(index)
        if (!newSrc) return
        zoomedImage.src = newSrc
        zoomedMask.style.display = 'flex';
    }

    function getVisibleEle(list) {
        for (const item of list) {
            if (parseInt(getComputedStyle(item).height) > 0) {
                return item
            }
        }
        return null
    }

    // 二分法查找,获取图片数量
    async function getImageCount() {
        // 0、1两张是封面,从2开始,最多20张
        let left = 2
        let right = 22
        let found = false
        while (left <= right) {
            let mid = Math.floor((left + right) / 2)
            const newSrc = getBgImageSrc(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
    }

    async function load() {
        if (!location.hash.includes('#!/details?id=') && !location.hash.startsWith('#!/item?id=')) return
        imageContainer.innerHTML = ''
        document.body.appendChild(zoomedMask)
        const imageCount = await getImageCount()
        maxImageCount = imageCount
        const imageFragment = document.createDocumentFragment()
        for (let i = 2; i <= imageCount; i++) {
            const newSrc = getBgImageSrc(i)
            if (newSrc) {
                const img = document.createElement('img')
                img.src = newSrc
                img.className = 'jv-image'
                img.onclick = function () {
                    curZoomIndex = i
                    showZoomedMask(i)
                };
                imageFragment.appendChild(img)
            }
        }

        if (imageCount > 0) {
            const primaryDiv = getVisibleEle(document.querySelectorAll('#castCollapsible')) || getVisibleEle(document.querySelectorAll('.peopleSection'))
            imageContainer.appendChild(imageFragment)
            imageContainer.style.display = 'block'
            primaryDiv.insertAdjacentElement('afterend', imageContainer)
        }

    }

    window.addEventListener('load', function () {
        setTimeout(load, 500)
    })

    document.addEventListener('viewshow', function () {
        setTimeout(load, 500)
    })

    function handleLeftBtnClick(e) {
        e.stopPropagation();
        if (curZoomIndex === 0) return
        if (curZoomIndex > 2) {
            curZoomIndex--
        } else {
            curZoomIndex = maxImageCount
        }
        showZoomedMask(curZoomIndex)
    }

    function handleRightBtnClick(e) {
        e.stopPropagation();
        if (curZoomIndex === 0) return
        if (curZoomIndex >= maxImageCount) {
            curZoomIndex = 2
        } else {
            curZoomIndex++
        }
        showZoomedMask(curZoomIndex)
    }

    const leftBtn = zoomedMask.querySelector('.jv-left-btn')
    const rightBtn = zoomedMask.querySelector('.jv-right-btn')

    leftBtn.addEventListener('click', handleLeftBtnClick)
    rightBtn.addEventListener('click', handleRightBtnClick)

    document.addEventListener('keydown', e => {
        if (curZoomIndex === 0) return
        e.stopPropagation()
        if (e.key === 'ArrowLeft') {
            handleLeftBtnClick(e)
        } else if (e.key === 'ArrowRight') {
            handleRightBtnClick(e)
        } else if (e.key === 'Escape') {
            hideZoomedMask()
        }

    })

    // 注入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;
        }

        #jv-zoom-img {
            max-width: calc(100% - 220px);
            user-select: none;
        }

        .jv-zoom-btn {
            width: 110px;
            height: 90px;
            overflow: visible;
            cursor: pointer;
            background: transparent;
            border: 0;
            outline: none;
            padding: 0;
            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)

})();