// ==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);
}
});
}
})();