// ==UserScript== // @name Jellyfin/Emby 显示剧照 (Show Jellyfin/Emby images under the extrafanart folder) // @namespace http://tampermonkey.net/ // @version 1.0.7 // @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 = `
abc
` const zoomedImage = zoomedMask.querySelector('#jv-zoom-img') const zoomedImageWrapper = zoomedMask.querySelector('#jv-zoom-img-wrapper') zoomedImageWrapper.addEventListener('click', handleRightBtnClick) const zoomedImageDesc = zoomedMask.querySelector('#jv-zoom-img-desc') 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() { zoomedImageWrapper.classList.remove('do-zoom'); curZoomIndex = 0 zoomedImageDesc.innerHTML = '' setTimeout(() => { zoomedMask.style.display = 'none' }, 400) } zoomedMask.onclick = hideZoomedMask function showZoomedMask(index) { const newSrc = getBgImageSrc(index) if (!newSrc) return zoomedMask.style.display = 'flex'; zoomedImage.src = newSrc setTimeout(() => { zoomedImageWrapper.classList.add('do-zoom'); }) zoomedImageDesc.innerHTML = `${curZoomIndex - 1} of ${maxImageCount - 1}` } 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.startsWith('#!/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; 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) })();