Guest User

NDC 0.7

a guest
Apr 17th, 2024
44
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. // ==UserScript==
  2. // @name         Nexus Download Collection
  3. // @namespace    NDC
  4. // @version      0.6.7
  5. // @description  Download every mods of a collection in a single click
  6. // @author       Drigtime
  7. // @match        https://next.nexusmods.com/*/collections*
  8. // @icon         data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
  9. // @grant        GM.xmlHttpRequest
  10. // @grant        GM_xmlhttpRequest
  11. // @grant        GM_setValue
  12. // @grant        GM_getValue
  13. // @grant        GM.setValue
  14. // @grant        GM.getValue
  15. // @connect      nexusmods.com
  16. // @downloadURL  https://update.greasyfork.org/scripts/483337/Nexus%20Download%20Collection.user.js
  17. // @updateURL    https://update.greasyfork.org/scripts/483337/Nexus%20Download%20Collection.meta.js
  18. // ==/UserScript==
  19.  
  20. // MDI : https://pictogrammers.com/library/mdi/
  21. // MDI : https://github.com/MathewSachin/Captura/blob/master/src/Captura.Core/MaterialDesignIcons.cs
  22.  
  23. /** CORSViaGM BEGINING */
  24.  
  25. const CORSViaGM = document.body.appendChild(Object.assign(document.createElement('div'), { id: 'CORSViaGM' }))
  26.  
  27. addEventListener('fetchViaGM', e => GM_fetch(e.detail.forwardingFetch))
  28.  
  29. CORSViaGM.init = function (window) {
  30.     if (!window) throw 'The `window` parameter must be passed in!'
  31.     window.fetchViaGM = fetchViaGM.bind(window)
  32.  
  33.     // Support for service worker
  34.     window.forwardingFetch = new BroadcastChannel('forwardingFetch')
  35.     window.forwardingFetch.onmessage = async e => {
  36.         const req = e.data
  37.         const { url } = req
  38.         const res = await fetchViaGM(url, req)
  39.         const response = await res.blob()
  40.         window.forwardingFetch.postMessage({ type: 'fetchResponse', url, response })
  41.     }
  42.  
  43.     window._CORSViaGM && window._CORSViaGM.inited.done()
  44.  
  45.     const info = '🙉 CORS-via-GM initiated!'
  46.     console.info(info)
  47.     return info
  48. }
  49.  
  50. function GM_fetch(p) {
  51.     GM_xmlhttpRequest({
  52.         ...p.init,
  53.         url: p.url, method: p.init.method || 'GET',
  54.         onload: responseDetails => p.res(new Response(responseDetails.response, responseDetails))
  55.     })
  56. }
  57.  
  58. function fetchViaGM(url, init) {
  59.     let _r
  60.     const p = new Promise(r => _r = r)
  61.     p.res = _r
  62.     p.url = url
  63.     p.init = init || {}
  64.     dispatchEvent(new CustomEvent('fetchViaGM', { detail: { forwardingFetch: p } }))
  65.     return p
  66. }
  67.  
  68. CORSViaGM.init(window);
  69.  
  70. /** CORSViaGM END */
  71.  
  72. class NDCElement {
  73.     constructor(elementName, options = {}) {
  74.         this.element = document.createElement(elementName);
  75.         this.childrens = [];
  76.         if (options.html) {
  77.             this.element.innerHTML = options.html;
  78.         }
  79.         if (options.classes) {
  80.             this.element.className = options.classes;
  81.         }
  82.         if (options.attributes) {
  83.             Object.entries(options.attributes).forEach(([key, value]) => {
  84.                 this.element.setAttribute(key, value);
  85.             });
  86.         }
  87.         if (options.events) {
  88.             Object.entries(options.events).forEach(([key, value]) => {
  89.                 this.element.addEventListener(key, value);
  90.             });
  91.         }
  92.     }
  93.  
  94.     remove() {
  95.         this.element.remove();
  96.     }
  97.  
  98.     hide() {
  99.         this.element.style.display = 'none';
  100.     }
  101.  
  102.     show() {
  103.         this.element.style.display = '';
  104.     }
  105.  
  106.     toggle() {
  107.         this.element.style.display = this.element.style.display === 'none' ? '' : 'none';
  108.     }
  109.  
  110.     appendChild(...elements) {
  111.         elements.forEach(element => {
  112.             this.childrens.push(element);
  113.             this.element.appendChild(element.element);
  114.         });
  115.     }
  116.  
  117.     removeChild(element) {
  118.         const index = this.childrens.indexOf(element);
  119.         if (index > -1) {
  120.             this.childrens.splice(index, 1);
  121.         }
  122.         element.element.remove();
  123.     }
  124. }
  125.  
  126. class NDC extends NDCElement {
  127.     constructor(gameId, collectionId, options = {}) {
  128.         super('div', options);
  129.  
  130.         this.pauseBetweenDownload = 5;
  131.         this.loading = true;
  132.         this.forceStop = false;
  133.  
  134.         this.gameId = gameId;
  135.         this.collectionId = collectionId;
  136.  
  137.         this.mods = {
  138.             mandatory: [],
  139.             optional: []
  140.         };
  141.  
  142.         this.downloadButtonContainer = new NDCDownloadButton(this);
  143.         this.progressBarContainer = new NDCProgressBar(this);
  144.         this.console = new NDCLogConsole(this);
  145.  
  146.         this.init();
  147.     }
  148.  
  149.     async init() {
  150.         this.pauseBetweenDownload = await GM.getValue('pauseBetweenDownload', 5);
  151.  
  152.         const loadingContainer = new NDCElement('button', {
  153.             classes: 'w-full font-montserrat font-semibold text-sm leading-none tracking-wider uppercase flex gap-x-2 justify-center items-center transition-colors relative min-h-9 focus:outline focus:outline-2 focus:outline-accent focus:outline-offset-2 px-2 py-1 cursor-pointer bg-primary-moderate fill-font-primary text-font-primary border-transparent focus:bg-primary-strong hover:bg-primary-subdued rounded',
  154.             html: 'Fetching mods list...'
  155.         });
  156.         this.appendChild(loadingContainer);
  157.  
  158.         const response = await this.fetchMods();
  159.         // const response = await new Promise(resolve => setTimeout(() => resolve({ modFiles: [] }), 1000));
  160.  
  161.         this.mods.mandatory = response.modFiles.filter(mod => !mod.optional);
  162.         this.mods.optional = response.modFiles.filter(mod => mod.optional);
  163.  
  164.         this.loading = false;
  165.         loadingContainer.remove();
  166.  
  167.         this.downloadButtonContainer.render();
  168.         this.downloadButtonContainer.mainBtn.element.addEventListener('click', () => this.downloadMods([...this.mods.mandatory, ...this.mods.optional], "all"));
  169.         this.downloadButtonContainer.menuBtnMandatory.element.addEventListener('click', () => this.downloadMods(this.mods.mandatory, "mandatory"));
  170.         this.downloadButtonContainer.menuBtnOptional.element.addEventListener('click', () => this.downloadMods(this.mods.optional, "optional"));
  171.  
  172.         this.appendChild(this.downloadButtonContainer, this.progressBarContainer, this.console);
  173.     }
  174.  
  175.     async fetchMods() {
  176.         const response = await fetch("https://next.nexusmods.com/api/graphql", {
  177.             "headers": {
  178.                 "content-type": "application/json",
  179.             },
  180.             "referrer": `https://next.nexusmods.com/${this.gameId}/collections/${this.collectionId}?tab=mods`,
  181.             "referrerPolicy": "strict-origin-when-cross-origin",
  182.             "body": JSON.stringify({
  183.                 "query": "query CollectionRevisionMods ($revision: Int, $slug: String!, $viewAdultContent: Boolean) { collectionRevision (revision: $revision, slug: $slug, viewAdultContent: $viewAdultContent) { externalResources { id, name, resourceType, resourceUrl }, modFiles { fileId, optional, file { fileId, name, scanned, size, sizeInBytes, version, mod { adult, author, category, modId, name, pictureUrl, summary, version, game { domainName }, uploader { avatar, memberId, name } } } } } }",
  184.                 "variables": { "slug": this.collectionId, "viewAdultContent": true },
  185.                 "operationName": "CollectionRevisionMods"
  186.             }),
  187.             "method": "POST",
  188.             "mode": "cors",
  189.             "credentials": "include"
  190.         });
  191.  
  192.         const data = await response.json();
  193.  
  194.         data.data.collectionRevision.modFiles = data.data.collectionRevision.modFiles.map(modFile => {
  195.             modFile.file.url = `https://www.nexusmods.com/${this.gameId}/mods/${modFile.file.mod.modId}?tab=files&file_id=${modFile.file.fileId}`;
  196.             return modFile;
  197.         });
  198.  
  199.         return data.data.collectionRevision;
  200.     }
  201.  
  202.     async fetchSlowDownloadLink(mod) {
  203.         let downloadUrl = '';
  204.         const url = mod.file.url + '&nmm=1';
  205.  
  206.         const response = await fetchViaGM(url, {
  207.             "headers": {
  208.                 "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
  209.                 "cache-control": "max-age=0",
  210.             },
  211.             "referrer": url,
  212.             "referrerPolicy": "strict-origin-when-cross-origin",
  213.             "method": "GET",
  214.             "mode": "cors",
  215.             "credentials": "include"
  216.         });
  217.  
  218.         const text = await response.text();
  219.         const html = new DOMParser().parseFromString(text, "text/html");
  220.  
  221.         const slow = html.getElementById("slowDownloadButton");
  222.         if (slow) {
  223.             downloadUrl = slow.getAttribute("data-download-url");
  224.         }
  225.  
  226.         return { downloadUrl, text, html }
  227.     }
  228.  
  229.     async addModToVortex(mod) {
  230.         // const {downloadUrl, text} = await new Promise(resolve => setTimeout(() => resolve({downloadUrl: 'debug', text: 'debug'}), 1000));
  231.         // const {downloadUrl, text} = await new Promise(resolve => setTimeout(() => resolve({downloadUrl: '', text: 'debug'}), 1000));
  232.         const { downloadUrl, text, html } = await this.fetchSlowDownloadLink(mod);
  233.         if (downloadUrl === '') {
  234.             // make link to copy in the clipboard the response
  235.  
  236.             const logRow = this.console.log('ERROR', `Failed to get download link for
  237.             <a href="${mod.file.url}" target="_blank" class="text-primary-moderate">${mod.file.name}</a>
  238.             <button class="text-primary-moderate" title="Copy response to clipboard"></button>`);
  239.             const svg = new NDCElement('svg', {
  240.                 classes: 'w-4 h-4 fill-current',
  241.                 attributes: {
  242.                     viewBox: '0 0 24 24',
  243.                     xmlns: 'http://www.w3.org/2000/svg',
  244.                     role: 'presentation',
  245.                     style: 'width: 1rem; height: 1rem;'
  246.                 },
  247.                 html: '<path d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z" style="fill: currentcolor;"></path>'
  248.             });
  249.             // add svg to the button
  250.             const copyButton = logRow.element.querySelector('button');
  251.             copyButton.innerHTML = svg.element.outerHTML;
  252.             copyButton.addEventListener('click', () => {
  253.                 navigator.clipboard.writeText(text);
  254.                 alert('Response copied to clipboard');
  255.             });
  256.  
  257.             // check if find .replaced-login-link in the html it is because the user is not connect on nexusmods
  258.             if (html.querySelector('.replaced-login-link')) {
  259.                 this.console.log('ERROR', 'You are not connected on NexusMods. <a href="https://users.nexusmods.com/auth/continue?client_id=nexus&redirect_uri=https://www.nexusmods.com/oauth/callback&response_type=code&referrer=https%3A%2F%2Fwww.nexusmods.com%2F" target="_blank" class="text-primary-moderate">Login</a> and try again.');
  260.                 this.forceStop = true;
  261.             }
  262.  
  263.             return false;
  264.         }
  265.  
  266.         document.location.href = downloadUrl;
  267.         return true;
  268.     }
  269.  
  270.     // function to avoid repeating the same code this.downloadButtonContainer.hide(); this.progressBarContainer.setModsCount(this.downloadButtonContainer.modsCount); this.progressBarContainer.show();
  271.  
  272.     async downloadMods(mods, type = "all") {
  273.         const history = await GM.getValue('history', {}); // {"gameId": {"collectionId": {"type": []}}}
  274.  
  275.         this.progressBarContainer.setModsCount(mods.length);
  276.         this.progressBarContainer.setProgress(0);
  277.         this.progressBarContainer.setStatus(NDCProgressBar.STATUS_DOWNLOADING);
  278.  
  279.         // get history for this collection (index is the collectionId)
  280.         let historyForThisCollection = [];
  281.         if (history[this.gameId] && history[this.gameId][this.collectionId] && history[this.gameId][this.collectionId][type]) {
  282.             historyForThisCollection = [...new Set(history[this.gameId][this.collectionId][type])];
  283.         } else {
  284.             history[this.gameId] = history[this.gameId] || {};
  285.             history[this.gameId][this.collectionId] = history[this.gameId][this.collectionId] || {};
  286.         }
  287.  
  288.         if (historyForThisCollection && historyForThisCollection.length > 0) {
  289.             const confirm = await new Promise(resolve => {
  290.                 const confirm = window.confirm(`You already downloaded ${historyForThisCollection.length} out of ${mods.length} mods from this collection.\nDo you want to resume the download?\nCancel will clear the history and download all mods again.`);
  291.                 resolve(confirm);
  292.             });
  293.             // if the user doesn't want to resume the download, clear the history
  294.             if (!confirm) {
  295.                 historyForThisCollection = [];
  296.                 history[this.gameId][this.collectionId][type] = historyForThisCollection;
  297.                 await GM.setValue('history', history);
  298.             } else {
  299.                 // update mods to download
  300.                 mods = mods.filter(mod => !historyForThisCollection.includes(mod.file.mod.modId));
  301.                 this.progressBarContainer.setProgress(historyForThisCollection.length);
  302.             }
  303.         }
  304.  
  305.         this.downloadButtonContainer.hide();
  306.         this.progressBarContainer.show();
  307.  
  308.         this.console.log('INFO', 'Download started.');
  309.  
  310.         for (const [_, mod] of mods.entries()) {
  311.             if (this.progressBarContainer.status == NDCProgressBar.STATUS_PAUSED) {
  312.                 this.console.log('INFO', `Download paused.`);
  313.                 while (this.progressBarContainer.status == NDCProgressBar.STATUS_PAUSED) {
  314.                     await new Promise(resolve => setTimeout(resolve, 100));
  315.                 }
  316.             }
  317.  
  318.             if (this.progressBarContainer.status == NDCProgressBar.STATUS_STOPPED) {
  319.                 this.console.log('INFO', 'Download stopped.');
  320.                 break;
  321.             }
  322.  
  323.             const status = await this.addModToVortex(mod);
  324.  
  325.             if (this.forceStop) {
  326.                 this.console.log('INFO', 'Download stopped.');
  327.                 break;
  328.             }
  329.  
  330.             if (!status) {
  331.                 continue;
  332.             }
  333.  
  334.             historyForThisCollection.push(mod.file.mod.modId); // update historyForThisCollection
  335.             history[this.gameId][this.collectionId][type] = [...new Set(historyForThisCollection)]; // remove duplicate and update history
  336.             await GM.setValue('history', history);
  337.  
  338.             this.console.log('INFO', `Sending download link to Vortex <a href="${mod.file.url}" target="_blank" class="text-primary-moderate">${mod.file.name}</a>`);
  339.             this.progressBarContainer.incrementProgress();
  340.  
  341.             // based on download 1.5mb/s wait until the download is supposed to be finished + 5 seconds for the download to start on vortex
  342.             const downloadTime = Math.round(mod.file.sizeInBytes / 1500000) + this.pauseBetweenDownload;
  343.             const downloadEstimatifTimeLog = this.console.log('INFO', `Waiting approximately ${downloadTime} seconds for the download to finish on Vortex before starting the next one.`);
  344.             const downloadProgressLog = this.console.log('INFO', `Downloading... ${downloadTime} seconds left (~0%)`);
  345.             const downloadProgressLogCreatedAt = Date.now();
  346.             await new Promise(resolve => {
  347.                 const downloadProgressLogInterval = setInterval(() => {
  348.                     // if Status is paused or stopped clear the interval
  349.                     if (this.progressBarContainer.status == NDCProgressBar.STATUS_PAUSED || this.progressBarContainer.status == NDCProgressBar.STATUS_STOPPED) {
  350.                         clearInterval(downloadProgressLogInterval);
  351.                         downloadEstimatifTimeLog.remove();
  352.                         downloadProgressLog.remove();
  353.                         resolve();
  354.                         return;
  355.                     }
  356.  
  357.                     const timeLeft = downloadTime - Math.round((Date.now() - downloadProgressLogCreatedAt) / 1000);
  358.                     // 0 to 100% based on the time left
  359.                     const approximativePercent = Math.round((downloadTime - timeLeft) / downloadTime * 100);
  360.                     downloadProgressLog.element.innerHTML = `[${new Date().toLocaleTimeString()}][INFO] Downloading... ${timeLeft} seconds left (~${approximativePercent}%)`;
  361.  
  362.                     if (timeLeft <= 0) {
  363.                         clearInterval(downloadProgressLogInterval);
  364.                         downloadEstimatifTimeLog.remove();
  365.                         downloadProgressLog.remove();
  366.                         resolve();
  367.                     }
  368.                 }, 1000);
  369.             });
  370.  
  371.             // if all mods are downloaded clear the history
  372.             if (this.progressBarContainer.progress === this.progressBarContainer.modsCount) {
  373.                 history[this.gameId][this.collectionId][type] = [];
  374.                 await GM.setValue('history', history);
  375.             }
  376.         }
  377.  
  378.         this.console.log('INFO', 'Download finished.');
  379.  
  380.  
  381.         this.progressBarContainer.setStatus(NDCProgressBar.STATUS_FINISHED);
  382.         this.progressBarContainer.hide();
  383.         this.downloadButtonContainer.show();
  384.  
  385.         if (this.forceStop) {
  386.             this.forceStop = false;
  387.             return;
  388.         }
  389.     }
  390.  
  391. }
  392.  
  393. class NDCDownloadButton extends NDCElement {
  394.     constructor(ndc, options = {}) {
  395.         super('div', { classes: 'flex w-100' });
  396.  
  397.         this.ndc = ndc;
  398.  
  399.         this.mainBtn = new NDCElement('button', {
  400.             classes: 'w-full font-montserrat font-semibold text-sm leading-none tracking-wider uppercase flex gap-x-2 justify-center items-center transition-colors relative min-h-9 focus:outline focus:outline-2 focus:outline-accent focus:outline-offset-2 px-2 py-1 cursor-pointer bg-primary-moderate fill-font-primary text-font-primary border-transparent focus:bg-primary-strong hover:bg-primary-subdued justify-between rounded-l',
  401.             html: 'Add all mods to vortex'
  402.         });
  403.  
  404.         this.mainModsCount = new NDCElement('span', {
  405.             classes: 'p-2 bg-surface-low rounded-full text-xs text-white whitespace-nowrap',
  406.         });
  407.  
  408.         this.mainBtn.appendChild(this.mainModsCount);
  409.  
  410.         this.menuBtn = new NDCElement('button', {
  411.             classes: 'font-montserrat font-semibold text-sm leading-none tracking-wider uppercase flex gap-x-2 justify-center items-center transition-colors relative min-h-9 focus:outline focus:outline-2 focus:outline-accent focus:outline-offset-2 px-2 py-1 cursor-pointer bg-primary-moderate fill-font-primary text-font-primary border-transparent focus:bg-primary-strong hover:bg-primary-subdued justify-between rounded-r',
  412.             html: '<svg class="w-4 h-4 fill-current" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" role="presentation" style="width: 1.5rem; height: 1.5rem;"><path d="M7.41,8.58L12,13.17L16.59,8.58L18,10L12,16L6,10L7.41,8.58Z" style="fill: currentcolor;"></path></svg>'
  413.         });
  414.  
  415.         this.menu = new NDCElement('div', {
  416.             classes: 'absolute z-10 min-w-48 py-1 px-0 mt-1 text-base text-gray-600 border-stroke-subdued bg-surface-low border border-gray-200 rounded-md shadow-lg outline-none hidden',
  417.             attributes: {
  418.                 style: 'transform: translate(488.453px, 36px);'
  419.             }
  420.         });
  421.  
  422.         this.menuBtnMandatory = new NDCElement('button', {
  423.             classes: 'font-montserrat text-sm font-semibold uppercase leading-none tracking-wider first:rounded-t last:rounded-b relative flex w-full items-center gap-x-2 p-2 text-left font-normal hover:bg-surface-mid hover:text-primary-moderate focus:shadow-accent focus:z-10 focus:outline-none text-start justify-between',
  424.             html: 'Add all mandatory mods'
  425.         });
  426.  
  427.         this.menuBtnMandatoryModsCount = new NDCElement('span', {
  428.             classes: 'p-2 bg-primary-moderate rounded-full text-xs text-white whitespace-nowrap',
  429.         });
  430.  
  431.         this.menuBtnMandatory.appendChild(this.menuBtnMandatoryModsCount);
  432.  
  433.         this.menuBtnOptional = new NDCElement('button', {
  434.             classes: 'font-montserrat text-sm font-semibold uppercase leading-none tracking-wider first:rounded-t last:rounded-b relative flex w-full items-center gap-x-2 p-2 text-left font-normal hover:bg-surface-mid hover:text-primary-moderate focus:shadow-accent focus:z-10 focus:outline-none text-start justify-between',
  435.             html: 'Add all optional mods'
  436.         });
  437.  
  438.         this.menuBtnOptionalModsCount = new NDCElement('span', {
  439.             classes: 'p-2 bg-primary-moderate rounded-full text-xs text-white whitespace-nowrap',
  440.         });
  441.  
  442.         this.menuBtnOptional.appendChild(this.menuBtnOptionalModsCount);
  443.  
  444.         this.menu.appendChild(this.menuBtnMandatory, this.menuBtnOptional);
  445.  
  446.         // on menuButton click show menuButtonMenu
  447.         this.menuBtn.element.addEventListener('click', () => {
  448.             const btnGroupOffset = this.element.getBoundingClientRect();
  449.             this.menu.element.classList.toggle('hidden');
  450.             const dropdownMenuOffset = this.menu.element.getBoundingClientRect();
  451.             this.menu.element.style.transform = `translate(${btnGroupOffset.width - dropdownMenuOffset.width}px, ${btnGroupOffset.height}px)`;
  452.         });
  453.  
  454.         document.addEventListener('click', (event) => {
  455.             const isClickInside = this.menu.element.contains(event.target) || this.menuBtn.element.contains(event.target);
  456.             if (!isClickInside) {
  457.                 this.menu.element.classList.add('hidden');
  458.             }
  459.         });
  460.  
  461.         this.appendChild(this.mainBtn, this.menuBtn, this.menu);
  462.     }
  463.  
  464.     updateModsCountSpan() {
  465.         this.mainModsCount.element.innerHTML = `${this.ndc.mods.mandatory.length + this.ndc.mods.optional.length} mods`;
  466.     }
  467.  
  468.     updateMenuButtonMenuDownloadAllMandatoryModsCount() {
  469.         this.menuBtnMandatoryModsCount.element.innerHTML = `${this.ndc.mods.mandatory.length} mods`;
  470.     }
  471.  
  472.     updateMenuButtonMenuDownloadAllOptionalModsCount() {
  473.         this.menuBtnOptionalModsCount.element.innerHTML = `${this.ndc.mods.optional.length} mods`;
  474.     }
  475.  
  476.     render() {
  477.         this.updateModsCountSpan();
  478.         this.updateMenuButtonMenuDownloadAllMandatoryModsCount();
  479.         this.updateMenuButtonMenuDownloadAllOptionalModsCount();
  480.     }
  481. }
  482.  
  483. class NDCProgressBar extends NDCElement {
  484.     static STATUS_DOWNLOADING = 0;
  485.     static STATUS_PAUSED = 1;
  486.     static STATUS_FINISHED = 2;
  487.     static STATUS_STOPPED = 3;
  488.  
  489.     static STATUS_TEXT = {
  490.         [NDCProgressBar.STATUS_DOWNLOADING]: 'Downloading...',
  491.         [NDCProgressBar.STATUS_PAUSED]: 'Paused',
  492.         [NDCProgressBar.STATUS_FINISHED]: 'Finished',
  493.         [NDCProgressBar.STATUS_STOPPED]: 'Stopped'
  494.     }
  495.  
  496.     constructor(ndc, options = {}) {
  497.         super('div', { classes: 'flex flex-wrap w-100', attributes: { style: 'display: none;' } });
  498.  
  499.         this.ndc = ndc;
  500.  
  501.         this.modsCount = 0;
  502.         this.progress = 0;
  503.         this.status = NDCProgressBar.STATUS_DOWNLOADING;
  504.  
  505.         this.progressBar = new NDCElement('div', {
  506.             classes: 'flex-1 relative w-100 min-h-9 bg-surface-low rounded-l overflow-hidden'
  507.         });
  508.  
  509.         this.progressBarFill = new NDCElement('div', {
  510.             classes: 'absolute top-0 left-0 w-0 h-full bg-primary-moderate',
  511.             attributes: {
  512.                 style: 'transition: width 0.3s ease 0s; width: 0%;'
  513.             }
  514.         });
  515.  
  516.         this.progressBarText = new NDCElement('div', {
  517.             classes: 'absolute top-0 left-0 w-full h-full cursor-pointer grid grid-cols-3 items-center text-white font-montserrat font-semibold text-sm leading-none tracking-wider uppercase'
  518.         });
  519.  
  520.         this.progressBarProgress = new NDCElement('div', {
  521.             classes: 'ml-2',
  522.             html: `${this.progress}%`
  523.         });
  524.  
  525.         this.progressBarTextCenter = new NDCElement('div', {
  526.             html: 'Downloading...',
  527.             classes: 'text-center'
  528.         });
  529.  
  530.         this.progressBarTextRight = new NDCElement('div', {
  531.             html: `${this.progress}/${this.modsCount}`,
  532.             classes: 'text-right mr-2'
  533.         });
  534.  
  535.         this.progressBarText.appendChild(this.progressBarProgress, this.progressBarTextCenter, this.progressBarTextRight);
  536.         this.progressBar.appendChild(this.progressBarFill, this.progressBarText);
  537.  
  538.         this.actionBtnGroup = new NDCElement('div', {
  539.             classes: 'flex'
  540.         });
  541.  
  542.         this.playPauseBtn = new NDCElement('button', {
  543.             classes: 'font-montserrat font-semibold text-sm leading-none tracking-wider uppercase flex gap-x-2 justify-center items-center transition-colors relative min-h-9 focus:outline focus:outline-2 focus:outline-accent focus:outline-offset-2 px-2 py-1 cursor-pointer bg-primary-moderate fill-font-primary text-font-primary border-transparent focus:bg-primary-strong hover:bg-primary-subdued justify-between',
  544.             html: '<svg class="w-4 h-4 fill-current" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" role="presentation" style="width: 1.5rem; height: 1.5rem;"><path d="M14,19H18V5H14M6,19H10V5H6V19Z" style="fill: currentcolor;"></path></svg>'
  545.         });
  546.  
  547.         this.stopBtn = new NDCElement('button', {
  548.             classes: 'font-montserrat font-semibold text-sm leading-none tracking-wider uppercase flex gap-x-2 justify-center items-center transition-colors relative min-h-9 focus:outline focus:outline-2 focus:outline-accent focus:outline-offset-2 px-2 py-1 cursor-pointer bg-primary-moderate fill-font-primary text-font-primary border-transparent focus:bg-primary-strong hover:bg-primary-subdued justify-between rounded-r',
  549.             html: '<svg class="w-4 h-4 fill-current" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" role="presentation" style="width: 1.5rem; height: 1.5rem;"><path d="M18,18H6V6H18V18Z" style="fill: currentcolor;"></path></svg>'
  550.         });
  551.  
  552.         this.playPauseBtn.element.addEventListener('click', () => {
  553.             const status = this.status == NDCProgressBar.STATUS_DOWNLOADING ? NDCProgressBar.STATUS_PAUSED : NDCProgressBar.STATUS_DOWNLOADING;
  554.             this.setStatus(status);
  555.         });
  556.  
  557.         this.stopBtn.element.addEventListener('click', () => {
  558.             this.setStatus(NDCProgressBar.STATUS_STOPPED);
  559.         });
  560.  
  561.         this.actionBtnGroup.appendChild(this.playPauseBtn, this.stopBtn);
  562.  
  563.         this.pauseBetweenDownloadInputContainer = new NDCElement('div', {
  564.             classes: 'flex my-2 gap-2 items-center',
  565.             attributes: {
  566.                 style: 'flex-basis: 100%;'
  567.             }
  568.         });
  569.         this.pauseBetweenDownloadInput = new NDCElement('input', {
  570.             classes: 'text-md text-neutral-subdued border-neutral-subdued bg-surface-mid rounded border indent-2 outline-none hover:border-white focus:border-white focus:text-white p-1 w-14',
  571.             attributes: {
  572.                 type: 'number',
  573.                 min: 0,
  574.                 placeholder: 'Extra pause between download (in seconds)',
  575.                 value: this.ndc.pauseBetweenDownload
  576.             }
  577.         });
  578.         this.pauseBetweenDownloadLabel = new NDCElement('label', {
  579.             classes: 'text-white font-montserrat font-semibold text-sm leading-none tracking-wider uppercase',
  580.             html: 'Extra pause between download (in seconds)'
  581.         });
  582.  
  583.  
  584.         this.pauseBetweenDownloadInput.element.addEventListener('change', async (event) => {
  585.             this.ndc.pauseBetweenDownload = parseInt(event.target.value);
  586.             await GM.setValue('pauseBetweenDownload', this.ndc.pauseBetweenDownload);
  587.         });
  588.  
  589.         this.pauseBetweenDownloadInputContainer.appendChild(this.pauseBetweenDownloadInput, this.pauseBetweenDownloadLabel);
  590.  
  591.         this.appendChild(this.progressBar, this.actionBtnGroup, this.pauseBetweenDownloadInputContainer);
  592.     }
  593.  
  594.     setModsCount(modsCount) {
  595.         this.modsCount = modsCount;
  596.         this.render();
  597.     }
  598.  
  599.     getProgressPercent() {
  600.         return this.modsCount ? Math.round((this.progress / this.modsCount) * 100) : 0;
  601.     }
  602.  
  603.     setProgress(progress) {
  604.         this.progress = progress;
  605.         this.render();
  606.     }
  607.  
  608.     incrementProgress() {
  609.         this.progress++;
  610.         this.render();
  611.     }
  612.  
  613.     setStatus(status) {
  614.         this.status = status;
  615.         this.progressBarTextCenter.element.innerHTML = NDCProgressBar.STATUS_TEXT[status];
  616.         this.render();
  617.     }
  618.  
  619.     updateProgressBarFillWidth() {
  620.         this.progressBarFill.element.style.width = `${this.getProgressPercent()}%`;
  621.     }
  622.  
  623.     updateProgressBarTextProgress() {
  624.         this.progressBarProgress.element.innerHTML = `${this.getProgressPercent()}%`;
  625.     }
  626.  
  627.     updateProgressBarTextRight() {
  628.         this.progressBarTextRight.element.innerHTML = `${this.progress}/${this.modsCount}`;
  629.     }
  630.  
  631.     updatePlayPauseBtn() {
  632.         this.playPauseBtn.element.innerHTML = this.status == NDCProgressBar.STATUS_PAUSED ? '<svg class="w-4 h-4 fill-current" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" role="presentation" style="width: 1.5rem; height: 1.5rem;"><path d="M8,5.14V19.14L19,12.14L8,5.14Z" style="fill: currentcolor;"></path></svg>' : '<svg class="w-4 h-4 fill-current" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" role="presentation" style="width: 1.5rem; height: 1.5rem;"><path d="M14,19H18V5H14M6,19H10V5H6V19Z" style="fill: currentcolor;"></path></svg>';
  633.     }
  634.  
  635.     updatePauseBetweenDownloadInput() {
  636.         this.pauseBetweenDownloadInput.element.value = this.ndc.pauseBetweenDownload;
  637.     }
  638.  
  639.     render() {
  640.         this.updateProgressBarFillWidth()
  641.         this.updateProgressBarTextProgress()
  642.         this.updateProgressBarTextRight()
  643.         this.updatePlayPauseBtn()
  644.         this.updatePauseBetweenDownloadInput()
  645.     }
  646. }
  647.  
  648. class NDCLogConsole extends NDCElement {
  649.     constructor(ndc, options = {}) {
  650.         super('div', { classes: 'flex flex-col w-100 gap-3 mt-3' });
  651.  
  652.         this.ndc = ndc;
  653.         this.hidden = false;
  654.  
  655.         this.toggleLogsButton = new NDCElement('button', {
  656.             classes: 'w-full font-montserrat font-semibold text-sm leading-none tracking-wider uppercase',
  657.             html: 'Hide logs'
  658.         });
  659.  
  660.         this.logContainer = new NDCElement('div', {
  661.             classes: 'w-full bg-surface-low rounded overflow-y-auto text-white font-montserrat font-semibold text-sm border border-primary',
  662.             attributes: {
  663.                 style: 'height: 10rem; resize: vertical;'
  664.             }
  665.         });
  666.  
  667.         this.toggleLogsButton.element.addEventListener('click', () => {
  668.             this.hidden = !this.hidden;
  669.             this.logContainer.element.style.display = this.hidden ? 'none' : '';
  670.             this.toggleLogsButton.element.innerHTML = this.hidden ? 'Show logs' : 'Hide logs';
  671.         });
  672.  
  673.         this.appendChild(this.toggleLogsButton, this.logContainer);
  674.     }
  675.  
  676.     log(type, message) {
  677.         const logMessage = new NDCElement('div', {
  678.             classes: 'gap-x-2 px-2 py-1',
  679.             html: `[${new Date().toLocaleTimeString()}][${type}] ${message}`
  680.         });
  681.  
  682.         this.logContainer.appendChild(logMessage);
  683.         this.logContainer.element.scrollTop = this.logContainer.element.scrollHeight;
  684.  
  685.         console.log(`[${type}] ${message}`);
  686.  
  687.         return logMessage;
  688.     }
  689.  
  690.     clear() {
  691.         this.logContainer.element.innerHTML = '';
  692.     }
  693. }
  694.  
  695. let previousRoute = null;
  696. let ndc = null;
  697.  
  698. async function handleNextRouterChange() {
  699.     if (next.router.state.route === "/[gameDomain]/collections/[collectionSlug]") {
  700.         const { gameDomain, collectionSlug, tab } = next.router.query;
  701.  
  702.         if (previousRoute !== `${gameDomain}/${collectionSlug}`) {
  703.             previousRoute = `${gameDomain}/${collectionSlug}`;
  704.  
  705.             ndc = new NDC(gameDomain, collectionSlug);
  706.         }
  707.  
  708.         if (tab === "mods") {
  709.             document.querySelector("#tabcontent-mods > div > div > div").prepend(ndc.element);
  710.         } else {
  711.             ndc.element.remove();
  712.         }
  713.     }
  714. }
  715.  
  716. // Add an event listener for the hashchange event
  717. next.router.events.on('routeChangeComplete', handleNextRouterChange);
  718.  
  719. handleNextRouterChange();
Add Comment
Please, Sign In to add comment