YouTube persistent speed slider overlay, minimize toggle. Pro Features: Automatic 1x speed for Music/Movies, intelligent chapter navigation, and high-fidelity Pitch Shift toggle.
// ==UserScript==
// @name YouTube Speed Controller Pro
// @namespace http://tampermonkey.net/
// @version 3.2.68
// @description YouTube persistent speed slider overlay, minimize toggle. Pro Features: Automatic 1x speed for Music/Movies, intelligent chapter navigation, and high-fidelity Pitch Shift toggle.
// @author AI-generated with assistance from Gemini 3 Flash based on concepts by RickZabel
// @license CC-BY-NC-SA-4.0
// @copyright 2026, RickZabel (https://greasyfork.org/en/users/5920-rickzabel)
// @icon data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHdpZHRoPSIxMDI0IiBoZWlnaHQ9IjEwMjQiIHZpZXdCb3g9IjAgMCAxMDI0IDEwMjQiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZmlsbD0iI2ZmMDAwMCIgZD0iTSA1MTIgNzQ1IGMgMCAwIDIxMiAwIDI2NCAtMTQgMjkgLTcgNTEgLTMwIDYwIC01OSAxNCAtNTEgMTQgLTE2MCAxNCAtMTYwIDAgMCAwIC0xMDggLTE0IC0xNjAgLTggLTI5IDMwIC01MSAtNjAgLTU5IC01MiAtMTQgLTI2NCAtMTQgLTI2NCAtMTQgMCAwIC0yMTEgMCAtMjY0IDE0IC0yOSA3IC01MSAzMCAtNjAgNTkgLTE0IDUyIC0xNCAxNjAgLTE0IDE2MCAwIDAgMCAxMDkgMTQgMTYwIDggMjkgMzAgNTEgNjAgNTkgNTIgMTQgMjY0IDE0IDI2NCAxNCB6Ii8+PHBhdGggZmlsbD0iI2ZmZmZmZiIgc3Ryb2tlPSIjZmZmZmZmIiBzdHJva2Utd2lkdGg9Ijg0IiBkPSJNIDY4MyA1MTIgbCAtMjcgLTE1IHYgMzEgei BNIDUwNiA1MTIgbCAtMjcgLTE1IHYgMzEgeiIvPjxwYXRoIGZpbGw9IiNmZmZmZmYiIGQ9Ik0gMzMwIDQyMSBoIDQxIHYgMTgyIGggLTQxIHogTSAyNTUgNDIxIGggNDEgdiAxODIgaCAtNDEgeiIvPjwvc3ZnPg==
// @match *://*.youtube.com/*
// @grant none
// @run-at document-idle
// ==/UserScript==
/**
* CHROME USERS: If this script does not appear to load:
* 1. Go to chrome://extensions/
* 2. Ensure "Developer mode" (top right) is ON.
* 3. Click "Details" on the Tampermonkey extension.
* 4. Scroll down and ensure "Allow user scripts" is ON.
* 5. Ensure "Allow access to file URLs" is ON.
*/
/*
AI-generated with assistance from Gemini 3 Flash based on concepts by RickZabel
MASTER PROMPT / REBUILD RECIPE (v3.2.67):
"Create a Tampermonkey script for YouTube that adds a persistent, draggable speed control overlay with a License/Pro system.
Core Features & UI:
- Precision Controls: Granular slider (0.05 increments) and presets (Pause to 2x).
- Custom Logo: Must use specific SVG paths (lp1, lp2, lp3) with Transformation Matrix (6.201...) and colors (#ff0000, #ffffff). Acts as a minimize toggle with hold-to-manage license logic.
- Transparency: 25% step toggle using a dynamic SVG pie-chart icon; persists via localStorage.
- Smart Tooltips: 0.8s transition delay, 11px normal weight text, and 'Smart Boundary' logic to auto-flip orientation at window edges.
- Color Palette: Primary text/icons #888888; Slider thumb dark red (#880000) switching to bright red (#ff0000) on hover. Pro 'Override' state uses green (#00ff00).
Layout & Structure:
- Main Container: Flexbox row alignment with 8px gap. Background: rgba(0,0,0,0.8) with 8px border-radius.
- Minimized State: When minimized, the container shrinks to only the Logo. A dedicated drag handle (dots icon) must appear to the left of the logo only when minimized to allow repositioning.
- Pro Button Group: A dedicated sub-container (.pro-btns) using 'display: flex; gap: 3px; flex: 1; overflow: visible !important;' to house Pro toggles.
Pro Features (License Key Required):
- Chapter Navigation: « and » arrows with fixed -2.1px top offset and 18px font size.
- Auto-Speed Logic: Context-aware 1x speed override for 'Music', 'Movies', and 'Shows' categories.
- High-Fidelity Audio: 'P' toggle to disable YouTube's pitch correction (preservesPitch = false).
- Analytics: Lifetime 'Time Saved' tracker based on playback rate vs. actual duration.
Logic & Correctness Constraints:
- Vertical Centering: Pause button text wrapped in a span with -1.8px top nudge for visual balance.
- Playback Preservation: Toggling Pro buttons (Music/Movies/P) while paused must never trigger auto-resume.
- License Guard: checkLicense() must immediately disable all Pro logic and reset audio fidelity if the key is removed.
- Persistence: LocalStorage must remember speed, coordinates, opacity, settings, and minimized state."
*/
(function() {
'use strict';
const GREASYFORK_URL = 'https://greasyfork.org/en/users/5920-rickzabel';
const VALID_KEY = '1234';
let isProUser = false;
let lastActiveSpeed = parseFloat(localStorage.getItem('ytPersistentSpeed')) || 1.0;
let prePauseSpeed = lastActiveSpeed;
let isMinimized = localStorage.getItem('ytSpeedMinimized') === 'true';
let opacityLevel = parseFloat(localStorage.getItem('ytSpeedOpacity')) || 1.0;
let pitchShiftEnabled = localStorage.getItem('ytPitchShiftEnabled') === 'true';
let totalSecondsSaved = parseFloat(localStorage.getItem('ytTotalTimeSaved')) || 0;
let lastTimestamp = 0;
let autoSpeedActive = false;
let proSettings = JSON.parse(localStorage.getItem('ytProSettings')) || { musicOneX: false, movieOneX: false };
const categoryMap = { "1": "Film & Animation", "10": "Music", "30": "Movies", "43": "Shows" };
function updateStateFromStorage() {
isProUser = localStorage.getItem('ytSpeed_ProKey') === VALID_KEY;
pitchShiftEnabled = isProUser && localStorage.getItem('ytPitchShiftEnabled') === 'true';
}
function checkLicense() {
const key = localStorage.getItem('ytSpeed_ProKey');
const wasPro = isProUser;
isProUser = (key === VALID_KEY);
// MANDATORY: If license is no longer valid, kill all Pro logic
if (!isProUser) {
pitchShiftEnabled = false;
proSettings.musicOneX = false;
proSettings.movieOneX = false;
autoSpeedActive = false;
localStorage.setItem('ytPitchShiftEnabled', 'false');
saveProSettings();
const v = document.querySelector('video');
if (v) v.preservesPitch = true; // Restore default high-quality audio
}
// Refresh the overlay to hide/show Pro sections based on the new state
if (document.getElementById('yt-speed-overlay')) redrawOverlay();
}
function saveProSettings() { if (isProUser) localStorage.setItem('ytProSettings', JSON.stringify(proSettings)); }
function isDarkMode() { return document.documentElement.getAttribute('dark') !== null || window.matchMedia('(prefers-color-scheme: dark)').matches; }
function trackTimeSaved() {
if (!isProUser) return;
const video = document.querySelector('video');
if (!video || video.paused || video.playbackRate <= 1.0) { lastTimestamp = 0; return; }
if (lastTimestamp === 0) { lastTimestamp = video.currentTime; return; }
const actualTimePassed = video.currentTime - lastTimestamp;
if (actualTimePassed > 0 && actualTimePassed < 1) {
const saved = actualTimePassed - (actualTimePassed / video.playbackRate);
totalSecondsSaved += saved;
localStorage.setItem('ytTotalTimeSaved', totalSecondsSaved);
}
lastTimestamp = video.currentTime;
}
function updateSavedDisplay() {
const valEl = document.getElementById('time-saved-stat');
if (!valEl || !isProUser) return;
let s = totalSecondsSaved;
const years = Math.floor(s / 31536000), days = Math.floor((s % 31536000) / 86400), hrs = Math.floor((s % 86400) / 3600), mins = Math.floor((s % 3600) / 60), secs = Math.floor(s % 60);
let parts = [];
if (years > 0) parts.push(years + "y"); if (days > 0) parts.push(days + "d"); if (hrs > 0) parts.push(hrs + "h"); if (mins > 0) parts.push(mins + "m"); if (secs > 0 || parts.length === 0) parts.push(secs + "s");
valEl.textContent = "Time Saved: " + parts.join(" ");
}
function getCategoryInfo() {
try {
let id = window.ytInitialPlayerResponse?.videoDetails?.categoryId;
let name = categoryMap[id];
if (!id || !name) {
const catStr = window.ytInitialPlayerResponse?.microformat?.playerMicroformatRenderer?.category;
if (catStr) { name = catStr; if (catStr === "Music") id = "10"; else if (catStr === "Movies" || catStr === "Film & Animation") id = "30"; else if (catStr === "Shows") id = "43"; }
}
return { id: id ? String(id) : "N/A", name: name || "Not Detected" };
} catch (e) { }
return { id: "N/A", name: "Not Detected" };
}
function checkCategoryAndAdjustSpeed() {
if (!isProUser) return;
const info = getCategoryInfo();
const isMusicActive = (info.id === "10" || info.name === "Music") && proSettings.musicOneX;
const isMovieActive = (["30", "43", "1"].includes(info.id) || ["Movies", "Shows", "Film & Animation"].includes(info.name)) && proSettings.movieOneX;
autoSpeedActive = (isMusicActive || isMovieActive);
const target = autoSpeedActive ? 1.0 : lastActiveSpeed;
applySpeedToVideo(target);
updateUI(target);
}
function navigateChapter(direction) {
const video = document.querySelector('video'); if (!video) return;
const chapters = getChapters(), currentTime = video.currentTime;
if (chapters.length === 0) return;
if (direction === 'next') { const next = chapters.find(t => t > currentTime + 5); if (next !== undefined) video.currentTime = next; }
else { const prevIndex = chapters.findLastIndex(t => t < currentTime - 3); video.currentTime = prevIndex !== -1 ? chapters[prevIndex] : 0; }
}
function getChapters() {
const video = document.querySelector('video'); if (!video || isNaN(video.duration)) return [];
let chapters = new Set();
document.querySelectorAll('.ytp-chapter-hover-container, .ytp-chapter-marker').forEach(el => {
const leftPct = parseFloat(el.style.left);
if (!isNaN(leftPct)) chapters.add((leftPct / 100) * video.duration);
});
let sorted = Array.from(chapters).sort((a, b) => a - b);
if (sorted.length > 0 && sorted[0] > 2) sorted.unshift(0);
return sorted;
}
function checkTooltipPos(e) {
const el = e.currentTarget;
el.classList.remove('tt-left', 'tt-right', 'tt-down');
const rect = el.getBoundingClientRect();
if (rect.left < 60) el.classList.add('tt-left');
else if (window.innerWidth - rect.right < 60) el.classList.add('tt-right');
if (rect.top < 60) el.classList.add('tt-down');
}
function ensureInView() {
const container = document.getElementById('yt-speed-overlay');
if (!container) return;
const rect = container.getBoundingClientRect();
const isOut = (rect.bottom < 0 || rect.top > window.innerHeight || rect.right < 0 || rect.left > window.innerWidth);
if (isOut) performReset();
}
function performReset() {
localStorage.setItem('ytSpeedPos', JSON.stringify({ top: '60px', left: '20px' }));
redrawOverlay();
}
let logoHoldTimer, countdownInterval, holdSecondsRemaining = 3, isLicenseMode = false;
function redrawOverlay() {
updateStateFromStorage();
const e1 = document.getElementById('yt-speed-overlay'), e2 = document.getElementById('yt-speed-styles');
if (e1) e1.remove(); if (e2) e2.remove();
setTimeout(inject, 0);
}
function getTransparencyPath(opacity) {
if (opacity === 0.25) return "M50,50 L50,0 A50,50 0 0,1 100,50 Z";
if (opacity === 0.50) return "M50,50 L50,0 A50,50 0 0,1 100,50 A50,50 0 0,1 50,100 Z";
if (opacity === 0.75) return "M50,50 L50,0 A50,50 0 0,1 100,50 A50,50 0 0,1 50,100 A50,50 0 0,1 0,50 Z";
return "M50,50 m-50,0 a50,50 0 1,0 100,0 a50,50 0 1,0 -100,0";
}
function inject() {
if (document.getElementById('yt-speed-overlay')) return;
updateStateFromStorage();
if (!document.body) return;
const dark = isDarkMode();
const colors = {
//usage name: check ? 'false' : 'true'
bg: dark ? 'rgba(15, 15, 15, 0.95)' : 'rgba(255, 255, 255, 0.95)',
text: dark ? '#ffffff' : '#000000',
accentBright: '#ff0000',
accentDark: '#880000',
muted: '#888888',
border: dark ? 'rgba(255, 255, 255, 0.15)' : 'rgba(0, 0, 0, 0.1)',
//btnBg: dark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.05)',
btnBg: dark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.05)',
btnHover: dark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.1)',
btnText: dark ? '#ffffff' : '#000000',
tooltipBg: '#111111',
activeGreen: '#2ecc71',
};
const style = document.createElement('style');
style.id = 'yt-speed-styles';
style.textContent = `
#yt-speed-overlay {
position: fixed;
bottom: 150px;
left: 20px;
width: 330px;
background: ${colors.bg} !important;
color: ${colors.text} !important;
padding: 6px;
border-radius: 12px;
backdrop-filter: blur(12px);
border: 1px solid ${colors.border};
z-index: 2147483647 !important;
font-family: "Segoe UI", Roboto, sans-serif;
font-size: 0px !important;
user-select: none;
opacity: ${opacityLevel};
transition: opacity 0.3s, width 0.3s, height 0.3s;
/*box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);*/
touch-action: none;
/*gap: 12px;*/
display: block;
box-sizing: border-box !important;
line-height: normal !important;
}
#yt-speed-overlay.minimized {
width: 42px !important;
height: 42px !important;
padding: 0 !important;
overflow: hidden;
align-items: center;
justify-content: center;
}
#yt-speed-overlay.minimized .header-row,
#yt-speed-overlay.minimized .chapter-nav,
#yt-speed-overlay.minimized .main-content-area,
#yt-speed-overlay.minimized .center-group,
#yt-speed-overlay.minimized .trans-toggle,
#yt-speed-overlay.minimized .reset-btn,
#yt-speed-overlay.minimized .right-group {
display: none !important;
}
#yt-speed-overlay.minimized .top-bar {
margin: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
}
#yt-speed-overlay.minimized .min-drag-dots {
display: block !important;
width: 100%;
text-align: center;
font-size: 8px;
color: ${colors.muted};
cursor: move;
line-height: 8px;
margin-top: 1px;
}
#yt-speed-overlay.minimized .left-group {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
}
#yt-speed-overlay:hover {
opacity: 1 !important;
}
.header-row {
display: flex;
align-items: center;
height: 12px;
margin-bottom: 2px;
position: relative;
}
.script-title {
position: absolute;
left: 50%;
transform: translateX(-50%);
font-size: 10.5px;
color: ${colors.muted};
text-transform: uppercase;
letter-spacing: 0.5px;
opacity: 0.8;
white-space: nowrap;
cursor: move;
}
.top-bar {
display: flex;
align-items: center;
height: 14px;
margin-bottom: 0px;
position: relative;
padding-top: 2px;
padding-bottom: 2px;
}
.left-group {
display: flex;
align-items: center;
flex: 0 0 40px;
}
.center-group {
position: absolute;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
}
.right-group {
display: flex;
align-items: center;
gap: 3px;
margin-left: auto;
}
.drag-handle {
display: flex;
justify-content: center;
align-items: center;
cursor: move;
font-size: 10px;
color: ${colors.text};
opacity: 0.7;
padding: 0 2px;
}
.min-drag-dots {
display: none;
}
[data-tooltip] {
position: relative;
}
/*[data-tooltip]:hover:before { opacity: 1; visibility: visible; }*/
[data-tooltip].tt-left:before {
left: 0;
transform: translateX(0);
}
[data-tooltip].tt-right:before {
left: auto;
right: 0;
transform: translateX(0);
}
[data-tooltip]:before {
content: attr(data-tooltip);
position: absolute !important;
/* Prevents button squishing */
bottom: 100%;
left: 50%;
transform: translateX(-50%);
padding: 5px 9px;
background: #111111 !important;
color: #ffffff !important;
font-size: 11px !important;
/* Uniform font size */
font-weight: normal !important;
/* Uniform normal weight */
border-radius: 4px;
white-space: nowrap;
opacity: 0;
visibility: hidden;
transition: opacity 0.2s, transform 0.2s;
transition-delay: 1.8s;
z-index: 2147483649;
pointer-events: none;
border: none !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
}
[data-tooltip]:hover:before {
opacity: 1;
visibility: visible;
}
/* Smart Boundary Fixes: Flips tips inward when near screen edges */
#yt-speed-overlay.edge-right [data-tooltip]:before {
left: auto !important;
right: 0 !important;
transform: translateX(0) !important;
}
#yt-speed-overlay.edge-left [data-tooltip]:before {
left: 0 !important;
right: auto !important;
transform: translateX(0) !important;
}
.pro-btns [data-tooltip]:before,
.logo-container [data-tooltip]:before,
.chapter-nav [data-tooltip]:before {
bottom: 100% !important;
}
/*[data-tooltip].tt-down:before { bottom: auto; top: 100%; }*/
[data-tooltip].tt-down:before {
bottom: auto !important;
top: 140% !important;
}
.pro-area,
.pro-btns {
overflow: visible !important;
}
.pro-area {
overflow: visible !important;
}
.logo-container {
cursor: pointer;
transition: transform 0.2s;
display: flex;
align-items: center;
justify-content: center;
transform: translate(-4px, -10px);
}
.logo-container:hover {
transform: translate(-4px, -10px) scale(1.1);
}
#yt-speed-overlay.minimized .logo-container {
transform: translate(0px, 0px);
}
.chapter-nav {
display: flex;
align-items: center;
gap: 6px;
font-size: 18px;
color: ${colors.muted};
}
.chapter-nav span:hover {
color: #ff0033;
cursor: pointer;
}
.trans-toggle {
cursor: pointer;
display: flex;
align-items: center;
padding: 0 2px;
/*opacity: 0.6;*/
transform: translate(-0px, 1.4px);
}
.trans-toggle svg {
width: 12px;
height: 12px;
fill: ${colors.muted};
}
.trans-toggle:hover {
opacity: 1;
}
#speed-val {
font-weight: bold;
font-size: 9px !important;
color: ${colors.muted};
text-align: center;
pointer-events: auto;
}
#speed-val.override {
color: ${colors.activeGreen} !important;
}
.gf-link {
margin-left: auto;
display: flex;
align-items: center;
}
.gf-link svg {
width: 10px;
height: 10px;
fill: ${colors.muted};
transition: fill 0.2s;
}
.gf-link:hover svg {
fill: #ff0033;
}
/*f2f2f2 hover e5e5e5*/
.reset-btn {
cursor: pointer;
font-size: 16px;
color: ${colors.muted};
transition: color 0.2s;
}
.reset-btn:hover {
color: #ff0033;
}
.preset-btn {
flex: 1;
/*background: ${colors.btnBg};*/
background: ${colors.btnBg} !important;
border: none;
color: ${colors.btnText};
padding: 7.5px 0;
cursor: pointer;
font-size: 12px;
border-radius: 4px;
transition: all 0.2s;
height: 33px;
min-width: 36px;
border-radius: 18px; /* Makes them pill-shaped like YouTube buttons */
}
.preset-btn:hover {
/*background: rgba(255, 255, 255, 0.15) !important;*/
background: ${colors.btnHover} !important;
/*color: #ff0033 !important;*/
}
.preset-btn.active {
color: #ff0033 !important;
font-weight: bold;
}
.preset-btn.override {
color: ${colors.activeGreen} !important;
}
.preset-btn.active.pro-toggle {
color: ${colors.activeGreen} !important;
}
#speed-slider {
width: 100%;
height: 4px;
background: #444;
-webkit-appearance: none;
border-radius: 2px;
margin: 4px 0 8px 0;
cursor: pointer;
}
#speed-slider::-webkit-slider-thumb {
-webkit-appearance: none;
height: 10px;
width: 10px;
border-radius: 50%;
background: #ff0033;
transition: background 0.2s;
}
#yt-speed-overlay.pro-active #speed-slider::-webkit-slider-thumb {
background: ${colors.activeGreen} !important;
}
#yt-speed-overlay.is-paused #speed-slider::-webkit-slider-thumb {
background: #ff0033 !important;
}
#speed-slider.override::-webkit-slider-thumb {
background: #00ff00 !important; /* Pro Green */
}
.pro-area {
display: flex;
flex-direction: column;
gap: 3px;
margin-top: 6px;
border-top: 1px solid ${colors.border};
}
.pro-row {
display: flex;
align-items: center;
justify-content: space-between;
}
.pro-btns {
display: flex;
gap: 3px;
flex: 1;
overflow: visible !important;
}
#time-saved-stat {
font-size: 12px;
color: ${colors.muted};
white-space: nowrap;
margin-left: 5px;
}
.chapter-nav {
flex: 1;
background: transparent !important;
border: none;
color: ${colors.muted};
padding: 0 6px 0 0;
cursor: pointer;
font-size: 27px;
transition: color 0.2s;
display: inline-flex;
align-items: center;
justify-content: flex-end;
position: relative;
top: -3.15px;
}
.chapter-nav.data-tooltip:hover {
color: #ffffff !important;
background: transparent !important;
}
.stats-row {
font-size: 9px;
color: ${colors.muted};
text-align: left;
display: flex;
align-items: center;
justify-content: space-between;
height: 15px;
}
.license-mgr {
display: flex;
flex-direction: column;
gap: 8px;
padding: 10px;
background: rgba(0, 0, 0, 0.3);
border-radius: 6px;
}
.license-mgr input {
background: #222;
border: 1px solid #444;
color: #fff;
padding: 6px;
border-radius: 4px;
font-size: 11px;
width: 100%;
box-sizing: border-box;
}
.license-mgr button {
background: ${colors.activeGreen};
border: none;
color: #000;
padding: 6px;
border-radius: 4px;
font-weight: bold;
cursor: pointer;
}
`;
document.head.appendChild(style);
const container = document.createElement('div'); container.id = 'yt-speed-overlay';
if (isMinimized) container.classList.add('minimized');
const svgNS = "http://www.w3.org/2000/svg", headerRow = document.createElement('div'); headerRow.className = 'header-row';
const title = document.createElement('div'); title.className = 'script-title'; title.textContent = 'YouTube Speed Controller Pro';
const gfLink = document.createElement('a'); gfLink.className = 'gf-link'; gfLink.href = GREASYFORK_URL; gfLink.target = '_blank'; gfLink.setAttribute('data-tooltip', "Rick's Scripts"); gfLink.onmouseover = checkTooltipPos;
const gfSvg = document.createElementNS(svgNS, "svg"); gfSvg.setAttribute("viewBox", "0 0 24 24");
const gfPath = document.createElementNS(svgNS, "path"); gfPath.setAttribute("d", "M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z");
gfSvg.appendChild(gfPath); gfLink.appendChild(gfSvg);
const titleDragHandle = document.createElement('div'); titleDragHandle.className = 'drag-handle'; titleDragHandle.textContent = '••••';
headerRow.appendChild(title); headerRow.appendChild(gfLink); headerRow.appendChild(titleDragHandle);
const topBar = document.createElement('div'); topBar.className = 'top-bar';
const minDrag = document.createElement('div'); minDrag.className = 'min-drag-dots'; minDrag.textContent = '••••';
const leftGroup = document.createElement('div'); leftGroup.className = 'left-group';
const logoWrap = document.createElement('div'); logoWrap.className = 'logo-container';
logoWrap.onmouseover = checkTooltipPos;
logoWrap.onmousedown = () => {
if (isLicenseMode) return;
holdSecondsRemaining = 3; logoWrap.setAttribute('data-tooltip', `Hold: ${holdSecondsRemaining}s...`);
countdownInterval = setInterval(() => { holdSecondsRemaining--; logoWrap.setAttribute('data-tooltip', `Hold: ${holdSecondsRemaining}s...`); if (holdSecondsRemaining <= 0) clearInterval(countdownInterval); }, 1000);
logoHoldTimer = setTimeout(() => { isLicenseMode = true; isMinimized = false; redrawOverlay(); }, 3000);
};
logoWrap.onmouseup = logoWrap.onmouseleave = () => { clearTimeout(logoHoldTimer); clearInterval(countdownInterval); logoWrap.setAttribute('data-tooltip', 'Hold 3s to Manage License'); };
logoWrap.onclick = () => { if (holdSecondsRemaining >= 1 && !isLicenseMode) { isMinimized = !isMinimized; localStorage.setItem('ytSpeedMinimized', isMinimized); redrawOverlay(); } };
const logoSvg = document.createElementNS(svgNS, "svg"); logoSvg.setAttribute("viewBox", "0 0 1024 1024");
logoSvg.style.width = isMinimized ? "32px" : "40px"; logoSvg.style.height = isMinimized ? "32px" : "40px";
const lp1 = document.createElementNS(svgNS, "path"); lp1.setAttribute("fill", "#ff0033"); lp1.setAttribute("d", "M 511.65 745.41 c 0 0 212.05 0 264.64 -14.01 29.62 -7.94 51.89 -30.81 59.7 -59.05 14.45 -51.82 14.45 -160.82 14.45 -160.82 0 0 0 -108.3 -14.45 -159.65 C 828.18 322.94 805.9 300.53 776.29 292.83 723.69 278.59 511.65 278.59 511.65 278.59 c 0 0 -211.57 0 -263.94 14.24 -29.14 7.7 -51.89 30.11 -60.18 59.05 -13.98 51.35 -13.98 159.65 -13.98 159.65 0 0 0 109 13.98 160.82 8.29 28.24 31.04 51.12 60.18 59.05 52.36 14.01 263.94 14.01 263.94 14.01 z");
const lp2 = document.createElementNS(svgNS, "path"); lp2.setAttribute("fill", "#ffffff"); lp2.setAttribute("stroke", "#ffffff"); lp2.setAttribute("stroke-width", "84"); lp2.setAttribute("d", "M 683 512 l -27 -15 v 31 z M 506 512 l -27 -15 v 31 z");
const lp3 = document.createElementNS(svgNS, "path"); lp3.setAttribute("fill", "#ffffff"); lp3.setAttribute("d", "M 330 421 h 41 v 182 h -41 z M 255 421 h 41 v 182 h -41 z");
logoSvg.appendChild(lp1); logoSvg.appendChild(lp2); logoSvg.appendChild(lp3); logoWrap.appendChild(logoSvg);
leftGroup.appendChild(logoWrap);
const centerGroup = document.createElement('div'); centerGroup.className = 'center-group';
const speedVal = document.createElement('span'); speedVal.id = 'speed-val';
centerGroup.appendChild(speedVal);
const rightGroup = document.createElement('div'); rightGroup.className = 'right-group';
const transTog = document.createElement('div');
transTog.className = 'trans-toggle';
transTog.setAttribute('data-tooltip', `Transparency: ${opacityLevel * 100}%`);
transTog.onmouseover = checkTooltipPos;
const transSvg = document.createElementNS(svgNS, "svg");
transSvg.setAttribute("viewBox", "0 0 100 100");
const transCircle = document.createElementNS(svgNS, "circle");
transCircle.setAttribute("cx", "50");
transCircle.setAttribute("cy", "50");
transCircle.setAttribute("r", "45");
transCircle.setAttribute("fill", colors.btnBg);
transCircle.setAttribute("stroke", colors.btnHover);
transCircle.setAttribute("stroke-width", "5");
const transPath = document.createElementNS(svgNS, "path");
transPath.setAttribute("fill", colors.btnHover);
transPath.setAttribute("d", getTransparencyPath(opacityLevel));
transSvg.appendChild(transCircle);
transSvg.appendChild(transPath);
transTog.appendChild(transSvg);
transTog.onclick = () => {
opacityLevel = opacityLevel >= 1.0 ? 0.25 : opacityLevel + 0.25;
localStorage.setItem('ytSpeedOpacity', opacityLevel);
container.style.opacity = opacityLevel;
transTog.setAttribute('data-tooltip', `Transparency: ${opacityLevel * 100}%`);
transPath.setAttribute("d", getTransparencyPath(opacityLevel));
};
const resetBtn = document.createElement('div'); resetBtn.className = 'reset-btn'; resetBtn.textContent = '↺'; resetBtn.setAttribute('data-tooltip', 'Reset Position'); resetBtn.onmouseover = checkTooltipPos;
resetBtn.onclick = performReset;
rightGroup.appendChild(transTog); rightGroup.appendChild(resetBtn);
topBar.appendChild(minDrag); topBar.appendChild(leftGroup); topBar.appendChild(centerGroup); topBar.appendChild(rightGroup);
const mainContent = document.createElement('div'); mainContent.className = 'main-content-area';
if (isLicenseMode) {
const lm = document.createElement('div'); lm.className = 'license-mgr';
const input = document.createElement('input'); input.placeholder = 'License Key...'; input.value = localStorage.getItem('ytSpeed_ProKey') || "";
const ok = document.createElement('button'); ok.textContent = 'OK';
//ok.onclick = () => { localStorage.setItem('ytSpeed_ProKey', input.value.trim()); isLicenseMode = false; redrawOverlay(); };
ok.onclick = () => {
localStorage.setItem('ytSpeed_ProKey', input.value.trim());
isLicenseMode = false;
checkLicense(); // Triggers the shutdown if the key is wrong
};
lm.appendChild(input); lm.appendChild(ok); mainContent.appendChild(lm);
} else {
const slider = document.createElement('input'); slider.type = 'range'; slider.id = 'speed-slider'; slider.min = '0'; slider.max = '2.0'; slider.step = '0.05'; slider.oninput = (e) => updateSpeed(e.target.value, true);
const btnRow = document.createElement('div'); btnRow.style.display="flex"; btnRow.style.gap="3px";
const ps = [{t:'⏸', type:'pause'}, {t:'.25', v:0.25}, {t:'.75', v:0.75}, {t:'1x', v:1}, {t:'1.25', v:1.25}, {t:'1.5', v:1.5}, {t:'2x', v:2}];
ps.forEach(item => {
//const b = document.createElement('button'); b.className = 'preset-btn'; b.textContent = item.t;
const b = document.createElement('button');
b.className = 'preset-btn';
if (item.type === 'pause') {
b.id = 'pause-toggle-btn';
// Wrap text in a span so we can move just the text in CSS
const textSpan = document.createElement('span');
textSpan.textContent = item.t;
textSpan.style.position = 'relative';
textSpan.style.top = '-2.7px'; // Exact match to your chapter nav
b.appendChild(textSpan);
b.onclick = () => togglePause();
} else {
b.textContent = item.t;
b.onclick = () => updateSpeed(item.v, true);
}
if (item.type === 'pause') { b.id = 'pause-toggle-btn'; b.onclick = () => togglePause(); } else b.onclick = () => updateSpeed(item.v, true);
btnRow.appendChild(b);
});
mainContent.appendChild(slider); mainContent.appendChild(btnRow);
if (isProUser) {
const proArea = document.createElement('div'); proArea.className = 'pro-area';
const stats = document.createElement('div'); stats.className = 'stats-row';
const timeSpan = document.createElement('span'); timeSpan.id = 'time-saved-stat';
const chapNav = document.createElement('div'); chapNav.className = 'chapter-nav';
const prevC = document.createElement('span'); prevC.textContent = '«'; prevC.setAttribute('data-tooltip', 'Prev Chapter'); prevC.onmouseover = checkTooltipPos; prevC.onclick = (e) => { e.stopPropagation(); navigateChapter('prev'); };
const nextC = document.createElement('span'); nextC.textContent = '»'; nextC.setAttribute('data-tooltip', 'Next Chapter'); nextC.onmouseover = checkTooltipPos; nextC.onclick = (e) => { e.stopPropagation(); navigateChapter('next'); };
chapNav.appendChild(prevC); chapNav.appendChild(nextC);
stats.appendChild(timeSpan); stats.appendChild(chapNav);
const proBtns = document.createElement('div'); proBtns.className = 'pro-btns';
const mBtn = document.createElement('button');
mBtn.className = 'preset-btn';
mBtn.textContent = 'Music';
mBtn.setAttribute('data-tooltip', 'Music Mode (1x)'); // Corrected
mBtn.onmouseover = checkTooltipPos; // Corrected
mBtn.onclick = () => {
proSettings.musicOneX = !proSettings.musicOneX;
saveProSettings();
const info = getCategoryInfo();
const isNowMusic = (info.id === "10" || info.name === "Music") && proSettings.musicOneX;
const v = document.querySelector('video');
if (v && !v.paused) {
checkCategoryAndAdjustSpeed();
} else {
autoSpeedActive = isNowMusic; // Update state without triggering video play
updateUI(0);
}
};
const vBtn = document.createElement('button');
vBtn.className = 'preset-btn';
vBtn.textContent = 'Movies';
vBtn.setAttribute('data-tooltip', 'Movie Mode (1x)'); // Corrected
vBtn.onmouseover = checkTooltipPos; // Corrected
vBtn.onclick = () => {
proSettings.movieOneX = !proSettings.movieOneX;
saveProSettings();
const info = getCategoryInfo();
const isNowMovie = (["30", "43", "1"].includes(info.id) || ["Movies", "Shows", "Film & Animation"].includes(info.name)) && proSettings.movieOneX;
const v = document.querySelector('video');
if (v && !v.paused) {
checkCategoryAndAdjustSpeed();
} else {
autoSpeedActive = isNowMovie; // Update state without triggering video play
updateUI(0);
}
};
const pBtn = document.createElement('button');
pBtn.className = 'preset-btn';
pBtn.textContent = 'P';
pBtn.setAttribute('data-tooltip', 'Toggle Pitch Shift'); // Corrected
pBtn.onmouseover = checkTooltipPos; // Corrected
pBtn.onclick = () => togglePitch();
proBtns.appendChild(mBtn); proBtns.appendChild(vBtn); proBtns.appendChild(pBtn);
proArea.appendChild(stats); proArea.appendChild(proBtns); mainContent.appendChild(proArea); updateSavedDisplay();
}
}
container.appendChild(headerRow); container.appendChild(topBar); container.appendChild(mainContent); document.body.appendChild(container);
[headerRow, titleDragHandle, minDrag].forEach(el => {
el.onmousedown = (e) => {
if (e.target.closest('.gf-link')) return;
const rect = container.getBoundingClientRect(); let sx = e.clientX - rect.left, sy = e.clientY - rect.top;
//function move(me) { container.style.left = (me.clientX - sx) + 'px'; container.style.top = (me.clientY - sy) + 'px'; container.style.bottom = 'auto'; }
function move(me) {
container.style.left = (me.clientX - sx) + 'px';
container.style.top = (me.clientY - sy) + 'px';
container.style.bottom = 'auto';
// Boundary Check for Tooltips
const rect = container.getBoundingClientRect();
if (rect.right > window.innerWidth - 100) {
container.classList.add('edge-right');
container.classList.remove('edge-left');
} else if (rect.left < 100) {
container.classList.add('edge-left');
container.classList.remove('edge-right');
} else {
container.classList.remove('edge-right', 'edge-left');
}
}
function stop() { document.removeEventListener('mousemove', move); document.removeEventListener('mouseup', stop); localStorage.setItem('ytSpeedPos', JSON.stringify({ top: container.style.top, left: container.style.left })); }
document.addEventListener('mousemove', move); document.addEventListener('mouseup', stop);
};
});
const savedPos = JSON.parse(localStorage.getItem('ytSpeedPos'));
if (savedPos) { container.style.left = savedPos.left; container.style.top = savedPos.top; container.style.bottom = 'auto'; }
updateUI(autoSpeedActive ? 1.0 : lastActiveSpeed);
setInterval(ensureInView, 3000);
}
function togglePitch() {
if (!isProUser) return;
pitchShiftEnabled = !pitchShiftEnabled;
localStorage.setItem('ytPitchShiftEnabled', pitchShiftEnabled);
const v = document.querySelector('video');
if (v) {
v.preservesPitch = !pitchShiftEnabled;
// Only trigger playback rate change if not paused
if (!v.paused) {
const target = autoSpeedActive ? 1.0 : lastActiveSpeed;
v.playbackRate = target;
}
}
updateUI(v && v.paused ? 0 : (autoSpeedActive ? 1.0 : lastActiveSpeed));
}
function togglePause() { const v = document.querySelector('video'); if (!v) return; if (!v.paused && v.playbackRate > 0) { prePauseSpeed = v.playbackRate; updateSpeed(0, false); } else { const res = autoSpeedActive ? 1.0 : (prePauseSpeed || lastActiveSpeed || 1.0); applySpeedToVideo(res); updateUI(res); } }
function updateSpeed(val, isUser) {
let n = parseFloat(val);
if (isNaN(n)) n = 1.0;
if (isUser && n > 0 && autoSpeedActive) {
const info = getCategoryInfo();
if ((info.name === "Music") && proSettings.musicOneX) proSettings.musicOneX = false;
else if (["Movies", "Shows", "Film & Animation"].includes(info.name) && proSettings.movieOneX) proSettings.movieOneX = false;
saveProSettings(); autoSpeedActive = false;
}
if (n > 0 && !autoSpeedActive) { lastActiveSpeed = n; localStorage.setItem('ytPersistentSpeed', n); }
applySpeedToVideo(n); updateUI(n);
}
function applySpeedToVideo(n) { const v = document.querySelector('video'); if (v) { v.preservesPitch = !pitchShiftEnabled; if (n === 0) v.pause(); else { v.playbackRate = n; if (v.paused) v.play().catch(e => { }); } } }
function updateUI(n) {
const container = document.getElementById('yt-speed-overlay');
const sld = document.getElementById('speed-slider');
const val = document.getElementById('speed-val');
if (!container) return;
// 1. Handle Slider Color Logic (Classes for CSS)
const isProFeatureOn = (autoSpeedActive || pitchShiftEnabled);
container.classList.toggle('pro-active', isProFeatureOn);
container.classList.toggle('is-paused', n === 0);
// 2. Update Slider Value & Text Display
//if (sld) sld.value = n;
if (sld) {
sld.value = n;
/** * FIX: Slider thumb logic
* Ensures it reverts to standard red when at 1x speed.
*/
sld.classList.toggle('override', isProFeatureOn && n > 0 && n !== 1);
}
if (val) {
// Update the text content (PAUSED or Speed)
val.textContent = n > 0 ? n.toFixed(2) + 'x' : 'PAUSED';
val.classList.toggle('override', isProFeatureOn && n > 0 && n !== 1);
}
// 3. Update All Button Active States
document.querySelectorAll('.preset-btn').forEach(b => {
const txt = b.textContent;
// A. Speed Presets (.25, 1x, 2x, etc.)
// We exclude Music, Movies, P, and ⏸ from the numeric comparison
if (!['Music', 'Movies', 'P', '⏸'].includes(txt)) {
const isCurrentSpeed = parseFloat(txt) === n || (txt === '1x' && n === 1);
b.classList.toggle('active', isCurrentSpeed);
}
// B. Pro Toggle Buttons (Music, Movies, P)
if (txt === 'Music') b.classList.toggle('active', proSettings.musicOneX);
if (txt === 'Movies') b.classList.toggle('active', proSettings.movieOneX);
if (txt === 'P') b.classList.toggle('active', pitchShiftEnabled);
// C. Pause Button (Optional: stays active while paused)
if (txt === '⏸') b.classList.toggle('active', n === 0);
});
// 4. Update the "Time Saved" display
updateSavedDisplay();
}
function updateUIClasses(rate) {
const video = document.querySelector('video'), info = getCategoryInfo();
const curRate = rate !== undefined ? rate : (video ? (video.paused ? 0 : video.playbackRate) : 1.0);
const isPaused = video ? (video.paused || curRate === 0) : (curRate === 0);
document.querySelectorAll('.preset-btn').forEach(btn => {
btn.classList.remove('active', 'override');
if (btn.id === 'pause-toggle-btn') { if (isPaused) btn.classList.add('active'); }
else { const val = parseFloat(btn.textContent); if (!isPaused && (val === curRate || (btn.textContent === '1x' && curRate === 1))) btn.classList.add('active'); }
if (btn.textContent === 'P' && isProUser && pitchShiftEnabled) { btn.classList.add('active'); if (curRate !== 1.0 && !isPaused) btn.classList.add('override'); }
if (btn.textContent === 'Music' && isProUser && proSettings.musicOneX) { btn.classList.add('active'); if (info.name === "Music" && !isPaused) btn.classList.add('override'); }
if (btn.textContent === 'Movies' && isProUser && proSettings.movieOneX) { btn.classList.add('active'); if (["Movies","Shows","Film & Animation"].includes(info.name) && !isPaused) btn.classList.add('override'); }
});
}
function syncLoop() {
if (!document.getElementById('yt-speed-overlay')) redrawOverlay();
const v = document.querySelector('video'); if (isProUser) updateSavedDisplay();
if (v) {
if (!v.dataset.trackingAttached) { v.addEventListener('timeupdate', trackTimeSaved); v.dataset.trackingAttached = "true"; }
const target = autoSpeedActive ? 1.0 : lastActiveSpeed;
if (v.paused) updateUI(0);
else { if (Math.abs(v.playbackRate - target) > 0.01 && !isLicenseMode) v.playbackRate = target; updateUI(v.playbackRate); }
updateUIClasses();
}
requestAnimationFrame(syncLoop);
}
window.addEventListener('yt-navigate-finish', () => { setTimeout(checkCategoryAndAdjustSpeed, 1200); });
updateStateFromStorage(); syncLoop();
})();