Quark Direct Link Helper

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

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 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);
})();