diff --git a/configuration/customMenuLinks-configuration.js b/configuration/customMenuLinks-configuration.js index 0aed482..d1cb489 100644 --- a/configuration/customMenuLinks-configuration.js +++ b/configuration/customMenuLinks-configuration.js @@ -154,7 +154,7 @@ `; - const modalInstance = window.ModalSystem.create({ + window.ModalSystem.create({ id: MODAL_ID, title: 'Custom Menu Links Configuration', content: content, @@ -162,6 +162,9 @@ closeOnBackdrop: true, closeOnEscape: true, showCloseButton: true, + dialogStyle: window.innerWidth >= 900 + ? { maxWidth: '90vw', width: '1400px', height: '90vh' } + : undefined, onOpen: (modal) => { const saveBtn = modal.dialogFooter.querySelector('#save-custommenulinks-config-btn'); if (saveBtn) { @@ -182,12 +185,6 @@ } }); - if (window.innerWidth >= 900) { - modalInstance.dialog.style.maxWidth = '90vw'; - modalInstance.dialog.style.width = '1400px'; - modalInstance.dialog.style.height = '90vh'; - } - LOG('Custom Menu Links configuration modal opened'); } catch (error) { ERR('Error opening custom menu links config modal:', error); diff --git a/configuration/homeScreen-configuration.js b/configuration/homeScreen-configuration.js index 4a44091..cfb2f95 100644 --- a/configuration/homeScreen-configuration.js +++ b/configuration/homeScreen-configuration.js @@ -5283,6 +5283,9 @@ window.KefinTweaksConfig = ${JSON.stringify(configToBackup, null, 2)};`; closeOnBackdrop: true, closeOnEscape: true, showCloseButton: true, + dialogStyle: window.innerWidth >= 900 + ? { maxWidth: '90vw', width: '1400px', height: '90vh' } + : undefined, onOpen: (modalInstance) => { // Attach tab switching attachTabListeners(modalInstance); @@ -5332,12 +5335,6 @@ window.KefinTweaksConfig = ${JSON.stringify(configToBackup, null, 2)};`; } }); - if (window.innerWidth >= 900) { - mainModalInstance.dialog.style.maxWidth = '90vw'; - mainModalInstance.dialog.style.width = '1400px'; - mainModalInstance.dialog.style.height = '90vh'; - } - LOG('Main configuration modal opened'); } catch (error) { ERR('Error opening config modal:', error); @@ -5353,7 +5350,6 @@ window.KefinTweaksConfig = ${JSON.stringify(configToBackup, null, 2)};`; if (Array.isArray(section.items) && section.items.length > 0) return false; if (Array.isArray(section.externalListUrls) && section.externalListUrls.length > 0) return false; if (section.discoveryType) return true; - if (section.discoveryEnabled === true) return true; const type = String(section.type || '').toLowerCase(); return ['genre', 'studio', 'person', 'collection', 'similar', 'watchlist', 'tag'].includes(type); } @@ -5736,9 +5732,9 @@ window.KefinTweaksConfig = ${JSON.stringify(configToBackup, null, 2)};`; size: 'large', closeOnBackdrop: true, closeOnEscape: true, + dialogStyle: { width: '1400px' }, onOpen: async (modalInstance) => { const container = modalInstance.dialogContent.querySelector('#preview-container'); - modalInstance.dialog.style.width = '1400px'; await renderSectionPreviewInto(previewSection, items, container); } }); diff --git a/configuration/search-configuration.js b/configuration/search-configuration.js index adad0c5..85d23fe 100644 --- a/configuration/search-configuration.js +++ b/configuration/search-configuration.js @@ -107,7 +107,7 @@ `; // Create modal - const modalInstance = window.ModalSystem.create({ + window.ModalSystem.create({ id: MODAL_ID, title: 'Search Configuration', content: content, @@ -115,6 +115,9 @@ closeOnBackdrop: true, closeOnEscape: true, showCloseButton: true, + dialogStyle: window.innerWidth >= 900 + ? { maxWidth: '90vw', width: '1400px', height: '90vh' } + : undefined, onOpen: (modal) => { // Save button const saveBtn = modal.dialogFooter.querySelector('#save-search-config-btn'); @@ -135,12 +138,6 @@ } }); - if (window.innerWidth >= 900) { - modalInstance.dialog.style.maxWidth = '90vw'; - modalInstance.dialog.style.width = '1400px'; - modalInstance.dialog.style.height = '90vh'; - } - LOG('Search configuration modal opened'); } catch (error) { ERR('Error opening search config modal:', error); diff --git a/configuration/seriesEpisodes-configuration.js b/configuration/seriesEpisodes-configuration.js index 89b5dfc..824fb1e 100644 --- a/configuration/seriesEpisodes-configuration.js +++ b/configuration/seriesEpisodes-configuration.js @@ -108,7 +108,7 @@ `; // Create modal - const modalInstance = window.ModalSystem.create({ + window.ModalSystem.create({ id: MODAL_ID, title: 'Episodes On Series Page Configuration', content: content, @@ -116,6 +116,9 @@ closeOnBackdrop: true, closeOnEscape: true, showCloseButton: true, + dialogStyle: window.innerWidth >= 900 + ? { maxWidth: '90vw', width: '1400px', height: '90vh' } + : undefined, onOpen: (modal) => { // Save button const saveBtn = modal.dialogFooter.querySelector('#save-seriesepisodes-config-btn'); @@ -135,12 +138,6 @@ } } }); - - if (window.innerWidth >= 900) { - modalInstance.dialog.style.maxWidth = '90vw'; - modalInstance.dialog.style.width = '1400px'; - modalInstance.dialog.style.height = '90vh'; - } LOG('Series Episodes configuration modal opened'); } catch (error) { diff --git a/configuration/seriesInfo-configuration.js b/configuration/seriesInfo-configuration.js index c55ea45..eaca85a 100644 --- a/configuration/seriesInfo-configuration.js +++ b/configuration/seriesInfo-configuration.js @@ -108,7 +108,7 @@ `; // Create modal - const modalInstance = window.ModalSystem.create({ + window.ModalSystem.create({ id: MODAL_ID, title: 'Series Info Configuration', content: content, @@ -116,6 +116,9 @@ closeOnBackdrop: true, closeOnEscape: true, showCloseButton: true, + dialogStyle: window.innerWidth >= 900 + ? { maxWidth: '90vw', width: '1400px', height: '90vh' } + : undefined, onOpen: (modal) => { // Save button const saveBtn = modal.dialogFooter.querySelector('#save-seriesinfo-config-btn'); @@ -135,12 +138,6 @@ } } }); - - if (window.innerWidth >= 900) { - modalInstance.dialog.style.maxWidth = '90vw'; - modalInstance.dialog.style.width = '1400px'; - modalInstance.dialog.style.height = '90vh'; - } LOG('Series Info configuration modal opened'); } catch (error) { diff --git a/configuration/skinManager-configuration.js b/configuration/skinManager-configuration.js index 95a99ad..4fd5727 100644 --- a/configuration/skinManager-configuration.js +++ b/configuration/skinManager-configuration.js @@ -598,7 +598,7 @@ `; // Create modal - const modalInstance = window.ModalSystem.create({ + window.ModalSystem.create({ id: MODAL_ID, title: 'Skin Manager Configuration', content: content, @@ -606,6 +606,9 @@ closeOnBackdrop: true, closeOnEscape: true, showCloseButton: true, + dialogStyle: window.innerWidth >= 900 + ? { maxWidth: '90vw', width: '1400px', height: '90vh' } + : undefined, onOpen: (modal) => { attachListeners(modal); @@ -628,12 +631,6 @@ } }); - if (window.innerWidth >= 900) { - modalInstance.dialog.style.maxWidth = '90vw'; - modalInstance.dialog.style.width = '1400px'; - modalInstance.dialog.style.height = '90vh'; - } - LOG('Skin Manager configuration modal opened'); } catch (error) { ERR('Error opening skin manager config modal:', error); diff --git a/configuration/thumbnailScrubber-configuration.js b/configuration/thumbnailScrubber-configuration.js index fe4c0d5..e89540b 100644 --- a/configuration/thumbnailScrubber-configuration.js +++ b/configuration/thumbnailScrubber-configuration.js @@ -246,7 +246,7 @@ `; // Create modal - const modalInstance = window.ModalSystem.create({ + window.ModalSystem.create({ id: MODAL_ID, title: 'Thumbnail Scrubber Configuration', content: content, @@ -254,6 +254,9 @@ closeOnBackdrop: true, closeOnEscape: true, showCloseButton: true, + dialogStyle: window.innerWidth >= 900 + ? { maxWidth: '90vw', width: '800px', height: 'auto' } + : undefined, onOpen: (modal) => { // Save button const saveBtn = modal.dialogFooter.querySelector('#save-thumbnailscrubber-config-btn'); @@ -274,12 +277,6 @@ } }); - if (window.innerWidth >= 900) { - modalInstance.dialog.style.maxWidth = '90vw'; - modalInstance.dialog.style.width = '800px'; - modalInstance.dialog.style.height = 'auto'; - } - LOG('Thumbnail Scrubber configuration modal opened'); } catch (error) { ERR('Error opening thumbnail scrubber config modal:', error); diff --git a/configuration/userManager-configuration.js b/configuration/userManager-configuration.js index 9a100e8..8e8a24b 100644 --- a/configuration/userManager-configuration.js +++ b/configuration/userManager-configuration.js @@ -469,6 +469,7 @@ closeOnBackdrop: true, closeOnEscape: true, showCloseButton: true, + dialogStyle: { maxWidth: '420px', width: '90vw' }, onOpen: (m) => { const input = m.dialogContent.querySelector('#um-tag-input'); const finish = (val) => { @@ -489,10 +490,6 @@ }, onClose: () => resolve(null) }); - if (modal?.dialog) { - modal.dialog.style.maxWidth = '420px'; - modal.dialog.style.width = '90vw'; - } }); } @@ -535,6 +532,7 @@ closeOnBackdrop: true, closeOnEscape: true, showCloseButton: true, + dialogStyle: { maxWidth: '420px', width: '90vw' }, onOpen: (m) => { m.dialogFooter.querySelector('#um-sched-cancel')?.addEventListener('click', () => finish(null)); m.dialogFooter.querySelector('#um-sched-ok')?.addEventListener('click', () => { @@ -550,10 +548,6 @@ }, onClose: () => finish(null) }); - if (modal?.dialog) { - modal.dialog.style.maxWidth = '420px'; - modal.dialog.style.width = '90vw'; - } }); } @@ -723,13 +717,10 @@ closeOnBackdrop: true, closeOnEscape: true, showCloseButton: true, + dialogStyle: window.innerWidth >= 900 + ? { maxWidth: '90vw', width: '1400px', height: '90vh' } + : undefined, onOpen: (modal) => { - if (window.innerWidth >= 900) { - modal.dialog.style.maxWidth = '90vw'; - modal.dialog.style.width = '1400px'; - modal.dialog.style.height = '90vh'; - } - modal.dialogFooter.querySelector('#um-editor-cancel')?.addEventListener('click', () => { window.ModalSystem.close(EDITOR_MODAL_ID); }); @@ -839,20 +830,18 @@ footer.style.cssText = 'display:flex;gap:0.75em;justify-content:flex-end;'; footer.innerHTML = ``; - const modal = window.ModalSystem.create({ + window.ModalSystem.create({ id: CONFIG_MODAL_ID, title: 'User Manager', content, footer, closeOnBackdrop: true, closeOnEscape: true, - showCloseButton: true + showCloseButton: true, + dialogStyle: window.innerWidth >= 900 + ? { maxWidth: '90vw', width: '1400px', height: '90vh' } + : undefined }); - if (modal?.dialog && window.innerWidth >= 900) { - modal.dialog.style.maxWidth = '90vw'; - modal.dialog.style.width = '1400px'; - modal.dialog.style.height = '90vh'; - } LOG('User Manager config opened'); } catch (e) { ERR('Error opening User Manager config', e); diff --git a/configuration/versionPreferences-configuration.js b/configuration/versionPreferences-configuration.js index 809ab21..47c09d4 100644 --- a/configuration/versionPreferences-configuration.js +++ b/configuration/versionPreferences-configuration.js @@ -443,7 +443,7 @@ `; - const modalInstance = window.ModalSystem.create({ + window.ModalSystem.create({ id: MODAL_ID, title: 'Item Version Preferences', content, @@ -451,6 +451,9 @@ closeOnBackdrop: true, closeOnEscape: true, showCloseButton: true, + dialogStyle: window.innerWidth >= 900 + ? { maxWidth: '90vw', width: '1400px', height: '90vh' } + : undefined, onOpen: (modal) => { const saveBtn = modal.dialogFooter.querySelector('#save-versionpreferences-config-btn'); saveBtn?.addEventListener('click', async () => { @@ -468,12 +471,6 @@ } }); - if (window.innerWidth >= 900) { - modalInstance.dialog.style.maxWidth = '90vw'; - modalInstance.dialog.style.width = '1400px'; - modalInstance.dialog.style.height = '90vh'; - } - LOG('Version Preferences configuration modal opened'); } catch (error) { ERR('Error opening version preferences config modal:', error); diff --git a/configuration/watchTogether-configuration.js b/configuration/watchTogether-configuration.js index e69906b..eea3eb7 100644 --- a/configuration/watchTogether-configuration.js +++ b/configuration/watchTogether-configuration.js @@ -88,7 +88,7 @@ `; - const modalInstance = window.ModalSystem.create({ + window.ModalSystem.create({ id: MODAL_ID, title: 'Watch Together', content, @@ -96,6 +96,7 @@ closeOnBackdrop: true, closeOnEscape: true, showCloseButton: true, + dialogStyle: { maxWidth: '90vw', width: '560px' }, onOpen: (modal) => { const saveBtn = modal.dialogFooter.querySelector('#save-watchtogether-config-btn'); if (saveBtn) { @@ -112,11 +113,6 @@ } } }); - - if (modalInstance?.dialog) { - modalInstance.dialog.style.maxWidth = '90vw'; - modalInstance.dialog.style.width = '560px'; - } LOG('Watch Together configuration modal opened'); } catch (error) { ERR('Error opening Watch Together config modal:', error); diff --git a/configuration/watchlist-configuration.js b/configuration/watchlist-configuration.js index 7b0f298..d5825c9 100644 --- a/configuration/watchlist-configuration.js +++ b/configuration/watchlist-configuration.js @@ -163,7 +163,7 @@ `; - const modalInstance = window.ModalSystem.create({ + window.ModalSystem.create({ id: MODAL_ID, title: 'Watchlist', content, @@ -171,6 +171,7 @@ closeOnBackdrop: true, closeOnEscape: true, showCloseButton: true, + dialogStyle: { maxWidth: '90vw', width: '560px' }, onOpen: (modal) => { attachToggleHandlers(modal.dialogContent || content); const saveBtn = modal.dialogFooter.querySelector('#save-watchlist-config-btn'); @@ -188,11 +189,6 @@ } } }); - - if (modalInstance?.dialog) { - modalInstance.dialog.style.maxWidth = '90vw'; - modalInstance.dialog.style.width = '560px'; - } LOG('Watchlist configuration modal opened'); } catch (error) { ERR('Error opening Watchlist config modal:', error); diff --git a/scripts/cardBuilder.css b/scripts/cardBuilder.css index 214a283..7b5da13 100644 --- a/scripts/cardBuilder.css +++ b/scripts/cardBuilder.css @@ -9,7 +9,6 @@ .spotlight-banner-container { position: relative; - overflow: hidden; border-radius: 8px; } @@ -56,7 +55,6 @@ background-size: cover; background-position: center; transition: opacity 0.8s ease-in-out; - contain: paint; isolation: isolate; } @@ -90,14 +88,6 @@ border-radius: 8px; } -/* Dual-backdrop background with masked crossfade */ -.spotlight-background-dual { - position: absolute; - inset: 0; - z-index: 1; - overflow: hidden; -} - .spotlight-section[data-layout="Border"] .spotlight-background-dual { border-radius: 8px; } @@ -128,12 +118,20 @@ } /* Single backdrop (when tileCount is 1 or only one image) */ -.spotlight-background-single { +.spotlight-background-single, +.spotlight-background-dual { position: absolute; z-index: 1; overflow: hidden; width: 100%; - height: 100%; + height: 160%; + transform: translateY(-15%); + -webkit-mask-image: linear-gradient( to bottom, transparent 0%, black calc(var(--spotlight-fullscreen-vertical) * 8), black calc(100% - calc(var(--spotlight-fullscreen-vertical) * 8)), transparent 100% ); + mask-image: linear-gradient( to bottom, transparent 0%, black calc(var(--spotlight-fullscreen-vertical) * 8), black calc(100% - calc(var(--spotlight-fullscreen-vertical) * 8)), transparent 100% ); + -webkit-mask-size: 100% 100%; + mask-size: 100% 100%; + -webkit-mask-position: 0 0; + mask-position: 0 0; } .spotlight-background-single img { @@ -158,7 +156,7 @@ @keyframes kenBurnsZoomIn { from { - transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 0); } to { transform: scale3d(1.12, 1.12, 1); @@ -167,7 +165,7 @@ @keyframes kenBurnsZoomOut { from { - transform: scale3d(1.15, 1.15, 1); + transform: scale3d(1.15, 1.15, 0); } to { transform: scale3d(1.02, 1.02, 1); @@ -177,7 +175,7 @@ /* Full-screen: start images pre-scaled so they extend past edges from the start (no hard edge before zoom) */ @keyframes kenBurnsZoomInFullscreen { from { - transform: scale3d(1.08, 1.08, 1); + transform: scale3d(1.08, 1.08, 0); } to { transform: scale3d(1.12, 1.12, 1); @@ -186,7 +184,7 @@ @keyframes kenBurnsZoomOutFullscreen { from { - transform: scale3d(1.15, 1.15, 1); + transform: scale3d(1.15, 1.15, 0); } to { transform: scale3d(1.08, 1.08, 1); @@ -205,7 +203,7 @@ @keyframes fadeInUp { from { opacity: 0; - transform: scale3d(1, 1, 1) translateY(30px); + transform: scale3d(1, 1, 0) translateY(30px); } to { opacity: 1; @@ -216,7 +214,7 @@ @keyframes fadeInDown { from { opacity: 0; - transform: scale3d(1, 1, 1) translateY(-30px); + transform: scale3d(1, 1, 0) translateY(-30px); } to { opacity: 1; @@ -226,7 +224,7 @@ @keyframes kenBurnsPanRight { from { - transform: scale3d(1.1, 1.1, 1) translateX(-5%); + transform: scale3d(1.1, 1.1, 0) translateX(-5%); } to { transform: scale3d(1.1, 1.1, 1) translateX(5%); @@ -235,46 +233,46 @@ @keyframes kenBurnsPanLeft { from { - transform: scale3d(1.1, 1.1, 1) translateX(5%); + transform: scale3d(1.1, 1.1, 0) translateX(5%); } to { - transform: scale3d(1.1, 1.1, 1) translateX(-5%); + transform: scale3d(1.1, 1.1, 0) translateX(-5%); } } @keyframes kenBurnsPanUp { from { - transform: scale3d(1.1, 1.1, 1) translateY(5%); + transform: scale3d(1.1, 1.1, 0) translateY(5%); } to { - transform: scale3d(1.1, 1.1, 1) translateY(-5%); + transform: scale3d(1.1, 1.1, 0) translateY(-5%); } } @keyframes kenBurnsDiagonal { from { - transform: scale3d(1.08, 1.08, 1) translate(-5%, 5%); + transform: scale3d(1.08, 1.08, 0) translate(-5%, 5%); } to { - transform: scale3d(1.13, 1.13, 1) translate(5%, -5%); + transform: scale3d(1.13, 1.13, 0) translate(5%, -5%); } } @keyframes fadeInScale { from { opacity: 0; - transform: scale3d(0.95, 0.95, 1); + transform: scale3d(0.95, 0.95, 0); } to { opacity: 1; - transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 0); } } @keyframes parallaxFloat { 0%, 100% { - transform: scale3d(1, 1, 1) translateY(0); + transform: scale3d(1, 1, 0) translateY(0); } 50% { transform: scale3d(1, 1, 1) translateY(-12px); @@ -284,7 +282,7 @@ @keyframes depthPulse { 0%, 100% { - transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 0); } 50% { transform: scale3d(1.03, 1.03, 1); @@ -293,7 +291,7 @@ @keyframes slowRotate { from { - transform: scale3d(1.15, 1.15, 1) rotate(0deg); + transform: scale3d(1.15, 1.15, 0) rotate(0deg); } to { transform: scale3d(1.15, 1.15, 1) rotate(3deg); @@ -303,7 +301,7 @@ @keyframes breathe { 0%, 100% { - transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 0); opacity: 0.9; } 50% { @@ -330,7 +328,7 @@ filter: saturate(1) brightness(1); } 50% { - filter: saturate(1.2) brightness(1.05); + filter: saturate(1.5) brightness(1.25); } 100% { filter: saturate(1) brightness(1); @@ -451,7 +449,6 @@ bottom: 0; left: 0; width: 45%; - background: linear-gradient(to right, rgba(0, 0, 0, 0.85) 0%, rgba(0, 0, 0, 0.7) 40%, transparent 100%); padding: 40px; color: white; display: grid; @@ -603,6 +600,10 @@ min-width: 0; } +.spotlight-metadata-row .sectionTabs { + font-size: inherit; + } + .spotlight-metadata-row .itemDetailsGroup, .spotlight-metadata-row .sectionTabs { flex-shrink: 0; @@ -844,7 +845,8 @@ min-width: 300px; opacity: 0; pointer-events: none; - transition: opacity 0.2s; + transition: opacity 0.2s ease; + transition-delay: 0s; z-index: 1000; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5); word-wrap: break-word; @@ -852,6 +854,14 @@ .spotlight-info-button:hover .spotlight-overview-tooltip { opacity: 1; + transition-delay: 0.5s; +} + +/* Hover overview only on Normal desktop spotlights */ +.spotlight-section[data-size="large"] .spotlight-overview-tooltip, +.spotlight-section[data-size="full"] .spotlight-overview-tooltip, +.layout-mobile .spotlight-overview-tooltip { + display: none !important; } .spotlight-section:not(.hide)[data-section-id] .spotlight-nav-container { @@ -958,7 +968,6 @@ width: calc(100% - 2rem); padding: 5rem 1rem 3rem !important; align-items: center; - background: rgba(0, 0, 0, 0.7); } .spotlight-tagline { @@ -972,7 +981,6 @@ display: inline-block; border-radius: 4px; background: rgba(255, 255, 255, 0.12); - animation: skeleton-pulse 1.5s ease-in-out infinite; } .cardText-first .skeleton-text-line { padding: 0 22.5% !important; @@ -998,6 +1006,50 @@ margin-top: 0.15em; } +/* One shimmer overlay per skeleton section — gated by .skeleton-shimmer-active (IO) */ +.itemsContainer { + position: relative; +} + +.skeleton-section .itemsContainer::after, +.skeleton-section .spotlight-items-container::after { + content: ""; + position: absolute; + inset: 0; + pointer-events: none; + z-index: 2; + background: linear-gradient( + 90deg, + transparent 0%, + rgba(255, 255, 255, 0.08) 45%, + rgba(255, 255, 255, 0.16) 50%, + rgba(255, 255, 255, 0.08) 55%, + transparent 100% + ); + transform: translateX(-100%); + opacity: 0; +} + +.skeleton-section.skeleton-shimmer-active .itemsContainer::after, +.skeleton-section.skeleton-shimmer-active .spotlight-items-container::after { + opacity: 1; + animation: skeleton-shimmer-sweep 1.4s ease-in-out infinite; +} + +@media (prefers-reduced-motion: reduce) { + .skeleton-section.skeleton-shimmer-active .itemsContainer::after, + .skeleton-section.skeleton-shimmer-active .spotlight-items-container::after { + animation: none !important; + opacity: 0 !important; + } +} + +/* Localize style/layout invalidation to section shells during enhance/lazy paint */ +.emby-scroller-container.custom-scroller-container, +.spotlight-section { + contain: layout style; +} + /* Tagline always 1 line - Movie-specific override kept for any edge cases */ [data-item-type="Movie"] .spotlight-tagline { -webkit-line-clamp: 1; @@ -1481,7 +1533,7 @@ /* Borderless layout: full width, no border, negative margins for overlap. Banner has NO padding so the background image overflows edge-to-edge; only content is inset. */ .spotlight-section[data-layout="Borderless"] { - --spotlight-fullscreen-vertical: 4%; + --spotlight-fullscreen-vertical: 20px; --spotlight-fullscreen-horizontal: max(env(safe-area-inset-left), env(safe-area-inset-right), 4%); padding: 0; width: 100%; @@ -1496,51 +1548,55 @@ .spotlight-section[data-layout="Borderless"] .spotlight-banner-container { border-radius: 0; - /* No padding: background fills full size so image extends edge-to-edge and into overlap zone */ position: relative; - /* Wider mask so backdrop fades gradually at top/bottom (no visible solid edge during animation) */ - -webkit-mask-image: linear-gradient( - to bottom, - transparent 0%, - black calc(var(--spotlight-fullscreen-vertical) * 2), - black calc(100% - calc(var(--spotlight-fullscreen-vertical) * 2)), - transparent 100% - ); - mask-image: linear-gradient( - to bottom, - transparent 0%, - black calc(var(--spotlight-fullscreen-vertical) * 2), - black calc(100% - calc(var(--spotlight-fullscreen-vertical) * 2)), - transparent 100% - ); - -webkit-mask-size: 100% 100%; - mask-size: 100% 100%; - -webkit-mask-position: 0 0; - mask-position: 0 0; +} + +.spotlight-section[data-layout="Border"] .spotlight-banner-container { + overflow: hidden; } /* Dark gradient overlays at top and bottom (like left-side overlay): graceful fade and better visibility when content overlaps */ -.spotlight-section[data-layout="Borderless"] .spotlight-banner-container::before, -.spotlight-section[data-layout="Borderless"] .spotlight-banner-container::after { +.spotlight-section .spotlight-item .spotlight-background-single::before, +.spotlight-section .spotlight-item .spotlight-background-single::after, +.spotlight-section .spotlight-item .spotlight-background-dual::before, +.spotlight-section .spotlight-item .spotlight-background-dual::after { content: ""; position: absolute; + top: 0; left: 0; right: 0; - width: 100%; - height: 15%; + height: 100%; pointer-events: none; z-index: 2; } -.spotlight-section[data-layout="Borderless"] .spotlight-banner-container::before { +.spotlight-background-single::before, +.spotlight-background-dual::before { + width: 45%; + background: radial-gradient(farthest-corner at bottom left, rgba(0, 0, 0, 0.9) 0%, rgba(0, 0, 0, 0.7) 40%, rgba(0, 0, 0, 0) 70%); +} + +.layout-mobile .spotlight-background-single::before, +.layout-mobile .spotlight-background-dual::before { + width: 100%; + background: linear-gradient(to bottom, transparent 0%, transparent 40%, rgba(0, 0, 0, 0.45) 60%, rgba(0, 0, 0, 0.85) 100%) !important; +} + +.spotlight-background-single::after, +.spotlight-background-dual::after { + width: 100%; + background: linear-gradient( 180deg, black 0%, transparent 35%, transparent 65%, black 100% ); +} + +/* .spotlight-section[data-layout="Borderless"] .spotlight-item .spotlight-background-single:::before { top: 0; background: linear-gradient(0deg, transparent, black); } -.spotlight-section[data-layout="Borderless"] .spotlight-banner-container::after { +.spotlight-section[data-layout="Borderless"] .spotlight-item .spotlight-background-single::after { bottom: 0; background: linear-gradient(180deg, transparent, black); -} +} */ .spotlight-section[data-layout="Borderless"] .spotlight-items-container, .spotlight-section[data-layout="Borderless"] .spotlight-item { @@ -1568,17 +1624,16 @@ z-index: 3; grid-template-rows: 1fr auto auto auto; gap: 1.5em; - background: linear-gradient(to right, rgba(0, 0, 0, 0.85) 0%, rgba(0, 0, 0, 0.7) 70%, transparent 100%); } .spotlight-section[data-layout="Borderless"][data-size="large"] .spotlight-overlay { - padding: calc(var(--spotlight-fullscreen-vertical) * 1.75) var(--spotlight-fullscreen-horizontal) - calc(var(--spotlight-fullscreen-vertical) * 2) var(--spotlight-fullscreen-horizontal); + padding: calc(var(--spotlight-fullscreen-vertical) * 5) var(--spotlight-fullscreen-horizontal) + calc(var(--spotlight-fullscreen-vertical) * 5) var(--spotlight-fullscreen-horizontal); } .spotlight-section[data-layout="Borderless"][data-size="full"] .spotlight-overlay { - padding: calc(var(--spotlight-fullscreen-vertical) * 2.25) var(--spotlight-fullscreen-horizontal) - calc(var(--spotlight-fullscreen-vertical) * 2.5) var(--spotlight-fullscreen-horizontal); + padding: calc(var(--spotlight-fullscreen-vertical) * 5) var(--spotlight-fullscreen-horizontal) + calc(var(--spotlight-fullscreen-vertical) * 5) var(--spotlight-fullscreen-horizontal); grid-template-rows: 1fr auto minmax(6rem, auto) minmax(48px, auto); align-content: center; gap: 1.5em; @@ -1733,7 +1788,6 @@ .layout-mobile .spotlight-overlay { width: calc(100% - 2rem) !important; - background: linear-gradient(to bottom, transparent 0%, rgba(0, 0, 0, 0.4) 40%, rgba(0, 0, 0, 0.85) 100%) !important; padding: 4rem 1rem 1rem 1rem !important; align-items: start; align-content: start; @@ -1795,16 +1849,12 @@ display: none !important; } -.layout-mobile .spotlight-section .spotlight-buttons-container.mainDetailButtons .btnPlay { - margin: 0.3em !important; -} - -.spotlight-section .spotlight-buttons-container.mainDetailButtons .btnPlay { +.spotlight-section .spotlight-buttons-container.mainDetailButtons > button { right: 0px !important; margin-top: 0 !important; margin-left: 0 !important; margin-bottom: 0 !important; - margin-right: 1em !important; + margin-right: 0 !important; min-width: unset; flex: unset !important; } @@ -1815,6 +1865,18 @@ margin: 0px !important; padding: 0px !important; } +.spotlight-section .mainDetailButtons > .btnPlay:is(.emby-button) { + padding: 0.25em !important; +} +.spotlight-section .spotlight-buttons-container.mainDetailButtons { + gap: 2em; +} +.spotlight-section .spotlight-buttons-container.mainDetailButtons > * { + padding: 0 !important; +} +.spotlight-section .spotlight-buttons-container.mainDetailButtons > * span { + font-size: 2em; +} .spotlight-section:not(.hide) .headerTabs, .spotlight-section:not(.hide) .sectionTabs { @@ -1831,11 +1893,11 @@ } .spotlight-section:not(.hide) .emby-tab-button { - height: initial !important; - padding: 0.5rem 0.5rem !important; + padding: 0.5em 1em !important; margin: 0px !important; top: initial !important; transform: initial !important; + align-items: center; } .spotlight-section-title.headerTabs.sectionTabs, @@ -1846,6 +1908,16 @@ align-items: start; justify-content: start; width: auto; + font-size: revert !important; + height: 40px !important; + align-items: center; +} + +:where(.spotlight-section-title.headerTabs.sectionTabs), +:where(.spotlight-section .headerTabs.sectionTabs), +:where(.spotlight-nav-container.headerTabs.sectionTabs) { + background-color: rgba(20, 20, 20, 0.85); + border-radius: 0.2em; } .spotlight-section[data-hide-section-name="true"] .spotlight-section-title, @@ -1858,28 +1930,25 @@ top: 0; } -.spotlight-nav-buttons-container { +.spotlight-section:not(.hide) .spotlight-nav-buttons-container { display: flex !important; - gap: 8px !important; - font-size: 1.1rem !important; + padding: 0 0.5em !important; +} +.spotlight-section:not(.hide) .spotlight-nav-buttons-container button { + padding: 0.5em; } .spotlight-nav-button span.material-icons { - font-size: 24px !important; display: inline-block !important; top: initial !important; } .spotlight-section-title > * { - font-size: 18px !important; + font-size: 1em !important; } .spotlight-metadata-row *:not(a) { cursor: default; } -.spotlight-section[data-layout="Borderless"][data-size="normal"] { - margin-top: calc(-1 * calc(var(--spotlight-fullscreen-vertical) * 1.5)); - margin-bottom: calc(-1 * calc(var(--spotlight-fullscreen-vertical) * 2)); -} .spotlight-section[data-layout="Borderless"][data-size="normal"] .spotlight-banner-container { height: 55vh; } @@ -1895,18 +1964,13 @@ } .spotlight-section[data-layout="Borderless"][data-size="normal"] .spotlight-overlay.has-title { - padding: calc(var(--spotlight-fullscreen-vertical) * 3) var(--spotlight-fullscreen-horizontal) - calc(var(--spotlight-fullscreen-vertical) * 2) var(--spotlight-fullscreen-horizontal); + padding: calc(var(--spotlight-fullscreen-vertical) * 5) var(--spotlight-fullscreen-horizontal) + calc(var(--spotlight-fullscreen-vertical) * 5) var(--spotlight-fullscreen-horizontal); } .spotlight-section[data-layout="Borderless"][data-size="normal"] { - --spotlight-fullscreen-vertical: 3em; --spotlight-fullscreen-horizontal: max(env(safe-area-inset-left), env(safe-area-inset-right), 4%); } -.spotlight-section[data-layout="Borderless"][data-size="normal"] .spotlight-section-title-container { - left: var(--spotlight-fullscreen-horizontal); - top: calc(var(--spotlight-fullscreen-vertical) * 2); -} .spotlight-section[data-layout="Borderless"][data-size="normal"] .spotlight-tagline { bottom: calc(var(--spotlight-fullscreen-vertical) * 1.5 + 40px); } @@ -1915,7 +1979,7 @@ } .spotlight-section[data-layout="Borderless"][data-size="normal"] .spotlight-nav-container { right: var(--spotlight-fullscreen-horizontal); - top: calc(var(--spotlight-fullscreen-vertical) * 2); + top: calc(var(--spotlight-fullscreen-vertical) * 2.5); } .spotlight-section[data-layout="Borderless"][data-size="normal"] .spotlight-overlay { grid-template-rows: 1fr auto auto auto; @@ -1924,7 +1988,7 @@ grid-template-rows: var(--spotlight-logo-height) auto 1fr auto; } .spotlight-section[data-layout="Border"][data-size="full"] .spotlight-overlay { - grid-template-rows: 3fr auto minmax(6rem, auto) 1fr; + grid-template-rows: 3fr auto minmax(6rem, auto) auto; } .spotlight-section[data-layout="Border"][data-size="full"] .spotlight-logo-section { align-self: end; @@ -1986,9 +2050,6 @@ top: 2rem !important; right: 2rem !important; } -.spotlight-section[data-layout="Border"][data-size="normal"] .spotlight-section-title a { - font-size: 1rem !important; -} .spotlight-section[data-layout="Border"] .spotlight-banner-container { border: 1px solid rgb(255 255 255 / 10%); } @@ -2004,11 +2065,6 @@ -webkit-line-clamp: 2; } -.spotlight-section[data-size="normal"] .spotlight-background-single img, -.spotlight-section[data-size="large"] .spotlight-background-single img { - object-position: center 20%; -} - .layout-mobile .sectionTitleContainer-cards .sectionTitle-link > * { padding-top: 0 !important; } @@ -2034,10 +2090,6 @@ align-items: center; } -.layout-mobile .spotlight-section[data-layout="Borderless"][data-size="full"] .spotlight-overlay { - /* padding-bottom: 5rem !important; */ -} - .layout-mobile .spotlight-overview-section, .layout-mobile .spotlight-credits-row, .layout-mobile .spotlight-series-info { @@ -2070,25 +2122,14 @@ min-width: 0; text-align: center; } - -/* .layout-mobile .spotlight-section[data-layout="Borderless"][data-order="1"] .spotlight-section-title { - top: 135px; -} -.layout-mobile .spotlight-section[data-layout="Borderless"][data-order="1"] { - margin-top: calc(-1 * calc(var(--spotlight-fullscreen-vertical) * 10)) !important; -} */ - -.layout-mobile .spotlight-section-title .emby-tab-button { - padding: 0.25rem !important; -} .layout-mobile .spotlight-section .emby-tab-button, .layout-mobile .spotlight-metadata-section span a, .layout-mobile .spotlight-metadata-section span { - font-size: 1rem !important; + font-size: 1em !important; } .layout-mobile .spotlight-section .spotlight-item-title { - font-size: 2rem !important; + font-size: 2em !important; text-align: center; } @@ -2096,10 +2137,6 @@ display: none !important; } -.layout-mobile .spotlight-buttons-container.mainDetailButtons span { - font-size: 1.2rem; -} - .layout-mobile .spotlight-section .spotlight-metadata-row { justify-content: center; align-self: center; @@ -2192,19 +2229,19 @@ html.sleekfin-main-ui .emby-scroller-container[data-section-id="liveTv-channels" /* .emby-scroller-container[data-card-format="Logo"] .cardImageContainer, .emby-scroller-container[data-card-format="Clear Art"] .cardImageContainer, .emby-scroller-container[data-card-format="Disc"] .cardImageContainer { - transition: opacity 0.15s ease-in-out; + transition: opacity 0.15s ease-in-out; } .emby-scroller-container[data-card-format="Logo"] .cardImageContainer.lazy-hidden, .emby-scroller-container[data-card-format="Clear Art"] .cardImageContainer.lazy-hidden, .emby-scroller-container[data-card-format="Disc"] .cardImageContainer.lazy-hidden { - opacity: 0; + opacity: 0; } .emby-scroller-container[data-card-format="Logo"] .cardImageContainer.lazy-loaded, .emby-scroller-container[data-card-format="Clear Art"] .cardImageContainer.lazy-loaded, .emby-scroller-container[data-card-format="Disc"] .cardImageContainer.lazy-loaded { - opacity: 1; + opacity: 1; } */ :not(.layout-mobile) .emby-scroller-container[data-card-format="Logo"]:not([data-border-style]) .itemsContainer, @@ -4064,42 +4101,43 @@ html.sleekfin-main-ui .emby-scroller-container[data-section-id="liveTv-channels" display: none; } -.spotlight-truncated-list:has(.spotlight-truncated-list-tooltip) { - overflow: visible; -} - .emby-scroller-container[data-layout="grid"] .emby-scrollbuttons { display: none !important; } -/* .spotlight-section[data-size="full"] .spotlight-truncated-list.itemDetailsGroup:has(> .detailsGroupItem), +/* +.spotlight-truncated-list:has(.spotlight-truncated-list-tooltip) { + overflow: visible; +} + +.spotlight-section[data-size="full"] .spotlight-truncated-list.itemDetailsGroup:has(> .detailsGroupItem), .spotlight-section[data-size="full"] .spotlight-truncated-list.sectionTabs:has(> .emby-tab-button) { - font-size: large; - display: flex; - align-items: center; - align-self: start; + font-size: large; + display: flex; + align-items: center; + align-self: start; } .spotlight-section .itemDetailsGroup:has(.detailsGroupItem:not(.hide)), .spotlight-section .sectionTabs:not(.spotlight-section-title):has(.emby-tab-button:not(.hide)) { - margin: 0px !important; - padding: 0px !important; - width: initial !important; - min-width: initial !important; + margin: 0px !important; + padding: 0px !important; + width: initial !important; + min-width: initial !important; } .spotlight-section:not(.hide) span.emby-tab-button.emby-tab-button-active:not(:has(a)) { - cursor: default; + cursor: default; } .layout-mobile .spotlight-section .spotlight-section-title.sectionTabs:has(.emby-tab-button:not(.hide)) { - justify-self: center; - display: flex !important; - justify-content: center; + justify-self: center; + display: flex !important; + justify-content: center; } .layout-mobile .spotlight-section .spotlight-section-title.sectionTabs:has(.emby-tab-button:not(.hide)) > * { - display: block !important; + display: block !important; padding: 0.25em 0.5em !important; } @@ -4122,3 +4160,21 @@ html.sleekfin-main-ui .emby-scroller-container[data-section-id="liveTv-channels" .spotlight-section .spotlight-overlay * { pointer-events: all; } + +.sectionTitleContainer { + display: flex !important; +} +.cardOverlayContainer > .button-link { + width: 100%; + height: 100%; +} + +.card[data-type="Program"] .cardTextStack .cardText-secondary:nth-child(3) { + display: none; +} +[data-section-id="liveTv-on-now"] .card[data-type="Program"] .cardTextStack .cardText-secondary:nth-child(2) { + display: none; +} +[data-section-id="liveTv-on-now"] .card[data-type="Program"] .cardTextStack .cardText-secondary:nth-child(3) { + display: block; +} \ No newline at end of file diff --git a/scripts/configuration.js b/scripts/configuration.js index ff57288..328b513 100644 --- a/scripts/configuration.js +++ b/scripts/configuration.js @@ -2525,17 +2525,10 @@ footer: footer, closeOnBackdrop: true, closeOnEscape: true, + dialogStyle: window.innerWidth >= 900 + ? { maxWidth: '90vw', width: '1400px', maxHeight: '90vh' } + : undefined, onOpen: (modalInstance) => { - // Check if window is less than 900px - const layoutMobile = window.innerWidth < 900; - - // Update modal dialog styling for large size - if (modalInstance && modalInstance.dialog && !layoutMobile) { - modalInstance.dialog.style.maxWidth = '90vw'; - modalInstance.dialog.style.width = '1400px'; - modalInstance.dialog.style.maxHeight = '90vh'; - } - // Add event handlers setupConfigModalHandlers(modalInstance, config); } diff --git a/scripts/modal.js b/scripts/modal.js index e4a7ada..4586155 100644 --- a/scripts/modal.js +++ b/scripts/modal.js @@ -9,6 +9,49 @@ window.ModalSystem = (function() { // Store active modals const activeModals = new Map(); + // Body scroll lock while any kefin modal is open (replaces non-passive wheel trap) + let bodyScrollLockCount = 0; + let savedBodyOverflow = null; + let savedHtmlOverflow = null; + + function lockBodyScroll() { + if (bodyScrollLockCount === 0) { + savedBodyOverflow = document.body.style.overflow; + savedHtmlOverflow = document.documentElement.style.overflow; + document.body.style.overflow = 'hidden'; + document.documentElement.style.overflow = 'hidden'; + } + bodyScrollLockCount++; + } + + function unlockBodyScroll() { + if (bodyScrollLockCount <= 0) return; + bodyScrollLockCount--; + if (bodyScrollLockCount === 0) { + document.body.style.overflow = savedBodyOverflow || ''; + document.documentElement.style.overflow = savedHtmlOverflow || ''; + savedBodyOverflow = null; + savedHtmlOverflow = null; + } + } + + /** + * Apply Jellyfin's scaleup open animation and clear it when finished. + * Clearing avoids a stuck compositor layer that leaves large dialogs soft/blurry. + * @param {HTMLElement} dialog + */ + function applyOpenAnimation(dialog) { + if (!dialog) return; + dialog.style.animation = '160ms ease-out 0s 1 normal both running scaleup'; + const onEnd = (e) => { + if (e.target !== dialog) return; + if (e.animationName && e.animationName !== 'scaleup') return; + dialog.style.animation = ''; + dialog.removeEventListener('animationend', onEnd); + }; + dialog.addEventListener('animationend', onEnd); + } + /** * Create a Jellyfin-style modal dialog * @param {Object} options - Modal configuration @@ -22,6 +65,7 @@ window.ModalSystem = (function() { * @param {boolean} options.closeOnEscape - Whether to close on Escape key (default: true) * @param {boolean} options.showCloseButton - Whether to show close button in header (default: true if title exists) * @param {boolean} [options.fixedSize] - Use dialog-fixedSize. If undefined, enable when window width < 900. + * @param {Object} [options.dialogStyle] - CSS styles applied to the dialog before open (e.g. width/height). * @returns {Object} Modal instance */ function createModal(options = {}) { @@ -35,7 +79,8 @@ window.ModalSystem = (function() { closeOnBackdrop = true, closeOnEscape = true, showCloseButton = true, - fixedSize + fixedSize, + dialogStyle } = options; if (!id) { @@ -68,12 +113,16 @@ window.ModalSystem = (function() { dialog.setAttribute('data-autofocus', 'true'); dialog.setAttribute('data-removeonclose', 'true'); dialog.setAttribute('data-name', 'kefin-modal'); - dialog.style.animation = '160ms ease-out 0s 1 normal both running scaleup'; dialog.style.display = 'flex'; dialog.style.flexDirection = 'column'; if (!useFixedSize) { dialog.style.maxHeight = '90vh'; } + // Final box size before scaleup so the compositor layer is not soft-scaled after open + if (dialogStyle && typeof dialogStyle === 'object') { + Object.assign(dialog.style, dialogStyle); + } + applyOpenAnimation(dialog); // Create header if title is provided let dialogHeader = null; @@ -121,89 +170,10 @@ window.ModalSystem = (function() { dialogContent.style.overflowY = 'auto'; dialogContent.style.flex = '1'; dialogContent.style.minHeight = '0'; - - // Helper function to check if an element is scrollable - const isScrollable = (element) => { - if (!element || element === document.body || element === document.documentElement) { - return false; - } - const style = window.getComputedStyle(element); - const overflowY = style.overflowY; - const overflow = style.overflow; - const hasScrollableContent = element.scrollHeight > element.clientHeight; - return (overflowY === 'auto' || overflowY === 'scroll' || overflow === 'auto' || overflow === 'scroll') && hasScrollableContent; - }; - - // Helper function to find the closest scrollable parent - const findClosestScrollable = (element, stopAt = null) => { - let current = element; - while (current && current !== stopAt && current !== document.body && current !== document.documentElement) { - if (isScrollable(current)) { - return current; - } - current = current.parentElement; - } - return null; - }; - - // Helper function to check if element can scroll in direction - const canScrollInDirection = (element, delta) => { - if (!element) return false; - const scrollTop = element.scrollTop; - const scrollHeight = element.scrollHeight; - const clientHeight = element.clientHeight; - - // Scrolling down (positive delta) - if (delta > 0) { - return scrollTop + clientHeight < scrollHeight - 1; // -1 for rounding - } - // Scrolling up (negative delta) - else { - return scrollTop > 0; - } - }; - - // Add scroll trap to prevent body scroll when scrolling within modal - const wheelHandler = (e) => { - const element = dialogContent; - const delta = e.deltaY; - - // Find if the event originated from a child scrollable element - const childScrollable = findClosestScrollable(e.target, element); - - // If event is from a child scrollable element - if (childScrollable && childScrollable !== element) { - // Check if the child can scroll in the direction of the wheel - if (canScrollInDirection(childScrollable, delta)) { - // Let the child handle the scroll, but stop propagation to prevent body scroll - e.stopPropagation(); - return; // Don't prevent default, let child scroll - } - // Child is at boundary, prevent default to stop body scroll - e.preventDefault(); - e.stopPropagation(); - return; - } - - // Event is on the modal content itself - const scrollTop = element.scrollTop; - const scrollHeight = element.scrollHeight; - const clientHeight = element.clientHeight; - - // At top and scrolling up - if (delta < 0 && scrollTop === 0) { - e.preventDefault(); - } - // At bottom and scrolling down - else if (delta > 0 && scrollTop + clientHeight >= scrollHeight) { - e.preventDefault(); - } - - // Stop propagation to prevent body scroll - e.stopPropagation(); - }; - - dialogContent.addEventListener('wheel', wheelHandler, { passive: false }); + // Contain overscroll so nested wheel/touch scroll does not chain to the page + // (avoids a non-passive wheel listener that delays input while the main thread is busy) + dialogContent.style.overscrollBehavior = 'contain'; + dialog.style.overscrollBehavior = 'contain'; // Add content if (content) { @@ -261,12 +231,12 @@ window.ModalSystem = (function() { updateContent: (newContent) => updateModalContent(id, newContent), addEventListener: (event, handler) => { dialog.addEventListener(event, handler); - }, - _wheelHandler: wheelHandler + } }; // Store modal instance activeModals.set(id, modalInstance); + //lockBodyScroll(); // Add event listeners if (closeOnBackdrop) { @@ -308,11 +278,6 @@ window.ModalSystem = (function() { if (modal._escapeHandler) { document.removeEventListener('keydown', modal._escapeHandler); } - - // Remove wheel handler if it exists - if (modal._wheelHandler && modal.dialogContent) { - modal.dialogContent.removeEventListener('wheel', modal._wheelHandler); - } // Remove from DOM if (modal.backdrop && modal.backdrop.parentNode) { @@ -324,6 +289,7 @@ window.ModalSystem = (function() { // Remove from active modals activeModals.delete(id); + //unlockBodyScroll(); // Call onClose callback if modal instance has it if (modal.onClose && typeof modal.onClose === 'function') { @@ -392,6 +358,7 @@ window.ModalSystem = (function() { updateContent: updateModalContent, isOpen: isModalOpen, getActiveIds: getActiveModalIds, - closeAll: closeAllModals + closeAll: closeAllModals, + applyOpenAnimation }; })();