// ==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)
})();