diff --git a/Jellyfin.Plugin.MediaBarEnhanced/Configuration/PluginConfiguration.cs b/Jellyfin.Plugin.MediaBarEnhanced/Configuration/PluginConfiguration.cs index 11e1026..9e30120 100644 --- a/Jellyfin.Plugin.MediaBarEnhanced/Configuration/PluginConfiguration.cs +++ b/Jellyfin.Plugin.MediaBarEnhanced/Configuration/PluginConfiguration.cs @@ -54,5 +54,7 @@ namespace Jellyfin.Plugin.MediaBarEnhanced.Configuration public string CustomOverlayText { get; set; } = ""; public string CustomOverlayImageUrl { get; set; } = ""; public string CustomOverlayStyle { get; set; } = "Shadowed"; + public string CustomOverlayImageStyle { get; set; } = "None"; + public string CustomOverlayPriority { get; set; } = "Image"; } } diff --git a/Jellyfin.Plugin.MediaBarEnhanced/Configuration/configPage.html b/Jellyfin.Plugin.MediaBarEnhanced/Configuration/configPage.html index 2e16926..895e04d 100644 --- a/Jellyfin.Plugin.MediaBarEnhanced/Configuration/configPage.html +++ b/Jellyfin.Plugin.MediaBarEnhanced/Configuration/configPage.html @@ -30,14 +30,14 @@ style="background: none; border: none; color: #ccc; cursor: pointer; transition: color 0.3s, border-bottom 0.3s; padding: 0.5em 1em; border-bottom: 2px solid transparent;">

Custom Content

- +
@@ -236,10 +236,14 @@ - + + + + +
Choose the visual styling animation for your custom text.
@@ -254,7 +258,33 @@
-
Absolute URL to an image to display on the overlay. If provided, this overrides the text above.
+
Absolute URL to an image to display on the overlay.
+
+ +
+ + +
Choose a visual effect to apply to your overlay image.
+
+ +
+ + +
What should be displayed if you have both an Image and Text configured?
@@ -600,7 +630,7 @@ 'IncludeWatchedContent', 'ShowPaginationDots', 'MaxParentalRating', 'MaxDaysRecent', 'ExcludeSeasonalContent', 'HideArrowsOnMobile', 'EnableCustomOverlay', 'CustomOverlayText', 'CustomOverlayImageUrl', - 'CustomOverlayStyle' + 'CustomOverlayStyle', 'CustomOverlayImageStyle', 'CustomOverlayPriority' ]; // Manual mapping for MediaBarIsEnabled -> IsEnabled, to avoid conflicts with other plugins @@ -688,13 +718,19 @@ var clearBtn = page.querySelector('#clearOverlayImageBtn'); function updatePreview() { + var icon = dropzone.querySelector('i'); + var text = dropzone.querySelector('span'); if (urlInput.value && urlInput.value.trim() !== '') { previewImg.src = urlInput.value; previewImg.style.display = 'block'; clearBtn.style.display = 'block'; + if(icon) icon.style.display = 'none'; + if(text) text.style.display = 'none'; } else { previewImg.style.display = 'none'; clearBtn.style.display = 'none'; + if(icon) icon.style.display = 'block'; + if(text) text.style.display = 'block'; } } @@ -824,7 +860,7 @@ 'IncludeWatchedContent', 'ShowPaginationDots', 'MaxParentalRating', 'MaxDaysRecent', 'ExcludeSeasonalContent', 'HideArrowsOnMobile', 'EnableCustomOverlay', 'CustomOverlayText', 'CustomOverlayImageUrl', - 'CustomOverlayStyle' + 'CustomOverlayStyle', 'CustomOverlayImageStyle', 'CustomOverlayPriority' ]; keys.forEach(function (key) { @@ -924,23 +960,27 @@ ' ' + '
Comma-separated or Newline separated list of Movie/Series/Collection IDs to show during this season.
Same options available as for the default media IDs.
' + '' + - '
' + - ' ' + - '
Optional: Override the global custom overlay text during this season.
' + - '
' + - '
' + - ' ' + - '
Optional: Override the global custom overlay image during this season. Overrides the text if provided.
' + - '
' + - '
' + - '
' + - ' cloud_upload' + - ' Drag and drop a seasonal image here, or click' + - ' ' + - ' ' + - ' ' + + '

Custom Overrides

' + + '
' + + '
' + + '
' + + ' ' + + '
' + + '
' + + ' ' + + '
Override the global text or image URL.
' + + '
' + + '
' + + '
' + + '
' + + ' cloud_upload' + + ' Upload seasonal image' + + ' ' + + ' ' + + ' ' + + '
' + '
' + '
'; @@ -1005,15 +1045,21 @@ }); function updatePreview() { + var icon = dropzone.querySelector('i'); + var text = dropzone.querySelector('span'); var val = urlInput.value.trim(); if (val) { previewImg.src = val; previewImg.style.display = 'block'; clearBtn.style.display = 'block'; + if(icon) icon.style.display = 'none'; + if(text) text.style.display = 'none'; } else { previewImg.src = ''; previewImg.style.display = 'none'; clearBtn.style.display = 'none'; + if(icon) icon.style.display = 'block'; + if(text) text.style.display = 'block'; } } diff --git a/Jellyfin.Plugin.MediaBarEnhanced/Web/mediaBarEnhanced.css b/Jellyfin.Plugin.MediaBarEnhanced/Web/mediaBarEnhanced.css index e5e9362..5788709 100644 --- a/Jellyfin.Plugin.MediaBarEnhanced/Web/mediaBarEnhanced.css +++ b/Jellyfin.Plugin.MediaBarEnhanced/Web/mediaBarEnhanced.css @@ -1112,31 +1112,32 @@ .custom-overlay-style-Frosted { color: #fff; - background: rgba(0, 0, 0, 0.4); - backdrop-filter: blur(10px); - -webkit-backdrop-filter: blur(10px); - padding: 8px 24px; + background: linear-gradient(135deg, rgba(255,255,255,0.15), rgba(255,255,255,0)); + backdrop-filter: blur(20px) saturate(1.5); + -webkit-backdrop-filter: blur(20px) saturate(1.5); + padding: 10px 30px; border-radius: 50px; - border: 1px solid rgba(255, 255, 255, 0.2); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5); - text-shadow: none; /* override default */ + border: 1px solid rgba(255, 255, 255, 0.3); + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + border-right: 1px solid rgba(255, 255, 255, 0.1); + box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37); + text-shadow: 1px 1px 2px rgba(0,0,0,0.5); } .custom-overlay-style-Cinematic { - background: linear-gradient(to right, #bf953f, #fcf6ba, #b38728, #fbf5b7, #aa771c); + background: linear-gradient(to right, #bf953f, #fcf6ba, #b38728, #fbf5b7, #aa771c, #fcf6ba, #bf953f); -webkit-background-clip: text; background-clip: text; color: transparent; text-shadow: none; /* override default */ filter: drop-shadow(0px 2px 8px rgba(255, 215, 0, 0.4)) drop-shadow(2px 2px 4px rgba(0,0,0,0.8)); - animation: shineCinematic 4s linear infinite; + animation: shineCinematic 6s linear infinite; background-size: 200% auto; } @keyframes shineCinematic { - to { - background-position: 200% center; - } + 0% { background-position: 0% center; } + 100% { background-position: 200% center; } } .custom-overlay-style-Pulse { @@ -1248,3 +1249,92 @@ opacity: 1; } } + +/* Image Overlay Styles */ +.custom-overlay-img-RoundedShadow { + border-radius: 12px; + filter: drop-shadow(0px 10px 15px rgba(0, 0, 0, 0.6)); +} + +.custom-overlay-img-GlowingBorder { + border-radius: 8px; + box-shadow: 0 0 10px #00a4dc, 0 0 20px #00a4dc, 0 0 30px #00a4dc, inset 0 0 10px #00a4dc; + animation: pulseGlowImg 2s infinite alternate; +} +@keyframes pulseGlowImg { + from { box-shadow: 0 0 10px #00a4dc, 0 0 20px #00a4dc, 0 0 30px #00a4dc; } + to { box-shadow: 0 0 15px #00a4dc, 0 0 30px #00a4dc, 0 0 45px #00a4dc; } +} + +.custom-overlay-img-Polaroid { + background: white; + padding: 10px 10px 25px 10px; + box-shadow: 0 4px 8px rgba(0,0,0,0.4), 0 12px 24px rgba(0,0,0,0.3); + transform: rotate(-3deg); + border-radius: 2px; +} + +.custom-overlay-img-Vintage { + filter: sepia(0.6) contrast(1.2) brightness(0.9) saturate(1.2) drop-shadow(2px 4px 8px rgba(0,0,0,0.6)); +} + +.custom-overlay-img-Grayscale { + filter: grayscale(100%) contrast(1.1) drop-shadow(2px 4px 8px rgba(0,0,0,0.6)); +} + +/* More Text Overlay Styles */ +.custom-overlay-style-SteadyNeon { + color: #fff; + text-shadow: + 0 0 5px #fff, + 0 0 10px #fff, + 0 0 20px #ff00de, + 0 0 40px #ff00de, + 0 0 80px #ff00de; +} + +.custom-overlay-style-Glitch { + color: #fff; + position: relative; + text-shadow: 2px 2px 8px rgba(0,0,0,0.8); + animation: glitchText 3s infinite; +} + +@keyframes glitchText { + 0% { text-shadow: 2px 2px 8px rgba(0,0,0,0.8); transform: translate(0); } + 20% { text-shadow: 2px 2px 8px rgba(0,0,0,0.8); transform: translate(0); } + 21% { text-shadow: -2px 0 0 #ff00c1, 2px 0 0 #00fff9; transform: translate(-2px, 1px); } + 23% { text-shadow: 2px 0 0 #ff00c1, -2px 0 0 #00fff9; transform: translate(2px, -1px); } + 25% { text-shadow: 2px 2px 8px rgba(0,0,0,0.8); transform: translate(0); } + 100% { text-shadow: 2px 2px 8px rgba(0,0,0,0.8); transform: translate(0); } +} + +.custom-overlay-style-RetroPop { + color: #f7f7f7; + text-shadow: + 2px 2px 0px #ff0055, + 4px 4px 0px #00a4dc, + 6px 6px 0px #ffcc00, + 8px 8px 10px rgba(0,0,0,0.6); + font-weight: 900; + letter-spacing: 2px; +} + +.custom-overlay-style-Shimmer { + color: rgba(255,255,255,0.7); + background: linear-gradient(to right, #222 20%, #fff 40%, #fff 60%, #222 80%); + background-size: 200% auto; + color: #000; + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + animation: shimmerText 3s linear infinite; + text-shadow: 2px 2px 8px rgba(0,0,0,0.3); +} + +@keyframes shimmerText { + to { + background-position: 200% center; + } +} + diff --git a/Jellyfin.Plugin.MediaBarEnhanced/Web/mediaBarEnhanced.js b/Jellyfin.Plugin.MediaBarEnhanced/Web/mediaBarEnhanced.js index afc6f22..def0ed9 100644 --- a/Jellyfin.Plugin.MediaBarEnhanced/Web/mediaBarEnhanced.js +++ b/Jellyfin.Plugin.MediaBarEnhanced/Web/mediaBarEnhanced.js @@ -62,6 +62,8 @@ const CONFIG = { customOverlayText: "", customOverlayImageUrl: "", customOverlayStyle: "Shadowed", + customOverlayImageStyle: "None", + customOverlayPriority: "Image", enableCustomMediaIds: true, enableSeasonalContent: false, customMediaIds: "", @@ -3825,14 +3827,9 @@ const slidesInit = async () => { if (isActive) { if (section.OverlayText || section.OverlayImageUrl) { isSeasonOverride = true; - // If the season has an image, clear text, and vice versa. - if (section.OverlayImageUrl) { - activeOverlayImage = section.OverlayImageUrl; - activeOverlayText = null; - } else if (section.OverlayText) { - activeOverlayText = section.OverlayText; - activeOverlayImage = null; - } + // Season fully overrides global overlay, even if empty + activeOverlayImage = section.OverlayImageUrl || null; + activeOverlayText = section.OverlayText || null; } break; } @@ -3851,12 +3848,17 @@ const slidesInit = async () => { const overlayContainer = document.createElement("div"); overlayContainer.className = "custom-overlay-container"; - if (activeOverlayImage) { + const overlayPriority = CONFIG.customOverlayPriority || "Image"; + const showImage = activeOverlayImage && (overlayPriority === "Image" || !activeOverlayText); + const showText = activeOverlayText && (!showImage); + + if (showImage) { const img = document.createElement("img"); - img.className = "custom-overlay-image"; + const imgStyle = CONFIG.customOverlayImageStyle || "None"; + img.className = `custom-overlay-image custom-overlay-img-${imgStyle}`; img.src = activeOverlayImage; overlayContainer.appendChild(img); - } else if (activeOverlayText) { + } else if (showText) { const p = document.createElement("p"); p.className = `custom-overlay-text custom-overlay-style-${CONFIG.customOverlayStyle || 'Shadowed'}`; p.textContent = activeOverlayText;