您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
为123网盘用户设计,一个功能强大的油猴脚本。不仅可以将超大JSON文件拆分,还新增了将多个JSON文件合并为一体的独立功能。完美集成至左侧菜单,适配所有布局。
// ==UserScript== // @name 123pan JSON 高级工具集 (拆分与合并) // @namespace http://tampermonkey.net/ // @version 2.2.2 // @description 为123网盘用户设计,一个功能强大的油猴脚本。不仅可以将超大JSON文件拆分,还新增了将多个JSON文件合并为一体的独立功能。完美集成至左侧菜单,适配所有布局。 // @author @一只氧气 // @match *://www.123pan.com/* // @match *://*.123pan.com/* // @match *://*.123pan.cn/* // @match *://*.123865.com/* // @match *://*.123684.com/* // @match *://*.123912.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=123pan.com // @grant GM_addStyle // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js // ==/UserScript== (function() { 'use strict'; // --- Constants --- const SCRIPT_NAME_SPLITTER = "123pan JSON 拆分工具"; const SCRIPT_NAME_MERGER = "123pan JSON 合并工具"; const SCRIPT_VERSION = "2.2.2"; // Final icon fix const BROWSER_DOWNLOAD_LIMIT = 10; const SEQUENTIAL_DOWNLOAD_DELAY = 1500; // ms // --- Core Logic (No changes here) --- const coreLogic = { analyzeJsonStructure: function(jsonData) { try { if (!jsonData || !Array.isArray(jsonData.files)) { return { error: "JSON数据格式无效,缺少 'files' 数组。" }; } const tree = {}; let maxDepth = 0; const commonPathPrefix = jsonData.commonPath ? `${jsonData.commonPath.replace(/\/$/, '')}/` : ''; jsonData.files.forEach(file => { const fullPath = commonPathPrefix + file.path; const parts = fullPath.split('/').filter(p => p); let currentLevel = tree; parts.forEach((part, index) => { if (index === parts.length - 1) { currentLevel[part] = null; } else { if (!currentLevel[part]) { currentLevel[part] = {}; } currentLevel = currentLevel[part]; } }); if (parts.length > maxDepth) { maxDepth = parts.length; } }); function formatTree(node, prefix = '', depth = 1) { let result = ''; const keys = Object.keys(node); keys.forEach((key, index) => { const isNodeLast = index === keys.length - 1; const connector = isNodeLast ? '└── ' : '├── '; const itemPrefix = node[key] === null ? '📄 ' : '📁 '; result += `${prefix}${connector}${itemPrefix}${key} (Lv. ${depth})\n`; if (node[key] !== null) { const newPrefix = prefix + (isNodeLast ? ' ' : '│ '); result += formatTree(node[key], newPrefix, depth + 1); } }); return result; } const treeString = formatTree(tree); const fileCount = jsonData.files.length; const totalSize = jsonData.files.reduce((sum, file) => sum + (Number(file.size) || 0), 0); return { fileCount, totalSize: uiManager.formatBytes(totalSize), maxDepth, treeString }; } catch (e) { return { error: `解析JSON时出错: ${e.message}` }; } }, splitImportedJsonFile: function(jsonData, originalFilteredData, originalFileName, splitMethod, config) { try { if (!jsonData || !Array.isArray(jsonData.files)) { uiManager.showError("JSON数据格式无效,缺少 'files' 数组。"); return null; } const baseMetadata = { ...jsonData }; delete baseMetadata.files; delete baseMetadata.totalFilesCount; delete baseMetadata.totalSize; delete baseMetadata.formattedTotalSize; const baseFileName = originalFileName.endsWith('.json') ? originalFileName.slice(0, -5) : originalFileName; const chunks = []; if (splitMethod === 'byCount') { const chunkSize = config.chunkSize; if (!chunkSize || chunkSize <= 0) { uiManager.showError("按数量拆分时,请输入有效的正整数。"); return null; } for (let i = 0; i < jsonData.files.length; i += chunkSize) { const chunkFiles = jsonData.files.slice(i, i + chunkSize); const chunkTotalSize = chunkFiles.reduce((sum, file) => sum + (Number(file.size) || 0), 0); const chunkJsonData = { ...baseMetadata, totalFilesCount: chunkFiles.length, totalSize: chunkTotalSize, formattedTotalSize: uiManager.formatBytes(chunkTotalSize), files: chunkFiles }; chunks.push({ data: chunkJsonData, filename: `${baseFileName}_part_${chunks.length + 1}.json` }); } } else if (splitMethod === 'byFolder') { const level = config.level; if (!level || level <= 0) { uiManager.showError("按目录层级拆分时,请输入有效的正整数。"); return null; } const commonPathPrefixForDepth = jsonData.commonPath ? `${jsonData.commonPath.replace(/\/$/, '')}/` : ''; const originalCommonPathForOutput = jsonData.commonPath ? `${jsonData.commonPath.replace(/\/$/, '')}/` : ''; const folders = new Map(); jsonData.files.forEach(file => { const fullPathForDepth = commonPathPrefixForDepth + file.path; const pathParts = fullPathForDepth.split('/').filter(p => p.trim() !== ''); const dirParts = pathParts.slice(0, -1); const groupKey = (dirParts.length >= level) ? dirParts.slice(0, level).join('/') : '_root_'; if (!folders.has(groupKey)) { folders.set(groupKey, []); } folders.get(groupKey).push(file); }); for (const [groupKey, filesInGroup] of folders.entries()) { const sanitizedGroupKey = groupKey.replace(/[\/:*?"<>|]/g, '_'); let newCommonPath, newFilesArray, outputFileName; if (groupKey === '_root_') { newCommonPath = originalCommonPathForOutput; newFilesArray = filesInGroup; outputFileName = `${baseFileName}_${sanitizedGroupKey}.json`; } else { newCommonPath = groupKey + '/'; newFilesArray = filesInGroup.map(originalFile => { const fullPathForThisFile = commonPathPrefixForDepth + originalFile.path; const newRelativePath = fullPathForThisFile.substring(groupKey.length).replace(/^\//, ''); return { ...originalFile, path: newRelativePath }; }); outputFileName = `${baseFileName}_${sanitizedGroupKey}.json`; } if (newFilesArray.length === 0) continue; const chunkTotalSize = newFilesArray.reduce((sum, file) => sum + (Number(file.size) || 0), 0); const chunkJsonData = { ...baseMetadata, commonPath: newCommonPath, totalFilesCount: newFilesArray.length, totalSize: chunkTotalSize, formattedTotalSize: uiManager.formatBytes(chunkTotalSize), files: newFilesArray }; chunks.push({ data: chunkJsonData, filename: outputFileName }); } } if (chunks.length > 0) { const verificationResult = coreLogic.verifyChunks(jsonData, chunks); if (!verificationResult.success) { const errorMessage = `校验失败!数据可能丢失!\n应有文件数: ${verificationResult.originalTotalFiles}, 拆分后总数: ${verificationResult.newTotalFiles}\n\n操作已取消。`; uiManager.showError(errorMessage.replace(/\n/g, '<br>'), 10000); return null; } if (splitMethod === 'byFolder') { const rootChunk = chunks.find(c => c.filename.includes('_root_.json')); if (rootChunk) { uiManager.showAlert(`提示:有 ${rootChunk.data.files.length} 个文件被打包到了 "_root_.json" 中。`, 4000); } } return { chunks, originalFilteredData, originalFileName, baseFileName }; } else { uiManager.showError("没有可供拆分的文件。请检查您的过滤设置。"); return null; } } catch (e) { console.error(`[${SCRIPT_NAME_SPLITTER}] 拆分失败:`, e); uiManager.showError(`拆分失败: ${e.message}.`); return null; } }, verifyChunks: function(originalData, chunks) { const originalTotalFiles = originalData.files.length; const newTotalFiles = chunks.reduce((sum, chunk) => sum + chunk.data.files.length, 0); return { success: originalTotalFiles === newTotalFiles, originalTotalFiles, newTotalFiles }; }, mergeJsonFiles: function(filesToMerge) { try { if (filesToMerge.length < 2) { uiManager.showError("请至少选择两个文件进行合并。"); return null; } const baseJson = filesToMerge[0].data; const mergedFilesArray = [...baseJson.files]; for (let i = 1; i < filesToMerge.length; i++) { const nextJson = filesToMerge[i].data; if (nextJson && Array.isArray(nextJson.files)) { mergedFilesArray.push(...nextJson.files); } else { uiManager.showError(`文件 "${filesToMerge[i].name}" 格式无效,已跳过。`); } } const finalJson = { ...baseJson }; finalJson.files = mergedFilesArray; finalJson.totalFilesCount = mergedFilesArray.length; const totalSize = mergedFilesArray.reduce((sum, file) => sum + (Number(file.size) || 0), 0); finalJson.totalSize = totalSize; finalJson.formattedTotalSize = uiManager.formatBytes(totalSize); return finalJson; } catch(e) { console.error(`[${SCRIPT_NAME_MERGER}] 合并失败:`, e); uiManager.showError(`合并失败: ${e.message}.`); return null; } } }; // --- UI Manager --- const uiManager = { showCompletionSummary: function(originalFileName, originalFilteredData, downloadedChunks) { const originalTotalFiles = originalFilteredData.files.length; const originalTotalSize = originalFilteredData.files.reduce((sum, file) => sum + (Number(file.size) || 0), 0); const listItems = downloadedChunks.map(chunk => `<li><strong>${chunk.filename}</strong>: ${chunk.data.files.length} 个文件 - ${this.formatBytes(chunk.data.totalSize)}</li>`).join(''); const newTotalFiles = downloadedChunks.reduce((sum, chunk) => sum + chunk.data.files.length, 0); const newTotalSize = downloadedChunks.reduce((sum, chunk) => sum + chunk.data.totalSize, 0); const summaryHtml = `<div style="text-align: left; line-height: 1.6;"><strong>原始文件 (过滤后):</strong> ${originalFileName}<br>- 总文件数: ${originalTotalFiles}<br>- 总大小: ${this.formatBytes(originalTotalSize)}<hr style="margin: 15px 0;"><strong>已下载文件列表 (${downloadedChunks.length}个):</strong><ul style="padding-left: 20px; margin-top: 10px; max-height: 150px; overflow-y: auto; background: #f7f7f7; border: 1px solid #eee; border-radius: 4px; padding: 10px;">${listItems}</ul><div style="font-weight: bold; margin-top: 10px; text-align: right;">合计: ${newTotalFiles} 个文件 - ${this.formatBytes(newTotalSize)}</div></div>`; this.showCenteredInfoModal("✅ 下载已启动!", summaryHtml); }, showCenteredInfoModal: function(title, htmlContent) { if (document.getElementById('summary-modal-overlay')) { document.getElementById('summary-modal-overlay').remove(); } const overlay = document.createElement('div'); overlay.id = 'summary-modal-overlay'; overlay.className = 'splitter-overlay'; const modalContainer = document.createElement('div'); modalContainer.className = 'summary-container'; modalContainer.innerHTML = `<h2 class="summary-title">${title}</h2><div class="summary-content">${htmlContent}</div><button id="summary-ok-btn" class="summary-ok-btn">确定</button>`; overlay.appendChild(modalContainer); document.body.appendChild(overlay); const close = () => overlay.remove(); modalContainer.querySelector('#summary-ok-btn').onclick = close; overlay.onclick = (e) => { if (e.target === overlay) { close(); } }; }, showProgressModal: function(message) { this.hideProgressModal(); const overlay = document.createElement('div'); overlay.id = 'progress-modal-overlay'; overlay.className = 'splitter-overlay'; overlay.style.zIndex = '10004'; overlay.innerHTML = `<div class="progress-container"><div class="progress-spinner"></div><div class="progress-text">${message}</div></div>`; document.body.appendChild(overlay); }, hideProgressModal: function() { const overlay = document.getElementById('progress-modal-overlay'); if (overlay) { overlay.remove(); } }, _downloadToFile: function(content, filename, contentType) { try { const blob = (content instanceof Blob) ? content : new Blob([content], { type: contentType }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(a.href); } catch (e) { console.error("下载失败:", e); this.showError(`下载文件 "${filename}" 失败。`); } }, formatBytes: function(bytes, decimals = 2) { if (bytes === 0) return '0 Bytes'; const k = 1024; const dm = decimals < 0 ? 0 : decimals; const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; }, showAlert: function(message, duration = 3000) { const el = document.createElement('div'); el.style.zIndex = '10005'; el.className = 'splitter-info-popup'; el.innerHTML = message; document.body.appendChild(el); setTimeout(() => { el.classList.add('fadeout'); setTimeout(() => el.remove(), 500); }, duration); }, showError: function(message, duration = 4000) { this.showAlert(`<span style="color: #ffcdd2;">⚠️ ${message}</span>`, duration); }, applyStyles: function() { // No custom styles needed for sidebar items, but keeping for modals. if (document.getElementById('splitter-tool-styles')) return; GM_addStyle(` .splitter-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center; z-index: 10000; } .splitter-modal-close-btn { position: absolute; top: 10px; right: 15px; font-size: 28px; font-weight: bold; color: #999; cursor: pointer; transition: color 0.3s; z-index: 1; } .splitter-container { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif; background: #fff; padding: 0; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); width: 90vw; max-width: 700px; text-align: center; position: relative; display: flex; flex-direction: column; max-height: 90vh; } .splitter-title { margin: 0; color: #333; padding: 1.5rem 2rem; border-bottom: 1px solid #f0f0f0; flex-shrink: 0; } .splitter-body { overflow-y: auto; padding: 1rem 2rem; flex-grow: 1; text-align: left; } .splitter-actions { padding: 1.5rem 2rem; border-top: 1px solid #f0f0f0; background: #fafafa; text-align: center; flex-shrink: 0; } .splitter-drop-area { border: 2px dashed #d9d9d9; padding: 40px 20px; border-radius: 8px; transition: all .3s; margin-bottom: 1.5rem; text-align: center; } .splitter-drop-area.drag-over { border-color: #1890ff; background-color: #e6f7ff; } #splitter-file-status, #merger-file-status { color: #52c41a; margin-top: 1rem; font-weight: 500; min-height: 1.2em; text-align: center;} #splitter-analysis-result { background-color: #fafafa; border: 1px solid #d9d9d9; border-radius: 4px; padding: 15px; margin-top: 1.5rem; text-align: left; max-height: 250px; overflow-y: auto; white-space: pre; font-family: 'Courier New', Courier, monospace; font-size: 0.85em; line-height: 1.6; } .splitter-options, .splitter-filter-options { margin: 1.5rem 0; border: 1px solid #f0f0f0; padding: 1.5rem; border-radius: 8px; text-align: left; } .splitter-options > div { margin-bottom: 1rem; } .splitter-options > div:last-child { margin-bottom: 0; } .button-primary, .download-action-btn, .button-secondary { font-size: 1rem; padding: 10px 15px; min-width: 170px; border: none; border-radius: 4px; cursor: pointer; transition: background-color .3s; margin: 5px; } .button-primary { background-color: #52c41a; color: #fff; } .button-primary:disabled { background-color: #d9d9d9; cursor: not-allowed; } .button-secondary { background-color: #1890ff; color: #fff; } .btn-green { background-color: #52c41a; color: white; } .btn-blue { background-color: #1890ff; color: white; } .btn-grey { background-color: #888; color: white; } .link-style-btn { background: none; border: none; color: #1890ff; cursor: pointer; padding: 0; font-size: inherit; text-decoration: underline; } .splitter-footer { padding: 1rem 2rem; font-size: 0.8em; color: #999; text-align: center; background: #fafafa; border-top: 1px solid #f0f0f0; flex-shrink: 0;} .splitter-info-popup { position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); background-color: rgba(0,0,0,0.8); color: white; padding: 12px 22px; border-radius: 5px; opacity: 1; transition: opacity 0.5s ease-out; font-size: 1em; max-width: 90vw; } .summary-container { background: #fff; padding: 25px 30px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); width: 90vw; max-width: 600px; text-align: center; } .summary-ok-btn { font-size: 1.1rem; padding: 10px 40px; background-color: #1890ff; color: #fff; border: none; border-radius: 4px; cursor: pointer; } .chunk-list { flex-grow: 1; overflow-y: auto; border: 1px solid #eee; border-radius: 4px; padding: 10px; margin-top: 1rem;} .chunk-item { display: flex; align-items: center; padding: 8px; border-bottom: 1px solid #f0f0f0; } .chunk-item:last-child { border-bottom: none; } .chunk-item input[type="checkbox"] { margin-right: 15px; flex-shrink: 0; } .chunk-item label { word-break: break-all; } .chunk-item-details { color: #666; font-size: 0.9em; margin-left: auto; white-space: nowrap; padding-left: 10px; } .selection-header, .selection-summary, .merger-summary { padding-bottom: 1rem; display: flex; justify-content: space-between; align-items: center; flex-shrink: 0; border-bottom: 1px solid #f0f0f0; padding: 1rem; margin-bottom: 1rem; background: #fafafa;} .selection-summary, .merger-summary { border-top: 1px solid #f0f0f0; margin-top: 1rem; margin-bottom: 0; font-weight: bold; } .progress-container { background: white; color: black; padding: 30px 40px; border-radius: 8px; display: flex; align-items: center; font-size: 1.2em; } .progress-spinner { border: 4px solid #f3f3f3; border-top: 4px solid #3498db; border-radius: 50%; width: 30px; height: 30px; animation: spin 1s linear infinite; margin-right: 20px; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } `); }, showDownloadSelectionView: function(resultData) { const { chunks, originalFilteredData, originalFileName, baseFileName } = resultData; const modalContainer = document.querySelector('.splitter-container'); const splitterBody = modalContainer.querySelector('.splitter-body'); const splitterActions = modalContainer.querySelector('.splitter-actions'); const splitterTitle = modalContainer.querySelector('.splitter-title'); const originalBodyHTML = splitterBody.innerHTML; const originalActionsHTML = splitterActions.innerHTML; const originalTitle = splitterTitle.textContent; const originalTotalFiles = originalFilteredData.files.length; const originalTotalSize = originalFilteredData.files.reduce((sum, file) => sum + (Number(file.size) || 0), 0); const chunkListItems = chunks.map((chunk, index) => `<div class="chunk-item"><input type="checkbox" class="chunk-checkbox" id="chunk-checkbox-${index}" value="${index}" checked><label for="chunk-checkbox-${index}"><strong>${chunk.filename}</strong></label><span class="chunk-item-details">${chunk.data.files.length} 个文件, ${this.formatBytes(chunk.data.totalSize)}</span></div>`).join(''); splitterTitle.textContent = '拆分完成 - 请选择文件下载'; splitterBody.innerHTML = `<div class="selection-header"><span><strong>原始文件 (过滤后):</strong> ${originalTotalFiles} 个, ${this.formatBytes(originalTotalSize)}</span><button id="back-to-splitter-btn" class="link-style-btn">返回修改</button></div><div class="chunk-list">${chunkListItems}</div><div class="selection-summary"><span>已选择: 0 个文件, 0 Bytes</span></div>`; splitterActions.innerHTML = ` <button id="download-seq-btn" class="download-action-btn btn-green">顺序下载选中项 (推荐)</button> <button id="download-ind-btn" class="download-action-btn btn-grey">单独下载选中项</button> <button id="download-zip-btn" class="download-action-btn btn-blue">打包下载全部 (ZIP)</button> `; const allCheckboxes = splitterBody.querySelectorAll('.chunk-checkbox'); const selectionSummaryEl = splitterBody.querySelector('.selection-summary > span'); const updateSelectionSummary = () => { const selectedChunks = [...allCheckboxes].filter(cb => cb.checked).map(cb => chunks[cb.value]); const selectedFileCount = selectedChunks.reduce((sum, chunk) => sum + chunk.data.files.length, 0); const selectedSize = selectedChunks.reduce((sum, chunk) => sum + chunk.data.totalSize, 0); selectionSummaryEl.textContent = `已选择: ${selectedFileCount} 个文件, ${this.formatBytes(selectedSize)}`; }; const selectAllCheckbox = document.createElement('input'); selectAllCheckbox.type = 'checkbox'; selectAllCheckbox.checked = true; selectAllCheckbox.onchange = () => { allCheckboxes.forEach(cb => cb.checked = selectAllCheckbox.checked); updateSelectionSummary(); }; allCheckboxes.forEach(cb => { cb.addEventListener('change', () => { selectAllCheckbox.checked = [...allCheckboxes].every(c => c.checked); updateSelectionSummary(); }); }); const selectAllLabel = document.createElement('label'); selectAllLabel.appendChild(selectAllCheckbox); selectAllLabel.append(' 全选/全不选'); splitterBody.querySelector('.selection-header').prepend(selectAllLabel); updateSelectionSummary(); splitterBody.querySelector('#back-to-splitter-btn').onclick = () => { splitterTitle.textContent = originalTitle; splitterBody.innerHTML = originalBodyHTML; splitterActions.innerHTML = originalActionsHTML; this.rebindMainViewEvents(modalContainer); }; const downloadZip = (chunksToZip, zipFilename) => { try { if (typeof JSZip === 'undefined') { this.showError("错误:ZIP库未能加载,请检查网络或浏览器插件冲突。"); return; } this.showProgressModal('正在生成ZIP压缩包,请稍候...'); const zip = new JSZip(); chunksToZip.forEach(chunk => zip.file(chunk.filename, JSON.stringify(chunk.data, null, 2))); zip.generateAsync({ type: "blob", compression: "DEFLATE" }) .then(blob => { this.hideProgressModal(); this._downloadToFile(blob, zipFilename, 'application/zip'); this.showCompletionSummary(originalFileName, originalFilteredData, chunksToZip); }) .catch(err => { this.hideProgressModal(); this.showError(`ZIP打包失败: ${err.message}`); console.error(err); }); } catch(e) { this.hideProgressModal(); this.showError(`下载操作失败: ${e.message}`); console.error(e); } }; const downloadSequentially = async (chunksToDownload) => { const delay = ms => new Promise(res => setTimeout(res, ms)); for (let i = 0; i < chunksToDownload.length; i++) { const chunk = chunksToDownload[i]; this.showProgressModal(`正在下载: ${chunk.filename} (${i + 1}/${chunksToDownload.length})`); this._downloadToFile(JSON.stringify(chunk.data, null, 2), chunk.filename, 'application/json'); await delay(SEQUENTIAL_DOWNLOAD_DELAY); } this.hideProgressModal(); this.showCompletionSummary(originalFileName, originalFilteredData, chunksToDownload); }; splitterActions.querySelector('#download-ind-btn').onclick = () => { const selectedChunks = [...allCheckboxes].filter(cb => cb.checked).map(cb => chunks[cb.value]); if (selectedChunks.length === 0) { this.showError("请至少选择一个文件。"); return; } if (selectedChunks.length >= BROWSER_DOWNLOAD_LIMIT) { this.showError(`选择文件过多(${selectedChunks.length}个),可能被浏览器拦截,建议使用“顺序下载”。`, 5000); } selectedChunks.forEach(chunk => this._downloadToFile(JSON.stringify(chunk.data, null, 2), chunk.filename, 'application/json')); this.showCompletionSummary(originalFileName, originalFilteredData, selectedChunks); }; splitterActions.querySelector('#download-seq-btn').onclick = () => { const selectedChunks = [...allCheckboxes].filter(cb => cb.checked).map(cb => chunks[cb.value]); if (selectedChunks.length === 0) { this.showError("请至少选择一个文件。"); return; } downloadSequentially(selectedChunks); }; splitterActions.querySelector('#download-zip-btn').onclick = () => downloadZip(chunks, `${baseFileName}_all.zip`); if (typeof JSZip === 'undefined') { const zipBtn = splitterActions.querySelector('#download-zip-btn'); zipBtn.disabled = true; zipBtn.title = "ZIP库加载失败,此功能不可用"; } }, rebindMainViewEvents: function(modalContainer) { this.fileContent = this.fileContent || null; this.originalFileName = this.originalFileName || ''; this.originalFilteredData = this.originalFilteredData || null; const dropArea = modalContainer.querySelector('#splitter-drop-area'); const fileInput = modalContainer.querySelector('#splitter-file-input'); const browseBtn = modalContainer.querySelector('#splitter-browse-btn'); const analyzeBtn = modalContainer.querySelector('#splitter-analyze-btn'); const startBtn = modalContainer.querySelector('#splitter-start-btn'); const radioButtons = modalContainer.querySelectorAll('input[name="split-method"]'); const handleFile = (file) => { const statusDiv = modalContainer.querySelector('#splitter-file-status'); if (file && file.name.endsWith('.json')) { const reader = new FileReader(); reader.onload = (e) => { this.fileContent = e.target.result; this.originalFileName = file.name; statusDiv.textContent = `已加载: ${file.name}`; analyzeBtn.disabled = false; startBtn.disabled = false; modalContainer.querySelector('#splitter-analysis-result').style.display = 'none'; }; reader.readAsText(file, 'UTF-8'); } else { this.showError("请选择一个有效的 .json 文件。"); this.fileContent = null; this.originalFileName = ''; statusDiv.textContent = ''; analyzeBtn.disabled = true; startBtn.disabled = true; } }; browseBtn.onclick = () => fileInput.click(); ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { if(dropArea.getAttribute('listener') !== 'true') { dropArea.addEventListener(eventName, e => { e.preventDefault(); e.stopPropagation(); if (eventName === 'dragover') dropArea.classList.add('drag-over'); if (eventName === 'dragleave' || eventName === 'drop') dropArea.classList.remove('drag-over'); if (eventName === 'drop') { handleFile(e.dataTransfer.files[0]); } }); } }); dropArea.setAttribute('listener', 'true'); // Prevent re-adding listeners fileInput.onchange = (e) => handleFile(e.target.files[0]); analyzeBtn.onclick = () => this.analyzeHandler(modalContainer); startBtn.onclick = () => this.startHandler(modalContainer); const toggleInputs = () => { const selectedMethod = modalContainer.querySelector('input[name="split-method"]:checked').value; modalContainer.querySelector('#splitter-level-container').style.display = (selectedMethod === 'byFolder') ? 'inline-block' : 'none'; modalContainer.querySelector('#splitter-chunk-size-container').style.display = (selectedMethod === 'byCount') ? 'inline-block' : 'none'; }; radioButtons.forEach(radio => radio.onchange = toggleInputs); toggleInputs(); }, showSplitterTool: function() { if (document.getElementById('splitter-modal-overlay')) return; this.applyStyles(); const overlay = document.createElement('div'); overlay.id = 'splitter-modal-overlay'; overlay.className = 'splitter-overlay'; const modalContainer = document.createElement('div'); modalContainer.className = 'splitter-container'; modalContainer.innerHTML = ` <span id="splitter-modal-close-btn" class="splitter-modal-close-btn">×</span> <h1 class="splitter-title">${SCRIPT_NAME_SPLITTER}</h1> <div class="splitter-body"> <div id="splitter-drop-area" class="splitter-drop-area"> <p>拖拽一个 .json 文件到此处 或 <button id="splitter-browse-btn" class="link-style-btn">点击选择文件</button></p> <input type="file" id="splitter-file-input" accept=".json" style="display: none;"> <p id="splitter-file-status"></p> </div> <div style="text-align:center; margin-bottom: 1.5rem;"><button id="splitter-analyze-btn" class="button-secondary" style="margin:0;">📊 分析JSON结构</button></div> <div id="splitter-analysis-result" style="display: none;"></div> <div class="splitter-options"> <strong>选择拆分模式:</strong> <div><label><input type="radio" name="split-method" value="byFolder" checked> 按目录层级</label> <span id="splitter-level-container"><label>层数: <input type="number" id="splitter-level" value="1" min="1" style="width: 60px;"></label></span></div> <div><label><input type="radio" name="split-method" value="byCount"> 按文件数量</label> <span id="splitter-chunk-size-container"><label><input type="number" id="splitter-chunk-size" value="500" min="1" style="width: 80px;"> 个文件/份</label></span></div> </div> <div class="splitter-filter-options"> <strong>元数据过滤设置 (可选):</strong> <div> <label style="margin-right: 10px;"><input type="checkbox" class="filter-ext" value="nfo">.nfo</label> <label style="margin-right: 10px;"><input type="checkbox" class="filter-ext" value="jpg,jpeg">.jpg/.jpeg</label> <label style="margin-right: 10px;"><input type="checkbox" class="filter-ext" value="png">.png</label> <label>自定义: <input type="text" id="custom-filter-extensions" placeholder="txt,url (逗号隔开)" style="width: 180px;"></label> </div> </div> </div> <div class="splitter-actions"> <button id="splitter-start-btn" class="button-primary" disabled>🚀 开始拆分</button> </div> <div class="splitter-footer">v${SCRIPT_VERSION} <span style="margin-left: 15px;">鸣谢: @一只氧气</span></div> `; document.body.appendChild(overlay); overlay.appendChild(modalContainer); modalContainer.querySelector('#splitter-modal-close-btn').onclick = () => overlay.remove(); this.rebindMainViewEvents(modalContainer); }, showMergerTool: function() { if (document.getElementById('splitter-modal-overlay')) return; this.applyStyles(); const overlay = document.createElement('div'); overlay.id = 'splitter-modal-overlay'; overlay.className = 'splitter-overlay'; const modalContainer = document.createElement('div'); modalContainer.className = 'splitter-container'; modalContainer.innerHTML = ` <span id="splitter-modal-close-btn" class="splitter-modal-close-btn">×</span> <h1 class="splitter-title">${SCRIPT_NAME_MERGER}</h1> <div class="splitter-body"> <div id="merger-drop-area" class="splitter-drop-area"> <p>拖拽多个 .json 文件到此处</p> <p style="font-size: 0.9em; color: #999;">或</p> <input type="file" id="merger-file-input" accept=".json" multiple style="display: none;"> <button id="merger-browse-btn" class="button-secondary" style="margin:0;">点击选择文件</button> <p id="merger-file-status"></p> </div> <div id="merger-file-list-container" style="display:none;"> <strong>选择要合并的文件 (第一个将作为元数据基准):</strong> <div class="chunk-list" id="merger-file-list"></div> <div class="merger-summary"><span>已选择: 0 个文件, 0 Bytes</span></div> </div> </div> <div class="splitter-actions"> <button id="merger-start-btn" class="button-primary" disabled>🔄 合并并下载</button> </div> <div class="splitter-footer">v${SCRIPT_VERSION} <span style="margin-left: 15px;">鸣谢: @一只氧气</span></div> `; document.body.appendChild(overlay); overlay.appendChild(modalContainer); modalContainer.querySelector('#splitter-modal-close-btn').onclick = () => overlay.remove(); // --- Merger Logic --- let uploadedFiles = []; const dropArea = modalContainer.querySelector('#merger-drop-area'); const fileInput = modalContainer.querySelector('#merger-file-input'); const browseBtn = modalContainer.querySelector('#merger-browse-btn'); const statusDiv = modalContainer.querySelector('#merger-file-status'); const listContainer = modalContainer.querySelector('#merger-file-list-container'); const fileListDiv = modalContainer.querySelector('#merger-file-list'); const summaryEl = modalContainer.querySelector('.merger-summary > span'); const startBtn = modalContainer.querySelector('#merger-start-btn'); const renderFileList = () => { fileListDiv.innerHTML = uploadedFiles.map((file, index) => ` <div class="chunk-item"> <input type="checkbox" class="merger-checkbox" id="merger-checkbox-${index}" value="${index}" checked> <label for="merger-checkbox-${index}"><strong>${file.name}</strong></label> <span class="chunk-item-details">${file.data.files.length} 个文件, ${this.formatBytes(file.data.files.reduce((s,f) => s + (Number(f.size) || 0), 0))}</span> </div> `).join(''); listContainer.style.display = 'block'; startBtn.disabled = uploadedFiles.length < 2; const allCheckboxes = fileListDiv.querySelectorAll('.merger-checkbox'); const updateSummary = () => { const selectedFiles = [...allCheckboxes].filter(cb => cb.checked).map(cb => uploadedFiles[cb.value]); const totalFiles = selectedFiles.reduce((sum, f) => sum + f.data.files.length, 0); const totalSize = selectedFiles.reduce((sum, f) => sum + f.data.files.reduce((s,i) => s + (Number(i.size) || 0), 0), 0); summaryEl.textContent = `已选择: ${totalFiles} 个文件, ${this.formatBytes(totalSize)}`; startBtn.disabled = selectedFiles.length < 2; }; allCheckboxes.forEach(cb => cb.addEventListener('change', updateSummary)); updateSummary(); }; const handleFiles = (files) => { if (files.length === 0) return; statusDiv.textContent = `正在加载 ${files.length} 个文件...`; uploadedFiles = []; const promises = [...files].map(file => new Promise((resolve, reject) => { if (file.name.endsWith('.json')) { const reader = new FileReader(); reader.onload = e => { try { const data = JSON.parse(e.target.result); if (!data || !Array.isArray(data.files)) { return reject(`文件 ${file.name} 格式无效!`); } resolve({ name: file.name, data: data }); } catch (err) { reject(`文件 ${file.name} 解析失败!`); } }; reader.onerror = () => reject(`读取文件 ${file.name} 失败!`); reader.readAsText(file, 'UTF-8'); } else { resolve(null); } })); Promise.all(promises).then(results => { uploadedFiles = results.filter(r => r !== null); if (uploadedFiles.length > 0) { statusDiv.textContent = `成功加载 ${uploadedFiles.length} 个JSON文件。`; renderFileList(); } else { this.showError("未能成功加载任何有效的JSON文件。"); statusDiv.textContent = ''; } }).catch(error => { this.showError(error); statusDiv.textContent = '加载失败。'; }); }; browseBtn.onclick = () => fileInput.click(); fileInput.onchange = (e) => handleFiles(e.target.files); dropArea.ondragover = (e) => { e.preventDefault(); e.stopPropagation(); dropArea.classList.add('drag-over'); }; dropArea.ondragleave = (e) => { e.preventDefault(); e.stopPropagation(); dropArea.classList.remove('drag-over'); }; dropArea.ondrop = (e) => { e.preventDefault(); e.stopPropagation(); dropArea.classList.remove('drag-over'); handleFiles(e.dataTransfer.files); }; startBtn.onclick = () => { const allCheckboxes = fileListDiv.querySelectorAll('.merger-checkbox'); const selectedFiles = [...allCheckboxes].filter(cb => cb.checked).map(cb => uploadedFiles[cb.value]); const mergedJson = coreLogic.mergeJsonFiles(selectedFiles); if (mergedJson) { this.showAlert("合并成功!开始下载...", 3000); this._downloadToFile(JSON.stringify(mergedJson, null, 2), "merged_files.json", "application/json"); } }; }, /** * Creates a menu item element that mimics the native sidebar items. * @param {string} id - The ID for the new list item. * @param {string} text - The text to display for the menu item. * @param {string} iconHref - The SVG icon's xlink:href value. * @returns {HTMLLIElement} - The fully constructed list item element. */ createToolMenuItem: function(id, text, iconHref) { const li = document.createElement('li'); li.id = id; li.className = 'ant-menu-item ant-menu-item-only-child'; li.setAttribute('role', 'menuitem'); li.style.paddingLeft = '24px'; // Match native padding const span = document.createElement('span'); span.className = 'ant-menu-title-content'; const a = document.createElement('a'); a.className = 'menu-item'; a.href = '#'; // Use a dummy href to make it behave like a link const iconWrapper = document.createElement('div'); iconWrapper.className = 'menu-icon-wrapper'; const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.setAttribute('class', 'icon menu-icon'); svg.setAttribute('aria-hidden', 'true'); const use = document.createElementNS('http://www.w3.org/2000/svg', 'use'); use.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', iconHref); svg.appendChild(use); iconWrapper.appendChild(svg); const textDiv = document.createElement('div'); textDiv.className = 'menu-text'; textDiv.textContent = text; a.appendChild(iconWrapper); a.appendChild(textDiv); span.appendChild(a); li.appendChild(span); return li; }, /** * Injects tool links into the stable left sidebar menu. */ injectSidebarTools: function() { const checkInterval = setInterval(() => { // More specific selector for the main menu, excluding the bottom one. const sidebarMenu = document.querySelector('.side-menu-container > ul.side-menu:not(.bottom-menu)'); if (sidebarMenu && !document.getElementById('gm-tool-splitter-li')) { // Define short names and icons for the menu items const SHORT_NAME_SPLITTER = "JSON拆分"; const SHORT_NAME_MERGER = "JSON合并"; const splitterIcon = '#business_share_24_1'; // Use the "Share" icon - it's guaranteed to exist const mergerIcon = '#business_toolcenter_24_1'; // Keep the tool icon // --- Create and inject Splitter tool menu item --- const splitterItem = this.createToolMenuItem('gm-tool-splitter-li', SHORT_NAME_SPLITTER, splitterIcon); splitterItem.onclick = (e) => { e.preventDefault(); this.showSplitterTool(); }; sidebarMenu.appendChild(splitterItem); // --- Create and inject Merger tool menu item --- const mergerItem = this.createToolMenuItem('gm-tool-merger-li', SHORT_NAME_MERGER, mergerIcon); mergerItem.onclick = (e) => { e.preventDefault(); this.showMergerTool(); }; sidebarMenu.appendChild(mergerItem); clearInterval(checkInterval); } }, 1000); }, fileContent: null, originalFileName: '', originalFilteredData: null, analyzeHandler: function(modalContainer) { const jsonData = this.getFilteredJsonData(modalContainer); if (!jsonData) { this.showError("请先加载JSON文件。"); return; } const analysis = coreLogic.analyzeJsonStructure(jsonData); const analysisResultDiv = modalContainer.querySelector('#splitter-analysis-result'); if (analysis.error) { analysisResultDiv.innerHTML = `<span style="color:red;">分析失败: ${analysis.error}</span>`; } else { analysisResultDiv.innerHTML = `<strong>分析结果 (已过滤):</strong><br>- 总文件数: ${analysis.fileCount}<br>- 总大小: ${analysis.totalSize}<br><strong>目录结构预览:</strong><br>${analysis.treeString}`; } analysisResultDiv.style.display = 'block'; }, startHandler: function(modalContainer) { const jsonData = this.getFilteredJsonData(modalContainer); if (!jsonData) { this.showError("请先加载JSON文件。"); return; } this.originalFilteredData = jsonData; const method = modalContainer.querySelector('input[name="split-method"]:checked').value; const config = {}; if (method === 'byFolder') { config.level = parseInt(modalContainer.querySelector('#splitter-level').value, 10); } else { config.chunkSize = parseInt(modalContainer.querySelector('#splitter-chunk-size').value, 10); } const result = coreLogic.splitImportedJsonFile(jsonData, this.originalFilteredData, this.originalFileName, method, config); if(result) { this.showDownloadSelectionView(result); } }, getFilteredJsonData: function(modalContainer) { if (!this.fileContent) return null; let jsonData; try { jsonData = JSON.parse(this.fileContent); } catch(e) { this.showError("JSON文件解析失败,请检查文件内容是否正确。"); return null; } const extensionsToFilter = new Set(); modalContainer.querySelectorAll('.filter-ext:checked').forEach(cb => { cb.value.split(',').forEach(ext => extensionsToFilter.add(ext.trim().toLowerCase())); }); const customInput = modalContainer.querySelector('#custom-filter-extensions').value; if (customInput) { customInput.split(',').forEach(ext => { const trimmedExt = ext.trim().toLowerCase(); if (trimmedExt) extensionsToFilter.add(trimmedExt); }); } if (extensionsToFilter.size > 0) { const originalCount = jsonData.files.length; jsonData.files = jsonData.files.filter(file => { const parts = file.path.split('.'); if (parts.length < 2) return true; const extension = parts.pop().toLowerCase(); return !extensionsToFilter.has(extension); }); const filteredCount = originalCount - jsonData.files.length; if (filteredCount > 0) { uiManager.showAlert(`已根据您的设置过滤掉 ${filteredCount} 个文件。`, 2500); } } return jsonData; }, }; // --- Script Entry Point --- uiManager.applyStyles(); uiManager.injectSidebarTools(); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址