// ==UserScript== // @name 光鸭云盘 - 获取直链 // @namespace http://tampermonkey.net/ // @author 快乐无极 // @version 1.7 // @description 获取所选文件的直链地址 // @match https://www.guangyapan.com/* // @grant GM.xmlHttpRequest // @connect localhost // @connect 127.0.0.1 // @connect * // @downloadURL https://update.greasyfork.icu/scripts/575452/%E5%85%89%E9%B8%AD%E4%BA%91%E7%9B%98%20-%20%E8%8E%B7%E5%8F%96%E7%9B%B4%E9%93%BE.user.js // @updateURL https://update.greasyfork.icu/scripts/575452/%E5%85%89%E9%B8%AD%E4%BA%91%E7%9B%98%20-%20%E8%8E%B7%E5%8F%96%E7%9B%B4%E9%93%BE.meta.js // ==/UserScript== (function() { 'use strict'; const API_URL = 'https://api.guangyapan.com/nd.bizuserres.s/v1/get_res_download_url'; const CONCURRENCY = 3; const BATCH_DELAY = 200; const FETCH_TIMEOUT = 10000; const MAX_RETRIES = 3; const RETRY_BASE_DELAY = 500; let modalCreated = false; let abortController = null; let fileListCache = null; let fileListCacheTime = 0; const FILE_LIST_CACHE_TTL = 2000; let lastCleanTime = 0; const CLEAN_INTERVAL = 5000; // 记录选中的文件 const selectedFilesMap = new Map(); // fileId -> { name, addedAt } // ========== React 内部状态获取选中项 ========== function looksLikeFileId(key) { if (!key || typeof key !== 'string') return false; const text = String(key).trim(); // 光鸭的 fileId 是 19 位数字字符串 return /^\d{16,22}$/.test(text); } // 从 React DevTools 可以看到:FileList -> props -> dataSource 和 selectedItems function findFileListComponent() { const now = Date.now(); if (fileListCache && (now - fileListCacheTime) < FILE_LIST_CACHE_TTL) { return fileListCache; } const roots = []; // 查找所有 React 容器 document.querySelectorAll('*').forEach(el => { Object.keys(el).forEach(k => { if (k.startsWith('__react') || k.startsWith('_react')) { roots.push({ el, fiber: el[k] }); } }); }); // 遍历 fiber 树查找 FileList 组件 const findFileList = (fiber, depth = 0) => { if (depth > 60 || !fiber) return null; // 检查组件名称是否包含 FileList const typeName = fiber.elementType?.name || fiber.elementType?.toString() || ''; const isFileList = typeName === 'FileList' || typeName.includes('FileList'); // 检查 memoizedProps(组件的 props) if (fiber.memoizedProps) { const props = fiber.memoizedProps; if (props.selectedItems !== undefined || props.dataSource) { return { props, type: 'memoizedProps', componentName: typeName }; } } // 检查 pendingProps if (fiber.pendingProps) { const props = fiber.pendingProps; if (props.selectedItems !== undefined || props.dataSource) { return { props, type: 'pendingProps', componentName: typeName }; } } // 检查 stateNode 的 props if (fiber.stateNode && typeof fiber.stateNode === 'object') { const node = fiber.stateNode; if (node.props && (node.props.selectedItems !== undefined || node.props.dataSource)) { return { props: node.props, type: 'stateNode.props', componentName: typeName }; } } // 递归查找 child if (fiber.child) { const found = findFileList(fiber.child, depth + 1); if (found) return found; } // 递归查找 sibling if (fiber.sibling) { const found = findFileList(fiber.sibling, depth); if (found) return found; } return null; }; for (const { fiber } of roots) { const found = findFileList(fiber); if (found) { fileListCache = found; fileListCacheTime = now; return found; } } return null; } function getSelectedItemsFromReact() { const result = findFileListComponent(); if (!result) { return { ids: new Set(), names: new Set(), filesMap: new Map() }; } const { props } = result; const marker = { ids: new Set(), names: new Set(), filesMap: new Map() }; // 从 dataSource 获取完整的文件列表(用于根据 ID 查文件名和判断类型) // resType: 1 = 文件, 2 = 文件夹 const dataSource = props.dataSource || props.list || props.fileList || []; if (Array.isArray(dataSource)) { dataSource.forEach(item => { if (item && item.fileId) { const fileId = String(item.fileId); const resType = item.resType; const isDir = resType === 2; // resType 2 是文件夹 marker.filesMap.set(fileId, { fileId: fileId, fileName: item.fileName || item.name || item.title || '', isDir: isDir, size: item.fileSize || 0 }); } }); } // 获取 selectedItems(选中的文件 ID) let selectedItems = props.selectedItems; // 也检查其他可能的字段名 if (!selectedItems && props.selectedRowKeys) { selectedItems = props.selectedRowKeys; } if (!selectedItems && props.selection) { selectedItems = props.selection; } if (!selectedItems && props.checkedKeys) { selectedItems = props.checkedKeys; } if (!selectedItems) { return marker; } // selectedItems 可能是 Set、Map、数组或普通对象 if (selectedItems instanceof Set) { selectedItems.forEach(id => marker.ids.add(String(id))); } else if (selectedItems instanceof Map) { selectedItems.forEach((val, id) => { marker.ids.add(String(id)); if (val && typeof val === 'object') { if (val.fileName) marker.names.add(val.fileName); if (val.name) marker.names.add(val.name); } }); } else if (Array.isArray(selectedItems)) { selectedItems.forEach(item => { if (item && typeof item === 'object') { if (item.fileId) marker.ids.add(String(item.fileId)); if (item.fileName) marker.names.add(item.fileName); if (item.name) marker.names.add(item.name); } else if (typeof item === 'string' || typeof item === 'number') { marker.ids.add(String(item)); } }); } else if (typeof selectedItems === 'object') { // 可能是普通对象 { fileId: true } 或 Set-like 对象 Object.entries(selectedItems).forEach(([id, val]) => { if (looksLikeFileId(id)) { marker.ids.add(id); if (val && typeof val === 'object') { if (val.fileName) marker.names.add(val.fileName); if (val.name) marker.names.add(val.name); } } }); } return marker; } function collectSelectedItemsFromDOM() { const marker = { ids: new Set(), names: new Set() }; // 从当前 DOM 获取选中的 checkbox document.querySelectorAll('.ant-table-row-selected').forEach(row => { const fileId = row.getAttribute('data-row-key'); if (fileId && looksLikeFileId(fileId)) { marker.ids.add(fileId); // 尝试多种选择器获取文件名,稳定性优先 const nameDiv = row.querySelector('.ant-table-cell:nth-child(2) [title]') || row.querySelector('.ant-table-cell:nth-child(2)') || row.querySelector('[class*="name"]') || row.querySelector('.ant-typography'); if (nameDiv) { const name = nameDiv.getAttribute('title') || nameDiv.textContent; if (name) marker.names.add(name.trim()); } } }); return marker; } function getSelectedFileIdsFromFramework() { // 首先尝试从 React 组件获取 const reactMarker = getSelectedItemsFromReact(); const domMarker = collectSelectedItemsFromDOM(); // 合并 domMarker.ids.forEach(id => reactMarker.ids.add(id)); domMarker.names.forEach(name => reactMarker.names.add(name)); return reactMarker; } // ========== 选中文件监听 ========== function setupCheckboxListener() { document.querySelectorAll('.ant-table-tbody').forEach(tbody => { tbody.addEventListener('click', (e) => { const checkbox = e.target.closest('.ant-checkbox-input'); if (!checkbox) return; const row = checkbox.closest('tr'); if (!row) return; const fileId = row.getAttribute('data-row-key'); if (!fileId) return; let fileName = null; const nameDiv = row.querySelector('.ant-table-cell:nth-child(2) [title]') || row.querySelector('.ant-table-cell:nth-child(2)'); if (nameDiv) { fileName = nameDiv.getAttribute('title') || nameDiv.textContent; } if (!fileName) { fileName = '文件_' + fileId; } let fileSize = 0; if (fileListCache && fileListCache.props) { const props = fileListCache.props; const dataSource = props.dataSource || []; if (Array.isArray(dataSource)) { const fileData = dataSource.find(item => item && String(item.fileId) === fileId); if (fileData) fileSize = fileData.fileSize || 0; } } if (checkbox.checked) { selectedFilesMap.set(fileId, { name: fileName.trim(), size: fileSize, addedAt: Date.now() }); } else { selectedFilesMap.delete(fileId); } }); }); document.querySelectorAll('.ant-table-header .ant-checkbox-input').forEach(checkbox => { checkbox.addEventListener('click', (e) => { setTimeout(() => { if (e.target.checked) { selectedFilesMap.clear(); const marker = getSelectedFileIdsFromFramework(); document.querySelectorAll('.ant-table-row-selected').forEach(row => { const fileId = row.getAttribute('data-row-key'); if (fileId) { const nameDiv = row.querySelector('.ant-table-cell:nth-child(2) [title]') || row.querySelector('.ant-table-cell:nth-child(2)'); const name = nameDiv ? (nameDiv.getAttribute('title') || nameDiv.textContent) : ('文件_' + fileId); const fileData = marker.filesMap ? marker.filesMap.get(fileId) : null; const fileSize = fileData ? (fileData.size || 0) : 0; selectedFilesMap.set(fileId, { name: name.trim(), size: fileSize, addedAt: Date.now() }); } }); } else { selectedFilesMap.clear(); } }, 100); }); }); } function getSelectedFilesFromMap() { const files = Array.from(selectedFilesMap.entries()).map(([id, data]) => ({ id: id, name: data.name, size: data.size || 0 })); return files; } // 清理过期的选中记录(当文件从列表中消失时) function cleanExpiredSelections() { const currentRowKeys = new Set(); document.querySelectorAll('tr[data-row-key]').forEach(tr => { currentRowKeys.add(tr.getAttribute('data-row-key')); }); let cleaned = 0; selectedFilesMap.forEach((_, key) => { if (!currentRowKeys.has(key)) { selectedFilesMap.delete(key); cleaned++; } }); } // 动态生成设备ID function getAuthToken() { try { const candidates = []; const currentUserId = localStorage.getItem('current_user_id') || localStorage.getItem('userId') || localStorage.getItem('uid'); for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key && key.startsWith('credentials_')) { const tokenData = localStorage.getItem(key); if (!tokenData) continue; try { const json = JSON.parse(tokenData); if (json.access_token) { // 尝试匹配用户ID const matchScore = (json.user_id === currentUserId) ? 2 : (key.includes(currentUserId)) ? 1 : 0; candidates.push({ key, token: json.access_token, score: matchScore, expiresAt: json.expires_at || 0 }); } } catch (e) { continue; } } } if (candidates.length === 0) return null; // 优先选择匹配当前用户的token,其次选择未过期的,最后选最新的 candidates.sort((a, b) => { if (a.score !== b.score) return b.score - a.score; const now = Date.now(); const aValid = a.expiresAt > now; const bValid = b.expiresAt > now; if (aValid !== bValid) return aValid ? -1 : 1; return b.score - a.score; }); const selected = candidates[0]; return selected.token; } catch (e) { console.error('GYP: Error getting token:', e); return null; } } function getAuthHeader() { const token = getAuthToken(); if (!token) return null; if (token.startsWith('Bearer ')) return token; return 'Bearer ' + token; } function findUploadButton() { // 查找包含"上传"文字的按钮 const buttons = document.querySelectorAll('button'); for (const btn of buttons) { if (btn.textContent.includes('上传')) { return btn; } } return null; } // ========== Aria2 发送功能 ========== const ARIA2_STORAGE_KEY = 'gyp_aria2_config'; function getAria2Config() { const saved = localStorage.getItem(ARIA2_STORAGE_KEY); if (saved) { try { return JSON.parse(saved); } catch (e) { return { rpc: '', secret: '' }; } } return { rpc: '', secret: '' }; } const loadAria2Config = getAria2Config; function openAria2Modal() { // 清理旧弹窗 const existingModal = document.getElementById('gyp-aria2-modal-overlay'); if (existingModal) existingModal.remove(); const config = loadAria2Config(); // 创建 Ant Design Modal,表单用 Shadow DOM 隔离 React 事件 const modal = document.createElement('div'); modal.id = 'gyp-aria2-modal-overlay'; modal.style.cssText = 'position: fixed; inset: 0; z-index: 1000000;'; // 遮罩层 const mask = document.createElement('div'); mask.style.cssText = 'position: fixed; inset: 0; background: rgba(0,0,0,0.45);'; mask.onclick = () => modal.remove(); modal.appendChild(mask); // 弹窗容器 const wrap = document.createElement('div'); wrap.style.cssText = 'position: fixed; inset: 0; overflow: auto; outline: 0; display: flex; align-items: flex-start; justify-content: center; padding-top: 100px;'; modal.appendChild(wrap); // Shadow DOM 宿主 - 隔离 React 事件 const shadowHost = document.createElement('div'); wrap.appendChild(shadowHost); const shadow = shadowHost.attachShadow({ mode: 'open' }); shadow.innerHTML = ` `; // Shadow DOM 事件 - 原生事件,不受 React 影响 shadow.getElementById('gyp-shadow-close').onclick = () => modal.remove(); shadow.getElementById('gyp-shadow-cancel').onclick = () => modal.remove(); // 密钥显示/隐藏处理 const secretInput = shadow.getElementById('gyp-shadow-secret'); const secretReal = shadow.getElementById('gyp-shadow-secret-real'); const realValue = secretReal.value; if (realValue) { secretInput.value = '•'.repeat(realValue.length); secretInput.classList.add('secret-mask'); } secretInput.addEventListener('input', () => { const val = secretInput.value; // 如果输入的是圆点,说明用户在修改被隐藏的密码 if (val.includes('•')) { // 清空,用 real 恢复显示 const stored = secretReal.value; if (stored) { secretInput.value = '•'.repeat(stored.length); // 把光标移到末尾 setTimeout(() => { secretInput.setSelectionRange(stored.length, stored.length); }, 0); } } else { // 用户输入了明文 secretReal.value = val; secretInput.classList.add('secret-mask'); } }); secretInput.addEventListener('focus', () => { // 聚焦时显示真实值(临时) const stored = secretReal.value; if (stored) { secretInput.value = stored; secretInput.classList.remove('secret-mask'); setTimeout(() => { secretInput.setSelectionRange(stored.length, stored.length); }, 0); } }); secretInput.addEventListener('blur', () => { // 失去焦点时重新隐藏 const stored = secretReal.value; if (stored) { secretInput.value = '•'.repeat(stored.length); secretInput.classList.add('secret-mask'); } }); shadow.getElementById('gyp-shadow-save').onclick = () => { const rpc = shadow.getElementById('gyp-shadow-rpc').value.trim(); const secret = secretReal.value; if (!rpc) { alert('请输入 RPC 地址'); return; } localStorage.setItem('gyp_aria2_config', JSON.stringify({ rpc, secret })); showToast('配置已保存'); updateAria2ButtonState(); modal.remove(); }; // 聚焦 setTimeout(() => shadow.getElementById('gyp-shadow-rpc').focus(), 100); document.body.appendChild(modal); } function updateAria2ButtonState() { // 更新配置按钮状态 } function getFileNameFromUrl(url) { try { const urlObj = new URL(url); const pathname = urlObj.pathname; const fileName = pathname.split('/').pop(); return decodeURIComponent(fileName) || '未知文件'; } catch { return url.split('/').pop() || '未知文件'; } } function getAllLinks() { const modal = document.getElementById('gyp-modal-overlay'); const rows = modal.querySelector('#gyp-result-tbody').querySelectorAll('tr'); const links = []; rows.forEach(row => { const urlLink = row.querySelector('.gyp-col-url a'); const nameCell = row.querySelector('.gyp-col-name'); const sizeCell = row.querySelector('.gyp-col-size'); if (urlLink && urlLink.textContent) { const url = urlLink.textContent.trim(); const name = nameCell ? nameCell.textContent.trim() : getFileNameFromUrl(url); const size = sizeCell ? parseInt(sizeCell.getAttribute('title') || '0', 10) : 0; links.push({ url, name, size }); } }); return links; } function getSelectedLinks() { const modal = document.getElementById('gyp-modal-overlay'); const checkboxes = modal.querySelectorAll('.gyp-row-checkbox:checked'); const links = []; checkboxes.forEach(cb => { const tr = cb.closest('tr'); if (tr) { const urlLink = tr.querySelector('.gyp-col-url a'); const nameCell = tr.querySelector('.gyp-col-name'); if (urlLink && urlLink.textContent) { const url = urlLink.textContent.trim(); const name = nameCell ? nameCell.textContent.trim() : getFileNameFromUrl(url); const size = parseInt(cb.getAttribute('data-size') || '0', 10); links.push({ url, name, size }); } } }); return links; } async function aria2SendLinks(links) { const config = getAria2Config(); if (!config.rpc) { showToast('请先配置 RPC 地址', 2000, 'warning'); return { success: 0, failed: 0, errors: [] }; } const secret = config.secret ? 'token:' + config.secret : ''; const rpcUrl = config.rpc; let success = 0; let failed = 0; const errors = []; for (const link of links) { try { const result = await new Promise((resolve, reject) => { GM.xmlHttpRequest({ method: 'POST', url: rpcUrl, headers: { 'Content-Type': 'application/json' }, data: JSON.stringify({ jsonrpc: '2.0', id: Date.now(), method: 'aria2.addUri', params: secret ? [secret, [link.url], { out: link.name }] : [[link.url], { out: link.name }] }), onload: function(response) { try { resolve(JSON.parse(response.responseText)); } catch (e) { reject(e); } }, onerror: reject }); }); if (result.error) { console.error('Aria2 error:', result.error); let errMsg = result.error.message || ''; if (errMsg === 'Unauthorized' || errMsg === 'Forbidden' || result.error.code === -32600) { errMsg = '密钥错误,请检查 RPC 密钥配置是否正确'; } else if (errMsg === 'Not Found' || result.error.code === -32600) { errMsg = 'Aria2 方法不存在,可能是版本不兼容'; } else if (!errMsg) { errMsg = 'Aria2 返回错误: ' + JSON.stringify(result.error); } errors.push({ name: link.name, error: errMsg }); failed++; break; } else { success++; } } catch (err) { console.error('Aria2 request failed:', err); let errMsg; const errStr = (err.error || err.statusText || '').toLowerCase(); if (errStr.includes('blocked by the user') || errStr.includes('denied') || errStr.includes('refused')) { errMsg = '请求被拒绝,请在弹窗中选择"允许"以继续请求'; } else if (err.status === 0 || err.status === undefined) { errMsg = '无法连接到 Aria2 服务,请确认服务已启动且 RPC 地址正确'; } else if (err.status >= 400 && err.status < 500) { errMsg = '请求错误 (HTTP ' + err.status + ')'; } else if (err.status >= 500) { errMsg = 'Aria2 服务器错误 (HTTP ' + err.status + ')'; } else { errMsg = err.statusText || ('HTTP ' + err.status); } errors.push({ name: link.name, error: errMsg }); failed++; break; } } return { success, failed, errors }; } function showAria2ErrorAlert(errors) { const errorList = errors.map(e => '
' + e.name + '
' + e.error + '
').join(''); const overlay = document.createElement('div'); overlay.id = 'gyp-aria2-error-overlay'; overlay.style.cssText = 'position: fixed; inset: 0; z-index: 10000000; display: flex; align-items: center; justify-content: center; background: rgba(0,0,0,0.5);'; overlay.innerHTML = '
' + '
' + '
' + '' + 'Aria2 发送失败' + '
' + '' + '
' + '
' + '
遇到错误,发送已中断,以下文件未能发送:
' + errorList + '
' + '
' + '' + '
' + '
'; document.body.appendChild(overlay); document.getElementById('gyp-error-close-btn').addEventListener('click', function() { overlay.remove(); }); document.getElementById('gyp-error-close-btn-bottom').addEventListener('click', function() { overlay.remove(); }); overlay.addEventListener('click', function(e) { if (e.target === overlay) { overlay.remove(); } }); } function showAria2SuccessAlert(count) { const overlay = document.createElement('div'); overlay.id = 'gyp-aria2-success-overlay'; overlay.style.cssText = 'position: fixed; inset: 0; z-index: 10000000; display: flex; align-items: center; justify-content: center; background: rgba(0,0,0,0.5);'; overlay.innerHTML = '
' + '
' + '' + '' + '' + '' + '
' + '
发送成功
' + '
' + count + ' 个任务已发送到 Aria2
' + '
3 秒后自动关闭
' + '
' + '
' + '
' + '' + '
' + '
'; document.body.appendChild(overlay); // 动画样式 const style = document.createElement('style'); style.textContent = '@keyframes gyp-check-draw { 0% { stroke-dashoffset: 50; } 100% { stroke-dashoffset: 0; } }' + '@keyframes gyp-circle-draw { 0% { stroke-dashoffset: 170; } 100% { stroke-dashoffset: 0; } }'; document.head.appendChild(style); const checkPath = overlay.querySelector('.gyp-check-path'); checkPath.style.strokeDasharray = '50'; checkPath.style.strokeDashoffset = '50'; checkPath.style.animation = 'gyp-check-draw 0.4s ease-out 0.3s forwards'; const circle = overlay.querySelector('circle'); circle.style.strokeDasharray = '170'; circle.style.strokeDashoffset = '170'; circle.style.animation = 'gyp-circle-draw 0.5s ease-out forwards'; const closeAlert = function() { overlay.remove(); style.remove(); }; document.getElementById('gyp-success-close-btn').addEventListener('click', closeAlert); // 3秒后自动关闭 let remaining = 3; const secondsSpan = document.getElementById('gyp-success-seconds'); const updateCountdown = function() { remaining--; if (remaining > 0 && secondsSpan) { secondsSpan.textContent = remaining; setTimeout(updateCountdown, 1000); } else { closeAlert(); } }; setTimeout(updateCountdown, 1000); } function formatSize(bytes) { if (!bytes || bytes === 0) return '-'; const units = ['B', 'KB', 'MB', 'GB', 'TB']; let i = 0; while (bytes >= 1024 && i < units.length - 1) { bytes /= 1024; i++; } return bytes.toFixed(bytes < 10 ? 2 : 1) + ' ' + units[i]; } // 确认弹窗 function showConfirmModal(title, message, count, totalSize, onConfirm) { const overlay = document.createElement('div'); overlay.style.cssText = 'position: fixed; inset: 0; z-index: 10000000; display: flex; align-items: center; justify-content: center;'; const sizeText = totalSize > 0 ? '
总计: ' + formatSize(totalSize) + '
' : ''; overlay.innerHTML = '
' + '
' + '
' + '
' + '' + title + '
' + '
' + '
' + '
' + message + '
' + '
' + '
' + '
' + count + '
' + '
个文件
' + '
' + '
' + '
' + (totalSize > 0 ? formatSize(totalSize) : '-') + '
' + '
总体积
' + '
' + '
' + '
点击确认后将开始下载任务
' + '
' + '
' + '' + '' + '
' + '
'; document.body.appendChild(overlay); const closeModal = () => overlay.remove(); overlay.querySelector('#gyp-confirm-cancel').onclick = closeModal; overlay.querySelector('#gyp-confirm-ok').onclick = () => { closeModal(); onConfirm(); }; } async function sendToAria2Selected() { const config = getAria2Config(); if (!config.rpc) { showToast('请先配置 RPC 地址', 2000, 'warning'); return; } const links = getSelectedLinks(); if (links.length === 0) { showToast('请先选择要发送的链接', 2000, 'warning'); return; } const totalSize = links.reduce((sum, link) => sum + (link.size || 0), 0); showConfirmModal('确认下载', '即将下载选中的文件到 Aria2', links.length, totalSize, async () => { showToast('正在发送到 Aria2...', 3000); const result = await aria2SendLinks(links); hideToast(); if (result.failed === 0) { showAria2SuccessAlert(result.success); } else { showToast('发送完成:成功 ' + result.success + ' 个,失败 ' + result.failed + ' 个', 3000, 'warning'); if (result.errors.length > 0) { showAria2ErrorAlert(result.errors); } } }); } async function sendToAria2All() { const config = getAria2Config(); if (!config.rpc) { showToast('请先配置 RPC 地址', 2000, 'warning'); return; } const links = getAllLinks(); if (links.length === 0) { showToast('没有可发送的链接', 2000, 'warning'); return; } const totalSize = links.reduce((sum, link) => sum + (link.size || 0), 0); showConfirmModal('确认下载全部', '即将下载全部文件到 Aria2', links.length, totalSize, async () => { showToast('正在发送到 Aria2...', 3000); const result = await aria2SendLinks(links); hideToast(); if (result.failed === 0) { showAria2SuccessAlert(result.success); } else { showToast('发送完成:成功 ' + result.success + ' 个,失败 ' + result.failed + ' 个', 3000, 'warning'); if (result.errors.length > 0) { showAria2ErrorAlert(result.errors); } } }); } function createModal() { if (modalCreated) return; const modal = document.createElement('div'); modal.id = 'gyp-modal-overlay'; modal.innerHTML = '
' + '
' + '获取直链' + '
' + '' + '' + '
' + '
' + '
' + '
' + '
' + '准备就绪' + '0%' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
文件名直链地址大小操作
' + '
' + '
' + '
' + '
' + '' + '已选择 0 项' + '' + '' + '
' + '
' + '' + '' + '' + '' + '
' + '
' + '
' + '
' + '
'; document.body.appendChild(modal); modal.querySelector('#gyp-modal-close').onclick = closeModal; modal.querySelector('#gyp-modal-close-btn').onclick = closeModal; modal.querySelector('#gyp-copy-all').onclick = copyAllUrls; modal.querySelector('#gyp-select-all').onclick = function() { const checked = this.checked; const tbody = modal.querySelector('#gyp-result-tbody'); const checkboxes = tbody.querySelectorAll('.gyp-row-checkbox'); checkboxes.forEach(function(cb) { cb.checked = checked; }); updateSelectedBar(); // 全选操作后,清除 indeterminate 状态 this.indeterminate = false; }; modal.querySelector('#gyp-copy-selected').onclick = copySelectedUrls; modal.querySelector('#gyp-copy-selected-name').onclick = copySelectedNames; modal.querySelector('#gyp-deselect-selected').onclick = deselectAll; // Aria2 事件处理 modal.querySelector('#gyp-aria2-config').onclick = openAria2Modal; modal.querySelector('#gyp-aria2-send-selected').onclick = sendToAria2Selected; modal.querySelector('#gyp-aria2-send-all').onclick = sendToAria2All; modalCreated = true; } function updateSelectAllState() { const modal = document.getElementById('gyp-modal-overlay'); const tbody = modal.querySelector('#gyp-result-tbody'); const selectAllCheckbox = modal.querySelector('#gyp-select-all'); if (!tbody || !selectAllCheckbox) return; const checkboxes = tbody.querySelectorAll('.gyp-row-checkbox'); const total = checkboxes.length; const checked = tbody.querySelectorAll('.gyp-row-checkbox:checked').length; if (total === 0) { selectAllCheckbox.checked = false; selectAllCheckbox.indeterminate = false; } else if (checked === 0) { selectAllCheckbox.checked = false; selectAllCheckbox.indeterminate = false; } else if (checked === total) { selectAllCheckbox.checked = true; selectAllCheckbox.indeterminate = false; } else { selectAllCheckbox.checked = false; selectAllCheckbox.indeterminate = true; } } function updateSelectedBar() { const modal = document.getElementById('gyp-modal-overlay'); const tbody = modal.querySelector('#gyp-result-tbody'); const checkboxes = tbody.querySelectorAll('.gyp-row-checkbox:checked'); const count = checkboxes.length; const countSpan = modal.querySelector('#gyp-selected-count'); const copyNameBtn = modal.querySelector('#gyp-copy-selected-name'); const copyUrlBtn = modal.querySelector('#gyp-copy-selected'); const aria2SelectedBtn = modal.querySelector('#gyp-aria2-send-selected'); countSpan.textContent = '已选择 ' + count + ' 项'; if (count > 0) { copyNameBtn.classList.remove('gyp-hidden'); copyUrlBtn.classList.remove('gyp-hidden'); aria2SelectedBtn.classList.remove('gyp-hidden'); } else { copyNameBtn.classList.add('gyp-hidden'); copyUrlBtn.classList.add('gyp-hidden'); aria2SelectedBtn.classList.add('gyp-hidden'); } updateSelectAllState(); } function copySelectedUrls() { const modal = document.getElementById('gyp-modal-overlay'); const tbody = modal.querySelector('#gyp-result-tbody'); const checkboxes = tbody.querySelectorAll('.gyp-row-checkbox:checked'); const urls = []; checkboxes.forEach(function(cb) { const url = cb.getAttribute('data-url'); if (url) urls.push(url); }); if (urls.length > 0) { copyToClipboard(urls.join('\n')); } else { showToast('没有可复制的URL', 2000, 'warning'); } } function copySelectedNames() { const modal = document.getElementById('gyp-modal-overlay'); const tbody = modal.querySelector('#gyp-result-tbody'); const rows = tbody.querySelectorAll('.gyp-row-checkbox:checked'); const names = []; rows.forEach(function(cb) { const tr = cb.closest('tr'); if (tr) { const nameCell = tr.querySelector('.gyp-col-name'); if (nameCell) { const name = nameCell.textContent.trim(); if (name) names.push(name); } } }); if (names.length > 0) { copyToClipboard(names.join('\n')); } else { showToast('没有可复制的文件名', 2000, 'warning'); } } function deselectAll() { const modal = document.getElementById('gyp-modal-overlay'); const tbody = modal.querySelector('#gyp-result-tbody'); const checkboxes = tbody.querySelectorAll('.gyp-row-checkbox'); checkboxes.forEach(function(cb) { cb.checked = !cb.checked; }); updateSelectedBar(); } function closeModal() { cancelFetch(); const modal = document.getElementById('gyp-modal-overlay'); if (modal) { modal.style.display = 'none'; } } function cancelFetch() { if (abortController) { abortController.abort(); abortController = null; } } function showToast(message, duration, type) { duration = duration || 2000; type = type || 'success'; let toast = document.getElementById('gyp-toast'); if (!toast) { toast = document.createElement('div'); toast.id = 'gyp-toast'; toast.innerHTML = ''; document.body.appendChild(toast); } if (toast._timeoutId) { clearTimeout(toast._timeoutId); toast._timeoutId = null; } toast.querySelector('.gyp-toast-msg').textContent = message; toast.className = 'gyp-toast gyp-toast-show gyp-toast-' + type; toast._timeoutId = setTimeout(function() { toast.className = 'gyp-toast'; toast._timeoutId = null; }, duration); } function hideToast() { const toast = document.getElementById('gyp-toast'); if (toast) { if (toast._timeoutId) { clearTimeout(toast._timeoutId); toast._timeoutId = null; } toast.className = 'gyp-toast'; } } function showModal() { createModal(); const modal = document.getElementById('gyp-modal-overlay'); modal.style.display = 'flex'; resetModalState(); } function resetModalState() { document.getElementById('gyp-progress-text').textContent = '准备就绪'; document.getElementById('gyp-progress-percent').textContent = '0%'; document.getElementById('gyp-progress-fill').style.width = '0%'; document.getElementById('gyp-result-tbody').innerHTML = ''; const selectAll = document.getElementById('gyp-select-all'); selectAll.checked = false; selectAll.indeterminate = false; const errorInfo = document.getElementById('gyp-error-info'); errorInfo.innerHTML = ''; errorInfo.style.display = 'none'; // 重置复制按钮为隐藏状态 document.getElementById('gyp-copy-selected-name').classList.add('gyp-hidden'); document.getElementById('gyp-copy-selected').classList.add('gyp-hidden'); document.getElementById('gyp-aria2-send-selected').classList.add('gyp-hidden'); document.getElementById('gyp-selected-count').textContent = '已选择 0 项'; } function updateProgress(current, total, message) { const percent = Math.round((current / total) * 100); document.getElementById('gyp-progress-text').textContent = message || '正在获取: ' + current + '/' + total; document.getElementById('gyp-progress-percent').textContent = percent + '%'; document.getElementById('gyp-progress-fill').style.width = percent + '%'; } function addResultRow(fileName, url, size, error) { const tbody = document.getElementById('gyp-result-tbody'); const tr = document.createElement('tr'); if (error) tr.className = 'gyp-row-error'; const selectTd = document.createElement('td'); selectTd.className = 'gyp-col-select'; const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.className = 'gyp-row-checkbox'; checkbox.setAttribute('data-url', url || ''); checkbox.setAttribute('data-size', size || 0); checkbox.onclick = updateSelectedBar; selectTd.appendChild(checkbox); const nameTd = document.createElement('td'); nameTd.className = 'gyp-col-name'; nameTd.textContent = fileName; nameTd.title = fileName; const urlTd = document.createElement('td'); urlTd.className = 'gyp-col-url'; if (error) { urlTd.textContent = '获取失败: ' + error; urlTd.style.color = '#dc3545'; urlTd.title = '获取失败: ' + error; } else { const urlLink = document.createElement('a'); urlLink.href = url; urlLink.target = '_blank'; urlLink.textContent = url; urlTd.appendChild(urlLink); urlTd.title = url; } const sizeTd = document.createElement('td'); sizeTd.className = 'gyp-col-size'; sizeTd.textContent = formatSize(size); sizeTd.title = size ? size + ' 字节' : ''; const actionTd = document.createElement('td'); actionTd.className = 'gyp-col-action'; if (!error) { const copyNameBtn = document.createElement('button'); copyNameBtn.className = 'gyp-btn gyp-btn-sm'; copyNameBtn.textContent = '复制文件名'; copyNameBtn.onclick = function() { copyToClipboard(fileName); }; actionTd.appendChild(copyNameBtn); const copyUrlBtn = document.createElement('button'); copyUrlBtn.className = 'gyp-btn gyp-btn-sm'; copyUrlBtn.textContent = '复制直链'; copyUrlBtn.style.marginLeft = '6px'; copyUrlBtn.onclick = function() { copyToClipboard(url); }; actionTd.appendChild(copyUrlBtn); } tr.appendChild(selectTd); tr.appendChild(nameTd); tr.appendChild(urlTd); tr.appendChild(sizeTd); tr.appendChild(actionTd); tbody.appendChild(tr); } function copyToClipboard(text) { if (!text || text.trim() === '') { showToast('没有内容可复制', 2000, 'warning'); return; } navigator.clipboard.writeText(text).then(function() { showToast('已复制到剪贴板'); }, function(err) { console.error('复制失败:', err); showToast('复制失败,请手动复制', 3000, 'error'); }); } function copyAllUrls() { const tbody = document.getElementById('gyp-result-tbody'); const rows = tbody.querySelectorAll('tr'); const urls = []; for (let i = 0; i < rows.length; i++) { const urlCell = rows[i].querySelector('.gyp-col-url a'); if (urlCell && urlCell.textContent) { urls.push(urlCell.textContent); } } if (urls.length > 0) { copyToClipboard(urls.join('\n')); } else { showToast('没有可复制的链接', 2000, 'warning'); } } function gmFetchWithTimeout(url, options, timeout, signal) { return new Promise(function(resolve, reject) { const timeoutId = setTimeout(function() { reject(new Error('请求超时')); }, timeout); if (signal && signal.aborted) { clearTimeout(timeoutId); reject(new Error('请求已取消')); return; } const controller = new AbortController(); const fetchSignal = signal ? signal : controller.signal; fetch(url, { ...options, signal: fetchSignal }).then(function(response) { clearTimeout(timeoutId); resolve(response); }, function(error) { clearTimeout(timeoutId); if (error.name === 'AbortError') { reject(new Error('请求已取消')); } else { reject(error); } }); }); } async function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } async function getDownloadUrl(fileId, fileSize, signal) { const authHeader = getAuthHeader(); if (!authHeader) { throw new Error('未登录或Token不存在'); } if (signal && signal.aborted) { throw new Error('请求已取消'); } let lastError; for (let attempt = 0; attempt < MAX_RETRIES; attempt++) { if (signal && signal.aborted) { throw new Error('请求已取消'); } try { const response = await gmFetchWithTimeout(API_URL, { method: 'POST', headers: { 'Accept': 'application/json, text/plain, */*', 'Authorization': authHeader, 'Content-Type': 'application/json' }, body: JSON.stringify({ fileId: fileId }) }, FETCH_TIMEOUT, signal); if (!response.ok) { throw new Error('请求失败: ' + response.status); } const data = await response.json(); if (data.msg === 'success' && data.data && data.data.signedURL) { return { url: data.data.signedURL, size: fileSize || 0 }; } else { throw new Error(data.msg || '获取直链失败'); } } catch (err) { lastError = err; if (err.message === '请求已取消') { throw err; } if (attempt < MAX_RETRIES - 1) { const delay = RETRY_BASE_DELAY * Math.pow(2, attempt); console.log(`GYP: Retry ${attempt + 1}/${MAX_RETRIES} after ${delay}ms:`, err.message); await sleep(delay); } } } throw lastError || new Error('获取直链失败'); } function getSelectedFiles() { // 优先使用 map 中记录的选中文件(用户点击过的) if (selectedFilesMap.size > 0) { return getSelectedFilesFromMap(); } // 尝试从 React 状态获取选中项 const marker = getSelectedFileIdsFromFramework(); if (marker.ids.size > 0) { const files = []; marker.ids.forEach(id => { const idStr = String(id); let name = null; let isDir = false; let size = 0; // 优先从 filesMap 获取文件名、类型和大小(React 的 dataSource) if (marker.filesMap && marker.filesMap.has(idStr)) { const fileData = marker.filesMap.get(idStr); name = fileData.fileName; isDir = fileData.isDir; size = fileData.size || 0; } // 否则从 names 中找 if (!name) { name = Array.from(marker.names).find(n => n) || null; } // 最后 fallback if (!name) { name = '文件_' + idStr; } // 跳过文件夹 if (isDir) { return; } files.push({ id: idStr, name, size }); }); return files; } // Fallback: 从当前 DOM 获取 const rows = document.querySelectorAll('.ant-table-row-selected'); const files = []; rows.forEach(row => { const fileId = row.getAttribute('data-row-key'); if (!fileId) return; // 检查是否是文件夹(通过图标判断) const folderIcon = row.querySelector('.swangpan-icon-typefolder, [class*="folder"]'); if (folderIcon) { return; } const nameDiv = row.querySelector('.ant-table-cell:nth-child(2) [title]') || row.querySelector('.ant-table-cell:nth-child(2)'); const name = nameDiv ? (nameDiv.getAttribute('title') || nameDiv.textContent) : ('文件_' + fileId); files.push({ id: fileId, name: name.trim() }); }); return files; } async function fetchWithConcurrency(files) { let completed = 0; const errors = []; let aborted = false; abortController = new AbortController(); const signal = abortController.signal; async function processFile(file) { if (signal.aborted) { return { file: file, url: null, size: 0, error: '已取消' }; } try { const result = await getDownloadUrl(file.id, file.size || 0, signal); return { file: file, url: result.url, size: file.size || 0, error: null }; } catch (err) { if (err.name === 'AbortError' || err.message === '请求已取消') { return { file: file, url: null, size: 0, error: '已取消' }; } return { file: file, url: null, size: 0, error: err.message }; } } try { for (let i = 0; i < files.length; i += CONCURRENCY) { if (signal.aborted) { aborted = true; break; } const batch = files.slice(i, i + CONCURRENCY); const batchPromises = batch.map(processFile); const batchResults = await Promise.all(batchPromises); for (let j = 0; j < batchResults.length; j++) { const result = batchResults[j]; completed++; updateProgress(completed, files.length, '正在获取: ' + completed + '/' + files.length); addResultRow(result.file.name, result.url, result.size, result.error); if (result.error) { errors.push({ name: result.file.name, error: result.error }); } } if (i + CONCURRENCY < files.length) { await sleep(BATCH_DELAY); } } } catch (err) { console.error('GYP: Fetch error:', err); } finally { abortController = null; } return { errors: errors, aborted: aborted }; } async function startFetch() { // 从 map 或 DOM 获取选中的文件 const files = getSelectedFiles(); if (files.length === 0) { showToast('请先选择要获取直链的文件', 2000, 'warning'); return; } showModal(); updateProgress(0, files.length, '开始获取: 0/' + files.length); const result = await fetchWithConcurrency(files); const total = files.length; const successCount = total - result.errors.length; const failCount = result.errors.length; // 用户取消时单独处理 if (result.aborted) { document.getElementById('gyp-progress-text').textContent = '已取消获取'; document.getElementById('gyp-progress-fill').style.width = '100%'; showToast('用户取消获取', 3000, 'warning'); return; } document.getElementById('gyp-progress-text').textContent = '获取完成:成功 ' + successCount + ' 个,失败 ' + failCount + ' 个'; document.getElementById('gyp-progress-fill').style.width = '100%'; if (result.errors.length > 0) { const errorDiv = document.getElementById('gyp-error-info'); errorDiv.innerHTML = '失败文件 (' + result.errors.length + '个):
' + result.errors.map(function(e) { return e.name + ': ' + e.error; }).join('
'); errorDiv.style.display = 'block'; } if (failCount === 0) { showToast('全部获取成功!'); } else { showToast('获取完成,' + failCount + ' 个失败', 3000, 'warning'); } } function removeButton() { const btn = document.querySelector('.gyp-script-btn'); if (btn) { btn.remove(); } } function addButton() { // 只在 /home/ 开头的路由下添加按钮 if (!window.location.hash.startsWith('#/home/')) { removeButton(); return; } // 如果按钮已存在,先移除再重新添加(可能需要重新定位) removeButton(); const uploadBtn = findUploadButton(); if (!uploadBtn) { return; } const btnContainer = uploadBtn.parentNode; if (!btnContainer) return; const btn = document.createElement('button'); btn.className = uploadBtn.className + ' gyp-script-btn'; btn.textContent = '获取直链'; btn.style.marginLeft = '10px'; btn.style.background = 'linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%)'; btn.style.border = 'none'; btn.style.borderRadius = '6px'; btn.style.color = '#fff'; btn.style.fontWeight = 'bold'; btn.style.padding = '8px 16px'; btn.style.boxShadow = '0 4px 15px rgba(102, 126, 234, 0.4), inset 0 1px 0 rgba(255,255,255,0.3)'; btn.style.textShadow = '0 1px 2px rgba(0,0,0,0.2)'; btn.style.transition = 'all 0.3s ease'; btn.style.cursor = 'pointer'; btn.onclick = startFetch; btnContainer.insertBefore(btn, uploadBtn.nextSibling); } function addStyles() { if (document.getElementById('gyp-styles')) return; const style = document.createElement('style'); style.id = 'gyp-styles'; style.textContent = [ '#gyp-modal-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); z-index: 999999; justify-content: center; align-items: center; overflow: auto; }', '.gyp-modal-v2 { background: #fff; border-radius: 8px; width: 900px !important; min-width: 900px !important; max-height: 85vh; display: flex; flex-direction: column; box-shadow: 0 4px 24px rgba(0, 0, 0, 0.2); overflow: hidden; user-select: text; -webkit-user-select: text; flex-shrink: 0; }', '.gyp-modal-header { display: flex; justify-content: space-between; align-items: center; padding: 16px 20px; border-bottom: 1px solid #e8e8e8; flex-shrink: 0; }', '.gyp-modal-title { font-size: 16px; font-weight: 500; color: #333; }', '.gyp-modal-header-actions { display: flex; align-items: center; gap: 8px; }', '.gyp-settings-btn { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border: none; border-radius: 6px; font-size: 13px; font-weight: 600; cursor: pointer; color: #fff; padding: 6px 14px; transition: all 0.3s ease; box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3); }', '.gyp-settings-btn:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4); }', '.gyp-modal-close { background: none; border: none; font-size: 24px; cursor: pointer; color: #999; padding: 0; line-height: 1; }', '.gyp-modal-close:hover { color: #666; }', '.gyp-modal-body { padding: 20px; flex: 1; min-height: 0; overflow: hidden; display: flex; flex-direction: column; }', '.gyp-progress-wrapper { margin-bottom: 16px; flex-shrink: 0; }', '.gyp-progress-info { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; font-size: 14px; font-weight: 500; }', '.gyp-progress-text { color: #333; }', '.gyp-progress-percent { color: #667eea; font-weight: 600; font-size: 14px; text-shadow: 0 1px 2px rgba(102, 126, 234, 0.3); }', '.gyp-progress-bar { height: 10px; background: linear-gradient(180deg, #e8eaf6 0%, #f5f5f5 100%); border-radius: 10px; overflow: hidden; position: relative; box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.8); }', '.gyp-progress-fill { height: 100%; background: linear-gradient(90deg, #667eea 0%, #764ba2 50%, #f093fb 100%); border-radius: 10px; transition: width 0.4s ease; position: relative; box-shadow: 0 0 10px rgba(102, 126, 234, 0.5), 0 2px 4px rgba(0, 0, 0, 0.1); }', '.gyp-progress-fill::after { content: ""; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.3) 50%, transparent 100%); animation: gyp-shine 2s infinite; }', '.gyp-progress-glow { position: absolute; top: 50%; left: 0; transform: translateY(-50%); height: 20px; width: 60px; background: radial-gradient(ellipse at center, rgba(240, 147, 251, 0.4) 0%, transparent 70%); pointer-events: none; }', '@keyframes gyp-shine { 0% { transform: translateX(-100%); } 100% { transform: translateX(200%); } }', '.gyp-result-table { border: 1px solid #e8e8e8; border-radius: 4px; display: flex; flex-direction: column; flex: 1; min-height: 0; overflow: hidden; min-width: 850px; }', '.gyp-table-head { width: 100%; border-collapse: separate; border-spacing: 0; flex-shrink: 0; table-layout: fixed; }', '.gyp-table-head td { background: linear-gradient(180deg, #f0f4ff 0%, #e8edff 100%); padding: 8px 12px; text-align: center; font-weight: 600; font-size: 13px; color: #5a67d8; border-bottom: none; box-shadow: inset 0 2px 4px rgba(255,255,255,0.8), inset 0 -1px 2px rgba(99, 102, 241, 0.03), 0 2px 4px rgba(99, 102, 241, 0.08); text-shadow: 0 1px 2px rgba(255,255,255,0.8); letter-spacing: 1px; }', '.gyp-col-select { width: 40px; text-align: center; }', '.gyp-col-name { width: 30%; text-align: center; max-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }', '.gyp-col-url { width: 35%; text-align: center; max-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }', '.gyp-col-size { width: 13%; text-align: center; color: #666; font-size: 13px; }', '.gyp-col-action { width: 22%; text-align: center; white-space: nowrap; }', '.gyp-table-body { overflow-y: auto; flex: 1; min-height: 0; }', '.gyp-table-content { width: 100%; border-collapse: collapse; table-layout: fixed; }', '.gyp-table-content td { padding: 12px 12px; font-size: 13px; color: #666; border-bottom: 1px solid #e8e8e8; text-align: center; background-color: #fff; }', '.gyp-table-content tr:last-child td { border-bottom: none; }', '.gyp-table-content tr.gyp-row-error { background-color: #fff2f0; }', '.gyp-table-content tr:hover { background-color: #f5f5f5; }', '.gyp-cell-name { max-width: 180px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }', '.gyp-cell-select { text-align: center; }', '.gyp-cell-url { max-width: 320px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }', '.gyp-col-url a { color: #1890ff; text-decoration: none; }', '.gyp-col-url a:hover { text-decoration: underline; }', '.gyp-cell-action { white-space: nowrap; }', '.gyp-row-checkbox { width: 16px; height: 16px; cursor: pointer; accent-color: #1890ff; }', '.gyp-select-all { width: 16px; height: 16px; cursor: pointer; accent-color: #1890ff; }', '.gyp-error-info { margin-top: 12px; padding: 12px; background-color: #fff2f0; border: 1px solid #ffccc7; border-radius: 4px; font-size: 13px; color: #dc3545; display: none; flex-shrink: 0; max-height: 100px; overflow-y: auto; }', '.gyp-selected-bar { display: flex; justify-content: space-between; align-items: center; padding: 16px 16px 20px 16px; background: linear-gradient(180deg, #f8f9ff 0%, #eef1fa 100%); border-top: 1px solid #d4d8f0; flex-shrink: 0; margin-top: 8px; }', '.gyp-selected-left { display: flex; align-items: center; gap: 12px; }', '.gyp-selected-right { display: flex; align-items: center; gap: 12px; }', // 反选按钮 - 极光绿渐变 '.gyp-selected-bar #gyp-deselect-selected { background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%); border: none; color: #fff; box-shadow: 0 2px 8px rgba(56, 239, 125, 0.3); transition: all 0.3s ease; }', '.gyp-selected-bar #gyp-deselect-selected:hover { background: linear-gradient(135deg, #15b3a6 0%, #4ff88f 100%); box-shadow: 0 4px 12px rgba(56, 239, 125, 0.4); transform: translateY(-1px); }', // 复制文件名按钮 - 科技蓝渐变风格 '.gyp-selected-bar #gyp-copy-selected-name { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border: none; color: #fff; box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3); transition: all 0.3s ease; }', '.gyp-selected-bar #gyp-copy-selected-name:hover { background: linear-gradient(135deg, #7b8ff0 0%, #8a5cb8 100%); box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4); transform: translateY(-1px); }', // 复制直链按钮 - 活力橙渐变风格 '.gyp-selected-bar #gyp-copy-selected { background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); border: none; color: #fff; box-shadow: 0 2px 8px rgba(245, 87, 108, 0.3); transition: all 0.3s ease; }', '.gyp-selected-bar #gyp-copy-selected:hover { background: linear-gradient(135deg, #f2a3fc 0%, #f76d84 100%); box-shadow: 0 4px 12px rgba(245, 87, 108, 0.4); transform: translateY(-1px); }', // 一键复制全部链接 - 阳光黄渐变 '.gyp-selected-bar #gyp-copy-all { background: linear-gradient(135deg, #f5af19 0%, #f12711 100%); border: none; color: #fff; font-size: 13px; font-weight: 600; padding: 7px 16px; box-shadow: 0 4px 12px rgba(245, 39, 17, 0.4); transition: all 0.3s ease; }', '.gyp-selected-bar #gyp-copy-all:hover { background: linear-gradient(135deg, #f7c41f 0%, #f23921 100%); box-shadow: 0 6px 16px rgba(245, 39, 17, 0.5); transform: translateY(-2px); }', // 关闭按钮 - 沉稳灰蓝渐变 '.gyp-selected-bar #gyp-modal-close-btn { background: linear-gradient(135deg, #4b6cb7 0%, #182848 100%); border: none; color: #fff; font-size: 13px; font-weight: 600; padding: 7px 16px; box-shadow: 0 4px 12px rgba(24, 40, 72, 0.4); transition: all 0.3s ease; }', '.gyp-selected-bar #gyp-modal-close-btn:hover { background: linear-gradient(135deg, #5b7cc7 0%, #283858 100%); box-shadow: 0 6px 16px rgba(24, 40, 72, 0.5); transform: translateY(-2px); }', '.gyp-btn { display: inline-flex; align-items: center; padding: 8px 20px; font-size: 14px; border-radius: 4px; cursor: pointer; border: 1px solid #d9d9d9; background: #fff; color: #333; transition: all 0.2s ease; }', '.gyp-hidden { display: none !important; }', '.gyp-btn:hover { color: #1890ff; border-color: #1890ff; }', '.gyp-btn-primary { background: #1890ff; border-color: #1890ff; color: #fff; }', '.gyp-btn-primary:hover { background: #40a9ff; border-color: #40a9ff; color: #fff; }', '.gyp-btn-danger { background: #ff4d4f; border-color: #ff4d4f; color: #fff; }', '.gyp-btn-danger:hover { background: #ff7875; border-color: #ff7875; color: #fff; }', '.gyp-btn-sm { padding: 4px 10px; font-size: 12px; cursor: pointer; }', '.gyp-script-btn:hover { background: linear-gradient(135deg, #5a67d8 0%, #6b46c1 50%, #e879f9 100%) !important; box-shadow: 0 6px 20px rgba(102, 126, 234, 0.5), inset 0 1px 0 rgba(255,255,255,0.3) !important; transform: translateY(-1px); }', '.gyp-toast { position: fixed; top: 80px; left: 50%; transform: translateX(-50%); z-index: 1000000; background: rgba(0, 0, 0, 0.75); color: #fff; padding: 12px 24px; border-radius: 6px; font-size: 14px; opacity: 0; transition: opacity 0.3s ease; pointer-events: none; }', '.gyp-toast.gyp-toast-show { opacity: 1; }', '.gyp-toast.gyp-toast-warning { background: rgba(250, 173, 20, 0.95); }', '.gyp-toast.gyp-toast-error { background: rgba(255, 77, 79, 0.95); }', '.gyp-toast.gyp-toast-success { background: rgba(34, 197, 94, 0.95); }', // Aria2 按钮样式 '.gyp-btn-aria2 { background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%) !important; border: none !important; color: #fff !important; font-size: 13px !important; font-weight: 600 !important; padding: 7px 16px !important; box-shadow: 0 4px 12px rgba(56, 239, 125, 0.4); }', '.gyp-btn-aria2:hover { background: linear-gradient(135deg, #15b3a6 0%, #4ff88f 100%) !important; box-shadow: 0 6px 16px rgba(56, 239, 125, 0.5); transform: translateY(-1px); }', '.gyp-btn-aria2-all { background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%) !important; border: none !important; color: #fff !important; font-size: 13px !important; font-weight: 600 !important; padding: 7px 16px !important; box-shadow: 0 4px 12px rgba(245, 87, 108, 0.4); }', '.gyp-btn-aria2-all:hover { background: linear-gradient(135deg, #f2a3fc 0%, #f76d84 100%) !important; box-shadow: 0 6px 16px rgba(245, 87, 108, 0.5); transform: translateY(-1px); }', // Aria2 配置弹窗样式 '#gyp-aria2-modal-overlay { display: flex; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); z-index: 1000000; justify-content: center; align-items: center; }', '.gyp-aria2-modal { background: #fff; border-radius: 12px; width: 480px; max-width: 95%; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); overflow: hidden; user-select: text; -webkit-user-select: text; }', '.gyp-aria2-modal-header { display: flex; justify-content: space-between; align-items: center; padding: 16px 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: #fff; user-select: none; -webkit-user-select: none; }', '.gyp-aria2-modal-header span { font-size: 16px; font-weight: 600; }', '.gyp-aria2-modal-close { background: none; border: none; font-size: 24px; cursor: pointer; color: rgba(255,255,255,0.8); padding: 0; line-height: 1; }', '.gyp-aria2-modal-close:hover { color: #fff; }', '.gyp-aria2-modal-body { padding: 24px; user-select: text; -webkit-user-select: text; }', '.gyp-aria2-form-group { margin-bottom: 16px; }', '.gyp-aria2-form-group label { display: block; font-size: 14px; font-weight: 500; color: #333; margin-bottom: 8px; }', '.gyp-aria2-form-group input { width: 100%; padding: 12px 16px; font-size: 14px; border: 2px solid #e8e8e8; border-radius: 8px; outline: none; transition: border-color 0.2s; box-sizing: border-box; background-color: #fff !important; color: #333 !important; -webkit-user-select: text !important; -moz-user-select: text !important; -ms-user-select: text !important; user-select: text !important; -webkit-touch-callout: default !important; -khtml-user-select: text !important; }', '.gyp-aria2-form-group input::placeholder { color: #999 !important; user-select: none !important; }', '.gyp-aria2-form-group input:focus { border-color: #667eea; box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); background-color: #fff; }', '.gyp-aria2-modal-actions { display: flex; gap: 12px; margin-top: 24px; }', '.gyp-aria2-modal-actions .gyp-btn { flex: 1; padding: 12px 16px; font-size: 14px; }' ].join('\n'); document.head.appendChild(style); } function tryInit() { addStyles(); addButton(); } function init() { addStyles(); // 设置 checkbox 监听器 setupCheckboxListener(); const scheduleInit = () => { tryInit(); setTimeout(tryInit, 500); setTimeout(tryInit, 1500); setTimeout(tryInit, 3000); }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', scheduleInit); } else { scheduleInit(); } // 使用 MutationObserver 监听页面变化,检测上传按钮 const observer = new MutationObserver(() => { if (window.location.hash.startsWith('#/home/')) { const uploadBtn = findUploadButton(); if (uploadBtn && !document.querySelector('.gyp-script-btn')) { tryInit(); } } // 定期清理过期的选中记录(带节流) const now = Date.now(); if (now - lastCleanTime > CLEAN_INTERVAL) { lastCleanTime = now; cleanExpiredSelections(); } }); observer.observe(document.body || document.documentElement, { childList: true, subtree: true }); // 监听 URL 变化(SPA 页面切换) window.addEventListener('hashchange', () => { // 页面切换后直接尝试添加,不重置 flag setTimeout(tryInit, 500); setTimeout(tryInit, 1500); }); } init(); })();