// ==UserScript== // @name Jellyfin/Emby 显示剧照 (Show Jellyfin/Emby images under the extrafanart folder) // @namespace http://tampermonkey.net/ // @version 1.0.3 // @description Jellyfin/Emby 显示剧照 // @author Squirtle // @match *://*/web/index.html* // @match *://*/*/web/index.html* // @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw== // @grant GM_addStyle // @license MIT // @downloadURL none // ==/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 = ` ` 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(100px); box-shadow: 0 0 20px rgba(0, 0, 0, 1); 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) })();