您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Speed control, precise aspect ratio control, nice 'n' accurate looper, volume booster, and more.
// ==UserScript== // @name Jupiter's Tools for YouTube // @namespace Violentmonkey Scripts // @license CC BY-SA // @version 1.0.2 // @description Speed control, precise aspect ratio control, nice 'n' accurate looper, volume booster, and more. // @description 2025-04-17 7:00 PM // @author Jupiter Liar // @match *://www.youtube.com/* // @grant none // @run-at document-start // ==/UserScript== (function () { "use strict"; let logging = false; function log(...args) { if (logging) { console.log(...args); } } log("YouTube Video Manipulator started..."); const head = document.head || document.getElementsByTagName("head")[0]; let video; let timeLoopDebug = false; let compressorTipDivTesting = false; let extraDebugButtons = false; let lastChecked = 0; let lockRate = null; // Variable to store the locked playback rate let playLock = false; let pauseLock = false; let rawAspectRatio; let aspectRatio; let savedAspectRatio; let aspectRatioSpecified = false; let nativeRatio; let zoom = 1; let isZoomed = false; let hideOverlaysVar = false; let flipRotateStyle; let videoZoomStyle; let overrideObjectFitStyle; let frameAspectStyle; let frameIsRescaled = false; let noScaleAndTop = false; let zoomInput; let buttonRefs = {}; let playbackRateLocked = false; let playbackRateForeverLocked = false; let speedLockCheckbox; let foreverLockCheckbox; let squareCorners = false; let squareCornersStyle; let noAspect = true; let customBlink; let startLoopingBlink; let resizeDefaultCheckbox; let resizeByDefault; let topRow; let actions; let chapters; let chapterMoving; let chapterPosition; let looping = false; let loopMode = "forever"; let lastCustom; let lastCustomRepeated = false; let lastCustomReset = false; let tipAspectStyle; let container; let containerWidth; let containerHeight; let containerRatio; frameAspectStyle = document.createElement("style"); frameAspectStyle.id = "frame-aspect-style"; head.appendChild(frameAspectStyle); const innerItemAfterStyles = document.createElement("style"); innerItemAfterStyles.id = "inner-item-after-styles"; head.appendChild(innerItemAfterStyles); tipAspectStyle = document.createElement("style"); tipAspectStyle.id = "tip-aspect-style"; head.appendChild(tipAspectStyle); let overlayHiderStyle; let controlPanelOuter; let controlPanel; let additionalStyles = document.createElement("style"); let videoControlsMasterHolder; let videoControlsShowHideMenu; let lockControlsOuterDiv; let playbackRateOuter; let videoSeekControlsOuterDiv; let cosmeticCheckboxesOuterDiv; let videoLoopControlsOuterDiv; let compressorControlsParentDiv; let aboutDiv; let chapterMoveAttempts = 0; let chapterMoveAttemptsMax = 15; // Declare an AudioContext &c. let audioContext = null; let preGain; let thresholdGain; let compressor; let finalGain; let source = null; let boostLockState = false; let establishChaptersAttempts; let maxEstablishChaptersAttempts; let establishChaptersAttemptsInterval; let establishChapters; let moveChapters; const generalStyles = ` :root { --video-manipulator-button-active: hsl(0, 100%, 85%); --video-manipulator-button-just-pressed: hsl(0, 100%, 75%); } ytd-watch-metadata #top-row #actions { margin-left: 8px; } ytd-watch-metadata #top-row ytd-menu-renderer { margin-right: 1em; } #video-controls-master-holder { display: flex; flex-direction: row; flex-wrap: wrap; gap: 10px 24px; } .video-manipulator-outer-div [invisible] { opacity: 0; } .video-manipulator-outer-div, .video-manipulator-outer-div * { color: inherit; --button-outline-op: .25; } .video-manipulator-outer-div { font-size: 14px; } .video-manipulator-outer-div * { font-size: inherit; } .video-manipulator-outer-div [hidden] { display: none; } .video-manipulator-outer-div button, .video-manipulator-outer-div input { cursor: pointer; } .video-manipulator-inner-item { display: flex; flex-wrap: wrap; gap: .5em; /* margin: auto; */ align-items: center; position: relative; } .video-manipulator-inner-item:after { content: ''; pointer-events: none; position: absolute; width: 100%; height: 100%; padding: 4px; left: -4px; top: -4px; border-radius: 8px; } .video-manipulator-inner-item:hover:after { box-shadow: 0px 0px 0px 2px hsla(240, 100%, 50%, var(--button-outline-op)); } .video-manipulator-sub-item { display: flex; flex-wrap: wrap; gap: .5em; align-items: center; } .video-manipulator-outer-div:hover button, button.video-manipulator-inner-item:hover { /* box-shadow: 0px 0px 0px 2px inset hsla(240, 100%, 50%, var(--button-outline-op)); */ } .video-manipulator-outer-div { /* margin-top: 12px; */ display: flex; /* margin-right: 1.5em; */ z-index: 1; } .video-manipulator-outer-div label, .video-manipulator-outer-div input[type="checkbox"], .video-manipulator-outer-div .video-manipulator-divider { /* font-size: 1.4rem; */ } #playback-rate-input { margin-right: .5em; } .video-manipulator-outer-div input[type="checkbox"] { margin: 0 .25em; } .video-manipulator-inner-item button, button.video-manipulator-inner-item { border-radius: 1em; border: unset; padding: 0.25em 0.75em; } .video-manipulator-inner-item button:not(:hover), button.video-manipulator-inner-item:not(:hover) { box-shadow: inset 0px 0px 0px 1px hsla(0, 0%, 50%, 0.065); } .video-manipulator-inner-item button:hover, button.video-manipulator-inner-item:hover { box-shadow: 0px 0px 0px 1px inset hsla(0, 0%, 50%, .75); } #video-display-controls #aspect-custom-input, #video-display-controls #zoom-input { width: 4em; text-align: center; } #video-display-controls span { /* font-size: 1.5rem; */ margin-left: .5em; } #playback-rate-controls input[type="number"] { width: 4em; text-align: center; padding: 0; /* border-radius: 1em; */ } .video-manipulator-inner-item label ~ label, .video-manipulator-inner-item ~ .video-manipulator-inner-item label { margin-left: .25em; } #playback-rate-controls label { padding-left: 0; } .video-manipulator-outer-div button.active { background-color: var(--video-manipulator-button-active); } @keyframes blink { 0% { background-color: var(--video-manipulator-button-color); } 50% { background-color: var(--video-manipulator-button-just-pressed); } 100% { background-color: var(--video-manipulator-button-color); } } .video-manipulator-outer-div button.blinking { animation: blink 1s infinite; } .video-manipulator-outer-div button.just-pressed, .video-manipulator-outer-div button.active.just-pressed { background-color: var(--video-manipulator-button-just-pressed); } #lock-controls .lock-button .lock-span { filter: drop-shadow(0px 0px .4px black) drop-shadow(0px 0px 0px black) drop-shadow(0px 0px 0px black) drop-shadow(0px 0px 0px black) drop-shadow(0px 0px 0.25px black) drop-shadow(0px 0px 0px black); } ytd-watch-metadata #top-row { display: flex; flex-wrap: wrap; } #compressor-controls-parent-div { z-index: 901; /* position: fixed; */ /* font-size: 16px; */ display: flex; flex-direction: column; gap: 0 .5em; /* display: none; */ width: 100%; } .compressor-control-div { display: flex; align-items: center; } .compressor-control-div label { width: 5.5em; } .compressor-control-div input { margin: 0 .5em; text-align: right; width: 3.5em; cursor: pointer; } .compressor-control-div .conversion-div { margin: 0 0.5em; font-size: 0.8em; } #compressor-controls-group-div, #compressor-control-tip-div, #compressor-control-tip-div-background { padding: 0.75em 1em; border-radius: 1em; } #compressor-control-tip-div { background: none; border: 3px solid transparent; } #compressor-controls-group-div, #compressor-control-tip-div-background { background: var(--yt-spec-base-background); border: 3px solid var(--video-manipulator-button-color, gray); } #compressor-control-tip-div-background { position: absolute; } #compressor-control-tip-div { max-width: 21.5em; position: absolute; } #compressor-control-tip-parent { } #compressor-controls-group-div { width: fit-content; } #compressor-simple-controls-div { display: flex; gap: .25em; } #compressor-simple-controls-div #simpleBoost-control-div { /* margin-right: 8px; */ } #compressor-advanced-controls-div { position: absolute; } #compressor-advanced-controls-container-div { } #compressor-controls-group-div, #compressor-control-tip-parent { margin-top: 0.25em; } .empty-spanner { width: 100%; height: 0; } .empty-spanner.line { margin: 10px 0; border-bottom: 1px solid var(--yt-spec-10-percent-layer); } #compressor-controls-parent-div:not(.active) > *:not(#compressor-simple-controls-div), #compressor-controls-parent-div:not(.active) #compressor-simple-controls-div > *:not(#compressor-boost-button), #compressor-advanced-controls-container-div:not(.active) { display: none; } #show-full-compressor-controls .down-expand-arrow { transition: scale 0.5s; } #show-full-compressor-controls.active .down-expand-arrow { scale: 1 -1; } #compressor-reset-button { position: absolute; bottom: 1.25em; right: 1.25em; } #boost-lock-div { display: flex; align-items: center; gap: 0.25em; } .video-manipulator-divider { width: 1px; height: 1em; background: var(--yt-spec-text-primary); } #video-seek-controls input[type="number"], #video-seek-controls input[type="text"], #video-loop-controls input[type="number"], #video-loop-controls input[type="text"] { width: 6.75em; text-align: center; } #video-loop-controls-outer-div { /* z-index: 9012; */ z-index: 2; } #video-seek-controls .video-manipulator-divider, #video-loop-controls .video-manipulator-divider, #video-display-controls .video-manipulator-divider { margin: 0 .25em; } #video-display-controls .video-manipulator-divider { } .video-manipulator-sub-item span { /* font-size: 1.4em; */ } #video-loop-controls #loop-times-input, #video-loop-controls #measure-rate-input { width: 3em; margin: 0 .25em; } .video-manipulator-inner-item .radio-div { display: flex; align-items: center; } .video-manipulator-inner-item .radio-div input[type="radio"] { margin-top: 0; } #looping-buttons-outer-div { flex-direction: column; position: relative; } #loop-error-holder-outer { position: absolute; bottom: 0; width: 100%; height: 0; left: 0; pointer-events: none; /* font-size: 1.4em; */ transition: opacity 0.5s; z-index: 9020; } #loop-error-holder-inner { margin-top: 0.5em; position: absolute; width: fit-content; display: block; } #loop-error-pre-span { background: var(--yt-spec-base-background); border: 3px solid var(--video-manipulator-button-color, gray); position: absolute; padding: 0.25em .5em; border-radius: 1em; z-index: 1; top: -2px; left: -2px; } #loop-error-span { /* border: 3px solid transparent; */ position: relative; display: inline; top: .25em; left: .5em; border-radius: 1em; z-index: 1; } #video-controls-show-hide-menu { margin-top: 12px; } #video-controls-show-hide-menu.video-manipulator-outer-div button.inactive:not(.just-pressed) { filter: contrast(0); box-shadow: inset 0px 0px 0px 1px hsla(0, 0%, 50%, .5); } #video-controls-show-hide-menu-inner.video-manipulator-inner-item button { border-radius: 0.75em; border-radius: 0em; font-size: .95em; padding: 0.2em 1.5em; /* border: 1px solid hsla(0, 0%, 0%, .20); */ } #video-controls-show-hide-menu-inner.video-manipulator-inner-item { gap: 0.25em; } #linktree-button { all: unset; background: #41df5d; height: 24px; width: 24px; border-radius: 20%; cursor: pointer; } #linktree-button svg { aspect-ratio: 1; padding: 15%; } #jl-about-div { font-size: 14px; display: flex; align-items: center; } #jl-about-span { margin-right: .5em; } #jl-about-span a { text-decoration: none; color: green; font-weight: bold; letter-spacing: .25px; } #compressor-control-tip-paragraph { display: inline; } #video-controls-manual { font-size: 14px; } #video-controls-manual h2 { } #video-controls-manual p { margin: 0.5em 1.5em; } `; // Default sections to hide, global variable let sectionsToHide = [ "video-seek-controls-outer-div", "video-loop-controls-outer-div", "cosmetic-checkboxes-outer-div", "compressor-controls-parent-div", "video-controls-manual", "jl-about-div" ]; // Global stylesheet constant let sectionsToHideStyle; // Load the saved sections from localStorage function loadSectionsState() { const storedState = localStorage.getItem("videoSectionsState"); if (storedState) { sectionsToHide = JSON.parse(storedState); } } // Save the sections state to localStorage function saveSectionsState() { localStorage.setItem("videoSectionsState", JSON.stringify(sectionsToHide)); } // Build the stylesheet based on sectionsToHide function buildSectionHiderStylesheet() { // Directly use sectionsToHide to create the CSS rules const hiddenSections = sectionsToHide .map((id) => `#${id}`) // Format the IDs as CSS selectors .join(", "); // Apply the styles to hide those sections sectionsToHideStyle.textContent = `${hiddenSections} { display: none; }`; } loadSectionsState(); function createDivider() { const divider = document.createElement("div"); divider.classList.add("video-manipulator-divider"); return divider; } function createVideoControlsMasterHolder() { videoControlsMasterHolder = document.createElement("div"); videoControlsMasterHolder.id = "video-controls-master-holder"; topRow.appendChild(videoControlsMasterHolder); } function insertAboutDiv() { aboutDiv = document.createElement("div"); aboutDiv.id = "jl-about-div"; aboutDiv.classList.add("video-manipulator-outer-div"); const aboutText = "This script was created by Jupiter Liar, and is licensed CC-BY-SA. " + "More projects can be found at my "; const linkTreeLink = document.createElement("a"); linkTreeLink.href = "https://linktr.ee/jupiterliar"; linkTreeLink.textContent = "Linktree"; const aboutSpan = document.createElement("span"); aboutSpan.id = "jl-about-span"; // aboutSpan.textContent = aboutText; const aboutTextNode = document.createTextNode(aboutText); const periodTextNode = document.createTextNode("."); const linktreeButton = document.createElement("button"); linktreeButton.id = "linktree-button"; linktreeButton.innerHTML = ` <svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 80 97.7" style="enable-background:new 0 0 80 97.7;" xml:space="preserve"> <path d="M0.2,33.1h24.2L7.1,16.7l9.5-9.6L33,23.8V0h14.2v23.8L63.6,7.1l9.5,9.6L55.8,33H80v13.5H55.7l17.3,16.7l-9.5,9.4L40,49.1 L16.5,72.7L7,63.2l17.3-16.7H0V33.1H0.2z M33.1,65.8h14.2v32H33.1V65.8z"> </path> </svg> `; linktreeButton.addEventListener("click", () => { window.open("https://linktr.ee/jupiterliar", "_blank"); }); aboutSpan.appendChild(aboutTextNode); aboutSpan.appendChild(linkTreeLink); aboutSpan.appendChild(periodTextNode); aboutDiv.appendChild(aboutSpan); aboutDiv.appendChild(linktreeButton); videoControlsMasterHolder.appendChild(aboutDiv); } function insertVideoControlsShowHideMenu() { // Create the video controls show/hide menu videoControlsShowHideMenu = document.createElement("div"); videoControlsShowHideMenu.id = "video-controls-show-hide-menu"; videoControlsShowHideMenu.classList.add("video-manipulator-outer-div"); // Append the menu to the body or another container topRow.appendChild(videoControlsShowHideMenu); } // Function to populate the Show/Hide menu function populateVideoControlsShowHideMenu() { const videoControlsShowHideMenuInner = document.createElement("div"); videoControlsShowHideMenuInner.id = "video-controls-show-hide-menu-inner"; videoControlsShowHideMenuInner.classList.add("video-manipulator-inner-item"); const controlTexts = [ "Speed Control", "Play/Pause Lock", "Aspect, Zoom, Flip, & Frame", "Jump to Time / Get Current Time", "Nice 'n' Accurate Looper", "Corners, Overlays, & Chapters", "Volume Boost", "Manual", "About" ]; let dividerNumber = 0; // Create buttons for each section const sectionIds = [ "playback-rate-controls-outer", "lock-controls-outer", "video-display-controls-outer", "video-seek-controls-outer-div", "video-loop-controls-outer-div", "cosmetic-checkboxes-outer-div", "compressor-controls-parent-div", "video-controls-manual", "jl-about-div" ]; sectionIds.forEach((sectionId, index) => { const button = document.createElement("button"); button.textContent = controlTexts[index]; button.id = sectionId + "-hider-button"; buttonRefs[button.id] = button; // Check if the section is hidden and mark the button as inactive if (sectionsToHide.includes(sectionId)) { button.classList.add("inactive"); } // Add event listener to toggle visibility of the corresponding section button.addEventListener("click", () => { if (sectionsToHide.includes(sectionId)) { sectionsToHide = sectionsToHide.filter((id) => id !== sectionId); // Remove from hide list button.classList.remove("inactive"); // Remove inactive class } else { sectionsToHide.push(sectionId); // Add to hide list button.classList.add("inactive"); // Add inactive class } saveSectionsState(); // Save the updated state to localStorage buildSectionHiderStylesheet(); // Update the stylesheet }); // Append the button to the menu videoControlsShowHideMenuInner.appendChild(button); // Divider for visual separation (every 4th button) const divider = createDivider(); dividerNumber += 1; divider.id = "divider-number-" + dividerNumber; videoControlsShowHideMenuInner.appendChild(divider); }); // Append the final divider style to hide the last divider if needed const hideFinalDividerStyle = document.createElement("style"); hideFinalDividerStyle.textContent = ` #divider-number-${dividerNumber} { display: none; } `; videoControlsShowHideMenuInner.appendChild(hideFinalDividerStyle); videoControlsShowHideMenu.appendChild(videoControlsShowHideMenuInner); } // Default values for the compressor settings, including preGain and finalGain const compressorValuesDefault = { threshold: 0, knee: 3, ratio: 20, attack: 0, // Fast attack release: 0.25, preGain: 1, // Default preGain finalGain: 1 // Default finalGain }; function findVideo() { video = document.querySelector("video"); } let gnrInProgress = false; let gnrInQ = false; function getNativeRatio() { if (gnrInProgress) { log("gnrInProgress: " + gnrInProgress); gnrInQ = true; return; } else { gnrInProgress = true; setTimeout(() => { gnrInProgress = false; }, 250); } log("Getting native ratio..."); findVideo(); if (!video) { setTimeout(() => { getNativeRatio(); }, 1000); return; } try { nativeRatio = parseFloat(video.videoWidth / video.videoHeight); log("nativeRatio: " + nativeRatio); } catch { setTimeout(() => { getNativeRatio(); }, 1000); return; } if (gnrInQ) { gnrInQ = false; setTimeout(() => { // gnrInProgress = false; getNativeRatio(); }, 250); } } function findContainerRatio() { container = video.closest("#player, #full-bleed-container"); log("Closest element was:", container); containerWidth = container.offsetWidth; containerHeight = container.offsetHeight; containerRatio = containerWidth / container.offsetHeight; // log('Container ratio:', containerRatio); log( "Container width: " + containerWidth + "\nContainer height: " + containerHeight + "\nContainer ratio: " + containerRatio ); } function parseAspectRatio() { log("Parsing aspect ratio:", rawAspectRatio); if (!savedAspectRatio) { getNativeRatio(); savedAspectRatio = nativeRatio; } if (rawAspectRatio === undefined || rawAspectRatio === null) { log("Nothing to parse."); aspectRatio = savedAspectRatio; return; } if (typeof rawAspectRatio === "string") { aspectRatio = rawAspectRatio.includes("/") ? parseFloat(rawAspectRatio.split("/")[0]) / parseFloat(rawAspectRatio.split("/")[1]) : parseFloat(rawAspectRatio); log("New aspect ratio:", aspectRatio); } else { aspectRatio = parseFloat(rawAspectRatio); } if (isNaN(aspectRatio) || aspectRatio === null || aspectRatio === undefined) { console.error("Invalid aspect ratio:", aspectRatio); aspectRatio = savedAspectRatio; log("Aspect ratio reverted to:", aspectRatio); return true; } log("Parsed aspect ratio: " + aspectRatio); savedAspectRatio = aspectRatio; } // Load the compressor values from localStorage or use the defaults let compressorValues = { ...compressorValuesDefault }; function loadCompressorValues() { // Check if values exist in localStorage const savedValues = JSON.parse(localStorage.getItem("compressorValues")); log("Saved compressor values: " + savedValues); if (savedValues) { // Merge saved values with defaults to keep missing ones defaulted compressorValues = { ...compressorValuesDefault, ...savedValues }; } else { // If no saved values, use the defaults compressorValues = { ...compressorValuesDefault }; } } loadCompressorValues(); function saveCompressorValues() { try { // Get the previously saved values from localStorage, or initialize an empty object const savedValues = JSON.parse(localStorage.getItem("compressorValues")) || {}; // Create an object to hold the updated values const updatedValues = { ...compressorValues }; // Compare current values with saved ones and store only the updated ones for (const key in compressorValues) { if (compressorValues[key] !== savedValues[key]) { updatedValues[key] = compressorValues[key]; } else { // If no change, ensure the key is still saved if (!savedValues.hasOwnProperty(key)) { updatedValues[key] = compressorValues[key]; } } } // If any values were updated, save them to localStorage if (Object.keys(updatedValues).length > 0) { localStorage.setItem("compressorValues", JSON.stringify(updatedValues)); } } catch (error) { console.error("Error saving compressor values to localStorage", error); } } // Example to update compressor values and save function updateCompressorSetting(key, value) { log("Updating compressor setting: " + key + ", " + value); if (compressorValues.hasOwnProperty(key)) { log("Saving setting..."); compressorValues[key] = value; saveCompressorValues(); // Save the updated value } } function deleteSavedCompressorValues() { try { // Remove the compressorValues from localStorage localStorage.removeItem("compressorValues"); log("Compressor values deleted from localStorage."); } catch (error) { console.error("Error deleting compressor values from localStorage", error); } } // Placeholder function to modify the compressor values function modifyCompression(property, value) { // log(`Modifying ${property} to ${value}`); // Actual logic for modifying the compressor settings goes here } // Function to create a compressor control function createCompressorControl(config, reset) { let input; let conversionSpan; const conversionType = config.conversion; // Handle the input population and modification let value = compressorValues[config.property ? config.property : config.id] || 0; // Fetch current value // // Apply conversions and modifiers when populating // if (config.conversion) { // value = convertValue(value, config.conversion); // } // if (config.calculationMod) { // value = applyCalculationMod(value, config.calculationMod); // } if (config.divideBy1000) { value *= 1000; // Multiply by 1000 when populating } // Function to handle the conversions (dB -> times or dB -> percent) function convertValue(value, conversionType) { // log('Converting', (config.property ? config.property : config.id), // 'with value', value, 'and type', conversionType + '.'); let calcVal = value; const scaleFactor = 1; if (conversionType === "times") { calcVal = Math.pow(2, value / 6); // dB to multiplier (times) } else if (conversionType === "percent") { calcVal = Math.pow(2, value / 6) * 100; // dB to percentage representation } log(config); if (config.calculationMod || config.hasOwnProperty("calculationMod")) { log("A calculation modification is present."); calcVal = applyCalculationMod(calcVal, config.calculationMod); // Apply modifier } return calcVal; // No conversion if not specified } function convertToDB(value) { return 6 * Math.log2(value); // return value; // No conversion if not specified } // Function to handle calculation modifiers dynamically function applyCalculationMod(value, mod) { log("Modifying calculations with inputs", value, "and", mod + "."); // Assume calculationMod contains simple expressions like '- 100' or any other formula const [operator, num] = mod.split(" "); if (operator === "-" && num) { return value - parseFloat(num); } log("Modified value equals", value + "."); return value; // If no valid modifier is found, return value unchanged } function getConvertedValue(newValue) { if (config.conversion) { newValue = convertValue(newValue, config.conversion); // Convert value based on config } if (config.divideBy1000) { newValue = newValue / 1000; // Divide by 1000 when sending back the value // modifyCompression((config.property ? config.property : config.id), newValue); // // Update the compressor with the modified value } else { // modifyCompression((config.property ? config.property : config.id), value); } return newValue; } function adjustStepSize() { log("Handling varying step sizes..."); const value = parseFloat(input.value); let step = config.stepSize; let nextValue = value; log("Step equals " + step + "."); for (let range of JSON.parse(config.stepRanges)) { log("Step ranges parsed as" + JSON.stringify(JSON.parse(config.stepRanges)) + "."); if (value >= range.range[0] && value < range.range[1]) { step = range.step; log("Step should be " + step + "."); // Adjust the value based on the current step nextValue = Math.round(value / step) * step; // Round to the nearest step increment break; } } // Set the next value programmatically // input.value = nextValue; input.step = step; // Dynamically adjust the step size for the input } if (!reset) { const controlDiv = document.createElement("div"); controlDiv.classList.add("compressor-control-div"); controlDiv.id = `${config.id}-control-div`; const label = document.createElement("label"); label.setAttribute("for", `${config.id}-input`); label.textContent = config.label; input = document.createElement("input"); input.type = "number"; input.id = `${config.id}-input`; // input.value = compressorValues[config.id] || 0; input.min = config.minValue || 0; input.max = config.maxValue || 100; input.step = config.stepSize; const postLabel = document.createElement("span"); postLabel.textContent = ` ${config.postLabel}`; const conversionDiv = document.createElement("div"); conversionDiv.classList.add("conversion-div"); const secondPostLabel = document.createElement("span"); secondPostLabel.textContent = ` ${config.secondPostLabel || ""}`; const openParenthesisSpan = document.createElement("span"); openParenthesisSpan.textContent = "("; conversionSpan = document.createElement("span"); conversionSpan.classList.add("conversion-span"); // Populate the input box with the modified value if (conversionType === "times") { input.value = convertToDB(value); } else { input.value = value; } // Handle variable step size logic for ratio, attack, release if (config.stepSize === "variable") { log("Setting up varying step sizes..."); adjustStepSize(); input.addEventListener("input", function () { adjustStepSize(); }); } // Handle the input change event dynamically input.addEventListener("input", function () { let newValue = parseFloat(input.value); log("New value: " + newValue); const minLimit = config.minLim; if (minLimit && newValue < minLimit) { newValue = minLimit; input.value = minLimit; } let retainValue = newValue; // if (config.divideBy1000) { // retainValue = retainValue / 1000; // Multiply by 1000 when populating // } conversionSpan.textContent = getConvertedValue(newValue).toFixed(2); if (config.link) { const linkedElement = document.getElementById(`${config.link}-input`); linkedElement.value = newValue; const linkedConversion = document.querySelector( `#${config.link}-control-div .conversion-div .conversion-span` ); if (linkedConversion) { log("Linked conversion found."); linkedConversion.textContent = getConvertedValue(newValue).toFixed(2); } else { log("No linked conversion found."); } } let passValue = getConvertedValue(newValue); if (config.id === "threshold") { log("Converting threshold..."); passValue = Math.pow(2, newValue / 6); } log("Pass value: " + passValue); changeCompressorValue(config.property ? config.property : config.id, passValue, newValue); // changeCompressorValue((config.property ? config.property : config.id), passValue, newValue); setTimeout(function () { changeCompressorValue(config.property ? config.property : config.id, passValue, newValue); // changeCompressorValue((config.property ? config.property : config.id), passValue, newValue); updateCompressorSetting(config.id, config.saveRaw ? retainValue : passValue); }, 0); }); // Tip logic for showing the relevant tip on hover controlDiv.addEventListener("mouseover", function () { // const tipDiv = document.getElementById("compressor-control-tip-div"); compressorControlTipParagraph.innerHTML = config.tip; compressorControlTipDiv.style.display = "block"; compressorControlTipDivBG.style.display = "block"; // Get the bounding rectangle of the paragraph const paragraphRect = compressorControlTipParagraph.getBoundingClientRect(); // Set the width and height of the div background to match the paragraph size compressorControlTipDivBG.style.width = `${paragraphRect.width}px`; compressorControlTipDivBG.style.height = `${paragraphRect.height}px`; log( `Updated size of background div: Width = ${paragraphRect.width}px, ` + `Height = ${paragraphRect.height}px` ); }); controlDiv.addEventListener("mouseout", function () { // const tipDiv = document.getElementById("compressor-control-tip-div"); if (compressorTipDivTesting) { return; } compressorControlTipParagraph.innerHTML = ""; compressorControlTipDiv.style.display = "none"; compressorControlTipDivBG.style.display = "none"; }); if (config.label) { controlDiv.appendChild(label); } controlDiv.appendChild(input); controlDiv.appendChild(postLabel); if (config.conversion) { conversionDiv.appendChild(openParenthesisSpan); conversionDiv.appendChild(conversionSpan); conversionDiv.appendChild(secondPostLabel); controlDiv.appendChild(conversionDiv); if (conversionType === "times") { conversionSpan.textContent = value.toFixed(2); } else { conversionSpan.textContent = getConvertedValue(value).toFixed(2); } } return controlDiv; } else { const controlDiv = document.getElementById(`${config.id}-control-div`); input = document.getElementById(`${config.id}-input`); // Populate the input box with the modified value if (conversionType === "times") { input.value = convertToDB(value); } else { input.value = value; } conversionSpan = controlDiv.querySelector(".conversion-span"); if (conversionSpan) { if (conversionType === "times") { conversionSpan.textContent = value.toFixed(2); } else { conversionSpan.textContent = getConvertedValue(value).toFixed(2); } } // Handle variable step size logic for ratio, attack, release if (config.stepSize === "variable") { log("Setting up varying step sizes..."); adjustStepSize(); input.addEventListener("input", function () { adjustStepSize(); }); } deleteSavedCompressorValues(); } } let compressorControlTipDiv; let compressorControlTipParagraph; let compressorControlTipDivBG; // Function to insert all the compressor controls function insertCompressorControls() { // const emptySpanner = insertEmptySpanner(); // videoControlsMasterHolder.appendChild(emptySpanner); compressorControlsParentDiv = document.createElement("div"); compressorControlsParentDiv.id = "compressor-controls-parent-div"; compressorControlsParentDiv.classList.add("video-manipulator-outer-div"); const compressorSimpleControlsDiv = document.createElement("div"); compressorSimpleControlsDiv.id = "compressor-simple-controls-div"; compressorSimpleControlsDiv.classList.add("video-manipulator-inner-item"); const compressorBoostButton = document.createElement("button"); compressorBoostButton.id = "compressor-boost-button"; compressorBoostButton.classList.add("video-manipulator-sub-item"); compressorBoostButton.textContent = "Boost Volume"; buttonRefs[compressorBoostButton.id] = compressorBoostButton; compressorSimpleControlsDiv.appendChild(compressorBoostButton); const simpleControlsConfig = [ { id: "simpleBoost", property: "preGain", link: "preGain", postLabel: "dB", conversion: "times", secondPostLabel: "x original volume)", stepSize: 0.5, minValue: -96, maxValue: 96, tip: `Measured in dB. A conversion is offered for those who can't think in dB.` } ]; // Loop through each control configuration and add it to the parent div simpleControlsConfig.forEach((config) => { const controlDiv = createCompressorControl(config); compressorSimpleControlsDiv.appendChild(controlDiv); }); const boostLockDiv = document.createElement("div"); boostLockDiv.id = "boost-lock-div"; const boostLockCheckbox = document.createElement("input"); boostLockCheckbox.id = "boost-lock-checkbox"; boostLockCheckbox.type = "checkbox"; const boostLockLabel = document.createElement("label"); boostLockLabel.textContent = "Boost all videos"; boostLockLabel.htmlFor = "boost-lock-checkbox"; boostLockDiv.appendChild(boostLockLabel); boostLockDiv.appendChild(boostLockCheckbox); // Retrieve the stored value from localStorage and update the checkbox state if it's set boostLockState = JSON.parse(localStorage.getItem("boostLockState") || "false"); if (boostLockState) { boostLockCheckbox.checked = true; } // Add an event listener to save the checkbox state to localStorage when clicked boostLockCheckbox.addEventListener("change", function () { localStorage.setItem("boostLockState", boostLockCheckbox.checked); }); compressorSimpleControlsDiv.appendChild(boostLockDiv); const showFullCompressorControlsButton = document.createElement("button"); showFullCompressorControlsButton.id = "show-full-compressor-controls"; showFullCompressorControlsButton.classList.add("video-manipulator-sub-item"); buttonRefs[showFullCompressorControlsButton.id] = showFullCompressorControlsButton; // Create the outer span const spanDownArrow = document.createElement("span"); spanDownArrow.classList.add("down-expand-arrow"); spanDownArrow.textContent = "▼"; // Create the text "Expert" const textNode = document.createTextNode("Expert"); // Create the second span const spanUpArrow = document.createElement("span"); spanUpArrow.classList.add("down-expand-arrow"); spanUpArrow.textContent = "▼"; // Append all the elements to the button showFullCompressorControlsButton.appendChild(spanDownArrow); showFullCompressorControlsButton.appendChild(textNode); showFullCompressorControlsButton.appendChild(spanUpArrow); compressorSimpleControlsDiv.appendChild(showFullCompressorControlsButton); compressorControlsParentDiv.appendChild(compressorSimpleControlsDiv); const compressorControlsGroupDiv = document.createElement("div"); compressorControlsGroupDiv.id = "compressor-controls-group-div"; compressorControlTipDiv = document.createElement("div"); compressorControlTipDiv.id = "compressor-control-tip-div"; compressorControlTipDiv.style.display = "none"; compressorControlTipParagraph = document.createElement("p"); compressorControlTipParagraph.id = "compressor-control-tip-paragraph"; compressorControlTipDivBG = document.createElement("div"); compressorControlTipDivBG.id = "compressor-control-tip-div-background"; compressorControlTipDivBG.style.display = "none"; const compressorControlTipParent = document.createElement("div"); compressorControlTipParent.id = "compressor-control-tip-parent"; const controlsConfig = [ { id: "preGain", label: "Input Boost", link: "simpleBoost", postLabel: "dB", conversion: "times", secondPostLabel: "x original volume)", stepSize: 0.5, minValue: -96, maxValue: 96, tip: `Measured in dB. A conversion is offered for those who can't think in dB.` }, { id: "threshold", label: "Threshold", postLabel: "dB", conversion: "percent", secondPostLabel: "% of max volume)", stepSize: 0.5, maxValue: 0, minValue: -100, saveRaw: true, tip: `Measured in dB. 0 is the ceiling — as loud as the sound can go. Lower values come below the ceiling.<br> If you put this too high, the compressor really won't do much. Going above the ceiling is not recommended.<br> A conversion is offered for those who can't think in dB.` }, { id: "knee", label: "Knee", postLabel: "dB", conversion: "percent", secondPostLabel: "% over the threshold)", calculationMod: `- 100`, stepSize: 1, minValue: 0, maxValue: 40, saveRaw: true, tip: `Bigger values are subtler, but if it gets too big, the compressor won't compress much.<br> Knee comes before threshold is reached with this compressor.<br> A conversion is offered for those who can't think in dB.` }, { id: "ratio", label: "Ratio", postLabel: " : 1", stepSize: "variable", stepRanges: ` [ { "range": [0, 4], "step": 0.5 }, { "range": [4, 8], "step": 1 }, { "range": [8, 12], "step": 2 }, { "range": [12, 24], "step": 4 } ] `, minValue: 0, maxValue: 20, minLim: 1, tip: `The higher the value, the harder it compresses. A value of 1 does nothing at all. 20 is as high as it can go.` }, { id: "attack", label: "Attack", postLabel: "ms", stepSize: "variable", stepRanges: ` [ { "range": [0, 1], "step": 0.1 }, { "range": [1, 5], "step": 0.5 }, { "range": [5, 10], "step": 1 }, { "range": [10, 50], "step": 5 }, { "range": [50, 100], "step": 10 }, { "range": [100, 500], "step": 50 }, { "range": [500, 2000], "step": 100 } ] `, minValue: 0, maxValue: 1000, divideBy1000: true, tip: `The lower the value, the faster it lowers the volume. 1000 is as slow as it goes.` }, { id: "release", label: "Release", postLabel: "ms", stepSize: "variable", stepRanges: ` [ { "range": [0, 100], "step": 10 }, { "range": [100, 250], "step": 25 }, { "range": [250, 500], "step": 50 }, { "range": [500, 2000], "step": 100 } ] `, minValue: 0, maxValue: 1000, divideBy1000: true, tip: `Find a sweet spot. Values below 100 may sound crummy.` } ]; // Loop through each control configuration and add it to the parent div controlsConfig.forEach((config) => { const controlDiv = createCompressorControl(config); compressorControlsGroupDiv.appendChild(controlDiv); }); const compressorResetButton = document.createElement("button"); compressorResetButton.id = "compressor-reset-button"; compressorResetButton.textContent = "Reset"; buttonRefs[compressorResetButton.id] = compressorResetButton; compressorControlsGroupDiv.appendChild(compressorResetButton); compressorResetButton.addEventListener("click", () => { compressorValues = { ...compressorValuesDefault }; simpleControlsConfig.forEach((config) => { const controlDiv = createCompressorControl(config, true); }); controlsConfig.forEach((config) => { const controlDiv = createCompressorControl(config, true); }); setCompressorValues(); }); const compressorAdvancedControlsDiv = document.createElement("div"); compressorAdvancedControlsDiv.id = "compressor-advanced-controls-div"; const compressorAdvancedControlsContainerDiv = document.createElement("div"); compressorAdvancedControlsContainerDiv.id = "compressor-advanced-controls-container-div"; compressorAdvancedControlsDiv.appendChild(compressorControlsGroupDiv); compressorControlTipDiv.appendChild(compressorControlTipParagraph); compressorControlTipParent.appendChild(compressorControlTipDivBG); compressorControlTipParent.appendChild(compressorControlTipDiv); compressorAdvancedControlsDiv.appendChild(compressorControlTipParent); // Declare the observer let closeFullCompressorControlsObserver; // Function to connect the observer function connectCloseFullCompressorControlsObserver() { closeFullCompressorControlsObserver = (event) => { // Check if the click is outside of the button or the controls container if ( !showFullCompressorControlsButton.contains(event.target) && !compressorAdvancedControlsContainerDiv.contains(event.target) ) { // Remove the 'active' class to close the controls showFullCompressorControlsButton.classList.remove("active"); compressorAdvancedControlsContainerDiv.classList.remove("active"); // Disconnect the observer once it's no longer needed document.removeEventListener("click", closeFullCompressorControlsObserver); } }; // Add event listener to the document for clicks document.addEventListener("click", closeFullCompressorControlsObserver); } showFullCompressorControlsButton.addEventListener("click", () => { if (showFullCompressorControlsButton.classList.contains("active")) { showFullCompressorControlsButton.classList.remove("active"); compressorAdvancedControlsContainerDiv.classList.remove("active"); } else { showFullCompressorControlsButton.classList.add("active"); compressorAdvancedControlsContainerDiv.classList.add("active"); connectCloseFullCompressorControlsObserver(); } }); compressorBoostButton.addEventListener("click", () => { if (compressorBoostButton.classList.contains("active")) { compressorBoostButton.classList.remove("active"); compressorControlsParentDiv.classList.remove("active"); boostLockState = false; localStorage.setItem("boostLockState", boostLockState); boostLockCheckbox.checked = boostLockState; disconnectCompressor(); } else { compressorBoostButton.classList.add("active"); compressorControlsParentDiv.classList.add("active"); initiateCompressor(); } recalculateForElement(compressorSimpleControlsDiv); }); if (boostLockState) { compressorBoostButton.classList.add("active"); compressorControlsParentDiv.classList.add("active"); initiateCompressor(); } compressorAdvancedControlsContainerDiv.appendChild(compressorAdvancedControlsDiv); compressorControlsParentDiv.appendChild(compressorAdvancedControlsContainerDiv); // Append the controls to the body or another container element videoControlsMasterHolder.appendChild(compressorControlsParentDiv); } function hideOverlays() { log("Hiding overlays..."); overlayHiderStyle.textContent = ` .ytp-chrome-top-buttons, .ytp-ce-element, .ytp-paid-content-overlay, .videowall-endscreen, .ytp-suggested-action, .annotation { display: none !important; } `; hideOverlaysVar = true; localStorage.setItem("hideOverlaysState", "true"); } function showOverlays() { log("Showing overlays..."); overlayHiderStyle.textContent = ` .ytp-chrome-top-buttons, .ytp-ce-element, .ytp-paid-content-overlay, .videowall-endscreen, .ytp-suggested-action, .annotation { /* display: none !important; */ } `; hideOverlaysVar = false; localStorage.setItem("hideOverlaysState", "false"); } function insertOverlayHider() { overlayHiderStyle = document.createElement("style"); overlayHiderStyle.id = "overlay-hider-style"; head.appendChild(overlayHiderStyle); const storedState = JSON.parse(localStorage.getItem("hideOverlaysState")) || false; if (storedState === true) { hideOverlays(); } else { showOverlays(); } } insertOverlayHider(); // Function to create and insert stylesheets function insertStylesheets() { insertCustomStyles(); flipRotateStyle = document.createElement("style"); flipRotateStyle.id = "flip-rotate-style"; head.appendChild(flipRotateStyle); videoZoomStyle = document.createElement("style"); videoZoomStyle.id = "video-zoom-style"; head.appendChild(videoZoomStyle); overrideObjectFitStyle = document.createElement("style"); overrideObjectFitStyle.id = "override-object-fit-style"; head.appendChild(overrideObjectFitStyle); additionalStyles.id = "video-display-controls-style"; additionalStyles.textContent += generalStyles; head.appendChild(additionalStyles); sectionsToHideStyle = document.createElement("style"); sectionsToHideStyle.id = "sections-to-hide-style"; document.head.appendChild(sectionsToHideStyle); buildSectionHiderStylesheet(); } // Function to create and insert the control panel function insertControlPanel() { log("The insertControlPanel function is running."); // const existingControls = document.querySelector("#video-display-controls-outer"); // if (existingControls) { // return; // } const buttons = [ { id: "aspect-4-3", class: "aspect", text: "4/3", titleText: "Converts the aspect ratio to 4 by 3.", onClick: () => handleAspectRatio("4/3", "aspect-4-3") }, { id: "aspect-16-9", class: "aspect", text: "16/9", titleText: "Converts the aspect ratio to 16 by 9.", onClick: () => handleAspectRatio("16/9", "aspect-16-9") }, { id: "aspect-2pt35", class: "aspect", text: "2.35", titleText: "Converts the aspect ratio to 2.35 to 1.", onClick: () => handleAspectRatio("2.35", "aspect-2pt35") }, { id: "aspect-custom", class: "aspect", text: "Custom", titleText: "Rescale the video to the custom aspect ration in the input box. Must be pressed to apply each time.", onClick: () => handleCustomAspect("aspect-custom") }, { id: "zoom", class: "zoom", text: "Zoom", titleText: "Zooms the video by a specified amount.", onClick: () => handleZoom("zoom") }, { id: "mirror", class: "orientation", text: "Mirror", titleText: "Mirrors the video horizontally.", onClick: () => handleMirror("mirror") }, { id: "flip", class: "orientation", text: "Flip", titleText: "Flips the video vertically.", onClick: () => handleFlip("flip") }, { id: "rotate-180", class: "orientation", text: "180°", titleText: "Rotates the video 180°.", onClick: () => handleRotate180("rotate-180") }, { id: "resize-frame", class: "frame-resize", text: "Resize Frame", titleText: "Resize the video container to match the selected aspect ratio. May still have some bugs.", onClick: () => handleFrameResize("resize-frame") }, { id: "reset", class: "reset", text: "Reset", titleText: "Resets all flips, mirrors, rotations, aspect ratios, and zooms. " + "Leaves the input boxes as they are so values can be easily recalled.", onClick: () => handleReset("reset") } ]; // Conditionally add debug buttons if (extraDebugButtons) { buttons.push( { id: "scale-and-top", class: "scale-and-top", text: "Scale and Top", titleText: "A debugging button that immediately runs the scaleAndTop function.", onClick: () => scaleAndTop() }, { id: "scale-and-top-direct", class: "scale-and-top-direct", text: "Scale and Top (direct)", titleText: "A debugging button that immediately runs the scaleAndTop function, direct set to true.", onClick: () => scaleAndTop(true) }, { id: "recalculate-after-pseudo-elements", class: "recalculate-after-pseudo-elements", text: "Recalculate After Pseudo Elements", titleText: "Recalculates size for the after pseudo elements.", onClick: () => recalculateAllAfterPseudoElements() }, { id: "test-difference", class: "test-difference", text: "Test Difference", titleText: "A debugging button that runs testDimensionDif.", onClick: () => testDimensionDif() } ); } controlPanelOuter = document.createElement("div"); controlPanelOuter.id = "video-display-controls-outer"; controlPanelOuter.classList.add("video-manipulator-outer-div"); controlPanel = document.createElement("div"); controlPanel.id = "video-display-controls"; controlPanel.classList.add("video-manipulator-inner-item"); buttons.forEach((button) => { const btn = document.createElement("button"); btn.id = button.id; btn.classList.add(button.class); btn.textContent = button.text; btn.addEventListener("click", button.onClick); if (button.titleText) { btn.title = button.titleText; } log("Creating button " + btn.id + "..."); // Save the button reference to the buttonRefs object buttonRefs[button.id] = btn; controlPanel.appendChild(btn); if (button.id === "aspect-custom") { const customContainer = document.createElement("div"); customContainer.id = "custom-container"; customContainer.classList.add("video-manipulator-sub-item"); const customInput = document.createElement("input"); customInput.type = "text"; customInput.id = "aspect-custom-input"; customInput.title = "This value is not reset when the reset button is pressed, to make the value easy to apply again."; customInput.placeholder = "e.g., 4/3"; customContainer.appendChild(btn); customContainer.appendChild(customInput); controlPanel.appendChild(customContainer); const divider = createDivider(); controlPanel.appendChild(divider); // Add event listener to detect changes in input value customInput.addEventListener("input", () => { // Add blinking class to button buttonRefs["aspect-custom"].classList.add("blinking"); // Optionally, you can remove the class after a certain duration to stop blinking clearTimeout(customBlink); customBlink = setTimeout(() => { buttonRefs["aspect-custom"].classList.remove("blinking"); }, 5000); // Adjust duration based on your preference }); } if (button.id === "resize-frame") { const resizeAutomaticallyText = "Resize the frame automatically when each new video loads."; resizeDefaultCheckbox = document.createElement("input"); resizeDefaultCheckbox.type = "checkbox"; resizeDefaultCheckbox.id = "resize-by-default-checkbox"; resizeDefaultCheckbox.title = resizeAutomaticallyText; const resizeDefaultLabel = document.createElement("label"); resizeDefaultLabel.textContent = "Auto Resize"; resizeDefaultLabel.htmlFor = "resize-by-default-checkbox"; resizeDefaultLabel.title = resizeAutomaticallyText; const resizeContainer = document.createElement("div"); resizeContainer.classList.add("video-manipulator-sub-item"); resizeContainer.id = "resize-container"; resizeContainer.appendChild(btn); resizeContainer.appendChild(resizeDefaultLabel); resizeContainer.appendChild(resizeDefaultCheckbox); controlPanel.appendChild(resizeContainer); const divider = createDivider(); controlPanel.appendChild(divider); resizeByDefault = JSON.parse(localStorage.getItem("resizeByDefault")) || false; if (resizeByDefault) { setTimeout(() => { // buttonRefs["resize-frame"].classList.add("active"); resizeDefaultCheckbox.checked = true; frameIsRescaled = true; handleFrameResize("resize-frame"); }, 0); } // Add event listener to save checkbox state to localStorage resizeDefaultCheckbox.addEventListener("change", () => { resizeByDefault = resizeDefaultCheckbox.checked; localStorage.setItem("resizeByDefault", JSON.stringify(resizeByDefault)); }); } if (button.id === "zoom") { const zoomContainer = document.createElement("div"); zoomContainer.id = "zoom-container"; zoomContainer.classList.add("video-manipulator-sub-item"); // Add zoom control zoomInput = document.createElement("input"); zoomInput.type = "number"; zoomInput.step = 0.01; // Increment step for numeric input zoomInput.value = 1; // Default value zoomInput.id = "zoom-input"; // Assign an ID for styling or JavaScript access zoomInput.title = "Click the Zoom button every time you wish to resize."; zoomContainer.appendChild(btn); zoomContainer.appendChild(zoomInput); controlPanel.appendChild(zoomContainer); const divider = createDivider(); controlPanel.appendChild(divider); // Add event listener for zoomInput changes zoomInput.addEventListener("input", () => { handleZoom("zoom"); // Call your function with the zoom value }); } if (button.id === "rotate-180") { const divider = createDivider(); controlPanel.appendChild(divider); } }); controlPanelOuter.appendChild(controlPanel); log("controlPanelOuter:", controlPanelOuter); videoControlsMasterHolder.appendChild(controlPanelOuter); log("Control panel inserted."); // Call the function to insert custom styles before inserting the control panel } function insertEmptySpanner(line) { const emptySpanner = document.createElement("div"); emptySpanner.classList.add("empty-spanner"); if (line) { emptySpanner.classList.add("line"); } return emptySpanner; } // Function to create and insert the playback rate control panel function insertPlaybackRateControls() { const existingControls = document.querySelector("#playback-rate-controls-outer"); if (existingControls) { return; } playbackRateOuter = document.createElement("div"); playbackRateOuter.id = "playback-rate-controls-outer"; playbackRateOuter.classList.add("video-manipulator-outer-div"); const playbackRateControls = document.createElement("div"); playbackRateControls.id = "playback-rate-controls"; playbackRateControls.classList.add("video-manipulator-inner-item"); const label = document.createElement("label"); label.textContent = "Speed:"; const input = document.createElement("input"); input.type = "number"; input.step = "0.05"; input.min = "0"; input.id = "playback-rate-input"; input.value = "1.00"; // Default value until actual playbackRate is detected input.addEventListener("input", handleRateChange); input.addEventListener("focus", handleRateChange); const playLockTitleText = "Prevents speed from being changed, whether by YouTube, an extension, or another script."; const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.id = "lock-rate-checkbox"; checkbox.addEventListener("change", handleLockToggle); checkbox.title = playLockTitleText; speedLockCheckbox = checkbox; const checkboxLabel = document.createElement("label"); checkboxLabel.htmlFor = "lock-rate-checkbox"; checkboxLabel.textContent = "Lock now"; checkboxLabel.title = playLockTitleText; const divider = createDivider(); const foreverLockTitleText = "Automatically sets the speed of videos when they load."; const foreverCheckbox = document.createElement("input"); foreverCheckbox.type = "checkbox"; foreverCheckbox.id = "lock-forever-checkbox"; foreverCheckbox.title = foreverLockTitleText; foreverCheckbox.addEventListener("change", handleLockToggle); const foreverCheckboxLabel = document.createElement("label"); foreverCheckboxLabel.htmlFor = "lock-forever-checkbox"; foreverCheckboxLabel.textContent = "Lock forever"; foreverCheckboxLabel.title = foreverLockTitleText; foreverLockCheckbox = foreverCheckbox; const savedSpeedLockActive = JSON.parse(localStorage.getItem("savedSpeedLockActive")) || false; const savedSpeedLockRate = parseFloat(localStorage.getItem("savedSpeedLockRate")) || 1.0; if (savedSpeedLockActive) { foreverCheckbox.checked = true; checkbox.checked = true; playbackRateLocked = true; playbackRateForeverLocked = true; lockRate = savedSpeedLockRate; input.value = lockRate; handleRateChange(null, savedSpeedLockRate); } playbackRateControls.appendChild(label); playbackRateControls.appendChild(input); playbackRateControls.appendChild(checkboxLabel); playbackRateControls.appendChild(checkbox); playbackRateControls.appendChild(divider); playbackRateControls.appendChild(foreverCheckboxLabel); playbackRateControls.appendChild(foreverCheckbox); playbackRateOuter.appendChild(playbackRateControls); videoControlsMasterHolder.appendChild(playbackRateOuter); monitorPlaybackRate(); log("Playback rate control panel inserted."); } let monitorPlaybackRateIsRunning = false; let rateChanging = false; let videoFound = false; // Monitor changes in the video playback rate function monitorPlaybackRate() { if (!monitorPlaybackRateIsRunning) { log("monitorPlaybackRate is running..."); monitorPlaybackRateIsRunning = true; } const checkPlaybackRate = () => { findVideo(); const input = document.getElementById("playback-rate-input"); if (!video) { log("No video found."); } else { if (!videoFound) { log("Video found."); videoFound = true; } } if (playbackRateLocked) { // Lock is on, force the playback rate to lockRate if (video.playbackRate !== lockRate) { log("Oh no you don't! Changing playback rate back..."); video.playbackRate = lockRate; } } else { // Update the input value if lock is not active if (!rateChanging && document.activeElement !== input) { if (input.value !== video.playbackRate.toFixed(2)) { log("Playback rate measured as " + video.playbackRate.toFixed(2) + "."); input.value = video.playbackRate.toFixed(2); } } else if ((rateChanging || document.activeElement == input) && !isNaN(input.value)) { if (parseFloat(input.value) !== parseFloat(video.playbackRate)) { rateChanging = true; log( "Video playback rate: " + video.playbackRate + "\nInput value: " + input.value + "\nDon't change that value while I'm changing it." ); video.playbackRate = input.value; // log("Don't change that value while I'm changing it."); rateChanging = false; } } else { rateChanging = false; } } }; // Run the check periodically setInterval(checkPlaybackRate, 100); } function insertLockControls() { // const existingControls = document.querySelector("#lock-controls-outer"); // if (existingControls) { // return; // } // Create a parent div for the play and pause lock buttons lockControlsOuterDiv = document.createElement("div"); lockControlsOuterDiv.id = "lock-controls-outer"; lockControlsOuterDiv.classList.add("video-manipulator-outer-div"); const lockControlsDiv = document.createElement("div"); lockControlsDiv.id = "lock-controls"; lockControlsDiv.classList.add("video-manipulator-inner-item"); // Create the Play Lock button const playLockButton = document.createElement("button"); playLockButton.id = "playLockButton"; playLockButton.classList.add("lock-button"); // Create the play triangle span const playTriangleSpan = document.createElement("span"); playTriangleSpan.textContent = "►"; // Unicode for the play triangle // Create the lock span const lockSpan = document.createElement("span"); lockSpan.classList.add("lock-span"); lockSpan.textContent = "🔒"; // Unicode for the lock icon // Append both elements to the playLockButton playLockButton.appendChild(playTriangleSpan); playLockButton.appendChild(lockSpan); playLockButton.title = "Play Lock - Prevents the video from getting unintentionally paused."; // Create the Pause Lock button const pauseLockButton = document.createElement("button"); pauseLockButton.id = "pauseLockButton"; pauseLockButton.classList.add("lock-button"); // Create the pause bars span const pauseBarsSpan = document.createElement("span"); pauseBarsSpan.textContent = "⏸"; // Unicode for pause symbol // Create the lock span const lockSpan2 = document.createElement("span"); lockSpan2.classList.add("lock-span"); lockSpan2.textContent = "🔒"; // Unicode for the lock icon // Append both elements to the pauseLockButton pauseLockButton.appendChild(pauseBarsSpan); pauseLockButton.appendChild(lockSpan2); pauseLockButton.title = "Pause Lock - Prevents the video from getting unintentionally unpaused."; // Append buttons to the parent div lockControlsDiv.appendChild(playLockButton); lockControlsDiv.appendChild(pauseLockButton); lockControlsOuterDiv.appendChild(lockControlsDiv); buttonRefs["play-lock-button"] = playLockButton; buttonRefs["pause-lock-button"] = pauseLockButton; videoControlsMasterHolder.appendChild(lockControlsOuterDiv); actions = topRow.querySelector("#actions"); log("Lock controls inserted."); // Now add the behavior for the lock buttons addLockButtonListeners(playLockButton, pauseLockButton); } let playPauseListenersAdded = false; let intervalId = null; function addPlayPauseListeners() { log("addPlayPauseListeners..."); function printLockStates() { log("playLock:", playLock, ", pauseLock", pauseLock); } printLockStates(); // Listen for play and pause events findVideo(); // Function to start or stop the interval function manageInterval() { // Start interval if either lock is active and interval isn't already running if ((playLock || pauseLock) && !intervalId) { log("Starting interval check..."); intervalId = setInterval(() => { if (playLock && video.paused) { log("Interval fallback: Play lock is active, playing video..."); video.play(); } else if (pauseLock && !video.paused) { log("Interval fallback: Pause lock is active, pausing video..."); video.pause(); } else if (!playLock && !pauseLock) { // If neither lock is active, stop the interval log("Neither lock is active, stopping interval..."); clearInterval(intervalId); intervalId = null; } }, 1000); } // Stop the interval if both locks are off and the interval is running else if (!playLock && !pauseLock && intervalId) { log("Stopping interval as both locks are inactive..."); clearInterval(intervalId); intervalId = null; } } if (video) { manageInterval(); if (playPauseListenersAdded) { return; } playPauseListenersAdded = true; log("Adding play/pause event listeners..."); video.addEventListener("play", () => { log("Video playing..."); printLockStates(); if (pauseLock) { video.pause(); // Pause the video if pauseLock is active } log("Video is playing."); }); video.addEventListener("pause", () => { log("Video paused..."); printLockStates(); if (playLock) { video.play(); // Play the video if playLock is active } log("Video is paused."); }); } } function addLockButtonListeners(playLockButton, pauseLockButton) { function updateButtonStates() { if (playLock) { playLockButton.classList.add("active"); pauseLockButton.classList.remove("active"); } else if (pauseLock) { pauseLockButton.classList.add("active"); playLockButton.classList.remove("active"); } else { playLockButton.classList.remove("active"); pauseLockButton.classList.remove("active"); } } playLockButton.addEventListener("click", () => { findVideo(); if (!video) { log("No video found."); return; // Exit if no video } log("Video found."); if (pauseLock) pauseLock = false; // Disable pause lock if active playLock = !playLock; if (playLock) { video.play(); } updateButtonStates(); addPlayPauseListeners(); }); pauseLockButton.addEventListener("click", () => { findVideo(); if (!video) { log("No video found."); return; // Exit if no video } log("Video found."); if (playLock) playLock = false; // Disable play lock if active pauseLock = !pauseLock; if (pauseLock) { video.pause(); } updateButtonStates(); addPlayPauseListeners(); }); } // Event handler for changing the playback rate from the input box function handleRateChange(event, newRate) { rateChanging = true; if (newRate === undefined) { newRate = parseFloat(event.target.value); } if (playbackRateForeverLocked) { localStorage.setItem("savedSpeedLockRate", newRate); } findVideo(); if (!isNaN(newRate) && video && input.value !== video.playbackRate.toFixed(2)) { if (playbackRateLocked) { lockRate = newRate; video.playbackRate = lockRate; } else { video.playbackRate = newRate; } log("Playback rate changed to:", newRate); } } // Event handler for toggling the lock function handleLockToggle(event) { const input = document.getElementById("playback-rate-input"); findVideo(); if (event.target.id === "lock-rate-checkbox") { log("Lock rate checkbox toggled"); if (event.target.checked) { playbackRateLocked = true; lockRate = parseFloat(input.value); log("Playback rate locked at:", lockRate); } else { playbackRateLocked = false; foreverLockCheckbox.checked = false; lockRate = null; log("Playback rate unlocked"); } } else if (event.target.id === "lock-forever-checkbox") { log("Lock forever checkbox toggled"); if (event.target.checked) { playbackRateLocked = true; speedLockCheckbox.checked = true; localStorage.setItem("savedSpeedLockActive", true); lockRate = parseFloat(input.value); localStorage.setItem("savedSpeedLockRate", lockRate); log("Playback rate locked at:", lockRate); playbackRateForeverLocked = true; } else { localStorage.setItem("savedSpeedLockActive", false); playbackRateForeverLocked = false; } } } // New function to handle active class toggling function handleActiveClass(btn, className, isCustomOrZoom) { log("Button: " + btn); const clickedButton = buttonRefs[btn]; log("Clicked button: " + clickedButton); let buttonsWithClass; // if (className) // Get all buttons with the matching class from the buttonRefs object buttonsWithClass = Object.values(buttonRefs).filter((button) => button.classList.contains(className)); // Remove the 'active' class from all buttons except the passed one buttonsWithClass.forEach((button) => { if (button !== clickedButton && button.classList.contains("active")) { button.classList.remove("active"); } }); if (isCustomOrZoom) { if (!clickedButton.classList.contains("active")) { clickedButton.classList.add("active"); } else if (lastCustomRepeated) { log("Last custom value was repeated. Making button inactive."); // setTimeout(() => { lastCustom = undefined; // }, 50); lastCustomRepeated = false; lastCustomReset = true; clickedButton.classList.remove("active"); return true; } return false; } // Check if the passed button already has the 'active' class if (clickedButton.classList.contains("active")) { // If it has the 'active' class, remove it clickedButton.classList.remove("active"); return true; } else { // Otherwise, add the 'active' class to the passed button clickedButton.classList.add("active"); return false; } } function resetFlipRotate() { flipRotateStyle.textContent = ""; } function resetAspect() { videoZoomStyle.textContent = ""; tipAspectStyle.textContent = ""; rawAspectRatio = undefined; aspectRatio = undefined; getNativeRatio(); savedAspectRatio = nativeRatio; frameAspectStyle.textContent = ""; frameIsRescaled = false; } // Event handler functions function handleFlip(btn) { const active = handleActiveClass(btn, "orientation"); if (active) { resetFlipRotate(); } else { flipRotateStyle.textContent = ` video, .ytp-tooltip:not(.ytp-text-detail) .ytp-tooltip-bg { transform: scale(1, -1); } `; } log("Flip button pressed"); } function handleMirror(btn) { const active = handleActiveClass(btn, "orientation"); if (active) { resetFlipRotate(); } else { flipRotateStyle.textContent = ` video, .ytp-tooltip:not(.ytp-text-detail) .ytp-tooltip-bg { transform: scale(-1, 1); } `; } log("Mirror button pressed"); } function handleRotate180(btn) { const active = handleActiveClass(btn, "orientation"); if (active) { resetFlipRotate(); } else { flipRotateStyle.textContent = ` video, .ytp-tooltip:not(.ytp-text-detail) .ytp-tooltip-bg { rotate: 180deg; } `; } log("180° button pressed"); } // let handleRepeat = 0; let scaleAndTopInProgress = false; let scaleAndTopQueued = false; let isStretched; function compareRatios() { findVideo(); getNativeRatio(); if (aspectRatio === nativeRatio || aspectRatio === null) { isStretched = false; } else { isStretched = true; } } function scaleAndTop(direct) { if (scaleAndTopInProgress) { scaleAndTopQueued = true; log("Scale and top already in progress."); return; } else { scaleAndTopInProgress = true; setTimeout(() => { scaleAndTopInProgress = false; }, 250); } compareRatios(); if (noScaleAndTop) { log("scaleAndTop will not be run."); return; } // if (!scaleAndTopInProgress) { // scaleAndTopInProgress = true; // setTimeout(() => { // scaleAndTopInProgress = false; // }, 225); // // return; // } else if (!scaleAndTopQueued) { // scaleAndTopQueued = true; // log("Queuing scaleAndTop."); // return; // } else { // log("Stopping scaleAndTop."); // return; // } log("Running scaleAndTop..."); let scaleDiff; let newWidth; let newHeight; let newTop; let newLeft; let isWiderThanContainer; let useThisRatio; findVideo(); if (!video) { log("scaleAndTop found no video."); return; } if (!aspectRatio) { // Get the native aspect ratio of the video getNativeRatio(); useThisRatio = nativeRatio; // aspectRatio = nativeRatio; // savedAspectRatio = aspectRatio; } else { useThisRatio = aspectRatio; } log("useThisRatio:", useThisRatio); if (isNaN(useThisRatio) || isNaN(nativeRatio)) { setTimeout(() => { scaleAndTop(); }, 250); return; } else { log("useThisRatio, checked again:", useThisRatio); } findContainerRatio(); // Determine whether to scale by width or height isWiderThanContainer = useThisRatio > containerRatio; if (isWiderThanContainer) { log("Video will be wider than container."); scaleDiff = useThisRatio - containerRatio; log("Scale difference:", scaleDiff); newWidth = containerWidth; newHeight = containerWidth / useThisRatio; newTop = (containerHeight - newHeight) / 2; newLeft = 0; } else { log("Video will not be wider than container."); newHeight = containerHeight; newWidth = containerHeight * useThisRatio; log("newHeight: " + newHeight + ", newWidth: " + newWidth); newTop = 0; newLeft = (containerWidth - newWidth) / 2; } // newHeight = Math.round(newHeight); // newWidth = Math.round(newWidth); const scaleX = (aspectRatio / nativeRatio).toFixed(8); reportVideoDimensions(); isStretched = aspectRatio != nativeRatio; // if (direct) { // setTimeout(() => { ignoreChange = true; log("scaleAndTop will scale the video directly."); video.style.height = newHeight + "px"; video.style.width = newWidth + "px"; video.style.top = newTop + "px"; video.style.left = newLeft + "px"; if (isStretched) { video.style.objectFit = "unset"; } else { video.style.objectFit = ""; } // reportVideoDimensions(); setTimeout(() => { ignoreChange = false; }, 250); // reportVideoDimensions(); // }, 250); // } else { // videoZoomStyle.textContent = ` // video { // width: ${newWidth}px !important; // height: ${newHeight}px !important; // object-fit: unset !important; // top: ${newTop}px !important; // left: ${newLeft}px !important; // scale: ${zoom}; // } // `; // } reportVideoDimensions(); tipAspectStyle.textContent = ` .ytp-tooltip:not(.ytp-text-detail) .ytp-tooltip-bg { scale: ${isWiderThanContainer ? `${zoom} ${zoom / scaleX}` : `${scaleX * zoom} ${zoom}`}; } `; log(`Aspect ratio set to ${aspectRatio}, scaleX: ${scaleX}`); if (scaleAndTopQueued) { scaleAndTopQueued = false; setTimeout(() => { scaleAndTop(); }, 250); } startTestDifInterval(); } let testDifInterval = null; // Global variable to store the interval ID const testDifIntervalTime = 250; // Interval time set to 250ms const testDifIntervalMaxIterations = 16; let testDifIntervalIterations = 0; function testDimensionDif() { findContainerRatio(); const heightDif = Math.abs(parseFloat(video.style.height) - containerHeight); const widthDif = Math.abs(parseFloat(video.style.width) - containerWidth); const topDif = Math.abs(parseFloat(video.style.top) - (heightDif / 2)); log("heightDif:", heightDif, "\nwidthDif:", widthDif, "\ntopDif:", topDif); if ((heightDif >= 1 && widthDif >= 1) || topDif >= 1) { log("Discrepancy detected. Rescaling..."); scaleAndTop(); } testDifIntervalIterations += 1; if (testDifIntervalIterations >= testDifIntervalMaxIterations) { stopTestDifInterval(); } } function startTestDifInterval() { if (testDifInterval === null) { // Start the interval only if it's not already running testDifInterval = setInterval(testDimensionDif, testDifIntervalTime); log("testDifInterval started."); } else { log("testDifInterval is already running. Resetting iterations;"); testDifIntervalIterations = 0; } } function stopTestDifInterval() { if (testDifInterval !== null) { clearInterval(testDifInterval); testDifInterval = null; testDifIntervalIterations = 0; log("testDifInterval stopped"); } } function reportVideoDimensions() { const videoWidth = video.offsetWidth; const videoHeight = video.offsetHeight; log(`Video height: ${videoHeight}\nVideo width: ${videoWidth}`); log("videoZoomStyle:\n" + videoZoomStyle.textContent); } let handleAspectActive = false; let switchAspectOff = false; let ignoreChange = false; function handleAspectRatio(value, btn, isCustomOrZoom) { if (btn) { log("Handling aspect ratio. Button pressed was " + btn + "."); if (btn != "aspect-custom") { log("Button was not aspect-custom."); lastCustom = undefined; lastCustomRepeated = false; } } else { log("Handling aspect ration. No button was pressed."); } reportVideoDimensions(); let failed; if (ignoreChange) { log("This change will be ignored."); // ignoreChange = false; // setTimeout(() => { // ignoreChange = false; // }, 250); return; } // if (!frameIsRescaled && aspectRatio === undefined) { // if (handleAspectActive === false) { // log('Nothing to do. Resetting aspect style.'); // resetAspect(); // return; // } else { // log('Not resetting aspect style yet.'); // switchAspectOff = true; // } // } else { // handleAspectActive = true; // switchAspectOff = false; // } findVideo(); if (!video) { return; } log("zoom:", zoom); reportVideoDimensions(); let active = false; // handleRepeat += 1; if (btn) { noAspect = false; if (isCustomOrZoom === "zoom") { handleActiveClass(btn, "zoom", true); } else { active = handleActiveClass(btn, "aspect", isCustomOrZoom); } } getNativeRatio(); // reportVideoDimensions(); if (value) { log("A value of " + value + " was specified to handleAspectRatio."); rawAspectRatio = value; log("value:", value); failed = parseAspectRatio(); log("aspectRatio: ", aspectRatio); if (isCustomOrZoom === "custom" && aspectRatio === value) { handleActiveClass(btn, "aspect"); noAspect = true; } } else { log("Aspect ratio will be native ratio."); aspectRatio = nativeRatio; noAspect = true; // savedAspectRatio = aspectRatio; } // reportVideoDimensions(); // log("Aspect ratio: ", unparsedAspectRatio, "\nParsed:", aspectRatio); if (active) { log("Button was active. Unsetting aspect ratio..."); aspectRatio = nativeRatio; savedAspectRatio = nativeRatio; noAspect = true; // scaleAndTop(); // setTimeout(() => { // scaleAndTop(); // }, 250); } if (aspectRatio === nativeRatio) { noAspect = true; } // reportVideoDimensions(); findContainerRatio(); log(`Aspect ratio is ${aspectRatio}. Contaner ratio is ${containerRatio}.`); rescaleFrame(); if (active) { scaleAndTop(); } // reportVideoDimensions(); // if (isNaN(zoom)) { // log("Defaulting zoom to 1."); // zoom = 1; // } // if (!video) { // console.error("Video element not found"); // return; // } // reportVideoDimensions(); // Get the aspect ratio of the container // const container = video.parentElement; // findContainerRatio(); // Get the native aspect ratio of the video // getNativeRatio(); // log("Native ratio:", nativeRatio); // reportVideoDimensions(); // rescaleFrame(); // reportVideoDimensions(); if (!isNaN(zoom)) { videoZoomStyle.textContent = ` video { scale: ${zoom}; } .ytp-tooltip:not(.ytp-text-detail) .ytp-tooltip-bg { /* background-size: ${zoom}; */ } `; } // Check if aspectRatio is undefined if (noAspect) { log("No aspect, and so..."); // findVideo(); // reportVideoDimensions(); tipAspectStyle.textContent = ""; // video { // top: ${adjustedTop}px !important; // } // reportVideoDimensions(); // setTimeout(() => { // findContainerRatio(); // const videoWidth = video.offsetWidth; // const videoHeight = video.offsetHeight; // const adjustedTop = (containerHeight - videoHeight) / 2; // log( // "videoHeight: " + // videoHeight + // "\ncontainerHeight: " + // containerHeight + // "\nadjustedTop: " + // adjustedTop // ); // video.style.top = adjustedTop + "px"; // }, 0); // ignoreChange = true; // scaleAndTop(true); } else { noScaleAndTop = false; scaleAndTop(); if (isCustomOrZoom === "custom" && btn && !failed) { handleActiveClass(btn, "aspect", true); } } reportVideoDimensions(); // rescaleFrame(); // if (handleRepeat != 2) { // handleAspectRatio(value, btn, isCustomOrZoom); // } else { // handleRepeat = 0; // } // if (switchAspectOff) { // handleAspectActive = false; // } } function handleCustomAspect(btn) { const customValue = document.getElementById("aspect-custom-input").value; if (lastCustomReset) { lastCustom = undefined; lastCustomReset = false; } if (lastCustom === customValue) { lastCustomRepeated = true; } else { lastCustomRepeated = false; } let buttonItself = document.querySelector(`button#${btn}`); buttonItself.classList.remove("blinking"); log("Custom value:", customValue); if (customValue === null || !customValue) { return; } handleAspectRatio(customValue, btn, "custom"); lastCustom = customValue; } let zoomDebounceTimeout; function handleZoom(btn) { // log('Zooming...'); clearTimeout(zoomDebounceTimeout); zoomDebounceTimeout = setTimeout(() => { if (zoom !== zoomInput.value) { isZoomed = true; } else { isZoomed = !isZoomed; } if (isZoomed) { zoom = zoomInput.value; } else { zoom = 1; } handleAspectRatio(aspectRatio, btn, "zoom"); if (zoom === 1) { buttonRefs[btn].classList.remove("active"); } }, 250); } function handleReset(btn) { resetFlipRotate(); resetAspect(); lastCustomRepeated = false; lastCustom = undefined; lastCustomReset = undefined; frameIsRescaled = false; const relevantButtons = controlPanel.querySelectorAll("button"); relevantButtons.forEach((button) => { button.classList.remove("active"); }); zoom = 1; // zoomInput.value = 1; setTimeout(() => { scaleAndTop(true); }, 250); // noScaleAndTop = true; log("Reset button pressed"); } function handleFrameResize(btn) { handleActiveClass(btn, "frame-resize"); let direct; const isActive = buttonRefs[btn].classList.contains("active"); if (isActive) { frameIsRescaled = true; rescaleFrame(); direct = false; } else { frameIsRescaled = false; frameAspectStyle.textContent = ""; direct = true; } setTimeout(() => { handleAspectRatio(aspectRatio); noScaleAndTop = false; setTimeout(() => { scaleAndTop(true); }, 250); }, 25); setTimeout(() => { scaleAndTop(); }, 500); } let rescaleInProgress = false; let rescaleInQ = false; function rescaleFrame() { if (rescaleInProgress) { rescaleInQ = true; log("Rescale already in progress."); return; } else { rescaleInProgress = true; setTimeout(() => { rescaleInProgress = false; }, 250); } // return; log("Rescaling frame..."); findVideo(); if (!video) { setTimeout(() => { rescaleFrame(); }, 1000); return; } if (!frameIsRescaled) { frameAspectStyle.textContent = ""; log("Frame will not be rescaled."); return; } else { log("Frame will be rescaled."); } let useThisRatio; if (!aspectRatio) { getNativeRatio(); useThisRatio = parseFloat(nativeRatio); // frameAspectStyle.textContent = ""; } else { // parseAspectRatio(); useThisRatio = aspectRatio; } if (isNaN(useThisRatio)) { setTimeout(() => { rescaleFrame(); }, 1000); return; } log("useThisRatio: " + useThisRatio); if (frameIsRescaled) { frameAspectStyle.textContent = ` ytd-watch-flexy[full-bleed-player]:not([fullscreen]) #full-bleed-container.ytd-watch-flexy, ytd-watch-flexy[default-layout] #player, ytd-watch-flexy[full-bleed-player]:not([fullscreen]) #full-bleed-container.ytd-watch-flexy #container, ytd-watch-flexy[full-bleed-player]:not([fullscreen]) #full-bleed-container.ytd-watch-flexy #container .html5-video-player { /* aspect-ratio: ${useThisRatio * 1.0011}; */ aspect-ratio: ${useThisRatio}; /* max-height: calc(100vh - 169px); */ max-height: calc(100vh - 64px); } ytd-watch-flexy[full-bleed-player]:not([fullscreen]) #full-bleed-container.ytd-watch-flexy #ytd-player, ytd-watch-flexy[full-bleed-player]:not([fullscreen]) #full-bleed-container.ytd-watch-flexy #player-container { /* max-height: calc(100vh - 169px) !important; */ } ytd-watch-flexy:not([full-bleed-player]):not([fullscreen]) #player { margin: auto; } ytd-watch-flexy[full-bleed-player]:not([fullscreen]) #full-bleed-container.ytd-watch-flexy { height: unset; /* max-height: unset; */ } ytd-watch-flexy[full-bleed-player]:not([fullscreen]) #full-bleed-container.ytd-watch-flexy .html5-video-container { height: 100%; } ytd-watch-flexy[full-bleed-player]:not([fullscreen]) #full-bleed-container.ytd-watch-flexy video { /* transform-origin: top; */ /* top: unset !important; */ /* max-height: 100%; */ /* max-width: 100%; */ /* object-fit: unset; */ } ytd-watch-flexy[default-layout] #player #player-container-outer, ytd-watch-flexy[default-layout] #player #player-container-outer #player-container-inner, ytd-watch-flexy[default-layout] #player #player-container-outer #player-container-inner .html5-video-container { width: 100%; height: 100%; padding-top: 0; } ytd-watch-flexy[default-layout] #player video { /* height: 100% !important; */ /* width: 100% !important; */ /* top: unset !important; */ /* object-fit: contain; */ } ytd-watch-flexy[default-layout] #player .html5-video-container, ytd-watch-flexy[full-bleed-player]:not([fullscreen]) #full-bleed-container.ytd-watch-flexy .html5-video-container { /display: flex; /align-items: center; } ytd-watch-flexy[full-bleed-player]:not([fullscreen]) #full-bleed-container.ytd-watch-flexy video { // height: 100% !important; // width: auto !important; /* position: relative; */ transform-origin: center; /* margin: auto; */ /* left: unset !important; */ /* right: unset !important; */ /* position: relative; */ } `; } if (rescaleInQ) { rescaleInQ = false; setTimeout(() => { rescaleFrame(); }, 250); } } let retryTimeout = null; // Function to get the most common background color from buttons in #top-row function getMostCommonButtonColor() { log("Finding most common button color..."); function findColor() { // const buttons = document.querySelectorAll('#top-row button:not(#video-display-controls button)'); const buttons = topRow.querySelectorAll("#actions button"); if (buttons.length < 3) { // log(`Found ${buttons.length} buttons, retrying in 1 second...`); if (!retryTimeout) { retryTimeout = setTimeout(() => { retryTimeout = null; findColor(); }, 1000); } return; } const colorCount = {}; let mostCommonColor = ""; let maxCount = 0; buttons.forEach((button) => { const color = window.getComputedStyle(button).backgroundColor; log("Button background color:", color); if (colorCount[color]) { colorCount[color]++; } else { colorCount[color] = 1; } if (colorCount[color] > maxCount) { maxCount = colorCount[color]; mostCommonColor = color; } }); log("Most common color found:", mostCommonColor); applyButtonColor(mostCommonColor); } findColor(); } // Function to apply the most common color to the custom buttons function applyButtonColor(color) { const customStyles = additionalStyles; if (customStyles) { customStyles.textContent += ` :root { --video-manipulator-button-color: ${color}; } .video-manipulator-inner-item button, button.video-manipulator-inner-item { background-color: var(--video-manipulator-button-color); } `; log("Applied most common color to custom buttons:", color); } else { console.warn("video-display-controls stylesheet not found"); } } // Function to insert custom styles function insertCustomStyles() { log("Inserting custom styles..."); const mostCommonColor = getMostCommonButtonColor(); const customStyles = additionalStyles; if (customStyles) { customStyles.textContent += ` #video-display-controls button { background-color: ${mostCommonColor}; } `; log("Custom styles inserted."); } else { console.warn("video-display-controls stylesheet not found"); } } function appendSquareCornersStyle() { squareCornersStyle = document.createElement("style"); squareCornersStyle.id = "square-corners-style"; head.appendChild(squareCornersStyle); log("Appending square corners style..."); squareCorners = JSON.parse(localStorage.getItem("squareCorners") || false); log("squareCorners:", squareCorners); makeCornersSquareOrNot(); } appendSquareCornersStyle(); function makeCornersSquareOrNot() { if (squareCorners) { squareCornersStyle.textContent = ` #player-api.round, ytd-player { border-radius: unset !important; } `; } else { squareCornersStyle.textContent = ""; } } let cosmeticCheckboxesDiv; function insertCosmeticControls() { function insertSquareCornerControl() { cosmeticCheckboxesDiv = document.createElement("div"); cosmeticCheckboxesDiv.id = "cosmetic-checkboxes-div"; cosmeticCheckboxesDiv.classList.add("video-manipulator-inner-item"); cosmeticCheckboxesOuterDiv = document.createElement("div"); cosmeticCheckboxesOuterDiv.id = "cosmetic-checkboxes-outer-div"; cosmeticCheckboxesOuterDiv.classList.add("video-manipulator-outer-div"); const squareCornersInnerDiv = document.createElement("div"); squareCornersInnerDiv.id = "square-corners-inner-div"; squareCornersInnerDiv.classList.add("video-manipulator-sub-item"); squareCornersInnerDiv.title = "Give the video normal square corners, because all pixels matter."; const squareCornersCheckbox = document.createElement("input"); squareCornersCheckbox.type = "checkbox"; squareCornersCheckbox.id = "square-corners-checkbox"; const squareCornersCheckboxLabel = document.createElement("label"); squareCornersCheckboxLabel.htmlFor = "square-corners-checkbox"; squareCornersCheckboxLabel.textContent = "Square Corners"; squareCornersInnerDiv.appendChild(squareCornersCheckboxLabel); squareCornersInnerDiv.appendChild(squareCornersCheckbox); cosmeticCheckboxesDiv.appendChild(squareCornersInnerDiv); cosmeticCheckboxesOuterDiv.appendChild(cosmeticCheckboxesDiv); videoControlsMasterHolder.appendChild(cosmeticCheckboxesOuterDiv); squareCorners = JSON.parse(localStorage.getItem("squareCorners") || false); log("squareCorners:", squareCorners); squareCornersCheckbox.checked = squareCorners; makeCornersSquareOrNot(); squareCornersCheckbox.addEventListener("change", () => { squareCorners = squareCornersCheckbox.checked; localStorage.setItem("squareCorners", squareCorners); makeCornersSquareOrNot(); }); } function insertOverlayButton() { const existingControls = document.querySelector("#overlay-button-outer"); if (existingControls) { return; } // const overlayButtonDiv = document.createElement("div"); // overlayButtonDiv.id = "overlay-button-outer"; // overlayButtonDiv.classList.add("video-manipulator-outer-div"); // const overlayButton = document.createElement("button"); // overlayButton.id = "overlay-button"; // overlayButton.classList.add("video-manipulator-inner-item"); // overlayButton.textContent = "Show Overlays"; // overlayButton.title = 'Show or hide all video overlays, depending.' const overlayButtonInner = document.createElement("div"); overlayButtonInner.id = "overlay-button-inner"; overlayButtonInner.classList.add("video-manipulator-sub-item"); // overlayButtonInner.textContent = "Show Overlays"; overlayButtonInner.title = "Show or hide all video overlays, including title cards and links."; const overlayCheckbox = document.createElement("input"); overlayCheckbox.id = "overlay-checkbox"; overlayCheckbox.type = "checkbox"; const overlayCheckboxLabel = document.createElement("label"); overlayCheckboxLabel.htmlFor = "overlay-checkbox"; overlayCheckboxLabel.textContent = "Hide Overlays"; overlayButtonInner.appendChild(overlayCheckboxLabel); overlayButtonInner.appendChild(overlayCheckbox); // log('Attempting to insert overlay button...'); // overlayButtonDiv.appendChild(overlayButtonInner); // if (controlPanelOuter) { // controlPanelOuter.after(overlayButtonDiv); // log("Overlay button inserted."); // } else { // console.warn("controlPanelOuter element not found"); // } const divider = createDivider(); cosmeticCheckboxesDiv.appendChild(divider); cosmeticCheckboxesDiv.appendChild(overlayButtonInner); log("hideOverlaysVar:", hideOverlaysVar); if (hideOverlaysVar) { overlayCheckbox.checked = true; } else { overlayCheckbox.checked = false; } overlayCheckbox.addEventListener("change", () => { hideOverlaysVar = !overlayCheckbox.checked; log("Overlay checkbox has been clicked..."); if (hideOverlaysVar) { showOverlays(); } else { hideOverlays(); } }); } function insertChapterMover() { const chapterMoverInner = document.createElement("div"); chapterMoverInner.id = "chapter-mover-inner"; chapterMoverInner.classList.add("video-manipulator-sub-item"); chapterMoverInner.title = "Move the chapter selector from the description to a higher, more accessible location."; const moveChapterSelectorLabel = document.createElement("label"); moveChapterSelectorLabel.htmlFor = "move-chapter-selector-checkbox"; moveChapterSelectorLabel.textContent = "Move Chapter Selector"; const moveChapterSelectorCheckbox = document.createElement("input"); moveChapterSelectorCheckbox.id = "move-chapter-selector-checkbox"; moveChapterSelectorCheckbox.type = "checkbox"; const chapterMoverRadios = document.createElement("div"); chapterMoverRadios.id = "chapter-mover-radios"; chapterMoverRadios.classList.add("radio-div"); const moveChaptersLowRadio = document.createElement("input"); moveChaptersLowRadio.type = "radio"; moveChaptersLowRadio.name = "move-chapters-group"; moveChaptersLowRadio.id = "move-chapters-low-radio"; moveChaptersLowRadio.disabled = true; const moveChaptersLowLabel = document.createElement("label"); moveChaptersLowLabel.htmlFor = "move-chapters-low-radio"; moveChaptersLowLabel.textContent = "Low"; const moveChaptersMiddleRadio = document.createElement("input"); moveChaptersMiddleRadio.type = "radio"; moveChaptersMiddleRadio.name = "move-chapters-group"; moveChaptersMiddleRadio.id = "move-chapters-middle-radio"; moveChaptersMiddleRadio.disabled = true; const moveChaptersMiddleLabel = document.createElement("label"); moveChaptersMiddleLabel.htmlFor = "move-chapters-middle-radio"; moveChaptersMiddleLabel.textContent = "Middle"; const moveChaptersHighRadio = document.createElement("input"); moveChaptersHighRadio.type = "radio"; moveChaptersHighRadio.name = "move-chapters-group"; moveChaptersHighRadio.id = "move-chapters-high-radio"; moveChaptersHighRadio.disabled = true; const moveChaptersHighLabel = document.createElement("label"); moveChaptersHighLabel.htmlFor = "move-chapters-high-radio"; moveChaptersHighLabel.textContent = "High"; chapterMoverRadios.appendChild(moveChaptersLowRadio); chapterMoverRadios.appendChild(moveChaptersLowLabel); chapterMoverRadios.appendChild(moveChaptersMiddleRadio); chapterMoverRadios.appendChild(moveChaptersMiddleLabel); chapterMoverRadios.appendChild(moveChaptersHighRadio); chapterMoverRadios.appendChild(moveChaptersHighLabel); chapterMoverInner.appendChild(moveChapterSelectorLabel); chapterMoverInner.appendChild(moveChapterSelectorCheckbox); chapterMoverInner.appendChild(chapterMoverRadios); const divider = createDivider(); cosmeticCheckboxesDiv.appendChild(divider); cosmeticCheckboxesDiv.appendChild(chapterMoverInner); // Function to enable or disable chapter radio buttons function toggleChapterRadios(arg) { const radios = [moveChaptersLowRadio, moveChaptersMiddleRadio, moveChaptersHighRadio]; // Enable or disable based on the argument if (arg === "enable") { radios.forEach((radio) => (radio.disabled = false)); } else if (arg === "disable") { radios.forEach((radio) => (radio.disabled = true)); } } // Retrieve and set values from localStorage const savedChapterMoving = localStorage.getItem("chapterMoving"); const savedChapterPosition = localStorage.getItem("chapterPosition"); // Set chapterMoving state and update checkbox if (savedChapterMoving !== null) { chapterMoving = JSON.parse(savedChapterMoving); // Parse as boolean moveChapterSelectorCheckbox.checked = chapterMoving; // Set the checkbox } // Enable/Disable radios based on chapterMoving if (chapterMoving) { toggleChapterRadios("enable"); } else { toggleChapterRadios("disable"); } // Set chapterPosition and select the corresponding radio if (savedChapterPosition) { chapterPosition = savedChapterPosition; } else { chapterPosition = "low"; // Default position } // Select the correct radio based on saved position switch (chapterPosition) { case "low": moveChaptersLowRadio.checked = true; break; case "middle": moveChaptersMiddleRadio.checked = true; break; case "high": moveChaptersHighRadio.checked = true; break; } // Add event listeners to update saved values moveChapterSelectorCheckbox.addEventListener("change", () => { chapterMoving = moveChapterSelectorCheckbox.checked; localStorage.setItem("chapterMoving", JSON.stringify(chapterMoving)); // Enable or disable radios based on the checkbox state if (chapterMoving) { moveChapters(); toggleChapterRadios("enable"); } else { restoreOriginalChapterPosition(); toggleChapterRadios("disable"); } }); chapterMoverRadios.addEventListener("change", (event) => { chapterPosition = event.target.id.replace("move-chapters-", "").replace("-radio", ""); localStorage.setItem("chapterPosition", chapterPosition); moveChapters(); }); let originalChapterPosition = { parent: null, sibling: null }; function storeOriginalChapterPosition() { if (chapters) { originalChapterPosition.parent = chapters.parentNode; // Store the parent node originalChapterPosition.sibling = chapters.previousSibling; // Store the sibling before it (if any) } } moveChapters = function () { if (!chapters) { if (chapterMoveAttempts < chapterMoveAttemptsMax) { setTimeout(() => { moveChapters(); }, 1000); chapterMoveAttempts += 1; } else { log("Chapters element not found. No movement performed."); } return; } toggleMovedChaptersStyles("on"); // Check the current value of chapterPosition and move chapters accordingly switch (chapterPosition) { case "low": // Move chapters after topRow topRow.parentNode.insertBefore(chapters, topRow.nextSibling); // chapters.style.marginTop = ''; break; case "high": // Move chapters before topRow topRow.parentNode.insertBefore(chapters, topRow); // chapters.style.marginTop = ''; break; case "middle": // Move chapters after actions // chapters.style.marginTop = '10px'; actions.parentNode.insertBefore(chapters, actions.nextSibling); break; default: log("Invalid chapter position."); } }; establishChaptersAttempts = 0; maxEstablishChaptersAttempts = 60; establishChaptersAttemptsInterval = 250; let establishChaptersInProgress = false; let establishChaptersInQ = false; establishChapters = function () { if (establishChaptersInProgress) { establishChaptersInQ = true; return; } else { establishChaptersInProgress = true; setTimeout(() => { establishChaptersInProgress = false; }, 250); } const existingChapters = document.querySelector( "#above-the-fold ytd-horizontal-card-list-renderer[modern-chapters]" ); const unmovedChapters = document.querySelector( "#bottom-row ytd-structured-description-content-renderer ytd-horizontal-card-list-renderer[modern-chapters]" ); if (existingChapters && existingChapters != chapters && existingChapters != unmovedChapters) { log("Removing existing chapters from previous video..."); existingChapters.remove(); } log("Establishing chapters..."); chapters = document.querySelector( "#bottom-row ytd-structured-description-content-renderer ytd-horizontal-card-list-renderer[modern-chapters]" ); if (chapters) { storeOriginalChapterPosition(); if (chapterMoving) { moveChapters(); } } else { setTimeout(() => { establishChapters(); establishChaptersAttempts += 1; if (establishChaptersAttempts >= maxEstablishChaptersAttempts) { establishChaptersAttemptsInterval = 2500; } }, establishChaptersAttemptsInterval); } if (establishChaptersInQ) { establishChaptersInQ = false; setTimeout(() => { establishChapters(); }, 250); } }; establishChapters(); function toggleMovedChaptersStyles(arg) { if (arg === "on") { chapters.style.borderTop = "unset"; chapters.style.padding = "0"; chapters.style.maxWidth = "100%"; } if (arg === "off") { // chapters.style.marginTop = ''; chapters.style.borderTop = ""; chapters.style.padding = ""; chapters.style.maxWidth = ""; } } function restoreOriginalChapterPosition() { log("Restoring original chapter selector position..."); if (chapters && originalChapterPosition.parent) { if (originalChapterPosition.sibling) { originalChapterPosition.parent.insertBefore(chapters, originalChapterPosition.sibling); } else { originalChapterPosition.parent.appendChild(chapters); } toggleMovedChaptersStyles("off"); } } } insertSquareCornerControl(); insertOverlayButton(); insertChapterMover(); } function addPressEffectToButtons() { // Iterate over each button in the buttonRefs object Object.values(buttonRefs).forEach((btn) => { btn.addEventListener("click", () => { // Add the 'just-pressed' class to the button btn.classList.add("just-pressed"); // Remove the 'just-pressed' class after 1 second setTimeout(() => { btn.classList.remove("just-pressed"); }, 250); }); }); } // let insertAttempts = 0; // function handleVideoSizeChangeFunction() { // // Add the temporary CSS to set the zoom transition time to 0 // video.style.transition = "zoom 0s"; // // Wait 25ms to ensure the CSS takes effect before zooming // setTimeout(() => { // handleAspectRatio(aspectRatio, null, "zoom"); // log("Video size changed"); // setTimeout(() => { // // Remove the temporary transition style // video.style.transition = ""; // }, 25); // }, 25); // } function monitorVideoSize() { video = document.querySelector("video.html5-main-video"); const handleVideoSizeChange = () => { // Add the temporary CSS to set the zoom transition time to 0 video.style.transition = "zoom 0s"; // Wait 25ms to ensure the CSS takes effect before zooming setTimeout(() => { handleAspectRatio(aspectRatio, null, "zoom"); log("Video size changed"); setTimeout(() => { // Remove the temporary transition style video.style.transition = ""; }, 25); }, 25); }; if (video) { const resizeObserver = new ResizeObserver(handleVideoSizeChange); const windowResizeObserver = new ResizeObserver(handleVideoSizeChange); resizeObserver.observe(video); windowResizeObserver.observe(document.body); } else { console.error("Video element not found"); } } function setCompressorValues() { log("compressorValues: " + JSON.stringify(compressorValues)); thresholdGain.gain.setValueAtTime(1 / Math.pow(2, compressorValues.threshold / 6), audioContext.currentTime); finalGain.gain.setValueAtTime(Math.pow(2, compressorValues.threshold / 6), audioContext.currentTime); //preGain.gain.setValueAtTime(Math.pow(2, compressorValues.preGain / 6), audioContext.currentTime); preGain.gain.setValueAtTime(compressorValues.preGain, audioContext.currentTime); compressor.threshold.setValueAtTime(compressorValues.knee * -1 - 1, audioContext.currentTime); // Threshold compressor.knee.setValueAtTime(compressorValues.knee, audioContext.currentTime); // Soft knee compressor.ratio.setValueAtTime(compressorValues.ratio, audioContext.currentTime); // High ratio for limiting compressor.attack.setValueAtTime(compressorValues.attack, audioContext.currentTime); // Fast attack compressor.release.setValueAtTime(compressorValues.release, audioContext.currentTime); // Release time } function initiateCompressor() { if (!audioContext) { // Create an AudioContext audioContext = new (window.AudioContext || window.webkitAudioContext)(); // Add a pre-gain (included in compressor values) preGain = audioContext.createGain(); // Add threshold gain to simulate the threshold thresholdGain = audioContext.createGain(); // Create a DynamicsCompressorNode (limiter with soft knee) compressor = audioContext.createDynamicsCompressor(); // Create a gain node for final output level control (included in compressor values) finalGain = audioContext.createGain(); // finalGain.gain.setValueAtTime(compressorValues.finalGain, audioContext.currentTime); // Default gain is 1 (no change) } connectCompressor(); // Set the compressor values using the new function setCompressorValues(); } function changeCompressorValue(property, value, rawValue) { log("Passed values to change compressor values: ", property, value, rawValue); if (property === "preGain") { preGain.gain.setValueAtTime(value, audioContext.currentTime); log("PreGain value: " + preGain.gain.value); // } else if (compressor.hasOwnProperty(property)) { // compressor[property].setValueAtTime(value, audioContext.currentTime); // } else { // console.error('Invalid property:', property); // } } else if (property === "knee") { compressor[property].setValueAtTime(value, audioContext.currentTime); compressor.threshold.setValueAtTime(value - 1, audioContext.currentTime); } else if (property === "threshold") { thresholdGain.gain.setValueAtTime(1 / value, audioContext.currentTime); // Log the threshold gain value log("Threshold Gain value set to:", thresholdGain.gain.value); finalGain.gain.setValueAtTime(value, audioContext.currentTime); log("Final Gain value set to:", finalGain.gain.value); } else { compressor[property].setValueAtTime(value, audioContext.currentTime); } } function connectCompressor() { findVideo(); if (video) { // Check if there's a video and no source node if (source) { log("Source found. Disconnecting..."); source.disconnect(); // Disconnect the previous source (if any) } else { // Create a MediaElementSourceNode to use the video's audio source = audioContext.createMediaElementSource(video); } // Connect the video audio to the compressor source.connect(preGain); preGain.connect(thresholdGain); thresholdGain.connect(compressor); // Connect the compressor to the final gain control compressor.connect(finalGain); // Connect the final gain control to the audio context's destination (speakers) finalGain.connect(audioContext.destination); log("Video is being processed with compression and gain control."); } else { log("No video found or source already connected."); } } function disconnectCompressor() { if (source) { source.disconnect(); // Disconnect the video audio source preGain.disconnect(); thresholdGain.disconnect(); compressor.disconnect(); // Disconnect the compressor finalGain.disconnect(); // Disconnect the gain node source.connect(audioContext.destination); log("Compressor and nodes disconnected."); // Reset source to null to allow future processing // source = null; } } const timeVariables = { seekTime: null, requestedCurrentTime: null, loopStartTime: null, loopEndTime: null, lastMeasuredTime: null }; const elementConfigMap = { "time-seek-input": "seekTime", "get-time-input": "requestedCurrentTime", "loop-start-input": "loopStartTime", "loop-end-input": "loopEndTime", "last-measured-time-input": "lastMeasuredTime" }; // Global object to store previous values of inputs const previousInputValues = {}; // Function to handle the time format input function formatTimeInput(inputElement) { inputElement.placeholder = "HH:MM:SS.mil"; inputElement.addEventListener("input", (event) => { let input = event.target.value; let inputId = event.target.id; // Get the input element's ID // Store the previous value in the global object if it's the first time we're checking it if (!previousInputValues[inputId]) { previousInputValues[inputId] = input; } // Get the previous valid input value from the global object let previousInput = previousInputValues[inputId]; // Check for decimal points and colons in the input string let decimalIndexes = []; let colonIndexes = []; // Loop through the string and collect all indexes of `.` and `:` for (let i = 0; i < input.length; i++) { if (input[i] === ".") { decimalIndexes.push(i); } if (input[i] === ":") { colonIndexes.push(i); } } // If there's a decimal point before any colon for (let decimalIndex of decimalIndexes) { for (let colonIndex of colonIndexes) { if (decimalIndex < colonIndex) { log("Blocked: Decimal comes before Colon"); event.preventDefault(); event.target.value = previousInput; // Revert to previous valid input return; } } } // If there's a colon after a decimal point for (let colonIndex of colonIndexes) { for (let decimalIndex of decimalIndexes) { if (colonIndex > decimalIndex) { log("Blocked: Colon comes after Decimal"); event.preventDefault(); event.target.value = previousInput; // Revert to previous valid input return; } } } // Remove any invalid characters (anything other than digits, colons, or periods) input = input.replace(/[^0-9:.]/g, ""); // Split input based on period and colon let groups = input.split("."); // Ensure that the colon part is formatted as HH:MM:SS let colonGroups = groups[0].split(":"); // Limit to no more than 3 groups (HH:MM:SS) if (colonGroups.length > 3) { colonGroups = colonGroups.slice(0, 3); } // Ensure that each group after the first one (MM, SS) has no more than 2 digits if (colonGroups[1] && colonGroups[1].length > 2) { colonGroups[1] = colonGroups[1].slice(0, 2); } if (colonGroups[2] && colonGroups[2].length > 2) { colonGroups[2] = colonGroups[2].slice(0, 2); } // Rebuild the colon-separated time part (HH:MM:SS) input = colonGroups.join(":"); // Handle the decimal part (milliseconds) if (groups.length > 1) { let decimalGroup = groups[1].slice(0, 3); // Limit to 3 digits input += "." + decimalGroup; } // Update the input field with the valid value event.target.value = input; // Update the global object with the current valid value previousInputValues[inputId] = input; }); // Ensure proper formatting when the user leaves the input field (blur event) inputElement.addEventListener("blur", () => { let input = inputElement.value; let inputId = event.target.id; // Get the input element's ID if (!input || input === null) { log("No input and nothing to convert."); const variableName = elementConfigMap[inputId]; timeVariables[variableName] = null; log(`Updated timeVariables[${variableName}] to ${timeVariables[variableName]}.`); return; } // Split the input into colon-separated parts and decimal part let groups = input.split(":"); let decimalGroup = ""; // If there's a decimal part, separate it out if (groups[groups.length - 1].includes(".")) { const lastGroupParts = groups[groups.length - 1].split("."); decimalGroup = lastGroupParts[1] || ""; groups[groups.length - 1] = lastGroupParts[0]; // Keep the part before the decimal } // Pad empty groups with "00" for (let i = 0; i < groups.length; i++) { if (groups[i] === "") { groups[i] = "00"; // Fill empty group with "00" } else if (i > 0 && groups[i].length < 2) { groups[i] = groups[i].padStart(2, "0"); // Pad with leading zero if needed (except for the first group) } } // Rebuild the string with fixed colon-separated groups input = groups.join(":"); // Handle the decimal part and ensure it's at most 3 digits if (decimalGroup) { decimalGroup = decimalGroup.slice(0, 3); // Limit to 3 digits after the decimal point input += "." + decimalGroup; } // Update the input field with the final value inputElement.value = input; // Check if the value is valid and convert accordingly if (isValidTimeFormat(input)) { // log('The input is valid.'); const rawSeconds = convertToRawSeconds(input, inputId); // You can store rawSeconds in a map or take other actions here // Store the rawSeconds in the appropriate variable using the elementConfigMap storeTimeVariable(rawSeconds, inputId); // // Get the corresponding variable name from the map // const variableName = elementConfigMap[inputId]; // if (variableName && timeVariables.hasOwnProperty(variableName)) { // // Update the actual variable // timeVariables[variableName] = rawSeconds; // log(`Updated timeVariables[${variableName}] to ${input}.`); // } else { // console.error(`No matching entry for inputId ${inputId} in elementConfigMap`); // } } else { console.error(`Invalid time format for input ${inputId}`); } if (inputId === "loop-start-input" || inputId === "loop-end-input") { let alertTriggered = getLoopLength(); if ( isValidTimeFormat && !isNaN(loopLength) && !alertTriggered && loopStartInput.value != null && loopEndInput.value != null && looping ) { startLoopingButton.classList.add("blinking"); clearTimeout(startLoopingBlink); startLoopingBlink = setTimeout(() => { startLoopingButton.classList.remove("blinking"); }, 5000); // Adjust duration based on your preference } } }); } function storeTimeVariable(value, id) { // Get the corresponding variable name from the map const variableName = elementConfigMap[id]; if (variableName && timeVariables.hasOwnProperty(variableName)) { // Update the actual variable timeVariables[variableName] = parseFloat(value); log(`Updated timeVariables[${variableName}] to ${timeVariables[variableName]}.`); } else { console.error(`No matching entry for inputId ${inputId} in elementConfigMap`); } } function convertToRawSeconds(timeString, elementId) { log(`Converting time for element ${elementId}: ${timeString}`); // Remove any extra whitespace timeString = timeString.trim(); // Split the string by the colon separator const parts = timeString.split(":"); let hours = 0, minutes = 0, seconds = 0; // If there is no colon, it's just seconds if (parts.length === 1) { // If there's a decimal, we need to split that as well const [sec, milli] = parts[0].split("."); seconds = parseInt(sec, 10) || 0; // Add milliseconds if present if (milli) { seconds += parseInt(milli, 10) / Math.pow(10, milli.length); } } else { // If there are parts, we process them if (parts.length === 2) { // Minutes:Seconds minutes = parseInt(parts[0], 10) || 0; seconds = parseFloat(parts[1]) || 0; } else if (parts.length === 3) { // Hours:Minutes:Seconds hours = parseInt(parts[0], 10) || 0; minutes = parseInt(parts[1], 10) || 0; seconds = parseFloat(parts[2]) || 0; } // Handle decimal in the last part (if any) if (seconds.toString().includes(".")) { const [sec, milli] = seconds.toString().split("."); seconds = parseInt(sec, 10) || 0; const milliseconds = parseInt(milli, 10) || 0; seconds += milliseconds / Math.pow(10, milli.length); } } // Now, calculate the total in raw seconds const rawSeconds = hours * 3600 + minutes * 60 + seconds; log(`Converted to raw seconds: ${rawSeconds}`); // You can store or do other things with rawSeconds here return rawSeconds; } function convertToTimeFormat(rawSeconds, elementId) { log(`Converting raw seconds for element ${elementId}: ${rawSeconds}`); // Calculate hours, minutes, and seconds (with potential decimal) const hours = Math.floor(rawSeconds / 3600); const minutes = Math.floor((rawSeconds % 3600) / 60); let seconds = rawSeconds % 60; // This could be a floating-point value // Format the time components into a string let timeString = `${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`; log(`Converted to time format (before modification): ${timeString}`); // Split into main part (HH:MM:SS) and decimal part let [mainPart, decimalPart] = timeString.split("."); // Trim leading zeros and colons from the main part while (mainPart.length > 1 && (mainPart.startsWith("0") || mainPart.startsWith(":"))) { mainPart = mainPart.slice(1); // Remove the first character (0 or :) } // If there's no decimal part, set it to an empty string if (decimalPart !== undefined) { // Prefix "0." to ensure it's a number let decimalValue = parseFloat("0." + decimalPart); // Round to 3 decimal places (you can change this to any precision you need) decimalValue = Math.round(decimalValue * 1000) / 1000; // Convert back to string, removing trailing zeros decimalPart = decimalValue.toString().split(".")[1] || ""; // Trim trailing zeros from the decimal part while (decimalPart.length > 1 && decimalPart.endsWith("0")) { decimalPart = decimalPart.slice(0, -1); // Remove the last character (0) } // Combine the main part and the decimal part back timeString = `${mainPart}.${decimalPart}`; } else { timeString = `${mainPart}`; } log(`Final time string: ${timeString}`); return timeString; } function isValidTimeFormat(timeString) { log("Raw input value:", timeString); // Log the raw value log("Trimmed value:", timeString.trim()); // Log the value after trimming whitespace const regex = /^(\d+)(?::(\d{2}))?(?::(\d{2}))?(?:\.(\d+))?$/; log(regex.test(timeString)); log(regex.test(timeString.trim())); return regex.test(timeString.trim()); } function getCurrentTime(target) { log("Getting current time..."); // Check if the video element is valid and exists const currentTime = video.currentTime; // Get the current time of the video if (!currentTime) { return; } log(`Current video time (raw): ${currentTime} seconds`); const convertedTime = convertToTimeFormat(currentTime, "get-time-input"); log(`Current video time (converted): ${convertedTime} seconds`); storeTimeVariable(currentTime, "get-time-input"); target.value = convertedTime; } let loopLength; function getLoopLength(start) { log("Loop start:", timeVariables.loopStartTime); log("Loop end:", timeVariables.loopEndTime); if (timeVariables.loopEndTime == null || timeVariables.loopStartTime == null) { log("No loop length to get."); return true; } loopLength = timeVariables.loopEndTime - timeVariables.loopStartTime; log("Loop length:", loopLength); let alertText; let alertTriggered = false; if (loopLength < 0) { // log('Condition 1.'); looping = false; alertTriggered = true; alertText = "The loop length is negative. Looping cannot take place."; } else if (loopLength === 0) { // log('Condition 2.'); looping = false; alertTriggered = true; alertText = "The loop length is 0. Looping cannot take place."; } else if (loopLength <= 1 / 30) { // log('Condition 3.'); looping = false; alertTriggered = true; alertText = "The loop length is too short. Looping cannot take place."; } else { // log('Condition 4.'); alertTriggered = false; } if (alertTriggered) { log(alertText); if (start) { loopErrorSpan.textContent = alertText; loopErrorHolderOuter.removeAttribute("invisible"); if (hideLoopErrorTimeout) { clearTimeout(hideLoopErrorTimeout); // Clear existing timeout } hideLoopErrorTimeout = setTimeout(() => { loopErrorHolderOuter.setAttribute("invisible", ""); }, 5000); return alertTriggered; } } } let hideLoopErrorTimeout; let measureRateInput; let lastMeasuredTimeInput; let loopStartInput; let loopEndInput; let startLoopingButton; let loopErrorSpan; let loopErrorPreSpan; let loopErrorHolderOuter; function insertSeekAndLoopControls() { const timeFormattedInputs = []; // Array to store the time-formatted inputs // const emptySpanner = insertEmptySpanner(); // videoControlsMasterHolder.appendChild(emptySpanner); const videoSeekControls = document.createElement("div"); videoSeekControls.id = "video-seek-controls"; videoSeekControls.classList.add("video-manipulator-inner-item"); videoSeekControlsOuterDiv = document.createElement("div"); videoSeekControlsOuterDiv.id = "video-seek-controls-outer-div"; videoSeekControlsOuterDiv.classList.add("video-manipulator-outer-div"); const videoLoopControls = document.createElement("div"); videoLoopControls.id = "video-loop-controls"; videoLoopControls.classList.add("video-manipulator-inner-item"); videoLoopControlsOuterDiv = document.createElement("div"); videoLoopControlsOuterDiv.id = "video-loop-controls-outer-div"; videoLoopControlsOuterDiv.classList.add("video-manipulator-outer-div"); // Jump section const jumpSection = document.createElement("div"); jumpSection.id = "jump-section"; jumpSection.classList.add("video-manipulator-sub-item"); const timeSeekButton = document.createElement("button"); timeSeekButton.id = "time-seek-button"; timeSeekButton.textContent = "Jump to"; buttonRefs[timeSeekButton.id] = timeSeekButton; const timeSeekInput = document.createElement("input"); timeSeekInput.id = "time-seek-input"; timeSeekInput.type = "text"; // Only numbers allowed timeSeekInput.classList.add("time-formatted"); timeFormattedInputs.push(timeSeekInput); jumpSection.appendChild(timeSeekButton); jumpSection.appendChild(timeSeekInput); // Divider const divider = createDivider(); // Get time section const getTimeSection = document.createElement("div"); getTimeSection.id = "get-time-section"; getTimeSection.classList.add("video-manipulator-sub-item"); const getTimeButton = document.createElement("button"); getTimeButton.id = "get-time-button"; getTimeButton.textContent = "Get Current Time"; getTimeButton.title = "Get the current time for the video, with millisecond precision."; buttonRefs[getTimeButton.id] = getTimeButton; const getTimeInput = document.createElement("input"); getTimeInput.id = "get-time-input"; getTimeInput.type = "text"; // Only numbers allowed getTimeInput.readOnly = true; getTimeInput.classList.add("time-formatted"); timeFormattedInputs.push(getTimeInput); const getTimeUrlButton = document.createElement("button"); getTimeUrlButton.id = "get-time-url-button"; getTimeUrlButton.textContent = "Copy URL at current time"; getTimeUrlButton.title = "Copy the URL of the video at the current time to the clipboard, with millisecond precision."; buttonRefs[getTimeUrlButton.id] = getTimeUrlButton; getTimeSection.appendChild(getTimeButton); getTimeSection.appendChild(getTimeInput); // The following is disabled because YouTube just rounds anyway. // getTimeSection.appendChild(getTimeUrlButton); // Divider const divider2 = createDivider(); // Loop section const loopSection = document.createElement("div"); loopSection.id = "loop-section"; loopSection.classList.add("video-manipulator-sub-item"); const loopSpan = document.createElement("span"); loopSpan.textContent = "Loop"; loopStartInput = document.createElement("input"); loopStartInput.id = "loop-start-input"; loopStartInput.type = "text"; // Only numbers allowed loopStartInput.classList.add("time-formatted"); timeFormattedInputs.push(loopStartInput); const loopEndSpan = document.createElement("span"); loopEndSpan.textContent = "to"; loopEndInput = document.createElement("input"); loopEndInput.id = "loop-end-input"; loopEndInput.type = "text"; // Only numbers allowed loopEndInput.classList.add("time-formatted"); timeFormattedInputs.push(loopEndInput); const loopingButtonsOuterDiv = document.createElement("div"); loopingButtonsOuterDiv.id = "looping-buttons-outer-div"; loopingButtonsOuterDiv.classList.add("video-manipulator-sub-item"); const loopingButtonsInnerDiv = document.createElement("div"); loopingButtonsInnerDiv.id = "looping-buttons-inner-div"; loopingButtonsInnerDiv.classList.add("video-manipulator-sub-item"); startLoopingButton = document.createElement("button"); startLoopingButton.id = "start-looping-button"; startLoopingButton.textContent = "Start Looping"; buttonRefs[startLoopingButton.id] = startLoopingButton; const stopLoopingButton = document.createElement("button"); stopLoopingButton.id = "stop-looping-button"; stopLoopingButton.textContent = "Stop Looping"; buttonRefs[stopLoopingButton.id] = stopLoopingButton; loopErrorHolderOuter = document.createElement("div"); loopErrorHolderOuter.id = "loop-error-holder-outer"; loopErrorHolderOuter.setAttribute("invisible", ""); const loopErrorHolderInner = document.createElement("div"); loopErrorHolderInner.id = "loop-error-holder-inner"; loopErrorPreSpan = document.createElement("div"); loopErrorPreSpan.id = "loop-error-pre-span"; loopErrorSpan = document.createElement("div"); loopErrorSpan.id = "loop-error-span"; loopErrorSpan.textContent = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " + "Vivamus lacinia odio vitae vestibulum."; loopErrorHolderInner.appendChild(loopErrorPreSpan); loopErrorHolderInner.appendChild(loopErrorSpan); loopErrorHolderOuter.appendChild(loopErrorHolderInner); loopingButtonsInnerDiv.appendChild(startLoopingButton); loopingButtonsInnerDiv.appendChild(stopLoopingButton); loopingButtonsOuterDiv.appendChild(loopingButtonsInnerDiv); loopingButtonsOuterDiv.appendChild(loopErrorHolderOuter); const loopForeverRadio = document.createElement("input"); loopForeverRadio.id = "loop-forever-radio"; loopForeverRadio.type = "radio"; loopForeverRadio.name = "loop-type"; loopForeverRadio.checked = true; const loopForeverLabel = document.createElement("label"); loopForeverLabel.setAttribute("for", "loop-forever-radio"); loopForeverLabel.textContent = "Loop forever"; const loopForeverDiv = document.createElement("div"); loopForeverDiv.appendChild(loopForeverRadio); loopForeverDiv.appendChild(loopForeverLabel); loopForeverDiv.classList.add("radio-div"); const loopXTimesRadio = document.createElement("input"); loopXTimesRadio.id = "loop-x-times-radio"; loopXTimesRadio.type = "radio"; loopXTimesRadio.name = "loop-type"; const loopXTimesLabel = document.createElement("label"); loopXTimesLabel.setAttribute("for", "loop-x-times-radio"); // Create the input element const loopTimesInput = document.createElement("input"); loopTimesInput.id = "loop-times-input"; loopTimesInput.type = "number"; loopTimesInput.min = 1; // Create the label text node const labelText = document.createTextNode("Loop "); // Clear existing content and append the label text and input element loopXTimesLabel.textContent = ""; // Clear any previous content loopXTimesLabel.appendChild(labelText); loopXTimesLabel.appendChild(loopTimesInput); loopXTimesLabel.appendChild(document.createTextNode(" times")); const loopTimesDiv = document.createElement("div"); loopTimesDiv.appendChild(loopXTimesRadio); loopTimesDiv.appendChild(loopXTimesLabel); loopTimesDiv.classList.add("radio-div"); loopSection.appendChild(loopSpan); loopSection.appendChild(loopStartInput); loopSection.appendChild(loopEndSpan); loopSection.appendChild(loopEndInput); loopSection.appendChild(loopingButtonsOuterDiv); loopSection.appendChild(loopForeverDiv); loopSection.appendChild(loopTimesDiv); // Append all sections to the outer div videoSeekControls.appendChild(jumpSection); videoSeekControls.appendChild(divider); videoSeekControls.appendChild(getTimeSection); // videoSeekControls.appendChild(divider2); // Append the loop controls separately videoLoopControls.appendChild(loopSection); // Insert diagnostic section if timeLoopDebug is true if (timeLoopDebug) { const diagnosticDivider = createDivider(); const diagnosticDiv = document.createElement("div"); diagnosticDiv.id = "diagnostic-div"; diagnosticDiv.classList.add("video-manipulator-sub-item"); lastMeasuredTimeInput = document.createElement("input"); lastMeasuredTimeInput.id = "last-measured-time-input"; lastMeasuredTimeInput.type = "text"; lastMeasuredTimeInput.readOnly = true; // Cannot be edited lastMeasuredTimeInput.classList.add("time-formatted"); timeFormattedInputs.push(lastMeasuredTimeInput); const measuringSpan = document.createElement("span"); measuringSpan.textContent = "Measuring"; // Create the measure rate input measureRateInput = document.createElement("input"); measureRateInput.id = "measure-rate-input"; measureRateInput.type = "number"; // Accepts only numbers measureRateInput.min = 1; // Min value 1 measureRateInput.max = 240; // Max value 240 measureRateInput.step = "1"; // Only accepts integer steps measureRateInput.value = 1; // Default value (could be set to whatever you'd like) const timesPerSecondSpan = document.createElement("span"); timesPerSecondSpan.textContent = "times per second"; diagnosticDiv.appendChild(lastMeasuredTimeInput); diagnosticDiv.appendChild(measuringSpan); diagnosticDiv.appendChild(measureRateInput); diagnosticDiv.appendChild(timesPerSecondSpan); // Append to the videoLoopControlsOuterDiv videoLoopControls.appendChild(diagnosticDivider); videoLoopControls.appendChild(diagnosticDiv); measureRateInput.addEventListener("input", (event) => { // Clear the previous timer if there's one clearTimeout(debounceCheckTimeIntervalTimer); // Set a new timer for debounce (1 second delay) debounceCheckTimeIntervalTimer = setTimeout(() => { let timeDivider = measureRateInput.value; // Check if timeDivider is undefined, NaN, or less than or equal to zero if (isNaN(timeDivider) || timeDivider <= 0) { timeDivider = 0.000001; // Set to a small positive value } // Calculate the new interval based on input value checkTimeIntervalTime = 1000 / timeDivider; log(`New rate: ${measureRateInput.value} actions per second.`); // Restart the interval with the new rate startCheckTimeInterval(); }, 1000); // Debounce by 1 second }); } // Finally, append everything to the outer div videoSeekControlsOuterDiv.appendChild(videoSeekControls); videoLoopControlsOuterDiv.appendChild(videoLoopControls); // Append to topRow const playbackRateControls = document.querySelector( ".video-manipulator-outer-div#playback-rate-controls-outer" ); // the element after which you want to append videoControlsMasterHolder.appendChild(videoSeekControlsOuterDiv); videoControlsMasterHolder.appendChild(videoLoopControlsOuterDiv); // if (playbackRateControls.nextSibling) { // // If there is a next sibling, insert the new element before the next sibling // topRow.insertBefore(videoSeekControlsOuterDiv, playbackRateControls.nextSibling); // } else { // // If there is no next sibling, append the new element at the end of the parent // topRow.appendChild(videoSeekControlsOuterDiv); // } timeFormattedInputs.forEach((input) => { formatTimeInput(input); // Apply formatting }); // Add event listener to the button getTimeButton.addEventListener("click", function () { getCurrentTime(getTimeInput); }); // Disabled because YouTube just rounds it anyway. // Add the event listener // getTimeUrlButton.addEventListener("click", () => { // getCurrentTime(getTimeInput); // // Get the current time of the video in seconds, rounded to 3 decimal places // const currentTime = video.currentTime.toFixed(3); // // Get the current URL (from window.location.href) // let currentUrl = window.location.href; // // Remove all query parameters except for 'v' (the video ID) // const urlParams = new URLSearchParams(window.location.search); // // urlParams.delete('t'); // Remove any existing 't' parameter // currentUrl = currentUrl.split("?")[0] + "?" + `v=${urlParams.get("v")}`; // // Retain only the video ID and other relevant parameters // // Add the 't' parameter with the current time // currentUrl += `&t=${currentTime}` + "s"; // // Copy the final URL to the clipboard // navigator.clipboard // .writeText(currentUrl) // .then(() => { // log(`Video URL at current time (${currentTime}s): ${currentUrl}`); // // alert('URL copied to clipboard!'); // }) // .catch((err) => { // console.error("Error copying to clipboard: ", err); // }); // }); // Add event listener for the timeSeekButton timeSeekButton.addEventListener("click", () => { // Check if timeVariables.seekTime has a valid value if (timeVariables.seekTime !== undefined) { // Set the video time to the value stored in timeVariables.seekTime video.currentTime = timeVariables.seekTime; log(`Jumping to time: ${timeVariables.seekTime} seconds.`); } else { console.error(`No valid time found in timeVariables.seekTime.`); } }); startLoopingButton.addEventListener("click", () => { // if () const dontStart = getLoopLength(true); log("dontStart:", dontStart); if (dontStart) { return; } looping = true; loopIterations = 0; startLoopingButton.classList.add("active"); startLoopingButton.classList.remove("blinking"); // startCheckTimeInterval(); log("dontStart:", dontStart); loopIntervalStart(); video.play(); }); stopLoopingButton.addEventListener("click", () => { looping = false; startLoopingButton.classList.remove("active"); if (checkTimeIntervalId) clearInterval(checkTimeIntervalId); loopIntervalEnd(); }); loopForeverRadio.addEventListener("click", () => { loopMode = "forever"; }); loopXTimesRadio.addEventListener("click", () => { loopMode = "x-times"; }); loopTimesInput.addEventListener("blur", () => { loopTimes = Math.round(parseFloat(loopTimesInput.value)); if (loopTimes != parseFloat(loopTimesInput.value)) { loopTimesInput.value = loopTimes; } loopXTimesRadio.checked = true; loopMode = "x-times"; }); // Create a MutationObserver to watch for changes in text content const loopErrorMutationObserver = new MutationObserver(() => { // Log the change in text content log("Text content of loopErrorSpan has changed."); // Adjust loopErrorPreSpan width and height based on the new content of loopErrorSpan loopErrorPreSpan.style.width = `${loopErrorSpan.offsetWidth}px`; loopErrorPreSpan.style.height = `${loopErrorSpan.offsetHeight}px`; }); // Configure the observer to watch for text content changes (childList or characterData) const config = { childList: true, // Watch for additions or removals of child nodes characterData: true, // Watch for changes to the text content subtree: true // Watch all descendants, not just immediate children }; // Start observing loopErrorSpan for text content changes loopErrorMutationObserver.observe(loopErrorSpan, config); loopErrorPreSpan.style.width = `${loopErrorSpan.offsetWidth}px`; loopErrorPreSpan.style.height = `${loopErrorSpan.offsetHeight}px`; getTimeInput.addEventListener("focus", () => { getTimeInput.select(); }); } let loopIntervalTime; let loopIntervalId; let deCapo = false; let tooFar = false; let loopTimes = 0; let loopIterations = 0; let loopInitialInterval; let loopStartTime; let loopEndTime; // Function to evaluate loop time function loopTimeEvaluator(timeLength) { deCapo = false; // Reset deCapo before starting evaluations let intervalLength; tooFar = false; // Boundary Conditions for Interval Duration if (timeLength > 2) { intervalLength = 1000; // 1 second } else if (timeLength <= 2 && timeLength > 0.25) { intervalLength = 250; // 250 ms } else if (timeLength <= 0.25) { intervalLength = timeLength * 1000; // Set interval to the remaining time deCapo = true; tooFar = timeLength < 0; // Check if the time goes beyond the end } return { intervalLength, deCapo, tooFar }; } // Function to evaluate remaining time and adjust interval function loopRemainingTimeCalculator(currentTime) { log("loopRemainingTimeCalculator..."); let remainingTime = loopEndTime - currentTime; // Time left in the loop // if (remainingTime <= 1/30) { // log('Loop time is too short. Terminating.'); // clearInterval(loopIntervalId); // return; // } let { intervalLength, deCapo, tooFar } = loopTimeEvaluator(remainingTime); if (timeLoopDebug) { const convertedTime = convertToTimeFormat(remainingTime, "get-time-input"); lastMeasuredTimeInput.value = convertedTime; } // // If the video time is past the end, restart the loop // if (tooFar) { // video.currentTime = loopStartTime; // Reset video time // loopIterations += 1; // } // If the interval has changed, reset the interval if (loopIntervalTime !== intervalLength) { // clearInterval(loopIntervalId); // if (tooFar) { // log('tooFar'); // loopIntervalTime = loopInitialInterval; // } else { loopIntervalTime = intervalLength; // Update the interval time // } clearInterval(loopIntervalId); if (looping) { loopIntervalId = setInterval(loopInterval, loopIntervalTime); } } } // Loop interval function function loopInterval() { if (loopMode === "x-times" && loopIterations >= loopTimes) { log("loopIterations:", +loopIterations); loopIntervalEnd(); return; } if (!looping) { log("Looping is false."); clearInterval(loopIntervalId); loopIntervalEnd(); return; } let currentTime = video.currentTime; if (looping) { loopRemainingTimeCalculator(currentTime); } if (deCapo || tooFar) { video.currentTime = loopStartTime; // Reset to loop start if deCapo is true deCapo = false; // Reset deCapo after looping loopIterations += 1; log("loopIterations:", +loopIterations); } if (video.paused) { return; // clearInterval(loopIntervalId); // Pause the loop interval if video is paused } } // Start loop interval function loopIntervalStart() { loopStartTime = timeVariables.loopStartTime; // Set start time loopEndTime = timeVariables.loopEndTime; // Set end time loopIterations = 0; // Reset loop iterations let timeLength = loopEndTime - loopStartTime; // Calculate loop length let { intervalLength, deCapo, tooFar } = loopTimeEvaluator(timeLength); loopInitialInterval = intervalLength; // Store initial interval video.currentTime = loopStartTime; clearInterval(loopIntervalId); loopIntervalId = setInterval(loopInterval, intervalLength); // Start interval } // End loop interval function loopIntervalEnd() { clearInterval(loopIntervalId); // Clear the interval when ending loopIterations = 0; // Reset loop iteration count looping = false; video.pause(); log("Loop ended"); } // Resize observer for individual inner elements (resizes the after pseudoelements) // Array to store content widths and conclusions (same or width value) const innerItemElements = []; const widthConclusions = []; // ResizeObserver for monitoring changes const innerItemResizeObserver = new ResizeObserver((entries) => { entries.forEach((entry) => { const parentElement = entry.target; const parentId = parentElement.id || parentElement.className; log(`Observed resize for: ${parentId}`); // Get the current width of the parent element and its content const parentRect = parentElement.getBoundingClientRect(); const contentWidth = calculateContentWidth(parentElement); log(`For ${parentId}:`); log(`Container width: ${parentRect.width}`); log(`Content width: ${contentWidth}`); // Compare the current width of the parent and the content width const conclusion = parentRect.width > contentWidth ? contentWidth : "same"; // Log the conclusion log(`Conclusion for ${parentId}: ${conclusion}`); // Store the measurement result if (!innerItemElements.includes(parentId)) { log(`Adding ${parentId} to innerItemElements.`); innerItemElements.push(parentId); } else { log(`${parentId} already in innerItemElements, updating conclusion.`); } widthConclusions[innerItemElements.indexOf(parentId)] = conclusion; // Update the styles after measuring all elements applyAfterPseudoCssRules(); }); }); function calculateContentWidth(parent) { log(`Calculating content width for ${parent.id || parent.className}`); let left = Number.POSITIVE_INFINITY; let right = Number.NEGATIVE_INFINITY; Array.from(parent.children).forEach((child, index) => { // Check if the child element's display property is not 'none' if (window.getComputedStyle(child).display !== "none") { const childRect = child.getBoundingClientRect(); // log(`Child ${index}: left: ${childRect.left}, right: ${childRect.right}`); left = Math.min(left, childRect.left); right = Math.max(right, childRect.right); } else { // log(`Child ${index} is hidden (display: none), skipping.`); } }); const contentWidth = right - left; // log(`Content width for ${parent.id || parent.className}: ${contentWidth}`); return contentWidth; } // Function to apply CSS rules dynamically to the stylesheet function applyAfterPseudoCssRules() { log("Applying CSS rules..."); let newRules = ""; innerItemElements.forEach((parentId, index) => { const conclusion = widthConclusions[index]; log(`Processing ${parentId}: Conclusion - ${conclusion}`); if (conclusion !== "same") { newRules += `#${parentId}:after { content: ""; max-width: ${conclusion}px; }\n`; log(`Rule for ${parentId}: max-width: ${conclusion}px`); } }); // Apply the new rules to the stylesheet innerItemAfterStyles.textContent = newRules; log(`CSS rules applied:\n${newRules}`); } function observeInnerElements() { log("Starting to observe elements with the class '.video-manipulator-inner-item'."); // Start observing all elements with the class '.video-manipulator-inner-item' const videoManipulatorItems = document.querySelectorAll(".video-manipulator-inner-item"); log(`Found ${videoManipulatorItems.length} elements to observe.`); videoManipulatorItems.forEach((item) => { log(`Observing: ${item.id || item.className}`); innerItemResizeObserver.observe(item); }); } function recalculateAllAfterPseudoElements() { log("Inner item elements: " + JSON.stringify(innerItemElements)); innerItemElements.forEach((parentId, index) => { // Retrieve the parent element by its ID or class name let parentElement = document.querySelector(`#${parentId}`) || document.querySelector(`.${parentId}`); if (parentElement) { // Get the current width of the parent element and its content const parentRect = parentElement.getBoundingClientRect(); const contentWidth = calculateContentWidth(parentElement); log(`For ${parentId}:`); log(`Container width: ${parentRect.width}`); log(`Content width: ${contentWidth}`); // Compare the current width of the parent and the content width const conclusion = parentRect.width > contentWidth ? contentWidth : "same"; // Update the conclusion stored in the array widthConclusions[index] = conclusion; // Log the result log(`Recalculated conclusion for ${parentId}: ${conclusion}`); // Update the styles after measuring all elements applyAfterPseudoCssRules(); } }); } function recalculateForElement(element) { // Ensure the element is valid if (!element) { log("Invalid element passed."); return; } // Get the element's ID or class name (just like in ResizeObserver) const parentId = element.id || element.className; log(`Recalculating for: ${parentId}`); // Get the current width of the parent element and its content const parentRect = element.getBoundingClientRect(); const contentWidth = calculateContentWidth(element); log(`For ${parentId}:`); log(`Container width: ${parentRect.width}`); log(`Content width: ${contentWidth}`); // Compare the current width of the parent and the content width const conclusion = parentRect.width > contentWidth ? contentWidth : "same"; // Update the conclusion in the widthConclusions array for this element const index = innerItemElements.indexOf(parentId); if (index !== -1) { widthConclusions[index] = conclusion; log(`Updated conclusion for ${parentId}: ${conclusion}`); } else { log(`${parentId} is not found in innerItemElements.`); } // Now, apply the styles after recalculating for this single element applyAfterPseudoCssRules(); } // Variable to store the interval time in milliseconds (default 1000 / 60) let checkTimeIntervalTime = 100000; // Interval ID for later clearing let checkTimeIntervalId = null; // Function to handle the action we want to perform at intervals function checkTimeAtInterval() { // log("Action performed at rate of", measureRateInput.value, "per second."); // Add whatever action needs to happen here if (timeLoopDebug) { getCurrentTime(lastMeasuredTimeInput); } } // Start the interval based on the input value function startCheckTimeInterval() { // Clear any existing interval if (checkTimeIntervalId) clearInterval(checkTimeIntervalId); // Set the new interval checkTimeIntervalId = setInterval(checkTimeAtInterval, checkTimeIntervalTime); } // Debounced input handling let debounceCheckTimeIntervalTimer = null; function handleSrcChange() { log("Handling video source change..."); handleReset(); resizeByDefault = JSON.parse(localStorage.getItem("resizeByDefault")) || false; if (resizeByDefault) { resizeDefaultCheckbox.checked = true; frameIsRescaled = true; handleFrameResize("resize-frame"); rescaleFrame(); buttonRefs["resize-frame"].classList.add("active"); } // if (chapters) { // chapters.remove(); // } } function reEstablishChapters() { establishChaptersAttempts = 0; establishChaptersAttemptsInterval = 250; establishChapters(); } let currentUrl = window.location.href; function checkUrl() { if (window.location.href !== currentUrl) { log("URL changed to (polling):", window.location.href); currentUrl = window.location.href; reEstablishChapters(); } setTimeout(checkUrl, 250); // Check every 100ms } checkUrl(); // Start the polling // Create the observer for the video element function createSrcObserver() { findVideo(); if (!video) { log("No video element found."); return; } // Create a mutation observer to detect changes in the video element's src const observer = new MutationObserver((mutationsList, observer) => { for (const mutation of mutationsList) { if (mutation.type === "attributes" && mutation.attributeName === "src") { log("Video source has changed."); handleSrcChange(); } } }); // Observe the video element for attribute changes (such as src) observer.observe(video, { attributes: true, // Observe changes to attributes (e.g., src) childList: false, // Do not observe child elements subtree: false // Do not observe descendants }); // If the video element is removed, disconnect the observer const videoParentObserver = new MutationObserver(() => { if (!document.contains(video)) { observer.disconnect(); initialize(); log("Video element removed from the DOM. Observer disconnected."); } }); // Observe the parent node to detect when the video is removed videoParentObserver.observe(document.body, { childList: true, // Observe additions and removals of child nodes subtree: true // Observe the entire DOM tree }); } function attachResizeObserverOnVideoPlayer() { let html5VideoPlayer = document.querySelector(".html5-video-player"); // Check if the element exists, if not, retry every 250ms const checkInterval = setInterval(() => { html5VideoPlayer = document.querySelector(".html5-video-player"); if (html5VideoPlayer) { clearInterval(checkInterval); // Stop checking once we find the element // Attach ResizeObserver const resizeObserver = new ResizeObserver(() => { // Function to run on resize log("Video player resized!"); scaleAndTop(); }); // Start observing the element for resizing resizeObserver.observe(html5VideoPlayer); log("Resize observer attached to .html5-video-player"); } }, 250); // Retry every 250ms until found } /* Manual */ function insertManual() { const manualDiv = document.createElement("div"); manualDiv.id = "video-controls-manual"; manualDiv.innerHTML = ` <div class="empty-spanner line"></div> <h2>Speed Control</h2> <p>This <b>playback rate controller</b> has a simple number input. It lets you enter the speed you want, or use the spinners to move up or down by 5%.</p> <p>The controls also include a <b>speed lock</b>. Sometimes, after you dial in just the right playback speed, scripts on YouTube try to change it. When this happens, the speed lock kicks in to put the speed back where you had it. You can lock the speed just for now, or lock it forever, so that every video you open will play at your custom speed.</p> <p>(<b>Note:</b> Some scripts and extensions may have their own speed locks, which will put them in a deadlock with this extension. In these cases, disabling the speed lock for this script is easy enough.)</p> <h2>Play/Pause Lock</h2> <p>Sometimes, scripts or extensions may try to play or pause a video when you didn't ask them to. The <b>play lock</b> and <b>pause lock</b> are designed to stop this. Because you should decide when your videos play.</p> <p>(<b>Note:</b> Sometimes, YouTube's native scripts may be so aggressive that the play lock cannot overcome them.)</p> <h2>Aspect, Zoom, Flip, & Frame</h2> <p>The <b>aspect</b> controls let you correct videos that were thoughtlessly stretched by the uploader. Because people don't look good when they get stretched out of shape. The input for <b>custom aspect ratios</b> lets you set ratios precisely, for when <b>4/3</b> or <b>16/9</b> are not the correct ratio.</p> <p>(<b>Note</b>: When you input a custom aspect ratio, you have to press the <b>Custom</b> button to apply it, every time.)</p> <p>The <b>zoom</b> control can help fix videos that were uploaded with a hard matte, getting rid of pointless black bars and allowing the video to fill its frame.</p> <p>The <b>flip, mirror, and 180°</b> buttons let you easily correct videos that have been altered by their uploader. Was the video flipped horizontally to avoid copyright bots, and does the text look wrong backwards? You can flip it back using the <b>Mirror</b> button. The <b>Flip</b> button is there just in case a video has been flipped vertically. And the 180° can help correct videos that were shot in the southern hemisphere, or Yes's music video for "Leave it".</p> <p>The <b>Frame Resize</b> button resizes the video frame to match the video it contains, avoiding unnecessary letterboxing or pillarboxing.</p> <p>(<b>Note:</b> Implementing frame resize has been challenging, and you may still encounter a few bugs.)</p> <p>The <b>Reset</b> button puts all of these controls back the way they were. It does not, however, clear the input boxes, which makes reapplying a custom aspect ratio or zoom easy.</p> <h2>Jump to Time / Get Current Time</h2> <p>YouTube has ways of getting to different chapters in a video, but getting to any other specific point is difficult. Now, you can enter a time value in hours, minutes, seconds, and milliseconds — that's how precise it is — and press the <b>Jump to</b> button to go directly to any point in the video. Any standard time value will work: hours, minutes, and seconds, or just minutes and seconds, or just seconds — any of these with or without milliseconds.</p> <p>The <b>Get Current Time</b> button will tell you exactly where you are in a video, with millisecond precision, making it easy to identify points you want to revisit.</p> <h2>Nice 'n' Accurate Looper</h2> <p>The time seeking continues with my own version of a <b>looper</b>. There are a number of available loopers for YouTube, but they tend to limit their accuracy to whole seconds, and sometimes, that just isn't enough. This looper accepts inputs that include milliseconds, just like the the time seeker in the last section. This allows you to specify loops right down to the frame. Additional controls let you decide if the video will loop forever, or just a set number of times.</p> <p>(<b>Note:</b> If your system is overworked, accuracy may suffer. However, in tests, the results have been impressive.)</p> <h2>Corners, Overlays, & Chapters</h2> <p>This set of cosmetic controls are somewhat miscellaneous, and have been grouped together for convenience.</p> <p>The <b>Square Corners</b> control undoes YouTube's corner rounding. Because all pixels matter.</p> <p>The <b>Hide Overlays</b> control gets rid of distracting video overlays, like channel logos, popup links, and title cards that interrupt the ends of music videos.</p> <p>The <b>Move Chapter Selector</b> takes the nice chapter selector that YouTube hides away in the video description, and moves it to a more convenient place. The <b>Low</b> option puts the selector just below Jupiter's Tools for YouTube. The <b>Middle</b> option puts it just above these tools. And the <b>High</b> option puts it even higher: right below the video's title.</p> <h2>Volume Boost</h2> <p>For videos that are too quiet, this control can <b>boost</b> the gain. You decide the amount of boost. The input works in decibels, with a helpful conversion to percent for people who can't think in decibels. To prevent unpleasant clipping, the audio is sent through a limiter. Not using one would be simply irresponsible.</p> <p>The following paragraphs will likely only appeal to audio engineering and web coding enthusiasts.</p> <p>This volume booster uses the <a href="https://webaudio.github.io/web-audio-api/#DynamicsCompressorNode">Web Audio API compressor</a>. By default, the ratio is set to 20:1 — as high as it will go — with a slightly soft knee, instantaneous attack, and a medium release time. For those who want to tinker, the <b>Expert</b> button opens another control panel that gives you full access to the compressor.</p> <p>Note: Because Web Audio API ties threshold to makeup gain, and gives the user no control over that makeup gain, the stock threshold has been replaced with a boost going in and a cut coming out, to make the compressor easier to control.</p> <div class="empty-spanner line"></div> `; videoControlsMasterHolder.appendChild(manualDiv); } /* INITIALIZE SECTION */ function insertCombinedControls() { const existingControls = topRow.querySelector(".video-manipulator-outer-div"); if (existingControls) { return; } else { log("Inserting controls..."); } actions = topRow.querySelector("#actions"); if (!actions) { log("Actions element not found."); // insertAttempts += 1; // if (insertAttempts < 60) { setTimeout(() => { insertCombinedControls(); }, 250); return; // } } let insertDelayIncrease = 10; let insertDelay = insertDelayIncrease; setTimeout(() => { const emptySpanner = insertEmptySpanner(); topRow.appendChild(emptySpanner); }, insertDelay); insertDelay += insertDelayIncrease; setTimeout(() => { insertVideoControlsShowHideMenu(); }, insertDelay); insertDelay += insertDelayIncrease; setTimeout(() => { const emptySpanner = insertEmptySpanner(true); topRow.appendChild(emptySpanner); }, insertDelay); insertDelay += insertDelayIncrease; setTimeout(() => { createVideoControlsMasterHolder(); }, insertDelay); insertDelay += insertDelayIncrease; setTimeout(() => { insertPlaybackRateControls(); }, insertDelay); insertDelay += insertDelayIncrease; setTimeout(() => { insertLockControls(); }, insertDelay); insertDelay += insertDelayIncrease; setTimeout(() => { // log('This is where the control panel SHOULD be inserted.'); insertControlPanel(); }, insertDelay); insertDelay += insertDelayIncrease; setTimeout(() => { insertSeekAndLoopControls(); }, insertDelay); insertDelay += insertDelayIncrease; setTimeout(() => { insertCosmeticControls(); }, insertDelay); insertDelay += insertDelayIncrease; // Initialize the compressor controls setTimeout(() => { insertCompressorControls(); }, insertDelay); insertDelay += insertDelayIncrease; setTimeout(() => { insertManual(); }, insertDelay); insertDelay += insertDelayIncrease; setTimeout(() => { insertAboutDiv(); }, insertDelay); insertDelay += insertDelayIncrease; setTimeout(() => { populateVideoControlsShowHideMenu(); }, insertDelay); insertDelay += insertDelayIncrease; setTimeout(() => { addPressEffectToButtons(); }, insertDelay); insertDelay += insertDelayIncrease; setTimeout(() => { monitorVideoSize(); observeInnerElements(); createSrcObserver(); attachResizeObserverOnVideoPlayer(); // if (timeLoopDebug) { // // startCheckTimeInterval(); // } }, insertDelay); insertDelay += insertDelayIncrease; setTimeout(() => { insertCombinedControls(); }, 5000); } let initializeMessageRecent = false; // Function to initialize the userscript function initialize() { if (!initializeMessageRecent) { log("The initialize funtion is running."); initializeMessageRecent = true; setTimeout(() => { initializeMessageRecent = false; }, 1000); } findVideo(); if (!video) { setTimeout(() => { initialize(); }, 1000); return; } topRow = document.querySelector("ytd-watch-metadata #top-row"); if (!topRow) { setTimeout(() => { initialize(); }, 1000); return; } insertStylesheets(); insertCombinedControls(); // if (!insertControlPanel()) { // const observer = new MutationObserver((mutations) => { // const now = Date.now(); // if (now - lastChecked >= 2000) { // lastChecked = now; // if (insertControlPanel()) { // insertCombinedControls(); // observer.disconnect(); // } // } // }); // observer.observe(document, { // childList: true, // subtree: true // }); // } else { // insertCombinedControls(); // } } // Run the initialize function when the page has fully loaded // window.addEventListener('load', initialize); initialize(); })();