Quark Direct Link Helper

Generate direct links for selected files on Quark Drive share pages.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

You will need to install an extension such as Tampermonkey to install this script.

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         Quark Direct Link Helper
// @namespace    Quark-Direct-Link-Helper
// @version      1.8.6
// @description  Generate direct links for selected files on Quark Drive share pages.
// @author       Mustafa Hakan
// @license      MIT
// @match        *://pan.quark.cn/*
// @grant        GM_xmlhttpRequest
// @grant        GM_setClipboard
// @grant        unsafeWindow
// @run-at       document-end
// ==/UserScript==

(function () {
  'use strict';

  const CONFIG = {
    API: 'https://drive.quark.cn/1/clouddrive/file/download?pr=ucpro&fr=pc',
    UA: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) quark-cloud-drive/2.5.20 Chrome/100.0.4896.160 Electron/18.3.5.4-b478491100 Safari/537.36 Channel/pckk_other_ch',
    DEPTH: 25
  };

  const THEME = {
    primary: '#a7c7e7',
    primaryDark: '#8fb3d9',
    surface: '#f8fbff',
    surface2: '#eef5fb',
    text: '#2f3a4a',
    muted: '#6f7b8a',
    border: '#dbe7f3',
    success: '#b8d8ba',
    danger: '#f2b8b5'
  };

  const Utils = {
    getFidFromFiber(dom) {
      if (!dom) return null;
      const key = Object.keys(dom).find(k => k.startsWith('__reactFiber$') || k.startsWith('__reactInternalInstance$'));
      if (!key) return null;

      let fiber = dom[key];
      let attempts = 0;

      while (fiber && attempts < CONFIG.DEPTH) {
        const props = fiber.memoizedProps || fiber.pendingProps || {};
        const candidate = props.record || props.file || props.item || props.data || props.node;

        if (candidate && (candidate.fid || candidate.id)) {
          return {
            fid: candidate.fid || candidate.id,
            name: candidate.file_name || candidate.name || candidate.title || 'Unnamed file',
            isDir: candidate.dir === true || candidate.is_dir === true || candidate.type === 'folder',
            size: candidate.size || 0,
            download_url: candidate.download_url || ''
          };
        }

        fiber = fiber.return;
        attempts += 1;
      }

      return null;
    },

    post(url, data) {
      return new Promise((resolve, reject) => {
        GM_xmlhttpRequest({
          method: 'POST',
          url,
          headers: {
            'Content-Type': 'application/json',
            'User-Agent': CONFIG.UA
          },
          data: JSON.stringify(data),
          responseType: 'json',
          withCredentials: true,
          onload: res => {
            if (res.status === 200) resolve(res.response);
            else reject(res);
          },
          onerror: err => reject(err)
        });
      });
    },

    formatSize(bytes) {
      if (!bytes) return '0 B';
      const units = ['B', 'KB', 'MB', 'GB', 'TB'];
      const i = Math.floor(Math.log(bytes) / Math.log(1024));
      return `${parseFloat((bytes / Math.pow(1024, i)).toFixed(2))} ${units[i]}`;
    },

    generateBatchLinks(files) {
      return files.map(f => f.download_url).filter(Boolean).join('\n');
    },

    buildCurl(file) {
      return `curl -L -C - "${file.download_url}" -o "${file.file_name}" -A "${CONFIG.UA}"`;
    },

    toast(message, type = 'info') {
      const div = document.createElement('div');
      div.textContent = message;
      div.style.cssText = [
        'position:fixed',
        'top:18px',
        'left:50%',
        'transform:translateX(-50%)',
        'z-index:2147483647',
        'padding:10px 14px',
        'border-radius:12px',
        'font:14px/1.4 Arial, sans-serif',
        'color:#2f3a4a',
        'background:' + (type === 'error' ? THEME.danger : type === 'success' ? THEME.success : THEME.surface),
        'border:1px solid ' + THEME.border,
        'box-shadow:0 8px 24px rgba(47,58,74,.12)'
      ].join(';');

      document.body.appendChild(div);
      setTimeout(() => {
        div.style.opacity = '0';
        div.style.transition = 'opacity .25s ease';
        setTimeout(() => div.remove(), 260);
      }, 1800);
    },

    copy(text) {
      if (typeof GM_setClipboard === 'function') {
        GM_setClipboard(text);
      } else {
        navigator.clipboard.writeText(text);
      }
    }
  };

  const App = {
    getSelectedFiles() {
      const selectedFiles = new Map();
      const checkBoxes = document.querySelectorAll('.ant-checkbox-wrapper-checked:not(.ant-checkbox-group-item), .file-item-selected, [aria-checked="true"]');
      const targets = checkBoxes.length > 0 ? checkBoxes : document.querySelectorAll('.ant-checkbox-checked');

      targets.forEach(box => {
        if (box.closest('.ant-table-thead') || box.closest('.list-head')) return;
        const fileData = Utils.getFidFromFiber(box);
        if (fileData && fileData.fid) {
          selectedFiles.set(fileData.fid, fileData);
        }
      });

      return Array.from(selectedFiles.values());
    },

    async run() {
      const btn = document.getElementById('quark-helper-btn');
      const originalText = btn.innerText;

      try {
        let files = App.getSelectedFiles().filter(f => !f.isDir);

        if (files.length === 0) {
          Utils.toast('Please select at least one file.', 'error');
          return;
        }

        btn.innerText = 'Processing';
        btn.style.background = THEME.primaryDark;

        const res = await Utils.post(CONFIG.API, { fids: files.map(f => f.fid) });

        if (res && res.code === 0) {
          UI.showResultWindow(res.data);
        } else {
          Utils.toast(`Parsing failed: ${res?.message || 'Unknown error'}`, 'error');
        }
      } catch (e) {
        console.error(e);
        Utils.toast('Request failed. Please check your network.', 'error');
      } finally {
        btn.innerText = originalText;
        btn.style.background = THEME.primary;
      }
    }
  };

  const UI = {
    createFloatButton() {
      if (document.getElementById('quark-helper-btn')) return;

      const btn = document.createElement('button');
      btn.id = 'quark-helper-btn';
      btn.innerText = 'Download Helper';
      btn.style.cssText = [
        'position:fixed',
        'top:40%',
        'left:12px',
        'z-index:2147483647',
        'background:' + THEME.primary,
        'color:' + THEME.text,
        'font-size:14px',
        'font-weight:600',
        'padding:12px 18px',
        'border:1px solid ' + THEME.border,
        'border-radius:999px',
        'cursor:pointer',
        'box-shadow:0 8px 20px rgba(47,58,74,.10)',
        'transition:all .2s ease',
        'user-select:none'
      ].join(';');

      btn.onclick = App.run;
      btn.onmouseenter = () => {
        btn.style.transform = 'scale(1.03)';
      };
      btn.onmouseleave = () => {
        btn.style.transform = 'scale(1)';
      };

      document.body.appendChild(btn);
    },

    showResultWindow(data) {
      const old = document.getElementById('quark-result-modal');
      if (old) old.remove();

      const modal = document.createElement('div');
      modal.id = 'quark-result-modal';
      modal.style.cssText = [
        'position:fixed',
        'top:0',
        'left:0',
        'width:100%',
        'height:100%',
        'background:rgba(47,58,74,.28)',
        'z-index:2147483648',
        'display:flex',
        'align-items:center',
        'justify-content:center'
      ].join(';');

      const allLinks = Utils.generateBatchLinks(data);

      const contentHTML = data.map(f => {
        const curl = Utils.buildCurl(f);
        const safeCurl = curl.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/"/g, '&quot;');

        return `
          <div style="background:${THEME.surface};padding:12px;margin-bottom:10px;border-radius:12px;border:1px solid ${THEME.border};display:flex;justify-content:space-between;align-items:center;gap:12px;">
            <div style="overflow:hidden;flex:1;">
              <div style="font-weight:600;color:${THEME.text};white-space:nowrap;overflow:hidden;text-overflow:ellipsis;" title="${f.file_name}">${f.file_name}</div>
              <div style="font-size:12px;color:${THEME.muted};margin-top:4px;">${Utils.formatSize(f.size)}</div>
            </div>
            <div style="display:flex;gap:8px;flex-shrink:0;">
              <a href="${f.download_url}" target="_blank" style="padding:6px 10px;background:${THEME.success};color:${THEME.text};text-decoration:none;border-radius:10px;font-size:12px;border:1px solid ${THEME.border};">IDM</a>
              <button class="quark-copy-curl" data-curl="${safeCurl}" style="padding:6px 10px;background:${THEME.primary};color:${THEME.text};border:1px solid ${THEME.border};border-radius:10px;cursor:pointer;font-size:12px;">cURL</button>
            </div>
          </div>
        `;
      }).join('');

      modal.innerHTML = `
        <div style="background:#fff;width:680px;max-width:92%;max-height:86%;border-radius:18px;box-shadow:0 20px 50px rgba(47,58,74,.18);display:flex;flex-direction:column;overflow:hidden;font-family:Arial,sans-serif;">
          <div style="padding:16px 20px;border-bottom:1px solid ${THEME.border};display:flex;justify-content:space-between;align-items:center;background:${THEME.surface};">
            <h3 style="margin:0;color:${THEME.text};font-size:16px;">Parsing complete (${data.length} files)</h3>
            <button id="quark-close-modal" style="border:0;background:transparent;color:${THEME.muted};font-size:18px;cursor:pointer;">Close</button>
          </div>

          <div style="padding:12px 20px;background:${THEME.surface2};border-bottom:1px solid ${THEME.border};display:flex;justify-content:space-between;align-items:center;gap:12px;">
            <span style="color:${THEME.muted};font-size:12px;">IDM User Agent: <b>${CONFIG.UA}</b></span>
            <button id="quark-batch-copy" style="padding:8px 14px;background:${THEME.primary};color:${THEME.text};border:1px solid ${THEME.border};border-radius:12px;cursor:pointer;font-size:13px;font-weight:600;">Copy all links</button>
          </div>

          <div style="padding:18px;overflow-y:auto;flex:1;background:#fff;">${contentHTML}</div>
        </div>
      `;

      document.body.appendChild(modal);

      document.getElementById('quark-close-modal').onclick = () => modal.remove();

      document.getElementById('quark-batch-copy').onclick = () => {
        GM_setClipboard(allLinks);
        Utils.toast('All links copied.', 'success');
      };

      modal.querySelectorAll('.quark-copy-curl').forEach(btn => {
        btn.onclick = e => {
          const curl = e.target.getAttribute('data-curl');
          GM_setClipboard(curl);
          Utils.toast('cURL copied.', 'success');
        };
      });
    }
  };

  setTimeout(() => {
    UI.createFloatButton();

    let lastUrl = location.href;
    new MutationObserver(() => {
      const url = location.href;
      if (url !== lastUrl) {
        lastUrl = url;
        setTimeout(() => UI.createFloatButton(), 800);
      }
    }).observe(document, { subtree: true, childList: true });
  }, 800);
})();