// ==UserScript== // @name 云盘批量重命名助手 | 支持123云盘、夸克网盘、光鸭云盘 // @name:en CloudDriveFastRename // @namespace meguoe // @version 1.0.2 // @description 云盘批量重命名助手,支持123云盘、夸克网盘、光鸭云盘,支持按序号、追加、查找替换、正则替换、格式替换等多种重命名模式,提供拖拽排序、实时预览、过滤视频/图片等功能 // @author meguoe@163.com // @license Apache-2.0 // @match *://*.123pan.com/* // @match *://*.123pan.cn/* // @match *://*.123684.com/* // @match *://*.123865.com/* // @match https://pan.quark.cn/* // @match https://drive.quark.cn/* // @match https://guangyapan.com/* // @match https://*.guangyapan.com/* // @icon https://img.remit.ee/api/file/BQACAgUAAyEGAASHRsPbAAETtahp8FnWo7fqBhJAv2tAUhxcgDrc2QACQSEAAgYIiVcf7ydSVVOwYDsE.png // @grant GM_addStyle // @grant GM_xmlhttpRequest // @connect * // @run-at document-idle // @tag 123 夸克 光鸭 云盘 网盘 123云盘 夸克网盘 光鸭云盘 批量重命名助手 CloudDriveFastRename // @noframes // ==/UserScript== (function () { 'use strict'; const DEBUG = false; const LOG_TAG = '%c[CloudDriveFastRename]%c'; const LOG_STYLE = 'color:#e74c3c;font-weight:bold'; const LOG_SUB_STYLE = 'color:#3498db'; function log(...args) { if (DEBUG) console.log(LOG_TAG, LOG_STYLE, '', LOG_SUB_STYLE, ...args); } function debounce(fn, delay = 300) { let timer; return (...args) => { clearTimeout(timer); timer = setTimeout(() => fn.apply(this, args), delay); }; } function waitForElement(selector, timeout = 10000) { return new Promise((resolve, reject) => { const el = document.querySelector(selector); if (el) { log(`[Utils] 元素已存在: ${selector}`); return resolve(el); } log(`[Utils] 等待元素出现: ${selector}`); const observer = new MutationObserver((_, obs) => { const el = document.querySelector(selector); if (el) { obs.disconnect(); log(`[Utils] 元素已出现: ${selector}`); resolve(el); } }); observer.observe(document.body, { childList: true, subtree: true }); setTimeout(() => { observer.disconnect(); reject(new Error(`Element "${selector}" not found within ${timeout}ms`)); }, timeout); }); } function detectPlatform() { const host = window.location.hostname; if (/123pan\.com|123pan\.cn|123684\.com|123865\.com/.test(host)) { return '123'; } if (/pan\.quark\.cn|drive\.quark\.cn/.test(host)) { return 'quark'; } if (/guangyapan\.com/.test(host)) { return 'guangya'; } return null; } const CSS_STYLES = ` .cfr-button-container { position: relative; display: none; } .cfr-modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center; z-index: 9999; } .cfr-modal-content { background: #fff; border-radius: 20px; width: 70vw; height: 80vh; display: flex; font-size: 14px; flex-direction: column; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); } .cfr-modal-header { padding: 16px 20px; border-bottom: 1px solid #e8e8e8; display: flex; justify-content: space-between; align-items: center; } .cfr-modal-title-container { display: flex; flex-direction: row; align-items: baseline; gap: 12px; } .cfr-modal-title { margin: 0; font-size: 16px; font-weight: 500; color: #333; } .cfr-modal-subtitle { margin: 0; font-size: 12px; color: #999; } .cfr-modal-body { padding: 20px; overflow-y: auto; flex: 1; } .cfr-modal-footer { padding: 16px 20px; border-top: 1px solid #e8e8e8; display: flex; justify-content: flex-end; gap: 12px; } .cfr-modal-header-right { margin-left: auto; display: flex; align-items: center; } .cfr-button-container-inner { display: flex; gap: 12px; align-items: center; } .cfr-file-list { display: flex; flex-direction: column; gap: 8px; max-width: 100%; width: 100%; overflow-x: hidden; } .cfr-file-item { display: flex; align-items: center; padding: 12px; background: #f5f5f5; border-radius: 8px; transition: background 0.2s, transform 0.2s; cursor: move; gap: 12px; border: 1px solid #d7d7d7; } .cfr-file-item:hover { background: #e8e8e8; } .cfr-file-item.dragging { opacity: 0.5; transform: scale(0.98); } .cfr-file-index { color: rgb(51, 51, 51); min-width: 30px; font-size: 14px; } .cfr-file-name { flex: 1; color: #333; word-break: break-all; } .cfr-file-extra { color: #999; font-size: 12px; min-width: 50px; text-align: center; } .cfr-file-delete-btn { padding: 2px; width: 16px; height: 16px; border: none; border-radius: 2px; background: #ff4d4f; color: #fff; cursor: pointer; font-size: 16px; display: flex; align-items: center; justify-content: center; transition: background 0.2s; } .cfr-file-delete-btn:hover { background: #f44336; } .cfr-drag-handle { color: #999; cursor: move; font-size: 16px; user-select: none; } .cfr-file-item-rename { display: flex; align-items: center; justify-content: center; gap: 12px; max-width: 100%; width: 100%; } .cfr-file-item-rename .cfr-file-name-original { flex: 1; color: #999; font-size: 14px; display: flex; align-items: center; gap: 8px; padding: 12px; background: #f5f5f5; border-radius: 8px; overflow: hidden; border: 1px solid #d7d7d7; } .cfr-file-item-rename .cfr-file-name-original span:last-child { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .cfr-file-item-rename .cfr-file-index { color: #666; min-width: 30px; font-size: 14px; flex-shrink: 0; } .cfr-file-item-rename .cfr-arrow-icon { flex-shrink: 0; display: flex; align-items: center; justify-content: center; width: 24px; height: 24px; color: #f44336; font-size: 18px; font-weight: 500; font-family: system-ui, -apple-system, sans-serif; } .cfr-file-item-rename .cfr-file-name-new { flex: 1; color: #333; font-size: 14px; padding: 12px; background: #f5f5f5; border-radius: 8px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; border: 1px solid #d7d7d7; } .cfr-stats-container { display: flex; align-items: center; gap: 16px; padding: 0; font-size: 13px; color: #666; } .cfr-stats-item { display: flex; align-items: center; } .cfr-stats-item strong { color: #2961D9; margin: 0 2px; } .cfr-modal-footer-content { display: flex; align-items: center; justify-content: space-between; width: 100%; gap: 16px; } .cfr-footer-buttons-container { display: flex; align-items: center; gap: 8px; } .cfr-btn { padding: 4px 15px; height: 32px; font-size: 14px; border-radius: 6px; cursor: pointer; border: 1px solid #d9d9d9; background: #fff; color: rgba(0, 0, 0, 0.88); transition: all 0.2s; } .cfr-btn:hover { color: #2961D9; border-color: #2961D9; } .cfr-btn-primary { padding: 4px 15px; height: 32px; font-size: 14px; border-radius: 6px; cursor: pointer; border: 1px solid #2961D9; background: #2961D9; color: #fff; transition: all 0.2s; } .cfr-btn-primary:hover { background: #1d4bbf; border-color: #1d4bbf; } .cfr-btn-primary:disabled { opacity: 0.6; cursor: not-allowed; } .cfr-toggle-button { padding: 6px 12px; background: #fff; border: 1px solid #d9d9d9; border-radius: 6px; cursor: pointer; font-size: 13px; color: #666; transition: all 0.2s; white-space: nowrap; } .cfr-toggle-button:hover { color: #2961D9; border-color: #2961D9; } .cfr-toggle-button.cfr-toggle-button-active { background: #2961D9; border-color: #2961D9; color: #fff; } .cfr-toggle-button.cfr-toggle-button-active:hover { background: #1d4bbf; border-color: #1d4bbf; } .cfr-checkbox-button { padding: 6px 10px; background: #fff; border: 1px solid #d9d9d9; border-radius: 8px; cursor: pointer; font-size: 12px; color: #666; transition: all 0.2s; display: flex; align-items: center; gap: 8px; } .cfr-checkbox-button:hover, .cfr-checkbox-button[data-checked="true"] { border-color: #2961D9; color: #2961D9; } .cfr-checkbox-input { cursor: pointer; pointer-events: none; } .cfr-checkbox-text { padding: 0; font-size: 13px; font-weight: 500; margin-left: 6px; color: #919191; } .cfr-checkbox-button[data-checked="true"] .cfr-checkbox-text { color: #2961D9; } .cfr-tab-container { display: flex; gap: 4px; background: #f5f5f5; padding: 4px; border-radius: 8px; } .cfr-tab-item { padding: 6px 12px; font-size: 12px; color: #666; cursor: pointer; border-radius: 6px; transition: all 0.2s; user-select: none; } .cfr-tab-item:hover { color: #2961D9; } .cfr-tab-item.active { background: #fff; color: #2961D9; font-weight: 500; } .cfr-rename-config { padding: 16px; background: #f5f5f5; border-radius: 8px; margin-bottom: 12px; border: 1px solid #d7d7d7; } .cfr-rename-inputs-container { display: flex; gap: 12px; align-items: center; } .cfr-rename-config-input { flex: 1; padding: 8px 12px; border: 1px solid #d9d9d9; border-radius: 8px; font-size: 14px; outline: none; transition: border-color 0.2s; } .cfr-rename-config-input:focus { border-color: #2961D9; } `; if (typeof GM_addStyle === 'function') { GM_addStyle(CSS_STYLES); } else { const styleElement = document.createElement('style'); styleElement.textContent = CSS_STYLES; document.head.appendChild(styleElement); } const TOAST_CSS = ` .cfr-toast-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.15); z-index: 99998; animation: cfr-fade-in 0.2s ease; } .cfr-toast-overlay.cfr-toast-out { animation: cfr-fade-out 0.2s ease forwards; } @keyframes cfr-fade-in { from { opacity: 0; } to { opacity: 1; } } @keyframes cfr-fade-out { from { opacity: 1; } to { opacity: 0; } } .cfr-toast-container { position: fixed; top: 20px; right: 20px; z-index: 99999; display: flex; flex-direction: column; gap: 8px; pointer-events: none; } .cfr-toast-container.cfr-toast-center { top: 0; left: 0; right: 0; bottom: 0; justify-content: center; align-items: center; } .cfr-toast { padding: 10px 16px; border-radius: 8px; font-size: 13px; color: #fff; background: rgba(0, 0, 0, 0.75); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); animation: cfr-toast-in 0.3s ease; pointer-events: auto; max-width: 320px; display: flex; align-items: center; gap: 8px; } .cfr-toast.cfr-toast-out { animation: cfr-toast-out 0.3s ease forwards; } .cfr-toast-icon { font-size: 16px; flex-shrink: 0; } .cfr-toast-icon-loading { display: inline-block; width: 16px; height: 16px; border: 2px solid rgba(255,255,255,0.3); border-top-color: #fff; border-radius: 50%; animation: cfr-spin 0.6s linear infinite; } @keyframes cfr-spin { to { transform: rotate(360deg); } } .cfr-toast-body { flex: 1; display: flex; flex-direction: column; gap: 2px; } .cfr-toast-title { font-size: 13px; font-weight: 500; line-height: 1.4; } .cfr-toast-desc { font-size: 12px; color: rgba(255,255,255,0.7); line-height: 1.3; } .cfr-toast.cfr-toast-center-mode { flex-direction: column; align-items: center; gap: 14px; padding: 24px 32px; max-width: 220px; text-align: center; animation: cfr-toast-in-center 0.3s ease; } .cfr-toast.cfr-toast-center-mode.cfr-toast-out { animation: cfr-toast-out-center 0.3s ease forwards; } .cfr-toast-center-mode .cfr-toast-icon { font-size: 28px; } .cfr-toast-center-mode .cfr-toast-icon-loading { width: 28px; height: 28px; border-width: 3px; } .cfr-toast-center-mode .cfr-toast-desc { font-size: 14px; color: rgba(255,255,255,0.9); } @keyframes cfr-toast-in-center { from { opacity: 0; transform: scale(0.9); } to { opacity: 1; transform: scale(1); } } @keyframes cfr-toast-out-center { from { opacity: 1; transform: scale(1); } to { opacity: 0; transform: scale(0.9); } } @keyframes cfr-toast-in { from { opacity: 0; transform: translateX(40px); } to { opacity: 1; transform: translateX(0); } } @keyframes cfr-toast-out { from { opacity: 1; transform: translateX(0); } to { opacity: 0; transform: translateX(40px); } } `; if (typeof GM_addStyle === 'function') { GM_addStyle(TOAST_CSS); } else { const toastStyle = document.createElement('style'); toastStyle.textContent = TOAST_CSS; document.head.appendChild(toastStyle); } function escapeHtml(str) { const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } function showToast(title, description = '', duration = 3000, options = {}) { const isCenter = options.center || false; const container = document.createElement('div'); container.className = 'cfr-toast-container' + (isCenter ? ' cfr-toast-center' : ''); document.body.appendChild(container); let overlay = null; if (isCenter) { overlay = document.createElement('div'); overlay.className = 'cfr-toast-overlay'; document.body.appendChild(overlay); } const toast = document.createElement('div'); toast.className = 'cfr-toast' + (isCenter ? ' cfr-toast-center-mode' : ''); const icon = options.icon || 'ℹ️'; const minDuration = options.minDuration || 0; const descHtml = description ? `${escapeHtml(description)}` : ''; toast.innerHTML = `${icon}
${escapeHtml(title)}${descHtml}
`; container.appendChild(toast); const startTime = Date.now(); let timer = null; const dismiss = () => { if (timer) { clearTimeout(timer); timer = null; } const elapsed = Date.now() - startTime; const remaining = Math.max(0, minDuration - elapsed); const doDismiss = () => { toast.classList.add('cfr-toast-out'); toast.addEventListener('animationend', () => { toast.remove(); container.remove(); }); if (overlay) { overlay.classList.add('cfr-toast-out'); overlay.addEventListener('animationend', () => overlay.remove()); } }; if (remaining > 0) { timer = setTimeout(doDismiss, remaining); } else { doDismiss(); } }; if (duration > 0) { timer = setTimeout(dismiss, duration); } return dismiss; } class Modal { constructor(options = {}) { this.title = options.title || ''; this.subtitle = options.subtitle || ''; this.bodyContent = options.bodyContent || null; this.headerButtons = options.headerButtons || null; this.headerRight = options.headerRight || null; this.footerButtons = options.footerButtons || []; this.footerContent = options.footerContent || null; this.onClose = options.onClose || null; this.modal = null; } create() { const modal = document.createElement('div'); modal.className = 'cfr-modal-overlay'; const modalContent = document.createElement('div'); modalContent.className = 'cfr-modal-content'; const modalHeader = document.createElement('div'); modalHeader.className = 'cfr-modal-header'; const titleContainer = document.createElement('div'); titleContainer.className = 'cfr-modal-title-container'; const modalTitle = document.createElement('h3'); modalTitle.textContent = this.title; modalTitle.className = 'cfr-modal-title'; titleContainer.appendChild(modalTitle); if (this.subtitle) { const modalSubtitle = document.createElement('p'); modalSubtitle.textContent = this.subtitle; modalSubtitle.className = 'cfr-modal-subtitle'; titleContainer.appendChild(modalSubtitle); } modalHeader.appendChild(titleContainer); if (this.headerButtons) modalHeader.appendChild(this.headerButtons); if (this.headerRight) modalHeader.appendChild(this.headerRight); const modalBody = document.createElement('div'); modalBody.className = 'cfr-modal-body'; if (this.bodyContent) modalBody.appendChild(this.bodyContent); const modalFooter = document.createElement('div'); modalFooter.className = 'cfr-modal-footer'; if (this.footerContent) { modalFooter.appendChild(this.footerContent); } else { this.footerButtons.forEach(btn => modalFooter.appendChild(btn)); } modalContent.appendChild(modalHeader); modalContent.appendChild(modalBody); modalContent.appendChild(modalFooter); modal.appendChild(modalContent); modal.onclick = (e) => { e.stopPropagation(); }; this.modal = modal; return modal; } show() { if (!this.modal) this.create(); document.body.appendChild(this.modal); this._escHandler = (e) => { if (e.key === 'Escape') this.close(); }; document.addEventListener('keydown', this._escHandler); } close() { if (this.modal) { this.modal.remove(); this.modal = null; } if (this._escHandler) { document.removeEventListener('keydown', this._escHandler); this._escHandler = null; } if (this.onClose) this.onClose(); } } function createToggleButton(text, defaultActive = false, onChange = null) { const button = document.createElement('button'); button.className = 'cfr-toggle-button'; button.dataset.active = String(defaultActive); button.textContent = text; if (defaultActive) button.classList.add('cfr-toggle-button-active'); button.onclick = () => { const isActive = button.dataset.active === 'true'; const newState = !isActive; button.dataset.active = String(newState); button.classList.toggle('cfr-toggle-button-active', newState); if (onChange) onChange(newState); }; return button; } function updateFileIndices(fileList) { const items = fileList.querySelectorAll('.cfr-file-item'); let visibleIndex = 1; items.forEach(item => { if (item.style.display !== 'none') { const indexEl = item.querySelector('.cfr-file-index'); if (indexEl) indexEl.textContent = `${visibleIndex}.`; visibleIndex++; } }); } function updateStats(fileList, statsContainer) { const visibleItems = fileList.querySelectorAll('.cfr-file-item:not([style*="display: none"])'); statsContainer.innerHTML = `${visibleItems.length} 个文件`; } function updateRenameStats(statsContainer, totalFiles, successCount, failCount) { const skippedCount = Math.max(0, totalFiles - successCount - failCount); const totalCountSpan = document.createElement('span'); totalCountSpan.className = 'cfr-stats-item'; totalCountSpan.innerHTML = `共 ${totalFiles} 个文件`; const successSpan = document.createElement('span'); successSpan.className = 'cfr-stats-item'; successSpan.innerHTML = `成功 ${successCount}`; const failSpan = document.createElement('span'); failSpan.className = 'cfr-stats-item'; failSpan.innerHTML = `失败 ${failCount}`; statsContainer.innerHTML = ''; statsContainer.appendChild(totalCountSpan); statsContainer.appendChild(successSpan); statsContainer.appendChild(failSpan); if (skippedCount > 0) { const skippedSpan = document.createElement('span'); skippedSpan.className = 'cfr-stats-item'; skippedSpan.innerHTML = `跳过 ${skippedCount}`; statsContainer.appendChild(skippedSpan); } } function getOrderedFiles(fileList, originalFiles) { const fileItems = fileList.querySelectorAll('.cfr-file-item'); const ordered = []; const fileMap = new Map(originalFiles.map(f => [f.id, f])); fileItems.forEach(item => { const fileId = item.dataset.fileId; const isVisible = item.style.display !== 'none'; if (isVisible) { const file = fileMap.get(fileId); if (file) ordered.push(file); } }); return ordered; } class Platform123 { constructor() { this.name = '123'; this.CATEGORY_AUDIO = '1'; this.CATEGORY_VIDEO = '2'; this.CATEGORY_IMAGE = '3'; this.FILE_TYPE_FOLDER = 1; this.categoryMap = { 1: '音频', 2: '视频', 3: '图片' }; this.apiClient = null; this.selector = null; this.selectedFilesManager = null; } async _getParentFileId() { try { const homeFilePath = JSON.parse(sessionStorage['filePath'])['homeFilePath']; const parentFileId = (homeFilePath[homeFilePath.length - 1] || 0); return parentFileId.toString(); } catch (e) { log('[Platform123] 获取父级文件ID失败:', e); return '0'; } } getExtraColumnInfo(file) { return this.categoryMap[file.category] || '其他'; } initAPI() { this.apiClient = new class PanApiClient { constructor() { this.host = 'https://' + window.location.host; this.authToken = localStorage['authorToken'] || ''; this.loginUuid = localStorage['LoginUuid'] || ''; this.appVersion = '3'; this.referer = document.location.href; if (!this.authToken || !this.loginUuid) { log('[123API] 缺少认证信息,请先登录'); } } buildURL(path, queryParams) { const queryString = new URLSearchParams(queryParams || {}).toString(); return `${this.host}${path}?${queryString}`; } async sendRequest(method, path, queryParams, body) { const headers = { 'Content-Type': 'application/json;charset=UTF-8', 'Authorization': 'Bearer ' + this.authToken, 'platform': 'web', 'App-Version': this.appVersion, 'LoginUuid': this.loginUuid, 'Origin': this.host, 'Referer': this.referer, }; try { log(`[123API] ${method} ${path}`, queryParams || ''); const response = await fetch(this.buildURL(path, queryParams), { method, headers, body, credentials: 'include' }); const data = await response.json(); if (data.code !== 0) { log(`[123API] 请求失败,code: ${data.code}, message: ${data.message}`); throw new Error(data.message || 'API请求失败'); } await new Promise(resolve => setTimeout(resolve, 50)); return data; } catch (e) { log(`[123API] 请求异常:`, e); throw e; } } async getOnePageFileList(parentFileId, page) { const urlParams = { driveId: '0', limit: '100', next: '0', orderBy: 'file_name', orderDirection: 'asc', parentFileId: parentFileId.toString(), trashed: 'false', SearchData: '', Page: page.toString(), OnlyLookAbnormalFile: '0', event: 'homeListFile', operateType: '1', inDirectSpace: 'false' }; const data = await this.sendRequest('GET', '/b/api/file/list/new', urlParams); return { data: { InfoList: data.data.InfoList }, total: data.data.Total }; } async getFileList(parentFileId) { let InfoList = []; log(`[123API] 开始获取文件列表,parentFileId: ${parentFileId}`); const dismissToast = showToast('', '正在获取文件信息..', 0, { icon: '', minDuration: 3000, center: true }); try { const info = await this.getOnePageFileList(parentFileId, 1); InfoList.push(...info.data.InfoList); const total = info.total; log(`[123API] 第1页返回: ${info.data.InfoList.length} 条,总计: ${total} 条`); if (total > 100) { const times = Math.ceil(total / 100); for (let i = 2; i < times + 1; i++) { const info = await this.getOnePageFileList(parentFileId, i); InfoList.push(...info.data.InfoList); log(`[123API] 第${i}页返回: ${info.data.InfoList.length} 条,累计: ${InfoList.length}/${total} 条`); } } log(`[123API] 文件列表获取完成,共 ${InfoList.length} 条`); return { data: { InfoList }, total }; } finally { dismissToast(); } } async getFileInfo(idList) { const fileIdList = idList.map(fileId => ({ fileId })); const data = await this.sendRequest('POST', '/b/api/file/info', {}, JSON.stringify({ fileIdList })); return { data: { InfoList: data.data.infoList } }; } async fileRename(fileId, newFileName) { log(`[123API] POST rename - fileId: ${fileId}, newFileName: ${newFileName}`); const data = await this.sendRequest('POST', '/b/api/file/rename', {}, JSON.stringify({ driveId: '0', duplicate: '1', fileId: Number(fileId), fileName: newFileName, })); log(`[123API] 重命名结果: ${data.code === 0 ? '成功' : '失败'}`); return data.code === 0; } }(); } initFileSelection(onSelectionChange) { this.selector = new class TableRowSelector { constructor() { this.selectedRowKeys = new Set(); this.unselectedRowKeys = new Set(); this.isSelectAll = false; this._inited = false; this._callbacks = []; this._observers = []; this._breadcrumbObserver = null; } init() { if (this._inited) return; this._inited = true; this._observeBreadcrumb(); this._observeRowSelection(); } _observeRowSelection() { const self = this; const selectAllClickHandler = (e) => { const checkbox = e.target.closest('.ant-checkbox-input[aria-label="Select all"]'); if (!checkbox) return; setTimeout(() => { if (checkbox.checked) { self.isSelectAll = true; self.unselectedRowKeys = new Set(); self.selectedRowKeys = new Set(); } else { self.isSelectAll = false; self.selectedRowKeys = new Set(); self.unselectedRowKeys = new Set(); } self._notifyCallbacks(); }, 0); }; this._selectAllClickHandler = selectAllClickHandler; document.addEventListener('click', selectAllClickHandler); let rowDebounceTimer = null; const pendingRowChanges = new Map(); const processRowChanges = () => { for (const [rowKey, isSelected] of pendingRowChanges) { if (self.isSelectAll) { if (isSelected) self.unselectedRowKeys.delete(rowKey); else self.unselectedRowKeys.add(rowKey); } else { if (isSelected) self.selectedRowKeys.add(rowKey); else self.selectedRowKeys.delete(rowKey); } } pendingRowChanges.clear(); self._notifyCallbacks(); }; const rowObserver = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.type !== 'attributes' || mutation.attributeName !== 'class') continue; const target = mutation.target; if (!target.classList || !target.classList.contains('ant-table-row')) continue; const rowKey = target.getAttribute('data-row-key'); if (!rowKey) continue; const isSelected = target.classList.contains('ant-table-row-selected'); pendingRowChanges.set(rowKey, isSelected); } if (pendingRowChanges.size > 0) { if (rowDebounceTimer) clearTimeout(rowDebounceTimer); rowDebounceTimer = setTimeout(processRowChanges, 50); } }); const observeTarget = () => { const container = document.querySelector('.home-content'); if (container) { rowObserver.observe(container, { subtree: true, attributes: true, attributeFilter: ['class'] }); self._observers.push(rowObserver); log('[123Selector] 行选中监听已启动,目标: .home-content'); } else { setTimeout(observeTarget, 200); } }; observeTarget(); } _notifyCallbacks() { this._callbacks.forEach(cb => cb()); } onSelectionChange(cb) { this._callbacks.push(cb); } getSelection() { return { isSelectAll: this.isSelectAll, selectedRowKeys: [...this.selectedRowKeys], unselectedRowKeys: [...this.unselectedRowKeys] }; } _observeBreadcrumb(retryCount = 0) { const MAX_RETRIES = 50; const breadcrumb = document.querySelector('.home-breadcrumb'); if (!breadcrumb) { if (retryCount >= MAX_RETRIES) { log('[123Selector] 面包屑元素不存在,已达到最大重试次数'); return; } setTimeout(() => this._observeBreadcrumb(retryCount + 1), 200); return; } this._breadcrumbObserver = new MutationObserver(() => { log('[123Selector] 检测到面包屑变化,清空所有选择'); this.selectedRowKeys.clear(); this.unselectedRowKeys.clear(); this.isSelectAll = false; this._notifyCallbacks(); }); this._breadcrumbObserver.observe(breadcrumb, { childList: true, subtree: true, attributes: true, characterData: true }); log('[123Selector] 面包屑监听已启动'); } }(); this.selectedFilesManager = new class SelectedFilesManager { constructor(apiClient, selector) { this.apiClient = apiClient; this.selector = selector; this.selectedFiles = []; this._callbacks = []; this._debounceTimer = null; this._cachedFileList = null; this._cachedParentFileId = null; this._isUpdating = false; } init() { this.selector.onSelectionChange(() => { this._debounceUpdate(); }); } _debounceUpdate() { if (this._debounceTimer) clearTimeout(this._debounceTimer); this._debounceTimer = setTimeout(() => { this._updateSelectedFiles(); }, 300); } async _updateSelectedFiles() { this._isUpdating = true; const selection = this.selector.getSelection(); log(`[123Files] 选择状态变化:`, selection); this.selectedFiles = []; if (selection.isSelectAll) { log('[123Files] 全选模式'); const parentFileId = await this._getParentFileId(); log(`[123Files] 父级目录ID: ${parentFileId}`); let allFiles; if (this._cachedFileList && this._cachedParentFileId === parentFileId) { allFiles = this._cachedFileList; log('[123Files] 使用缓存文件列表'); } else { const fileList = await this.apiClient.getFileList(parentFileId); allFiles = fileList.data.InfoList; this._cachedFileList = allFiles; this._cachedParentFileId = parentFileId; } if (selection.unselectedRowKeys.length === 0) { this.selectedFiles = allFiles .filter(file => file.Type !== 1) .map(file => ({ id: String(file.FileId), name: file.FileName, category: file.Category || '0', })); } else { log(`[123Files] 全选但有取消,取消数量: ${selection.unselectedRowKeys.length}`); this.selectedFiles = allFiles .filter(file => { const isUnselected = selection.unselectedRowKeys.includes(String(file.FileId)); const isFile = file.Type !== 1; return !isUnselected && isFile; }) .map(file => ({ id: String(file.FileId), name: file.FileName, category: file.Category || '0', })); } } else { log('[123Files] 非全选模式'); const fileIds = selection.selectedRowKeys; log(`[123Files] 选中文件ID:`, fileIds); if (fileIds.length > 0) { try { const fileInfoList = await this.apiClient.getFileInfo(fileIds); this.selectedFiles = fileInfoList.data.InfoList .filter(file => file.Type !== 1) .map(file => ({ id: String(file.FileId), name: file.FileName, category: file.Category || '0', })); } catch (e) { log('[123Files] 获取文件信息失败:', e); } } } log(`[123Files] 最终选中文件数: ${this.selectedFiles.length}`); this._notifyCallbacks(); this._isUpdating = false; return this.selectedFiles; } async _getParentFileId() { try { const homeFilePath = JSON.parse(sessionStorage['filePath'])['homeFilePath']; const parentFileId = (homeFilePath[homeFilePath.length - 1] || 0); return parentFileId.toString(); } catch (e) { log('[123Files] 获取父级文件ID失败:', e); return '0'; } } _notifyCallbacks() { this._callbacks.forEach(cb => cb()); } onFilesChange(cb) { this._callbacks.push(cb); } isUpdating() { return this._isUpdating; } refresh() { return this._updateSelectedFiles(); } getSelectedFiles() { return [...this.selectedFiles]; } hasSelectedFiles() { return this.selectedFiles.length > 0; } }(this.apiClient, this.selector); this.selector.init(); this.selectedFilesManager.init(); this.selectedFilesManager.onFilesChange(() => { if (onSelectionChange) onSelectionChange(); }); } async getSelectedFiles() { if (this.selectedFilesManager.isUpdating()) { await new Promise(resolve => { this.selectedFilesManager.onFilesChange(resolve); }); } return this.selectedFilesManager.getSelectedFiles(); } isUpdating() { return this.selectedFilesManager.isUpdating(); } hasSelectedFiles() { return this.selectedFilesManager.hasSelectedFiles(); } async renameFile(fileId, newFileName) { return await this.apiClient.fileRename(fileId, newFileName); } injectButton(onClick) { const container = document.querySelector('.home-operator-button-group'); if (!container) return; const buttonExist = document.querySelector('.cfr-button-container'); if (buttonExist) return buttonExist; const firstButton = container.querySelector('button'); const buttonClass = firstButton ? firstButton.className : 'cfr-btn-primary'; const btnContainer = document.createElement('div'); btnContainer.className = 'cfr-button-container'; const btn = document.createElement('button'); btn.className = buttonClass; btn.innerHTML = '批量重命名'; btn.addEventListener('click', onClick); btnContainer.appendChild(btn); container.insertBefore(btnContainer, container.firstChild); return btnContainer; } updateButtonVisibility(buttonContainer) { if (!buttonContainer) return; buttonContainer.style.display = this.hasSelectedFiles() ? 'inline-block' : 'none'; } getButtonContainerSelector() { return '.home-operator-button-group'; } setupObserver(onChange) { } clearCache() { if (this.selectedFilesManager) { this.selectedFilesManager._cachedFileList = null; this.selectedFilesManager._cachedParentFileId = null; } } } class PlatformQuark { constructor() { this.name = 'quark'; this.CATEGORY_VIDEO = '1'; this.CATEGORY_IMAGE = '3'; this.CATEGORY_AUDIO = '2'; this.categoryMap = { 1: '视频', 2: '音频', 3: '图片' }; this.fileCache = new Map(); this.cachedDirId = null; this.selectedFiles = new Map(); this.buttonId = 'cfr-quark-fast-rename-btn'; } _getCurrentDirId() { const hash = window.location.hash; const segments = hash.replace(/^#\/list\/all\/?/, '').split('/').filter(Boolean); if (segments.length === 0) return null; const last = segments[segments.length - 1]; const dirId = last.split('-')[0]; return dirId; } _normalizeFile(file) { return { id: file.fid, name: file.file_name, category: String(file.category || 0), _raw: file, }; } getExtraColumnInfo(file) { return this.categoryMap[file.category] || '其他'; } async _getAllFiles(pdirFid, size = 1000) { let allList = []; let page = 1; log(`[QuarkAPI] 开始获取文件列表,pdir_fid: ${pdirFid}`); while (true) { log(`[QuarkAPI] 请求第 ${page} 页...`); const data = await this._getFileList(pdirFid, page, size); const list = (data?.data?.list ?? []) .filter(item => item.file_type === 1) .map(({ fid, file_name, file_type, category }) => ({ fid, file_name, file_type, category }) ); const metadata = data?.metadata ?? {}; const { _count, _total } = metadata; log(`[QuarkAPI] 第 ${page} 页返回: 本页 ${_count} 条,累计 ${allList.length + list.length}/${_total} 条`); allList = allList.concat(list); if (_count === 0 || allList.length >= _total) { log(`[QuarkAPI] 翻页完成,共 ${allList.length} 条`); break; } page++; } return allList; } _getFileList(pdirFid, page = 1, size = 1000) { return new Promise((resolve, reject) => { const params = new URLSearchParams({ pr: 'ucpro', fr: 'pc', pdir_fid: pdirFid, _page: String(page), _size: String(size), _fetch_total: '1', _sort: 'file_name:asc', }); const url = `https://drive-pc.quark.cn/1/clouddrive/file/sort?${params}`; log(`[QuarkAPI] GET ${url}`); GM_xmlhttpRequest({ method: 'GET', url, responseType: 'json', onload(res) { if (res.status === 200) { resolve(res.response); } else { reject(new Error(`请求失败: ${res.status}`)); } }, onerror(err) { reject(err); }, }); }); } _renameFile(fid, newFileName) { return new Promise((resolve, reject) => { const url = 'https://drive-pc.quark.cn/1/clouddrive/file/rename?pr=ucpro&fr=pc'; log(`[QuarkAPI] POST rename - fid: ${fid}, newFileName: ${newFileName}`); GM_xmlhttpRequest({ method: 'POST', url, headers: { 'Content-Type': 'application/json' }, data: JSON.stringify({ fid, file_name: newFileName }), responseType: 'json', onload(res) { if (res.status === 200) { resolve(res.response); } else { reject(new Error(`重命名失败: ${res.status}`)); } }, onerror(err) { reject(err); }, }); }); } async _ensureFileCache() { const dirId = this._getCurrentDirId(); if (!dirId) return; if (this.cachedDirId === dirId && this.fileCache.size > 0) { log(`[QuarkCache] 缓存命中,目录: ${dirId},共 ${this.fileCache.size} 个文件`); return; } if (this.cachedDirId !== dirId) { log(`[QuarkCache] 目录变更 ${this.cachedDirId} -> ${dirId},清空旧缓存`); this.fileCache = new Map(); this.selectedFiles = new Map(); this.cachedDirId = dirId; } if (this._cachePromise) return this._cachePromise; this._cachePromise = (async () => { const fetchDirId = dirId; const dismissToast = showToast('', '正在获取文件信息..', 0, { icon: '', minDuration: 3000, center: true }); try { log(`[QuarkCache] 开始获取全量文件列表,目录: ${fetchDirId}`); const files = await this._getAllFiles(fetchDirId); if (this.cachedDirId === fetchDirId) { this.fileCache = new Map(files.map(f => [f.fid, f])); log(`[QuarkCache] 全量缓存完成,共 ${this.fileCache.size} 个文件`); } else { log(`[QuarkCache] 目录已变更为 ${this.cachedDirId},丢弃旧目录 ${fetchDirId} 的缓存数据`); } } catch (err) { log(`[QuarkCache] 获取文件列表失败:`, err); } finally { dismissToast(); this._cachePromise = null; } })(); return this._cachePromise; } initFileSelection(onSelectionChange) { this._onSelectionChange = onSelectionChange; this._bindCheckboxEvents(); } _bindCheckboxEvents() { const headerCheckbox = document.querySelector('.ant-table-thead .ant-checkbox-input'); if (headerCheckbox && !headerCheckbox.dataset.cfrBound) { headerCheckbox.dataset.cfrBound = '1'; headerCheckbox.addEventListener('change', async (e) => { if (this._isProcessingSelection) return; this._isProcessingSelection = true; try { if (e.target.checked) { log(`[QuarkCheckbox] 全选触发`); await this._ensureFileCache(); this.selectedFiles = new Map( [...this.fileCache].map(([k, v]) => [k, this._normalizeFile(v)]) ); log(`[QuarkCheckbox] 全选完成,共 ${this.selectedFiles.size} 个文件`); } else { log(`[QuarkCheckbox] 取消全选,清空选中列表`); this.selectedFiles = new Map(); } if (this._onSelectionChange) this._onSelectionChange(); } finally { this._isProcessingSelection = false; } }); } const rowCheckboxes = document.querySelectorAll('.ant-table-tbody .ant-checkbox-input'); rowCheckboxes.forEach(cb => { if (cb.dataset.cfrBound) return; cb.dataset.cfrBound = '1'; cb.addEventListener('change', async (e) => { if (this._isProcessingSelection) return; this._isProcessingSelection = true; try { const row = e.target.closest('tr'); const rowKey = row?.dataset.rowKey ?? '未知'; const fileName = row?.querySelector('.filename-text')?.textContent?.trim() ?? '未知'; if (e.target.checked) { const cached = this.fileCache.get(rowKey); if (cached) { this.selectedFiles.set(rowKey, this._normalizeFile(cached)); log(`[QuarkCheckbox] 文件选中(缓存)- fid: ${rowKey}, 文件名: ${cached.file_name},当前共 ${this.selectedFiles.size} 个`); } else { log(`[QuarkCheckbox] 文件选中(缓存未命中)- fid: ${rowKey},触发全量获取`); await this._ensureFileCache(); const info = this.fileCache.get(rowKey) || { fid: rowKey, file_name: fileName, file_type: 1 }; this.selectedFiles.set(rowKey, this._normalizeFile(info)); log(`[QuarkCheckbox] 文件选中 - fid: ${rowKey}, 文件名: ${info.file_name},当前共 ${this.selectedFiles.size} 个`); } } else { this.selectedFiles.delete(rowKey); log(`[QuarkCheckbox] 文件取消 - fid: ${rowKey},当前共 ${this.selectedFiles.size} 个`); } if (this._onSelectionChange) this._onSelectionChange(); } finally { this._isProcessingSelection = false; } }); }); } async getSelectedFiles() { return [...this.selectedFiles.values()].filter(f => f._raw?.file_type === 1); } isUpdating() { return this._isProcessingSelection; } hasSelectedFiles() { return [...this.selectedFiles.values()].some(f => f._raw?.file_type === 1); } async renameFile(fileId, newFileName) { const res = await this._renameFile(fileId, newFileName); if (res.code !== undefined && res.code !== 0) { throw new Error(res.message || '重命名失败'); } return true; } injectButton(onClick) { const dirId = this._getCurrentDirId(); if (!dirId) return null; const container = document.querySelector('.btn-operate .btn-main'); if (!container || document.getElementById(this.buttonId)) return document.getElementById(this.buttonId); log(`[QuarkButton] 注入按钮,当前目录ID: ${dirId}`); const btn = document.createElement('button'); btn.id = this.buttonId; btn.className = 'ant-btn btn-file btn-file-primary upload-btn ant-btn-primary'; btn.innerHTML = '批量重命名'; btn.style.display = 'none'; btn.addEventListener('click', onClick); const dropdownTrigger = container.querySelector('.btn-create-folder'); if (dropdownTrigger) { dropdownTrigger.before(btn); } else { container.prepend(btn); } return btn; } updateButtonVisibility(buttonEl) { if (!buttonEl) return; buttonEl.style.display = this.hasSelectedFiles() ? '' : 'none'; } getButtonContainerSelector() { return '.btn-operate .btn-main'; } setupObserver(onChange) { const debouncedBind = debounce(() => { this._bindCheckboxEvents(); if (onChange) onChange(); }, 300); this._observer = new MutationObserver(() => { debouncedBind(); }); this._observer.observe(document.body, { childList: true, subtree: true }); log('[QuarkInit] MutationObserver 已启动'); } clearCache() { this.fileCache = new Map(); this.cachedDirId = null; } } class PlatformGuangya { constructor() { this.name = '光鸭网盘'; this.buttonId = 'cfr-guangya-rename-btn'; this.FILE_TYPE_IMAGE = 1; this.FILE_TYPE_VIDEO = 2; this.FILE_TYPE_AUDIO = 3; this.CATEGORY_IMAGE = String(this.FILE_TYPE_IMAGE); this.CATEGORY_VIDEO = String(this.FILE_TYPE_VIDEO); this.CATEGORY_AUDIO = String(this.FILE_TYPE_AUDIO); this.categoryMap = { 1: '图片', 2: '视频', 3: '音频' }; this.selectedFiles = new Map(); this.fileCache = new Map(); this.cachedDirId = null; this._cachePromise = null; this._isProcessingSelection = false; this._onSelectionChange = null; } _getCurrentDirId() { const hash = window.location.hash; const segments = hash.replace(/^#\/home\/all\/?/, '').split('/').filter(Boolean); if (segments.length === 0) return null; const last = segments[segments.length - 1]; const dirId = last.split('-')[0]; return dirId; } _normalizeFile(file) { return { id: file.fid, name: file.file_name, category: String(file.file_type || 0), _raw: file, }; } getButtonContainerSelector() { return '[aria-label="upload"]'; } injectButton(onClick) { const existingBtn = document.getElementById(this.buttonId); if (existingBtn && document.contains(existingBtn)) return existingBtn; const uploadBtn = document.querySelector('[aria-label="upload"]'); if (!uploadBtn) return null; const uploadButton = uploadBtn.closest('button'); if (!uploadButton) return null; const folderBtn = document.querySelector('[aria-label="addfolder"]'); const folderButton = folderBtn ? folderBtn.closest('button') : null; log('[GuangyaButton] 注入按钮'); const btn = uploadButton.cloneNode(false); btn.id = this.buttonId; btn.innerHTML = '批量重命名'; btn.style.display = 'none'; btn.addEventListener('click', onClick); if (folderButton) { folderButton.before(btn); } else { uploadButton.after(btn); } return btn; } updateButtonVisibility(buttonEl) { if (!buttonEl) return; buttonEl.style.display = this.hasSelectedFiles() ? '' : 'none'; } hasSelectedFiles() { return [...this.selectedFiles.values()].some(f => f._raw?.resType === 1); } getExtraColumnInfo(file) { return this.categoryMap[file.category] || '其他'; } _getAuthToken() { try { const key = Object.keys(localStorage).find(k => k.startsWith('credentials_')); if (!key) return null; const credentials = JSON.parse(localStorage.getItem(key)); return `${credentials.token_type} ${credentials.access_token}`; } catch (e) { log('[GuangyaAPI] 获取 Token 失败:', e); return null; } } _getFileList(parentId, pageSize = 1000, page = 1) { return new Promise((resolve, reject) => { const token = this._getAuthToken(); if (!token) { reject(new Error('未登录,请先登录光鸭网盘')); return; } const url = 'https://api.guangyapan.com/nd.bizuserres.s/v1/file/get_file_list'; const body = JSON.stringify({ parentId, pageSize, orderBy: 1, sortType: 1, }); log(`[GuangyaAPI] POST ${url}, parentId: ${parentId}, page: ${page}`); GM_xmlhttpRequest({ method: 'POST', url, headers: { 'Content-Type': 'application/json', 'Authorization': token, }, data: body, responseType: 'json', onload(res) { if (res.status === 200) { resolve(res.response); } else { reject(new Error(`请求失败: ${res.status}`)); } }, onerror(err) { reject(err); }, }); }); } async _getAllFiles(parentId, size = 1000) { let page = 1; let allList = []; let rawFetched = 0; log(`[GuangyaAPI] 开始获取文件列表,parentId: ${parentId}`); while (true) { const data = await this._getFileList(parentId, size, page); const rawList = data?.data?.list ?? []; const list = rawList .filter(item => item.resType === 1) .map(({ fileId, fileName, fileType, resType }) => ({ fid: fileId, file_name: fileName, file_type: fileType, resType }) ); const total = data?.data?.total ?? 0; rawFetched += rawList.length; log(`[GuangyaAPI] 第 ${page} 页返回: 本页 ${rawList.length} 条(文件 ${list.length}),累计原始 ${rawFetched}/${total} 条`); allList = allList.concat(list); if (rawList.length === 0 || rawFetched >= total) { log(`[GuangyaAPI] 翻页完成,共 ${allList.length} 个文件`); break; } page++; } return allList; } async _ensureFileCache() { const dirId = this._getCurrentDirId(); if (!dirId) return; if (this.cachedDirId === dirId && this.fileCache.size > 0) { log(`[GuangyaCache] 缓存命中,目录: ${dirId},共 ${this.fileCache.size} 个文件`); return; } if (this.cachedDirId !== dirId) { log(`[GuangyaCache] 目录变更 ${this.cachedDirId} -> ${dirId},清空旧缓存`); this.fileCache = new Map(); this.selectedFiles = new Map(); this.cachedDirId = dirId; } if (this._cachePromise) return this._cachePromise; this._cachePromise = (async () => { const fetchDirId = dirId; const dismissToast = showToast('', '正在获取文件信息..', 0, { icon: '', minDuration: 3000, center: true }); try { log(`[GuangyaCache] 开始获取全量文件列表,目录: ${fetchDirId}`); const files = await this._getAllFiles(fetchDirId); if (this.cachedDirId === fetchDirId) { this.fileCache = new Map(files.map(f => [f.fid, f])); log(`[GuangyaCache] 全量缓存完成,共 ${this.fileCache.size} 个文件`); } else { log(`[GuangyaCache] 目录已变更为 ${this.cachedDirId},丢弃旧目录 ${fetchDirId} 的缓存数据`); } } catch (err) { log(`[GuangyaCache] 获取文件列表失败:`, err); } finally { dismissToast(); this._cachePromise = null; } })(); return this._cachePromise; } initFileSelection(onSelectionChange) { this._onSelectionChange = onSelectionChange; this._bindCheckboxEvents(); } _bindCheckboxEvents() { const headerCheckbox = document.querySelector('.ant-table-thead .ant-checkbox-input'); if (headerCheckbox && !headerCheckbox.dataset.cfrBound) { headerCheckbox.dataset.cfrBound = '1'; headerCheckbox.addEventListener('change', async (e) => { if (this._isProcessingSelection) return; this._isProcessingSelection = true; try { if (e.target.checked) { log(`[GuangyaCheckbox] 全选触发`); await this._ensureFileCache(); this.selectedFiles = new Map( [...this.fileCache].map(([k, v]) => [k, this._normalizeFile(v)]) ); log(`[GuangyaCheckbox] 全选完成,共 ${this.selectedFiles.size} 个文件`); } else { log(`[GuangyaCheckbox] 取消全选,清空选中列表`); this.selectedFiles = new Map(); } if (this._onSelectionChange) this._onSelectionChange(); } finally { this._isProcessingSelection = false; } }); } const rowCheckboxes = document.querySelectorAll('.ant-table-tbody .ant-checkbox-input'); rowCheckboxes.forEach(cb => { if (cb.dataset.cfrBound) return; cb.dataset.cfrBound = '1'; cb.addEventListener('change', async (e) => { if (this._isProcessingSelection) return; this._isProcessingSelection = true; try { const row = e.target.closest('tr'); const rowKey = row?.dataset.rowKey ?? '未知'; const fileName = row?.querySelector('div[title]')?.getAttribute('title') ?? '未知'; if (e.target.checked) { const cached = this.fileCache.get(rowKey); if (cached) { this.selectedFiles.set(rowKey, this._normalizeFile(cached)); log(`[GuangyaCheckbox] 文件选中(缓存)- fid: ${rowKey}, 文件名: ${cached.file_name},当前共 ${this.selectedFiles.size} 个`); } else { log(`[GuangyaCheckbox] 文件选中(缓存未命中)- fid: ${rowKey},触发全量获取`); await this._ensureFileCache(); const info = this.fileCache.get(rowKey) || { fid: rowKey, file_name: fileName, file_type: 1, resType: 1 }; this.selectedFiles.set(rowKey, this._normalizeFile(info)); log(`[GuangyaCheckbox] 文件选中 - fid: ${rowKey}, 文件名: ${info.file_name},当前共 ${this.selectedFiles.size} 个`); } } else { this.selectedFiles.delete(rowKey); log(`[GuangyaCheckbox] 文件取消 - fid: ${rowKey},当前共 ${this.selectedFiles.size} 个`); } if (this._onSelectionChange) this._onSelectionChange(); } finally { this._isProcessingSelection = false; } }); }); } async getSelectedFiles() { return [...this.selectedFiles.values()].filter(f => f._raw?.resType === 1); } isUpdating() { return this._isProcessingSelection; } async renameFile(fileId, newFileName) { const token = this._getAuthToken(); if (!token) throw new Error('未登录,请先登录光鸭网盘'); return new Promise((resolve, reject) => { const url = 'https://api.guangyapan.com/nd.bizuserres.s/v1/file/rename'; const body = JSON.stringify({ fileId, newName: newFileName }); log(`[GuangyaAPI] POST rename - fileId: ${fileId}, newName: ${newFileName}`); GM_xmlhttpRequest({ method: 'POST', url, headers: { 'Content-Type': 'application/json', 'Authorization': token, }, data: body, responseType: 'json', onload(res) { if (res.status === 200) { const data = res.response; if (data?.msg === 'success') { resolve(true); } else { reject(new Error(data?.msg || '重命名失败')); } } else { reject(new Error(`重命名失败: ${res.status}`)); } }, onerror(err) { reject(err); }, }); }); } setupObserver(onChange) { let lastDirId = this._getCurrentDirId(); const debouncedBind = debounce(() => { const currentDirId = this._getCurrentDirId(); if (currentDirId !== lastDirId) { log(`[GuangyaInit] 目录变更 ${lastDirId} -> ${currentDirId},清空选中`); this.selectedFiles = new Map(); lastDirId = currentDirId; } this._bindCheckboxEvents(); if (onChange) onChange(); }, 300); this._observer = new MutationObserver(() => { debouncedBind(); }); this._observer.observe(document.body, { childList: true, subtree: true }); log('[GuangyaInit] MutationObserver 已启动'); } clearCache() { this.fileCache = new Map(); this.cachedDirId = null; } } let sortModal = null; let renameModal = null; function showSortModal(files, platform) { const fileList = document.createElement('div'); fileList.className = 'cfr-file-list'; const sortedFiles = [...files].sort((a, b) => a.name.localeCompare(b.name, 'zh-CN')); let draggedItem = null; const fragment = document.createDocumentFragment(); sortedFiles.forEach((file, index) => { const fileItem = document.createElement('div'); fileItem.className = 'cfr-file-item'; fileItem.dataset.fileId = file.id; fileItem.dataset.category = String(file.category || 0); fileItem.draggable = true; fileItem.addEventListener('dragstart', (e) => { draggedItem = fileItem; fileItem.classList.add('dragging'); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', file.id); }); fileItem.addEventListener('dragend', () => { draggedItem = null; fileItem.classList.remove('dragging'); fileList.querySelectorAll('.cfr-file-item').forEach(item => { item.style.transform = ''; }); updateFileIndices(fileList); }); fileItem.addEventListener('dragover', (e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; if (draggedItem && draggedItem !== fileItem) { const rect = fileItem.getBoundingClientRect(); const midY = rect.top + rect.height / 2; if (e.clientY < midY) { fileList.insertBefore(draggedItem, fileItem); } else { fileList.insertBefore(draggedItem, fileItem.nextSibling); } } }); fileItem.addEventListener('dragenter', (e) => { e.preventDefault(); if (draggedItem && draggedItem !== fileItem) fileItem.style.transform = 'translateY(4px)'; }); fileItem.addEventListener('dragleave', () => { if (draggedItem && draggedItem !== fileItem) fileItem.style.transform = ''; }); fileItem.addEventListener('drop', (e) => { e.preventDefault(); fileItem.style.transform = ''; }); const dragHandle = document.createElement('span'); dragHandle.innerHTML = '⋮⋮'; dragHandle.className = 'cfr-drag-handle'; const fileIndex = document.createElement('span'); fileIndex.textContent = `${index + 1}.`; fileIndex.className = 'cfr-file-index'; const fileName = document.createElement('span'); fileName.textContent = file.name; fileName.className = 'cfr-file-name'; const extraInfo = document.createElement('span'); extraInfo.textContent = platform.getExtraColumnInfo(file); extraInfo.className = 'cfr-file-extra'; const deleteBtn = document.createElement('button'); deleteBtn.innerHTML = '×'; deleteBtn.className = 'cfr-file-delete-btn'; deleteBtn.onmousedown = (e) => e.stopPropagation(); deleteBtn.onclick = (e) => { e.stopPropagation(); fileItem.remove(); updateFileIndices(fileList); updateStats(fileList, statsContainer); }; fileItem.appendChild(dragHandle); fileItem.appendChild(fileIndex); fileItem.appendChild(fileName); fileItem.appendChild(extraInfo); fileItem.appendChild(deleteBtn); fragment.appendChild(fileItem); }); fileList.appendChild(fragment); const sortButton = createToggleButton('文件名降序', false, (isChecked) => { const fileItems = Array.from(fileList.querySelectorAll('.cfr-file-item')); fileItems.sort((a, b) => { const nameA = a.querySelector('.cfr-file-name').textContent; const nameB = b.querySelector('.cfr-file-name').textContent; return isChecked ? nameB.localeCompare(nameA, 'zh-CN') : nameA.localeCompare(nameB, 'zh-CN'); }); fileItems.forEach(item => fileList.appendChild(item)); updateFileIndices(fileList); }); const filterButtons = []; const videoCategory = String(platform.CATEGORY_VIDEO); const filterVideoButton = createToggleButton('仅视频', false, (isChecked) => { if (isChecked) filterButtons.forEach(b => { if (b !== filterVideoButton) { b.classList.remove('cfr-toggle-button-active'); b.dataset.active = 'false'; } }); const fileItems = fileList.querySelectorAll('.cfr-file-item'); fileItems.forEach(item => { const cat = item.dataset.category; item.style.display = (isChecked && cat !== videoCategory) ? 'none' : 'flex'; }); updateFileIndices(fileList); updateStats(fileList, statsContainer); }); filterButtons.push(filterVideoButton); const imageCategory = String(platform.CATEGORY_IMAGE); const filterImageButton = createToggleButton('仅图片', false, (isChecked) => { if (isChecked) filterButtons.forEach(b => { if (b !== filterImageButton) { b.classList.remove('cfr-toggle-button-active'); b.dataset.active = 'false'; } }); const fileItems = fileList.querySelectorAll('.cfr-file-item'); fileItems.forEach(item => { const cat = item.dataset.category; item.style.display = (isChecked && cat !== imageCategory) ? 'none' : 'flex'; }); updateFileIndices(fileList); updateStats(fileList, statsContainer); }); filterButtons.push(filterImageButton); const audioCategory = String(platform.CATEGORY_AUDIO); const filterAudioButton = createToggleButton('仅音频', false, (isChecked) => { if (isChecked) filterButtons.forEach(b => { if (b !== filterAudioButton) { b.classList.remove('cfr-toggle-button-active'); b.dataset.active = 'false'; } }); const fileItems = fileList.querySelectorAll('.cfr-file-item'); fileItems.forEach(item => { const cat = item.dataset.category; item.style.display = (isChecked && cat !== audioCategory) ? 'none' : 'flex'; }); updateFileIndices(fileList); updateStats(fileList, statsContainer); }); filterButtons.push(filterAudioButton); const headerButtonsContainer = document.createElement('div'); headerButtonsContainer.className = 'cfr-button-container-inner'; headerButtonsContainer.appendChild(sortButton); filterButtons.forEach(b => headerButtonsContainer.appendChild(b)); const statsContainer = document.createElement('div'); statsContainer.className = 'cfr-stats-container'; updateStats(fileList, statsContainer); const nextBtn = document.createElement('button'); nextBtn.textContent = '下一步'; nextBtn.className = 'cfr-btn-primary'; nextBtn.onclick = () => { const orderedFiles = getOrderedFiles(fileList, files); log(`[Modal] 排序后文件列表:`, orderedFiles); sortModal.close(); showRenameModal(orderedFiles, platform); }; const closeBtn = document.createElement('button'); closeBtn.textContent = '取消'; closeBtn.className = 'cfr-btn'; closeBtn.onclick = () => sortModal.close(); const footerButtonsContainer = document.createElement('div'); footerButtonsContainer.className = 'cfr-footer-buttons-container'; footerButtonsContainer.appendChild(closeBtn); footerButtonsContainer.appendChild(nextBtn); const footerContent = document.createElement('div'); footerContent.className = 'cfr-modal-footer-content'; footerContent.appendChild(statsContainer); footerContent.appendChild(footerButtonsContainer); sortModal = new Modal({ title: '文件排序', subtitle: '拖动调整顺序,然后点击下一步', bodyContent: fileList, headerButtons: headerButtonsContainer, footerContent: footerContent, }); sortModal.show(); } const RENAME_TYPES = [ { key: 'sequence', label: '按序号重命名' }, { key: 'append', label: '追加重命名' }, { key: 'replace', label: '查找替换' }, { key: 'regex', label: '正则替换' }, { key: 'format', label: '格式替换' }, ]; function showRenameModal(files, platform) { const bodyContainer = document.createElement('div'); const configArea = document.createElement('div'); configArea.className = 'cfr-rename-config'; const separator = document.createElement('div'); separator.className = 'separator'; const fileList = document.createElement('div'); fileList.className = 'cfr-file-list'; const fragment = document.createDocumentFragment(); files.forEach((file, index) => { const fileItem = document.createElement('div'); fileItem.className = 'cfr-file-item-rename'; fileItem.dataset.fileId = file.id; fileItem.dataset.originalFileName = file.name; fileItem.dataset.index = String(index); const originalName = document.createElement('div'); originalName.className = 'cfr-file-name-original'; const fileIndex = document.createElement('span'); fileIndex.textContent = `${index + 1}.`; fileIndex.className = 'cfr-file-index'; const fileName = document.createElement('span'); fileName.textContent = file.name; originalName.appendChild(fileIndex); originalName.appendChild(fileName); const arrowIcon = document.createElement('div'); arrowIcon.className = 'cfr-arrow-icon'; arrowIcon.innerHTML = '→'; const newName = document.createElement('div'); newName.className = 'cfr-file-name-new'; newName.textContent = file.name; fileItem.appendChild(originalName); fileItem.appendChild(arrowIcon); fileItem.appendChild(newName); fragment.appendChild(fileItem); }); fileList.appendChild(fragment); bodyContainer.appendChild(configArea); bodyContainer.appendChild(separator); bodyContainer.appendChild(fileList); const tabContainer = document.createElement('div'); tabContainer.className = 'cfr-tab-container'; let activeType = 'sequence'; const tabItems = []; RENAME_TYPES.forEach(({ key, label }, index) => { const tabItem = document.createElement('div'); tabItem.className = 'cfr-tab-item' + (index === 0 ? ' active' : ''); tabItem.dataset.type = key; tabItem.textContent = label; tabItem.onclick = () => { tabItems.forEach(t => t.classList.remove('active')); tabItem.classList.add('active'); activeType = key; updateConfigArea(key); updateRenamePreview(); log(`[Modal] 切换重命名类型: ${label}`); }; tabItems.push(tabItem); tabContainer.appendChild(tabItem); }); const headerRight = document.createElement('div'); headerRight.className = 'cfr-modal-header-right'; headerRight.appendChild(tabContainer); function updateConfigArea(type) { configArea.innerHTML = ''; const inputsContainer = document.createElement('div'); inputsContainer.className = 'cfr-rename-inputs-container'; switch (type) { case 'sequence': { const prefixInput = document.createElement('input'); prefixInput.type = 'text'; prefixInput.className = 'cfr-rename-config-input'; prefixInput.placeholder = '追加前缀'; const numberInput = document.createElement('input'); numberInput.type = 'number'; numberInput.className = 'cfr-rename-config-input'; numberInput.placeholder = '默认序号'; const suffixInput = document.createElement('input'); suffixInput.type = 'text'; suffixInput.className = 'cfr-rename-config-input'; suffixInput.placeholder = '追加后缀'; inputsContainer.appendChild(prefixInput); inputsContainer.appendChild(numberInput); inputsContainer.appendChild(suffixInput); break; } case 'append': { const prefixInput = document.createElement('input'); prefixInput.type = 'text'; prefixInput.className = 'cfr-rename-config-input'; prefixInput.placeholder = '追加前缀'; const suffixInput = document.createElement('input'); suffixInput.type = 'text'; suffixInput.className = 'cfr-rename-config-input'; suffixInput.placeholder = '追加后缀'; inputsContainer.appendChild(prefixInput); inputsContainer.appendChild(suffixInput); break; } case 'replace': { const findInput = document.createElement('input'); findInput.type = 'text'; findInput.className = 'cfr-rename-config-input'; findInput.placeholder = '查找内容'; const replaceInput = document.createElement('input'); replaceInput.type = 'text'; replaceInput.className = 'cfr-rename-config-input'; replaceInput.placeholder = '替换内容'; inputsContainer.appendChild(findInput); inputsContainer.appendChild(replaceInput); const ignoreCaseLabel = document.createElement('label'); ignoreCaseLabel.className = 'cfr-checkbox-button'; const ignoreCaseCheckbox = document.createElement('input'); ignoreCaseCheckbox.type = 'checkbox'; ignoreCaseCheckbox.className = 'cfr-checkbox-input'; const ignoreCaseText = document.createElement('span'); ignoreCaseText.textContent = '忽略大小写'; ignoreCaseText.className = 'cfr-checkbox-text'; ignoreCaseLabel.appendChild(ignoreCaseCheckbox); ignoreCaseLabel.appendChild(ignoreCaseText); inputsContainer.appendChild(ignoreCaseLabel); break; } case 'regex': { const regexInput = document.createElement('input'); regexInput.type = 'text'; regexInput.className = 'cfr-rename-config-input'; regexInput.placeholder = '正则表达式'; const replaceInput = document.createElement('input'); replaceInput.type = 'text'; replaceInput.className = 'cfr-rename-config-input'; replaceInput.placeholder = '替换内容'; inputsContainer.appendChild(regexInput); inputsContainer.appendChild(replaceInput); const regexErrorHint = document.createElement('span'); regexErrorHint.style.cssText = 'color: #ff4d4f; font-size: 12px; display: none; margin-top: 4px;'; regexErrorHint.className = 'cfr-regex-error-hint'; inputsContainer.appendChild(regexErrorHint); break; } case 'format': { const suffixInput = document.createElement('input'); suffixInput.type = 'text'; suffixInput.className = 'cfr-rename-config-input'; suffixInput.placeholder = '新格式名'; inputsContainer.appendChild(suffixInput); break; } } configArea.appendChild(inputsContainer); inputsContainer.querySelectorAll('input').forEach(input => { input.addEventListener('input', updateRenamePreview); }); inputsContainer.querySelectorAll('input[type="checkbox"]').forEach(input => { input.addEventListener('change', updateRenamePreview); }); } function updateRenamePreview() { const fileItems = fileList.querySelectorAll('.cfr-file-item-rename'); const inputs = configArea.querySelectorAll('.cfr-rename-config-input'); fileItems.forEach(item => { const original = item.dataset.originalFileName; const idx = Number(item.dataset.index); const newNameEl = item.querySelector('.cfr-file-name-new'); if (!newNameEl) return; if (!activeType) { newNameEl.textContent = original; return; } let result = original; const ext = original.includes('.') ? '.' + original.split('.').pop() : ''; const nameWithoutExt = ext ? original.slice(0, -ext.length) : original; switch (activeType) { case 'sequence': { const prefix = inputs[0]?.value || ''; const startNumberStr = inputs[1]?.value || ''; const suffix = inputs[2]?.value || ''; let startNumber = 1; let paddingLength = 0; if (startNumberStr) { const parsedNumber = parseInt(startNumberStr); if (parsedNumber === 0) { paddingLength = startNumberStr.length; startNumber = 1; } else { startNumber = parsedNumber; paddingLength = startNumberStr.length; } } const sequenceNumber = startNumber + idx; const numberPart = paddingLength > 0 ? sequenceNumber.toString().padStart(paddingLength, '0') : sequenceNumber.toString(); result = prefix + numberPart + suffix + ext; break; } case 'append': { const prefix = inputs[0]?.value || ''; const suffix = inputs[1]?.value || ''; result = prefix + nameWithoutExt + suffix + ext; break; } case 'replace': { const search = inputs[0]?.value || ''; const replace = inputs[1]?.value || ''; const ignoreCaseCheckbox = configArea.querySelector('input[type="checkbox"]'); const ignoreCase = ignoreCaseCheckbox ? ignoreCaseCheckbox.checked : false; if (search) { if (ignoreCase) { const regex = new RegExp(search.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi'); result = original.replace(regex, replace); } else { result = original.split(search).join(replace); } } break; } case 'regex': { const pattern = inputs[0]?.value || ''; const replacement = inputs[1]?.value || ''; const regexErrorHint = configArea.querySelector('.cfr-regex-error-hint'); if (pattern) { try { result = original.replace(new RegExp(pattern, 'g'), () => replacement); if (regexErrorHint) { regexErrorHint.style.display = 'none'; regexErrorHint.textContent = ''; } inputs[0].style.borderColor = ''; } catch (e) { if (regexErrorHint) { regexErrorHint.textContent = '正则表达式语法错误'; regexErrorHint.style.display = 'inline'; } inputs[0].style.borderColor = '#ff4d4f'; } } break; } case 'format': { const newExt = inputs[0]?.value || ''; if (newExt) { result = nameWithoutExt + '.' + newExt.replace(/^\./, ''); } break; } } newNameEl.textContent = result; }); } updateConfigArea('sequence'); const statsContainer = document.createElement('div'); statsContainer.className = 'cfr-stats-container'; statsContainer.innerHTML = `${files.length} 个文件`; const prevBtn = document.createElement('button'); prevBtn.textContent = '上一步'; prevBtn.className = 'cfr-btn'; prevBtn.onclick = () => { renameModal.close(); showSortModal(files, platform); }; const confirmBtn = document.createElement('button'); confirmBtn.textContent = '确定'; confirmBtn.className = 'cfr-btn-primary'; let hasExecuted = false; confirmBtn.onclick = async () => { if (hasExecuted) { log('[Modal] 重命名已完成,关闭弹窗并刷新页面'); renameModal.close(); window.location.reload(); return; } log(`[Modal] 开始执行重命名,类型: ${activeType}`); prevBtn.style.display = 'none'; const inputs = configArea.querySelectorAll('input'); inputs.forEach(input => { input.disabled = true; }); const renamedFiles = []; const fileItems = fileList.querySelectorAll('.cfr-file-item-rename'); fileItems.forEach(item => { const fileId = item.dataset.fileId; const originalFileName = item.dataset.originalFileName; const newNameEl = item.querySelector('.cfr-file-name-new'); const newFileName = newNameEl ? newNameEl.textContent : originalFileName; renamedFiles.push({ id: fileId, originalFileName, newFileName }); }); log(`[Modal] 待重命名文件数量: ${renamedFiles.length}`); try { confirmBtn.disabled = true; confirmBtn.textContent = '重命名中...'; let successCount = 0; let failCount = 0; for (const fileInfo of renamedFiles) { log(`[Modal] 正在重命名: ${fileInfo.originalFileName} -> ${fileInfo.newFileName}`); const fileItem = fileList.querySelector(`.cfr-file-item-rename[data-file-id="${fileInfo.id}"]`); const newNameElement = fileItem?.querySelector('.cfr-file-name-new'); if (fileInfo.originalFileName === fileInfo.newFileName) { if (newNameElement) { newNameElement.style.backgroundColor = '#dfffcc'; newNameElement.style.border = '1px solid #84d75b'; } successCount++; if (fileItem) fileItem.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); continue; } try { await platform.renameFile(fileInfo.id, fileInfo.newFileName); await new Promise(r => setTimeout(r, 100)); if (newNameElement) { newNameElement.style.backgroundColor = '#dfffcc'; newNameElement.style.border = '1px solid #84d75b'; } successCount++; } catch (err) { log(`[Modal] 重命名失败: ${fileInfo.originalFileName}`, err); if (newNameElement) { newNameElement.style.backgroundColor = '#ffc4c4'; newNameElement.style.border = '1px solid #d75b5b'; } failCount++; } if (fileItem) fileItem.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } log(`[Modal] 重命名完成,成功: ${successCount},失败: ${failCount}`); updateRenameStats(statsContainer, renamedFiles.length, successCount, failCount); if (platform.clearCache) platform.clearCache(); hasExecuted = true; confirmBtn.disabled = false; confirmBtn.textContent = '关闭'; } catch (error) { log(`[Modal] 重命名异常:`, error); confirmBtn.disabled = false; confirmBtn.textContent = '确定'; inputs.forEach(input => { input.disabled = false; }); } }; const footerButtonsContainer = document.createElement('div'); footerButtonsContainer.className = 'cfr-footer-buttons-container'; footerButtonsContainer.appendChild(prevBtn); footerButtonsContainer.appendChild(confirmBtn); const footerContent = document.createElement('div'); footerContent.className = 'cfr-modal-footer-content'; footerContent.appendChild(statsContainer); footerContent.appendChild(footerButtonsContainer); renameModal = new Modal({ title: '批量重命名', bodyContent: bodyContainer, headerRight: headerRight, footerContent: footerContent, }); renameModal.show(); } let currentPlatform = null; async function handleButtonClick() { if (!currentPlatform) return; if (currentPlatform.isUpdating()) return; const selectedFiles = await currentPlatform.getSelectedFiles(); if (selectedFiles.length === 0) return; showSortModal(selectedFiles, currentPlatform); } (async () => { const platformType = detectPlatform(); if (!platformType) { log('[Init] 无法识别当前平台,脚本退出'); return; } log(`[Init] 检测到平台: ${platformType}`); if (platformType === '123') { currentPlatform = new Platform123(); } else if (platformType === 'quark') { currentPlatform = new PlatformQuark(); } else if (platformType === 'guangya') { currentPlatform = new PlatformGuangya(); } if (currentPlatform.initAPI) currentPlatform.initAPI(); await waitForElement(currentPlatform.getButtonContainerSelector()); let buttonEl = null; const ensureButton = () => { if (!buttonEl || !document.contains(buttonEl)) { buttonEl = currentPlatform.injectButton(() => handleButtonClick()); } return buttonEl; }; const onSelectionChange = () => { const btn = ensureButton(); currentPlatform.updateButtonVisibility(btn); }; currentPlatform.initFileSelection(onSelectionChange); buttonEl = currentPlatform.injectButton(() => handleButtonClick()); currentPlatform.updateButtonVisibility(buttonEl); currentPlatform.setupObserver(() => { const btn = ensureButton(); currentPlatform.updateButtonVisibility(btn); }); log(`[Init] 初始化完成,平台: ${platformType}`); })(); })();