Youtube Video Downloader 2025

Download Youtube videos in various formats. Download multiple videos at once.

  1. // ==UserScript==
  2. // @name Youtube Video Downloader 2025
  3. // @namespace http://tampermonkey.net/
  4. // @author fb
  5. // @version 1.3.1
  6. // @description Download Youtube videos in various formats. Download multiple videos at once.
  7. // @match https://www.youtube.com/*
  8. // @grant GM_xmlhttpRequest
  9. // @grant GM_setValue
  10. // @grant GM_getValue
  11. // @connect p.oceansaver.in
  12. // @license GPL-3.0-or-later
  13. // @run-at document-end
  14. // ==/UserScript==
  15.  
  16. (function() {
  17. 'use strict';
  18.  
  19. const STORAGE_FORMAT = 'selectedFormat';
  20. const STORAGE_DOWNLOADS = 'ytDownloads';
  21. const UI_WRAPPER_ID = 'yt-downloader-wrapper';
  22. const FORMAT_BUTTON_ID = 'yt-downloader-format-button';
  23. const FORMAT_POPUP_ID = 'yt-downloader-format-popup';
  24. const COMBINED_BUTTON_ID = 'yt-downloader-combined-button';
  25. const DOWNLOAD_ACTION_ID = 'yt-downloader-action-part';
  26. const DROPDOWN_ACTION_ID = 'yt-downloader-dropdown-part';
  27. const DOWNLOAD_POPUP_ID = 'download-dropdown-popup';
  28. const POLL_INTERVALS = {};
  29.  
  30. // Format definitions
  31. const FORMAT_GROUPS = [
  32. { label: 'Audio', options: [['mp3','MP3'],['m4a','M4A'],['webm','WEBM'],['aac','AAC'],['flac','FLAC'],['opus','OPUS'],['ogg','OGG'],['wav','WAV']] },
  33. { label: 'Video', options: [['360','MP4 (360p)'],['480','MP4 (480p)'],['720','MP4 (720p)'],['1080','MP4 (1080p)'],['1440','MP4 (1440p)'],['4k','WEBM (4K)']] }
  34. ];
  35. const DEFAULT_FORMAT = '1080';
  36.  
  37. function getFormatText(value) {
  38. for (const group of FORMAT_GROUPS) {
  39. for (const [val, text] of group.options) {
  40. if (val === value) return text;
  41. }
  42. }
  43. return 'Select Format';
  44. }
  45.  
  46. function isDarkTheme() {
  47. return document.documentElement.hasAttribute('dark');
  48. }
  49.  
  50. function isYouTubeLiveStream() {
  51. const ypr = window.ytInitialPlayerResponse || {};
  52. return !!(
  53. ypr.videoDetails?.isLiveContent === true ||
  54. ypr.microformat?.playerMicroformatRenderer?.liveBroadcastDetails ||
  55. document.querySelector('meta[itemprop="isLiveBroadcast"][content="True"]') ||
  56. document.querySelector('.ytp-live')
  57. );
  58. }
  59.  
  60. function checkPageAndInjectUI() {
  61. const existing = document.getElementById(UI_WRAPPER_ID);
  62. const container = document.querySelector('#end');
  63.  
  64. if (container && !existing) injectUI(container);
  65. else if (!container && existing) removeUI();
  66.  
  67. updateUIState();
  68. }
  69.  
  70. document.addEventListener('yt-navigate-finish', checkPageAndInjectUI);
  71. window.addEventListener('load', checkPageAndInjectUI);
  72.  
  73. function updateUIState() {
  74. const wrapper = document.getElementById(UI_WRAPPER_ID);
  75. if (!wrapper) return;
  76. const isWatchOrShorts = window.location.pathname === '/watch' || window.location.pathname.startsWith('/shorts/');
  77. const disabled = !isWatchOrShorts || isYouTubeLiveStream();
  78.  
  79. const formatButton = wrapper.querySelector(`#${FORMAT_BUTTON_ID}`);
  80. const downloadActionPart = wrapper.querySelector(`#${DOWNLOAD_ACTION_ID}`);
  81.  
  82. if(formatButton) {
  83. formatButton.disabled = disabled;
  84. formatButton.style.opacity = disabled ? 0.6 : 1;
  85. formatButton.style.cursor = disabled ? 'not-allowed' : 'var(--btn-cursor)';
  86. }
  87. if(downloadActionPart) {
  88. downloadActionPart.classList.toggle('disabled', disabled);
  89. downloadActionPart.style.pointerEvents = disabled ? 'none' : 'auto';
  90. }
  91. }
  92.  
  93. function injectUI(container) {
  94. if (document.getElementById(UI_WRAPPER_ID)) return;
  95. const wrapper = document.createElement('div');
  96. wrapper.id = UI_WRAPPER_ID;
  97. wrapper.style.display = 'flex';
  98. wrapper.style.alignItems = 'center';
  99. wrapper.style.marginRight = '10px';
  100. wrapper.style.position = 'relative';
  101.  
  102. // --- Format Button ---
  103. const formatButton = document.createElement('button');
  104. formatButton.id = FORMAT_BUTTON_ID;
  105. formatButton.style.marginRight = '8px';
  106. const savedFormat = GM_getValue(STORAGE_FORMAT, DEFAULT_FORMAT);
  107. formatButton.dataset.value = savedFormat;
  108. formatButton.textContent = getFormatText(savedFormat);
  109. formatButton.addEventListener('click', toggleFormatPopup);
  110. wrapper.appendChild(formatButton);
  111.  
  112. // --- Combined Download/Dropdown Button ---
  113. const combinedBtn = document.createElement('div');
  114. combinedBtn.id = COMBINED_BUTTON_ID;
  115. combinedBtn.style.display = 'inline-flex';
  116. combinedBtn.style.alignItems = 'stretch';
  117. combinedBtn.style.height = '36px';
  118. combinedBtn.style.borderRadius = 'var(--btn-radius)';
  119. combinedBtn.style.backgroundColor = 'var(--btn-bg)';
  120. combinedBtn.style.cursor = 'default';
  121. combinedBtn.style.position = 'relative';
  122.  
  123. const downloadPart = document.createElement('div');
  124. downloadPart.id = DOWNLOAD_ACTION_ID;
  125. downloadPart.title = 'Download Video/Audio';
  126. downloadPart.style.display = 'inline-flex';
  127. downloadPart.style.alignItems = 'center';
  128. downloadPart.style.padding = '0 12px 0 8px';
  129. downloadPart.style.cursor = 'var(--btn-cursor)';
  130. downloadPart.style.transition = 'background-color .2s ease';
  131. downloadPart.style.borderRadius = 'var(--btn-radius) 0 0 var(--btn-radius)';
  132.  
  133. // Create SVG element programmatically
  134. const downloadSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
  135. downloadSvg.setAttribute('viewBox', '0 0 24 24');
  136. downloadSvg.setAttribute('width', '24');
  137. downloadSvg.setAttribute('height', '24');
  138. downloadSvg.style.marginRight = '6px';
  139. downloadSvg.style.fill = 'none';
  140. downloadSvg.style.stroke = 'currentColor';
  141. downloadSvg.style.strokeWidth = '1.5';
  142. downloadSvg.style.strokeLinecap = 'round';
  143. downloadSvg.style.strokeLinejoin = 'round';
  144.  
  145. const path1 = document.createElementNS('http://www.w3.org/2000/svg', 'path');
  146. path1.setAttribute('d', 'M12 4v12');
  147. const path2 = document.createElementNS('http://www.w3.org/2000/svg', 'path');
  148. path2.setAttribute('d', 'M8 12l4 4 4-4');
  149. const path3 = document.createElementNS('http://www.w3.org/2000/svg', 'path');
  150. path3.setAttribute('d', 'M4 18h16');
  151.  
  152. downloadSvg.appendChild(path1);
  153. downloadSvg.appendChild(path2);
  154. downloadSvg.appendChild(path3);
  155.  
  156. // Create span element programmatically
  157. const downloadSpan = document.createElement('span');
  158. downloadSpan.textContent = 'Download';
  159.  
  160. // Append elements
  161. downloadPart.appendChild(downloadSvg);
  162. downloadPart.appendChild(downloadSpan);
  163.  
  164. downloadPart.addEventListener('click', startDownload);
  165. downloadPart.addEventListener('mouseenter', () => { if (!downloadPart.classList.contains('disabled')) downloadPart.style.backgroundColor = 'var(--btn-hover-bg)'; });
  166. downloadPart.addEventListener('mouseleave', () => { downloadPart.style.backgroundColor = 'transparent'; });
  167.  
  168.  
  169. const separator = document.createElement('div');
  170. separator.style.width = '1px';
  171. separator.style.backgroundColor = 'var(--separator-color)';
  172. separator.style.height = '20px';
  173. separator.style.alignSelf = 'center';
  174.  
  175. const dropdownPart = document.createElement('div');
  176. dropdownPart.id = DROPDOWN_ACTION_ID;
  177. dropdownPart.dataset.count = '0';
  178. dropdownPart.title = 'Show active downloads';
  179. dropdownPart.style.display = 'inline-flex';
  180. dropdownPart.style.alignItems = 'center';
  181. dropdownPart.style.justifyContent = 'center';
  182. dropdownPart.style.padding = '0 10px';
  183. dropdownPart.style.cursor = 'var(--btn-cursor)';
  184. dropdownPart.style.position = 'relative';
  185. dropdownPart.style.transition = 'background-color .2s ease';
  186. dropdownPart.style.borderRadius = '0 var(--btn-radius) var(--btn-radius) 0';
  187. const darkTheme = isDarkTheme();
  188.  
  189. // Create SVG element programmatically
  190. const dropdownSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
  191. dropdownSvg.setAttribute('width', '10');
  192. dropdownSvg.setAttribute('height', '7');
  193. dropdownSvg.setAttribute('viewBox', '0 0 10 7');
  194. dropdownSvg.style.fill = darkTheme ? '#fff' : '#000';
  195.  
  196. const dropdownPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
  197. dropdownPath.setAttribute('d', 'M0 0l5 7 5-7z');
  198. dropdownSvg.appendChild(dropdownPath);
  199.  
  200. // Append SVG
  201. dropdownPart.appendChild(dropdownSvg);
  202.  
  203. dropdownPart.addEventListener('click', toggleDownloadPopup);
  204. dropdownPart.addEventListener('mouseenter', () => { dropdownPart.style.backgroundColor = 'var(--btn-hover-bg)'; });
  205. dropdownPart.addEventListener('mouseleave', () => { dropdownPart.style.backgroundColor = 'transparent'; });
  206.  
  207. combinedBtn.appendChild(downloadPart);
  208. combinedBtn.appendChild(separator);
  209. combinedBtn.appendChild(dropdownPart);
  210. wrapper.appendChild(combinedBtn);
  211.  
  212. container.insertAdjacentElement('afterbegin', wrapper);
  213.  
  214. const style = document.createElement('style');
  215.  
  216. style.textContent = `
  217. :root {
  218. --btn-bg: ${darkTheme ? "#272727" : "#f2f2f2"};
  219. --btn-hover-bg: ${darkTheme ? "#3f3f3f" : "#e5e5e5"};
  220. --btn-color: ${darkTheme ? "#fff" : "#000"};
  221. --btn-radius: 18px;
  222. --btn-padding: 0 12px;
  223. --btn-font: 500 14px/36px "Roboto", "Arial", sans-serif;
  224. --btn-cursor: pointer;
  225. --progress-bg: ${darkTheme ? "#3f3f3f" : "#e5e5e5"};
  226. --progress-fill-color: #2196F3;
  227. --progress-text-color: ${darkTheme ? "#fff" : "#000"};
  228. --popup-bg: ${darkTheme ? "#212121" : "#fff"};
  229. --popup-border: ${darkTheme ? "#444" : "#ccc"};
  230. --popup-text: ${darkTheme ? "#fff" : "#030303"};
  231. --badge-bg: #cc0000;
  232. --badge-text: #fff;
  233. --separator-color: ${darkTheme ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.1)'};
  234. --popup-radius: 6px;
  235. --popup-shadow: 0 4px 12px rgba(0,0,0,0.15);
  236. }
  237.  
  238. /* Format Button Styling */
  239. #${FORMAT_BUTTON_ID} {
  240. display: inline-flex;
  241. align-items: center;
  242. justify-content: center;
  243. color: var(--btn-color);
  244. background-color: var(--btn-bg);
  245. border: none;
  246. border-radius: var(--btn-radius);
  247. padding: 0 12px;
  248. padding-right: 30px;
  249. white-space: nowrap;
  250. text-transform: none;
  251. font: var(--btn-font);
  252. cursor: var(--btn-cursor);
  253. transition: background-color .2s ease;
  254. height: 36px;
  255. width: 130px;
  256. box-sizing: border-box;
  257. position: relative;
  258. box-shadow: none;
  259. }
  260. #${FORMAT_BUTTON_ID}:disabled {
  261. cursor: not-allowed;
  262. opacity: 0.6;
  263. }
  264. #${FORMAT_BUTTON_ID}:hover:not(:disabled) {
  265. background-color: var(--btn-hover-bg);
  266. }
  267. /* Dropdown Arrow for Format Button */
  268. #${FORMAT_BUTTON_ID}::after {
  269. content: '';
  270. position: absolute;
  271. right: 12px; /* Position arrow within the padding */
  272. top: 50%;
  273. transform: translateY(-50%);
  274. width: 0;
  275. height: 0;
  276. border-left: 5px solid transparent;
  277. border-right: 5px solid transparent;
  278. border-top: 7px solid var(--btn-color);
  279. }
  280.  
  281. /* Combined Button Styling */
  282. #${COMBINED_BUTTON_ID} {
  283. color: var(--btn-color);
  284. font: var(--btn-font);
  285. line-height: 36px;
  286. }
  287.  
  288. #${DOWNLOAD_ACTION_ID}, #${DROPDOWN_ACTION_ID} {
  289. background-color: transparent;
  290. }
  291.  
  292. #${DOWNLOAD_ACTION_ID} {
  293. color: inherit;
  294. }
  295. #${DOWNLOAD_ACTION_ID} svg {
  296. stroke: currentColor;
  297. }
  298. #${DOWNLOAD_ACTION_ID}.disabled {
  299. opacity: 0.6;
  300. cursor: not-allowed;
  301. }
  302. #${DOWNLOAD_ACTION_ID}.disabled:hover {
  303. background-color: transparent !important;
  304. }
  305.  
  306. #${DROPDOWN_ACTION_ID} {
  307. color: inherit;
  308. }
  309. #${DROPDOWN_ACTION_ID} svg {
  310. fill: currentColor;
  311. }
  312.  
  313. #${DROPDOWN_ACTION_ID}::after {
  314. content: attr(data-count);
  315. position: absolute;
  316. top: 2px;
  317. right: -8px;
  318. background-color: var(--badge-bg);
  319. color: var(--badge-text);
  320. border-radius: 50%;
  321. min-width: 16px;
  322. height: 16px;
  323. padding: 0 3px;
  324. font-size: 10px;
  325. line-height: 16px;
  326. text-align: center;
  327. font-weight: bold;
  328. display: none;
  329. font-family: "Roboto", "Arial", sans-serif;
  330. box-sizing: border-box;
  331. z-index: 2;
  332. }
  333. #${DROPDOWN_ACTION_ID}[data-count]:not([data-count="0"])::after {
  334. display: inline-block;
  335. }
  336.  
  337. /* General Popup Styling */
  338. #${FORMAT_POPUP_ID}, #${DOWNLOAD_POPUP_ID} {
  339. position: absolute;
  340. background: var(--popup-bg);
  341. color: var(--popup-text);
  342. border: 1px solid var(--popup-border);
  343. border-radius: var(--popup-radius);
  344. box-shadow: var(--popup-shadow);
  345. padding: 10px;
  346. z-index: 10000;
  347. max-height: 350px;
  348. overflow-y: auto;
  349. }
  350.  
  351. /* Format Popup Specifics */
  352. #${FORMAT_POPUP_ID} {
  353. width: 200px;
  354. }
  355. .format-group-label {
  356. font-weight: bold;
  357. font-size: 12px;
  358. color: ${darkTheme ? '#aaa' : '#555'};
  359. margin-top: 8px;
  360. margin-bottom: 4px;
  361. padding-left: 5px;
  362. text-transform: uppercase;
  363. }
  364. .format-group-label:first-child {
  365. margin-top: 0;
  366. }
  367. .format-item {
  368. display: block;
  369. width: 100%;
  370. padding: 6px 10px;
  371. font-size: 14px;
  372. cursor: pointer;
  373. border-radius: 4px;
  374. box-sizing: border-box;
  375. text-align: left;
  376. background: none;
  377. border: none;
  378. color: inherit;
  379. }
  380. .format-item:hover {
  381. background-color: var(--btn-hover-bg);
  382. }
  383. .format-item.selected {
  384. font-weight: bold;
  385. background-color: rgba(0, 100, 255, 0.1);
  386. }
  387.  
  388.  
  389. /* Download Popup Specifics */
  390. #${DOWNLOAD_POPUP_ID} {
  391. width: 280px;
  392. }
  393. .download-item {
  394. margin-bottom: 12px;
  395. padding-bottom: 8px;
  396. border-bottom: 1px solid var(--popup-border);
  397. }
  398. .download-item:last-child {
  399. margin-bottom: 0;
  400. border-bottom: none;
  401. }
  402. .progress-bar {
  403. width: 100%;
  404. height: 18px;
  405. background-color: var(--progress-bg);
  406. border-radius: 9px;
  407. overflow: hidden;
  408. position: relative;
  409. margin-top: 4px;
  410. }
  411. .progress-fill {
  412. height: 100%;
  413. width: 0%;
  414. background-color: var(--progress-fill-color);
  415. transition: width 0.3s ease-in-out;
  416. display: flex;
  417. align-items: center;
  418. justify-content: center;
  419. }
  420. .progress-text {
  421. position: absolute; top: 0; left: 0; right: 0; bottom: 0;
  422. display: flex;
  423. align-items: center;
  424. justify-content: center;
  425. color: var(--progress-text-color);
  426. font: var(--btn-font);
  427. font-size: 11px;
  428. line-height: 18px;
  429. white-space: nowrap;
  430. z-index: 1;
  431. }
  432. .download-item-title {
  433. font-size: 13px;
  434. font-weight: 500;
  435. margin-bottom: 2px;
  436. white-space: nowrap;
  437. overflow: hidden;
  438. text-overflow: ellipsis;
  439. display: block;
  440. }
  441. .download-item-format {
  442. font-size: 11px;
  443. color: ${darkTheme ? '#aaa' : '#555'};
  444. display: block;
  445. margin-bottom: 4px;
  446. }
  447. .no-downloads-message {
  448. font-size: 15px;
  449. color: ${darkTheme ? '#aaa' : '#555'};
  450. text-align: center;
  451. padding: 10px 0;
  452. }
  453. `;
  454.  
  455. document.head.appendChild(style);
  456.  
  457. resumeDownloads();
  458. updateDownloadCountBadge();
  459. updateUIState();
  460. }
  461.  
  462. function removeUI() {
  463. const w = document.getElementById(UI_WRAPPER_ID);
  464. if (w) w.remove();
  465. // Remove both popups if they exist
  466. const formatPopup = document.getElementById(FORMAT_POPUP_ID);
  467. if (formatPopup) formatPopup.remove();
  468. const downloadPopup = document.getElementById(DOWNLOAD_POPUP_ID);
  469. if (downloadPopup) downloadPopup.remove();
  470. }
  471.  
  472. function startDownload() {
  473. const downloadActionPart = document.getElementById(DOWNLOAD_ACTION_ID);
  474. if (downloadActionPart && downloadActionPart.classList.contains('disabled')) return;
  475.  
  476. const formatButton = document.getElementById(FORMAT_BUTTON_ID);
  477. const fmt = formatButton.dataset.value;
  478. const formatText = formatButton.textContent;
  479. const videoUrl = encodeURIComponent(location.href);
  480. const initUrl = `https://p.oceansaver.in/ajax/download.php?format=${fmt}&url=${videoUrl}`;
  481. const id = Date.now().toString();
  482. const title = document.querySelector('h1.ytd-watch-metadata #video-title, h1.title.ytd-video-primary-info-renderer')?.textContent.trim() || 'YouTube Video';
  483.  
  484. GM_xmlhttpRequest({
  485. method: 'GET', url: initUrl, responseType: 'json',
  486. onload(res) {
  487. const data = res.response;
  488. if (!data?.success) return alert('Failed to initialize');
  489. const downloads = GM_getValue(STORAGE_DOWNLOADS, []);
  490. downloads.push({ id, title, format: formatText, progress_url: data.progress_url, progress: 0, status: 'in_progress' });
  491. GM_setValue(STORAGE_DOWNLOADS, downloads);
  492. renderDownloadPopup(); // Update download popup if open
  493. updateDownloadCountBadge();
  494. pollProgress(id);
  495. },
  496. onerror() { alert('Network error'); }
  497. });
  498. }
  499.  
  500. function pollProgress(id) {
  501. const downloads = GM_getValue(STORAGE_DOWNLOADS, []);
  502. const dl = downloads.find(d=>d.id===id);
  503. if (!dl || dl.status !== 'in_progress') return;
  504.  
  505. if (POLL_INTERVALS[id]) clearInterval(POLL_INTERVALS[id]);
  506.  
  507. const interval = setInterval(()=>{
  508. const currentDownloads = GM_getValue(STORAGE_DOWNLOADS, []);
  509. const currentDl = currentDownloads.find(d=>d.id===id);
  510. if (!currentDl || currentDl.status !== 'in_progress') {
  511. console.log(`Stopping poll for ${id}, status changed.`);
  512. clearInterval(interval);
  513. delete POLL_INTERVALS[id];
  514. renderDownloadPopup();
  515. updateDownloadCountBadge();
  516. return;
  517. }
  518.  
  519. GM_xmlhttpRequest({ method:'GET', url: dl.progress_url, responseType:'json', onload(res){
  520. const p = res.response;
  521. const all = GM_getValue(STORAGE_DOWNLOADS, []);
  522. const obj = all.find(x=>x.id===id);
  523. if (!obj) {
  524. clearInterval(interval); delete POLL_INTERVALS[id];
  525. updateDownloadCountBadge();
  526. return;
  527. }
  528. if (!p) {
  529. console.warn(`Empty poll response for ${id}`);
  530. return;
  531. }
  532. let statusChanged = false;
  533. if (p.success) {
  534. clearInterval(interval); delete POLL_INTERVALS[id];
  535. obj.progress=100; obj.status='completed'; obj.download_url=p.download_url;
  536. statusChanged = true;
  537. GM_setValue(STORAGE_DOWNLOADS, all);
  538. renderDownloadPopup();
  539. triggerFileDownload(p.download_url);
  540. } else if (p.error) {
  541. clearInterval(interval); delete POLL_INTERVALS[id];
  542. obj.status = 'error'; obj.errorMsg = p.error;
  543. statusChanged = true;
  544. GM_setValue(STORAGE_DOWNLOADS, all);
  545. renderDownloadPopup();
  546. console.error(`Download ${id} failed: ${p.error}`);
  547. } else {
  548. const percent = p.progress ? Math.min(Math.round(p.progress/10),100) : obj.progress;
  549. if (obj.progress !== percent || obj.status !== 'in_progress') {
  550. obj.progress = percent;
  551. obj.status = 'in_progress';
  552. GM_setValue(STORAGE_DOWNLOADS, all);
  553. renderDownloadPopup();
  554. }
  555. }
  556. if (statusChanged) {
  557. updateDownloadCountBadge();
  558. }
  559. },
  560. onerror(){
  561. clearInterval(interval); delete POLL_INTERVALS[id];
  562. const all = GM_getValue(STORAGE_DOWNLOADS, []);
  563. const obj = all.find(x=>x.id===id);
  564. if(obj) {
  565. obj.status = 'error'; obj.errorMsg = 'Network error during polling';
  566. GM_setValue(STORAGE_DOWNLOADS, all);
  567. renderDownloadPopup();
  568. updateDownloadCountBadge();
  569. }
  570. console.error(`Network error polling ${id}`);
  571. }
  572. });
  573. }, 2000);
  574. POLL_INTERVALS[id] = interval;
  575. }
  576.  
  577. function triggerFileDownload(url) {
  578. const a = document.createElement('a'); a.href=url; a.download=''; document.body.appendChild(a);
  579. a.click(); a.remove();
  580. }
  581.  
  582. // --- Popup Toggle Functions ---
  583.  
  584. function toggleFormatPopup() {
  585. let popup = document.getElementById(FORMAT_POPUP_ID);
  586. if (popup) { popup.remove(); return; }
  587.  
  588. // Close download popup if open
  589. const downloadPopup = document.getElementById(DOWNLOAD_POPUP_ID);
  590. if (downloadPopup) downloadPopup.remove();
  591.  
  592. const wrapper = document.getElementById(UI_WRAPPER_ID);
  593. const formatButton = document.getElementById(FORMAT_BUTTON_ID);
  594. if (!wrapper || !formatButton) return;
  595.  
  596. popup = document.createElement('div');
  597. popup.id = FORMAT_POPUP_ID;
  598. wrapper.appendChild(popup);
  599. renderFormatPopup();
  600.  
  601. // Position popup below format button
  602. const buttonRect = formatButton.getBoundingClientRect();
  603. const wrapperRect = wrapper.getBoundingClientRect();
  604. popup.style.top = (buttonRect.bottom - wrapperRect.top + 5) + 'px';
  605. popup.style.left = (buttonRect.left - wrapperRect.left) + 'px';
  606.  
  607. setTimeout(() => {
  608. document.addEventListener('click', handleClickOutsideFormatPopup, { capture: true, once: true });
  609. }, 0);
  610. }
  611.  
  612. function toggleDownloadPopup() {
  613. let popup = document.getElementById(DOWNLOAD_POPUP_ID);
  614. if (popup) { popup.remove(); return; }
  615.  
  616. // Close format popup if open
  617. const formatPopup = document.getElementById(FORMAT_POPUP_ID);
  618. if (formatPopup) formatPopup.remove();
  619.  
  620. const wrapper = document.getElementById(UI_WRAPPER_ID);
  621. const combinedButton = document.getElementById(COMBINED_BUTTON_ID);
  622. if (!wrapper || !combinedButton) return;
  623.  
  624. popup = document.createElement('div');
  625. popup.id = DOWNLOAD_POPUP_ID;
  626. wrapper.appendChild(popup);
  627. renderDownloadPopup();
  628.  
  629. // Position popup below combined button, aligned right
  630. const buttonRect = combinedButton.getBoundingClientRect();
  631. const wrapperRect = wrapper.getBoundingClientRect();
  632. popup.style.top = (buttonRect.bottom - wrapperRect.top + 5) + 'px';
  633. popup.style.right = (wrapperRect.right - buttonRect.right) + 'px';
  634.  
  635. setTimeout(() => {
  636. document.addEventListener('click', handleClickOutsideDownloadPopup, { capture: true, once: true });
  637. }, 0);
  638. }
  639.  
  640. // --- Popup Click Outside Handlers ---
  641.  
  642. function handleClickOutsideFormatPopup(event) {
  643. const popup = document.getElementById(FORMAT_POPUP_ID);
  644. const button = document.getElementById(FORMAT_BUTTON_ID);
  645. if (popup && !popup.contains(event.target) && !button.contains(event.target)) {
  646. popup.remove();
  647. } else if (popup) {
  648. // Re-attach listener if click was inside popup or on button
  649. document.addEventListener('click', handleClickOutsideFormatPopup, { capture: true, once: true });
  650. }
  651. }
  652.  
  653. function handleClickOutsideDownloadPopup(event) {
  654. const popup = document.getElementById(DOWNLOAD_POPUP_ID);
  655. const button = document.getElementById(DROPDOWN_ACTION_ID); // Check against the dropdown part
  656. if (popup && !popup.contains(event.target) && !button.contains(event.target)) {
  657. popup.remove();
  658. } else if (popup) {
  659. document.addEventListener('click', handleClickOutsideDownloadPopup, { capture: true, once: true });
  660. }
  661. }
  662.  
  663. // --- Popup Render Functions ---
  664.  
  665. function renderFormatPopup() {
  666. const popup = document.getElementById(FORMAT_POPUP_ID);
  667. if (!popup) return;
  668. popup.textContent = '';
  669. const currentFormat = GM_getValue(STORAGE_FORMAT, DEFAULT_FORMAT);
  670.  
  671. FORMAT_GROUPS.forEach(group => {
  672. const groupLabel = document.createElement('div');
  673. groupLabel.className = 'format-group-label';
  674. groupLabel.textContent = group.label;
  675. popup.appendChild(groupLabel);
  676.  
  677. group.options.forEach(([value, text]) => {
  678. const item = document.createElement('button');
  679. item.className = 'format-item';
  680. item.textContent = text;
  681. item.dataset.value = value;
  682. if (value === currentFormat) {
  683. item.classList.add('selected');
  684. }
  685. item.onclick = () => {
  686. GM_setValue(STORAGE_FORMAT, value);
  687. const formatButton = document.getElementById(FORMAT_BUTTON_ID);
  688. if (formatButton) {
  689. formatButton.textContent = text;
  690. formatButton.dataset.value = value;
  691. }
  692. popup.remove();
  693. };
  694. popup.appendChild(item);
  695. });
  696. });
  697. }
  698.  
  699.  
  700. function renderDownloadPopup() {
  701. const popup = document.getElementById(DOWNLOAD_POPUP_ID);
  702. if (!popup) return;
  703. popup.textContent = '';
  704.  
  705. const downloads = GM_getValue(STORAGE_DOWNLOADS, [])
  706. .filter(d => d.status === 'in_progress' || d.status === 'error')
  707. .sort((a, b) => (b.id - a.id));
  708.  
  709. if (!downloads.length) {
  710. const noDownloadsMsg = document.createElement('div');
  711. noDownloadsMsg.className = 'no-downloads-message';
  712. noDownloadsMsg.textContent = 'No active downloads.';
  713. popup.appendChild(noDownloadsMsg);
  714. return;
  715. }
  716.  
  717. downloads.forEach(d => {
  718. const item = document.createElement('div');
  719. item.className = 'download-item';
  720.  
  721. const titleDiv = document.createElement('div');
  722. titleDiv.className = 'download-item-title';
  723. titleDiv.textContent = d.title || `Download ${d.id}`;
  724. titleDiv.title = d.title || `Download ${d.id}`;
  725. item.appendChild(titleDiv);
  726.  
  727. const formatDiv = document.createElement('div');
  728. formatDiv.className = 'download-item-format';
  729. formatDiv.textContent = d.format || 'Unknown Format';
  730. item.appendChild(formatDiv);
  731.  
  732. if (d.status === 'in_progress') {
  733. const bar = document.createElement('div'); bar.className = 'progress-bar';
  734. const fill = document.createElement('div'); fill.className = 'progress-fill';
  735. fill.style.width = `${d.progress}%`;
  736. bar.appendChild(fill);
  737. const txt = document.createElement('div'); txt.className = 'progress-text';
  738. txt.textContent = `${d.progress}%`;
  739. bar.appendChild(txt);
  740. item.appendChild(bar);
  741. } else if (d.status === 'error') {
  742. const errorDiv = document.createElement('div');
  743. errorDiv.style.color = '#f44336'; errorDiv.style.fontSize = '12px';
  744. errorDiv.textContent = `Error: ${d.errorMsg || 'Unknown error'}`;
  745. item.appendChild(errorDiv);
  746. }
  747. popup.appendChild(item);
  748. });
  749.  
  750. if (downloads.some(d => d.status === 'error')) {
  751. const clearButton = document.createElement('button');
  752. clearButton.textContent = 'Clear Errors';
  753. clearButton.style.marginTop = '10px';
  754. clearButton.style.fontSize = '12px';
  755. clearButton.style.padding = '4px 8px';
  756. clearButton.style.backgroundColor = 'var(--btn-bg)';
  757. clearButton.style.color = 'var(--btn-color)';
  758. clearButton.style.border = 'none';
  759. clearButton.style.borderRadius = '4px';
  760. clearButton.style.cursor = 'pointer';
  761. clearButton.onmouseover = () => clearButton.style.backgroundColor = 'var(--btn-hover-bg)';
  762. clearButton.onmouseout = () => clearButton.style.backgroundColor = 'var(--btn-bg)';
  763.  
  764. clearButton.onclick = () => {
  765. const allDownloads = GM_getValue(STORAGE_DOWNLOADS, []);
  766. const keptDownloads = allDownloads.filter(dl => dl.status !== 'error');
  767. GM_setValue(STORAGE_DOWNLOADS, keptDownloads);
  768. renderDownloadPopup();
  769. updateDownloadCountBadge(); // Badge only shows 'in_progress', errors don't count
  770. };
  771. popup.appendChild(clearButton);
  772. }
  773. }
  774.  
  775. function updateDownloadCountBadge() {
  776. const dropdownPart = document.getElementById(DROPDOWN_ACTION_ID);
  777. if (!dropdownPart) return;
  778.  
  779. const downloads = GM_getValue(STORAGE_DOWNLOADS, []);
  780. const activeCount = downloads.filter(d => d.status === 'in_progress').length;
  781.  
  782. dropdownPart.dataset.count = activeCount.toString();
  783. }
  784.  
  785. function resumeDownloads() {
  786. const downloads = GM_getValue(STORAGE_DOWNLOADS, []).filter(d => d.status === 'in_progress');
  787. console.log(`Resuming ${downloads.length} downloads.`);
  788. downloads.forEach(d => {
  789. if (!POLL_INTERVALS[d.id]) {
  790. pollProgress(d.id);
  791. }
  792. });
  793. }
  794. })();