Add client-side settings feature and support for SVG file type
This commit is contained in:
@@ -70,6 +70,7 @@ namespace Jellyfin.Plugin.MediaBarEnhanced.Api
|
|||||||
if (path.EndsWith(".js", StringComparison.OrdinalIgnoreCase)) return "application/javascript";
|
if (path.EndsWith(".js", StringComparison.OrdinalIgnoreCase)) return "application/javascript";
|
||||||
if (path.EndsWith(".css", StringComparison.OrdinalIgnoreCase)) return "text/css";
|
if (path.EndsWith(".css", StringComparison.OrdinalIgnoreCase)) return "text/css";
|
||||||
if (path.EndsWith(".html", StringComparison.OrdinalIgnoreCase)) return "text/html";
|
if (path.EndsWith(".html", StringComparison.OrdinalIgnoreCase)) return "text/html";
|
||||||
|
if (path.EndsWith(".svg", StringComparison.OrdinalIgnoreCase)) return "image/svg+xml";
|
||||||
return "application/octet-stream";
|
return "application/octet-stream";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,5 +34,6 @@ namespace Jellyfin.Plugin.MediaBarEnhanced.Configuration
|
|||||||
public string PreferredVideoQuality { get; set; } = "Auto";
|
public string PreferredVideoQuality { get; set; } = "Auto";
|
||||||
public bool EnableSeasonalContent { get; set; } = false;
|
public bool EnableSeasonalContent { get; set; } = false;
|
||||||
public bool IsEnabled { get; set; } = true;
|
public bool IsEnabled { get; set; } = true;
|
||||||
|
public bool EnableClientSideSettings { get; set; } = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -221,6 +221,15 @@
|
|||||||
Space (pause), M (mute/unmute)) for
|
Space (pause), M (mute/unmute)) for
|
||||||
the slideshow.</div>
|
the slideshow.</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||||
|
<label>
|
||||||
|
<input is="emby-checkbox" type="checkbox" id="EnableClientSideSettings"
|
||||||
|
name="EnableClientSideSettings" />
|
||||||
|
<span>Enable Client-Side Settings</span>
|
||||||
|
</label>
|
||||||
|
<div class="fieldDescription">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.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h2 class="sectionTitle">Time Settings</h2>
|
<h2 class="sectionTitle">Time Settings</h2>
|
||||||
<p>Leave a setting blank to use the default value.</p>
|
<p>Leave a setting blank to use the default value.</p>
|
||||||
@@ -361,7 +370,7 @@
|
|||||||
'WaitForTrailerToEnd', 'StartMuted', 'FullWidthVideo', 'EnableMobileVideo',
|
'WaitForTrailerToEnd', 'StartMuted', 'FullWidthVideo', 'EnableMobileVideo',
|
||||||
'ShowTrailerButton', 'AlwaysShowArrows', 'EnableKeyboardControls',
|
'ShowTrailerButton', 'AlwaysShowArrows', 'EnableKeyboardControls',
|
||||||
'EnableCustomMediaIds', 'CustomMediaIds', 'EnableLoadingScreen',
|
'EnableCustomMediaIds', 'CustomMediaIds', 'EnableLoadingScreen',
|
||||||
'EnableSeasonalContent'
|
'EnableSeasonalContent', 'EnableClientSideSettings'
|
||||||
];
|
];
|
||||||
|
|
||||||
keys.forEach(function (key) {
|
keys.forEach(function (key) {
|
||||||
@@ -410,7 +419,7 @@
|
|||||||
'WaitForTrailerToEnd', 'StartMuted', 'FullWidthVideo', 'EnableMobileVideo',
|
'WaitForTrailerToEnd', 'StartMuted', 'FullWidthVideo', 'EnableMobileVideo',
|
||||||
'ShowTrailerButton', 'AlwaysShowArrows', 'EnableKeyboardControls',
|
'ShowTrailerButton', 'AlwaysShowArrows', 'EnableKeyboardControls',
|
||||||
'EnableCustomMediaIds', 'CustomMediaIds', 'EnableLoadingScreen',
|
'EnableCustomMediaIds', 'CustomMediaIds', 'EnableLoadingScreen',
|
||||||
'EnableSeasonalContent'
|
'EnableSeasonalContent', 'EnableClientSideSettings'
|
||||||
];
|
];
|
||||||
|
|
||||||
keys.forEach(function (key) {
|
keys.forEach(function (key) {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
* - option to set a maximum for the pagination dots (will turn into a counter style if exceeded)
|
* - option to set a maximum for the pagination dots (will turn into a counter style if exceeded)
|
||||||
* - option to disable loading screen
|
* - option to disable loading screen
|
||||||
* - option to put collection (boxsets) IDs into the slideshow to display their items
|
* - 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);
|
@import url(https://fonts.googleapis.com/css2?family=Archivo+Narrow:ital,wght@0,400..700;1,400..700&display=swap);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
* - option to set a maximum for the pagination dots (will turn into a counter style if exceeded)
|
* - option to set a maximum for the pagination dots (will turn into a counter style if exceeded)
|
||||||
* - option to disable loading screen
|
* - option to disable loading screen
|
||||||
* - option to put collection (boxsets) IDs into the slideshow to display their items
|
* - 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
|
//Core Module Configuration
|
||||||
@@ -49,6 +50,7 @@ const CONFIG = {
|
|||||||
enableSeasonalContent: false,
|
enableSeasonalContent: false,
|
||||||
customMediaIds: "",
|
customMediaIds: "",
|
||||||
enableLoadingScreen: true,
|
enableLoadingScreen: true,
|
||||||
|
enableClientSideSettings: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
// State management
|
// State management
|
||||||
@@ -1440,7 +1442,12 @@ const SlideCreator = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
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) {
|
if (trailerUrl && shouldPlayVideo) {
|
||||||
let isYoutube = false;
|
let isYoutube = false;
|
||||||
@@ -2774,7 +2781,10 @@ const SlideshowManager = {
|
|||||||
this.nextSlide();
|
this.nextSlide();
|
||||||
}, CONFIG.shuffleInterval);
|
}, 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 activeSlide = document.querySelector('.slide.active');
|
||||||
const hasActiveVideo = !!(activeSlide && activeSlide.querySelector('.video-backdrop'));
|
const hasActiveVideo = !!(activeSlide && activeSlide.querySelector('.video-backdrop'));
|
||||||
if (hasActiveVideo) {
|
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 = '<span class="material-icons">tune</span>';
|
||||||
|
button.innerHTML = '<img src="/MediaBarEnhanced/Resources/assets/logo_SW.svg" style="width: 24px; height: 24px; vertical-align: middle;">';
|
||||||
|
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 = '<h3 style="margin-top:0; margin-bottom:1em; border-bottom:1px solid #444; padding-bottom:0.5em;">Media Bar Settings</h3>';
|
||||||
|
|
||||||
|
settings.forEach(setting => {
|
||||||
|
const isChecked = this.getSetting(setting.key, setting.default);
|
||||||
|
html += `
|
||||||
|
<div class="checkboxContainer checkboxContainer-withDescription" style="margin-bottom: 0.5em;">
|
||||||
|
<label class="emby-checkbox-label">
|
||||||
|
<input id="mb-setting-${setting.key}" type="checkbox" is="emby-checkbox" class="emby-checkbox" ${isChecked ? 'checked' : ''} />
|
||||||
|
<span class="checkboxLabel">${setting.label}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reload button
|
||||||
|
html += `
|
||||||
|
<div style="margin-top:1em; text-align:right;">
|
||||||
|
<button is="emby-button" type="button" class="raised button-submit emby-button" id="mb-settings-save">
|
||||||
|
<span>Reload</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
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
|
* Initialize the slideshow
|
||||||
*/
|
*/
|
||||||
@@ -2927,6 +3089,16 @@ const slidesInit = async () => {
|
|||||||
console.log("⚠️ Slideshow already initialized, skipping");
|
console.log("⚠️ Slideshow already initialized, skipping");
|
||||||
return;
|
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;
|
STATE.slideshow.hasInitialized = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user