diff --git a/Jellyfin.Plugin.MediaBarEnhanced/Api/MediaBarEnhancedController.cs b/Jellyfin.Plugin.MediaBarEnhanced/Api/MediaBarEnhancedController.cs index b807725..b8b9343 100644 --- a/Jellyfin.Plugin.MediaBarEnhanced/Api/MediaBarEnhancedController.cs +++ b/Jellyfin.Plugin.MediaBarEnhanced/Api/MediaBarEnhancedController.cs @@ -70,6 +70,7 @@ namespace Jellyfin.Plugin.MediaBarEnhanced.Api if (path.EndsWith(".js", StringComparison.OrdinalIgnoreCase)) return "application/javascript"; if (path.EndsWith(".css", StringComparison.OrdinalIgnoreCase)) return "text/css"; if (path.EndsWith(".html", StringComparison.OrdinalIgnoreCase)) return "text/html"; + if (path.EndsWith(".svg", StringComparison.OrdinalIgnoreCase)) return "image/svg+xml"; return "application/octet-stream"; } } diff --git a/Jellyfin.Plugin.MediaBarEnhanced/Configuration/PluginConfiguration.cs b/Jellyfin.Plugin.MediaBarEnhanced/Configuration/PluginConfiguration.cs index 987ae4c..162a05a 100644 --- a/Jellyfin.Plugin.MediaBarEnhanced/Configuration/PluginConfiguration.cs +++ b/Jellyfin.Plugin.MediaBarEnhanced/Configuration/PluginConfiguration.cs @@ -34,5 +34,6 @@ namespace Jellyfin.Plugin.MediaBarEnhanced.Configuration public string PreferredVideoQuality { get; set; } = "Auto"; public bool EnableSeasonalContent { get; set; } = false; public bool IsEnabled { get; set; } = true; + public bool EnableClientSideSettings { get; set; } = false; } } diff --git a/Jellyfin.Plugin.MediaBarEnhanced/Configuration/configPage.html b/Jellyfin.Plugin.MediaBarEnhanced/Configuration/configPage.html index f995c91..8746922 100644 --- a/Jellyfin.Plugin.MediaBarEnhanced/Configuration/configPage.html +++ b/Jellyfin.Plugin.MediaBarEnhanced/Configuration/configPage.html @@ -221,6 +221,15 @@ Space (pause), M (mute/unmute)) for the slideshow. +
+ +
If enabled, users will see a media bar icon in the header to + override settings (like disabling the bar or video backdrops) locally on their device.
+

Time Settings

Leave a setting blank to use the default value.

@@ -361,7 +370,7 @@ 'WaitForTrailerToEnd', 'StartMuted', 'FullWidthVideo', 'EnableMobileVideo', 'ShowTrailerButton', 'AlwaysShowArrows', 'EnableKeyboardControls', 'EnableCustomMediaIds', 'CustomMediaIds', 'EnableLoadingScreen', - 'EnableSeasonalContent' + 'EnableSeasonalContent', 'EnableClientSideSettings' ]; keys.forEach(function (key) { @@ -410,7 +419,7 @@ 'WaitForTrailerToEnd', 'StartMuted', 'FullWidthVideo', 'EnableMobileVideo', 'ShowTrailerButton', 'AlwaysShowArrows', 'EnableKeyboardControls', 'EnableCustomMediaIds', 'CustomMediaIds', 'EnableLoadingScreen', - 'EnableSeasonalContent' + 'EnableSeasonalContent', 'EnableClientSideSettings' ]; keys.forEach(function (key) { diff --git a/Jellyfin.Plugin.MediaBarEnhanced/Web/mediaBarEnhanced.css b/Jellyfin.Plugin.MediaBarEnhanced/Web/mediaBarEnhanced.css index 909126f..2911e93 100644 --- a/Jellyfin.Plugin.MediaBarEnhanced/Web/mediaBarEnhanced.css +++ b/Jellyfin.Plugin.MediaBarEnhanced/Web/mediaBarEnhanced.css @@ -13,6 +13,7 @@ * - option to set a maximum for the pagination dots (will turn into a counter style if exceeded) * - option to disable loading screen * - option to put collection (boxsets) IDs into the slideshow to display their items + * - option to enable client-side settings (allow users to override settings locally on their device) */ @import url(https://fonts.googleapis.com/css2?family=Archivo+Narrow:ital,wght@0,400..700;1,400..700&display=swap); diff --git a/Jellyfin.Plugin.MediaBarEnhanced/Web/mediaBarEnhanced.js b/Jellyfin.Plugin.MediaBarEnhanced/Web/mediaBarEnhanced.js index da34562..f89c321 100644 --- a/Jellyfin.Plugin.MediaBarEnhanced/Web/mediaBarEnhanced.js +++ b/Jellyfin.Plugin.MediaBarEnhanced/Web/mediaBarEnhanced.js @@ -13,6 +13,7 @@ * - option to set a maximum for the pagination dots (will turn into a counter style if exceeded) * - option to disable loading screen * - option to put collection (boxsets) IDs into the slideshow to display their items + * - option to enable client-side settings (allow users to override settings locally on their device) */ //Core Module Configuration @@ -49,6 +50,7 @@ const CONFIG = { enableSeasonalContent: false, customMediaIds: "", enableLoadingScreen: true, + enableClientSideSettings: false, }; // State management @@ -1440,7 +1442,12 @@ const SlideCreator = { } const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); - const shouldPlayVideo = CONFIG.enableVideoBackdrop && (!isMobile || CONFIG.enableMobileVideo); + + // Client Setting Overrides + const enableVideo = SettingsManager.getSetting('videoBackdrops', CONFIG.enableVideoBackdrop); + const enableMobileVideo = SettingsManager.getSetting('mobileVideo', CONFIG.enableMobileVideo); + + const shouldPlayVideo = enableVideo && (!isMobile || enableMobileVideo); if (trailerUrl && shouldPlayVideo) { let isYoutube = false; @@ -2774,7 +2781,10 @@ const SlideshowManager = { this.nextSlide(); }, CONFIG.shuffleInterval); - if (CONFIG.waitForTrailerToEnd && STATE.slideshow.slideInterval) { + // Check if we should wait for trailer + const waitForTrailer = SettingsManager.getSetting('waitForTrailer', CONFIG.waitForTrailerToEnd); + + if (waitForTrailer && STATE.slideshow.slideInterval) { const activeSlide = document.querySelector('.slide.active'); const hasActiveVideo = !!(activeSlide && activeSlide.querySelector('.video-backdrop')); if (hasActiveVideo) { @@ -2919,6 +2929,158 @@ const initArrowNavigation = () => { ); }; +const SettingsManager = { + initialized: false, + + init() { + if (this.initialized) return; + if (!CONFIG.enableClientSideSettings) return; + + this.initialized = true; + this.injectSettingsIcon(); + console.log("MediaBarEnhanced: Client-Side Settings Manager initialized."); + }, + + getSetting(key, defaultValue) { + if (!CONFIG.enableClientSideSettings) return defaultValue; + const value = localStorage.getItem(`mediaBarEnhanced-${key}`); + return value !== null ? value === 'true' : defaultValue; + }, + + setSetting(key, value) { + localStorage.setItem(`mediaBarEnhanced-${key}`, value); + }, + + createIcon() { + const button = document.createElement('button'); + button.type = 'button'; + button.className = 'paper-icon-button-light headerButton media-bar-settings-button'; + button.title = 'Media Bar Settings'; + // button.innerHTML = 'tune'; + button.innerHTML = ''; + button.style.verticalAlign = 'middle'; + + button.addEventListener('click', (e) => { + e.stopPropagation(); + this.toggleSettingsPopup(button); + }); + + return button; + }, + + injectSettingsIcon() { + const observer = new MutationObserver((mutations, obs) => { + const headerRight = document.querySelector('.headerRight'); + if (headerRight && !document.querySelector('.media-bar-settings-button')) { + const icon = this.createIcon(); + headerRight.prepend(icon); + } + }); + + observer.observe(document.body, { + childList: true, + subtree: true + }); + }, + + createPopup(anchorElement) { + const existing = document.querySelector('.media-bar-settings-popup'); + if (existing) existing.remove(); + + const popup = document.createElement('div'); + popup.className = 'media-bar-settings-popup dialog'; + + Object.assign(popup.style, { + position: 'fixed', + zIndex: '10000', + backgroundColor: '#202020', + padding: '1em', + borderRadius: '0.3em', + boxShadow: '0 0 20px rgba(0,0,0,0.5)', + minWidth: '250px', + color: '#fff', + }); + + const rect = anchorElement.getBoundingClientRect(); + + let rightPos = window.innerWidth - rect.right; + if (window.innerWidth < 450 || (window.innerWidth - rightPos) < 260) { + popup.style.right = '1rem'; + popup.style.left = 'auto'; + } else { + popup.style.right = `${rightPos}px`; + popup.style.left = 'auto'; + } + + popup.style.top = `${rect.bottom + 10}px`; + + const settings = [ + { key: 'enabled', label: 'Enable Media Bar', default: true }, + { key: 'videoBackdrops', label: 'Enable Video Backdrops', default: CONFIG.enableVideoBackdrop }, + { key: 'trailerButton', label: 'Show Trailer Button', default: CONFIG.showTrailerButton }, + { key: 'mobileVideo', label: 'Enable Mobile Video', default: CONFIG.enableMobileVideo }, + { key: 'waitForTrailer', label: 'Wait For Trailer To End', default: CONFIG.waitForTrailerToEnd }, + { key: 'slideAnimations', label: 'Enable Slide Animations', default: CONFIG.slideAnimationEnabled }, + ]; + + let html = '

Media Bar Settings

'; + + settings.forEach(setting => { + const isChecked = this.getSetting(setting.key, setting.default); + html += ` +
+ +
+ `; + }); + + // Reload button + html += ` +
+ +
+ `; + + popup.innerHTML = html; + + // Add Listeners + settings.forEach(setting => { + const checkbox = popup.querySelector(`#mb-setting-${setting.key}`); + checkbox.addEventListener('change', (e) => { + this.setSetting(setting.key, e.target.checked); + }); + }); + + popup.querySelector('#mb-settings-save').addEventListener('click', () => { + location.reload(); + }); + + const closeHandler = (e) => { + if (!popup.contains(e.target) && e.target !== anchorElement && !anchorElement.contains(e.target)) { + popup.remove(); + document.removeEventListener('click', closeHandler); + } + }; + setTimeout(() => document.addEventListener('click', closeHandler), 0); + + document.body.appendChild(popup); + }, + + toggleSettingsPopup(anchorElement) { + const existing = document.querySelector('.media-bar-settings-popup'); + if (existing) { + existing.remove(); + } else { + this.createPopup(anchorElement); + } + } +}; + /** * Initialize the slideshow */ @@ -2927,6 +3089,16 @@ const slidesInit = async () => { console.log("⚠️ Slideshow already initialized, skipping"); return; } + + if (CONFIG.enableClientSideSettings) { + SettingsManager.init(); + const isEnabled = SettingsManager.getSetting('enabled', true); + if (!isEnabled) { + console.log("MediaBarEnhanced: Disabled by client-side setting."); + return; + } + } + STATE.slideshow.hasInitialized = true; /**