// ==UserScript==
// @name Jellyfin/Emby 显示剧照 (Show Jellyfin/Emby images under the extrafanart folder)
// @namespace http://tampermonkey.net/
// @version 1.1.0
// @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 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 = `
![]()
abc
`
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));
document.addEventListener('viewshow', () => setTimeout(debounceLoadImages));
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)
}());