diff --git a/Jellyfin.Plugin.MediaBarEnhanced/Api/OverlayImageController.cs b/Jellyfin.Plugin.MediaBarEnhanced/Api/OverlayImageController.cs index c222d0e..0e58f4d 100644 --- a/Jellyfin.Plugin.MediaBarEnhanced/Api/OverlayImageController.cs +++ b/Jellyfin.Plugin.MediaBarEnhanced/Api/OverlayImageController.cs @@ -20,17 +20,13 @@ namespace Jellyfin.Plugin.MediaBarEnhanced.Api public OverlayImageController(IApplicationPaths applicationPaths) { _applicationPaths = applicationPaths; - - // We use the plugin's data folder to store the image - _imageDirectory = MediaBarEnhancedPlugin.Instance?.DataFolderPath ?? Path.Combine(applicationPaths.DataPath, "plugins", "MediaBarEnhanced"); - - // We no longer define the exact path here, just the directory - // The filename is determined per request + _imageDirectory = Path.Combine(applicationPaths.DataPath, "plugins", "configurations", "MediaBarEnhancedAssets"); } /// /// Uploads a new custom overlay image. /// + // [Microsoft.AspNetCore.Authorization.Authorize] [HttpPost("OverlayImage")] [Consumes("multipart/form-data")] public async Task UploadImage([FromForm] IFormFile file, [FromQuery] string? filename = null) @@ -40,10 +36,12 @@ namespace Jellyfin.Plugin.MediaBarEnhanced.Api return BadRequest("No file uploaded."); } - string targetFileName = string.IsNullOrWhiteSpace(filename) - ? "custom_overlay_image.dat" - : $"custom_overlay_image_{filename}.dat"; - string targetPath = Path.Combine(_imageDirectory, targetFileName); + // Extract original extension or fallback to .jpg + string extension = Path.GetExtension(file.FileName); + if (string.IsNullOrWhiteSpace(extension)) extension = ".jpg"; + + // Delete any existing file with this prefix before saving the new one (as extensions might differ) + string prefix = string.IsNullOrWhiteSpace(filename) ? "custom_overlay_image_global" : $"custom_overlay_image_{filename}"; try { @@ -52,6 +50,16 @@ namespace Jellyfin.Plugin.MediaBarEnhanced.Api Directory.CreateDirectory(_imageDirectory); } + // Remove existing + var existingFiles = Directory.GetFiles(_imageDirectory, $"{prefix}.*"); + foreach(var extFile in existingFiles) + { + System.IO.File.Delete(extFile); + } + + string targetFileName = $"{prefix}{extension}"; + string targetPath = Path.Combine(_imageDirectory, targetFileName); + // Delete is not strictly necessary and can cause locking issues if someone is currently reading it. // FileMode.Create will truncate the file if it exists, effectively overwriting it. // We use FileShare.None to ensure we have exclusive write access, but handle potential IOExceptions gracefully. @@ -74,18 +82,20 @@ namespace Jellyfin.Plugin.MediaBarEnhanced.Api /// /// Retrieves the custom overlay image. /// + // [Microsoft.AspNetCore.Authorization.Authorize] [HttpGet("OverlayImage")] public IActionResult GetImage([FromQuery] string? filename = null) { - string targetFileName = string.IsNullOrWhiteSpace(filename) - ? "custom_overlay_image.dat" - : $"custom_overlay_image_{filename}.dat"; - string targetPath = Path.Combine(_imageDirectory, targetFileName); - - if (!System.IO.File.Exists(targetPath)) - { + string prefix = string.IsNullOrWhiteSpace(filename) ? "custom_overlay_image_global" : $"custom_overlay_image_{filename}"; + + if (!Directory.Exists(_imageDirectory)) return NotFound(); - } + + var existingFiles = Directory.GetFiles(_imageDirectory, $"{prefix}.*"); + if (existingFiles.Length == 0) + return NotFound(); + + string targetPath = existingFiles[0]; // Read the file and return as a generic octet stream. // We use FileShare.ReadWrite so that if someone is currently overwriting the file (uploading), we don't block them, @@ -99,25 +109,27 @@ namespace Jellyfin.Plugin.MediaBarEnhanced.Api /// /// Deletes a custom overlay image. /// + // [Microsoft.AspNetCore.Authorization.Authorize] [HttpDelete("OverlayImage")] public IActionResult DeleteImage([FromQuery] string? filename = null) { - string targetFileName = string.IsNullOrWhiteSpace(filename) - ? "custom_overlay_image.dat" - : $"custom_overlay_image_{filename}.dat"; - string targetPath = Path.Combine(_imageDirectory, targetFileName); - - if (System.IO.File.Exists(targetPath)) + string prefix = string.IsNullOrWhiteSpace(filename) ? "custom_overlay_image_global" : $"custom_overlay_image_{filename}"; + + if (Directory.Exists(_imageDirectory)) { - try + var existingFiles = Directory.GetFiles(_imageDirectory, $"{prefix}.*"); + foreach(var file in existingFiles) { - System.IO.File.Delete(targetPath); - return Ok(); - } - catch (Exception ex) - { - return StatusCode(500, $"Error deleting file: {ex.Message}"); + try + { + System.IO.File.Delete(file); + } + catch (Exception ex) + { + return StatusCode(500, $"Error deleting file: {ex.Message}"); + } } + return Ok(); } return NotFound(); @@ -126,6 +138,7 @@ namespace Jellyfin.Plugin.MediaBarEnhanced.Api /// /// Renames a custom overlay image (used when a seasonal section is renamed). /// + // [Microsoft.AspNetCore.Authorization.Authorize] [HttpPut("OverlayImage/Rename")] public IActionResult RenameImage([FromQuery] string oldName, [FromQuery] string newName) { @@ -134,22 +147,23 @@ namespace Jellyfin.Plugin.MediaBarEnhanced.Api return BadRequest("Both oldName and newName must be provided."); } - string oldPath = Path.Combine(_imageDirectory, $"custom_overlay_image_{oldName}.dat"); - string newPath = Path.Combine(_imageDirectory, $"custom_overlay_image_{newName}.dat"); + if (!Directory.Exists(_imageDirectory)) + return Ok(); - if (!System.IO.File.Exists(oldPath)) - { - // If it doesn't exist, there is nothing to rename, but we still consider it a success - // since the end state (file with oldName is gone, file with newName doesn't exist yet) is acceptable. + var oldFiles = Directory.GetFiles(_imageDirectory, $"custom_overlay_image_{oldName}.*"); + if (oldFiles.Length == 0) return Ok(); - } try { + string oldPath = oldFiles[0]; + string extension = Path.GetExtension(oldPath); + string newPath = Path.Combine(_imageDirectory, $"custom_overlay_image_{newName}{extension}"); + // If a file with the new name already exists, delete it first to avoid conflicts - if (System.IO.File.Exists(newPath)) - { - System.IO.File.Delete(newPath); + var existingNewFiles = Directory.GetFiles(_imageDirectory, $"custom_overlay_image_{newName}.*"); + foreach(var existing in existingNewFiles) { + System.IO.File.Delete(existing); } System.IO.File.Move(oldPath, newPath); diff --git a/Jellyfin.Plugin.MediaBarEnhanced/Configuration/configPage.html b/Jellyfin.Plugin.MediaBarEnhanced/Configuration/configPage.html index 1db654d..3af78be 100644 --- a/Jellyfin.Plugin.MediaBarEnhanced/Configuration/configPage.html +++ b/Jellyfin.Plugin.MediaBarEnhanced/Configuration/configPage.html @@ -259,8 +259,8 @@ -
Uploading an image will securely save it to the server and automatically update the URL field above.
@@ -273,6 +273,7 @@
Choose the visual styling animation for your custom text.
@@ -300,6 +304,9 @@ + + +
Choose a visual effect to apply to your overlay image.
@@ -728,12 +735,26 @@ previewImg.style.display = 'block'; clearBtn.style.display = 'block'; if(icon) icon.style.display = 'none'; - if(text) text.style.display = 'none'; + if(text) { + text.textContent = 'Drag and drop a new image here, or click to select'; + text.style.display = 'block'; + text.style.position = 'absolute'; + text.style.bottom = '10px'; + text.style.zIndex = '3'; + text.style.backgroundColor = 'rgba(0,0,0,0.6)'; + text.style.padding = '4px 8px'; + text.style.borderRadius = '4px'; + } } else { previewImg.style.display = 'none'; clearBtn.style.display = 'none'; if(icon) icon.style.display = 'block'; - if(text) text.style.display = 'block'; + if(text) { + text.textContent = 'Drag and drop an image here, or click to select'; + text.style.display = 'block'; + text.style.position = 'static'; + text.style.backgroundColor = 'transparent'; + } } } @@ -981,7 +1002,7 @@ ' Upload seasonal image' + ' ' + ' ' + - ' ' + + ' ' + ' ' + ' ' + ''; @@ -1055,13 +1076,29 @@ previewImg.style.display = 'block'; clearBtn.style.display = 'block'; if(icon) icon.style.display = 'none'; - if(text) text.style.display = 'none'; + if(text) { + text.textContent = 'Drag and drop a new image here...'; + text.style.display = 'block'; + text.style.position = 'absolute'; + text.style.bottom = '5px'; + text.style.zIndex = '3'; + text.style.backgroundColor = 'rgba(0,0,0,0.6)'; + text.style.padding = '2px 6px'; + text.style.borderRadius = '4px'; + text.style.fontSize = '0.7em'; + } } else { previewImg.src = ''; previewImg.style.display = 'none'; clearBtn.style.display = 'none'; if(icon) icon.style.display = 'block'; - if(text) text.style.display = 'block'; + if(text) { + text.textContent = 'Upload seasonal image'; + text.style.display = 'block'; + text.style.position = 'static'; + text.style.backgroundColor = 'transparent'; + text.style.fontSize = '0.85em'; + } } } diff --git a/Jellyfin.Plugin.MediaBarEnhanced/Web/mediaBarEnhanced.css b/Jellyfin.Plugin.MediaBarEnhanced/Web/mediaBarEnhanced.css index 5788709..de5e97b 100644 --- a/Jellyfin.Plugin.MediaBarEnhanced/Web/mediaBarEnhanced.css +++ b/Jellyfin.Plugin.MediaBarEnhanced/Web/mediaBarEnhanced.css @@ -1031,7 +1031,8 @@ -webkit-backdrop-filter: none; } } -/* Floating Custom Overlay Styling */ + +/* Custom Overlay Styling */ .custom-overlay-container { position: absolute; top: 8vh; @@ -1040,10 +1041,27 @@ display: flex; align-items: center; justify-content: flex-start; - pointer-events: none; /* Let clicks pass through to the slider */ - animation: fadeInOverlay 1.5s ease-in-out forwards; + pointer-events: none; + animation: overlayFadeInGlobal 1.5s ease-in-out forwards; + /* animation: fadeInOverlay 1.5s ease-in-out forwards; */ } +@keyframes overlayFadeInGlobal { + from { opacity: 0; } + to { opacity: 1; } +} + +/* @keyframes fadeInOverlay { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} */ + .custom-overlay-text { font-family: "Archivo Narrow", sans-serif; color: #fff; @@ -1061,21 +1079,10 @@ filter: drop-shadow(2px 4px 6px rgba(0,0,0,0.5)); } -@keyframes fadeInOverlay { - from { - opacity: 0; - transform: translateY(-10px); - } - to { - opacity: 1; - transform: translateY(0); - } -} -/* Make it smaller on mobile portrait */ @media only screen and (max-width: 767px) and (orientation: portrait) { .custom-overlay-container { - top: 5vh; + top: 8%; /* oder 5vh? */ left: 50%; transform: translateX(-50%); width: 90%; @@ -1084,15 +1091,10 @@ } .custom-overlay-text { - font-size: 1.8rem; - } - - .custom-overlay-image { - max-width: 200px; - max-height: 80px; + font-size: 1.5rem; /* 1.5 - 1.8 */ } - @keyframes fadeInOverlay { + /* @keyframes fadeInOverlay { from { opacity: 0; transform: translate(-50%, -10px); @@ -1101,10 +1103,20 @@ opacity: 1; transform: translate(-50%, 0); } + } */ + + .custom-overlay-image { + max-width: 150px; /* oder 200px? */ + max-height: 60px; /* oder 80px? */ } } -/* Custom Overlay Styles */ +/* Custom Overlay Text Styles */ +.custom-overlay-style-None { + color: #fff; + text-shadow: none; +} + .custom-overlay-style-Shadowed { color: #fff; text-shadow: 2px 2px 8px rgba(0, 0, 0, 0.9), -1px -1px 4px rgba(0, 0, 0, 0.8); @@ -1129,7 +1141,7 @@ -webkit-background-clip: text; background-clip: text; color: transparent; - text-shadow: none; /* override default */ + text-shadow: none; 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 6s linear infinite; background-size: 200% auto; @@ -1155,8 +1167,6 @@ } } - -/* Text Overlay Styles */ .custom-overlay-style-Neon { color: #fff; text-shadow: @@ -1250,39 +1260,6 @@ } } -/* 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: @@ -1338,3 +1315,94 @@ } } +.custom-overlay-style-Wave { + color: #fff; + text-shadow: 2px 2px 8px rgba(0,0,0,0.8); + display: inline-block; + animation: liquidWave 3s ease-in-out infinite; +} +@keyframes liquidWave { + 0%, 100% { transform: translateY(0) skewY(0); } + 25% { transform: translateY(-3px) skewY(1deg); } + 75% { transform: translateY(3px) skewY(-1deg); } +} + +.custom-overlay-style-VHS { + color: #fff; + text-shadow: 3px 0 0 #f00, -3px 0 0 #0ff; + animation: vhsTracking 2s steps(2, start) infinite; +} +@keyframes vhsTracking { + 0%, 100% { text-shadow: 3px 0 0 #f00, -3px 0 0 #0ff; } + 50% { text-shadow: 2px 1px 0 #f00, -2px -1px 0 #0ff; } +} + +.custom-overlay-style-Matrix { + color: #0f0; + font-family: monospace; + text-shadow: 0 0 5px #0f0, 0 0 10px #0f0; + letter-spacing: 2px; + animation: matrixGlow 2s infinite alternate; +} +@keyframes matrixGlow { + to { text-shadow: 0 0 10px #0f0, 0 0 20px #0f0; } +} + +/* Custom Overlay Image 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)); +} + +.custom-overlay-img-Hologram { + filter: drop-shadow(0 0 10px #0ff) sepia(0.8) hue-rotate(180deg) saturate(3); + opacity: 0.8; + animation: hologramFlicker 3s infinite; +} +@keyframes hologramFlicker { + 0% { opacity: 0.8; transform: skewX(0); } + 5% { opacity: 0.5; transform: skewX(2deg); } + 10% { opacity: 0.9; transform: skewX(-2deg); } + 15% { opacity: 0.8; transform: skewX(0); } + 100% { opacity: 0.8; } +} + +.custom-overlay-img-CRT { + filter: contrast(1.5) brightness(1.2) drop-shadow(3px 0 0 rgba(255,0,0,0.5)) drop-shadow(-3px 0 0 rgba(0,0,255,0.5)); +} + +.custom-overlay-img-Floating { + filter: drop-shadow(0 15px 20px rgba(0,0,0,0.6)); + animation: floatImg 4s ease-in-out infinite; +} +@keyframes floatImg { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-15px); } +} + diff --git a/configPageExample.html b/configPageExample.html new file mode 100644 index 0000000..3ab30c1 --- /dev/null +++ b/configPageExample.html @@ -0,0 +1,2911 @@ + + + + Jellyfin Enhanced + + +
+
+
+
+
+

Jellyfin Enhanced

+ + + Help + +
+
+
+ + + +
+ + + + + +
+
+
+
+

Default User Settings

+
+
+ ⏯️ Playback Settings +
+
+
+
+
+
+
+
+
+ ↪️ Auto-Skip Settings +
+
+
+
+
+
+ 📝 Subtitle Settings +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ 🎲 Random Button Settings +
+
+
+
+
+
+
+
+ 🖥️ UI Settings +
+
+
+ + +
Choose default display for watch progress.
+
+
+ + +
Select how time-based progress is shown.
+
+
+
+
+
+
+
+
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
How long to cache the above tags data (1-365 days, default: 30)
+
+
+ +
+
+ +
Forces all clients to clear Language, Quality, Genre and Rating tag caches on next load
After the cache is cleared, the clients will re-fetch the tags data to build the cache again, which might cause some slowness on first load.
+
+
+
+
+
+
+ 🌐 Language Settings +
+
+ + +
Sets the default language for all users. Users can still override this in their own settings.
+
+
+
+
+
+
Disables all keyboard shortcuts, and hides the 'Shortcuts' tab in enhanced panel
+
+
+
+ +
+
+ info +
+ This will save the current configuration and apply all the above settings to every user on this server.
+ Note: Users can individually customize their own settings by opening the Enhanced Panel:
+ • Press ? key (default shortcut)
+ • Long Press on user profile picture in the top-right corner
+ • Through "Jellyfin Enhanced" link from the side bar
+ • Access via playback controls menu during video playback (only on mobile devices) +
+
+
+
+
+
+

Icons

+
+

Icon Settings

+
+ +
+
+ Display icons throughout the UI (toasts, settings panel headers, etc.) +
+
+ + +
Choose between emoji characters, Lucide icons, or Material UI icons.
+
+
+
+

Shortcut Overrides

+
+

Add Override

+
+
+ +
+
+ +
+ +
+ +

+ Modifier Keys: Use `Shift+`, `Ctrl+`, or `Alt+`. Examples: `Shift+A`, `Ctrl+S`. +

+
+

Configured Overrides

+
+
+
+
+

Bookmarks

+
+
+ +
+
+ Save custom bookmarks/timestamps while watching videos to quickly jump back to your favorite scenes or moments. +
+
+ +
+
Adds a "Bookmarks" link to the sidebar via Plugin Pages.
Note: Jellyfin must be restarted after enabling this option for the first time for changes to take effect.
+
+
+ info +
+

How to Use Bookmarks:

+
    +
  • During Playback: Press B or click the bookmark icon bookmark_add in the video player controls to save a bookmark at the current time.
  • +
  • Visual Markers: Bookmarks appear as location pins location_pin on the video progress bar. Click a marker to jump to that timestamp.
  • +
  • Multi-Version Support: Bookmarks track by TMDB/TVDB IDs, so they work across different file versions of the same content.
  • +
+

To View & Manage All Your Bookmarks:

+

You can either enable Plugin Pages above to add a sidebar link, or create a custom view using the Custom Tabs plugin.

+
    +
  1. Install the Custom Tabs plugin and all its prerequisites.
  2. +
  3. Navigate to Dashboard > Plugins > Custom Tabs and add a new tab.
  4. +
  5. Add "Display Text" according to your preference (e.g., "My Bookmarks").
  6. +
  7. Copy the code below and paste it into the 'HTML Content' field.
  8. +
+
+ +
<div class="sections bookmarks"></div>
+
+
+
+ Already have Custom Tabs installed? + Click here to configure it. +
+
+
+
+
+
+
+

Timeout Settings

+
+
+ + +
How long the help panel stays open before auto-closing.
+
+
+ + +
How long toast notifications are displayed.
+
+
+
+
+

Hidden Content

+
+
+ +
+
Enable or disable the Hidden Content feature server-wide. When disabled, hide buttons, filtering, sidebar navigation, and settings panel options are all removed. User data is preserved and restored when re-enabled.
+
+ +
+
Replaces the default Hidden Content page with a Plugin Pages implementation.
Note: Jellyfin must be restarted after enabling this option for the first time for changes to take effect.
+
+ +
+
+
+ info +
+

Embed Hidden Content in Custom Tabs:

+

Display the Hidden Content page in a Custom Tabs tab instead of the sidebar link.

+

You need the Custom Tabs plugin.

+
    +
  1. Install the Custom Tabs plugin and all its prerequisites.
  2. +
  3. Navigate to Dashboard > Plugins > Custom Tabs and add a new tab.
  4. +
  5. Add "Display Text" according to your preference (e.g., "Hidden Content").
  6. +
  7. Copy the code below and paste it into the 'HTML Content' field.
  8. +
+
+ +
<div class="jellyfinenhanced hidden-content"></div>
+
+
+
+ Already have Custom Tabs installed? + Click here to configure it. +
+
+
+
+
+
+
+ + + + + +
+ info +
+ All changes require a page refresh to take effect.
+ If old settings persist, please force clear browser cache. +
+
+
+ +
+ +
+
+
+ +
+ +