Files
jellyfin-plugin-media-bar-e…/configPageExample.html

2912 lines
225 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<title>Jellyfin Enhanced</title>
</head>
<body style="background: black;">
<div class="page type-interior pluginConfigurationPage" data-require="emby-input,emby-checkbox,emby-select" data-role="page" id="JellyfinEnhancedPage" style="color: #eee;">
<div data-role="content">
<div class="content-primary">
<div class="verticalSection">
<div class="sectionTitleContainer">
<h2 class="sectionTitle">Jellyfin Enhanced</h2>
<a is="emby-linkbutton" class="raised raised-mini emby-button" style="margin-left: 2em;" target="_blank" href="https://github.com/n00bcodr/Jellyfin-Enhanced">
<i class="md-icon button-icon button-icon-left secondaryText"></i>
<span>Help</span>
</a>
</div>
</div>
<hr>
<div id="fileTransformationWarning" style="display: none; margin-bottom: 1em; padding: 1em 1.2em; background-color: rgba(255, 165, 0, 0.15); border: 1px solid rgba(255, 165, 0, 0.5); border-radius: 8px; color: #ffcc00; font-size: 0.95em; line-height: 1.5; position: relative;">
<strong style="font-size: 1.05em;">⚠️ Highly Recommended</strong><br>
It is highly recommended to have the <a href="https://github.com/IAmParadox27/jellyfin-plugin-file-transformation" target="_blank" style="color: #ffa500; text-decoration: underline;">File Transformation</a> plugin installed. It helps avoid permission issues while modifying <code style="background: rgba(255,255,255,0.1); padding: 2px 5px; border-radius: 3px;">index.html</code> on any kind of installation.
<a href="https://github.com/n00bcodr/Jellyfin-Enhanced?tab=readme-ov-file#-installation" target="_blank" style="color: #ffa500; text-decoration: underline;">Learn more</a>
<div style="margin-top: 0.8em; display: flex; gap: 0.6em;">
<button id="dismissFtWarningSession" type="button" style="background: rgba(255,255,255,0.1); border: 1px solid rgba(255,165,0,0.4); color: #ffcc00; padding: 4px 12px; border-radius: 4px; cursor: pointer; font-size: 0.85em;">Dismiss</button>
<button id="dismissFtWarningForever" type="button" style="background: rgba(255,255,255,0.1); border: 1px solid rgba(255,165,0,0.4); color: #ffcc00; padding: 4px 12px; border-radius: 4px; cursor: pointer; font-size: 0.85em;">Don't show again</button>
</div>
</div>
<div style="margin-bottom: 1em;">
<button class="jellyfin-tab-button active" data-tab="enhanced" style="background: none; border: none; color: var(--primary-accent-color, #fff); cursor: pointer; transition: color 0.3s, border-bottom 0.3s; border-bottom: 2px solid var(--primary-accent-color, #fff);"><h3>Enhanced Settings</h3></button>
<button class="jellyfin-tab-button" data-tab="elsewhere" style="background: none; border: none; color: #ccc; cursor: pointer; transition: color 0.3s, border-bottom 0.3s; border-bottom: 2px solid transparent;"><h3>Elsewhere Settings</h3></button>
<button class="jellyfin-tab-button" data-tab="jellyseerr" style="background: none; border: none; color: #ccc; cursor: pointer; transition: color 0.3s, border-bottom 0.3s; border-bottom: 2px solid transparent;"><h3>Seerr Settings</h3></button>
<button class="jellyfin-tab-button" data-tab="arr-links" style="background: none; border: none; color: #ccc; cursor: pointer; transition: color 0.3s, border-bottom 0.3s; border-bottom: 2px solid transparent;"><h3>*arr Settings</h3></button>
<button class="jellyfin-tab-button" data-tab="extras" style="background: none; border: none; color: #ccc; cursor: pointer; transition: color 0.3s, border-bottom 0.3s; border-bottom: 2px solid transparent;"><h3>Other Settings</h3></button>
</div>
<form id="JellyfinEnhancedForm">
<div id="enhanced" class="jellyfin-tab-content active" style="display: block; font-family: inherit;">
<fieldset class="verticalSection-extrabottompadding">
<legend class="sectionTitle"><h3>Default User Settings</h3></legend>
<div class="configSection">
<details style="margin-bottom: 1em; background-color: rgba(255,255,255,0.05); padding: 1em; border-radius: 5px;">
<summary style="cursor: pointer; font-weight: bold;" data-icon="playback" data-text="Playback Settings">⏯️ Playback Settings</summary>
<div style="padding-top: 1em;">
<div class="checkboxContainer" style="margin-bottom: 0.5em;"><label><input id="autoPauseEnabled" is="emby-checkbox" type="checkbox"/><span>Auto-pause on tab switch</span></label></div>
<div class="checkboxContainer" style="margin-bottom: 0.5em;"><label><input id="autoResumeEnabled" is="emby-checkbox" type="checkbox"/><span>Auto-resume on tab switch</span></label></div>
<div class="checkboxContainer" style="margin-bottom: 0.5em;"><label><input id="autoPipEnabled" is="emby-checkbox" type="checkbox"/><span>Auto Picture-in-Picture on tab switch</span></label></div>
<div class="checkboxContainer" style="margin-bottom: 0.5em;"><label><input id="longPress2xEnabled" is="emby-checkbox" type="checkbox"/><span>Long press/hold for 2x speed <sup> β</sup></span></label></div>
<div class="checkboxContainer" style="margin-bottom: 0.5em;"><label style="line-height: 1.2em; align-self: flex-start;"><input id="pauseScreenEnabled" is="emby-checkbox" type="checkbox"/><span>Enable Custom Pause Screen<div>This feature is a modified version of the original script by <a class="sectionTitle" target="_blank" href="https://github.com/BobHasNoSoul/Jellyfin-PauseScreen">BobHasNoSoul</a></div></span></label></div>
</div>
</details>
<details style="margin-bottom: 1em; background-color: rgba(255,255,255,0.05); padding: 1em; border-radius: 5px;">
<summary style="cursor: pointer; font-weight: bold;" data-icon="skip" data-text="Auto-Skip Settings">↪️ Auto-Skip Settings</summary>
<div style="padding-top: 1em;">
<div class="checkboxContainer" style="margin-bottom: 0.5em;"><label><input id="autoSkipIntro" is="emby-checkbox" type="checkbox"/><span>Auto-skip Intro</span></label></div>
<div class="checkboxContainer" style="margin-bottom: 0.5em;"><label><input id="autoSkipOutro" is="emby-checkbox" type="checkbox"/><span>Auto-skip Outro</span></label></div>
</div>
</details>
<details style="margin-bottom: 1em; background-color: rgba(255,255,255,0.05); padding: 1em; border-radius: 5px;">
<summary style="cursor: pointer; font-weight: bold;" data-icon="subtitles" data-text="Subtitle Settings">📝 Subtitle Settings</summary>
<div style="padding-top: 1em;">
<div class="checkboxContainer" style="margin-bottom: 1em;"><label><input id="disableCustomSubtitleStyles" is="emby-checkbox" type="checkbox"/><span>Disable Custom Subtitle Styles by default</span></label></div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="DefaultSubtitleStyle">Default Subtitle Style</label>
<select is="emby-select" id="DefaultSubtitleStyle" name="DefaultSubtitleStyle" class="emby-select">
<option value="0">Clean White</option>
<option value="1">Classic Black Box</option>
<option value="2">Netflix Style</option>
<option value="3">Cinema Yellow</option>
<option value="4">Soft Gray</option>
<option value="5">High Contrast</option>
</select>
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="DefaultSubtitleSize">Default Subtitle Size</label>
<select is="emby-select" id="DefaultSubtitleSize" name="DefaultSubtitleSize" class="emby-select">
<option value="0">Tiny</option>
<option value="1">Small</option>
<option value="2">Normal</option>
<option value="3">Large</option>
<option value="4">Extra Large</option>
<option value="5">Gigantic</option>
</select>
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="DefaultSubtitleFont">Default Subtitle Font</label>
<select is="emby-select" id="DefaultSubtitleFont" name="DefaultSubtitleFont" class="emby-select">
<option value="0">Default</option>
<option value="1">Noto Sans</option>
<option value="2">Sans Serif</option>
<option value="3">Typewriter</option>
<option value="4">Roboto</option>
</select>
</div>
</div>
</details>
<details style="margin-bottom: 1em; background-color: rgba(255,255,255,0.05); padding: 1em; border-radius: 5px;">
<summary style="cursor: pointer; font-weight: bold;" data-icon="random" data-text="Random Button Settings">🎲 Random Button Settings</summary>
<div style="padding-top: 1em;">
<div class="checkboxContainer" style="margin-bottom: 0.5em;"><label><input id="randomButtonEnabled" is="emby-checkbox" type="checkbox"/><span>Enable Random Button</span></label></div>
<div class="checkboxContainer" style="margin-bottom: 0.5em;"><label><input id="randomUnwatchedOnly" is="emby-checkbox" type="checkbox"/><span>Show unwatched only in random selection</span></label></div>
<div class="checkboxContainer" style="margin-bottom: 0.5em;"><label><input id="randomIncludeMovies" is="emby-checkbox" type="checkbox"/><span>Include movies in random selection</span></label></div>
<div class="checkboxContainer" style="margin-bottom: 0.5em;"><label><input id="randomIncludeShows" is="emby-checkbox" type="checkbox"/><span>Include shows in random selection</span></label></div>
</div>
</details>
<details style="margin-bottom: 1em; background-color: rgba(255,255,255,0.05); padding: 1em; border-radius: 5px;">
<summary style="cursor: pointer; font-weight: bold;" data-icon="ui" data-text="UI Settings">🖥️ UI Settings</summary>
<div style="padding-top: 1em;">
<div class="checkboxContainer" style="margin-bottom: 0.5em;"><label><input id="showWatchProgress" is="emby-checkbox" type="checkbox"/><span>Show watch progress</span></label></div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="watchProgressDefaultMode">Watch Progress Default</label>
<select is="emby-select" id="watchProgressDefaultMode" name="watchProgressDefaultMode" class="emby-select">
<option value="percentage">Percentage</option>
<option value="time">Time</option>
</select>
<div class="fieldDescription">Choose default display for watch progress.</div>
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="watchProgressTimeFormat">Watch Progress Time Format</label>
<select is="emby-select" id="watchProgressTimeFormat" name="watchProgressTimeFormat" class="emby-select">
<option value="hours">h:m</option>
<option value="full">y:mo:d:h:m</option>
</select>
<div class="fieldDescription">Select how time-based progress is shown.</div>
</div>
<div class="checkboxContainer" style="margin-bottom: 0.5em;"><label><input id="showFileSizes" is="emby-checkbox" type="checkbox"/><span>Show file sizes</span></label></div>
<div class="checkboxContainer" style="margin-bottom: 0.5em;"><label><input id="showAudioLanguages" is="emby-checkbox" type="checkbox"/><span>Show available audio languages on item detail page</span></label></div>
<div class="checkboxContainer" style="margin-bottom: 0.5em;"><label><input id="removeContinueWatchingEnabled" is="emby-checkbox" type="checkbox"/><span>Enable "Remove from Continue Watching"</span></label></div>
<div class="checkboxContainer" style="margin-bottom: 0.5em;"><label style="line-height: 1.2em; align-self: flex-start;"><input id="qualityTagsEnabled" is="emby-checkbox" type="checkbox"/><span>Enable Quality Tags<div>This feature is a slightly modified version of the original script by <a class="sectionTitle" target="_blank" href="https://github.com/BobHasNoSoul/Jellyfin-Qualitytags/">BobHasNoSoul</a></div></span></label></div>
<div class="checkboxContainer" style="margin-bottom: 0.5em;"><label><input id="genreTagsEnabled" is="emby-checkbox" type="checkbox"/><span>Enable Genre Tags</span></label></div>
<div class="checkboxContainer" style="margin-bottom: 0.5em;"><label><input id="languageTagsEnabled" is="emby-checkbox" type="checkbox"/><span>Enable Language Tags</span></label></div>
<div class="checkboxContainer" style="margin-bottom: 0.5em;"><label><input id="ratingTagsEnabled" is="emby-checkbox" type="checkbox"/><span>Enable Rating Tags</span></label></div>
<div class="checkboxContainer" style="margin-bottom: 0.5em;"><label><input id="peopleTagsEnabled" is="emby-checkbox" type="checkbox"/><span>Enable People Tags</span></label></div>
<fieldset style="margin-top: 1em;padding: 2em;border-radius: 20px;border-color: #ffffff25;">
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="qualityTagsPosition">Quality Tags Position</label>
<select is="emby-select" id="qualityTagsPosition" name="qualityTagsPosition" class="emby-select">
<option value="top-left">Top Left</option>
<option value="top-right">Top Right</option>
<option value="bottom-left">Bottom Left</option>
<option value="bottom-right">Bottom Right</option>
</select>
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="genreTagsPosition">Genre Tags Position</label>
<select is="emby-select" id="genreTagsPosition" name="genreTagsPosition" class="emby-select">
<option value="top-left">Top Left</option>
<option value="top-right">Top Right</option>
<option value="bottom-left">Bottom Left</option>
<option value="bottom-right">Bottom Right</option>
</select>
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="languageTagsPosition">Language Tags Position</label>
<select is="emby-select" id="languageTagsPosition" name="languageTagsPosition" class="emby-select">
<option value="top-left">Top Left</option>
<option value="top-right">Top Right</option>
<option value="bottom-left">Bottom Left</option>
<option value="bottom-right">Bottom Right</option>
</select>
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="ratingTagsPosition">Rating Tags Position</label>
<select is="emby-select" id="ratingTagsPosition" name="ratingTagsPosition" class="emby-select">
<option value="top-left">Top Left</option>
<option value="top-right">Top Right</option>
<option value="bottom-left">Bottom Left</option>
<option value="bottom-right">Bottom Right</option>
</select>
</div>
<div class="inputContainer" style="margin-top: 1em;">
<label class="inputLabel inputLabelUnfocused" for="tagsCacheTtlDays">Tags Cache Duration (days)</label>
<input id="tagsCacheTtlDays" is="emby-input" type="number" min="1" max="365" value="30"/>
<div class="fieldDescription">How long to cache the above tags data (1-365 days, default: 30)</div>
</div>
<div class="checkboxContainer" style="margin-top: 1em;">
<label style="line-height: 1.2em; align-self: flex-start;"><input id="showRatingInPlayer" is="emby-checkbox" type="checkbox"/>
<span>
Show Rating in Video Player
<div>Display TMDB and Rotten Tomatoes ratings before "Ends at" time in video player</div>
</span>
</label>
</div>
<div style="margin-top: 1em;">
<button id="clearTagsCacheBtn" class="raised button-submit emby-button" is="emby-button" type="button">
<span>Clear All Client Caches</span>
</button>
<div class="fieldDescription">Forces all clients to clear Language, Quality, Genre and Rating tag caches on next load<br> 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.</div>
</div>
</fieldset>
<div class="checkboxContainer" style="margin-top: 2em;margin-bottom: 2em;"><label style="line-height: 1.2em; align-self: flex-start;"><input id="disableTagsOnSearchPage" is="emby-checkbox" type="checkbox"/><span>Disable Tags on Search Page<div style="font-size: 0.8em;">Prevents quality/language/genre/rating tags from showing on search results. Recommended for compatibility with <b>Gelato</b> plugin.</div></span></label></div>
</div>
</details>
<details style="margin-bottom: 1em; background-color: rgba(255,255,255,0.05); padding: 1em; border-radius: 5px;">
<summary style="cursor: pointer; font-weight: bold;" data-icon="language" data-text="Language Settings">🌐 Language Settings</summary>
<div style="padding-top: 1em;">
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="DefaultLanguage">Default UI Language</label>
<select is="emby-select" id="DefaultLanguage" name="DefaultLanguage" class="emby-select">
<option value="">System Default</option>
</select>
<div class="fieldDescription">Sets the default language for all users. Users can still override this in their own settings.</div>
</div>
</div>
</details>
<div style="padding-top: 1em;">
<div class="checkboxContainer" style="margin-bottom: 0.5em;"><label><input id="disableAllShortcuts" is="emby-checkbox" type="checkbox"/><span data-icon="keyboard" data-text="Disable Keyboard Shortcuts">⌨️ Disable Keyboard Shortcuts</span></label></div>
<div class="fieldDescription">Disables all keyboard shortcuts, and hides the 'Shortcuts' tab in enhanced panel</div>
</div>
</div>
<div class="verticalSection" style="margin-top: 2em;">
<button id="resetAllUserSettingsBtn" class="raised button-submit block emby-button" is="emby-button" type="button">
<span>Apply Above Settings to All Users</span>
</button>
<div style="background-color: rgba(0, 164, 220, 0.1); border-left: 4px solid #00a4dc; border-radius: 4px; padding: 1em 1.5em; margin-top: 1em;">
<div style="display: flex; align-items: flex-start; gap: 1em;">
<i class="material-icons" style="color: #00a4dc; font-size: 24px; margin-top: 2px;">info</i>
<div>
<strong>This will save the current configuration and apply all the above settings to every user on this server.</strong><br>
<strong>Note:</strong> Users can individually customize their own settings by opening the Enhanced Panel:<br>
• Press <code>?</code> key (default shortcut)<br>
• Long Press on user profile picture in the top-right corner<br>
• Through "Jellyfin Enhanced" link from the side bar<br>
• Access via playback controls menu during video playback (only on mobile devices)
</div>
</div>
</div>
</div>
</fieldset>
<fieldset class="verticalSection-extrabottompadding">
<legend class="sectionTitle"><h3>Icons</h3></legend>
<div style="margin-bottom: 2em; padding-bottom: 1.5em; border-bottom: 1px solid rgba(255,255,255,0.1);">
<h4 class="sectionTitle" style="margin-top: 0;">Icon Settings</h4>
<div class="checkboxContainer" style="margin-bottom: 0.5em !important;">
<label>
<input id="useIcons" is="emby-checkbox" type="checkbox"/>
<span>Use Icons</span>
</label>
</div>
<div class="fieldDescription" style="margin-bottom: 1.5em;">
Display icons throughout the UI (toasts, settings panel headers, etc.)
</div>
<div class="selectContainer" style="margin-bottom: 1.5em;">
<label class="selectLabel" for="iconStyle">Icon Style</label>
<select id="iconStyle" is="emby-select" class="emby-select-withcolor">
<option value="emoji">Emoji</option>
<option value="lucide">Lucide Icons</option>
<option value="mui">Material UI Icons</option>
</select>
<div class="fieldDescription">Choose between emoji characters, Lucide icons, or Material UI icons.</div>
</div>
</fieldset>
<fieldset class="verticalSection-extrabottompadding">
<legend class="sectionTitle"><h3>Shortcut Overrides</h3></legend>
<div style="margin-bottom: 2em; padding-bottom: 1.5em; border-bottom: 1px solid rgba(255,255,255,0.1);">
<h4 class="sectionTitle" style="margin-top: 0;">Add Override</h4>
<div style="display: flex; gap: 1em; align-items: flex-end;">
<div style="flex: 1;">
<select is="emby-select" id="add-shortcut-select" label="Shortcut Action"></select>
</div>
<div style="flex: 1;">
<input is="emby-input" id="add-shortcut-key" type="text" label="New Key"/>
</div>
<button is="emby-button" type="button" id="add-shortcut-btn" class="raised button-submit" style="top: 10px;">
<span>Add</span>
</button>
</div>
<div id="shortcut-error-comment" style="display: none; color: #ffdddd; font-size: 0.9em; margin-top: 0.75em; background-color: rgba(220, 53, 69, 0.2); padding: 0.5em; border-radius: 4px; border-left: 3px solid #dc3545;"></div>
<p class="fieldDescription" style="margin-top: 1em;">
<b>Modifier Keys:</b> Use `Shift+`, `Ctrl+`, or `Alt+`. Examples: `Shift+A`, `Ctrl+S`.
</p>
</div>
<h4 class="sectionTitle" style="margin-top: 0;">Configured Overrides</h4>
<div id="shortcut-list-container" style="display: flex; flex-direction: column; margin-top: 1em;">
</div>
</fieldset>
<fieldset class="verticalSection-extrabottompadding">
<legend class="sectionTitle"><h3>Bookmarks</h3></legend>
<div class="configSection">
<div class="checkboxContainer" style="margin-bottom: 0.5em !important;">
<label>
<input id="bookmarksEnabled" is="emby-checkbox" type="checkbox"/>
<span>Enable Bookmarks Feature</span>
</label>
</div>
<div class="fieldDescription" style="margin-bottom: 1.5em;">
Save custom bookmarks/timestamps while watching videos to quickly jump back to your favorite scenes or moments.
</div>
<div class="checkboxContainer" style="margin-bottom: 0.5em !important;">
<label>
<input id="bookmarksUsePluginPages" is="emby-checkbox" type="checkbox"/>
<span>Use Plugin Pages for Bookmarks Library</span>
</label>
</div>
<div class="fieldDescription" style="margin-bottom: 1.5em;">Adds a "Bookmarks" link to the sidebar via <a class="sectionTitle" target="_blank" href="https://github.com/IAmParadox27/jellyfin-plugin-pages">Plugin Pages</a>.<br><strong>Note:</strong> Jellyfin must be restarted after enabling this option for the first time for changes to take effect.</div>
<div style="background-color: rgba(255, 255, 255, 0.05); border-left: 4px solid #00a4dc; border-radius: 4px; padding: 1em 1.5em; margin: 1.5em 0;">
<div style="display: flex; align-items: flex-start; gap: 1em;">
<i class="material-icons" style="color: #00a4dc; font-size: 24px; margin-top: 2px;">info</i>
<div>
<h4 style="margin: 0 0 0.5em 0;">How to Use Bookmarks:</h4>
<ul class="fieldDescription" style="margin: 0.5em 0; padding-left: 1.5em;">
<li style="margin-bottom: 0.5em;"><strong>During Playback:</strong> Press <kbd style="background: rgba(255,255,255,0.1); padding: 2px 6px; border-radius: 3px; font-family: monospace;">B</kbd> or click the bookmark icon <i class="material-icons" style="font-size: 16px; vertical-align: middle;">bookmark_add</i> in the video player controls to save a bookmark at the current time.</li>
<li style="margin-bottom: 0.5em;"><strong>Visual Markers:</strong> Bookmarks appear as location pins <i class="material-icons" style="font-size: 16px; vertical-align: middle; color: #00d4ff;">location_pin</i> on the video progress bar. Click a marker to jump to that timestamp.</li>
<li style="margin-bottom: 0.5em;"><strong>Multi-Version Support:</strong> Bookmarks track by TMDB/TVDB IDs, so they work across different file versions of the same content.</li>
</ul>
<h4 style="margin: 1em 0 0.5em 0;">To View & Manage All Your Bookmarks:</h4>
<p class="fieldDescription" style="margin: 0;">You can either enable <a class="sectionTitle" target="_blank" href="https://github.com/IAmParadox27/jellyfin-plugin-pages">Plugin Pages</a> above to add a sidebar link, or create a custom view using the <a class="sectionTitle" target="_blank" href="https://github.com/IAmParadox27/jellyfin-plugin-custom-tabs">Custom Tabs</a> plugin.</p>
<ol class="fieldDescription" style="margin: 1em 0; padding-left: 1.5em;">
<li style="margin-bottom: 0.5em;">Install the <strong>Custom Tabs</strong> plugin and all its prerequisites.</li>
<li style="margin-bottom: 0.5em;">Navigate to Dashboard > Plugins > Custom Tabs and add a new tab.</li>
<li style="margin-bottom: 0.5em;">Add "Display Text" according to your preference (e.g., "My Bookmarks").</li>
<li style="margin-bottom: 0.5em;">Copy the code below and paste it into the 'HTML Content' field.</li>
</ol>
<div style="position: relative;">
<button type="button" class="je-copy-html-btn raised raised-mini emby-button" data-copy-text="&lt;div class=&quot;sections bookmarks&quot;&gt;&lt;/div&gt;" style="position: absolute; top: 8px; right: 8px; background-color: rgba(255,255,255,0.1); border: none; padding: 4px 8px; min-width: 80px; transition: color 0.3s;">
<i class="material-icons" style="font-size: 16px; margin-right: 4px; vertical-align: middle;">content_copy</i>
<span class="copy-btn-text" style="vertical-align: middle;">Copy</span>
</button>
<pre style="background-color: rgba(0,0,0,0.3); padding: 1em; border-radius: 4px; white-space: pre-wrap; word-break: break-all; margin: 0; font-family: 'Courier New', Courier, monospace;"><code>&lt;div class="sections bookmarks"&gt;&lt;/div&gt;
</code></pre>
</div>
<div class="fieldDescription" style="margin-top: 1em;">
Already have Custom Tabs installed?
<a class="sectionTitle" href="/web/#/configurationpage?name=Custom%20Tabs" style="text-decoration: underline;">Click here to configure it.</a>
</div>
</div>
</div>
</div>
</div>
</fieldset>
<fieldset class="verticalSection-extrabottompadding">
<legend class="sectionTitle"><h3>Timeout Settings</h3></legend>
<div class="configSection">
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="HelpPanelAutocloseDelay">Help Panel Autoclose Delay (in milliseconds)</label>
<input id="HelpPanelAutocloseDelay" is="emby-input" name="HelpPanelAutocloseDelay" type="number"/>
<div class="fieldDescription">How long the help panel stays open before auto-closing.</div>
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="ToastDuration">Toast Duration (in milliseconds)</label>
<input id="ToastDuration" is="emby-input" name="ToastDuration" type="number"/>
<div class="fieldDescription">How long toast notifications are displayed.</div>
</div>
</div>
</fieldset>
<fieldset class="verticalSection-extrabottompadding">
<legend class="sectionTitle"><h3>Hidden Content</h3></legend>
<div class="configSection">
<div class="checkboxContainer" style="margin-bottom: 0.5em !important;">
<label>
<input id="hiddenContentEnabled" is="emby-checkbox" type="checkbox"/>
<span>Enable Hidden Content</span>
</label>
</div>
<div class="fieldDescription" style="margin-bottom: 2em;">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.</div>
<div class="checkboxContainer" style="margin-bottom: 0.5em !important;">
<label>
<input id="hiddenContentUsePluginPages" is="emby-checkbox" type="checkbox"/>
<span>Use Plugin Pages for Hidden Content</span>
</label>
</div>
<div class="fieldDescription" style="margin-bottom: 2em;">Replaces the default Hidden Content page with a <a class="sectionTitle" target="_blank" href="https://github.com/IAmParadox27/jellyfin-plugin-pages">Plugin Pages</a> implementation.<br><strong>Note:</strong> Jellyfin must be restarted after enabling this option for the first time for changes to take effect.</div>
<div class="checkboxContainer" style="margin-bottom: 0.5em !important;">
<label>
<input id="hiddenContentUseCustomTabs" is="emby-checkbox" type="checkbox"/>
<span>Use Custom Tabs for Hidden Content (instead of sidebar)</span>
</label>
</div>
<div style="background-color: rgba(255, 255, 255, 0.05); border-left: 4px solid #00a4dc; border-radius: 4px; padding: 1em 1.5em; margin: 1.5em 0;">
<div style="display: flex; align-items: flex-start; gap: 1em;">
<i class="material-icons" style="color: #00a4dc; font-size: 24px; margin-top: 2px;">info</i>
<div>
<h4 style="margin: 0 0 0.5em 0;">Embed Hidden Content in Custom Tabs:</h4>
<p class="fieldDescription" style="margin: 0 0 0.5em 0;">Display the Hidden Content page in a Custom Tabs tab instead of the sidebar link.</p>
<p class="fieldDescription" style="margin: 0.5em 0;">You need the <a class="sectionTitle" target="_blank" href="https://github.com/IAmParadox27/jellyfin-plugin-custom-tabs">Custom Tabs</a> plugin.</p>
<ol class="fieldDescription" style="margin: 1em 0; padding-left: 1.5em;">
<li style="margin-bottom: 0.5em;">Install the <strong>Custom Tabs</strong> plugin and all its prerequisites.</li>
<li style="margin-bottom: 0.5em;">Navigate to Dashboard > Plugins > Custom Tabs and add a new tab.</li>
<li style="margin-bottom: 0.5em;">Add "Display Text" according to your preference (e.g., "Hidden Content").</li>
<li style="margin-bottom: 0.5em;">Copy the code below and paste it into the 'HTML Content' field.</li>
</ol>
<div style="position: relative;">
<button type="button" class="je-copy-html-btn raised raised-mini emby-button" data-copy-text="&lt;div class=&quot;jellyfinenhanced hidden-content&quot;&gt;&lt;/div&gt;" style="position: absolute; top: 8px; right: 8px; background-color: rgba(255,255,255,0.1); border: none; padding: 4px 8px; min-width: 80px; transition: color 0.3s;">
<i class="material-icons" style="font-size: 16px; margin-right: 4px; vertical-align: middle;">content_copy</i>
<span class="copy-btn-text" style="vertical-align: middle;">Copy</span>
</button>
<pre style="background-color: rgba(0,0,0,0.3); padding: 1em; border-radius: 4px; white-space: pre-wrap; word-break: break-all; margin: 0; font-family: 'Courier New', Courier, monospace;"><code>&lt;div class="jellyfinenhanced hidden-content"&gt;&lt;/div&gt;
</code></pre>
</div>
<div class="fieldDescription" style="margin-top: 1em;">
Already have Custom Tabs installed?
<a class="sectionTitle" href="/web/#/configurationpage?name=Custom%20Tabs" style="text-decoration: underline;">Click here to configure it.</a>
</div>
</div>
</div>
</div>
</div>
</fieldset>
</div>
<div id="elsewhere" class="jellyfin-tab-content" style="display: none; font-family: inherit;">
<fieldset class="verticalSection-extrabottompadding">
<legend class="sectionTitle"><h3>Configuration</h3></legend>
<div class="checkboxContainer" style="margin-bottom: 2em;">
<label>
<input id="elsewhereEnabled" is="emby-checkbox" type="checkbox"/>
<span>Enable Elsewhere<div style="font-size: .9em;">Show Streaming Provider Lookup on Item Details Page</div></span>
</label>
</div>
<div class="configSection">
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="TMDB_API_KEY">TMDB API Key</label>
<div style="display: flex; align-items: center; gap: 1em; margin-left: -1em;">
<input id="TMDB_API_KEY" is="emby-input" name="TMDB_API_KEY" type="text" style="flex-grow: 1;"/>
<span id="tmdbStatusIndicator" class="material-icons" style="transition: color 0.3s ease;"></span>
<button is="emby-button" type="button" class="testTmdbBtn raised button-submit">
<span>Test</span>
</button>
</div>
<div class="fieldDescription">Your API key from The Movie DB (TMDB). <a class="sectionTitle" target="_blank" href="https://www.themoviedb.org/settings/api">How to?</a></div>
<div class="fieldDescription"><br>Shared with Seerr Settings</div>
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="DEFAULT_REGION">Default Region</label>
<input id="DEFAULT_REGION" is="emby-input" name="DEFAULT_REGION" type="text" placeholder="e.g., US"/>
<div class="fieldDescription">The default two-letter country code for streaming provider lookup. Empty defaults to US. <a class="sectionTitle" target="_blank" href="https://cdn.jsdelivr.net/gh/n00bcodr/Jellyfin-Elsewhere/resources/regions.txt">Full List</a></div>
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="DEFAULT_PROVIDERS">Default Providers</label>
<textarea style="display:block; height: 10vh !important;" class="emby-textarea emby-input" id="DEFAULT_PROVIDERS" name="DEFAULT_PROVIDERS" placeholder="e.g., Netflix,Hulu"></textarea>
<div class="fieldDescription">A list of default streaming providers to show. Leave blank to show all. <a class="sectionTitle" target="_blank" href="https://cdn.jsdelivr.net/gh/n00bcodr/Jellyfin-Elsewhere/resources/providers.txt">Full List</a></div>
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="IGNORE_PROVIDERS">Ignore Providers</label>
<textarea style="display:block; height: 10vh !important;" class="emby-textarea emby-input" id="IGNORE_PROVIDERS" name="IGNORE_PROVIDERS" placeholder="e.g., .*with Ads, Hulu"></textarea>
<div class="fieldDescription">A list of providers to ignore from the default region (supports regex).<a class="sectionTitle" target="_blank" href="https://cdn.jsdelivr.net/gh/n00bcodr/Jellyfin-Elsewhere/resources/providers.txt">Full List</a></div>
</div>
<h3>Elsewhere Custom Branding</h3>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="ElsewhereCustomBrandingText">Custom Branding Message</label>
<input id="ElsewhereCustomBrandingText" is="emby-input" name="ElsewhereCustomBrandingText" type="text" placeholder="e.g., Only available on My Server"/>
<div class="fieldDescription">Custom message to display when content is not available on any streaming providers. Leave blank to disable custom branding.</div>
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="ElsewhereCustomBrandingImageUrl">Custom Branding Icon URL</label>
<input id="ElsewhereCustomBrandingImageUrl" is="emby-input" name="ElsewhereCustomBrandingImageUrl" type="text" placeholder="e.g., /web/assets/img/icon.png"/>
<div class="fieldDescription">Optional URL for an icon to display next to the custom message. Leave blank for no icon.</div>
</div>
<h3>Extras</h3>
<div class="checkboxContainer">
<label style="line-height: 1.2em; align-self: flex-start;"><input id="showReviews" is="emby-checkbox" type="checkbox"/>
<span>Show Reviews from TMDB in Item Details Page<div style="font-size: .9em;">Requires TMDB API Key to be set</div></span>
</label>
</div>
<div class="checkboxContainer">
<label style="line-height: 1.2em; align-self: flex-start;"><input id="reviewsExpandedByDefault" is="emby-checkbox" type="checkbox"/>
<span>Expand reviews by default<div style="font-size: .9em;">When enabled, the reviews section opens expanded by default for all users (unless they override in their session).</div></span>
</label>
</div>
</div>
</fieldset>
</div>
<div id="jellyseerr" class="jellyfin-tab-content" style="display: none; font-family: inherit;">
<fieldset class="verticalSection-extrabottompadding">
<legend class="sectionTitle"><h3>Setup</h3></legend>
<div class="configSection">
<div class="inputContainer">
<label class="sectionTitle" style="margin-bottom: 5px;" for="jellyseerrUrls">Seerr URL(s)</label>
<textarea style="display:block; height: 8vh !important;" class="emby-textarea emby-input" id="jellyseerrUrls" name="jellyseerrUrls" placeholder="http://192.168.1.10:5055&#10;https://jellyseerr.example.com"></textarea>
<div class="fieldDescription">Enter your Seerr instance URLs, one per line. The script will use the first one that connects successfully.</div>
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="JellyseerrApiKey">Seerr API Key</label>
<div style="display: flex; align-items: center; gap: 1em; margin-left: -1em;">
<input id="JellyseerrApiKey" is="emby-input" name="JellyseerrApiKey" type="text" style="flex-grow: 1;"/>
<span id="jellyseerrStatusIndicator" class="material-icons" style="transition: color 0.3s ease;"></span>
<button is="emby-button" type="button" id="testJellyseerrBtn" class="raised button-submit">
<span>Test</span>
</button>
</div>
<div class="fieldDescription">API key from Seerr. (Settings > General Settings > API Key)</div>
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="jellyseerr_TMDB_API_KEY">TMDB API Key</label>
<div style="display: flex; align-items: center; gap: 1em; margin-left: -1em;">
<input id="jellyseerr_TMDB_API_KEY" is="emby-input" name="TMDB_API_KEY" type="text" style="flex-grow: 1;"/>
<span id="jellyseerrTmdbStatusIndicator" class="material-icons" style="transition: color 0.3s ease;"></span>
<button is="emby-button" type="button" class="testTmdbBtn raised button-submit">
<span>Test</span>
</button>
</div>
<div class="fieldDescription">Your API key from The Movie DB (TMDB). <a class="sectionTitle" target="_blank" href="https://www.themoviedb.org/settings/api">How to?</a><br>TMDB API key for automatic movie collection requests and Seerr Collection Results. Shared with Elsewhere Settings</div>
</div>
<details style="margin: 2em 0; background-color: rgba(255,255,255,0.05); padding: 1em; border-radius: 5px;">
<summary style="cursor: pointer; font-weight: bold;">Advanced URL Mappings (Optional)</summary>
<div style="padding-top: 1em;">
<div class="inputContainer">
<label class="sectionTitle" style="margin-bottom: 5px;" for="jellyseerrUrlMappings">Seerr URL Mappings</label>
<textarea style="display:block; height: 12vh !important;" class="emby-textarea emby-input" id="jellyseerrUrlMappings" name="jellyseerrUrlMappings" placeholder="https://jellyfin.mydomain.com|https://jellyseerr.mydomain.com&#10;http://192.168.1.10:8096|http://192.168.1.10:5055&#10;https://example.com/jellyfin|https://example.com/jellyseerr"></textarea>
<div class="fieldDescription">Map your Jellyfin URLs to specific Seerr URLs for displaying links to users. Users will get the appropriate Seerr link when using "Link result titles to Seerr instead of TMDB", based on what link they use to access Jellyfin. <br>Format: <code>JellyfinURL|SeerrURL</code>, one mapping per line.<br>If no mapping matches or this is empty, the first URL from above will be used.</div>
</div>
<div
style="background-color: rgba(255, 255, 255, 0.05); border-left: 4px solid #00a4dc; border-radius: 4px; padding: 1em 1.5em; margin: 1em 0; display: flex; align-items: flex-start; gap: 1em;">
<i class="material-icons" style="color: #00a4dc; font-size: 18px; margin-top: 2px;">info</i>
<div>
<strong>URL Mapping Examples:</strong><br>
• Remote access: <code>https://jellyfin.mydomain.com|https://jellyseerr.mydomain.com</code><br>
• Local access: <code>http://192.168.1.10:8096|http://192.168.1.10:5055</code><br>
• With base URL: <code>https://example.com/jellyfin|https://example.com/jellyseerr</code><br><br>
</div>
</div>
<div
style="background-color: rgba(255, 193, 7, 0.1); border-left: 4px solid #ffc107; border-radius: 4px; padding: 1em 1.5em; margin: 1em 0; display: flex; align-items: flex-start; gap: 1em;">
<i class="material-icons" style="color: #ffc107; font-size: 18px; margin-top: 2px;">phone_android</i>
<div>
Make sure proper mappings are added to appropriate URLs in order for mobile clients to open Seerr links correctly.
</div>
</div>
</div>
</details>
<div
style="background-color: rgba(255, 255, 255, 0.05); border-left: 4px solid #00a4dc; border-radius: 4px; padding: 1em 1.5em; margin: 1.5em 0; display: flex; align-items: center; gap: 1em;">
<i class="material-icons" style="color: #00a4dc; font-size: 18px;">info</i>
<div>
"<b>Enable Jellyfin Sign-In</b>" must be enabled in Seerr User Settings (/settings/users)<br><br>
All users must be imported to Seerr as Jellyfin users for them to be able to request content.
</div>
</div>
<div
style="background-color: rgba(100, 50, 150, 0.08); border-left: 4px solid #9b59b6; border-radius: 4px; padding: 1em 1.5em; margin: 1.5em 0; display: flex; align-items: center; gap: 1em;">
<i class="material-icons" style="color: #9b59b6; font-size: 18px;">favorite</i>
<div>
<b>This plugin is not affiliated with Jellyseerr.</b><br><br>
Seerr is an independent project, and this plugin simply integrates with it to enhance the Jellyfin experience, huge thanks to their API<br><br>
<b>Please report any issues with this plugin to the plugin repository, not to the Seerr team.</b>
</div>
</div>
</div>
</fieldset>
<fieldset class="verticalSection-extrabottompadding">
<legend class="sectionTitle"><h3>Search</h3></legend>
<div class="configSection">
<details style="margin-bottom: 1em; background-color: rgba(255,255,255,0.05); padding: 1em; border-radius: 5px;">
<summary style="cursor: pointer; font-weight: bold;">Search Integration</summary>
<div style="padding-top: 1em;">
<div class="checkboxContainer" style="margin-bottom: 0.5em !important;">
<label>
<input id="jellyseerrEnabled" is="emby-checkbox" type="checkbox"/>
<span>Show Seerr Results in Search</span>
</label>
</div>
<div class="fieldDescription" style="margin-bottom: 2em;">Enhance Jellyfin search by showing results from Seerr.<br>This allows users to discover and request missing movies and shows directly from Jellyfin.</div>
<div class="checkboxContainer" style="margin-bottom: 0.5em !important;">
<label>
<input id="showCollectionsInSearch" is="emby-checkbox" type="checkbox" data-plugin-config/>
<span>Show Collections in Seerr Search Results</span>
</label>
</div>
<div class="fieldDescription" style="margin-bottom: 2em;">Display TMDB collections (e.g., Harry Potter, Marvel Cinematic Universe) in Seerr search results with an option to request the entire collection at once.<br><br><b>Requires TMDB API Key to be configured.</b></div>
</div>
</details>
<details style="margin-bottom: 1em; background-color: rgba(255,255,255,0.05); padding: 1em; border-radius: 5px;">
<summary style="cursor: pointer; font-weight: bold;">Request Options</summary>
<div style="padding-top: 1em;">
<div class="checkboxContainer" style="margin-bottom: 0.5em !important;">
<label>
<input id="jellyseerrEnable4kRequests" is="emby-checkbox" type="checkbox"/>
<span>Enable 4K Requests</span>
</label>
</div>
<div class="fieldDescription" style="margin-bottom: 2em;">When enabled, movie request buttons include a dropdown to request a 4K version.</div>
<div class="checkboxContainer" style="margin-bottom: 0.5em !important;">
<label>
<input id="jellyseerrShowAdvanced" is="emby-checkbox" type="checkbox"/>
<span>Show advanced request options</span>
</label>
</div>
<div class="fieldDescription" style="margin-bottom: 2em;">Enable this if you want users to be able to choose between Servers, Quality and Paths while requesting. <br> Note: This will disregard any Override Rules set in Seerr.</div>
</div>
</details>
</div>
</fieldset>
<fieldset class="verticalSection-extrabottompadding">
<legend class="sectionTitle"><h3>Other Features</h3></legend>
<div class="configSection">
<div class="checkboxContainer" style="margin-bottom: 0.5em !important;">
<label>
<input id="jellyseerrUseMoreInfoModal" is="emby-checkbox" type="checkbox"/>
<span>Open result titles and posters in "More Info" modal</span>
</label>
</div>
<div class="fieldDescription" style="margin-bottom: 2em;">When enabled, clicking a Seerr result title or poster opens an in-app More Info modal.<br>When disabled, clicking will open the item in Seerr.</div>
<div class="checkboxContainer" style="margin-bottom: 0.5em !important;">
<label>
<input id="jellyseerrShowReportButton" is="emby-checkbox" type="checkbox"/>
<span>Show "Report Issue" button on item detail pages</span>
</label>
</div>
<div class="fieldDescription" style="margin-bottom: 2em;">When enabled, a small report icon will be added to item action icons allowing users to report playback/content issues to Seerr.</div>
<div class="checkboxContainer" style="margin-bottom: 0.5em !important;">
<label>
<input id="jellyseerrExcludeBlocklistedItems" is="emby-checkbox" type="checkbox"/>
<span>Exclude blocklisted movies/series from suggestions</span>
</label>
</div>
<div class="fieldDescription" style="margin-bottom: 2em;">When enabled, items marked as blocklisted in Seerr will not appear in similar/recommended suggestions.</div>
<div class="checkboxContainer" style="margin-bottom: 0.5em !important;">
<label>
<input id="showElsewhereOnJellyseerr" is="emby-checkbox" type="checkbox"/>
<span>Show Streaming Providers on Seerr Posters</span>
</label>
</div>
<div class="fieldDescription" style="margin-bottom: 2em;">Displays icons of available streaming services (from your default region) on Seerr Posters.<br><br><b>Requires TMDB API Key to be configured in Elsewhere Settings</b></div>
</div>
</fieldset>
<fieldset class="verticalSection-extrabottompadding">
<legend class="sectionTitle"><h3>Discovery & Recommendations</h3></legend>
<div class="configSection">
<details style="margin-bottom: 1em; background-color: rgba(255,255,255,0.05); padding: 1em; border-radius: 5px;">
<summary style="cursor: pointer; font-weight: bold;">Item Detail Page</summary>
<div style="padding-top: 1em;">
<div class="checkboxContainer" style="margin-bottom: 0.5em !important;">
<label>
<input id="jellyseerrShowSimilar" is="emby-checkbox" type="checkbox"/>
<span>Show similar items on item details page</span>
</label>
</div>
<div class="fieldDescription" style="margin-bottom: 2em;">Displays similar movies/shows from Seerr on item details pages. Limited to 20 items.</div>
<div class="checkboxContainer" style="margin-bottom: 0.5em !important;">
<label>
<input id="jellyseerrShowRecommended" is="emby-checkbox" type="checkbox"/>
<span>Show recommended items on item details page</span>
</label>
</div>
<div class="fieldDescription" style="margin-bottom: 2em;">Displays recommended movies/shows from Seerr on item details pages. Limited to 20 items.</div>
<div class="checkboxContainer" style="margin-bottom: 0.5em !important;">
<label>
<input id="jellyseerrExcludeLibraryItems" is="emby-checkbox" type="checkbox"/>
<span>Exclude items already in library</span>
</label>
</div>
<div class="fieldDescription" style="margin-bottom: 2em;">Hide similar/recommended items that already exist in your Jellyfin library from the results.<br>If disabled, the items that are available link to the media item in the library.</div>
</div>
</details>
<details style="margin-bottom: 1em; background-color: rgba(255,255,255,0.05); padding: 1em; border-radius: 5px;">
<summary style="cursor: pointer; font-weight: bold;">"More" Discovery</summary>
<div style="padding-top: 1em;">
<div class="checkboxContainer" style="margin-bottom: 0.5em !important;">
<label>
<input id="jellyseerrShowNetworkDiscovery" is="emby-checkbox" type="checkbox"/>
<span>Show "More from [Network]" on studio/network pages</span>
</label>
</div>
<div class="fieldDescription" style="margin-bottom: 1em;">When viewing a network page (e.g., Netflix, HBO), displays additional content from that network available in Seerr.</div>
<div class="checkboxContainer" style="margin-bottom: 0.5em !important;">
<label>
<input id="jellyseerrShowGenreDiscovery" is="emby-checkbox" type="checkbox"/>
<span>Show "More [Genre]" on genre pages</span>
</label>
</div>
<div class="fieldDescription" style="margin-bottom: 1em;">When viewing a genre page (e.g., Action, Comedy), displays additional content in that genre from Seerr.</div>
<div class="checkboxContainer" style="margin-bottom: 0.5em !important;">
<label>
<input id="jellyseerrShowTagDiscovery" is="emby-checkbox" type="checkbox"/>
<span>Show "More [Tag]" on tag pages</span>
</label>
</div>
<div class="fieldDescription" style="margin-bottom: 1em;">When viewing a tag page (e.g., cooking, superhero), displays additional content with that keyword from Seerr.</div>
<div class="checkboxContainer" style="margin-bottom: 0.5em !important;">
<label>
<input id="jellyseerrShowPersonDiscovery" is="emby-checkbox" type="checkbox"/>
<span>Show "More from [Actor]" on person pages</span>
</label>
</div>
<div class="fieldDescription" style="margin-bottom: 1em;">When viewing an actor/person page, displays their filmography from Seerr including content not in your library.</div>
<div class="checkboxContainer" style="margin-bottom: 0.5em !important;">
<label>
<input id="jellyseerrShowCollectionDiscovery" is="emby-checkbox" type="checkbox"/>
<span>Show missing collection movies on BoxSet pages</span>
</label>
</div>
<div class="fieldDescription" style="margin-bottom: 2em;">When viewing a collection/BoxSet page, shows movies from the collection that are not yet in your library with request buttons.</div>
</div>
</details>
</div>
</fieldset>
<fieldset class="verticalSection-extrabottompadding">
<legend class="sectionTitle"><h3>Testing / Debug</h3></legend>
<div class="configSection">
<div class="checkboxContainer" style="margin-bottom: 0.5em !important;">
<label>
<input id="jellyseerrDisableCache" is="emby-checkbox" type="checkbox"/>
<span>Disable server-side response cache</span>
</label>
</div>
<div class="fieldDescription" style="margin-bottom: 2em;">When enabled, all Seerr proxy requests bypass the server-side cache and are fetched fresh from Seerr every time. This is useful for testing but will increase load on your Seerr instance. <b>Not recommended for normal use.</b></div>
<div class="inputContainer" style="margin-bottom: 1em;">
<label class="inputLabel inputLabelUnfocused" for="jellyseerrResponseCacheTtlMinutes">Response Cache TTL (minutes)</label>
<input id="jellyseerrResponseCacheTtlMinutes" is="emby-input" type="number" min="1" max="1440" value="10"/>
<div class="fieldDescription">How long Seerr API responses (search, discovery, recommendations) are cached. Default: 10 minutes.</div>
</div>
<div class="inputContainer" style="margin-bottom: 1em;">
<label class="inputLabel inputLabelUnfocused" for="jellyseerrUserIdCacheTtlMinutes">User ID Cache TTL (minutes)</label>
<input id="jellyseerrUserIdCacheTtlMinutes" is="emby-input" type="number" min="1" max="1440" value="30"/>
<div class="fieldDescription">How long the Jellyfin → Seerr user ID mapping is cached. Default: 30 minutes.</div>
</div>
</div>
</fieldset>
<fieldset class="verticalSection-extrabottompadding">
<legend class="sectionTitle"><h3>Automatic Requests</h3></legend>
<div class="configSection">
<details style="margin-bottom: 1em; background-color: rgba(255,255,255,0.05); padding: 1em; border-radius: 5px;">
<summary style="cursor: pointer; font-weight: bold;">Advanced Season Requests</summary>
<div style="padding-top: 1em;">
<div class="checkboxContainer" style="margin-bottom: 0.5em !important;">
<label>
<input id="autoSeasonRequestEnabled" is="emby-checkbox" type="checkbox"/>
<span>Enable Automatic Advance Season Requests</span>
</label>
</div>
<div class="fieldDescription" style="margin-bottom: 2em;">
Automatically request the next season in Seerr when a user about to finish the current season.
<br><br>
</div>
<div class="checkboxContainer" style="margin-bottom: 0.5em !important;">
<label>
<input id="autoSeasonRequestRequireAllWatched" is="emby-checkbox" type="checkbox"/>
<span>Require All Prior Episodes Watched</span>
</label>
</div>
<div class="fieldDescription" style="margin-bottom: 2em;">
Require all episodes before the threshold to be watched. For example, if threshold is 2 and there are 10 episodes, when watching episode 8, all episodes 1-7 must be watched or marked as watched. This prevents accidentally triggering requests by jumping to later episodes. <br> This might cause requests to not be triggered if users skip episodes.
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="autoSeasonRequestThresholdValue">Episodes Remaining Threshold</label>
<input id="autoSeasonRequestThresholdValue" is="emby-input" name="autoSeasonRequestThresholdValue" type="number" min="1" max="20" value="2"/>
<div class="fieldDescription">Request the next season when the number of unwatched episodes is at or below this number. Default: 2 episodes.</div>
</div>
<div
style="background-color: rgba(255, 255, 255, 0.05); border-left: 4px solid #00a4dc; border-radius: 4px; padding: 1em 1.5em; margin: 1.5em 0; display: flex; align-items: flex-start; gap: 1em;">
<i class="material-icons" style="color: #00a4dc; font-size: 18px; margin-top: 2px;">info</i>
<div>
• Requests are triggered when users start or complete watching episodes<br>
• Set threshold to 2-3 episodes to give time for downloading before users finish watching<br>
• Example: With threshold set to 2, when user starts episode 8 of 10 of season 1, season 2 will be requested<br>
• If a user has already triggered a request for the season, it will not be requested again even if another user reaches the threshold<br>
</div>
</div>
</div>
</details>
<details style="margin-bottom: 1em; background-color: rgba(255,255,255,0.05); padding: 1em; border-radius: 5px;">
<summary style="cursor: pointer; font-weight: bold;">Movie Collection Requests</summary>
<div style="padding-top: 1em;">
<div class="checkboxContainer" style="margin-bottom: 0.5em !important;">
<label>
<input id="autoMovieRequestEnabled" is="emby-checkbox" type="checkbox"/>
<span>Enable Automatic Movie Requests</span>
</label>
</div>
<div class="fieldDescription" style="margin-bottom: 2em;">
Automatically request the next movie in a collection based on configured triggers.
<br><br>
<strong>Requirements:</strong> TMDB API key must be configured.
</div>
<div style="margin-bottom: 1.5em;">
<label style="display: block; margin-bottom: 0.5em; font-weight: 500;">Request Triggers:</label>
<div class="checkboxContainer" style="margin-bottom: 0.75em;">
<label>
<input id="autoMovieRequestTriggerOnStart" is="emby-checkbox" type="checkbox"/>
<span>When movie starts (within first 5 minutes)</span>
</label>
</div>
<div class="checkboxContainer" style="margin-bottom: 0.75em;">
<label>
<input id="autoMovieRequestTriggerOnMinutesWatched" is="emby-checkbox" type="checkbox"/>
<span>After watching for a certain amount of time</span>
</label>
</div>
<div class="fieldDescription" style="margin-top: 0.5em;">
Select one or both triggers. If both are selected, the request will be triggered if either condition is met.
</div>
</div>
<div style="margin-bottom: 1.5em;">
<label for="autoMovieRequestMinutesWatched" style="display: block; margin-bottom: 0.5em; font-weight: 500;">Minutes Watched Before Auto-Request Check:</label>
<input id="autoMovieRequestMinutesWatched" type="text" pattern="[0-9]+" style="width: 100px; padding: 0.5em; border: 1px solid #555; border-radius: 4px;" value="20" />
<span style="margin-left: 0.5em;">minutes</span>
<div class="fieldDescription" style="margin-top: 0.5em;">
How many minutes a user should watch before triggering a request? Must be between 1 and 180 minutes.
</div>
</div>
<div class="checkboxContainer" style="margin-bottom: 1.5em;">
<label>
<input id="autoMovieRequestCheckReleaseDate" is="emby-checkbox" type="checkbox"/>
<span>Only request if movie is already released</span>
</label>
</div>
<div class="fieldDescription" style="margin-top: 0.5em;">
When enabled, future releases will be skipped. Only movies that have already been released will be requested.
</div>
<div
style="background-color: rgba(255, 255, 255, 0.05); border-left: 4px solid #00a4dc; border-radius: 4px; padding: 1em 1.5em; margin: 1.5em 0; display: flex; align-items: flex-start; gap: 1em;">
<i class="material-icons" style="color: #00a4dc; font-size: 18px; margin-top: 2px;">info</i>
<div>
• Only works for movies that are part of a TMDB collection (e.g., <a href="https://www.themoviedb.org/collection/86311" target="_blank" style="text-decoration: underline; font-weight: bolder;">The Avengers Collection</a>, <a href="https://www.themoviedb.org/collection/10" target="_blank" style="text-decoration: underline; font-weight: bolder;">Star Wars Collection</a>, etc.)<br>
• Automatically requests the next movie in the collection based on release order<br>
• Movies already available or requested will be skipped<br>
</div>
</div>
</div>
</details>
</div>
</fieldset>
<fieldset class="verticalSection-extrabottompadding">
<legend class="sectionTitle"><h3>Watchlist Integration</h3></legend>
<div class="configSection">
<div class="checkboxContainer" style="margin-bottom: 0.5em !important;">
<label>
<input id="addRequestedMediaToWatchlist" is="emby-checkbox" type="checkbox"/>
<span>Automatically add requested media to user's Watchlist</span>
</label>
</div>
<div class="fieldDescription" style="margin-bottom: 2em;">
When enabled, any media requested through Seerr Search through Jellyfin Enhanced will automatically be added to the requesting user's Watchlist.
<br><br>
<strong>Note:</strong> This feature requires <a class="sectionTitle" target="_blank" href="https://github.com/ranaldsgift/KefinTweaks" style="text-decoration: underline; font-weight: bolder;">KefinTweaks</a> to be installed to actually view the watchlisted items.
</div>
<div class="checkboxContainer" style="margin-bottom: 0.5em !important;">
<label>
<input id="syncJellyseerrWatchlist" is="emby-checkbox" type="checkbox"/>
<span>Sync Seerr Watchlist to Jellyfin</span>
</label>
</div>
<div class="fieldDescription" style="margin-bottom: 2em;">
When enabled, items from each user's Seerr watchlist will be automatically synced to their Jellyfin watchlist. <br><br>
Configure how often the sync should run in Jellyfin Dashboard &gt; Scheduled Tasks &gt; "Sync Watchlist from Seerr to Jellyfin".
</div>
<div class="checkboxContainer" style="margin-bottom: 0.5em !important;">
<label>
<input id="preventWatchlistReAddition" is="emby-checkbox" type="checkbox"/>
<span>Prevent re-adding removed watchlist items</span>
</label>
</div>
<div class="fieldDescription" style="margin-bottom: 1em;">
When enabled, items will only be added to a user's watchlist once. If a user manually removes an item from their watchlist, it won't be automatically re-added during future sync runs.
</div>
<div class="inputContainer" style="margin-bottom: 0.5em;">
<label class="inputLabel inputLabelUnfocused" for="watchlistMemoryRetentionDays">Memory retention period (days):</label>
<input id="watchlistMemoryRetentionDays" is="emby-input" type="number" min="1" max="3650" step="1" style="width: 100px;"/>
<div class="fieldDescription" style="margin-top: 0.5em; margin-bottom: 1em;">
How long to remember that an item was processed before allowing it to be re-added.<br>After this period, manually removed items (if unwatched) will be automatically re-added to watchlist if they're still in Seerr requests or Seerr watchlist.
<br><br>
<strong>Examples:</strong>
<ul style="margin: 0.5em 0; padding-left: 1.5em;">
<li><strong>30 days</strong> - Short memory, items can be re-added after 1 month</li>
<li><strong>365 days</strong> - Remember for 1 year (recommended)</li>
<li><strong>3650 days</strong> - Remember for 10 years (nearly permanent)</li>
</ul>
<strong>Note:</strong> This setting only applies when "Prevent re-adding removed watchlist items" is enabled.
</div>
</div>
</div>
</fieldset>
</div>
<div id="arr-links" class="jellyfin-tab-content" style="display: none; font-family: inherit;">
<fieldset class="verticalSection-extrabottompadding">
<legend class="sectionTitle"><h3>Configuration</h3></legend>
<div class="configSection">
<div class="inputContainer" style="margin-top: 1em;">
<label class="inputLabel inputLabelUnfocused" for="sonarrUrl">Sonarr URL</label>
<input id="sonarrUrl" is="emby-input" name="sonarrUrl" type="text" placeholder="e.g., http://192.168.100.100:8989"/>
</div>
<div class="inputContainer" style="margin-top: 1em;">
<label class="inputLabel inputLabelUnfocused" for="radarrUrl">Radarr URL</label>
<input id="radarrUrl" is="emby-input" name="radarrUrl" type="text" placeholder="e.g., http://192.168.100.100:7878"/>
</div>
<div class="inputContainer" style="margin-top: 1em;">
<label class="inputLabel inputLabelUnfocused" for="bazarrUrl">Bazarr URL</label>
<input id="bazarrUrl" is="emby-input" name="bazarrUrl" type="text" placeholder="e.g., http://192.168.100.100:6767"/>
<div class="fieldDescription">Note: This links to the main movies/series page as Bazarr does not support direct links.</div>
</div>
<div class="inputContainer" style="margin-top: 1em;">
<label class="inputLabel inputLabelUnfocused" for="sonarrApiKey">Sonarr API Key</label>
<input id="sonarrApiKey" is="emby-input" name="sonarrApiKey" placeholder="Your Sonarr API key"/>
<div class="fieldDescription">Find this in Sonarr under Settings > General > Security > API Key</div>
</div>
<div class="inputContainer" style="margin-top: 1em; margin-bottom: 2em;">
<label class="inputLabel inputLabelUnfocused" for="radarrApiKey">Radarr API Key</label>
<input id="radarrApiKey" is="emby-input" name="radarrApiKey" placeholder="Your Radarr API key"/>
<div class="fieldDescription">Find this in Radarr under Settings > General > Security > API Key</div>
</div>
</div>
</fieldset>
<fieldset class="verticalSection-extrabottompadding">
<legend class="sectionTitle"><h3>*arr Links</h3></legend>
<div class="configSection">
<div class="checkboxContainer" style="margin-bottom: 0.5em !important;">
<label>
<input id="arrLinksEnabled" is="emby-checkbox" type="checkbox"/>
<span>Enable *arr Links</span>
</label>
</div>
<div class="fieldDescription" style="margin-bottom: 2em;">Show Sonarr, Radarr, and Bazarr links of the items on item detail pages for quick access. Only for admins.<br> If no URL is added for a service below, links for that service are not shown.</div>
<div>
<label class="sectionTitle" style="margin-bottom: 5px;" for="sonarrUrlMappings">Sonarr URL Mappings</label>
<textarea style="display:block; height: 12vh !important;" class="emby-textarea emby-input" id="sonarrUrlMappings" name="sonarrUrlMappings" placeholder="http://192.168.1.10:8096|http://192.168.1.10:8989&#10;https://jellyfin.example.com|https://sonarr.example.com"></textarea>
<div class="fieldDescription">Map Jellyfin access URLs to Sonarr URLs. Format: <code>jellyfin_url|sonarr_url</code> (one per line).</div>
</div>
<div style="margin-top: 1em;">
<label class="sectionTitle" style="margin-bottom: 5px;" for="radarrUrlMappings">Radarr URL Mappings</label>
<textarea style="display:block; height: 12vh !important;" class="emby-textarea emby-input" id="radarrUrlMappings" name="radarrUrlMappings" placeholder="http://192.168.1.10:8096|http://192.168.1.10:7878&#10;https://jellyfin.example.com|https://radarr.example.com"></textarea>
<div class="fieldDescription">Map Jellyfin access URLs to Radarr URLs. Format: <code>jellyfin_url|radarr_url</code> (one per line).</div>
</div>
<div style="margin-top: 1em;">
<label class="sectionTitle" style="margin-bottom: 5px;" for="bazarrUrlMappings">Bazarr URL Mappings</label>
<textarea style="display:block; height: 12vh !important;" class="emby-textarea emby-input" id="bazarrUrlMappings" name="bazarrUrlMappings" placeholder="http://192.168.1.10:8096|http://192.168.1.10:6767&#10;https://jellyfin.example.com|https://bazarr.example.com"></textarea>
<div class="fieldDescription">Map Jellyfin access URLs to Bazarr URLs. Format: <code>jellyfin_url|bazarr_url</code> (one per line).<br>These mappings are useful when accessing Jellyfin via different URLs (e.g., internal network vs. public domain) and you need *arr links to use the appropriate corresponding URL.</div>
</div>
<div class="checkboxContainer" style="margin-top: 1em; margin-bottom: 2em;">
<label>
<input id="showArrLinksAsText" is="emby-checkbox" type="checkbox"/>
<span>Show links as text instead of icons</span>
</label>
</div>
</div>
</fieldset>
<fieldset class="verticalSection-extrabottompadding">
<legend class="sectionTitle"><h3>*arr Tags Sync</h3></legend>
<div class="configSection">
<div class="checkboxContainer" style="margin-bottom: 0.5em !important;">
<label>
<input id="arrTagsSyncEnabled" is="emby-checkbox" type="checkbox"/>
<span>Enable *arr Tags Sync</span>
</label>
</div>
<div class="fieldDescription" style="margin-bottom: 2em;">
Fetches all tags from Radarr and Sonarr instances defined above and adds them as Jellyfin tags.
<br><br>
<strong>Note:</strong> Run the sync task from Dashboard > Scheduled Tasks > "Sync Tags from *arr to Jellyfin" and configure how often it should run.
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="arrTagsPrefix">Tag Prefix</label>
<input id="arrTagsPrefix" is="emby-input" name="arrTagsPrefix" type="text" placeholder="JE Arr Tag: "/>
<div class="fieldDescription">Prefix to add before each tag (e.g., "JE Arr Tag: " will result in "JE Arr Tag: 1 - Jellyfish")</div>
<br>
<strong>Warning:</strong> Keep the tag prefix constant - it's used to remove old tags before syncing new ones. Having no prefix will remove ALL tags! You can change it, but make sure it's unique.
</div>
<div class="checkboxContainer" style="margin-bottom: 0.5em !important;">
<label>
<input id="arrTagsClearOldTags" is="emby-checkbox" type="checkbox"/>
<span>Clear old tags with prefix before syncing</span>
</label>
</div>
<div class="fieldDescription" style="margin-bottom: 2em;">When enabled, removes all existing tags starting with the prefix before adding new ones. This keeps tags in sync with your *arr applications.</div>
<div class="checkboxContainer" style="margin-bottom: 0.5em !important;">
<label>
<input id="arrTagsShowAsLinks" is="emby-checkbox" type="checkbox"/>
<span>Show synced tags as clickable links</span>
</label>
</div>
<div class="fieldDescription" style="margin-bottom: 2em;">Displays tags with the configured prefix as clickable links on item detail pages. Helpful if your theme hides tags.</div>
<div class="inputContainer">
<label style="margin-bottom: 2em;" for="arrTagsSyncFilter">Filter Tags to Sync to Jellyfin (Optional)</label>
<textarea style="display:block; height: 10vh !important;" class="emby-textarea emby-input" id="arrTagsSyncFilter" name="arrTagsSyncFilter" rows="3" placeholder="1 - Jellyfish&#10;trakt&#10;imdb_250"></textarea>
<br>
<div class="fieldDescription">
Only sync specific tags from *arr to Jellyfin. Enter one tag name per line (without the prefix).
<br>Leave empty to sync all tags.
<br><strong>Example:</strong> If you only want to sync "1 - Jellyfish" and "trakt" tags, enter:
<br><code>1 - Jellyfish</code>
<br><code>trakt</code>
</div>
</div>
<div class="inputContainer">
<label style="margin-bottom: 2em;" for="arrTagsLinksFilter">Filter Tags to Show as Links (Optional)</label>
<textarea style="display:block; height: 10vh !important;" class="emby-textarea emby-input" id="arrTagsLinksFilter" name="arrTagsLinksFilter" rows="3" placeholder="1 - Jellyfish&#10;trakt&#10;imdb_250"></textarea>
<br>
<div class="fieldDescription">
Only show specific tags as links. Enter one tag name per line (without the prefix).
<br>Leave empty to show all tags with the prefix.
<br><strong>Example:</strong> If you only want "JE Arr Tag: 1 - Jellyfish" and "JE Arr Tag: trakt" as links, enter:
<br><code>1 - Jellyfish</code>
<br><code>trakt</code>
</div>
</div>
<div class="inputContainer">
<label style="margin-bottom: 2em;" for="arrTagsLinksHideFilter">Hide Specific Tags from Links (Optional)</label>
<textarea style="display:block; height: 10vh !important;" class="emby-textarea emby-input" id="arrTagsLinksHideFilter" name="arrTagsLinksHideFilter" rows="3" placeholder="unwanted_tag&#10;spam_tag&#10;test_tag"></textarea>
<br>
<div class="fieldDescription">
Hide specific tags from being displayed as links. Enter one tag name per line (without the prefix).
<br>This filter takes priority over the "Show" filter above.
<br><strong>Example:</strong> To hide "JE Arr Tag: spam_tag" and "JE Arr Tag: test_tag" from links, enter:
<br><code>spam_tag</code>
<br><code>test_tag</code>
</div>
</div>
</div>
</fieldset>
<fieldset class="verticalSection-extrabottompadding">
<legend class="sectionTitle"><h3>Requests Page</h3></legend>
<div class="configSection">
<div class="checkboxContainer" style="margin-bottom: 0.5em !important;">
<label>
<input id="downloadsPageEnabled" is="emby-checkbox" type="checkbox"/>
<span>Enable Requests Page</span>
</label>
</div>
<div class="fieldDescription" style="margin-bottom: 2em;">Adds a "Requests" link to the navigation showing active downloads and requests.</div>
<div class="checkboxContainer" style="margin-bottom: 0.5em !important;">
<label>
<input id="downloadsPageShowIssues" is="emby-checkbox" type="checkbox"/>
<span>Show Seerr Issues Section</span>
</label>
</div>
<div class="fieldDescription" style="margin-bottom: 2em;">Shows open/resolved Seerr issues beneath Requests.</div>
<div class="checkboxContainer" style="margin-bottom: 0.5em !important;">
<label>
<input id="downloadsUsePluginPages" is="emby-checkbox" type="checkbox"/>
<span>Use Plugin Pages for Requests Page</span>
</label>
</div>
<div class="fieldDescription" style="margin-bottom: 2em;">Replaces the default Requests page with a <a class="sectionTitle" target="_blank" href="https://github.com/IAmParadox27/jellyfin-plugin-pages">Plugin Pages</a> implementation.<br><strong>Note:</strong> Jellyfin must be restarted after enabling this option for the first time for changes to take effect.</div>
<div class="checkboxContainer" style="margin-bottom: 0.5em !important;">
<label>
<input id="downloadsUseCustomTabs" is="emby-checkbox" type="checkbox"/>
<span>Use Custom Tabs for Requests (instead of sidebar)</span>
</label>
</div>
<div style="background-color: rgba(255, 255, 255, 0.05); border-left: 4px solid #00a4dc; border-radius: 4px; padding: 1em 1.5em; margin: 1.5em 0;">
<div style="display: flex; align-items: flex-start; gap: 1em;">
<i class="material-icons" style="color: #00a4dc; font-size: 24px; margin-top: 2px;">info</i>
<div>
<h4 style="margin: 0 0 0.5em 0;">Embed Requests in Custom Tabs:</h4>
<p class="fieldDescription" style="margin: 0 0 0.5em 0;">Display the Requests page in a Custom Tabs tab instead of the sidebar link.</p>
<p class="fieldDescription" style="margin: 0.5em 0;">You need the <a class="sectionTitle" target="_blank" href="https://github.com/IAmParadox27/jellyfin-plugin-custom-tabs">Custom Tabs</a> plugin.</p>
<ol class="fieldDescription" style="margin: 1em 0; padding-left: 1.5em;">
<li style="margin-bottom: 0.5em;">Install the <strong>Custom Tabs</strong> plugin and all its prerequisites.</li>
<li style="margin-bottom: 0.5em;">Navigate to Dashboard > Plugins > Custom Tabs and add a new tab.</li>
<li style="margin-bottom: 0.5em;">Add "Display Text" according to your preference (e.g., "Requests")</li>
<li style="margin-bottom: 0.5em;">Copy the code below and paste it into the 'HTML Content' field.</li>
</ol>
<div style="position: relative;">
<button type="button" class="je-copy-html-btn raised raised-mini emby-button" data-copy-text="&lt;div class=&quot;jellyfinenhanced requests&quot;&gt;&lt;/div&gt;" style="position: absolute; top: 8px; right: 8px; background-color: rgba(255,255,255,0.1); border: none; padding: 4px 8px; min-width: 80px; transition: color 0.3s;">
<i class="material-icons" style="font-size: 16px; margin-right: 4px; vertical-align: middle;">content_copy</i>
<span class="copy-btn-text" style="vertical-align: middle;">Copy</span>
</button>
<pre style="background-color: rgba(0,0,0,0.3); padding: 1em; border-radius: 4px; white-space: pre-wrap; word-break: break-all; margin: 0; font-family: 'Courier New', Courier, monospace;"><code>&lt;div class="jellyfinenhanced requests"&gt;&lt;/div&gt;
</code></pre>
</div>
<div class="fieldDescription" style="margin-top: 1em;">
Already have Custom Tabs installed?
<a class="sectionTitle" href="/web/#/configurationpage?name=Custom%20Tabs" style="text-decoration: underline;">Click here to configure it.</a>
</div>
</div>
</div>
</div>
<div class="checkboxContainer" style="margin-bottom: 1em;">
<label>
<input id="downloadsPagePollingEnabled" is="emby-checkbox" type="checkbox"/>
<span>Enable Auto-Refresh for Downloads</span>
</label>
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="downloadsPollIntervalSeconds">Poll Interval (seconds)</label>
<input id="downloadsPollIntervalSeconds" is="emby-input" type="number" min="30" max="300"/>
</div>
<div class="fieldDescription" style="margin-bottom: 2em;">How often to refresh download status automatically. Minimum is 30 seconds and applies only when auto-refresh is enabled.</div>
</div>
<div style="background-color: rgba(255, 255, 255, 0.05); border-left: 4px solid #00a4dc; border-radius: 4px; padding: 1em 1.5em; margin: 1.5em 0; display: flex; align-items: center; gap: 1em;">
<div style="display: flex; align-items: flex-start; gap: 1em;">
<i class="material-icons" style="color: #00a4dc; font-size: 24px; margin-top: 2px;">info</i>
<div>
<strong>Requests Page</strong> shows a Requests Button in sidebar with a dedicated view of active downloads from Sonarr/Radarr and requests from Jellyseerr.
<br><br>
<strong>Requirements:</strong> Configure Sonarr/Radarr and Seerr URLs and API keys.
</div>
</div>
</div>
</fieldset>
<fieldset class="verticalSection-extrabottompadding">
<legend class="sectionTitle"><h3>Calendar Page</h3></legend>
<div class="configSection">
<div class="checkboxContainer" style="margin-bottom: 0.5em !important;">
<label>
<input id="calendarPageEnabled" is="emby-checkbox" type="checkbox"/>
<span>Enable Calendar Page</span>
</label>
</div>
<div class="fieldDescription" style="margin-bottom: 2em;">Adds a "Calendar" link to the navigation showing upcoming releases from Sonarr/Radarr.</div>
<div class="checkboxContainer" style="margin-bottom: 0.5em !important;">
<label>
<input id="calendarUsePluginPages" is="emby-checkbox" type="checkbox"/>
<span>Use Plugin Pages for Calendar Page</span>
</label>
</div>
<div class="fieldDescription" style="margin-bottom: 2em;">Replaces the default Calendar page with a <a class="sectionTitle" target="_blank" href="https://github.com/IAmParadox27/jellyfin-plugin-pages">Plugin Pages</a> implementation.<br><strong>Note:</strong> Jellyfin must be restarted after enabling this option for the first time for changes to take effect.</div>
<div class="checkboxContainer" style="margin-bottom: 0.5em !important;">
<label>
<input id="calendarUseCustomTabs" is="emby-checkbox" type="checkbox"/>
<span>Use Custom Tabs for Calendar (instead of sidebar)</span>
</label>
</div>
<div style="background-color: rgba(255, 255, 255, 0.05); border-left: 4px solid #00a4dc; border-radius: 4px; padding: 1em 1.5em; margin: 1.5em 0;">
<div style="display: flex; align-items: flex-start; gap: 1em;">
<i class="material-icons" style="color: #00a4dc; font-size: 24px; margin-top: 2px;">info</i>
<div>
<h4 style="margin: 0 0 0.5em 0;">Embed Calendar in Custom Tabs:</h4>
<p class="fieldDescription" style="margin: 0 0 0.5em 0;">Display the Calendar page in a Custom Tabs tab instead of the sidebar link.</p>
<p class="fieldDescription" style="margin: 0.5em 0;">You need the <a class="sectionTitle" target="_blank" href="https://github.com/IAmParadox27/jellyfin-plugin-custom-tabs">Custom Tabs</a> plugin.</p>
<ol class="fieldDescription" style="margin: 1em 0; padding-left: 1.5em;">
<li style="margin-bottom: 0.5em;">Install the <strong>Custom Tabs</strong> plugin and all its prerequisites.</li>
<li style="margin-bottom: 0.5em;">Navigate to Dashboard > Plugins > Custom Tabs and add a new tab.</li>
<li style="margin-bottom: 0.5em;">Add "Display Text" according to your preference (e.g., "Calendar").</li>
<li style="margin-bottom: 0.5em;">Copy the code below and paste it into the 'HTML Content' field.</li>
</ol>
<div style="position: relative;">
<button type="button" class="je-copy-html-btn raised raised-mini emby-button" data-copy-text="&lt;div class=&quot;jellyfinenhanced calendar&quot;&gt;&lt;/div&gt;" style="position: absolute; top: 8px; right: 8px; background-color: rgba(255,255,255,0.1); border: none; padding: 4px 8px; min-width: 80px; transition: color 0.3s;">
<i class="material-icons" style="font-size: 16px; margin-right: 4px; vertical-align: middle;">content_copy</i>
<span class="copy-btn-text" style="vertical-align: middle;">Copy</span>
</button>
<pre style="background-color: rgba(0,0,0,0.3); padding: 1em; border-radius: 4px; white-space: pre-wrap; word-break: break-all; margin: 0; font-family: 'Courier New', Courier, monospace;"><code>&lt;div class="jellyfinenhanced calendar"&gt;&lt;/div&gt;
</code></pre>
</div>
<div class="fieldDescription" style="margin-top: 1em;">
Already have Custom Tabs installed?
<a class="sectionTitle" href="/web/#/configurationpage?name=Custom%20Tabs" style="text-decoration: underline;">Click here to configure it.</a>
</div>
</div>
</div>
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="calendarFirstDayOfWeek">First Day of Week</label>
<select id="calendarFirstDayOfWeek" is="emby-select">
<option value="Sunday">Sunday</option>
<option value="Monday">Monday</option>
<option value="Tuesday">Tuesday</option>
<option value="Wednesday">Wednesday</option>
<option value="Thursday">Thursday</option>
<option value="Friday">Friday</option>
<option value="Saturday">Saturday</option>
</select>
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="calendarTimeFormat">Time Format</label>
<select id="calendarTimeFormat" is="emby-select">
<option value="5pm/5:30pm">5pm/5:30pm</option>
<option value="17:00/17:30">17:00/17:30</option>
</select>
</div>
<div class="checkboxContainer" style="margin-top: 1.5em; margin-bottom: 0.5em !important;">
<label>
<input id="calendarHighlightFavorites" is="emby-checkbox" type="checkbox"/>
<span>Highlight Favorites/Watchlist</span>
</label>
</div>
<div class="fieldDescription" style="margin-bottom: 1em;">Show a golden border on calendar entries for items in your Jellyfin favorites.</div>
<div class="checkboxContainer" style="margin-bottom: 0.5em !important;">
<label>
<input id="calendarHighlightWatchedSeries" is="emby-checkbox" type="checkbox"/>
<span>Highlight Watched Series</span>
</label>
</div>
<div class="fieldDescription">Show a border on calendar entries for series you have watched episodes from.</div>
<div class="checkboxContainer" style="margin-top: 1.5em; margin-bottom: 0.5em !important;">
<label>
<input id="calendarShowOnlyRequested" is="emby-checkbox" type="checkbox"/>
<span>Show Requested Items Only by Default</span>
</label>
</div>
<div class="fieldDescription">When enabled, the calendar will load showing only Requested items by default, but users can still change filters.</div>
</div>
<div style="background-color: rgba(255, 255, 255, 0.05); border-left: 4px solid #00a4dc; border-radius: 4px; padding: 1em 1.5em; margin: 1.5em 0; display: flex; align-items: center; gap: 1em;">
<div style="display: flex; align-items: flex-start; gap: 1em;">
<i class="material-icons" style="color: #00a4dc; font-size: 24px; margin-top: 2px;">calendar_today</i>
<div>
<strong>Calendar Page</strong> adds a Calendar Button in sidebar with a dedicated view that displays upcoming releases from Sonarr and Radarr in a calendar view.
<br><br>
<strong>Requirements:</strong> Configure Sonarr and/or Radarr URLs and API keys.
</div>
</div>
</div>
</fieldset>
</div>
<div id="extras" class="jellyfin-tab-content" style="display: none; font-family: inherit;">
<fieldset class="verticalSection-extrabottompadding">
<legend class="sectionTitle"><h3>n00bcodr's Personal Scripts</h3></legend>
<div class="configSection">
<div class="checkboxContainer" style="margin-bottom: 0.5em;">
<label>
<input id="coloredRatingsEnabled" is="emby-checkbox" type="checkbox"/>
<span>Enable Colored Ratings</span>
</label>
</div>
<div class="fieldDescription" style="margin-bottom: 1em;">
Applies color-coded backgrounds to media ratings.
<details style="margin: 0.5em 0;">
<summary style="cursor: pointer; color: #00a4dc;">View Screenshot</summary>
<a href="https://cdn.jsdelivr.net/gh/n00bcodr/Jellyfin-Enhanced@main/docs/images/ratings.png" target="_blank">
<img src="https://cdn.jsdelivr.net/gh/n00bcodr/Jellyfin-Enhanced@main/docs/images/ratings.png" style="max-width: 400px; margin: 0.5em 0; border-radius: 10px;" alt="Colored Ratings Example">
</a>
</details>
If you notice missing or incorrect ratings, please submit a PR to <a class="sectionTitle" target="_blank" href="https://github.com/n00bcodr/Jellyfin-Enhanced/">Jellyfin-Enhanced</a>.
</div>
<div class="checkboxContainer" style="margin-bottom: 0.5em;">
<label>
<input id="themeSelectorEnabled" is="emby-checkbox" type="checkbox"/>
<span>Enable Theme Selector</span>
</label>
</div>
<div class="fieldDescription" style="margin-bottom: 1em;">
Adds a theme selector to quickly switch between Jellyfish color themes.
<details style="margin: 0.5em 0;">
<summary style="cursor: pointer; color: #00a4dc;">View Screenshot</summary>
<a href="https://cdn.jsdelivr.net/gh/n00bcodr/Jellyfin-Enhanced@main/docs/images/theme-selector.png" target="_blank">
<img src="https://cdn.jsdelivr.net/gh/n00bcodr/Jellyfin-Enhanced@main/docs/images/theme-selector.png" style="max-width: 400px; margin: 0.5em 0; border-radius: 10px;" alt="Theme Selector Example">
</a>
</details>
</div>
<div style="background-color: rgba(255, 255, 255, 0.05); border-left: 4px solid #ffc107; border-radius: 4px; padding: .5em 1em; margin: 0 0 1.5em 0; display: flex; align-items: center; gap: 1em;">
<i class="material-icons" style="color: #ffc107; font-size: 18px;">note</i>
<div>
Only changes the color theme of Jellyfish when used natively, not through KefinTweaks.
</div>
</div>
<div class="checkboxContainer" style="margin-bottom: 0.5em;">
<label>
<input id="coloredActivityIconsEnabled" is="emby-checkbox" type="checkbox"/>
<span>Enable Colored Activity Icons</span>
</label>
</div>
<div class="fieldDescription" style="margin-bottom: 1em;">
Replaces Dashboard Activity Icons with Material Design icons and background color.
<details style="margin: 0.5em 0;">
<summary style="cursor: pointer; color: #00a4dc;">View Screenshot</summary>
<a href="https://cdn.jsdelivr.net/gh/n00bcodr/Jellyfin-Enhanced@main/docs/images/colored-activity-icons.png" target="_blank">
<img src="https://cdn.jsdelivr.net/gh/n00bcodr/Jellyfin-Enhanced@main/docs/images/colored-activity-icons.png" style="max-width: 400px; margin: 0.5em 0; border-radius: 10px;" alt="Colored Activity Icons Example">
</a>
</details>
</div>
<div style="background-color: rgba(255, 255, 255, 0.05); border-left: 4px solid #00a4dc; border-radius: 4px; padding: .5em 1em; margin: 0 0 1.5em 0; display: flex; align-items: center; gap: 1em;">
<i class="material-icons" style="color: #00a4dc; font-size: 18px;">info</i>
<div>
Currently works with English activity logs only. PRs welcome for additional languages.
</div>
</div>
<div class="checkboxContainer" style="margin-bottom: 0.5em;">
<label>
<input id="loginImageEnabled" is="emby-checkbox" type="checkbox"/>
<span>Enable Login Image</span>
</label>
</div>
<div class="fieldDescription" style="margin-bottom: 1em;">
Displays the user's profile picture on the manual login screen instead of the name. When a user is selected, their avatar appears above the password field.
<details style="margin: 0.5em 0;">
<summary style="cursor: pointer; color: #00a4dc;">View Screenshot</summary>
<a href="https://cdn.jsdelivr.net/gh/n00bcodr/Jellyfin-Enhanced@main/docs/images/login-image.png" target="_blank">
<img src="https://cdn.jsdelivr.net/gh/n00bcodr/Jellyfin-Enhanced@main/docs/images/login-image.png" style="max-width: 400px; margin: 0.5em 0; border-radius: 10px;" alt="Login Image Example">
</a>
</details>
</div>
<div style="background-color: rgba(255, 255, 255, 0.05); border-left: 4px solid #dc3545; border-radius: 4px; padding: .5em 1em; margin: 0 0 1.5em 0; display: flex; align-items: center; gap: 1em;">
<i class="material-icons" style="color: #dc3545; font-size: 18px;">warning</i>
<div>
Only use this script if all your users are visible on the login screen. This completely hides the username text input field, making it difficult to manually login.
</div>
</div>
<div class="checkboxContainer" style="margin-bottom: 0.5em;">
<label>
<input id="pluginIconsEnabled" is="emby-checkbox" type="checkbox"/>
<span>Enable Plugin Icons</span>
</label>
</div>
<div class="fieldDescription" style="margin-bottom: 1em;">
Replaces default plugin folder icons with custom icons on the Dashboard sidebar.
<details style="margin: 0.5em 0;">
<summary style="cursor: pointer; color: #00a4dc;">View Screenshot</summary>
<a href="https://cdn.jsdelivr.net/gh/n00bcodr/Jellyfin-Enhanced@main/docs/images/plugin-icons.png" target="_blank">
<img src="https://cdn.jsdelivr.net/gh/n00bcodr/Jellyfin-Enhanced@main/docs/images/plugin-icons.png" style="max-width: 200px; margin: 0.5em 0; border-radius: 10px;" alt="Plugin Icons Example">
</a>
</details>
</div>
<div style="background-color: rgba(255, 255, 255, 0.05); border-left: 4px solid #ffc107; border-radius: 4px; padding: .5em 1em; margin: 0 0 1.5em 0; display: flex; align-items: center; gap: 1em;">
<i class="material-icons" style="color: #ffc107; font-size: 18px;">note</i>
<div>
Currently replaces icons for (if installed): Jellyfin Enhanced, JavaScript Injector, Intro Skipper, Reports, JellySleep, Home Screen Sections, and File Transformation.
</div>
</div>
<div style="margin-top: 2em; padding-top: 1.5em; border-top: 1px solid rgba(255,255,255,0.1);">
<h4 class="sectionTitle" style="margin-top: 0;">Custom Plugin Links</h4>
<div class="fieldDescription" style="margin-bottom: 1em;">
Add custom plugin links to the Dashboard sidebar. Each link should be on a new line in the format: <code>Configuration Page Name | icon_name</code>
</div>
<div class="inputContainer" style="margin-bottom: 1em;">
<label class="inputLabel inputLabelUnfocused" for="customPluginLinks">Custom Plugin Links</label>
<textarea id="customPluginLinks" class="emby-textarea emby-input" rows="6" placeholder="Jellyfin Tweaks | auto_awesome&#10;Newsletters | newspaper&#10;Webhook | webhook"></textarea>
<div class="fieldDescription" style="margin-top: 0.5em;">
<strong>Format:</strong> <code>Configuration Page Name | Material Icon Name</code><br>
<strong>Important:</strong> Use the exact name that appears in the configuration page URL (e.g., "Jellyfin%20Tweaks" becomes "Jellyfin Tweaks")<br>
<br>
<a href="https://fonts.google.com/icons?icon.set=Material+Icons" target="_blank" class="sectionTitle">Browse Material Icons</a>
</div>
</div>
<button id="testCustomPluginLinksBtn" class="raised button-submit emby-button" is="emby-button" type="button">
<span>Test Links</span>
</button>
<div class="fieldDescription" style="margin-top: 0.5em;">
Use "Test Links" to preview your custom plugin links in the sidebar.<br>Test links will appear immediately and disappear on page refresh.
</div>
</div>
</div>
</fieldset>
<fieldset class="verticalSection-extrabottompadding">
<legend class="sectionTitle"><h3>External Links</h3></legend>
<div class="configSection">
<div class="checkboxContainer" style="margin-bottom: 0.5em !important;">
<label>
<input id="letterboxdEnabled" is="emby-checkbox" type="checkbox"/>
<span>Enable Letterboxd External Links</span>
</label>
</div>
<div class="fieldDescription" style="margin-bottom: 1em;">Adds Letterboxd links to item details page.</div>
<div class="checkboxContainer" style="margin-bottom: 0.5em !important;">
<label>
<input id="showLetterboxdLinkAsText" is="emby-checkbox" type="checkbox"/>
<span>Show Letterboxd link as text instead of icon</span>
</label>
</div>
<div class="fieldDescription" style="margin-bottom: 2em;">When enabled, shows "Letterboxd" as text instead of the Letterboxd icon.</div>
<div class="checkboxContainer" style="margin-bottom: 0.5em !important;">
<label>
<input id="metadataIconsEnabled" is="emby-checkbox" type="checkbox"/>
<span>Enable Metadata Icons (by Druidblack)</span>
</label>
</div>
<div class="fieldDescription" style="margin-bottom: 1em;">
Shows metadata icons instead of text. From <a class="sectionTitle" target="_blank" href="https://github.com/Druidblack/jellyfin-icon-metadata">Druidblack/jellyfin-icon-metadata</a>.<br>
When enabled, links from the plugin will also use icons instead of text (Letterboxd, *arr Links.).
</div>
</div>
</fieldset>
<fieldset class="verticalSection-extrabottompadding">
<legend class="sectionTitle"><h3>Custom Branding</h3></legend>
<div class="configSection">
<div style="padding-top: 1em;">
<div style="background-color: rgba(255, 255, 255, 0.05); border-left: 4px solid #00a4dc; border-radius: 4px; padding: .5em 1em; margin: 0 0 1.5em 0; display: flex; align-items: center; gap: 1em;">
<i class="material-icons" style="color: #00a4dc; font-size: 18px;">info</i>
<div>
Requires the <a class="sectionTitle" target="_blank" href="https://github.com/IAmParadox27/jellyfin-plugin-file-transformation">File Transformation Plugin</a> to be installed and enabled. Upload custom PNG images to replace Jellyfin's default web logos.
</div>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5em; margin-bottom: 1.5em; align-items: stretch;">
<div style="display: flex; flex-direction: column; gap: 0.5em; height: 100%;">
<label style="font-weight: bold; color: #eee;">Icon Transparent</label>
<label style="color: #eee;">The icon in the top-left beside the server version</label>
<div style="font-size: 0.75em; color: #999; margin-bottom: 0.5em;">
<strong>Jellyfin Default:</strong> 536x536px (square)<br>
<strong>Format:</strong> PNG with transparency
</div>
<div style="position: relative; background: rgba(255,255,255,0.05); border: 2px dashed rgba(0,164,220,0.5); border-radius: 4px; padding: 0.85em; text-align: center; cursor: pointer; min-height: 170px; display: flex; flex-direction: column; align-items: center; justify-content: center; flex: 1;" id="iconTransparentDropZone">
<input type="file" id="iconTransparentInput" accept="image/*" style="display: none;" />
<div id="iconTransparentPlaceholder" class="branding-placeholder" style="margin-bottom: 0.5em;">
<i class="material-icons" style="font-size: 32px; color: #00a4dc;">image</i>
</div>
<img id="iconTransparentPreview" style="display:none; max-height:80px; border-radius:4px; display:block; margin: 0 auto .5em auto" />
<div id="iconTransparentDimensions" style="display:none; font-size: 0.75em; color: #00a4dc; margin-top: 0.25em;"></div>
<div style="font-size: 0.9em; color: #ccc;">Drop image here or click to upload</div>
<div style="font-size: 0.8em; color: #999; margin-top: 0.5em;">Max 10MB</div>
<div id="iconTransparentStatus" style="font-size: 0.8em; margin-top: 0.5em; color: #999;"></div>
<button type="button" class="raised button-submit" id="iconTransparentDelete" title="Delete" style="display:none; position:absolute; top:8px; right:8px; background: rgba(0,0,0,0.4) !important; border:none; color:#fff !important; padding:6px !important; border-radius:4px !important;" onmouseover="this.style.setProperty('background','#c62828','important')" onmouseout="this.style.setProperty('background','rgba(0,0,0,0.4)','important')">
<i class="material-icons">delete</i>
</button>
</div>
</div>
<div style="display: flex; flex-direction: column; gap: 0.5em; height: 100%;">
<label style="font-weight: bold; color: #eee;">Favicon</label>
<label style="color: #eee;">Favicon in the browser tab</label>
<div style="font-size: 0.75em; color: #999; margin-bottom: 0.5em;">
<strong>Jellyfin Default:</strong> 16x16px, 32x32px, or 48x48px<br>
<strong>Format:</strong> ICO, PNG, or SVG
</div>
<div style="position: relative; background: rgba(255,255,255,0.05); border: 2px dashed rgba(0,164,220,0.5); border-radius: 4px; padding: 0.85em; text-align: center; cursor: pointer; min-height: 170px; display: flex; flex-direction: column; align-items: center; justify-content: center; flex: 1;" id="faviconDropZone">
<input type="file" id="faviconInput" accept="image/*" style="display: none;" />
<div id="faviconPlaceholder" class="branding-placeholder" style="margin-bottom: 0.5em;">
<i class="material-icons" style="font-size: 32px; color: #00a4dc;">image</i>
</div>
<img id="faviconPreview" style="display:none; max-height:80px; border-radius:4px; display:block; margin: 0 auto .5em auto" />
<div id="faviconDimensions" style="display:none; font-size: 0.75em; color: #00a4dc; margin-top: 0.25em;"></div>
<div style="font-size: 0.9em; color: #ccc;">Drop image here or click to upload</div>
<div style="font-size: 0.8em; color: #999; margin-top: 0.5em;">Max 10MB</div>
<div id="faviconStatus" style="font-size: 0.8em; margin-top: 0.5em; color: #999;"></div>
<button type="button" class="raised button-submit" id="faviconDelete" title="Delete" style="display:none; position:absolute; top:8px; right:8px; background: rgba(0,0,0,0.4) !important; border:none; color:#fff !important; padding:6px !important; border-radius:4px !important;" onmouseover="this.style.setProperty('background','#c62828','important')" onmouseout="this.style.setProperty('background','rgba(0,0,0,0.4)','important')">
<i class="material-icons">delete</i>
</button>
</div>
</div>
<div style="display: flex; flex-direction: column; gap: 0.5em; height: 100%;">
<label style="font-weight: bold; color: #eee;">Banner Light</label>
<label style="color: #eee;">Splash Screen Banner for Dark Mode</label>
<div style="font-size: 0.75em; color: #999; margin-bottom: 0.5em;">
<strong>Jellyfin Default:</strong> 1302x378px<br>
<strong>Format:</strong> PNG, JPG, or WebP
</div>
<div style="position: relative; background: rgba(255,255,255,0.05); border: 2px dashed rgba(0,164,220,0.5); border-radius: 4px; padding: 0.85em; text-align: center; cursor: pointer; min-height: 170px; display: flex; flex-direction: column; align-items: center; justify-content: center; flex: 1;" id="bannerLightDropZone">
<input type="file" id="bannerLightInput" accept="image/*" style="display: none;" />
<div id="bannerLightPlaceholder" class="branding-placeholder" style="margin-bottom: 0.5em;">
<i class="material-icons" style="font-size: 32px; color: #00a4dc;">image</i>
</div>
<img id="bannerLightPreview" style="display:none; max-height:80px; border-radius:4px; display:block; margin: 0 auto .5em auto" />
<div id="bannerLightDimensions" style="display:none; font-size: 0.75em; color: #00a4dc; margin-top: 0.25em;"></div>
<div style="font-size: 0.9em; color: #ccc;">Drop image here or click to upload</div>
<div style="font-size: 0.8em; color: #999; margin-top: 0.5em;">Max 10MB</div>
<div id="bannerLightStatus" style="font-size: 0.8em; margin-top: 0.5em; color: #999;"></div>
<button type="button" class="raised button-submit" id="bannerLightDelete" title="Delete" style="display:none; position:absolute; top:8px; right:8px; background: rgba(0,0,0,0.4) !important; border:none; color:#fff !important; padding:6px !important; border-radius:4px !important;" onmouseover="this.style.setProperty('background','#c62828','important')" onmouseout="this.style.setProperty('background','rgba(0,0,0,0.4)','important')">
<i class="material-icons">delete</i>
</button>
</div>
</div>
<div style="display: flex; flex-direction: column; gap: 0.5em; height: 100%;">
<label style="font-weight: bold; color: #eee;">Banner Dark</label>
<label style="color: #eee;">Splash Screen Banner for Light Mode</label>
<div style="font-size: 0.75em; color: #999; margin-bottom: 0.5em;">
<strong>Jellyfin Default:</strong> 1302x378px<br>
<strong>Format:</strong> PNG, JPG, or WebP
</div>
<div style="position: relative; background: rgba(255,255,255,0.05); border: 2px dashed rgba(0,164,220,0.5); border-radius: 4px; padding: 0.85em; text-align: center; cursor: pointer; min-height: 170px; display: flex; flex-direction: column; align-items: center; justify-content: center; flex: 1;" id="bannerDarkDropZone">
<input type="file" id="bannerDarkInput" accept="image/*" style="display: none;" />
<div id="bannerDarkPlaceholder" class="branding-placeholder" style="margin-bottom: 0.5em;">
<i class="material-icons" style="font-size: 32px; color: #00a4dc;">image</i>
</div>
<img id="bannerDarkPreview" style="display:none; max-height:80px; border-radius:4px; display:block; margin: 0 auto .5em auto" />
<div id="bannerDarkDimensions" style="display:none; font-size: 0.75em; color: #00a4dc; margin-top: 0.25em;"></div>
<div style="font-size: 0.9em; color: #ccc;">Drop image here or click to upload</div>
<div style="font-size: 0.8em; color: #999; margin-top: 0.5em;">Max 10MB</div>
<div id="bannerDarkStatus" style="font-size: 0.8em; margin-top: 0.5em; color: #999;"></div>
<button type="button" class="raised button-submit" id="bannerDarkDelete" title="Delete" style="display:none; position:absolute; top:8px; right:8px; background: rgba(0,0,0,0.4) !important; border:none; color:#fff !important; padding:6px !important; border-radius:4px !important;" onmouseover="this.style.setProperty('background','#c62828','important')" onmouseout="this.style.setProperty('background','rgba(0,0,0,0.4)','important')">
<i class="material-icons">delete</i>
</button>
</div>
</div>
<div style="display: flex; flex-direction: column; gap: 0.5em; height: 100%;">
<label style="font-weight: bold; color: #eee;">Apple Touch Icon</label>
<label style="color: #eee;">Icon shown when adding to iOS Home Screen</label>
<div style="font-size: 0.75em; color: #999; margin-bottom: 0.5em;">
<strong>Jellyfin Default:</strong> 180x180px<br>
<strong>Format:</strong> PNG
</div>
<div style="position: relative; background: rgba(255,255,255,0.05); border: 2px dashed rgba(0,164,220,0.5); border-radius: 4px; padding: 0.85em; text-align: center; cursor: pointer; min-height: 170px; display: flex; flex-direction: column; align-items: center; justify-content: center; flex: 1;" id="touchiconDropZone">
<input type="file" id="touchiconInput" accept="image/*" style="display: none;" />
<div id="touchiconPlaceholder" class="branding-placeholder" style="margin-bottom: 0.5em;">
<i class="material-icons" style="font-size: 32px; color: #00a4dc;">image</i>
</div>
<img id="touchiconPreview" style="display:none; max-height:80px; border-radius:4px; display:block; margin: 0 auto .5em auto" />
<div id="touchiconDimensions" style="display:none; font-size: 0.75em; color: #00a4dc; margin-top: 0.25em;"></div>
<div style="font-size: 0.9em; color: #ccc;">Drop image here or click to upload</div>
<div style="font-size: 0.8em; color: #999; margin-top: 0.5em;">Max 10MB</div>
<div id="touchiconStatus" style="font-size: 0.8em; margin-top: 0.5em; color: #999;"></div>
<button type="button" class="raised button-submit" id="touchiconDelete" title="Delete" style="display:none; position:absolute; top:8px; right:8px; background: rgba(0,0,0,0.4) !important; border:none; color:#fff !important; padding:6px !important; border-radius:4px !important;" onmouseover="this.style.setProperty('background','#c62828','important')" onmouseout="this.style.setProperty('background','rgba(0,0,0,0.4)','important')">
<i class="material-icons">delete</i>
</button>
</div>
</div>
</div>
</div>
</div>
</fieldset>
<fieldset class="verticalSection-extrabottompadding">
<legend class="sectionTitle"><h3></h3></legend>
<div class="configSection">
<div style="padding-top: 1em;">
<div class="checkboxContainer" style="margin-bottom: 0.5em;"><label><input id="enableCustomSplashScreen" is="emby-checkbox" type="checkbox"/><span>Enable Custom Splash Screen</span></label></div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="splashScreenImageUrl">Splash Screen Image URL</label>
<input id="splashScreenImageUrl" is="emby-input" name="splashScreenImageUrl" type="text" placeholder="/web/assets/img/banner-light.png"/>
<div class="fieldDescription">URL for the splash screen image. Defaults to Jellyfin banner.</div>
</div>
</div>
<div
style="background-color: rgba(255, 255, 255, 0.05); border-left: 4px solid #00a4dc; border-radius: 4px; padding: .5em 1em; margin: 2em 0 .5em 0; display: flex; align-items: center; gap: 1em;">
<i class="material-icons" style="color: #00a4dc; font-size: 18px;">info</i>
<div>
Might cause strange behaviour if <a class="sectionTitle" target="_blank" href="https://github.com/IAmParadox27/jellyfin-plugin-media-bar">jellyfin-plugin-media-bar</a> is installed.
<br>
<br>
If you are on Jellyfin version 10.11 and you want the default Jellyfin Banner - Change the splash screen URL to "<strong>/web/banner-light.b113d4d1c6c07fcb73f0.png</strong>"
</div>
</div>
</div>
</fieldset>
</div>
<div
style="background-color: rgba(255, 255, 255, 0.05); border-left: 4px solid #00a4dc; border-radius: 4px; padding: 1em 1.5em; margin: 1.5em 0; display: flex; align-items: center; gap: 1em;">
<i class="material-icons" style="color: #00a4dc; font-size: 24px;">info</i>
<div>
All changes require a page refresh to take effect. <br/>
If old settings persist, please force clear browser cache.
</div>
</div>
<div>
<button class="raised button-submit block emby-button" is="emby-button" type="submit">
<span>Save</span>
</button>
</div>
</form>
</div>
</div>
<script type="text/javascript">
(() => {
const pluginId = 'f69e946a-4b3c-4e9a-8f0a-8d7c1b2c4d9b';
const page = document.querySelector('#JellyfinEnhancedPage');
const form = document.querySelector('#JellyfinEnhancedForm');
const resetAllUserSettingsBtn = document.querySelector('#resetAllUserSettingsBtn');
const clearTagsCacheBtn = document.querySelector('#clearTagsCacheBtn');
const shortcutListContainer = document.getElementById('shortcut-list-container');
const addShortcutSelect = document.getElementById('add-shortcut-select');
const addShortcutKeyInput = document.getElementById('add-shortcut-key');
const addShortcutBtn = document.getElementById('add-shortcut-btn');
const shortcutErrorComment = document.getElementById('shortcut-error-comment');
const testJellyseerrBtn = document.getElementById('testJellyseerrBtn');
const jellyseerrStatusIndicator = document.getElementById('jellyseerrStatusIndicator');
const tmdbStatusIndicator = document.getElementById('tmdbStatusIndicator');
const style = document.createElement('style');
style.textContent = `
@keyframes spin { to { transform: rotate(360deg); } }
.status-check { animation: spin 1s linear infinite; }
@keyframes shake {
10%, 90% { transform: translate3d(-1px, 0, 0); }
20%, 80% { transform: translate3d(2px, 0, 0); }
30%, 50%, 70% { transform: translate3d(-4px, 0, 0); }
40%, 60% { transform: translate3d(4px, 0, 0); }
}
.shake {
animation: shake 0.82s cubic-bezier(.36,.07,.19,.97) both;
}
.jellyfin-tab-button {
position: relative;
z-index: 1000;
}
.jellyfin-tab-button h3 {
pointer-events: none;
}
.jellyfin-tab-button img {
pointer-events: none;
}
`;
document.head.appendChild(style);
let shortcutOverrides = [];
const tabs = document.querySelectorAll('.jellyfin-tab-button');
const tabContents = document.querySelectorAll('.jellyfin-tab-content');
function activateTab(tabId) {
tabs.forEach(t => {
const isActive = t.dataset.tab === tabId;
if (isActive) {
t.classList.add('active');
t.style.color = 'var(--primary-accent-color, #fff)';
t.style.borderBottom = '2px solid var(--primary-accent-color, #fff)';
} else {
t.classList.remove('active');
t.style.color = '#ccc';
t.style.borderBottom = '2px solid transparent';
}
});
tabContents.forEach(content => {
const isActive = content.id === tabId;
if (isActive) {
content.classList.add('active');
content.style.display = 'block';
} else {
content.classList.remove('active');
content.style.display = 'none';
}
});
}
tabs.forEach(tab => {
tab.addEventListener('click', () => {
const tabId = tab.dataset.tab;
activateTab(tabId);
// Store active tab in sessionStorage to persist across refreshes
try {
sessionStorage.setItem('jellyfinEnhancedActiveTab', tabId);
} catch (e) {
// Ignore if sessionStorage is not available
}
});
});
// Restore tab from sessionStorage on page load
try {
const savedTab = sessionStorage.getItem('jellyfinEnhancedActiveTab');
if (savedTab && document.getElementById(savedTab)) {
activateTab(savedTab);
}
} catch (e) {
// Ignore if sessionStorage is not available
}
const defaultShortcuts = [
{ Name: "OpenSearch", Key: "/", Label: "Open Search", Category: "Global" },
{ Name: "GoToHome", Key: "Shift+H", Label: "Go to Home", Category: "Global" },
{ Name: "GoToDashboard", Key: "D", Label: "Go to Dashboard", Category: "Global" },
{ Name: "QuickConnect", Key: "Q", Label: "Quick Connect", Category: "Global" },
{ Name: "PlayRandomItem", Key: "R", Label: "Play Random Item", Category: "Global" },
{ Name: "CycleAspectRatio", Key: "A", Label: "Cycle Aspect Ratio", Category: "Player" },
{ Name: "ShowPlaybackInfo", Key: "I", Label: "Show Playback Info", Category: "Player" },
{ Name: "SubtitleMenu", Key: "S", Label: "Subtitle Menu", Category: "Player" },
{ Name: "CycleSubtitleTracks", Key: "C", Label: "Cycle Subtitle Tracks", Category: "Player" },
{ Name: "CycleAudioTracks", Key: "V", Label: "Cycle Audio Tracks", Category: "Player" },
{ Name: "IncreasePlaybackSpeed", Key: "+", Label: "Increase Playback Speed", Category: "Player" },
{ Name: "DecreasePlaybackSpeed", Key: "-", Label: "Decrease Playback Speed", Category: "Player" },
{ Name: "ResetPlaybackSpeed", Key: "R", Label: "Reset Playback Speed", Category: "Player" },
{ Name: "BookmarkCurrentTime", Key: "B", Label: "Bookmark Current Time", Category: "Player" },
{ Name: "OpenEpisodePreview", Key: "P", Label: "Open Episode Preview", Category: "Player" },
{ Name: "SkipIntroOutro", Key: "O", Label: "Skip Intro/Outro", Category: "Player" }
];
function renderOverrides() {
shortcutListContainer.innerHTML = '';
if (shortcutOverrides.length === 0) {
shortcutListContainer.innerHTML = '<p class="fieldDescription" style="text-align: center;">No overrides configured. All shortcuts are using default values.</p>';
}
shortcutOverrides.forEach((shortcut, index) => {
const row = document.createElement('div');
row.className = 'inputContainer';
row.style.display = 'flex';
row.style.alignItems = 'center';
row.style.gap = '1em';
const label = document.createElement('label');
label.className = 'inputLabel';
label.textContent = shortcut.Label;
label.style.flex = '1';
const input = document.createElement('input');
input.setAttribute('is', 'emby-input');
input.type = 'text';
input.value = shortcut.Key;
input.style.flex = '1';
input.style.textAlign = 'center';
input.addEventListener('input', (e) => {
let value = e.target.value;
// Automatically convert single lowercase letters to uppercase ***
if (value.match(/^[a-z]$/)) {
value = value.toUpperCase();
e.target.value = value;
}
shortcutOverrides[index].Key = value;
});
const buttonContainer = document.createElement('div');
const removeBtn = document.createElement('button');
removeBtn.setAttribute('is', 'emby-button');
removeBtn.type = 'button';
removeBtn.textContent = 'Remove';
removeBtn.className = 'raised button-cancel';
removeBtn.style.marginLeft = '1em';
removeBtn.addEventListener('click', () => {
shortcutOverrides.splice(index, 1);
renderOverrides();
populateAddShortcutDropdown();
});
buttonContainer.appendChild(removeBtn);
row.appendChild(label);
row.appendChild(input);
row.appendChild(buttonContainer);
shortcutListContainer.appendChild(row);
});
}
function populateAddShortcutDropdown() {
addShortcutSelect.innerHTML = '';
const overriddenNames = shortcutOverrides.map(s => s.Name);
const availableShortcuts = defaultShortcuts.filter(s => !overriddenNames.includes(s.Name));
availableShortcuts.forEach(shortcut => {
const option = document.createElement('option');
option.value = shortcut.Name;
option.textContent = shortcut.Label;
addShortcutSelect.appendChild(option);
});
addShortcutBtn.disabled = availableShortcuts.length === 0;
addShortcutKeyInput.disabled = availableShortcuts.length === 0;
}
function showValidationError(elementToShake, message) {
shortcutErrorComment.textContent = message;
shortcutErrorComment.style.display = 'block';
elementToShake.classList.add('shake');
setTimeout(() => {
elementToShake.classList.remove('shake');
shortcutErrorComment.style.display = 'none';
}, 8000);
}
addShortcutBtn.addEventListener('click', () => {
const selectedName = addShortcutSelect.value;
let newKey = addShortcutKeyInput.value.trim();
// Automatically convert single lowercase letters to uppercase ***
if (newKey.match(/^[a-z]$/)) {
newKey = newKey.toUpperCase();
}
// Check 0: See if there is a key being added
if (!selectedName || !newKey) {
showValidationError(addShortcutBtn, 'Please enter a key to use as an override.');
return;
}
// Check 1: See if the key is already used in another custom override.
const overrideConflict = shortcutOverrides.find(s => s.Key.toLowerCase() === newKey.toLowerCase());
if (overrideConflict) {
const errorMessage = "The key '" + newKey + "' is already assigned to '" + overrideConflict.Label + "' as an override.";
showValidationError(addShortcutKeyInput.parentElement, errorMessage);
return;
}
// Check 2: See if the key is used by another default shortcut.
const defaultConflict = defaultShortcuts.find(s => s.Key.toLowerCase() === newKey.toLowerCase() && s.Name !== selectedName);
if (defaultConflict) {
const errorMessage = "The key '" + newKey + "' is already used by '" + defaultConflict.Label + "'.";
showValidationError(addShortcutKeyInput.parentElement, errorMessage);
return;
}
const defaultConfig = defaultShortcuts.find(s => s.Name === selectedName);
if (defaultConfig) {
shortcutOverrides.push({ ...defaultConfig, Key: newKey });
renderOverrides();
populateAddShortcutDropdown();
addShortcutKeyInput.value = '';
}
});
async function testJellyseerrConnection() {
const urls = (document.querySelector('#jellyseerrUrls').value || '').split('\n').map(u => u.trim()).filter(Boolean);
const apiKey = (document.querySelector('#JellyseerrApiKey').value || '').trim();
if (!urls.length || !apiKey) {
Dashboard.alert({ title: 'Missing Information', message: 'Please provide at least one Seerr URL and an API key to test the connection.' });
return;
}
testJellyseerrBtn.disabled = true;
jellyseerrStatusIndicator.textContent = 'sync';
jellyseerrStatusIndicator.className = 'material-icons status-check';
jellyseerrStatusIndicator.style.color = '#00a4dc';
let validated = false;
for (const url of urls) {
try {
const validationUrl = ApiClient.getUrl(`/JellyfinEnhanced/jellyseerr/validate`, {
url: url,
apiKey: apiKey
});
console.debug("Attempting to validate with URL:", validationUrl);
const res = await ApiClient.ajax({ type: 'GET', url: validationUrl, dataType: 'json' });
if (res && res.ok) {
validated = true;
break;
}
} catch (e) {
console.error(`Seerr validation failed for ${url}:`, e);
}
}
testJellyseerrBtn.disabled = false;
jellyseerrStatusIndicator.classList.remove('status-check');
if (validated) {
jellyseerrStatusIndicator.textContent = 'check_circle';
jellyseerrStatusIndicator.style.color = '#52b54b';
Dashboard.alert({ title: 'Success', message: 'Successfully connected to Jellyseerr!' });
} else {
jellyseerrStatusIndicator.textContent = 'error';
jellyseerrStatusIndicator.style.color = '#dc3545';
Dashboard.alert({ title: 'Connection Failed', message: 'Could not validate the API key against any provided URL.<br><br>Please check the details.' });
}
}
async function testTmdbConnection(event) {
const apiKey = (document.querySelector('#TMDB_API_KEY').value || '').trim();
if (!apiKey) {
Dashboard.alert({ title: 'Missing Information', message: 'Please provide a TMDB API key to test the connection.' });
return;
}
// Determine which status indicator to update based on button context
const button = event.target.closest('button');
const statusIndicator = button.parentElement.querySelector('.material-icons') || tmdbStatusIndicator;
// Disable all test buttons during the test
const allTestButtons = document.querySelectorAll('.testTmdbBtn');
allTestButtons.forEach(btn => btn.disabled = true);
statusIndicator.textContent = 'sync';
statusIndicator.className = 'material-icons status-check';
statusIndicator.style.color = '#00a4dc';
try {
const validationUrl = ApiClient.getUrl(`/JellyfinEnhanced/tmdb/validate`, { apiKey: apiKey });
await ApiClient.ajax({ type: 'GET', url: validationUrl });
statusIndicator.textContent = 'check_circle';
statusIndicator.style.color = '#52b54b';
Dashboard.alert({ title: 'Success', message: 'Successfully connected to TMDB!' });
} catch (e) {
console.error('TMDB validation failed:', e);
const errorMessage = (e.status === 401)
? 'The API Key is invalid.'
: 'Could not connect to TMDB. Please check the key and your network.';
statusIndicator.textContent = 'error';
statusIndicator.style.color = '#dc3545';
Dashboard.alert({ title: 'Connection Failed', message: errorMessage });
} finally {
allTestButtons.forEach(btn => btn.disabled = false);
if (statusIndicator) {
statusIndicator.classList.remove('status-check');
}
}
}
/** @type {boolean} Tracks whether the File Transformation warning has been dismissed this session */
var ftWarningDismissedSession = false;
/**
* Checks if the File Transformation plugin is installed and shows a warning banner if not.
* The warning can be dismissed per-session (resets on page reload) or permanently via localStorage.
* Called during loadConfig() on page load.
* @see https://github.com/IAmParadox27/jellyfin-plugin-file-transformation
*/
function checkFileTransformationPlugin() {
// Skip check if user has already dismissed the warning
if (ftWarningDismissedSession || localStorage.getItem('je_ft_warning_dismissed') === 'true') {
return;
}
// Query the Plugins API to check if File Transformation is installed
ApiClient.ajax({
type: 'GET',
url: ApiClient.getUrl('/Plugins'),
dataType: 'json'
}).then(function(plugins) {
var hasFileTransformation = plugins.some(function(p) {
return p.Name === 'File Transformation';
});
var warning = document.getElementById('fileTransformationWarning');
if (warning) {
warning.style.display = hasFileTransformation ? 'none' : 'block';
}
}).catch(function() {
// If we can't check, don't show the warning
});
// Set up dismiss button handlers
var dismissSession = document.getElementById('dismissFtWarningSession');
var dismissForever = document.getElementById('dismissFtWarningForever');
if (dismissSession) {
dismissSession.onclick = function() {
ftWarningDismissedSession = true;
document.getElementById('fileTransformationWarning').style.display = 'none';
};
}
if (dismissForever) {
dismissForever.onclick = function() {
localStorage.setItem('je_ft_warning_dismissed', 'true');
document.getElementById('fileTransformationWarning').style.display = 'none';
};
}
}
function loadConfig() {
Dashboard.showLoadingMsg();
checkFileTransformationPlugin();
ApiClient.getPluginConfiguration(pluginId).then((config) => {
const savedShortcuts = (config.Shortcuts && config.Shortcuts.length > 0) ? config.Shortcuts : defaultShortcuts;
shortcutOverrides = savedShortcuts.filter(saved => {
const def = defaultShortcuts.find(d => d.Name === saved.Name);
return !def || saved.Key !== def.Key;
});
renderOverrides();
populateAddShortcutDropdown();
// Load all other settings
document.querySelector('#enableCustomSplashScreen').checked = config.EnableCustomSplashScreen;
document.querySelector('#splashScreenImageUrl').value = config.SplashScreenImageUrl;
document.querySelector('#disableAllShortcuts').checked = config.DisableAllShortcuts;
document.querySelector('#ToastDuration').value = config.ToastDuration;
document.querySelector('#HelpPanelAutocloseDelay').value = config.HelpPanelAutocloseDelay;
document.querySelector('#elsewhereEnabled').checked = config.ElsewhereEnabled;
document.querySelector('#TMDB_API_KEY').value = config.TMDB_API_KEY;
document.querySelector('#DEFAULT_REGION').value = config.DEFAULT_REGION;
document.querySelector('#DEFAULT_PROVIDERS').value = config.DEFAULT_PROVIDERS;
document.querySelector('#IGNORE_PROVIDERS').value = config.IGNORE_PROVIDERS;
document.querySelector('#ElsewhereCustomBrandingText').value = config.ElsewhereCustomBrandingText || '';
document.querySelector('#ElsewhereCustomBrandingImageUrl').value = config.ElsewhereCustomBrandingImageUrl || '';
document.querySelector('#jellyseerr_TMDB_API_KEY').value = config.TMDB_API_KEY;
// Set up bidirectional sync between TMDB API key fields
const tmdbKeyField = document.querySelector('#TMDB_API_KEY');
const jellyseerrTmdbKeyField = document.querySelector('#jellyseerr_TMDB_API_KEY');
tmdbKeyField.addEventListener('input', function() {
jellyseerrTmdbKeyField.value = this.value;
});
jellyseerrTmdbKeyField.addEventListener('input', function() {
tmdbKeyField.value = this.value;
});
document.querySelector('#autoPauseEnabled').checked = config.AutoPauseEnabled;
document.querySelector('#autoResumeEnabled').checked = config.AutoResumeEnabled;
document.querySelector('#autoPipEnabled').checked = config.AutoPipEnabled;
document.querySelector('#autoSkipIntro').checked = config.AutoSkipIntro;
document.querySelector('#autoSkipOutro').checked = config.AutoSkipOutro;
document.querySelector('#longPress2xEnabled').checked = config.LongPress2xEnabled;
document.querySelector('#randomButtonEnabled').checked = config.RandomButtonEnabled;
document.querySelector('#randomIncludeMovies').checked = config.RandomIncludeMovies;
document.querySelector('#randomIncludeShows').checked = config.RandomIncludeShows;
document.querySelector('#randomUnwatchedOnly').checked = config.RandomUnwatchedOnly;
document.querySelector('#showWatchProgress').checked = config.ShowWatchProgress;
document.querySelector('#watchProgressDefaultMode').value = (config.WatchProgressDefaultMode || 'percentage');
document.querySelector('#watchProgressTimeFormat').value = (config.WatchProgressTimeFormat || 'hours');
document.querySelector('#showFileSizes').checked = config.ShowFileSizes;
document.querySelector('#showAudioLanguages').checked = config.ShowAudioLanguages;
document.querySelector('#removeContinueWatchingEnabled').checked = config.RemoveContinueWatchingEnabled;
document.querySelector('#qualityTagsEnabled').checked = config.QualityTagsEnabled;
document.querySelector('#genreTagsEnabled').checked = config.GenreTagsEnabled;
document.querySelector('#peopleTagsEnabled').checked = config.PeopleTagsEnabled;
const qPos = (config.QualityTagsPosition || 'top-left');
const gPos = (config.GenreTagsPosition || 'top-right');
const lPos = (config.LanguageTagsPosition || 'bottom-left');
const rPos = (config.RatingTagsPosition || 'bottom-right');
const langEnabled = !!config.LanguageTagsEnabled;
const ratingEnabled = !!config.RatingTagsEnabled;
const peopleEnabled = !!config.PeopleTagsEnabled;
const showRatingInPlayer = config.ShowRatingInPlayer !== false;
const qSel = document.querySelector('#qualityTagsPosition');
const gSel = document.querySelector('#genreTagsPosition');
const lSel = document.querySelector('#languageTagsPosition');
const rSel = document.querySelector('#ratingTagsPosition');
const lChk = document.querySelector('#languageTagsEnabled');
const rChk = document.querySelector('#ratingTagsEnabled');
const rPlayerChk = document.querySelector('#showRatingInPlayer');
if (qSel) qSel.value = qPos;
if (gSel) gSel.value = gPos;
if (lSel) lSel.value = lPos;
if (rSel) rSel.value = rPos;
if (lChk) lChk.checked = langEnabled;
if (rChk) rChk.checked = ratingEnabled;
const pChk = document.querySelector('#peopleTagsEnabled');
if (pChk) pChk.checked = peopleEnabled;
if (rPlayerChk) rPlayerChk.checked = showRatingInPlayer;
document.querySelector('#disableTagsOnSearchPage').checked = config.DisableTagsOnSearchPage === true;
document.querySelector('#tagsCacheTtlDays').value = config.TagsCacheTtlDays || 30;
document.querySelector('#pauseScreenEnabled').checked = config.PauseScreenEnabled;
document.querySelector('#showReviews').checked = config.ShowReviews;
document.querySelector('#reviewsExpandedByDefault').checked = config.ReviewsExpandedByDefault;
document.querySelector('#jellyseerrEnabled').checked = config.JellyseerrEnabled;
document.querySelector('#showCollectionsInSearch').checked = config.ShowCollectionsInSearch !== false;
document.querySelector('#jellyseerrEnable4kRequests').checked = !!config.JellyseerrEnable4KRequests;
document.querySelector('#jellyseerrUseMoreInfoModal').checked = !!config.JellyseerrUseMoreInfoModal;
document.querySelector('#jellyseerrShowAdvanced').checked = config.JellyseerrShowAdvanced;
document.querySelector('#jellyseerrShowSimilar').checked = config.JellyseerrShowSimilar !== false;
document.querySelector('#jellyseerrShowRecommended').checked = config.JellyseerrShowRecommended !== false;
document.querySelector('#jellyseerrExcludeLibraryItems').checked = config.JellyseerrExcludeLibraryItems !== false;
document.querySelector('#jellyseerrShowNetworkDiscovery').checked = config.JellyseerrShowNetworkDiscovery !== false;
document.querySelector('#jellyseerrShowGenreDiscovery').checked = config.JellyseerrShowGenreDiscovery !== false;
document.querySelector('#jellyseerrShowTagDiscovery').checked = config.JellyseerrShowTagDiscovery !== false;
document.querySelector('#jellyseerrShowPersonDiscovery').checked = config.JellyseerrShowPersonDiscovery !== false;
document.querySelector('#jellyseerrShowCollectionDiscovery').checked = config.JellyseerrShowCollectionDiscovery !== false;
document.querySelector('#jellyseerrShowReportButton').checked = !!config.JellyseerrShowReportButton;
document.querySelector('#jellyseerrExcludeBlocklistedItems').checked = !!config.JellyseerrExcludeBlocklistedItems;
document.querySelector('#jellyseerrDisableCache').checked = !!config.JellyseerrDisableCache;
document.querySelector('#jellyseerrResponseCacheTtlMinutes').value = config.JellyseerrResponseCacheTtlMinutes || 10;
document.querySelector('#jellyseerrUserIdCacheTtlMinutes').value = config.JellyseerrUserIdCacheTtlMinutes || 30;
document.querySelector('#showElsewhereOnJellyseerr').checked = config.ShowElsewhereOnJellyseerr;
document.querySelector('#jellyseerrUrls').value = config.JellyseerrUrls;
document.querySelector('#JellyseerrApiKey').value = config.JellyseerrApiKey;
document.querySelector('#jellyseerrUrlMappings').value = config.JellyseerrUrlMappings || '';
document.querySelector('#jellyseerr_TMDB_API_KEY').value = config.TMDB_API_KEY;
document.querySelector('#autoSeasonRequestEnabled').checked = config.AutoSeasonRequestEnabled || false;
document.querySelector('#autoSeasonRequestRequireAllWatched').checked = config.AutoSeasonRequestRequireAllWatched || false;
document.querySelector('#autoSeasonRequestThresholdValue').value = config.AutoSeasonRequestThresholdValue || 2;
document.querySelector('#autoMovieRequestEnabled').checked = config.AutoMovieRequestEnabled || false;
const triggerType = config.AutoMovieRequestTriggerType || 'OnMinutesWatched';
document.querySelector('#autoMovieRequestTriggerOnStart').checked = (triggerType === 'OnStart' || triggerType === 'Both');
document.querySelector('#autoMovieRequestTriggerOnMinutesWatched').checked = (triggerType === 'OnMinutesWatched' || triggerType === 'Both');
document.querySelector('#autoMovieRequestMinutesWatched').value = config.AutoMovieRequestMinutesWatched || 20;
document.querySelector('#autoMovieRequestCheckReleaseDate').checked = config.AutoMovieRequestCheckReleaseDate !== false;
document.querySelector('#addRequestedMediaToWatchlist').checked = config.AddRequestedMediaToWatchlist || false;
document.querySelector('#syncJellyseerrWatchlist').checked = config.SyncJellyseerrWatchlist || false;
document.querySelector('#preventWatchlistReAddition').checked = config.PreventWatchlistReAddition !== false;
document.querySelector('#watchlistMemoryRetentionDays').value = config.WatchlistMemoryRetentionDays || 365;
document.querySelector('#bookmarksEnabled').checked = config.BookmarksEnabled !== false;
document.querySelector('#bookmarksUsePluginPages').checked = config.BookmarksUsePluginPages === true;
document.querySelector('#useIcons').checked = config.UseIcons !== false;
document.querySelector('#iconStyle').value = config.IconStyle || 'emoji';
document.querySelector('#arrLinksEnabled').checked = config.ArrLinksEnabled;
document.querySelector('#sonarrUrlMappings').value = config.SonarrUrlMappings || '';
document.querySelector('#radarrUrlMappings').value = config.RadarrUrlMappings || '';
document.querySelector('#bazarrUrlMappings').value = config.BazarrUrlMappings || '';
document.querySelector('#sonarrUrl').value = config.SonarrUrl;
document.querySelector('#radarrUrl').value = config.RadarrUrl;
document.querySelector('#bazarrUrl').value = config.BazarrUrl;
document.querySelector('#showArrLinksAsText').checked = config.ShowArrLinksAsText;
document.querySelector('#arrTagsSyncEnabled').checked = config.ArrTagsSyncEnabled;
document.querySelector('#radarrApiKey').value = config.RadarrApiKey;
document.querySelector('#sonarrApiKey').value = config.SonarrApiKey;
document.querySelector('#arrTagsPrefix').value = config.ArrTagsPrefix || 'Requested by: ';
document.querySelector('#arrTagsClearOldTags').checked = config.ArrTagsClearOldTags !== false;
document.querySelector('#arrTagsShowAsLinks').checked = config.ArrTagsShowAsLinks !== false;
document.querySelector('#arrTagsSyncFilter').value = config.ArrTagsSyncFilter || '';
document.querySelector('#arrTagsLinksFilter').value = config.ArrTagsLinksFilter || '';
document.querySelector('#arrTagsLinksHideFilter').value = config.ArrTagsLinksHideFilter || '';
document.querySelector('#DefaultSubtitleStyle').value = config.DefaultSubtitleStyle;
document.querySelector('#DefaultSubtitleSize').value = config.DefaultSubtitleSize;
document.querySelector('#DefaultSubtitleFont').value = config.DefaultSubtitleFont;
document.querySelector('#disableCustomSubtitleStyles').checked = config.DisableCustomSubtitleStyles;
document.querySelector('#DefaultLanguage').value = config.DefaultLanguage || '';
document.querySelector('#letterboxdEnabled').checked = config.LetterboxdEnabled;
document.querySelector('#showLetterboxdLinkAsText').checked = config.ShowLetterboxdLinkAsText;
const metadataIconsChk = document.querySelector('#metadataIconsEnabled');
if (metadataIconsChk) metadataIconsChk.checked = !!config.MetadataIconsEnabled;
// Tie icon display settings
if (config.MetadataIconsEnabled) {
// Force icon display where applicable
const showLbText = document.querySelector('#showLetterboxdLinkAsText');
const showArrText = document.querySelector('#showArrLinksAsText');
if (showLbText) showLbText.checked = false;
if (showArrText) showArrText.checked = false;
}
// Load extras settings
document.querySelector('#coloredRatingsEnabled').checked = config.ColoredRatingsEnabled;
document.querySelector('#themeSelectorEnabled').checked = config.ThemeSelectorEnabled;
document.querySelector('#coloredActivityIconsEnabled').checked = config.ColoredActivityIconsEnabled;
document.querySelector('#pluginIconsEnabled').checked = config.PluginIconsEnabled;
// Requests Page settings
document.querySelector('#downloadsPageEnabled').checked = config.DownloadsPageEnabled !== false;
document.querySelector('#downloadsPageShowIssues').checked = config.DownloadsPageShowIssues === true;
document.querySelector('#downloadsUsePluginPages').checked = config.DownloadsUsePluginPages !== false;
document.querySelector('#downloadsUseCustomTabs').checked = config.DownloadsUseCustomTabs === true;
document.querySelector('#downloadsPagePollingEnabled').checked = config.DownloadsPagePollingEnabled !== false;
document.querySelector('#downloadsPollIntervalSeconds').value = (config.DownloadsPollIntervalSeconds !== undefined && config.DownloadsPollIntervalSeconds !== null) ? config.DownloadsPollIntervalSeconds : 30;
// Calendar Page settings
document.querySelector('#calendarPageEnabled').checked = config.CalendarPageEnabled !== false;
document.querySelector('#calendarUseCustomTabs').checked = config.CalendarUseCustomTabs === true;
document.querySelector('#calendarUsePluginPages').checked = config.CalendarUsePluginPages !== false;
document.querySelector('#calendarFirstDayOfWeek').value = config.CalendarFirstDayOfWeek || 'Monday';
document.querySelector('#calendarTimeFormat').value = config.CalendarTimeFormat || '5pm/5:30pm';
document.querySelector('#calendarHighlightFavorites').checked = config.CalendarHighlightFavorites || false;
document.querySelector('#calendarHighlightWatchedSeries').checked = config.CalendarHighlightWatchedSeries || false;
document.querySelector('#calendarShowOnlyRequested').checked = config.CalendarShowOnlyRequested || false;
// Hidden Content settings
document.querySelector('#hiddenContentEnabled').checked = config.HiddenContentEnabled || false;
document.querySelector('#hiddenContentUsePluginPages').checked = config.HiddenContentUsePluginPages === true;
document.querySelector('#hiddenContentUseCustomTabs').checked = config.HiddenContentUseCustomTabs === true;
document.querySelector('#loginImageEnabled').checked = config.EnableLoginImage || false;
document.querySelector('#customPluginLinks').value = config.CustomPluginLinks || '';
// Set up event handler for watchlist prevention checkbox
function toggleWatchlistRetentionVisibility() {
const preventionEnabled = document.querySelector('#preventWatchlistReAddition').checked;
const retentionContainer = document.querySelector('#watchlistMemoryRetentionDays').closest('.inputContainer');
if (retentionContainer) {
retentionContainer.style.display = preventionEnabled ? 'block' : 'none';
}
}
// Set initial visibility
toggleWatchlistRetentionVisibility();
// Add event listener
document.querySelector('#preventWatchlistReAddition').addEventListener('change', toggleWatchlistRetentionVisibility);
Dashboard.hideLoadingMsg();
});
}
async function buildConfigFromForm() {
const config = await ApiClient.getPluginConfiguration(pluginId);
const finalShortcuts = [...defaultShortcuts];
shortcutOverrides.forEach(override => {
const index = finalShortcuts.findIndex(s => s.Name === override.Name);
if (index !== -1) finalShortcuts[index] = override;
});
config.Shortcuts = finalShortcuts;
config.EnableCustomSplashScreen = document.querySelector('#enableCustomSplashScreen').checked;
config.SplashScreenImageUrl = document.querySelector('#splashScreenImageUrl').value;
config.DisableAllShortcuts = document.querySelector('#disableAllShortcuts').checked;
config.ToastDuration = parseInt(document.querySelector('#ToastDuration').value, 10);
config.HelpPanelAutocloseDelay = parseInt(document.querySelector('#HelpPanelAutocloseDelay').value, 10);
config.ElsewhereEnabled = document.querySelector('#elsewhereEnabled').checked;
config.TMDB_API_KEY = document.querySelector('#TMDB_API_KEY').value;
config.DEFAULT_REGION = document.querySelector('#DEFAULT_REGION').value;
config.DEFAULT_PROVIDERS = document.querySelector('#DEFAULT_PROVIDERS').value;
config.IGNORE_PROVIDERS = document.querySelector('#IGNORE_PROVIDERS').value;
config.ElsewhereCustomBrandingText = document.querySelector('#ElsewhereCustomBrandingText').value;
config.ElsewhereCustomBrandingImageUrl = document.querySelector('#ElsewhereCustomBrandingImageUrl').value;
config.AutoPauseEnabled = document.querySelector('#autoPauseEnabled').checked;
config.AutoResumeEnabled = document.querySelector('#autoResumeEnabled').checked;
config.AutoPipEnabled = document.querySelector('#autoPipEnabled').checked;
config.AutoSkipIntro = document.querySelector('#autoSkipIntro').checked;
config.AutoSkipOutro = document.querySelector('#autoSkipOutro').checked;
config.LongPress2xEnabled = document.querySelector('#longPress2xEnabled').checked;
config.RandomButtonEnabled = document.querySelector('#randomButtonEnabled').checked;
config.RandomIncludeMovies = document.querySelector('#randomIncludeMovies').checked;
config.RandomIncludeShows = document.querySelector('#randomIncludeShows').checked;
config.RandomUnwatchedOnly = document.querySelector('#randomUnwatchedOnly').checked;
config.ShowWatchProgress = document.querySelector('#showWatchProgress').checked;
config.WatchProgressDefaultMode = document.querySelector('#watchProgressDefaultMode').value;
config.WatchProgressTimeFormat = document.querySelector('#watchProgressTimeFormat').value;
config.ShowFileSizes = document.querySelector('#showFileSizes').checked;
config.ShowAudioLanguages = document.querySelector('#showAudioLanguages').checked;
config.RemoveContinueWatchingEnabled = document.querySelector('#removeContinueWatchingEnabled').checked;
config.QualityTagsEnabled = document.querySelector('#qualityTagsEnabled').checked;
config.GenreTagsEnabled = document.querySelector('#genreTagsEnabled').checked;
config.LanguageTagsEnabled = document.querySelector('#languageTagsEnabled').checked;
config.RatingTagsEnabled = document.querySelector('#ratingTagsEnabled').checked;
config.PeopleTagsEnabled = document.querySelector('#peopleTagsEnabled').checked;
config.DisableTagsOnSearchPage = document.querySelector('#disableTagsOnSearchPage').checked;
config.QualityTagsPosition = document.querySelector('#qualityTagsPosition').value;
config.GenreTagsPosition = document.querySelector('#genreTagsPosition').value;
config.LanguageTagsPosition = document.querySelector('#languageTagsPosition').value;
config.RatingTagsPosition = document.querySelector('#ratingTagsPosition').value;
config.ShowRatingInPlayer = document.querySelector('#showRatingInPlayer').checked;
config.TagsCacheTtlDays = parseInt(document.querySelector('#tagsCacheTtlDays').value, 10) || 30;
config.PauseScreenEnabled = document.querySelector('#pauseScreenEnabled').checked;
config.ShowReviews = document.querySelector('#showReviews').checked;
config.ReviewsExpandedByDefault = document.querySelector('#reviewsExpandedByDefault').checked;
config.JellyseerrEnabled = document.querySelector('#jellyseerrEnabled').checked;
config.ShowCollectionsInSearch = document.querySelector('#showCollectionsInSearch').checked;
config.JellyseerrEnable4KRequests = document.querySelector('#jellyseerrEnable4kRequests').checked;
config.JellyseerrUseMoreInfoModal = document.querySelector('#jellyseerrUseMoreInfoModal').checked;
config.JellyseerrShowAdvanced = document.querySelector('#jellyseerrShowAdvanced').checked;
config.JellyseerrShowSimilar = document.querySelector('#jellyseerrShowSimilar').checked;
config.JellyseerrShowRecommended = document.querySelector('#jellyseerrShowRecommended').checked;
config.JellyseerrExcludeLibraryItems = document.querySelector('#jellyseerrExcludeLibraryItems').checked;
config.JellyseerrExcludeBlocklistedItems = document.querySelector('#jellyseerrExcludeBlocklistedItems').checked;
config.JellyseerrDisableCache = document.querySelector('#jellyseerrDisableCache').checked;
config.JellyseerrResponseCacheTtlMinutes = parseInt(document.querySelector('#jellyseerrResponseCacheTtlMinutes').value, 10) || 10;
config.JellyseerrUserIdCacheTtlMinutes = parseInt(document.querySelector('#jellyseerrUserIdCacheTtlMinutes').value, 10) || 30;
config.JellyseerrShowNetworkDiscovery = document.querySelector('#jellyseerrShowNetworkDiscovery').checked;
config.JellyseerrShowGenreDiscovery = document.querySelector('#jellyseerrShowGenreDiscovery').checked;
config.JellyseerrShowTagDiscovery = document.querySelector('#jellyseerrShowTagDiscovery').checked;
config.JellyseerrShowPersonDiscovery = document.querySelector('#jellyseerrShowPersonDiscovery').checked;
config.JellyseerrShowCollectionDiscovery = document.querySelector('#jellyseerrShowCollectionDiscovery').checked;
config.JellyseerrShowReportButton = document.querySelector('#jellyseerrShowReportButton').checked;
config.ShowElsewhereOnJellyseerr = document.querySelector('#showElsewhereOnJellyseerr').checked;
config.JellyseerrUrls = (document.querySelector('#jellyseerrUrls').value || '').split('\n').map(u => u.trim()).filter(Boolean).join('\n');
config.JellyseerrApiKey = (document.querySelector('#JellyseerrApiKey').value || '').trim();
config.JellyseerrUrlMappings = (document.querySelector('#jellyseerrUrlMappings').value || '').split('\n').map(u => u.trim()).filter(Boolean).join('\n');
config.TMDB_API_KEY = document.querySelector('#jellyseerr_TMDB_API_KEY').value;
config.AutoSeasonRequestEnabled = document.querySelector('#autoSeasonRequestEnabled').checked;
config.AutoSeasonRequestRequireAllWatched = document.querySelector('#autoSeasonRequestRequireAllWatched').checked;
config.AutoSeasonRequestThresholdValue = parseInt(document.querySelector('#autoSeasonRequestThresholdValue').value, 10) || 2;
config.AutoMovieRequestEnabled = document.querySelector('#autoMovieRequestEnabled').checked;
const onStart = document.querySelector('#autoMovieRequestTriggerOnStart').checked;
const onMinutes = document.querySelector('#autoMovieRequestTriggerOnMinutesWatched').checked;
if (onStart && onMinutes) {
config.AutoMovieRequestTriggerType = 'Both';
} else if (onStart) {
config.AutoMovieRequestTriggerType = 'OnStart';
} else if (onMinutes) {
config.AutoMovieRequestTriggerType = 'OnMinutesWatched';
} else {
config.AutoMovieRequestTriggerType = 'OnMinutesWatched'; // Default to minutes watched if nothing selected
}
const minutesValue = parseInt(document.querySelector('#autoMovieRequestMinutesWatched').value, 10);
config.AutoMovieRequestMinutesWatched = isNaN(minutesValue) || minutesValue < 1 ? 20 : Math.min(minutesValue, 180);
config.AutoMovieRequestCheckReleaseDate = document.querySelector('#autoMovieRequestCheckReleaseDate').checked;
config.AddRequestedMediaToWatchlist = document.querySelector('#addRequestedMediaToWatchlist').checked;
config.SyncJellyseerrWatchlist = document.querySelector('#syncJellyseerrWatchlist').checked;
config.PreventWatchlistReAddition = document.querySelector('#preventWatchlistReAddition').checked;
const retentionDays = parseInt(document.querySelector('#watchlistMemoryRetentionDays').value);
config.WatchlistMemoryRetentionDays = isNaN(retentionDays) || retentionDays < 1 ? 365 : Math.min(retentionDays, 3650);
config.BookmarksEnabled = document.querySelector('#bookmarksEnabled').checked;
config.BookmarksUsePluginPages = document.querySelector('#bookmarksUsePluginPages').checked;
config.UseIcons = document.querySelector('#useIcons').checked;
config.IconStyle = document.querySelector('#iconStyle').value;
config.ArrLinksEnabled = document.querySelector('#arrLinksEnabled').checked;
config.SonarrUrlMappings = document.querySelector('#sonarrUrlMappings').value || '';
config.RadarrUrlMappings = document.querySelector('#radarrUrlMappings').value || '';
config.BazarrUrlMappings = document.querySelector('#bazarrUrlMappings').value || '';
config.SonarrUrl = document.querySelector('#sonarrUrl').value;
config.RadarrUrl = document.querySelector('#radarrUrl').value;
config.BazarrUrl = document.querySelector('#bazarrUrl').value;
config.ShowArrLinksAsText = document.querySelector('#showArrLinksAsText').checked;
config.ArrTagsSyncEnabled = document.querySelector('#arrTagsSyncEnabled').checked;
config.RadarrApiKey = document.querySelector('#radarrApiKey').value;
config.SonarrApiKey = document.querySelector('#sonarrApiKey').value;
config.ArrTagsPrefix = document.querySelector('#arrTagsPrefix').value || 'Requested by: ';
config.ArrTagsClearOldTags = document.querySelector('#arrTagsClearOldTags').checked;
config.ArrTagsShowAsLinks = document.querySelector('#arrTagsShowAsLinks').checked;
config.ArrTagsSyncFilter = document.querySelector('#arrTagsSyncFilter').value || '';
config.ArrTagsLinksFilter = document.querySelector('#arrTagsLinksFilter').value || '';
config.ArrTagsLinksHideFilter = document.querySelector('#arrTagsLinksHideFilter').value || '';
config.DefaultSubtitleStyle = parseInt(document.querySelector('#DefaultSubtitleStyle').value, 10);
config.DefaultSubtitleSize = parseInt(document.querySelector('#DefaultSubtitleSize').value, 10);
config.DefaultSubtitleFont = parseInt(document.querySelector('#DefaultSubtitleFont').value, 10);
config.DisableCustomSubtitleStyles = document.querySelector('#disableCustomSubtitleStyles').checked;
config.DefaultLanguage = document.querySelector('#DefaultLanguage').value || '';
config.LetterboxdEnabled = document.querySelector('#letterboxdEnabled').checked;
config.ShowLetterboxdLinkAsText = document.querySelector('#showLetterboxdLinkAsText').checked;
config.MetadataIconsEnabled = document.querySelector('#metadataIconsEnabled').checked;
// If metadata icons are enabled, ensure icons are shown for Letterboxd and *arr links
if (config.MetadataIconsEnabled) {
config.ShowLetterboxdLinkAsText = false;
config.ShowArrLinksAsText = false;
}
// Extras settings
config.ColoredRatingsEnabled = document.querySelector('#coloredRatingsEnabled').checked;
config.ThemeSelectorEnabled = document.querySelector('#themeSelectorEnabled').checked;
config.ColoredActivityIconsEnabled = document.querySelector('#coloredActivityIconsEnabled').checked;
config.PluginIconsEnabled = document.querySelector('#pluginIconsEnabled').checked;
// Requests Page settings
config.DownloadsPageEnabled = document.querySelector('#downloadsPageEnabled').checked;
config.DownloadsPageShowIssues = document.querySelector('#downloadsPageShowIssues').checked;
config.DownloadsUsePluginPages = document.querySelector('#downloadsUsePluginPages').checked;
config.DownloadsPagePollingEnabled = document.querySelector('#downloadsPagePollingEnabled').checked;
const pollInterval = parseInt(document.querySelector('#downloadsPollIntervalSeconds').value, 10);
config.DownloadsPollIntervalSeconds = pollInterval >= 30 ? pollInterval : 30;
config.DownloadsUseCustomTabs = document.querySelector('#downloadsUseCustomTabs').checked;
// Calendar Page settings
config.CalendarPageEnabled = document.querySelector('#calendarPageEnabled').checked;
config.CalendarUseCustomTabs = document.querySelector('#calendarUseCustomTabs').checked;
config.CalendarUsePluginPages = document.querySelector('#calendarUsePluginPages').checked;
config.CalendarFirstDayOfWeek = document.querySelector('#calendarFirstDayOfWeek').value || 'Monday';
config.CalendarTimeFormat = document.querySelector('#calendarTimeFormat').value || '5pm/5:30pm';
config.CalendarHighlightFavorites = document.querySelector('#calendarHighlightFavorites').checked;
config.CalendarHighlightWatchedSeries = document.querySelector('#calendarHighlightWatchedSeries').checked;
config.CalendarShowOnlyRequested = document.querySelector('#calendarShowOnlyRequested').checked;
// Hidden Content settings
config.HiddenContentEnabled = document.querySelector('#hiddenContentEnabled').checked;
config.HiddenContentUsePluginPages = document.querySelector('#hiddenContentUsePluginPages').checked;
config.HiddenContentUseCustomTabs = document.querySelector('#hiddenContentUseCustomTabs').checked;
config.EnableLoginImage = document.querySelector('#loginImageEnabled').checked;
config.CustomPluginLinks = document.querySelector('#customPluginLinks').value || '';
return config;
}
async function saveConfig(e) {
e.preventDefault();
Dashboard.showLoadingMsg();
try {
const config = await buildConfigFromForm();
const result = await ApiClient.updatePluginConfiguration(pluginId, config);
Dashboard.processPluginConfigurationUpdateResult(result);
} catch {
Dashboard.hideLoadingMsg();
}
return false;
}
// Saves current config and applies it to all users
async function resetAllUserSettings() {
if (confirm("Are you sure?\n\nThis will save the current configuration overwriting the above settings to ALL users on this server.")) {
Dashboard.showLoadingMsg();
try {
// First, save the current configuration
const config = await buildConfigFromForm();
await ApiClient.updatePluginConfiguration(pluginId, config);
// Then reset all user settings to match the saved config
await ApiClient.ajax({
type: 'POST',
url: ApiClient.getUrl('/JellyfinEnhanced/reset-all-users-settings'),
dataType: 'json'
});
Dashboard.hideLoadingMsg();
Dashboard.alert({
title: 'Success',
message: 'Configuration saved and applied to all users successfully!\n\nSettings will take effect after users refresh their browsers.'
});
} catch (e) {
Dashboard.hideLoadingMsg();
console.error('Failed to save and apply settings:', e);
Dashboard.alert({
title: 'Error',
message: 'Failed to save and apply settings to all users. Check server logs for details.'
});
}
}
}
// Setup branding file uploads and previews
function setupBrandingUploads() {
const uploadConfigs = [
{ inputId: 'iconTransparentInput', dropZoneId: 'iconTransparentDropZone', statusId: 'iconTransparentStatus', fileName: 'icon-transparent.png', previewId: 'iconTransparentPreview', placeholderId: 'iconTransparentPlaceholder', deleteId: 'iconTransparentDelete', dimensionsId: 'iconTransparentDimensions' },
{ inputId: 'faviconInput', dropZoneId: 'faviconDropZone', statusId: 'faviconStatus', fileName: 'favicon.ico', previewId: 'faviconPreview', placeholderId: 'faviconPlaceholder', deleteId: 'faviconDelete', dimensionsId: 'faviconDimensions' },
{ inputId: 'bannerLightInput', dropZoneId: 'bannerLightDropZone', statusId: 'bannerLightStatus', fileName: 'banner-light.png', previewId: 'bannerLightPreview', placeholderId: 'bannerLightPlaceholder', deleteId: 'bannerLightDelete', dimensionsId: 'bannerLightDimensions' },
{ inputId: 'bannerDarkInput', dropZoneId: 'bannerDarkDropZone', statusId: 'bannerDarkStatus', fileName: 'banner-dark.png', previewId: 'bannerDarkPreview', placeholderId: 'bannerDarkPlaceholder', deleteId: 'bannerDarkDelete', dimensionsId: 'bannerDarkDimensions' },
{ inputId: 'touchiconInput', dropZoneId: 'touchiconDropZone', statusId: 'touchiconStatus', fileName: 'apple-touch-icon.png', previewId: 'touchiconPreview', placeholderId: 'touchiconPlaceholder', deleteId: 'touchiconDelete', dimensionsId: 'touchiconDimensions' }
];
uploadConfigs.forEach(config => {
const input = document.getElementById(config.inputId);
const dropZone = document.getElementById(config.dropZoneId);
const statusDiv = document.getElementById(config.statusId);
const previewImg = document.getElementById(config.previewId);
const placeholder = document.getElementById(config.placeholderId);
const deleteButton = document.getElementById(config.deleteId);
if (!input || !dropZone || !statusDiv) return;
// Click to upload (ignore clicks on Delete button)
dropZone.addEventListener('click', (e) => {
if (e.target && e.target.closest && e.target.closest('button')) return;
input.click();
});
// Handle file selection
input.addEventListener('change', (e) => {
if (e.target.files.length > 0) {
uploadBrandingImage(e.target.files[0], config, statusDiv);
}
});
// Drag and drop
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.style.borderColor = '#00a4dc';
dropZone.style.backgroundColor = 'rgba(0,164,220,0.1)';
});
dropZone.addEventListener('dragleave', (e) => {
e.preventDefault();
dropZone.style.borderColor = 'rgba(0,164,220,0.5)';
dropZone.style.backgroundColor = 'rgba(255,255,255,0.05)';
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.style.borderColor = 'rgba(0,164,220,0.5)';
dropZone.style.backgroundColor = 'rgba(255,255,255,0.05)';
if (e.dataTransfer.files.length > 0) {
uploadBrandingImage(e.dataTransfer.files[0], config, statusDiv);
}
});
if (deleteButton) {
deleteButton.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
deleteBrandingImage(config, statusDiv);
});
}
// Load existing preview if present
refreshBrandingPreview(config);
});
}
async function uploadBrandingImage(file, config, statusDiv) {
// Validate that it's an image file
if (!file.type || !file.type.startsWith('image/')) {
statusDiv.textContent = '✗ Only image files allowed';
statusDiv.style.color = '#ff6b6b';
return;
}
const maxFileSize = 10 * 1024 * 1024; // 10MB
if (file.size > maxFileSize) {
statusDiv.textContent = `✗ File too large (max ${maxFileSize / (1024 * 1024)}MB)`;
statusDiv.style.color = '#ff6b6b';
return;
}
// Show image preview and dimensions
const previewImg = document.getElementById(config.previewId);
const dimensionsDiv = document.getElementById(config.dimensionsId);
const placeholder = document.getElementById(config.placeholderId);
if (previewImg) {
const objectUrl = URL.createObjectURL(file);
previewImg.src = objectUrl;
previewImg.style.display = 'block';
if (placeholder) placeholder.style.display = 'none';
// Get image dimensions
previewImg.onload = function() {
const img = new Image();
img.onload = function() {
if (dimensionsDiv) {
dimensionsDiv.textContent = `${img.width} × ${img.height}px`;
dimensionsDiv.style.display = 'block';
}
URL.revokeObjectURL(objectUrl);
};
img.src = objectUrl;
};
}
statusDiv.textContent = 'Uploading...';
statusDiv.style.color = '#ffa500';
try {
const formData = new FormData();
const renamedFile = new File([file], config.fileName, { type: file.type });
formData.append('file', renamedFile);
formData.append('fileName', config.fileName);
const token = ApiClient.accessToken ? ApiClient.accessToken() : '';
const response = await fetch(ApiClient.getUrl('/JellyfinEnhanced/UploadBrandingImage'), {
method: 'POST',
body: formData,
headers: {
'X-MediaBrowser-Token': token
}
});
if (response.ok) {
statusDiv.textContent = '✓ Uploaded';
statusDiv.style.color = '#51cf66';
await refreshBrandingPreview(config);
setTimeout(() => { statusDiv.textContent = ''; }, 3000);
} else {
const error = await response.text();
statusDiv.textContent = `${error || 'Upload failed'}`;
statusDiv.style.color = '#ff6b6b';
}
} catch (error) {
console.error('Upload exception:', error);
statusDiv.textContent = `${error.message || 'Upload error'}`;
statusDiv.style.color = '#ff6b6b';
}
}
async function refreshBrandingPreview(config) {
const previewImg = document.getElementById(config.previewId);
const placeholder = document.getElementById(config.placeholderId);
const deleteButton = document.getElementById(config.deleteId);
const dimensionsDiv = document.getElementById(config.dimensionsId);
if (!previewImg) return;
const token = ApiClient.accessToken ? ApiClient.accessToken() : '';
try {
const response = await fetch(ApiClient.getUrl('/JellyfinEnhanced/BrandingImage', { fileName: config.fileName, t: Date.now() }), {
headers: { 'X-MediaBrowser-Token': token }
});
if (!response.ok) {
previewImg.style.display = 'none';
if (placeholder) placeholder.style.display = 'block';
if (deleteButton) deleteButton.style.display = 'none';
if (dimensionsDiv) dimensionsDiv.style.display = 'none';
return;
}
const blob = await response.blob();
const objectUrl = URL.createObjectURL(blob);
previewImg.src = objectUrl;
previewImg.style.display = 'block';
if (placeholder) placeholder.style.display = 'none';
if (deleteButton) deleteButton.style.display = 'inline-block';
// Show dimensions for existing images
if (dimensionsDiv) {
previewImg.onload = function() {
dimensionsDiv.textContent = previewImg.naturalWidth + ' × ' + previewImg.naturalHeight + 'px';
dimensionsDiv.style.display = 'block';
};
}
} catch (err) {
previewImg.style.display = 'none';
if (placeholder) placeholder.style.display = 'block';
if (deleteButton) deleteButton.style.display = 'none';
if (dimensionsDiv) dimensionsDiv.style.display = 'none';
}
}
async function deleteBrandingImage(config, statusDiv) {
statusDiv.textContent = 'Deleting...';
statusDiv.style.color = '#ffa500';
const formData = new FormData();
formData.append('fileName', config.fileName);
const token = ApiClient.accessToken ? ApiClient.accessToken() : '';
try {
const response = await fetch(ApiClient.getUrl('/JellyfinEnhanced/DeleteBrandingImage'), {
method: 'POST',
body: formData,
headers: { 'X-MediaBrowser-Token': token }
});
if (response.ok) {
statusDiv.textContent = '✓ Deleted';
statusDiv.style.color = '#51cf66';
// Hide dimensions when image is deleted
const dimensionsDiv = document.getElementById(config.dimensionsId);
if (dimensionsDiv) dimensionsDiv.style.display = 'none';
await refreshBrandingPreview(config);
setTimeout(() => { statusDiv.textContent = ''; }, 2000);
} else {
const error = await response.text();
statusDiv.textContent = `${error || 'Delete failed'}`;
statusDiv.style.color = '#ff6b6b';
}
} catch (err) {
statusDiv.textContent = `${err.message || 'Delete error'}`;
statusDiv.style.color = '#ff6b6b';
}
}
setupBrandingUploads();
// Populate language options dynamically
(async () => {
const defaultLanguageSelect = document.getElementById('DefaultLanguage');
if (!defaultLanguageSelect) return;
const GITHUB_RAW_BASE = 'https://raw.githubusercontent.com/n00bcodr/Jellyfin-Enhanced/main/Jellyfin.Plugin.JellyfinEnhanced/js/locales';
const AVAILABLE_LANGUAGES_CACHE_KEY = 'JE_available_languages_config';
const AVAILABLE_LANGUAGES_CACHE_TS_KEY = 'JE_available_languages_config_ts';
const CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours
const CUSTOM_LANGUAGES = {
'pr': { Name: 'Pirate', DisplayName: 'Pirate', TwoLetterISOLanguageName: 'pr', LocaleCode: 'pr' },
'en-GB': { Name: 'English (United Kingdom)', DisplayName: 'English (United Kingdom)', TwoLetterISOLanguageName: 'en-GB', LocaleCode: 'en-GB' },
'en-US': { Name: 'English (United States)', DisplayName: 'English (United States)', TwoLetterISOLanguageName: 'en-US', LocaleCode: 'en-US' }
};
let supportedLanguages = [];
try {
// Check cache first
const cachedLanguages = localStorage.getItem(AVAILABLE_LANGUAGES_CACHE_KEY);
const cachedTimestamp = localStorage.getItem(AVAILABLE_LANGUAGES_CACHE_TS_KEY);
if (cachedLanguages && cachedTimestamp) {
const age = Date.now() - parseInt(cachedTimestamp, 10);
if (age < CACHE_DURATION) {
supportedLanguages = JSON.parse(cachedLanguages);
supportedLanguages.sort((a, b) => a.Name.localeCompare(b.Name));
supportedLanguages.forEach(culture => {
const option = document.createElement('option');
option.value = culture.LocaleCode || culture.TwoLetterISOLanguageName;
option.textContent = culture.DisplayName || culture.Name;
defaultLanguageSelect.appendChild(option);
});
return;
}
}
// Fetch cultures from Jellyfin API
const cultures = await ApiClient.ajax({
type: 'GET',
url: ApiClient.getUrl('/Localization/Cultures'),
dataType: 'json'
});
// Check which cultures have translation files on GitHub
const checkPromises = cultures.map(async (culture) => {
const langCode = culture.TwoLetterISOLanguageName;
// Skip generic "en"
if (langCode === 'en') {
return;
}
let normalizedLangCode;
if (langCode.includes('-')) {
const parts = langCode.split('-');
normalizedLangCode = parts[0].toLowerCase() + '-' + parts[1].toUpperCase();
} else {
normalizedLangCode = langCode.toLowerCase();
}
try {
const response = await fetch(GITHUB_RAW_BASE + '/' + normalizedLangCode + '.json', { method: 'HEAD' });
if (response.ok) {
supportedLanguages.push({
...culture,
LocaleCode: normalizedLangCode
});
}
} catch (err) {
// Translation file doesn't exist
}
});
await Promise.all(checkPromises);
// Always add English variants
supportedLanguages.push(CUSTOM_LANGUAGES['en-GB']);
supportedLanguages.push(CUSTOM_LANGUAGES['en-US']);
// Check other custom languages
for (const langCode in CUSTOM_LANGUAGES) {
// Skip English variants
if (langCode === 'en-GB' || langCode === 'en-US') {
continue;
}
let normalizedLangCode;
if (langCode.includes('-')) {
const parts = langCode.split('-');
normalizedLangCode = parts[0].toLowerCase() + '-' + parts[1].toUpperCase();
} else {
normalizedLangCode = langCode.toLowerCase();
}
try {
const response = await fetch(GITHUB_RAW_BASE + '/' + normalizedLangCode + '.json', { method: 'HEAD' });
if (response.ok) {
supportedLanguages.push(CUSTOM_LANGUAGES[langCode]);
}
} catch (err) {
// Translation file doesn't exist
}
}
// Cache the results
if (supportedLanguages.length > 0) {
try {
localStorage.setItem(AVAILABLE_LANGUAGES_CACHE_KEY, JSON.stringify(supportedLanguages));
localStorage.setItem(AVAILABLE_LANGUAGES_CACHE_TS_KEY, Date.now().toString());
} catch (err) {
// Ignore cache errors
}
}
} catch (err) {
// Failed to fetch languages
}
// Sort and populate dropdown
supportedLanguages.sort((a, b) => a.Name.localeCompare(b.Name));
supportedLanguages.forEach(culture => {
const option = document.createElement('option');
option.value = culture.LocaleCode || culture.TwoLetterISOLanguageName;
option.textContent = culture.DisplayName || culture.Name;
defaultLanguageSelect.appendChild(option);
});
})();
page.addEventListener('pageshow', loadConfig);
form.addEventListener('submit', saveConfig);
resetAllUserSettingsBtn.addEventListener('click', resetAllUserSettings);
// ================================
// TMDB API KEY VALIDATION
// ================================
/**
* Updates the state of TMDB-dependent settings based on whether a TMDB API key is present.
* Disables checkboxes and adds tooltips when no key is configured.
*/
function updateTmdbDependentSettings() {
const tmdbKey = document.querySelector('#TMDB_API_KEY').value.trim();
const jellyseerrTmdbKey = document.querySelector('#jellyseerr_TMDB_API_KEY').value.trim();
const hasTmdbKey = tmdbKey.length > 0 || jellyseerrTmdbKey.length > 0;
// List of TMDB-dependent setting IDs
const tmdbDependentSettings = [
{ id: 'elsewhereEnabled', tooltip: 'Requires TMDB API Key to be configured' },
{ id: 'showReviews', tooltip: 'Requires TMDB API Key to be configured' },
{ id: 'showCollectionsInSearch', tooltip: 'Requires TMDB API Key to be configured' },
{ id: 'showElsewhereOnJellyseerr', tooltip: 'Requires TMDB API Key to be configured' }
];
tmdbDependentSettings.forEach(setting => {
const checkbox = document.getElementById(setting.id);
if (!checkbox) return;
const label = checkbox.closest('label');
if (!label) return;
if (!hasTmdbKey) {
// Disable the checkbox and add visual indication
checkbox.disabled = true;
checkbox.checked = false;
label.style.opacity = '0.5';
label.style.cursor = 'not-allowed';
label.title = setting.tooltip;
// Add a visual indicator icon if not already present
if (!label.querySelector('.tmdb-required-icon')) {
const icon = document.createElement('i');
icon.className = 'material-icons tmdb-required-icon';
icon.textContent = 'key';
icon.style.cssText = 'font-size: 16px; vertical-align: middle; margin-left: 8px; color: #ff9800;';
icon.title = setting.tooltip;
label.querySelector('span').appendChild(icon);
}
} else {
// Enable the checkbox and remove visual indication
checkbox.disabled = false;
label.style.opacity = '1';
label.style.cursor = 'pointer';
label.title = '';
// Remove the visual indicator icon
const icon = label.querySelector('.tmdb-required-icon');
if (icon) {
icon.remove();
}
}
});
}
// Update on page load
setTimeout(updateTmdbDependentSettings, 100);
// Update when TMDB keys change
document.querySelector('#TMDB_API_KEY').addEventListener('input', updateTmdbDependentSettings);
document.querySelector('#jellyseerr_TMDB_API_KEY').addEventListener('input', updateTmdbDependentSettings);
// Update after successful TMDB test
const originalTestTmdb = testTmdbConnection;
testTmdbConnection = async function(event) {
await originalTestTmdb(event);
updateTmdbDependentSettings();
};
clearTagsCacheBtn.addEventListener('click', async () => {
if (confirm("Clear all client caches?\n\nThis will force all clients to clear their quality and genre tag caches on next page load.")) {
Dashboard.showLoadingMsg();
try {
const config = await ApiClient.getPluginConfiguration(pluginId);
config.ClearLocalStorageTimestamp = Date.now();
await ApiClient.updatePluginConfiguration(pluginId, config);
Dashboard.hideLoadingMsg();
Dashboard.alert({
title: 'Success',
message: 'Cache clear signal sent. All clients will clear their caches on next page load.'
});
} catch (e) {
Dashboard.hideLoadingMsg();
console.error('Failed to set cache clear timestamp:', e);
Dashboard.alert({
title: 'Error',
message: 'Failed to set cache clear timestamp. Check server logs for details.'
});
}
}
});
testJellyseerrBtn.addEventListener('click', testJellyseerrConnection);
// Custom Plugin Links functionality
const testCustomPluginLinksBtn = document.getElementById('testCustomPluginLinksBtn');
const customPluginLinksTextarea = document.getElementById('customPluginLinks');
if (testCustomPluginLinksBtn) {
testCustomPluginLinksBtn.addEventListener('click', async () => {
const linksText = customPluginLinksTextarea.value.trim();
if (!linksText) {
Dashboard.alert({
title: 'No Links',
message: 'Please add some custom plugin links first.'
});
return;
}
// Parse and validate the links
const lines = linksText.split('\n');
const validLinks = [];
const invalidLines = [];
lines.forEach((line, index) => {
const trimmedLine = line.trim();
if (!trimmedLine) return;
const parts = trimmedLine.split('|').map(part => part.trim());
if (parts.length >= 2 && parts[0] && parts[1]) {
validLinks.push({ name: parts[0], icon: parts[1] });
} else {
invalidLines.push(`Line ${index + 1}: "${trimmedLine}"`);
}
});
if (invalidLines.length > 0) {
Dashboard.alert({
title: 'Invalid Format',
message: `The following lines have invalid format:\n\n${invalidLines.join('\n')}\n\nPlease use the format: Configuration Page Name | icon_name`
});
return;
}
if (validLinks.length === 0) {
Dashboard.alert({
title: 'No Valid Links',
message: 'No valid plugin links found. Please check the format.'
});
return;
}
// Test the links by temporarily adding them to the sidebar
// Trigger the plugin icons script to refresh with test data
if (window.JellyfinEnhanced && window.JellyfinEnhanced.customPlugins) {
// Temporarily store test data
window.testCustomPluginLinks = validLinks;
window.JellyfinEnhanced.customPlugins.refresh();
}
});
}
// click handlers to all TMDB test buttons
document.querySelectorAll('.testTmdbBtn').forEach(btn => {
btn.addEventListener('click', testTmdbConnection);
});
// Copy button handler for HTML code snippets
document.querySelectorAll('.je-copy-html-btn').forEach(function(btn) {
btn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
const htmlCode = this.getAttribute('data-copy-text');
const btnText = this.querySelector('.copy-btn-text');
// Try clipboard API first
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(htmlCode).then(function() {
btnText.textContent = 'Copied!';
btn.style.color = '#4CAF50';
setTimeout(function() {
btnText.textContent = 'Copy';
btn.style.color = '';
}, 2000);
}).catch(function(err) {
console.error('Clipboard API failed: ', err);
fallbackCopy(htmlCode, btn, btnText);
});
} else {
// Fallback for older browsers
fallbackCopy(htmlCode, btn, btnText);
}
});
});
// Fallback copy method
function fallbackCopy(text, btn, btnText) {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
btnText.textContent = 'Copied!';
btn.style.color = '#4CAF50';
setTimeout(function() {
btnText.textContent = 'Copy';
btn.style.color = '';
}, 2000);
} catch (err) {
console.error('Fallback copy failed: ', err);
alert('Failed to copy to clipboard');
}
document.body.removeChild(textarea);
}
})();
</script>
</div>
</body>
</html>