(function () { 'use strict'; var POLL_INTERVAL_MS = 1000; var OVERVIEW_MAX_LENGTH = 160; var overlayEl = null; var detailEl = null; var personCache = {}; function truncate(text, maxLength) { if (text.length <= maxLength) { return text; } return text.slice(0, maxLength).trim() + '…'; } function getPerson(jellyfinId) { if (personCache[jellyfinId]) { return personCache[jellyfinId]; } var promise = window.ApiClient.getItem(window.ApiClient.getCurrentUserId(), jellyfinId) .catch(function () { return null; }); personCache[jellyfinId] = promise; return promise; } function getNowPlaying() { if (!window.ApiClient) { return Promise.reject(new Error('no ApiClient')); } var url = window.ApiClient.getUrl('Sessions', { DeviceId: window.ApiClient.deviceId() }); return window.ApiClient.ajax({ url: url, type: 'GET', dataType: 'json' }).then(function (sessions) { var session = sessions && sessions[0]; if (!session || !session.NowPlayingItem || !session.PlayState) { return null; } return { itemId: session.NowPlayingItem.Id, positionSeconds: (session.PlayState.PositionTicks || 0) / 10000000 }; }); } function removeDetail() { if (detailEl && detailEl.parentNode) { detailEl.parentNode.removeChild(detailEl); } detailEl = null; document.removeEventListener('keydown', onDetailKeydown, true); } function removeOverlay() { removeDetail(); if (overlayEl && overlayEl.parentNode) { overlayEl.parentNode.removeChild(overlayEl); } overlayEl = null; } function onDetailKeydown(event) { // Back button on TV remotes / keyboards maps to Escape / Backspace. if (event.key === 'Escape' || event.key === 'Backspace' || event.keyCode === 27 || event.keyCode === 8) { event.stopPropagation(); event.preventDefault(); removeDetail(); } } // A larger, closeable "pop-up" card shown over the video when an actor is // clicked. It stays inside the player so playback position is never lost. function showDetail(container, actor, person) { removeDetail(); detailEl = document.createElement('div'); detailEl.className = 'jrayActorDetail'; detailEl.style.position = 'absolute'; detailEl.style.top = '0'; detailEl.style.left = '0'; detailEl.style.right = '0'; detailEl.style.bottom = '0'; detailEl.style.zIndex = '10000'; detailEl.style.display = 'flex'; detailEl.style.alignItems = 'center'; detailEl.style.justifyContent = 'center'; detailEl.style.background = 'rgba(0, 0, 0, 0.6)'; detailEl.style.pointerEvents = 'auto'; // Click on the dimmed backdrop closes the pop-up. detailEl.addEventListener('click', function (event) { if (event.target === detailEl) { removeDetail(); } }); var panel = document.createElement('div'); panel.style.position = 'relative'; panel.style.display = 'flex'; panel.style.gap = '24px'; panel.style.maxWidth = '720px'; panel.style.width = '80%'; panel.style.maxHeight = '80%'; panel.style.overflowY = 'auto'; panel.style.background = 'rgba(20, 20, 20, 0.96)'; panel.style.color = '#fff'; panel.style.padding = '24px'; panel.style.borderRadius = '10px'; panel.style.boxShadow = '0 8px 32px rgba(0, 0, 0, 0.6)'; if (person && person.ImageTags && person.ImageTags.Primary) { var img = document.createElement('img'); img.src = window.ApiClient.getImageUrl(actor.jellyfin_id, { type: 'Primary', maxHeight: 400, tag: person.ImageTags.Primary }); img.style.height = '300px'; img.style.width = 'auto'; img.style.borderRadius = '8px'; img.style.objectFit = 'cover'; img.style.flexShrink = '0'; panel.appendChild(img); } var text = document.createElement('div'); text.style.flex = '1'; var name = document.createElement('div'); name.style.fontWeight = 'bold'; name.style.fontSize = '24px'; name.style.marginBottom = '12px'; name.textContent = actor.name; text.appendChild(name); if (person && person.Overview) { var overview = document.createElement('div'); overview.style.fontSize = '15px'; overview.style.lineHeight = '1.5'; overview.style.opacity = '0.9'; overview.textContent = person.Overview; text.appendChild(overview); } panel.appendChild(text); var closeBtn = document.createElement('button'); closeBtn.type = 'button'; closeBtn.setAttribute('aria-label', 'Close'); closeBtn.textContent = '✕'; closeBtn.style.position = 'absolute'; closeBtn.style.top = '8px'; closeBtn.style.right = '8px'; closeBtn.style.width = '32px'; closeBtn.style.height = '32px'; closeBtn.style.border = 'none'; closeBtn.style.borderRadius = '50%'; closeBtn.style.background = 'rgba(255, 255, 255, 0.15)'; closeBtn.style.color = '#fff'; closeBtn.style.fontSize = '16px'; closeBtn.style.cursor = 'pointer'; closeBtn.addEventListener('click', function (event) { event.stopPropagation(); removeDetail(); }); panel.appendChild(closeBtn); detailEl.appendChild(panel); container.appendChild(detailEl); // Back button (Escape/Backspace) closes the pop-up first. document.addEventListener('keydown', onDetailKeydown, true); } function showOverlay(video, actors) { removeOverlay(); if (!actors || actors.length === 0) { return; } var container = video.parentElement; if (!container) { return; } overlayEl = document.createElement('div'); overlayEl.className = 'jrayOverlay'; overlayEl.style.position = 'absolute'; overlayEl.style.bottom = '10%'; overlayEl.style.left = '2%'; overlayEl.style.zIndex = '9999'; overlayEl.style.display = 'flex'; overlayEl.style.flexWrap = 'wrap'; overlayEl.style.gap = '12px'; overlayEl.style.pointerEvents = 'none'; actors.forEach(function (actor) { var card = document.createElement('div'); card.className = 'jrayActorCard'; card.style.background = 'rgba(0, 0, 0, 0.75)'; card.style.color = '#fff'; card.style.padding = '8px 12px'; card.style.borderRadius = '6px'; card.style.display = 'flex'; card.style.alignItems = 'center'; card.style.gap = '10px'; card.style.maxWidth = '360px'; var textBlock = document.createElement('div'); var name = document.createElement('div'); name.style.fontWeight = 'bold'; name.style.fontSize = '16px'; name.textContent = actor.name; textBlock.appendChild(name); card.appendChild(textBlock); overlayEl.appendChild(card); if (!actor.jellyfin_id || !window.ApiClient) { return; } card.style.pointerEvents = 'auto'; card.style.cursor = 'pointer'; getPerson(actor.jellyfin_id).then(function (person) { if (!person || !overlayEl || !overlayEl.contains(card)) { return; } // Clicking the card opens the in-player detail pop-up rather than // navigating away — this keeps the current playback position. card.addEventListener('click', function (event) { event.stopPropagation(); showDetail(container, actor, person); }); if (person.ImageTags && person.ImageTags.Primary) { var img = document.createElement('img'); img.src = window.ApiClient.getImageUrl(actor.jellyfin_id, { type: 'Primary', maxHeight: 120, tag: person.ImageTags.Primary }); img.style.height = '120px'; img.style.width = 'auto'; img.style.borderRadius = '4px'; img.style.objectFit = 'cover'; card.insertBefore(img, textBlock); } if (person.Overview) { var overview = document.createElement('div'); overview.style.fontSize = '12px'; overview.style.opacity = '0.85'; overview.style.marginTop = '4px'; overview.textContent = truncate(person.Overview, OVERVIEW_MAX_LENGTH); textBlock.appendChild(overview); } }); }); container.appendChild(overlayEl); } function fetchContext(itemId, t) { if (!window.ApiClient) { return; } var url = window.ApiClient.getUrl('Plugins/JRay/Items/' + itemId + '/jray', { t: t }); window.ApiClient.ajax({ url: url, type: 'GET', dataType: 'json' }).then(function (context) { var video = document.querySelector('video'); if (!video || !video.paused) { return; } showOverlay(video, context.actors); }, function () { // No truth data (404) or request error - fail silently, never break playback. }); } function onPause() { getNowPlaying().then(function (info) { if (info) { fetchContext(info.itemId, info.positionSeconds); } }, function () { // ApiClient unavailable or request error - fail silently, never break playback. }); } function onPlay() { removeOverlay(); } function attach(video) { if (video.dataset.jrayAttached) { return; } video.dataset.jrayAttached = 'true'; video.addEventListener('pause', onPause); video.addEventListener('play', onPlay); video.addEventListener('playing', onPlay); video.addEventListener('seeking', removeOverlay); } setInterval(function () { var video = document.querySelector('video'); if (video) { attach(video); } else { removeOverlay(); } }, POLL_INTERVAL_MS); })();