Last active
October 19, 2024 13:22
-
-
Save danrahn/5a5703c1ea02a529f0179b64ff763495 to your computer and use it in GitHub Desktop.
Userscript that adjusts the width and height of dialogs in the Plex Web App, and remembers the last tab you selected
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name Better Plex Modal Dialogs | |
| // @version 0.4 | |
| // @description Improve modal dialogs in the Plex Web App | |
| // @author danrahn | |
| // @match https://app.plex.tv/* | |
| // @match http://localhost/* | |
| // @match http://127.0.0.1/* | |
| // @match https://customPlexDomain.example.com/* | |
| // @icon https://www.google.com/s2/favicons?sz=64&domain=plex.tv | |
| // @grant none | |
| // ==/UserScript== | |
| //////////////////// | |
| /// INSTRUCTIONS /// | |
| //////////////////// | |
| /** | |
| This script will work for most people without any additional adjustments, but can be configured to be more useful for your specific needs: | |
| * If you access the web app using an IP/domain that isn't app.plex.tv or localhost/127.0.0.1, add a @match item above with your | |
| specific hostname, following the existing pattern (e.g. "http://192.168.1.2/*", or "https://plex.domain.com/*") | |
| * If you run multiple services on any of the @matches above, add an entry to the HOST_MAP below, mapping the host name to the list of | |
| ports that Plex is listening on (e.g. "'plex.mydomain.com' : [80, 443]" if plex.mydomain.com forwards to your Plex server on the | |
| standard HTTP and HTTPS ports, or "'192.168.1.2' : [32400]" if your server's LAN IP is 192.168.1.2, and is listening on port 32400). | |
| * Tweak anything else between "Configurable Values" and "End of Configurable Values" below. | |
| NOTE: This script assumes a 'desktop' window - i.e. at least 768 pixels wide, as Plex adjusts the layout for small width screens. | |
| At best, this script won't do anything with small width windows. At worst, it will mess up the styling. | |
| */ | |
| (function() { | |
| 'use strict'; | |
| const DEBUG = true; | |
| const dbg = msg => { if (DEBUG) console.log(`[BetterPlexDialogs] ${msg}`); }; | |
| ////////////////////////////// | |
| /// Configurable Values | |
| //////////////////// | |
| /// Host mapping /// | |
| //////////////////// | |
| const HOST_MAP = { | |
| 'localhost' : [32400], | |
| '127.0.0.1' : [32400], | |
| 'LANIP' : [32400], // Just an example | |
| 'plex.mydomain.com' : [80, 433], // Just an example | |
| }; | |
| const hostPort = HOST_MAP[window.location.hostname]; | |
| if (hostPort && !hostPort.includes(+window.location.port)) { | |
| // The host is in our host map, which means we have to ensure the port is correct. Bail out early if it doesn't match | |
| dbg(`'${window.location.hostname}' found in host map, but port '${window.location.port}' not in [${hostPort.join(',')}], can't continue.`); | |
| return; | |
| } | |
| ////////////////////// | |
| /// Width settings /// | |
| ////////////////////// | |
| // Whether to enable Ctrl/Meta + Click on the dialog header to adjust its width. Otherwise the STATIC_WIDTH value set below. | |
| const ADJUSTABLE_WIDTH = true; | |
| // Custom widths to cycle through via Ctrl/Meta+[Shift+]Click. | |
| // Ideally in order from smallest to largest, but it doesn't have to be. | |
| const WIDTH_STOPS = [ | |
| '750px', // 0, default width for large modal dialogs | |
| '60%', // 1 | |
| '95%', // 2, maximum width | |
| ]; | |
| // The initial width of the dialog, as a 0-based index into WIDTH_STOPS (e.g. '1' means the second item in WIDTH_STOPS) | |
| const DEFAULT_STOP = 0; | |
| // Save the previous width for the next re-open | |
| const SAVE_STOP = true; | |
| // The transition time between widths. 0 for no animation. | |
| const ANIMATE_DURATION = 250; | |
| ////////////////////// | |
| /// Other Settings /// | |
| ////////////////////// | |
| // Custom width to set when ADJUSTABLE_WIDTH is false. | |
| const STATIC_WIDTH = '50%'; | |
| // Whether to shrink the top/bottom margins of the dialog from 70px down to 20px; | |
| const SMALLER_PADDING = true; | |
| // Wehther to automatically navigate to tab last selected in the last modal that was opened. | |
| const REMEMBER_SELECTED_TAB = true; | |
| ////////////////////////////// | |
| // End of Configurable Values | |
| ////////////////////////////////////////////////////////////////// | |
| // Do not edit anything below unless you know what you're doing // | |
| let currentStop = 1; | |
| let lastTab = -1; | |
| if (ADJUSTABLE_WIDTH || REMEMBER_SELECTED_TAB) { | |
| const onHeaderClick = e => { | |
| if ((!e.ctrlKey && !e.metaKey) || e.altKey) { | |
| return; | |
| } | |
| let modal = e.target; | |
| while (modal) { | |
| if (modal.classList.contains('modal-dialog')) { | |
| break; | |
| } | |
| modal = modal.parentElement; | |
| } | |
| if (!modal) { | |
| dbg(`Header clicked, but couldn't find modal-dialog. How did that happen?`); | |
| return; | |
| } | |
| // Reset any custom width if we're on a small screen, since the dialog | |
| // already uses all available width. Assumes the window isn't resized, | |
| // since I'm too lazy to add resize listeners. | |
| if (document.body.clientWidth < 768) { | |
| modal.style.removeProperty('width'); | |
| dbg(`Browser window width is less than 768 pixels, aborting dialog width adjustment`); | |
| return; | |
| } | |
| const oldWidth = WIDTH_STOPS[currentStop]; | |
| currentStop = (currentStop + (e.shiftKey ? -1 : 1) + WIDTH_STOPS.length) % WIDTH_STOPS.length; | |
| const newWidth = WIDTH_STOPS[currentStop]; | |
| if (isNaN(ANIMATE_DURATION) || ANIMATE_DURATION <= 0) { | |
| dbg(`Setting dialog width to ${newWidth}`); | |
| modal.style.width = newWidth; | |
| } else { | |
| dbg(`Animating dialog width from ${oldWidth} to ${newWidth}`); | |
| modal.animate([ | |
| { width : oldWidth }, | |
| { width : newWidth } | |
| ], ANIMATE_DURATION / 2).addEventListener('finish', () => { | |
| modal.style.width = newWidth; | |
| }); | |
| } | |
| }; | |
| const onNavClick = e => { | |
| let li = e.target; | |
| if (!['LI', 'A', 'I'].includes(li.tagName)) { | |
| dbg(`Nav click registered, but on an unexpected element type: ${li.tagName}`); | |
| return; | |
| } | |
| while (li && li.tagName != 'LI') { | |
| li = li.parentElement; | |
| } | |
| lastTab = li ? li.querySelector('a').getAttribute('data-pane') : -1; | |
| dbg(`Last tab set to ${lastTab}`); | |
| }; | |
| const nodeAddedCallback = (mutationList, _observer) => { | |
| for (const mutation of mutationList) { | |
| for (const node of mutation.addedNodes) { | |
| if (!node.classList?.contains('modal-dialog')) { | |
| continue; | |
| } | |
| if (ADJUSTABLE_WIDTH) { | |
| node.style.width = WIDTH_STOPS[SAVE_STOP ? currentStop : DEFAULT_STOP]; | |
| if (!SAVE_STOP) { currentStop = DEFAULT_STOP; } | |
| node.querySelector('.modal-header')?.addEventListener('click', onHeaderClick); | |
| } | |
| if (REMEMBER_SELECTED_TAB) { | |
| const nav = node.querySelector('.list.modal-nav-list'); | |
| nav?.addEventListener('click', onNavClick); | |
| if (nav && lastTab != -1) { | |
| const a = nav.querySelector(`a[data-pane="${lastTab}"]`)?.click(); | |
| } | |
| } | |
| } | |
| } | |
| }; | |
| new MutationObserver(nodeAddedCallback).observe(document.body, { attributes : false, childList : true, subtree : true }); | |
| } | |
| // ~66px each for the the dialog header/footer, and either 70 or 20px each, | |
| // for the top/bottom padding, depending on the SMALLER_PADDING setting. | |
| const heightAdjust = 132 + (SMALLER_PADDING ? 40 : 140); | |
| let style =` | |
| :root { | |
| --dialog-max-height: calc(100vh - ${heightAdjust}px); | |
| } | |
| .modal-body-scroll { | |
| max-height: var(--dialog-max-height); | |
| } | |
| .modal-body-with-panes .modal-body-pane { | |
| min-height: 400px; /* 400px was the static height previously. Make it the min height now */ | |
| height: auto; | |
| max-height: var(--dialog-max-height); | |
| } | |
| .modal-dialog { | |
| max-width: 95%; | |
| ${ADJUSTABLE_WIDTH ? '' : ' width: ' + STATIC_WIDTH + ' !important;'} | |
| } | |
| `; | |
| if (SMALLER_PADDING) { | |
| style += ` | |
| .modal-dialog { | |
| padding-top: 20px; | |
| padding-bottom: 20px; | |
| } | |
| `; | |
| } | |
| const ele = document.createElement('STYLE'); | |
| ele.innerText = style; | |
| document.body.appendChild(ele); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment