diff --git a/JellyfinPlugin/Api/BlackBarService.cs b/JellyfinPlugin/Api/BlackBarService.cs index 8e896b8..132b665 100644 --- a/JellyfinPlugin/Api/BlackBarService.cs +++ b/JellyfinPlugin/Api/BlackBarService.cs @@ -61,10 +61,27 @@ namespace JellyfinPlugin.Api return Content(string.Empty, "text/plain"); } + [HttpGet("Config")] + [Produces("application/json")] + public IActionResult GetConfig() + { + var config = Plugin.Instance!.Configuration; + return Ok(new + { + cropDetectLimit = config.CropDetectLimit, + playbackMode = config.PlaybackMode, + subtitleOffsetVh = config.SubtitleOffsetVh, + maxParallelScans = config.MaxParallelScans + }); + } + [HttpGet("ClientScript.js")] [Produces("application/javascript")] public ContentResult GetClientScript() { + double defaultSubOffset = Plugin.Instance!.Configuration.SubtitleOffsetVh; + string defaultSubOffsetStr = defaultSubOffset.ToString(System.Globalization.CultureInfo.InvariantCulture); + string js = @" (function() { console.log('Black Bar Remover: Script loaded'); @@ -73,6 +90,8 @@ namespace JellyfinPlugin.Api let currentCropData = null; window._currentPlayMethod = null; window._isVideoDirect = null; + window._configuredSubtitleOffsetVh = {SUB_OFFSET}; + window._customSubtitleOffsetVh = null; function isTranscoding() { if (window._isVideoDirect === false) { @@ -84,6 +103,85 @@ namespace JellyfinPlugin.Api return false; } + function fetchServerConfig() { + if (!window._originalFetch) return; + window._originalFetch('/BlackBarRemover/Config') + .then(r => r.json()) + .then(data => { + if (data && typeof data.subtitleOffsetVh === 'number') { + const newOffset = data.subtitleOffsetVh; + if (newOffset !== window._configuredSubtitleOffsetVh) { + window._configuredSubtitleOffsetVh = newOffset; + console.log('Black Bar Remover: Synced configured subtitle offset:', newOffset + 'vh'); + if (window._customSubtitleOffsetVh === null && lastBottomPercent > 0) { + updateSubtitleStyles(lastBottomPercent); + } + } + } + }) + .catch(e => {}); + } + + function showSubtitleToast(msg) { + let toastEl = document.getElementById('black-bar-remover-sub-toast'); + if (!toastEl) { + toastEl = document.createElement('div'); + toastEl.id = 'black-bar-remover-sub-toast'; + toastEl.style.position = 'fixed'; + toastEl.style.bottom = '15%'; + toastEl.style.left = '50%'; + toastEl.style.transform = 'translateX(-50%)'; + toastEl.style.backgroundColor = 'rgba(0, 0, 0, 0.85)'; + toastEl.style.color = '#fff'; + toastEl.style.padding = '8px 16px'; + toastEl.style.borderRadius = '20px'; + toastEl.style.fontSize = '0.95rem'; + toastEl.style.fontWeight = 'bold'; + toastEl.style.zIndex = '999999'; + toastEl.style.pointerEvents = 'none'; + toastEl.style.transition = 'opacity 0.3s ease'; + document.body.appendChild(toastEl); + } + toastEl.textContent = msg; + toastEl.style.opacity = '1'; + if (window._toastTimeout) clearTimeout(window._toastTimeout); + window._toastTimeout = setTimeout(() => { + toastEl.style.opacity = '0'; + }, 2000); + } + + function getActiveOffsetVh() { + if (window._customSubtitleOffsetVh !== null && window._customSubtitleOffsetVh !== undefined) { + return window._customSubtitleOffsetVh; + } + if (window._configuredSubtitleOffsetVh !== null && window._configuredSubtitleOffsetVh !== undefined) { + return window._configuredSubtitleOffsetVh; + } + return 5.0; + } + + if (!window._subHotkeyBound) { + window._subHotkeyBound = true; + window.addEventListener('keydown', function(e) { + if (e.altKey && (e.key === 'ArrowUp' || e.key === 'ArrowDown')) { + const video = document.querySelector('video'); + if (!video) return; + e.preventDefault(); + let current = getActiveOffsetVh(); + if (e.key === 'ArrowUp') { + current = Math.min(25.0, current + 0.5); + } else if (e.key === 'ArrowDown') { + current = Math.max(0.0, current - 0.5); + } + window._customSubtitleOffsetVh = Math.round(current * 10) / 10; + showSubtitleToast('Subtitle Offset: ' + window._customSubtitleOffsetVh.toFixed(1) + ' vh (Alt+Up/Down)'); + if (lastBottomPercent > 0) { + updateSubtitleStyles(lastBottomPercent); + } + } + }); + } + function ensureBoundingBoxHook(video) { if (!video._origGetBoundingClientRect) { video._origGetBoundingClientRect = video.getBoundingClientRect; @@ -109,60 +207,53 @@ namespace JellyfinPlugin.Api let lastBottomPercent = 0; - function updateSubtitleStyles(bottomPercent) { + function updateSubtitleStyles(bottomPercent) {{ lastBottomPercent = bottomPercent; let styleEl = document.getElementById('black-bar-remover-sub-style'); - if (!styleEl) { + if (!styleEl) {{ styleEl = document.createElement('style'); styleEl.id = 'black-bar-remover-sub-style'; document.head.appendChild(styleEl); - } + }} - if (bottomPercent > 0) { - const offsetVh = (bottomPercent * 0.4 + 2.5).toFixed(2); - styleEl.textContent = ` - .videoSubtitles { - display: flex !important; - flex-direction: column !important; - justify-content: flex-end !important; - transform: translateY(-${offsetVh}vh) !important; - } - .track-text-output, - .custom-subtitle-overlay, - .subtitleSyncContainer, - .videoSubtitlesContainer, - .libassjs-canvas-parent, - canvas.libassjs-canvas, - .subtitles-octopus-canvas, - .htmlvideoplayer::-webkit-media-text-track-container, - .htmlvideoplayer::-webkit-media-text-track-display { - transform: translateY(-${offsetVh}vh) !important; - } - `; + if (bottomPercent > 0) {{ + const activeOffset = getActiveOffsetVh(); + const offsetVh = activeOffset.toFixed(2); + styleEl.textContent = + '.videoSubtitles {' + + 'display: flex !important;' + + 'flex-direction: column !important;' + + 'justify-content: flex-end !important;' + + 'transform: translateY(-' + offsetVh + 'vh) !important;' + + '}' + + '.track-text-output, .custom-subtitle-overlay, .subtitleSyncContainer, .videoSubtitlesContainer, .libassjs-canvas-parent, canvas.libassjs-canvas, .subtitles-octopus-canvas, .htmlvideoplayer::-webkit-media-text-track-container, .htmlvideoplayer::-webkit-media-text-track-display {' + + 'transform: translateY(-' + offsetVh + 'vh) !important;' + + '}'; console.log('Black Bar Remover: Subtitle CSS style updated translateY(-' + offsetVh + 'vh)'); - } else { + }} else {{ styleEl.textContent = ''; - } - } + }} + }} - function adjustVttCues(bottomPercent) { + function adjustVttCues(bottomPercent) {{ const video = document.querySelector('video'); if (!video || !video.textTracks) return; - const targetLine = Math.max(10, 92 - Math.round(bottomPercent * 0.4 + 2.5)); - for (let i = 0; i < video.textTracks.length; i++) { + const activeOffset = getActiveOffsetVh(); + const targetLine = Math.max(10, 95 - Math.round(activeOffset * 1.5)); + for (let i = 0; i < video.textTracks.length; i++) {{ const track = video.textTracks[i]; - if (track && track.cues) { - for (let j = 0; j < track.cues.length; j++) { + if (track && track.cues) {{ + for (let j = 0; j < track.cues.length; j++) {{ const cue = track.cues[j]; - if (cue && !cue._pluginAdjusted) { + if (cue && !cue._pluginAdjusted) {{ cue.snapToLines = false; cue.line = targetLine; cue._pluginAdjusted = true; - } - } + }} + }} } - } - } + }} + }} function applyCrop(cropData) { const video = document.querySelector('video'); @@ -376,6 +467,7 @@ namespace JellyfinPlugin.Api playbackInfoObserver.observe(document.body, { childList: true, subtree: true, characterData: true }); setInterval(() => { + fetchServerConfig(); const video = document.querySelector('video'); if (video && interceptedItemId && interceptedItemId !== currentItemId) { currentItemId = interceptedItemId; @@ -393,6 +485,7 @@ namespace JellyfinPlugin.Api }, 2000); })(); "; + js = js.Replace("{SUB_OFFSET}", defaultSubOffsetStr); return Content(js, "application/javascript"); } } diff --git a/JellyfinPlugin/Configuration/PluginConfiguration.cs b/JellyfinPlugin/Configuration/PluginConfiguration.cs index 8b5b65b..ae75991 100644 --- a/JellyfinPlugin/Configuration/PluginConfiguration.cs +++ b/JellyfinPlugin/Configuration/PluginConfiguration.cs @@ -15,5 +15,8 @@ namespace JellyfinPlugin.Configuration // Maksymalna liczba równoległych wątków skanowania czarnych pasów (1 - 16, domyślnie 4) public int MaxParallelScans { get; set; } = 4; + + // Przesunięcie pionowe napisów w vh w trybie klienckim CSS (0.0 - 25.0, domyślnie 5.0) + public double SubtitleOffsetVh { get; set; } = 5.0; } } diff --git a/JellyfinPlugin/Configuration/configPage.html b/JellyfinPlugin/Configuration/configPage.html index 1f48195..71ceca8 100644 --- a/JellyfinPlugin/Configuration/configPage.html +++ b/JellyfinPlugin/Configuration/configPage.html @@ -39,6 +39,12 @@
Number of files to analyze concurrently during library scheduled tasks. Default is 4. Higher values speed up library scans on multi-core CPUs.
+
+ + +
Vertical offset shift for subtitles in Jellyfin Web player (in viewport height % / vh). Default is 5.0. Increase to move subtitles higher up into the video, or decrease if they appear too high.
+
+