From fe4b40c3dda0a71efe71c5df1abfa5fb649004c9 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 20 May 2026 22:03:03 +0000 Subject: [PATCH] Refactor plugin UI components for Jellyfin 10.11+ compatibility - Updated item detail page integration to target `.mainDetailButtons`. - Replaced rating dropdown with an icon button and custom selection dialog. - Integrated "Recommend Film" and "Add Movie" as text buttons in Library and Home views. - Aligned UI styles with Jellyfin's native `emby-button` patterns and ElegantFin theme. - Improved URL parameter parsing and added throttling to UI injection logic. Co-authored-by: devitq <118541411+devitq@users.noreply.github.com> --- .../Configuration/ui.js | 184 +++++++++++++----- 1 file changed, 134 insertions(+), 50 deletions(-) diff --git a/plugins/jellyfin/Jellyfin.Plugin.MovieNight/Configuration/ui.js b/plugins/jellyfin/Jellyfin.Plugin.MovieNight/Configuration/ui.js index 9efbad4..0e92b85 100644 --- a/plugins/jellyfin/Jellyfin.Plugin.MovieNight/Configuration/ui.js +++ b/plugins/jellyfin/Jellyfin.Plugin.MovieNight/Configuration/ui.js @@ -13,66 +13,142 @@ const showMsg = getAlert(); + function createTextButton(text, className, onClick) { + const btn = document.createElement('button'); + btn.type = 'button'; + btn.is = 'emby-button'; + btn.className = `emby-button raised ${className}`; + btn.style.margin = '0.5em'; + btn.style.padding = '0.4em 1em'; + btn.innerHTML = `${text}`; + btn.onclick = onClick; + return btn; + } + + function createIconButton(icon, title, className, onClick) { + const btn = document.createElement('button'); + btn.type = 'button'; + btn.is = 'emby-button'; + btn.className = `button-flat detailButton emby-button ${className}`; + btn.title = title; + btn.innerHTML = ` +
+ +
+ `; + btn.onclick = onClick; + return btn; + } + function injectUI() { - const headerButtons = document.querySelector('.headerViewButtons, .view-library .content-primary, .home-section .sectionTitleContainer'); - - if (headerButtons) { - if (!document.querySelector('.btnMovieNightRecommend')) { - const btn = document.createElement('button'); - btn.className = 'emby-button raised btnMovieNightRecommend'; - btn.innerHTML = 'Recommend Film'; - btn.style.marginLeft = '1em'; - btn.onclick = showRecommendation; - headerButtons.appendChild(btn); - } - - if (!document.querySelector('.btnMovieNightAddMovie')) { - const btn = document.createElement('button'); - btn.className = 'emby-button raised btnMovieNightAddMovie'; - btn.innerHTML = 'Add Movie (STRM)'; - btn.style.marginLeft = '1em'; - btn.onclick = promptAddMovie; - headerButtons.appendChild(btn); + // 1. Item Detail Page - Add icon button for rating + const detailButtons = document.querySelector('.mainDetailButtons'); + if (detailButtons && !document.querySelector('.btnMovieNightRate')) { + const itemId = getItemIdFromUrl(); + if (itemId) { + const rateBtn = createIconButton('star_rate', 'Rate on MovieNight', 'btnMovieNightRate', (e) => { + e.preventDefault(); + e.stopPropagation(); + showRatingDialog(itemId); + }); + const moreBtn = detailButtons.querySelector('.btnMoreCommands'); + if (moreBtn) { + detailButtons.insertBefore(rateBtn, moreBtn); + } else { + detailButtons.appendChild(rateBtn); + } } } - const detailButtons = document.querySelector('.itemDetailButtons, .itemDetailsButtons'); - if (detailButtons && !document.querySelector('.movieNightRatingContainer')) { - const itemId = getItemIdFromUrl(); - if (itemId) { - const container = document.createElement('div'); - container.className = 'movieNightRatingContainer'; - container.style.display = 'inline-flex'; - container.style.alignItems = 'center'; - container.style.marginLeft = '1em'; + // 2. Library Pages - Add text buttons to toolbar + const toolBar = document.querySelector('.libraryPage:not(.itemDetailPage) .flex.align-items-center.justify-content-center.focuscontainer-x'); + if (toolBar && !document.querySelector('.btnMovieNightRecommend')) { + toolBar.appendChild(createTextButton('Recommend Film', 'btnMovieNightRecommend', (e) => { + e.preventDefault(); + showRecommendation(); + })); + toolBar.appendChild(createTextButton('Add Movie (STRM)', 'btnMovieNightAddMovie', (e) => { + e.preventDefault(); + promptAddMovie(); + })); + } - const label = document.createElement('span'); - label.innerText = 'MovieNight: '; - label.style.marginRight = '0.5em'; - container.appendChild(label); - - const select = document.createElement('select'); - select.className = 'emby-select'; - select.style.padding = '0.2em'; - for (let i = 0; i <= 10; i++) { - const opt = document.createElement('option'); - opt.value = i; - opt.innerText = i === 0 ? 'Rate...' : i; - select.appendChild(opt); - } - select.onchange = (e) => submitRating(itemId, e.target.value); - container.appendChild(select); - - detailButtons.appendChild(container); - } + // 3. Home Page - Prepend a MovieNight section + const homeSections = document.querySelector('.sections.homeSectionsContainer'); + if (homeSections && !document.querySelector('.movieNightHomeButtons')) { + const section = document.createElement('div'); + section.className = 'verticalSection movieNightHomeButtons'; + section.style.padding = '0 var(--sidePadding)'; + section.innerHTML = '

MovieNight

'; + const btnContainer = section.querySelector('.movieNightBtnContainer'); + btnContainer.appendChild(createTextButton('Recommend Film', 'btnMovieNightRecommend', showRecommendation)); + btnContainer.appendChild(createTextButton('Add Movie (STRM)', 'btnMovieNightAddMovie', promptAddMovie)); + homeSections.insertBefore(section, homeSections.firstChild); } } function getItemIdFromUrl() { - const params = new URLSearchParams(window.location.search); + const queryString = window.location.hash.includes('?') ? window.location.hash.split('?')[1] : window.location.search; + const params = new URLSearchParams(queryString); return params.get('id') || params.get('itemId'); } + async function showRatingDialog(itemId) { + const overlay = document.createElement('div'); + overlay.className = 'dialogBackdrop dialogBackdropOpened'; + overlay.style.zIndex = '99998'; + overlay.style.backgroundColor = 'rgba(0,0,0,0.5)'; + overlay.style.position = 'fixed'; + overlay.style.top = '0'; + overlay.style.left = '0'; + overlay.style.right = '0'; + overlay.style.bottom = '0'; + + const dialog = document.createElement('div'); + dialog.className = 'dialog'; + dialog.style.position = 'fixed'; + dialog.style.top = '50%'; + dialog.style.left = '50%'; + dialog.style.transform = 'translate(-50%, -50%)'; + dialog.style.zIndex = '99999'; + dialog.style.padding = '2em'; + dialog.style.minWidth = '250px'; + dialog.style.backgroundColor = '#222'; + dialog.style.borderRadius = '1em'; + dialog.style.color = 'white'; + + dialog.innerHTML = ` +

Rate on MovieNight

+
+ + `; + + const grid = dialog.querySelector('.rating-grid'); + for (let i = 1; i <= 10; i++) { + const btn = document.createElement('button'); + btn.type = 'button'; + btn.is = 'emby-button'; + btn.className = 'emby-button raised'; + btn.innerText = i; + btn.style.padding = '0.5em'; + btn.onclick = async () => { + cleanup(); + await submitRating(itemId, i); + }; + grid.appendChild(btn); + } + + const cleanup = () => { + if (overlay.parentNode) document.body.removeChild(overlay); + }; + + dialog.querySelector('.btnCancel').onclick = cleanup; + overlay.onclick = (e) => { if (e.target === overlay) cleanup(); }; + + overlay.appendChild(dialog); + document.body.appendChild(overlay); + } + async function showRecommendation() { const userId = ApiClient.getCurrentUserId(); try { @@ -114,7 +190,6 @@ } async function submitRating(itemId, score) { - if (score === "0") return; const userId = ApiClient.getCurrentUserId(); try { await ApiClient.ajax({ @@ -130,7 +205,16 @@ } } - const observer = new MutationObserver(injectUI); + let timeout; + const throttledInject = () => { + if (timeout) return; + timeout = setTimeout(() => { + injectUI(); + timeout = null; + }, 100); + }; + + const observer = new MutationObserver(throttledInject); observer.observe(document.body, { childList: true, subtree: true }); injectUI();