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