// ==UserScript== // @name YouTube downloader // @icon https://raw.githubusercontent.com/madkarmaa/youtube-downloader/main/images/icon.png // @namespace aGkgdGhlcmUgOik= // @source https://github.com/madkarmaa/youtube-downloader // @supportURL https://github.com/madkarmaa/youtube-downloader // @version 3.4.0 // @description A simple userscript to download YouTube videos in MAX QUALITY // @author mk_ // @match *://*.youtube.com/* // @connect api.cobalt.tools // @connect raw.githubusercontent.com // @grant GM_info // @grant GM_addStyle // @grant GM_xmlHttpRequest // @grant GM_xmlhttpRequest // @run-at document-start // @downloadURL https://update.greasyfork.icu/scripts/479944/YouTube%20downloader.user.js // @updateURL https://update.greasyfork.icu/scripts/479944/YouTube%20downloader.meta.js // ==/UserScript== (async () => { 'use strict'; // prettier-ignore // abort if not on youtube or youtube music or if in an iframe if (!detectYoutubeService() || window !== window.parent) return; // ===== VARIABLES ===== let ADVANCED_SETTINGS = localStorage.getItem('ytdl-advanced-settings') ? JSON.parse(localStorage.getItem('ytdl-advanced-settings')) : { enabled: false, openUrl: '', }; localStorage.setItem('ytdl-advanced-settings', JSON.stringify(ADVANCED_SETTINGS)); let DEV_MODE = String(localStorage.getItem('ytdl-dev-mode')).toLowerCase() === 'true'; let SHOW_NOTIFICATIONS = localStorage.getItem('ytdl-notif-enabled') === null ? true : String(localStorage.getItem('ytdl-notif-enabled')).toLowerCase() === 'true'; let oldILog = console.log; let oldWLog = console.warn; let oldELog = console.error; let VIDEO_DATA = { video_duration: null, video_url: null, video_author: null, video_title: null, video_id: null, }; let videoDataReady = false; // https://github.com/imputnet/cobalt/blob/current/docs/api.md#request-body-variables const QUALITIES = { MAX: 'max', '2160p': '2160', '1440p': '1440', '1080p': '1080', '720p': '720', '480p': '480', '360p': '360', '240p': '240', '144p': '144', }; // ===== END VARIABLES ===== // ===== METHODS ===== function logger(level, ...args) { if (DEV_MODE && level.toLowerCase() === 'info') oldILog.apply(console, ['%c[YTDL]', 'color: #f00;', ...args]); else if (DEV_MODE && level.toLowerCase() === 'warn') oldWLog.apply(console, ['%c[YTDL]', 'color: #f00;', ...args]); else if (level.toLowerCase() === 'error') oldELog.apply(console, ['%c[YTDL]', 'color: #f00;', ...args]); } function Cobalt(videoUrl, audioOnly = false) { // Use Promise because GM.xmlHttpRequest behaves differently with different userscript managers return new Promise((resolve, reject) => { // https://github.com/imputnet/cobalt/blob/current/docs/api.md GM_xmlhttpRequest({ method: 'POST', url: 'https://api.cobalt.tools/api/json', headers: { 'Cache-Control': 'no-cache', Accept: 'application/json', 'Content-Type': 'application/json', }, data: JSON.stringify({ url: encodeURI(videoUrl), vQuality: localStorage.getItem('ytdl-quality') ?? 'max', filenamePattern: 'basic', // file name = video title isAudioOnly: audioOnly, disableMetadata: true, // privacy }), onload: (response) => { const data = JSON.parse(response.responseText); if (data?.url) resolve(data.url); else reject(data); }, onerror: (err) => reject(err), }); }); } // https://stackoverflow.com/a/61511955 function waitForElement(selector) { return new Promise((resolve) => { if (document.querySelector(selector)) return resolve(document.querySelector(selector)); const observer = new MutationObserver(() => { if (document.querySelector(selector)) { observer.disconnect(); resolve(document.querySelector(selector)); } }); observer.observe(document.body, { childList: true, subtree: true }); }); } function fetchNotifications() { // Use Promise because GM.xmlHttpRequest behaves differently with different userscript managers return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: 'https://raw.githubusercontent.com/madkarmaa/youtube-downloader/main/notifications.json', headers: { 'Cache-Control': 'no-cache', Accept: 'application/json', 'Content-Type': 'application/json', }, onload: (response) => { const data = JSON.parse(response.responseText); if (data?.length) resolve(data); else reject(data); }, onerror: (err) => reject(err), }); }); } class Notification { constructor(title, body, uuid, storeUUID = true) { const notification = document.createElement('div'); notification.classList.add('ytdl-notification', 'opened', uuid); hideOnAnimationEnd(notification, 'closeNotif', true); const nTitle = document.createElement('h2'); nTitle.textContent = title; notification.appendChild(nTitle); const nBody = document.createElement('div'); body.split('\n').forEach((text) => { const paragraph = document.createElement('p'); paragraph.textContent = text; nBody.appendChild(paragraph); }); notification.appendChild(nBody); const nDismissButton = document.createElement('button'); nDismissButton.textContent = 'Dismiss'; nDismissButton.addEventListener('click', () => { if (storeUUID) { const localNotificationsHashes = JSON.parse(localStorage.getItem('ytdl-notifications') ?? '[]'); localNotificationsHashes.push(uuid); localStorage.setItem('ytdl-notifications', JSON.stringify(localNotificationsHashes)); logger('info', `Notification ${uuid} set as read`); } notification.classList.remove('opened'); notification.classList.add('closed'); }); notification.appendChild(nDismissButton); document.body.appendChild(notification); logger('info', 'New notification displayed', notification); } } async function manageNotifications() { if (!SHOW_NOTIFICATIONS) { logger('info', 'Notifications disabled by the user'); return; } const localNotificationsHashes = JSON.parse(localStorage.getItem('ytdl-notifications')) ?? []; logger('info', 'Local read notifications hashes\n\n', localNotificationsHashes); const onlineNotifications = await fetchNotifications(); logger( 'info', 'Online notifications hashes\n\n', onlineNotifications.map((n) => n.uuid) ); const unreadNotifications = onlineNotifications.filter((n) => !localNotificationsHashes.includes(n.uuid)); logger( 'info', 'Unread notifications hashes\n\n', unreadNotifications.map((n) => n.uuid) ); unreadNotifications.reverse().forEach((n) => { new Notification(n.title, n.body, n.uuid); }); } async function updateVideoData(e) { videoDataReady = false; const temp_video_data = e.detail?.getVideoData(); VIDEO_DATA.video_duration = e.detail?.getDuration(); VIDEO_DATA.video_url = e.detail?.getVideoUrl(); VIDEO_DATA.video_author = temp_video_data?.author; VIDEO_DATA.video_title = temp_video_data?.title; VIDEO_DATA.video_id = temp_video_data?.video_id; videoDataReady = true; logger('info', 'Video data updated\n\n', VIDEO_DATA); } async function hookPlayerEvent(...fns) { document.addEventListener('yt-player-updated', (e) => { for (let i = 0; i < fns.length; i++) fns[i](e); }); logger( 'info', 'Video player event hooked. Callbacks:\n\n', fns.map((f) => f.name) ); } async function hookNavigationEvents(...fns) { ['yt-navigate', 'yt-navigate-finish', 'yt-navigate-finish', 'yt-page-data-updated'].forEach((evName) => { document.addEventListener(evName, (e) => { for (let i = 0; i < fns.length; i++) fns[i](e); }); }); logger( 'info', 'Navigation events hooked. Callbacks:\n\n', fns.map((f) => f.name) ); } function hideOnAnimationEnd(target, animationName, alsoRemove = false) { target.addEventListener('animationend', (e) => { if (e.animationName === animationName) { if (alsoRemove) e.target.remove(); else e.target.style.display = 'none'; } }); } // https://stackoverflow.com/a/10344293 function isTyping() { const el = document.activeElement; return ( el && (el.tagName.toLowerCase() === 'input' || el.tagName.toLowerCase() === 'textarea' || String(el.getAttribute('contenteditable')).toLowerCase() === 'true') ); } function replacePlaceholders(inputString) { return inputString.replace(/{{\s*([^}\s]+)\s*}}/g, (match, placeholder) => VIDEO_DATA[placeholder] || match); } async function appendSideMenu() { const sideMenu = document.createElement('div'); sideMenu.id = 'ytdl-sideMenu'; sideMenu.classList.add('closed'); sideMenu.style.display = 'none'; hideOnAnimationEnd(sideMenu, 'closeMenu'); const sideMenuHeader = document.createElement('h2'); sideMenuHeader.textContent = 'Youtube downloader settings'; sideMenuHeader.classList.add('header'); sideMenu.appendChild(sideMenuHeader); // ===== templates, don't use, just clone the node ===== const sideMenuSettingContainer = document.createElement('div'); sideMenuSettingContainer.classList.add('setting-row'); const sideMenuSettingLabel = document.createElement('h3'); sideMenuSettingLabel.classList.add('setting-label'); const sideMenuSettingDescription = document.createElement('p'); sideMenuSettingDescription.classList.add('setting-description'); sideMenuSettingContainer.append(sideMenuSettingLabel, sideMenuSettingDescription); const switchContainer = document.createElement('span'); switchContainer.classList.add('ytdl-switch'); const switchCheckbox = document.createElement('input'); switchCheckbox.type = 'checkbox'; const switchLabel = document.createElement('label'); switchContainer.append(switchCheckbox, switchLabel); // ===== end templates ===== // NOTIFICATIONS const notifContainer = sideMenuSettingContainer.cloneNode(true); notifContainer.querySelector('.setting-label').textContent = 'Notifications'; notifContainer.querySelector('.setting-description').textContent = "Disable if you don't want to receive notifications from the developer."; const notifSwitch = switchContainer.cloneNode(true); notifSwitch.querySelector('input').checked = SHOW_NOTIFICATIONS; notifSwitch.querySelector('input').id = 'ytdl-notif-switch'; notifSwitch.querySelector('label').setAttribute('for', 'ytdl-notif-switch'); notifSwitch.querySelector('input').addEventListener('change', (e) => { SHOW_NOTIFICATIONS = e.target.checked; localStorage.setItem('ytdl-notif-enabled', SHOW_NOTIFICATIONS); logger('info', `Notifications ${SHOW_NOTIFICATIONS ? 'enabled' : 'disabled'}`); }); notifContainer.appendChild(notifSwitch); sideMenu.appendChild(notifContainer); // VIDEO QUALITY CONTROL const qualityContainer = sideMenuSettingContainer.cloneNode(true); qualityContainer.querySelector('.setting-label').textContent = 'Video download quality'; qualityContainer.querySelector('.setting-description').textContent = 'Control the resolution of the downloaded videos. Not all the resolutions are supported by some videos.'; const qualitySelect = document.createElement('select'); qualitySelect.name = 'dl-quality'; qualitySelect.id = 'ytdl-dl-quality-select'; qualitySelect.disabled = ADVANCED_SETTINGS.enabled; Object.entries(QUALITIES).forEach(([name, value]) => { const qualityOption = document.createElement('option'); qualityOption.textContent = name; qualityOption.value = value; qualitySelect.appendChild(qualityOption); }); qualitySelect.value = localStorage.getItem('ytdl-quality') ?? 'max'; qualitySelect.addEventListener('change', (e) => { localStorage.setItem('ytdl-quality', String(e.target.value)); logger('info', `Download quality set to ${e.target.value}`); }); qualityContainer.appendChild(qualitySelect); sideMenu.appendChild(qualityContainer); // DEVELOPER MODE const devModeContainer = sideMenuSettingContainer.cloneNode(true); devModeContainer.querySelector('.setting-label').textContent = 'Developer mode'; devModeContainer.querySelector('.setting-description').textContent = "Show a detailed output of what's happening under the hood in the console."; const devModeSwitch = switchContainer.cloneNode(true); devModeSwitch.querySelector('input').checked = DEV_MODE; devModeSwitch.querySelector('input').id = 'ytdl-dev-mode-switch'; devModeSwitch.querySelector('label').setAttribute('for', 'ytdl-dev-mode-switch'); devModeSwitch.querySelector('input').addEventListener('change', (e) => { DEV_MODE = e.target.checked; localStorage.setItem('ytdl-dev-mode', DEV_MODE); // always use console.log here to show output console.log(`\x1b[31m[YTDL]\x1b[0m Developer mode ${DEV_MODE ? 'enabled' : 'disabled'}`); }); devModeContainer.appendChild(devModeSwitch); sideMenu.appendChild(devModeContainer); // ADVANCED SETTINGS const advancedSettingsContainer = sideMenuSettingContainer.cloneNode(true); advancedSettingsContainer.querySelector('.setting-label').textContent = 'Advanced settings'; advancedSettingsContainer.querySelector('.setting-description').textContent = 'FOR EXPERIENCED USERS ONLY. Modify the behaviour of the download button.'; const advancedOptionsContainer = document.createElement('div'); advancedOptionsContainer.classList.add('advanced-options', ADVANCED_SETTINGS.enabled ? 'opened' : 'closed'); advancedOptionsContainer.style.display = ADVANCED_SETTINGS.enabled ? 'flex' : 'none'; hideOnAnimationEnd(advancedOptionsContainer, 'closeNotif'); const advancedSwitch = switchContainer.cloneNode(true); advancedSwitch.querySelector('input').checked = ADVANCED_SETTINGS.enabled; advancedSwitch.querySelector('input').id = 'ytdl-advanced-switch'; advancedSwitch.querySelector('label').setAttribute('for', 'ytdl-advanced-switch'); advancedSwitch.querySelector('input').addEventListener('change', (e) => { ADVANCED_SETTINGS.enabled = e.target.checked; localStorage.setItem('ytdl-advanced-settings', JSON.stringify(ADVANCED_SETTINGS)); qualitySelect.disabled = e.target.checked; if (e.target.checked) { advancedOptionsContainer.style.display = 'flex'; advancedOptionsContainer.classList.remove('closed'); advancedOptionsContainer.classList.add('opened'); } else { advancedOptionsContainer.classList.remove('opened'); advancedOptionsContainer.classList.add('closed'); } logger('info', `Advanced settings ${ADVANCED_SETTINGS.enabled ? 'enabled' : 'disabled'}`); }); advancedSettingsContainer.appendChild(advancedSwitch); const openUrlLabel = document.createElement('label'); openUrlLabel.setAttribute('for', 'advanced-settings-open-url'); openUrlLabel.textContent = 'Open the given URL in a new window. GET request only.'; const placeholdersLink = document.createElement('a'); placeholdersLink.href = 'https://github.com/madkarmaa/youtube-downloader/blob/main/docs/PLACEHOLDERS.md'; placeholdersLink.target = '_blank'; placeholdersLink.textContent = 'Use placeholders to access video data. Click to know about placeholders'; openUrlLabel.appendChild(placeholdersLink); const openUrlInput = document.createElement('input'); openUrlInput.id = 'advanced-settings-open-url'; openUrlInput.type = 'url'; openUrlInput.placeholder = 'URL to open'; openUrlInput.value = ADVANCED_SETTINGS.openUrl ?? null; openUrlInput.addEventListener('focusout', (e) => { if (e.target.checkValidity()) { ADVANCED_SETTINGS.openUrl = e.target.value; localStorage.setItem('ytdl-advanced-settings', JSON.stringify(ADVANCED_SETTINGS)); logger('info', `Advanced settings: URL to open set to "${e.target.value}"`); } else { logger('error', `Invalid URL to open: "${e.target.value}"`); alert(e.target.validationMessage); e.target.value = ''; } }); advancedOptionsContainer.append(openUrlLabel, openUrlInput); advancedSettingsContainer.appendChild(advancedOptionsContainer); sideMenu.appendChild(advancedSettingsContainer); // SIDE MENU EVENTS document.addEventListener('mousedown', (e) => { if (sideMenu.style.display !== 'none' && !sideMenu.contains(e.target)) { sideMenu.classList.remove('opened'); sideMenu.classList.add('closed'); logger('info', 'Side menu closed'); } }); document.addEventListener('keydown', (e) => { if (e.key !== 'p') return; if (isTyping()) return; if (sideMenu.style.display === 'none') { sideMenu.style.top = window.scrollY + 'px'; sideMenu.style.display = 'flex'; sideMenu.classList.remove('closed'); sideMenu.classList.add('opened'); logger('info', 'Side menu opened'); } else { sideMenu.classList.remove('opened'); sideMenu.classList.add('closed'); logger('info', 'Side menu closed'); } }); window.addEventListener('scroll', () => { if (sideMenu.classList.contains('closed')) return; sideMenu.classList.remove('opened'); sideMenu.classList.add('closed'); logger('info', 'Side menu closed'); }); document.body.appendChild(sideMenu); logger('info', 'Side menu created\n\n', sideMenu); } function detectYoutubeService() { if (window.location.hostname === 'www.youtube.com' && window.location.pathname.startsWith('/shorts')) return 'SHORTS'; if (window.location.hostname === 'www.youtube.com' && window.location.pathname.startsWith('/watch')) return 'WATCH'; else if (window.location.hostname === 'music.youtube.com') return 'MUSIC'; else if (window.location.hostname === 'www.youtube.com') return 'YOUTUBE'; else return null; } function elementInContainer(container, element) { return container.contains(element); } async function leftClick() { const isYtMusic = detectYoutubeService() === 'MUSIC'; if (!isYtMusic && !videoDataReady) { logger('warn', 'Video data not ready'); new Notification('Wait!', 'The video data is not ready yet, try again in a few seconds.', 'popup', false); return; } else if (isYtMusic && !window.location.pathname.startsWith('/watch')) { logger('warn', 'Video URL not avaiable'); new Notification( 'Wait!', 'Open the music player so the song link is visible, then try again.', 'popup', false ); return; } try { logger('info', 'Download started'); if (!ADVANCED_SETTINGS.enabled) window.open( await Cobalt( isYtMusic ? window.location.href.replace('music.youtube.com', 'www.youtube.com') : VIDEO_DATA.video_url ), '_blank' ); else if (ADVANCED_SETTINGS.openUrl) window.open(replacePlaceholders(ADVANCED_SETTINGS.openUrl)); logger('info', 'Download completed'); } catch (err) { logger('error', JSON.parse(JSON.stringify(err))); new Notification('Error', JSON.stringify(err), 'error', false); } } async function rightClick(e) { const isYtMusic = detectYoutubeService() === 'MUSIC'; e.preventDefault(); if (!isYtMusic && !videoDataReady) { logger('warn', 'Video data not ready'); new Notification('Wait!', 'The video data is not ready yet, try again in a few seconds.', 'popup', false); return false; } else if (isYtMusic && !window.location.pathname.startsWith('/watch')) { logger('warn', 'Video URL not avaiable'); new Notification( 'Wait!', 'Open the music player so the song link is visible, then try again.', 'popup', false ); return; } try { logger('info', 'Download started'); if (!ADVANCED_SETTINGS.enabled) window.open( await Cobalt( isYtMusic ? window.location.href.replace('music.youtube.com', 'www.youtube.com') : VIDEO_DATA.video_url, true ), '_blank' ); else if (ADVANCED_SETTINGS.openUrl) window.open(replacePlaceholders(ADVANCED_SETTINGS.openUrl)); logger('info', 'Download completed'); } catch (err) { logger('error', JSON.parse(JSON.stringify(err))); new Notification('Error', JSON.stringify(err), 'error', false); } return false; } // https://www.30secondsofcode.org/js/s/element-is-visible-in-viewport/ function elementIsVisibleInViewport(el, partiallyVisible = false) { const { top, left, bottom, right } = el.getBoundingClientRect(); const { innerHeight, innerWidth } = window; return partiallyVisible ? ((top > 0 && top < innerHeight) || (bottom > 0 && bottom < innerHeight)) && ((left > 0 && left < innerWidth) || (right > 0 && right < innerWidth)) : top >= 0 && left >= 0 && bottom <= innerHeight && right <= innerWidth; } async function appendDownloadButton(e) { const ytContainerSelector = '#movie_player > div.ytp-chrome-bottom > div.ytp-chrome-controls > div.ytp-right-controls'; const ytmContainerSelector = '#layout > ytmusic-player-bar > div.middle-controls.style-scope.ytmusic-player-bar > div.middle-controls-buttons.style-scope.ytmusic-player-bar'; const ytsContainerSelector = '#actions.style-scope.ytd-reel-player-overlay-renderer'; // ===== templates, don't use, just clone the node ===== const downloadIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); downloadIcon.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); downloadIcon.setAttribute('fill', 'currentColor'); downloadIcon.setAttribute('height', '24'); downloadIcon.setAttribute('viewBox', '0 0 24 24'); downloadIcon.setAttribute('width', '24'); downloadIcon.setAttribute('focusable', 'false'); downloadIcon.style.pointerEvents = 'none'; downloadIcon.style.display = 'block'; downloadIcon.style.width = '100%'; downloadIcon.style.height = '100%'; const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); path.setAttribute('d', 'M17 18v1H6v-1h11zm-.5-6.6-.7-.7-3.8 3.7V4h-1v10.4l-3.8-3.8-.7.7 5 5 5-4.9z'); downloadIcon.appendChild(path); const downloadButton = document.createElement('button'); downloadButton.id = 'ytdl-download-button'; downloadButton.classList.add('ytp-button'); downloadButton.title = 'Left click to download as video, right click as audio only'; downloadButton.appendChild(downloadIcon); // ===== end templates ===== switch (detectYoutubeService()) { case 'WATCH': const ytCont = await waitForElement(ytContainerSelector); logger('info', 'Download button container found\n\n', ytCont); if (elementInContainer(ytCont, ytCont.querySelector('#ytdl-download-button'))) { logger('warn', 'Download button already in container'); break; } const ytDlBtnClone = downloadButton.cloneNode(true); ytDlBtnClone.classList.add('YT'); ytDlBtnClone.addEventListener('click', leftClick); ytDlBtnClone.addEventListener('contextmenu', rightClick); logger('info', 'Download button created\n\n', ytDlBtnClone); ytCont.insertBefore(ytDlBtnClone, ytCont.firstChild); logger('info', 'Download button inserted in container'); break; case 'MUSIC': const ytmCont = await waitForElement(ytmContainerSelector); logger('info', 'Download button container found\n\n', ytmCont); if (elementInContainer(ytmCont, ytmCont.querySelector('#ytdl-download-button'))) { logger('warn', 'Download button already in container'); break; } const ytmDlBtnClone = downloadButton.cloneNode(true); ytmDlBtnClone.classList.add('YTM'); ytmDlBtnClone.addEventListener('click', leftClick); ytmDlBtnClone.addEventListener('contextmenu', rightClick); logger('info', 'Download button created\n\n', ytmDlBtnClone); ytmCont.insertBefore(ytmDlBtnClone, ytmCont.firstChild); logger('info', 'Download button inserted in container'); break; case 'SHORTS': if (e.type !== 'yt-navigate-finish') return; await waitForElement(ytsContainerSelector); // wait for the UI to finish loading const visibleYtsConts = Array.from(document.querySelectorAll(ytsContainerSelector)).filter((el) => elementIsVisibleInViewport(el) ); logger('info', 'Download button containers found\n\n', visibleYtsConts); visibleYtsConts.forEach((ytsCont) => { if (elementInContainer(ytsCont, ytsCont.querySelector('#ytdl-download-button'))) { logger('warn', 'Download button already in container'); return; } const ytsDlBtnClone = downloadButton.cloneNode(true); ytsDlBtnClone.classList.add( 'YTS', 'yt-spec-button-shape-next', 'yt-spec-button-shape-next--tonal', 'yt-spec-button-shape-next--mono', 'yt-spec-button-shape-next--size-l', 'yt-spec-button-shape-next--icon-button' ); ytsDlBtnClone.addEventListener('click', leftClick); ytsDlBtnClone.addEventListener('contextmenu', rightClick); logger('info', 'Download button created\n\n', ytsDlBtnClone); ytsCont.insertBefore(ytsDlBtnClone, ytsCont.firstChild); logger('info', 'Download button inserted in container'); }); break; default: return; } } async function devStuff() { if (!DEV_MODE) return; logger('info', 'Current service is: ' + detectYoutubeService()); } // ===== END METHODS ===== GM_addStyle(` #ytdl-sideMenu { min-height: 100vh; z-index: 9998; position: absolute; top: 0; left: -100vw; width: 50vw; background-color: var(--yt-spec-base-background); border-right: 2px solid var(--yt-spec-static-grey); display: flex; flex-direction: column; gap: 2rem; padding: 2rem 2.5rem; font-family: "Roboto", "Arial", sans-serif; } #ytdl-sideMenu.opened { animation: openMenu .3s linear forwards; } #ytdl-sideMenu.closed { animation: closeMenu .3s linear forwards; } #ytdl-sideMenu a { color: var(--yt-brand-youtube-red); text-decoration: none; font-weight: 600; } #ytdl-sideMenu a:hover { text-decoration: underline; } #ytdl-sideMenu label { display: flex; flex-direction: column; gap: 0.5rem; font-size: 1.4rem; color: var(--yt-spec-text-primary); } #ytdl-sideMenu .header { text-align: center; font-size: 2.5rem; color: var(--yt-brand-youtube-red); } #ytdl-sideMenu .setting-row { display: flex; flex-direction: column; gap: 1rem; transition: all 0.2s ease-in-out; } #ytdl-sideMenu .setting-label { font-size: 1.8rem; color: var(--yt-brand-youtube-red); } #ytdl-sideMenu .setting-description { font-size: 1.4rem; color: var(--yt-spec-text-primary); } .ytdl-switch { display: inline-block; } .ytdl-switch input { display: none; } .ytdl-switch label { display: block; width: 50px; height: 19.5px; padding: 3px; border-radius: 15px; border: 2px solid var(--yt-brand-medium-red); cursor: pointer; transition: 0.3s; } .ytdl-switch label::after { content: ""; display: inherit; width: 20px; height: 20px; border-radius: 12px; background: var(--yt-brand-medium-red); transition: 0.3s; } .ytdl-switch input:checked ~ label { border-color: var(--yt-spec-light-green); } .ytdl-switch input:checked ~ label::after { translate: 30px 0; background: var(--yt-spec-light-green); } .ytdl-switch input:disabled ~ label { opacity: 0.5; cursor: not-allowed; } #ytdl-sideMenu .advanced-options { display: flex; flex-direction: column; gap: 0.7rem; margin: 1rem 0; } #ytdl-sideMenu .advanced-options.opened { animation: openNotif 0.3s linear forwards; } #ytdl-sideMenu .advanced-options.closed { animation: closeNotif .3s linear forwards; } #ytdl-sideMenu input[type="url"] { background: none; padding: 0.7rem 1rem; border: none; outline: none; border-bottom: 2px solid var(--yt-spec-red-70); color: var(--yt-spec-text-primary); font-family: monospace; transition: border-bottom-color 0.2s ease-in-out; } #ytdl-sideMenu input[type="url"]:focus { border-bottom-color: var(--yt-brand-youtube-red); } .ytdl-notification { display: flex; flex-direction: column; gap: 2rem; position: fixed; top: 50vh; left: 50vw; transform: translate(-50%, -50%); background-color: var(--yt-spec-base-background); border: 2px solid var(--yt-spec-static-grey); border-radius: 8px; color: var(--yt-spec-text-primary); z-index: 9999; padding: 1.5rem 1.6rem; font-family: "Roboto", "Arial", sans-serif; font-size: 1.4rem; width: fit-content; height: fit-content; max-width: 40vw; max-height: 50vh; word-wrap: break-word; line-height: var(--yt-caption-line-height); } .ytdl-notification.opened { animation: openNotif 0.3s linear forwards; } .ytdl-notification.closed { animation: closeNotif 0.3s linear forwards; } .ytdl-notification h2 { color: var(--yt-brand-youtube-red); } .ytdl-notification > div { display: flex; flex-direction: column; gap: 1rem; } .ytdl-notification > button { transition: all 0.2s ease-in-out; cursor: pointer; border: 2px solid var(--yt-spec-static-grey); border-radius: 8px; background-color: var(--yt-brand-medium-red); padding: 0.7rem 0.8rem; color: #fff; font-weight: 600; } .ytdl-notification button:hover { background-color: var(--yt-spec-red-70); } #ytdl-download-button { background: none; border: none; outline: none; color: var(--yt-spec-text-primary); cursor: pointer; transition: color 0.2s ease-in-out; display: inline-flex; justify-content: center; align-items: center; } #ytdl-download-button:hover { color: var(--yt-brand-youtube-red); } #ytdl-download-button.YTM { transform: scale(1.5); margin: 0 1rem; } #ytdl-download-button > svg { transform: translateX(3.35%); } #ytdl-dl-quality-select { background-color: var(--yt-spec-base-background); color: var(--yt-spec-text-primary); padding: 0.7rem 1rem; border: none; outline: none; border-bottom: 2px solid var(--yt-spec-red-70); border-left: 2px solid var(--yt-spec-red-70); transition: all 0.2s ease-in-out; font-family: "Roboto", "Arial", sans-serif; font-size: 1.4rem; } #ytdl-dl-quality-select:focus { border-bottom-color: var(--yt-brand-youtube-red); border-left-color: var(--yt-brand-youtube-red); } #ytdl-sideMenu > div:has(> #ytdl-dl-quality-select:disabled) { filter: grayscale(0.8); } #ytdl-dl-quality-select:disabled { cursor: not-allowed; } @keyframes openMenu { 0% { left: -100vw; } 100% { left: 0; } } @keyframes closeMenu { 0% { left: 0; } 100% { left: -100vw; } } @keyframes openNotif { 0% { opacity: 0; } 100% { opacity: 1; } } @keyframes closeNotif { 0% { opacity: 1; } 100% { opacity: 0; } } `); logger('info', 'Custom styles added'); hookPlayerEvent(updateVideoData); hookNavigationEvents(appendDownloadButton, devStuff); // functions that require the DOM to exist window.addEventListener('DOMContentLoaded', () => { appendSideMenu(); appendDownloadButton(); manageNotifications(); }); })();