// ==UserScript== // @name 夸克网盘网页一键在客户端打开 // @namespace quark-open-in-client // @version 1.1.0 // @description 在夸克网盘网页版角落显示按钮,点击后用本机已安装的夸克网盘客户端打开(客户端已登录时无需在浏览器再次登录);分享页会把真实分享 ID 完整传给客户端,右键按钮可切换按钮所在角落 // @match *://pan.quark.cn/* // @noframes // @run-at document-idle // @grant none // @license MIT // ==/UserScript== (function () { 'use strict'; var STORAGE_KEY = 'quark_open_client_corner'; var CORNERS = { 'top-left': { label: '左上角' }, 'top-right': { label: '右上角' }, 'bottom-left': { label: '左下角' }, 'bottom-right': { label: '右下角' } }; // 读取上次记住的位置,默认右下角 var corner = 'bottom-right'; try { var saved = localStorage.getItem(STORAGE_KEY); if (saved && CORNERS[saved]) corner = saved; } catch (e) { /* 隐私模式下可能不可用,忽略 */ } var btn = null; var menu = null; // 获取当前分享页的真实分享 ID // 优先级:页面 Redux store(window.store,官方同款取法,最可靠)→ URL 路径最后一段 function getShareId() { try { var st = window.store && window.store.getState && window.store.getState(); if (st && st.share && st.share.pwd_id) return st.share.pwd_id; } catch (e) { /* 拿不到 store 就回退 URL */ } var seg = (window.location.pathname || '').split('/').filter(Boolean).pop(); return seg || ''; } // 获取分享提取码(官方从 cookie 的 pwd / passcode 读取) function getPasscode() { var m = document.cookie.match(/(?:^|;\s*)(?:pwd|passcode)=([^;]+)/); return m ? decodeURIComponent(m[1]) : ''; } // 构造唤起夸克网盘客户端的协议链接 // 参数与网页版官方「在客户端打开」按钮完全一致(取自官方 share.js 的 M()/T() 逻辑): // qkclouddrive://openShareModal?pwdid=&source=13&pwd_id=[&passcode=<提取码>]&share_dn=[&user_token=<登录码>]&is_download=1 function buildDeepLink() { var path = window.location.pathname || ''; var isSharePage = /^\/s\//.test(path); var shareId = getShareId(); if (isSharePage && shareId) { var p = []; p.push('pwdid=' + encodeURIComponent(shareId)); // 客户端识别分享的主参数 p.push('source=13'); // 官方枚举:share_client_view p.push('pwd_id=' + encodeURIComponent(shareId)); var passcode = getPasscode(); if (passcode) p.push('passcode=' + encodeURIComponent(passcode)); // 有提取码才带 p.push('share_dn=' + encodeURIComponent(shareId)); try { var st = window.store && window.store.getState && window.store.getState(); if (st && st.user && st.user.userCode) { p.push('user_token=' + encodeURIComponent(st.user.userCode)); // 登录态透传 } } catch (e) { /* 忽略 */ } p.push('is_download=1'); return 'qkclouddrive://openShareModal?' + p.join('&'); } // 其他网盘页面:唤起客户端(打开网盘首页,使用客户端自身的登录态) return 'qkclouddrive://activate?from=web'; } function openInClient() { var url = window.location.href; if (!/^https?:\/\//i.test(url)) return; // 由真实点击触发,浏览器才会放行协议跳转 window.location.href = buildDeepLink(); } // 按当前 corner 摆放按钮并记住选择 function applyCorner() { if (!btn) return; var s = btn.style; s.left = s.right = s.top = s.bottom = 'auto'; if (corner.indexOf('right') !== -1) s.right = '24px'; else s.left = '24px'; if (corner.indexOf('bottom') !== -1) s.bottom = '24px'; else s.top = '24px'; try { localStorage.setItem(STORAGE_KEY, corner); } catch (e) {} } function hideMenu() { if (menu) menu.style.display = 'none'; } // 右键时在按钮旁弹出菜单 function showMenu(e) { e.preventDefault(); e.stopPropagation(); if (!menu || !btn) return; var rect = btn.getBoundingClientRect(); var gap = 8; var m = menu.style; m.display = 'block'; m.left = m.right = m.top = m.bottom = 'auto'; if (corner.indexOf('right') !== -1) { m.right = (window.innerWidth - rect.right) + 'px'; } else { m.left = rect.left + 'px'; } if (corner.indexOf('bottom') !== -1) { m.bottom = (window.innerHeight - rect.top) + gap + 'px'; } else { m.top = rect.bottom + gap + 'px'; } } // 刷新当前选中项的 ✓ 标记 function refreshMenuMarks() { if (!menu) return; Array.prototype.forEach.call(menu.children, function (item) { var c = item.getAttribute('data-corner'); item.textContent = (c === corner ? '✓ ' : '  ') + CORNERS[c].label; }); } function createMenu() { menu = document.createElement('div'); menu.id = 'quark-open-in-client-menu'; menu.setAttribute('role', 'menu'); menu.style.cssText = 'position:fixed;z-index:2147483647;display:none;min-width:116px;' + 'padding:4px 0;border-radius:6px;background:linear-gradient(135deg,#4e6ef2,#2b45c4);' + 'border:1px solid rgba(255,255,255,0.35);box-shadow:0 4px 16px rgba(0,0,0,0.45);' + 'font:13px/1.4 "Segoe UI","Microsoft YaHei",Arial,sans-serif;user-select:none;'; Object.keys(CORNERS).forEach(function (key) { var item = document.createElement('div'); item.setAttribute('role', 'menuitem'); item.setAttribute('data-corner', key); item.style.cssText = 'padding:7px 14px;color:#ffffff;cursor:pointer;white-space:nowrap;'; item.textContent = (key === corner ? '✓ ' : '  ') + CORNERS[key].label; item.addEventListener('mouseenter', function () { item.style.background = 'rgba(255,255,255,0.18)'; item.style.color = '#ffffff'; }); item.addEventListener('mouseleave', function () { item.style.background = 'transparent'; item.style.color = '#ffffff'; }); item.addEventListener('click', function () { corner = key; applyCorner(); refreshMenuMarks(); hideMenu(); }); menu.appendChild(item); }); (document.body || document.documentElement).appendChild(menu); } // 点击菜单外 / Esc / 窗口变化时收起菜单 function bindDismiss() { document.addEventListener('click', function (e) { if (menu && menu.style.display !== 'none' && !menu.contains(e.target)) { hideMenu(); } }, true); document.addEventListener('keydown', function (e) { if (e.key === 'Escape') hideMenu(); }); window.addEventListener('resize', hideMenu); window.addEventListener('blur', hideMenu); } function ensureButton() { if (btn) return; btn = document.createElement('button'); btn.id = 'quark-open-in-client-btn'; btn.type = 'button'; btn.title = '左键:在夸克网盘客户端打开;右键:调整按钮位置'; btn.textContent = '在夸克网盘客户端打开'; var s = btn.style; s.position = 'fixed'; s.zIndex = '2147483647'; s.padding = '8px 14px'; s.border = 'none'; s.borderRadius = '6px'; s.background = 'linear-gradient(135deg, #4e6ef2, #2b45c4)'; s.color = '#ffffff'; s.font = '13px/1.4 "Segoe UI", "Microsoft YaHei", Arial, sans-serif'; s.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.35)'; s.cursor = 'pointer'; s.opacity = '0.88'; s.transition = 'opacity .15s ease, transform .15s ease'; btn.addEventListener('mouseenter', function () { btn.style.opacity = '1'; btn.style.transform = 'translateY(-1px)'; }); btn.addEventListener('mouseleave', function () { btn.style.opacity = '0.88'; btn.style.transform = 'none'; }); btn.addEventListener('click', openInClient); btn.addEventListener('contextmenu', showMenu); (document.body || document.documentElement).appendChild(btn); createMenu(); applyCorner(); bindDismiss(); } if (document.body) { ensureButton(); } else { document.addEventListener('DOMContentLoaded', ensureButton); } })();