Quark Direct Link Helper

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

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==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);
})();