- // ==UserScript==
- // @name Youtube Video Downloader 2025
- // @namespace http://tampermonkey.net/
- // @author fb
- // @version 1.3.1
- // @description Download Youtube videos in various formats. Download multiple videos at once.
- // @match https://www.youtube.com/*
- // @grant GM_xmlhttpRequest
- // @grant GM_setValue
- // @grant GM_getValue
- // @connect p.oceansaver.in
- // @license GPL-3.0-or-later
- // @run-at document-end
- // ==/UserScript==
-
- (function() {
- 'use strict';
-
- const STORAGE_FORMAT = 'selectedFormat';
- const STORAGE_DOWNLOADS = 'ytDownloads';
- const UI_WRAPPER_ID = 'yt-downloader-wrapper';
- const FORMAT_BUTTON_ID = 'yt-downloader-format-button';
- const FORMAT_POPUP_ID = 'yt-downloader-format-popup';
- const COMBINED_BUTTON_ID = 'yt-downloader-combined-button';
- const DOWNLOAD_ACTION_ID = 'yt-downloader-action-part';
- const DROPDOWN_ACTION_ID = 'yt-downloader-dropdown-part';
- const DOWNLOAD_POPUP_ID = 'download-dropdown-popup';
- const POLL_INTERVALS = {};
-
- // Format definitions
- const FORMAT_GROUPS = [
- { label: 'Audio', options: [['mp3','MP3'],['m4a','M4A'],['webm','WEBM'],['aac','AAC'],['flac','FLAC'],['opus','OPUS'],['ogg','OGG'],['wav','WAV']] },
- { label: 'Video', options: [['360','MP4 (360p)'],['480','MP4 (480p)'],['720','MP4 (720p)'],['1080','MP4 (1080p)'],['1440','MP4 (1440p)'],['4k','WEBM (4K)']] }
- ];
- const DEFAULT_FORMAT = '1080';
-
- function getFormatText(value) {
- for (const group of FORMAT_GROUPS) {
- for (const [val, text] of group.options) {
- if (val === value) return text;
- }
- }
- return 'Select Format';
- }
-
- function isDarkTheme() {
- return document.documentElement.hasAttribute('dark');
- }
-
- function isYouTubeLiveStream() {
- const ypr = window.ytInitialPlayerResponse || {};
- return !!(
- ypr.videoDetails?.isLiveContent === true ||
- ypr.microformat?.playerMicroformatRenderer?.liveBroadcastDetails ||
- document.querySelector('meta[itemprop="isLiveBroadcast"][content="True"]') ||
- document.querySelector('.ytp-live')
- );
- }
-
- function checkPageAndInjectUI() {
- const existing = document.getElementById(UI_WRAPPER_ID);
- const container = document.querySelector('#end');
-
- if (container && !existing) injectUI(container);
- else if (!container && existing) removeUI();
-
- updateUIState();
- }
-
- document.addEventListener('yt-navigate-finish', checkPageAndInjectUI);
- window.addEventListener('load', checkPageAndInjectUI);
-
- function updateUIState() {
- const wrapper = document.getElementById(UI_WRAPPER_ID);
- if (!wrapper) return;
- const isWatchOrShorts = window.location.pathname === '/watch' || window.location.pathname.startsWith('/shorts/');
- const disabled = !isWatchOrShorts || isYouTubeLiveStream();
-
- const formatButton = wrapper.querySelector(`#${FORMAT_BUTTON_ID}`);
- const downloadActionPart = wrapper.querySelector(`#${DOWNLOAD_ACTION_ID}`);
-
- if(formatButton) {
- formatButton.disabled = disabled;
- formatButton.style.opacity = disabled ? 0.6 : 1;
- formatButton.style.cursor = disabled ? 'not-allowed' : 'var(--btn-cursor)';
- }
- if(downloadActionPart) {
- downloadActionPart.classList.toggle('disabled', disabled);
- downloadActionPart.style.pointerEvents = disabled ? 'none' : 'auto';
- }
- }
-
- function injectUI(container) {
- if (document.getElementById(UI_WRAPPER_ID)) return;
- const wrapper = document.createElement('div');
- wrapper.id = UI_WRAPPER_ID;
- wrapper.style.display = 'flex';
- wrapper.style.alignItems = 'center';
- wrapper.style.marginRight = '10px';
- wrapper.style.position = 'relative';
-
- // --- Format Button ---
- const formatButton = document.createElement('button');
- formatButton.id = FORMAT_BUTTON_ID;
- formatButton.style.marginRight = '8px';
- const savedFormat = GM_getValue(STORAGE_FORMAT, DEFAULT_FORMAT);
- formatButton.dataset.value = savedFormat;
- formatButton.textContent = getFormatText(savedFormat);
- formatButton.addEventListener('click', toggleFormatPopup);
- wrapper.appendChild(formatButton);
-
- // --- Combined Download/Dropdown Button ---
- const combinedBtn = document.createElement('div');
- combinedBtn.id = COMBINED_BUTTON_ID;
- combinedBtn.style.display = 'inline-flex';
- combinedBtn.style.alignItems = 'stretch';
- combinedBtn.style.height = '36px';
- combinedBtn.style.borderRadius = 'var(--btn-radius)';
- combinedBtn.style.backgroundColor = 'var(--btn-bg)';
- combinedBtn.style.cursor = 'default';
- combinedBtn.style.position = 'relative';
-
-
- const downloadPart = document.createElement('div');
- downloadPart.id = DOWNLOAD_ACTION_ID;
- downloadPart.title = 'Download Video/Audio';
- downloadPart.style.display = 'inline-flex';
- downloadPart.style.alignItems = 'center';
- downloadPart.style.padding = '0 12px 0 8px';
- downloadPart.style.cursor = 'var(--btn-cursor)';
- downloadPart.style.transition = 'background-color .2s ease';
- downloadPart.style.borderRadius = 'var(--btn-radius) 0 0 var(--btn-radius)';
-
- // Create SVG element programmatically
- const downloadSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
- downloadSvg.setAttribute('viewBox', '0 0 24 24');
- downloadSvg.setAttribute('width', '24');
- downloadSvg.setAttribute('height', '24');
- downloadSvg.style.marginRight = '6px';
- downloadSvg.style.fill = 'none';
- downloadSvg.style.stroke = 'currentColor';
- downloadSvg.style.strokeWidth = '1.5';
- downloadSvg.style.strokeLinecap = 'round';
- downloadSvg.style.strokeLinejoin = 'round';
-
- const path1 = document.createElementNS('http://www.w3.org/2000/svg', 'path');
- path1.setAttribute('d', 'M12 4v12');
- const path2 = document.createElementNS('http://www.w3.org/2000/svg', 'path');
- path2.setAttribute('d', 'M8 12l4 4 4-4');
- const path3 = document.createElementNS('http://www.w3.org/2000/svg', 'path');
- path3.setAttribute('d', 'M4 18h16');
-
- downloadSvg.appendChild(path1);
- downloadSvg.appendChild(path2);
- downloadSvg.appendChild(path3);
-
- // Create span element programmatically
- const downloadSpan = document.createElement('span');
- downloadSpan.textContent = 'Download';
-
- // Append elements
- downloadPart.appendChild(downloadSvg);
- downloadPart.appendChild(downloadSpan);
-
- downloadPart.addEventListener('click', startDownload);
- downloadPart.addEventListener('mouseenter', () => { if (!downloadPart.classList.contains('disabled')) downloadPart.style.backgroundColor = 'var(--btn-hover-bg)'; });
- downloadPart.addEventListener('mouseleave', () => { downloadPart.style.backgroundColor = 'transparent'; });
-
-
- const separator = document.createElement('div');
- separator.style.width = '1px';
- separator.style.backgroundColor = 'var(--separator-color)';
- separator.style.height = '20px';
- separator.style.alignSelf = 'center';
-
- const dropdownPart = document.createElement('div');
- dropdownPart.id = DROPDOWN_ACTION_ID;
- dropdownPart.dataset.count = '0';
- dropdownPart.title = 'Show active downloads';
- dropdownPart.style.display = 'inline-flex';
- dropdownPart.style.alignItems = 'center';
- dropdownPart.style.justifyContent = 'center';
- dropdownPart.style.padding = '0 10px';
- dropdownPart.style.cursor = 'var(--btn-cursor)';
- dropdownPart.style.position = 'relative';
- dropdownPart.style.transition = 'background-color .2s ease';
- dropdownPart.style.borderRadius = '0 var(--btn-radius) var(--btn-radius) 0';
- const darkTheme = isDarkTheme();
-
- // Create SVG element programmatically
- const dropdownSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
- dropdownSvg.setAttribute('width', '10');
- dropdownSvg.setAttribute('height', '7');
- dropdownSvg.setAttribute('viewBox', '0 0 10 7');
- dropdownSvg.style.fill = darkTheme ? '#fff' : '#000';
-
- const dropdownPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
- dropdownPath.setAttribute('d', 'M0 0l5 7 5-7z');
- dropdownSvg.appendChild(dropdownPath);
-
- // Append SVG
- dropdownPart.appendChild(dropdownSvg);
-
- dropdownPart.addEventListener('click', toggleDownloadPopup);
- dropdownPart.addEventListener('mouseenter', () => { dropdownPart.style.backgroundColor = 'var(--btn-hover-bg)'; });
- dropdownPart.addEventListener('mouseleave', () => { dropdownPart.style.backgroundColor = 'transparent'; });
-
- combinedBtn.appendChild(downloadPart);
- combinedBtn.appendChild(separator);
- combinedBtn.appendChild(dropdownPart);
- wrapper.appendChild(combinedBtn);
-
- container.insertAdjacentElement('afterbegin', wrapper);
-
- const style = document.createElement('style');
-
- style.textContent = `
- :root {
- --btn-bg: ${darkTheme ? "#272727" : "#f2f2f2"};
- --btn-hover-bg: ${darkTheme ? "#3f3f3f" : "#e5e5e5"};
- --btn-color: ${darkTheme ? "#fff" : "#000"};
- --btn-radius: 18px;
- --btn-padding: 0 12px;
- --btn-font: 500 14px/36px "Roboto", "Arial", sans-serif;
- --btn-cursor: pointer;
- --progress-bg: ${darkTheme ? "#3f3f3f" : "#e5e5e5"};
- --progress-fill-color: #2196F3;
- --progress-text-color: ${darkTheme ? "#fff" : "#000"};
- --popup-bg: ${darkTheme ? "#212121" : "#fff"};
- --popup-border: ${darkTheme ? "#444" : "#ccc"};
- --popup-text: ${darkTheme ? "#fff" : "#030303"};
- --badge-bg: #cc0000;
- --badge-text: #fff;
- --separator-color: ${darkTheme ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.1)'};
- --popup-radius: 6px;
- --popup-shadow: 0 4px 12px rgba(0,0,0,0.15);
- }
-
- /* Format Button Styling */
- #${FORMAT_BUTTON_ID} {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- color: var(--btn-color);
- background-color: var(--btn-bg);
- border: none;
- border-radius: var(--btn-radius);
- padding: 0 12px;
- padding-right: 30px;
- white-space: nowrap;
- text-transform: none;
- font: var(--btn-font);
- cursor: var(--btn-cursor);
- transition: background-color .2s ease;
- height: 36px;
- width: 130px;
- box-sizing: border-box;
- position: relative;
- box-shadow: none;
- }
- #${FORMAT_BUTTON_ID}:disabled {
- cursor: not-allowed;
- opacity: 0.6;
- }
- #${FORMAT_BUTTON_ID}:hover:not(:disabled) {
- background-color: var(--btn-hover-bg);
- }
- /* Dropdown Arrow for Format Button */
- #${FORMAT_BUTTON_ID}::after {
- content: '';
- position: absolute;
- right: 12px; /* Position arrow within the padding */
- top: 50%;
- transform: translateY(-50%);
- width: 0;
- height: 0;
- border-left: 5px solid transparent;
- border-right: 5px solid transparent;
- border-top: 7px solid var(--btn-color);
- }
-
- /* Combined Button Styling */
- #${COMBINED_BUTTON_ID} {
- color: var(--btn-color);
- font: var(--btn-font);
- line-height: 36px;
- }
-
- #${DOWNLOAD_ACTION_ID}, #${DROPDOWN_ACTION_ID} {
- background-color: transparent;
- }
-
- #${DOWNLOAD_ACTION_ID} {
- color: inherit;
- }
- #${DOWNLOAD_ACTION_ID} svg {
- stroke: currentColor;
- }
- #${DOWNLOAD_ACTION_ID}.disabled {
- opacity: 0.6;
- cursor: not-allowed;
- }
- #${DOWNLOAD_ACTION_ID}.disabled:hover {
- background-color: transparent !important;
- }
-
- #${DROPDOWN_ACTION_ID} {
- color: inherit;
- }
- #${DROPDOWN_ACTION_ID} svg {
- fill: currentColor;
- }
-
- #${DROPDOWN_ACTION_ID}::after {
- content: attr(data-count);
- position: absolute;
- top: 2px;
- right: -8px;
- background-color: var(--badge-bg);
- color: var(--badge-text);
- border-radius: 50%;
- min-width: 16px;
- height: 16px;
- padding: 0 3px;
- font-size: 10px;
- line-height: 16px;
- text-align: center;
- font-weight: bold;
- display: none;
- font-family: "Roboto", "Arial", sans-serif;
- box-sizing: border-box;
- z-index: 2;
- }
- #${DROPDOWN_ACTION_ID}[data-count]:not([data-count="0"])::after {
- display: inline-block;
- }
-
- /* General Popup Styling */
- #${FORMAT_POPUP_ID}, #${DOWNLOAD_POPUP_ID} {
- position: absolute;
- background: var(--popup-bg);
- color: var(--popup-text);
- border: 1px solid var(--popup-border);
- border-radius: var(--popup-radius);
- box-shadow: var(--popup-shadow);
- padding: 10px;
- z-index: 10000;
- max-height: 350px;
- overflow-y: auto;
- }
-
- /* Format Popup Specifics */
- #${FORMAT_POPUP_ID} {
- width: 200px;
- }
- .format-group-label {
- font-weight: bold;
- font-size: 12px;
- color: ${darkTheme ? '#aaa' : '#555'};
- margin-top: 8px;
- margin-bottom: 4px;
- padding-left: 5px;
- text-transform: uppercase;
- }
- .format-group-label:first-child {
- margin-top: 0;
- }
- .format-item {
- display: block;
- width: 100%;
- padding: 6px 10px;
- font-size: 14px;
- cursor: pointer;
- border-radius: 4px;
- box-sizing: border-box;
- text-align: left;
- background: none;
- border: none;
- color: inherit;
- }
- .format-item:hover {
- background-color: var(--btn-hover-bg);
- }
- .format-item.selected {
- font-weight: bold;
- background-color: rgba(0, 100, 255, 0.1);
- }
-
-
- /* Download Popup Specifics */
- #${DOWNLOAD_POPUP_ID} {
- width: 280px;
- }
- .download-item {
- margin-bottom: 12px;
- padding-bottom: 8px;
- border-bottom: 1px solid var(--popup-border);
- }
- .download-item:last-child {
- margin-bottom: 0;
- border-bottom: none;
- }
- .progress-bar {
- width: 100%;
- height: 18px;
- background-color: var(--progress-bg);
- border-radius: 9px;
- overflow: hidden;
- position: relative;
- margin-top: 4px;
- }
- .progress-fill {
- height: 100%;
- width: 0%;
- background-color: var(--progress-fill-color);
- transition: width 0.3s ease-in-out;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .progress-text {
- position: absolute; top: 0; left: 0; right: 0; bottom: 0;
- display: flex;
- align-items: center;
- justify-content: center;
- color: var(--progress-text-color);
- font: var(--btn-font);
- font-size: 11px;
- line-height: 18px;
- white-space: nowrap;
- z-index: 1;
- }
- .download-item-title {
- font-size: 13px;
- font-weight: 500;
- margin-bottom: 2px;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- display: block;
- }
- .download-item-format {
- font-size: 11px;
- color: ${darkTheme ? '#aaa' : '#555'};
- display: block;
- margin-bottom: 4px;
- }
- .no-downloads-message {
- font-size: 15px;
- color: ${darkTheme ? '#aaa' : '#555'};
- text-align: center;
- padding: 10px 0;
- }
- `;
-
- document.head.appendChild(style);
-
- resumeDownloads();
- updateDownloadCountBadge();
- updateUIState();
- }
-
- function removeUI() {
- const w = document.getElementById(UI_WRAPPER_ID);
- if (w) w.remove();
- // Remove both popups if they exist
- const formatPopup = document.getElementById(FORMAT_POPUP_ID);
- if (formatPopup) formatPopup.remove();
- const downloadPopup = document.getElementById(DOWNLOAD_POPUP_ID);
- if (downloadPopup) downloadPopup.remove();
- }
-
- function startDownload() {
- const downloadActionPart = document.getElementById(DOWNLOAD_ACTION_ID);
- if (downloadActionPart && downloadActionPart.classList.contains('disabled')) return;
-
- const formatButton = document.getElementById(FORMAT_BUTTON_ID);
- const fmt = formatButton.dataset.value;
- const formatText = formatButton.textContent;
- const videoUrl = encodeURIComponent(location.href);
- const initUrl = `https://p.oceansaver.in/ajax/download.php?format=${fmt}&url=${videoUrl}`;
- const id = Date.now().toString();
- const title = document.querySelector('h1.ytd-watch-metadata #video-title, h1.title.ytd-video-primary-info-renderer')?.textContent.trim() || 'YouTube Video';
-
- GM_xmlhttpRequest({
- method: 'GET', url: initUrl, responseType: 'json',
- onload(res) {
- const data = res.response;
- if (!data?.success) return alert('Failed to initialize');
- const downloads = GM_getValue(STORAGE_DOWNLOADS, []);
- downloads.push({ id, title, format: formatText, progress_url: data.progress_url, progress: 0, status: 'in_progress' });
- GM_setValue(STORAGE_DOWNLOADS, downloads);
- renderDownloadPopup(); // Update download popup if open
- updateDownloadCountBadge();
- pollProgress(id);
- },
- onerror() { alert('Network error'); }
- });
- }
-
- function pollProgress(id) {
- const downloads = GM_getValue(STORAGE_DOWNLOADS, []);
- const dl = downloads.find(d=>d.id===id);
- if (!dl || dl.status !== 'in_progress') return;
-
- if (POLL_INTERVALS[id]) clearInterval(POLL_INTERVALS[id]);
-
- const interval = setInterval(()=>{
- const currentDownloads = GM_getValue(STORAGE_DOWNLOADS, []);
- const currentDl = currentDownloads.find(d=>d.id===id);
- if (!currentDl || currentDl.status !== 'in_progress') {
- console.log(`Stopping poll for ${id}, status changed.`);
- clearInterval(interval);
- delete POLL_INTERVALS[id];
- renderDownloadPopup();
- updateDownloadCountBadge();
- return;
- }
-
- GM_xmlhttpRequest({ method:'GET', url: dl.progress_url, responseType:'json', onload(res){
- const p = res.response;
- const all = GM_getValue(STORAGE_DOWNLOADS, []);
- const obj = all.find(x=>x.id===id);
- if (!obj) {
- clearInterval(interval); delete POLL_INTERVALS[id];
- updateDownloadCountBadge();
- return;
- }
- if (!p) {
- console.warn(`Empty poll response for ${id}`);
- return;
- }
- let statusChanged = false;
- if (p.success) {
- clearInterval(interval); delete POLL_INTERVALS[id];
- obj.progress=100; obj.status='completed'; obj.download_url=p.download_url;
- statusChanged = true;
- GM_setValue(STORAGE_DOWNLOADS, all);
- renderDownloadPopup();
- triggerFileDownload(p.download_url);
- } else if (p.error) {
- clearInterval(interval); delete POLL_INTERVALS[id];
- obj.status = 'error'; obj.errorMsg = p.error;
- statusChanged = true;
- GM_setValue(STORAGE_DOWNLOADS, all);
- renderDownloadPopup();
- console.error(`Download ${id} failed: ${p.error}`);
- } else {
- const percent = p.progress ? Math.min(Math.round(p.progress/10),100) : obj.progress;
- if (obj.progress !== percent || obj.status !== 'in_progress') {
- obj.progress = percent;
- obj.status = 'in_progress';
- GM_setValue(STORAGE_DOWNLOADS, all);
- renderDownloadPopup();
- }
- }
- if (statusChanged) {
- updateDownloadCountBadge();
- }
- },
- onerror(){
- clearInterval(interval); delete POLL_INTERVALS[id];
- const all = GM_getValue(STORAGE_DOWNLOADS, []);
- const obj = all.find(x=>x.id===id);
- if(obj) {
- obj.status = 'error'; obj.errorMsg = 'Network error during polling';
- GM_setValue(STORAGE_DOWNLOADS, all);
- renderDownloadPopup();
- updateDownloadCountBadge();
- }
- console.error(`Network error polling ${id}`);
- }
- });
- }, 2000);
- POLL_INTERVALS[id] = interval;
- }
-
- function triggerFileDownload(url) {
- const a = document.createElement('a'); a.href=url; a.download=''; document.body.appendChild(a);
- a.click(); a.remove();
- }
-
- // --- Popup Toggle Functions ---
-
- function toggleFormatPopup() {
- let popup = document.getElementById(FORMAT_POPUP_ID);
- if (popup) { popup.remove(); return; }
-
- // Close download popup if open
- const downloadPopup = document.getElementById(DOWNLOAD_POPUP_ID);
- if (downloadPopup) downloadPopup.remove();
-
- const wrapper = document.getElementById(UI_WRAPPER_ID);
- const formatButton = document.getElementById(FORMAT_BUTTON_ID);
- if (!wrapper || !formatButton) return;
-
- popup = document.createElement('div');
- popup.id = FORMAT_POPUP_ID;
- wrapper.appendChild(popup);
- renderFormatPopup();
-
- // Position popup below format button
- const buttonRect = formatButton.getBoundingClientRect();
- const wrapperRect = wrapper.getBoundingClientRect();
- popup.style.top = (buttonRect.bottom - wrapperRect.top + 5) + 'px';
- popup.style.left = (buttonRect.left - wrapperRect.left) + 'px';
-
- setTimeout(() => {
- document.addEventListener('click', handleClickOutsideFormatPopup, { capture: true, once: true });
- }, 0);
- }
-
- function toggleDownloadPopup() {
- let popup = document.getElementById(DOWNLOAD_POPUP_ID);
- if (popup) { popup.remove(); return; }
-
- // Close format popup if open
- const formatPopup = document.getElementById(FORMAT_POPUP_ID);
- if (formatPopup) formatPopup.remove();
-
- const wrapper = document.getElementById(UI_WRAPPER_ID);
- const combinedButton = document.getElementById(COMBINED_BUTTON_ID);
- if (!wrapper || !combinedButton) return;
-
- popup = document.createElement('div');
- popup.id = DOWNLOAD_POPUP_ID;
- wrapper.appendChild(popup);
- renderDownloadPopup();
-
- // Position popup below combined button, aligned right
- const buttonRect = combinedButton.getBoundingClientRect();
- const wrapperRect = wrapper.getBoundingClientRect();
- popup.style.top = (buttonRect.bottom - wrapperRect.top + 5) + 'px';
- popup.style.right = (wrapperRect.right - buttonRect.right) + 'px';
-
- setTimeout(() => {
- document.addEventListener('click', handleClickOutsideDownloadPopup, { capture: true, once: true });
- }, 0);
- }
-
- // --- Popup Click Outside Handlers ---
-
- function handleClickOutsideFormatPopup(event) {
- const popup = document.getElementById(FORMAT_POPUP_ID);
- const button = document.getElementById(FORMAT_BUTTON_ID);
- if (popup && !popup.contains(event.target) && !button.contains(event.target)) {
- popup.remove();
- } else if (popup) {
- // Re-attach listener if click was inside popup or on button
- document.addEventListener('click', handleClickOutsideFormatPopup, { capture: true, once: true });
- }
- }
-
- function handleClickOutsideDownloadPopup(event) {
- const popup = document.getElementById(DOWNLOAD_POPUP_ID);
- const button = document.getElementById(DROPDOWN_ACTION_ID); // Check against the dropdown part
- if (popup && !popup.contains(event.target) && !button.contains(event.target)) {
- popup.remove();
- } else if (popup) {
- document.addEventListener('click', handleClickOutsideDownloadPopup, { capture: true, once: true });
- }
- }
-
- // --- Popup Render Functions ---
-
- function renderFormatPopup() {
- const popup = document.getElementById(FORMAT_POPUP_ID);
- if (!popup) return;
- popup.textContent = '';
- const currentFormat = GM_getValue(STORAGE_FORMAT, DEFAULT_FORMAT);
-
- FORMAT_GROUPS.forEach(group => {
- const groupLabel = document.createElement('div');
- groupLabel.className = 'format-group-label';
- groupLabel.textContent = group.label;
- popup.appendChild(groupLabel);
-
- group.options.forEach(([value, text]) => {
- const item = document.createElement('button');
- item.className = 'format-item';
- item.textContent = text;
- item.dataset.value = value;
- if (value === currentFormat) {
- item.classList.add('selected');
- }
- item.onclick = () => {
- GM_setValue(STORAGE_FORMAT, value);
- const formatButton = document.getElementById(FORMAT_BUTTON_ID);
- if (formatButton) {
- formatButton.textContent = text;
- formatButton.dataset.value = value;
- }
- popup.remove();
- };
- popup.appendChild(item);
- });
- });
- }
-
-
- function renderDownloadPopup() {
- const popup = document.getElementById(DOWNLOAD_POPUP_ID);
- if (!popup) return;
- popup.textContent = '';
-
- const downloads = GM_getValue(STORAGE_DOWNLOADS, [])
- .filter(d => d.status === 'in_progress' || d.status === 'error')
- .sort((a, b) => (b.id - a.id));
-
- if (!downloads.length) {
- const noDownloadsMsg = document.createElement('div');
- noDownloadsMsg.className = 'no-downloads-message';
- noDownloadsMsg.textContent = 'No active downloads.';
- popup.appendChild(noDownloadsMsg);
- return;
- }
-
- downloads.forEach(d => {
- const item = document.createElement('div');
- item.className = 'download-item';
-
- const titleDiv = document.createElement('div');
- titleDiv.className = 'download-item-title';
- titleDiv.textContent = d.title || `Download ${d.id}`;
- titleDiv.title = d.title || `Download ${d.id}`;
- item.appendChild(titleDiv);
-
- const formatDiv = document.createElement('div');
- formatDiv.className = 'download-item-format';
- formatDiv.textContent = d.format || 'Unknown Format';
- item.appendChild(formatDiv);
-
- if (d.status === 'in_progress') {
- const bar = document.createElement('div'); bar.className = 'progress-bar';
- const fill = document.createElement('div'); fill.className = 'progress-fill';
- fill.style.width = `${d.progress}%`;
- bar.appendChild(fill);
- const txt = document.createElement('div'); txt.className = 'progress-text';
- txt.textContent = `${d.progress}%`;
- bar.appendChild(txt);
- item.appendChild(bar);
- } else if (d.status === 'error') {
- const errorDiv = document.createElement('div');
- errorDiv.style.color = '#f44336'; errorDiv.style.fontSize = '12px';
- errorDiv.textContent = `Error: ${d.errorMsg || 'Unknown error'}`;
- item.appendChild(errorDiv);
- }
- popup.appendChild(item);
- });
-
- if (downloads.some(d => d.status === 'error')) {
- const clearButton = document.createElement('button');
- clearButton.textContent = 'Clear Errors';
- clearButton.style.marginTop = '10px';
- clearButton.style.fontSize = '12px';
- clearButton.style.padding = '4px 8px';
- clearButton.style.backgroundColor = 'var(--btn-bg)';
- clearButton.style.color = 'var(--btn-color)';
- clearButton.style.border = 'none';
- clearButton.style.borderRadius = '4px';
- clearButton.style.cursor = 'pointer';
- clearButton.onmouseover = () => clearButton.style.backgroundColor = 'var(--btn-hover-bg)';
- clearButton.onmouseout = () => clearButton.style.backgroundColor = 'var(--btn-bg)';
-
- clearButton.onclick = () => {
- const allDownloads = GM_getValue(STORAGE_DOWNLOADS, []);
- const keptDownloads = allDownloads.filter(dl => dl.status !== 'error');
- GM_setValue(STORAGE_DOWNLOADS, keptDownloads);
- renderDownloadPopup();
- updateDownloadCountBadge(); // Badge only shows 'in_progress', errors don't count
- };
- popup.appendChild(clearButton);
- }
- }
-
- function updateDownloadCountBadge() {
- const dropdownPart = document.getElementById(DROPDOWN_ACTION_ID);
- if (!dropdownPart) return;
-
- const downloads = GM_getValue(STORAGE_DOWNLOADS, []);
- const activeCount = downloads.filter(d => d.status === 'in_progress').length;
-
- dropdownPart.dataset.count = activeCount.toString();
- }
-
- function resumeDownloads() {
- const downloads = GM_getValue(STORAGE_DOWNLOADS, []).filter(d => d.status === 'in_progress');
- console.log(`Resuming ${downloads.length} downloads.`);
- downloads.forEach(d => {
- if (!POLL_INTERVALS[d.id]) {
- pollProgress(d.id);
- }
- });
- }
- })();