Quark Direct Link Helper

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

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği yüklemek için Tampermonkey gibi bir uzantı yüklemeniz gerekir.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği indirebilmeniz için ayrıca Tampermonkey gibi bir eklenti kurmanız gerekmektedir.

Bu komut dosyasını yüklemek için bir kullanıcı komut dosyası yöneticisi uzantısı yüklemeniz gerekecek.

(Zaten bir kullanıcı komut dosyası yöneticim var, kurmama izin verin!)

Bu stili yüklemek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için Stylus gibi bir uzantı kurmanız gerekir.

Bu stili yükleyebilmek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı kurmanız gerekir.

Bu stili yükleyebilmek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

(Zateb bir user-style yöneticim var, yükleyeyim!)

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