Quark Direct Link Helper

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

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

ستحتاج إلى تثبيت إضافة مثل Stylus لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتتمكن من تثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

(لدي بالفعل مثبت أنماط للمستخدم، دعني أقم بتثبيته!)

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