// ==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 // @downloadURL https://update.greasyfork.icu/scripts/533560/Youtube%20Video%20Downloader%202025.user.js // @updateURL https://update.greasyfork.icu/scripts/533560/Youtube%20Video%20Downloader%202025.meta.js // ==/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); } }); } })();