// ==UserScript== // @name 夸克网盘批量重命名 // @version 1.0.0 // @description 支持按序号、追加、查找替换、正则替换、格式替换等多种重命名模式,提供拖拽排序、实时预览、过滤等功能 // @author wpys.cc // @match https://pan.quark.cn/* // @match https://drive.quark.cn/* // @icon https://pan.quark.cn/favicon.ico // @noframes // @grant GM_xmlhttpRequest // @grant GM_addStyle // ==/UserScript== (function () { 'use strict'; function waitForElement(selector, timeout = 10000) { return new Promise((resolve, reject) => { const el = document.querySelector(selector); if (el) return resolve(el); const observer = new MutationObserver((_, obs) => { const el = document.querySelector(selector); if (el) { obs.disconnect(); 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 debounce(fn, delay = 300) { let timer; return (...args) => { clearTimeout(timer); timer = setTimeout(() => fn.apply(this, args), delay); }; } 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 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'; this._cachePromise = null; this._isProcessingSelection = false; this._onSelectionChange = null; } _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; while (true) { 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; allList = allList.concat(list); if (_count === 0 || allList.length >= _total) { 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}`; 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'; 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) return; if (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; try { const files = await this._getAllFiles(fetchDirId); if (this.cachedDirId === fetchDirId) { this.fileCache = new Map(files.map(f => [f.fid, f])); } } catch (err) { console.error('[夸克批量重命名] 获取文件列表失败:', err); } finally { 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) { await this._ensureFileCache(); this.selectedFiles = new Map( [...this.fileCache].map(([k, v]) => [k, this._normalizeFile(v)]) ); } else { 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 ?? ''; if (e.target.checked) { const cached = this.fileCache.get(rowKey); if (cached) { this.selectedFiles.set(rowKey, this._normalizeFile(cached)); } else { await this._ensureFileCache(); const info = this.fileCache.get(rowKey); if (info) { this.selectedFiles.set(rowKey, this._normalizeFile(info)); } } } else { this.selectedFiles.delete(rowKey); } 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); 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 }); } 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); 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(); }; 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) { renameModal.close(); window.location.reload(); return; } 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 }); }); try { confirmBtn.disabled = true; confirmBtn.textContent = '重命名中...'; let successCount = 0; let failCount = 0; for (const fileInfo of renamedFiles) { 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) { if (newNameElement) { newNameElement.style.backgroundColor = '#ffc4c4'; newNameElement.style.border = '1px solid #d75b5b'; } failCount++; } if (fileItem) fileItem.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } updateRenameStats(statsContainer, renamedFiles.length, successCount, failCount); if (platform.clearCache) platform.clearCache(); hasExecuted = true; confirmBtn.disabled = false; confirmBtn.textContent = '关闭'; } catch (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 () => { currentPlatform = new PlatformQuark(); 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); }); })(); })();