Video Area Inverter

Dark mode but for videos. Remembered your options per channel. ctrl+i to show/hide options, 'i' to toggle the area

// ==UserScript==
// @name         Video Area Inverter
// @namespace    twitch.tv/simplevar
// @version      2024-01-08
// @description  Dark mode but for videos. Remembered your options per channel. ctrl+i to show/hide options, 'i' to toggle the area
// @author       SimpleVar
// @match        https://www.youtube.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @license      The Unlicense
// @run-at       document-end
// @grant        none
// ==/UserScript==

(() => {
    'use strict';

    setTimeout(() =>
    {
        const style = document.createElement("style");
        document.head.appendChild(style); // must append before you can access sheet property
        style.sheet.addRule('.html5-video-container', 'height: 100% !important')
        style.sheet.addRule('.html5-video-player', 'overflow: visible !important; z-index: unset')
        style.sheet.addRule('#player-container', 'z-index: 1')
        style.sheet.addRule('#ytd-player', 'overflow: visible !important')
        style.sheet.addRule('body[data-darkverysimpleunique="x"] .darkverysimpleunique', 'opacity: 1')
        style.sheet.addRule('body[data-darkverysimpleuniqueui="x"] .darkverysimpleuniqueui', 'opacity: 1')
        style.sheet.addRule('.darkverysimpleuniqueinput', 'pointer-events: none; user-select: auto;')
        style.sheet.addRule('body[data-darkverysimpleuniqueui="x"] .darkverysimpleuniqueinput', 'pointer-events: auto;')
        style.sheet.addRule('.darkverysimpleunique', 'position: absolute; pointer-events: none; transition: opacity 200ms; opacity: 0; border: 3px solid black; backdrop-filter: invert(1); box-sizing: border-box; box-shadow: black 0 0 0 2px; border-radius: calc(var(--height) * var(--radius) * 0.1px); height: calc(var(--height) * 1%); aspect-ratio: var(--aspect); left: 50%; transform: translateX(calc(var(--left) / var(--aspect) * 1%))')
        style.sheet.addRule('.darkverysimpleuniqueui', 'position: absolute; pointer-events: none; user-select: none; transition: opacity 80ms; opacity: 0; bottom: 0; right: 0; transform: translateY(100%); display: flex; align-items: baseline; font-size: medium; background-color: rgba(10, 10, 10, 0.9); padding-bottom: 0.334em; color: #eee')
    }, 100)
    document.body.addEventListener('keydown', e => {
        const anyModifier = e.altKey | e.ctrlKey | e.shiftKey | e.metaKey
        if (e.key === 'i' && !anyModifier) {
            e.preventDefault()
            e.stopPropagation()
            const x = document.body.dataset.darkverysimpleunique === 'x'
            document.body.dataset.darkverysimpleunique = x ? '' : 'x'
        }
        if (e.key === 'i' && e.ctrlKey) {
            e.preventDefault()
            e.stopPropagation()
            const x = document.body.dataset.darkverysimpleuniqueui === 'x'
            document.body.dataset.darkverysimpleuniqueui = x ? '' : 'x'
        }
    }, {passive: false, capture: true})

    function waitTruthy(pollInterval, fn) {
        return new Promise((res, _) => {
            poll()
            function poll() {
                const x = fn()
                if (x) res(x)
                else setTimeout(poll, pollInterval)
            }
        })
    }

    async function waitEl(elOrSelector, predicate = undefined, pollInterval = 100, knownParent = undefined) {
        if (!(elOrSelector instanceof HTMLElement)) {
            knownParent ??= document
            elOrSelector = await waitTruthy(pollInterval, () => knownParent.querySelector(elOrSelector))
        }
        if (predicate) await waitTruthy(pollInterval, () => predicate(elOrSelector))
        return elOrSelector
    }

    ;(async () => {
        const ch = (await waitEl('.ytd-channel-name a.yt-formatted-string')).getAttribute('href')
        const vid = await waitEl('video')
        fixVideoElement(vid, ch)
    })();
    /*
    new MutationObserver(muts => {
         for (const m of muts) {
             for (const el of m.addedNodes) {
                 if (el.tagName === 'video') setTimeout(fixVideoElement, 1000, el)
             }
         }
    }).observe(document.body, { childList: true, subtree: true });
    */

    function fixVideoElement(el, ch) {
        if (el.__eww_9832475) return
        el.__eww_9832475 = true
        el.style.position = 'relative'
        const area = document.createElement('div')
        el.parentElement.appendChild(area)
        area.className = 'darkverysimpleunique'
        const ui = document.createElement('div')
        el.parentElement.parentElement.parentElement.appendChild(ui)
        ui.className = 'darkverysimpleuniqueui'
        const STOP = e => { e.stopPropagation() };
        const chStorageKey = 'darkverysimpleuniqueedges_' + ch
                debugger
        let memberedEdges = null
        try { memberedEdges = JSON.parse(localStorage.getItem(chStorageKey) ?? '{}') } catch {}
        if (!(memberedEdges instanceof Object)) memberedEdges = null
        const edges = Object.assign({left: 0, top: 0, height: 0, aspect: 0, radius: 0}, memberedEdges ?? {})
        const mkInp = (key, label, min, max) => {
            if (label) {
                const lbl = document.createElement('label')
                lbl.textContent = label
                lbl.style.marginLeft = '1ch'
                ui.appendChild(lbl)
            }
            const inp = document.createElement('input')
            ui.appendChild(inp)
            inp.className = 'darkverysimpleuniqueinput'
            inp.type = 'number'
            inp.min = min
            inp.max = max
            inp.step = 0.025
            inp.style.width = '6ch'
            inp.style.fontSize = 'inherit'
            const storageKey = 'darkverysimpleuniqueedge' + key
            inp.value = (memberedEdges && memberedEdges[key]) ?? localStorage.getItem(storageKey)
            if (!inp.value && inp.value !== 0) inp.value = key === 'aspect' ? 1 : 25
            const onChange = e => {
                e && STOP(e);
                edges[key] = inp.value
                if (key === 'radius') area.style.setProperty('--radius', +(inp.value ?? 25))
                else if (key === 'height') area.style.setProperty('--height', +(edges.height ?? 25))
                else if (key === 'aspect') area.style.setProperty('--aspect', +(inp.value ?? 100) * 0.01)
                else if (key === 'left') area.style.setProperty('--left', +(inp.value ?? 0))
                else area.style[key] = (50 - +(inp.value ?? 0)) + '%'
                localStorage.setItem(storageKey, inp.value)
                localStorage.setItem(chStorageKey, JSON.stringify(edges))
            };
            onChange()
            inp.__onChange = onChange
            const stoppedEvents = [
                'click', 'dblclick', 'auxclick', 'contextmenu', 'wheel', 'scroll', 'tap', 'pointerdown', 'pointerup', 'touchstart', 'mouseleave', 'mousedown',
                'panmove', 'panstart', 'panend', 'pinchin', 'pinchout', 'mouseover', 'mousemove', 'focusin', 'gesturechange', 'gestureend', 'keyup',
            ];
            for (const ev of stoppedEvents) inp.addEventListener('click', STOP)
            inp.addEventListener('keydown', e => {
                STOP(e)
                let dy = 0
                switch (e.key) {
                    case 'ArrowUp': dy = 1; break;
                    case 'ArrowDown': dy = -1; break;
                }
                if (dy) {
                    dy *= (e.ctrlKey | e.shiftKey) ? 5 : 0.1
                    inp.value = dy + +(inp.value ?? 0)
                }
            })
            inp.addEventListener('input', onChange)
            return inp
        };
        mkInp('top', 'T=', 0, 100)
        mkInp('height', 'H=', 0, 100)
        mkInp('left', 'L=', undefined, undefined)
        mkInp('aspect', 'W=', 0, undefined)
        mkInp('radius', 'radius=', 0, 50)
    }

})()