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