diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 64a4bc2..fbcf396 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -57,6 +57,8 @@ jobs: dist-electron/*.dmg dist-electron/*.zip dist-electron/*.exe + dist-electron/*.blockmap + dist-electron/latest*.yml release: needs: build @@ -69,6 +71,9 @@ jobs: path: dist merge-multiple: true + - name: List release files + run: ls -la dist/ + - uses: softprops/action-gh-release@v2 with: files: dist/* diff --git a/electron/main.js b/electron/main.js index 3d2af86..8af617e 100644 --- a/electron/main.js +++ b/electron/main.js @@ -171,6 +171,34 @@ app.whenReady().then(async () => { return null; }); + ipcMain.handle('scan-directory', async (event, dirPath) => { + const fs = require('fs'); + const pathModule = require('path'); + try { + if (!dirPath || !fs.existsSync(dirPath)) return []; + const entries = fs.readdirSync(dirPath, { withFileTypes: true }); + const files = []; + for (const entry of entries) { + if (entry.isFile() && !entry.name.startsWith('.')) { + const fullPath = pathModule.join(dirPath, entry.name); + try { + const stat = fs.statSync(fullPath); + files.push({ + name: entry.name, + path: fullPath, + size: stat.size, + mtime: stat.mtimeMs, + }); + } catch { /* skip unreadable files */ } + } + } + return files; + } catch (e) { + console.error('scan-directory error:', e); + return []; + } + }); + ipcMain.handle('get-downloads-path', async () => { try { return app.getPath('downloads'); diff --git a/electron/preload.js b/electron/preload.js index d0af835..5d247a7 100644 --- a/electron/preload.js +++ b/electron/preload.js @@ -44,6 +44,7 @@ contextBridge.exposeInMainWorld('electronAPI', { // 选择目录 selectDirectory: () => ipcRenderer.invoke('select-directory'), + scanDirectory: (dirPath) => ipcRenderer.invoke('scan-directory', dirPath), getDownloadsPath: () => ipcRenderer.invoke('get-downloads-path'), // 批量Reels - 烧录字幕 diff --git a/package-lock.json b/package-lock.json index 119bbda..d149585 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pymediatools", - "version": "2.0.2", + "version": "2.1.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pymediatools", - "version": "2.0.2", + "version": "2.1.2", "dependencies": { "archiver": "^7.0.1", "diff-match-patch": "^1.0.5", @@ -68,9 +68,9 @@ } }, "node_modules/@electron/asar/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -260,9 +260,9 @@ } }, "node_modules/@electron/universal/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -1954,9 +1954,9 @@ "optional": true }, "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -2682,9 +2682,9 @@ } }, "node_modules/dir-compare/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -3841,9 +3841,9 @@ } }, "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index df1d1b9..027b274 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pymediatools", - "version": "2.1.0", + "version": "2.1.2", "description": "统一媒体工具包 - 字幕对齐 + 媒体转换", "author": { "name": "TimCode", @@ -86,6 +86,7 @@ "nsis", "zip" ], + "artifactName": "${productName}-Setup-${version}.${ext}", "icon": "assets/icon.ico", "extraResources": [ { diff --git a/src/reels-batch-table.js b/src/reels-batch-table.js index 0624b94..4ef1fae 100644 --- a/src/reels-batch-table.js +++ b/src/reels-batch-table.js @@ -16,8 +16,72 @@ const _batchTableState = { visible: false, container: null, + // ── 多标签页 ── + tabs: [ + { id: 'tab_1', name: '默认', materialDir: '', lastRefreshTime: null, tasks: [] } + ], + activeTabId: 'tab_1', + nextTabId: 2, + // ── 批量选择 ── + selectedRows: new Set(), }; +// ── 标签页辅助 ── +function _getActiveTab() { + return _batchTableState.tabs.find(t => t.id === _batchTableState.activeTabId) || _batchTableState.tabs[0]; +} + +function _syncTasksToActiveTab() { + const tab = _getActiveTab(); + if (tab) tab.tasks = _serializeTasks(window._reelsState.tasks); +} + +function _loadTabTasks(tab) { + window._reelsState.tasks = (tab.tasks || []).map(t => ({ ...t })); + window._reelsState.selectedIdx = -1; +} + +function _switchToTab(tabId) { + if (tabId === _batchTableState.activeTabId) return; + // Save current tab tasks + _syncTasksToActiveTab(); + _batchTableState.activeTabId = tabId; + const tab = _getActiveTab(); + _loadTabTasks(tab); + _batchTableState.selectedRows = new Set(); + _renderBatchTable(); +} + +function _addTab(name) { + const id = 'tab_' + _batchTableState.nextTabId++; + const tab = { id, name: name || `标签${_batchTableState.tabs.length + 1}`, materialDir: '', lastRefreshTime: null, tasks: [] }; + _batchTableState.tabs.push(tab); + _switchToTab(id); +} + +function _removeTab(tabId) { + if (_batchTableState.tabs.length <= 1) { alert('至少保留一个标签页'); return; } + const idx = _batchTableState.tabs.findIndex(t => t.id === tabId); + if (idx < 0) return; + if (!confirm(`确定删除标签「${_batchTableState.tabs[idx].name}」及其所有任务?`)) return; + _batchTableState.tabs.splice(idx, 1); + if (_batchTableState.activeTabId === tabId) { + _batchTableState.activeTabId = _batchTableState.tabs[Math.min(idx, _batchTableState.tabs.length - 1)].id; + _loadTabTasks(_getActiveTab()); + } + _renderBatchTable(); +} + +function _renameTab(tabId) { + const tab = _batchTableState.tabs.find(t => t.id === tabId); + if (!tab) return; + const newName = prompt('输入标签名称:', tab.name); + if (newName && newName.trim()) { + tab.name = newName.trim(); + _renderBatchTable(); + } +} + // ═══════════════════════════════════════════════════════ // 2. Initialization // ═══════════════════════════════════════════════════════ @@ -42,6 +106,8 @@ function reelsToggleBatchTable() { if (!_batchTableState.container) _initBatchTable(); _batchTableState.visible = !_batchTableState.visible; if (_batchTableState.visible) { + // Sync current tasks into active tab on open + _syncTasksToActiveTab(); _renderBatchTable(); _batchTableState.container.style.display = 'flex'; } else { @@ -271,15 +337,56 @@ function _renderBatchTable() { // 自动保存当前配置到 localStorage _batchAutoSave(); const tasks = state.tasks || []; + const activeTab = _getActiveTab(); // 获取已保存的卡片模板列表 const cardTemplates = _getOverlayGroupPresetList(); const subtitlePresets = _getSubtitlePresetList(); + // 标签栏 HTML + const tabsHtml = _batchTableState.tabs.map(tab => { + const isActive = tab.id === _batchTableState.activeTabId; + return `
| ☐ | # | 🖼 背景素材 | 🔊 音频 | 📝 字幕 | -� 文案内容 | +📃 文案内容 | 🎵 配乐 | 📋 覆层标题 | 📋 覆层内容 | @@ -351,7 +480,7 @@ function _renderBatchTable() { @@ -362,6 +491,9 @@ function _renderBatchTable() { // 事件 _bindBatchTableEvents(); + + // 更新批量选中计数 + _updateBatchSelectCount(); } function _renderBatchRow(task, idx, subtitlePresets, cardTemplates) { @@ -407,7 +539,8 @@ function _renderBatchRow(task, idx, subtitlePresets, cardTemplates) { ).join(''); return ` -
|---|---|---|---|---|---|---|---|---|---|
| ${idx + 1} |
@@ -502,6 +635,110 @@ function _bindBatchTableEvents() {
// ── Language searchable picker ──
_initLangPicker(container);
+ // ══ Tab bar events ══
+ const tabBar = container.querySelector('.rbt-tabs-scroll');
+ if (tabBar) {
+ tabBar.addEventListener('click', (e) => {
+ // Add new tab
+ if (e.target.closest('.rbt-tab-add')) {
+ _addTab();
+ return;
+ }
+ // Close tab
+ const closeEl = e.target.closest('.rbt-tab-close');
+ if (closeEl) {
+ e.stopPropagation();
+ _removeTab(closeEl.dataset.tabId);
+ return;
+ }
+ // Switch tab
+ const tabEl = e.target.closest('.rbt-tab');
+ if (tabEl && tabEl.dataset.tabId) {
+ _switchToTab(tabEl.dataset.tabId);
+ }
+ });
+ tabBar.addEventListener('dblclick', (e) => {
+ const tabEl = e.target.closest('.rbt-tab');
+ if (tabEl && tabEl.dataset.tabId && !tabEl.classList.contains('rbt-tab-add')) {
+ _renameTab(tabEl.dataset.tabId);
+ }
+ });
+ }
+
+ // ══ Material folder selection & refresh ══
+ container.querySelector('#rbt-select-mat-dir')?.addEventListener('click', async () => {
+ if (window.electronAPI && window.electronAPI.selectDirectory) {
+ const dir = await window.electronAPI.selectDirectory();
+ if (dir) {
+ const tab = _getActiveTab();
+ tab.materialDir = dir;
+ _renderBatchTable();
+ }
+ } else {
+ alert('请在桌面应用中使用此功能');
+ }
+ });
+ container.querySelector('#rbt-refresh-mat')?.addEventListener('click', () => {
+ _refreshMaterialFolder();
+ });
+
+ // ══ Batch selection events ══
+ container.querySelector('#rbt-select-all')?.addEventListener('change', (e) => {
+ const tasks = window._reelsState.tasks || [];
+ _batchTableState.selectedRows = new Set(e.target.checked ? tasks.map((_, i) => i) : []);
+ container.querySelectorAll('.rbt-row-check').forEach(cb => cb.checked = e.target.checked);
+ _updateBatchSelectCount();
+ });
+ container.querySelector('#rbt-invert-select')?.addEventListener('click', () => {
+ const tasks = window._reelsState.tasks || [];
+ const newSet = new Set();
+ for (let i = 0; i < tasks.length; i++) {
+ if (!_batchTableState.selectedRows.has(i)) newSet.add(i);
+ }
+ _batchTableState.selectedRows = newSet;
+ container.querySelectorAll('.rbt-row-check').forEach(cb => {
+ const idx = parseInt(cb.dataset.idx);
+ cb.checked = newSet.has(idx);
+ });
+ _updateBatchSelectCount();
+ });
+ container.querySelector('#rbt-deselect-all')?.addEventListener('click', () => {
+ _batchTableState.selectedRows = new Set();
+ container.querySelectorAll('.rbt-row-check').forEach(cb => cb.checked = false);
+ const selectAll = container.querySelector('#rbt-select-all');
+ if (selectAll) selectAll.checked = false;
+ _updateBatchSelectCount();
+ });
+
+ // ══ Batch template apply ══
+ container.querySelector('#rbt-apply-batch-sub')?.addEventListener('click', () => {
+ const val = container.querySelector('#rbt-batch-sub-tpl')?.value;
+ if (!val) { alert('请先选择字幕模板'); return; }
+ const indices = _getSelectedIndices();
+ if (indices.length === 0) { alert('请先勾选需要批量设置的行'); return; }
+ for (const idx of indices) {
+ const task = window._reelsState.tasks[idx];
+ if (task) task._subtitlePreset = val;
+ }
+ _renderBatchTable();
+ alert(`✅ 已将字幕模板「${val}」应用到 ${indices.length} 行`);
+ });
+ container.querySelector('#rbt-apply-batch-card')?.addEventListener('click', () => {
+ const val = container.querySelector('#rbt-batch-card-tpl')?.value;
+ if (!val) { alert('请先选择覆层预设'); return; }
+ const indices = _getSelectedIndices();
+ if (indices.length === 0) { alert('请先勾选需要批量设置的行'); return; }
+ for (const idx of indices) {
+ const task = window._reelsState.tasks[idx];
+ if (task) {
+ task._overlayPresetName = val;
+ _applyOverlayGroupPresetToTask(task, val);
+ }
+ }
+ _renderBatchTable();
+ alert(`✅ 已将覆层预设「${val}」应用到 ${indices.length} 行`);
+ });
+
// Add row
container.querySelector('#rbt-add-row-btn')?.addEventListener('click', () => {
_batchAddEmptyRow();
@@ -712,6 +949,18 @@ function _bindBatchTableEvents() {
}
}
});
+ // Row checkbox change handler
+ tbody.addEventListener('change', (e) => {
+ if (e.target.classList.contains('rbt-row-check')) {
+ const idx = parseInt(e.target.dataset.idx);
+ if (e.target.checked) {
+ _batchTableState.selectedRows.add(idx);
+ } else {
+ _batchTableState.selectedRows.delete(idx);
+ }
+ _updateBatchSelectCount();
+ }
+ });
tbody.addEventListener('click', (e) => {
// 预览按钮
const selectBtn = e.target.closest('.rbt-select-btn');
@@ -1826,6 +2075,164 @@ async function _batchAlignAllTasks() {
}
}
+// ═══════════════════════════════════════════════════════
+// 10b. Batch selection helpers
+// ═══════════════════════════════════════════════════════
+
+function _getSelectedIndices() {
+ return Array.from(_batchTableState.selectedRows).sort((a, b) => a - b);
+}
+
+function _updateBatchSelectCount() {
+ const el = _batchTableState.container?.querySelector('#rbt-selected-count');
+ if (el) {
+ const count = _batchTableState.selectedRows.size;
+ el.textContent = count > 0 ? `已选 ${count} 行` : '';
+ }
+}
+
+// ═══════════════════════════════════════════════════════
+// 10c. Material folder refresh
+// ═══════════════════════════════════════════════════════
+
+const _MAT_BG_EXTS = new Set(['mp4', 'mov', 'mkv', 'avi', 'wmv', 'flv', 'webm', 'jpg', 'jpeg', 'png', 'webp', 'gif', 'bmp']);
+const _MAT_AUDIO_EXTS = new Set(['mp3', 'wav', 'm4a', 'aac', 'flac', 'ogg', 'wma']);
+const _MAT_SRT_EXTS = new Set(['srt']);
+const _MAT_TXT_EXTS = new Set(['txt']);
+
+async function _refreshMaterialFolder() {
+ const tab = _getActiveTab();
+ if (!tab.materialDir) {
+ alert('请先设置素材文件夹');
+ return;
+ }
+
+ const refreshBtn = _batchTableState.container?.querySelector('#rbt-refresh-mat');
+ if (refreshBtn) {
+ refreshBtn.disabled = true;
+ refreshBtn.textContent = '⏳ 扫描中...';
+ }
+
+ try {
+ let files;
+ if (window.electronAPI && window.electronAPI.scanDirectory) {
+ files = await window.electronAPI.scanDirectory(tab.materialDir);
+ } else {
+ alert('请在桌面应用中使用此功能');
+ return;
+ }
+
+ if (!files || files.length === 0) {
+ alert('文件夹为空或无法读取');
+ return;
+ }
+
+ // Classify files
+ const classified = { bg: [], audio: [], srt: [], txt: [] };
+ for (const f of files) {
+ const ext = (f.name || '').split('.').pop().toLowerCase();
+ if (_MAT_BG_EXTS.has(ext)) classified.bg.push(f);
+ else if (_MAT_AUDIO_EXTS.has(ext)) classified.audio.push(f);
+ else if (_MAT_SRT_EXTS.has(ext)) classified.srt.push(f);
+ else if (_MAT_TXT_EXTS.has(ext)) classified.txt.push(f);
+ }
+
+ const state = window._reelsState;
+ const tasks = state.tasks || [];
+
+ // Build a map: baseName → task index for existing tasks
+ const existingMap = new Map();
+ tasks.forEach((t, i) => {
+ const bgBase = _baseFileName(t.bgPath || t.videoPath || '');
+ const audioBase = _baseFileName(t.audioPath || '');
+ if (bgBase) existingMap.set(bgBase, i);
+ if (audioBase) existingMap.set(audioBase, i);
+ });
+
+ let newCount = 0, updateCount = 0;
+
+ // Match by baseName: group files
+ const groups = new Map(); // baseName → { bg, audio, srt, txt }
+ const allFiles = [
+ ...classified.bg.map(f => ({ ...f, type: 'bg' })),
+ ...classified.audio.map(f => ({ ...f, type: 'audio' })),
+ ...classified.srt.map(f => ({ ...f, type: 'srt' })),
+ ...classified.txt.map(f => ({ ...f, type: 'txt' })),
+ ];
+ for (const f of allFiles) {
+ const base = _baseFileName(f.name);
+ if (!groups.has(base)) groups.set(base, {});
+ groups.get(base)[f.type] = f;
+ }
+
+ // Process each group
+ for (const [base, group] of groups) {
+ let taskIdx = existingMap.get(base);
+ let task;
+
+ if (taskIdx != null) {
+ task = tasks[taskIdx];
+ // Update existing task if files changed
+ let changed = false;
+ if (group.bg && task.bgPath !== group.bg.path) {
+ task.bgPath = group.bg.path; task.videoPath = group.bg.path; task.bgSrcUrl = ''; changed = true;
+ }
+ if (group.audio && task.audioPath !== group.audio.path) {
+ task.audioPath = group.audio.path; changed = true;
+ }
+ if (group.srt && task.srtPath !== group.srt.path) {
+ task.srtPath = group.srt.path; changed = true;
+ }
+ if (changed) {
+ task._justRefreshed = true;
+ updateCount++;
+ }
+ } else {
+ // Create new task
+ task = _createEmptyTask();
+ task.baseName = base;
+ if (group.bg) { task.bgPath = group.bg.path; task.videoPath = group.bg.path; }
+ if (group.audio) { task.audioPath = group.audio.path; }
+ if (group.srt) { task.srtPath = group.srt.path; }
+ task._justRefreshed = true;
+ tasks.push(task);
+ newCount++;
+ }
+ }
+
+ tab.lastRefreshTime = Date.now();
+ state.tasks = tasks;
+
+ _renderBatchTable();
+
+ // Clear _justRefreshed flags after animation
+ setTimeout(() => {
+ for (const t of state.tasks) delete t._justRefreshed;
+ }, 3500);
+
+ const summary = [];
+ if (newCount > 0) summary.push(`🆕 新增 ${newCount} 行`);
+ if (updateCount > 0) summary.push(`🔄 更新 ${updateCount} 行`);
+ if (summary.length === 0) summary.push('✅ 没有新变化');
+ alert(`刷新完成\n${summary.join('\n')}\n\n文件夹: ${tab.materialDir}\n共 ${files.length} 个文件`);
+
+ } catch (err) {
+ console.error('[BatchTable] Refresh error:', err);
+ alert(`刷新失败: ${err.message}`);
+ } finally {
+ if (refreshBtn) {
+ refreshBtn.disabled = false;
+ refreshBtn.textContent = '🔄 一键刷新';
+ }
+ }
+}
+
+function _baseFileName(filePath) {
+ if (!filePath) return '';
+ const name = filePath.replace(/\\/g, '/').split('/').pop() || '';
+ return name.replace(/\.[^.]+$/, '').toLowerCase().trim();
+}
+
// ═══════════════════════════════════════════════════════
// 11. Utility
// ═══════════════════════════════════════════════════════
@@ -1945,6 +2352,78 @@ function _injectBatchTableCSS() {
.rbt-file-name { cursor:pointer !important; }
.rbt-file-name:hover { border-color:#00D4FF !important; color:#00D4FF !important; }
.rbt-footer-hint { font-size:11px; color:#555; }
+
+ /* ══ Tab bar ══ */
+ .rbt-tabbar {
+ display:flex; align-items:stretch; background:#08081a; border-bottom:2px solid #1a1a4a;
+ padding:0 8px; min-height:36px; overflow-x:auto; flex-shrink:0;
+ }
+ .rbt-tabs-scroll {
+ display:flex; align-items:stretch; gap:2px; flex:1; min-width:0;
+ }
+ .rbt-tab {
+ display:flex; align-items:center; gap:6px; padding:6px 16px; cursor:pointer;
+ font-size:12px; color:#888; border:1px solid transparent; border-bottom:none;
+ border-radius:8px 8px 0 0; transition:all .15s; position:relative; white-space:nowrap;
+ background:transparent; user-select:none;
+ }
+ .rbt-tab:hover { color:#ccc; background:rgba(255,255,255,0.05); }
+ .rbt-tab-active {
+ color:#00D4FF !important; background:#12123a !important;
+ border-color:#1a1a4a #1a1a4a transparent; font-weight:600;
+ }
+ .rbt-tab-active::after {
+ content:''; position:absolute; bottom:-2px; left:0; right:0; height:2px; background:#00D4FF;
+ }
+ .rbt-tab-close {
+ font-size:14px; line-height:1; opacity:0.4; transition:all .15s; padding:0 2px;
+ }
+ .rbt-tab-close:hover { opacity:1; color:#ff6b6b; }
+ .rbt-tab-add {
+ color:#555; font-size:16px; font-weight:700; padding:6px 12px;
+ }
+ .rbt-tab-add:hover { color:#00D4FF; }
+
+ /* ══ Material folder bar ══ */
+ .rbt-material-bar {
+ display:flex; align-items:center; gap:8px; padding:6px 16px;
+ background:#0a0a22; border-bottom:1px solid #1a1a3a; flex-shrink:0;
+ }
+ .rbt-mat-path {
+ font-size:12px; color:#a0a0d0; background:#12122a; padding:3px 10px;
+ border-radius:4px; border:1px solid #2a2a4a; max-width:300px;
+ overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
+ }
+ .rbt-btn-refresh {
+ background:linear-gradient(135deg, #1a6b3a, #2a8b4a) !important;
+ color:#8f8 !important; border-color:#3a8b4a !important; font-weight:600;
+ transition:all .2s;
+ }
+ .rbt-btn-refresh:hover:not(:disabled) {
+ background:linear-gradient(135deg, #2a8b4a, #3aab5a) !important;
+ box-shadow:0 0 12px rgba(0,255,100,0.3);
+ }
+ .rbt-btn-refresh:disabled { opacity:0.4; cursor:not-allowed; }
+ .rbt-refresh-time { font-size:10px; color:#666; margin-left:4px; }
+
+ /* ══ Batch actions bar ══ */
+ .rbt-batch-actions-bar {
+ display:flex; align-items:center; gap:8px; padding:6px 16px;
+ background:#0e0e28; border-bottom:1px solid #1a1a3a; flex-shrink:0;
+ }
+ .rbt-batch-label {
+ display:flex; align-items:center; gap:4px; font-size:11px; color:#aaa; cursor:pointer;
+ }
+
+ /* ══ Row refresh animation ══ */
+ .rbt-row-refreshed {
+ animation: rbt-refresh-glow 3s ease-out;
+ }
+ @keyframes rbt-refresh-glow {
+ 0% { background:rgba(0,255,100,0.25); box-shadow:inset 0 0 20px rgba(0,255,100,0.15); }
+ 30% { background:rgba(0,255,100,0.15); }
+ 100% { background:transparent; box-shadow:none; }
+ }
`;
document.head.appendChild(style);
}
@@ -1969,12 +2448,23 @@ function _serializeTasks(tasks) {
});
}
-/** 自动保存到 localStorage */
+/** 自动保存到 localStorage (含所有标签页) */
function _batchAutoSave() {
try {
+ // Sync current tasks to active tab first
+ _syncTasksToActiveTab();
const data = {
timestamp: new Date().toISOString(),
- tasks: _serializeTasks(window._reelsState.tasks),
+ version: '2.0',
+ activeTabId: _batchTableState.activeTabId,
+ nextTabId: _batchTableState.nextTabId,
+ tabs: _batchTableState.tabs.map(tab => ({
+ id: tab.id,
+ name: tab.name,
+ materialDir: tab.materialDir || '',
+ lastRefreshTime: tab.lastRefreshTime || null,
+ tasks: _serializeTasks(tab.tasks),
+ })),
};
localStorage.setItem(BATCH_CONFIG_KEY, JSON.stringify(data));
} catch (e) {
@@ -1988,11 +2478,34 @@ function _batchAutoRestore() {
const raw = localStorage.getItem(BATCH_CONFIG_KEY);
if (!raw) return;
const data = JSON.parse(raw);
- if (data.tasks && data.tasks.length > 0) {
+
+ if (data.version === '2.0' && data.tabs && data.tabs.length > 0) {
+ // v2: multi-tab format
+ _batchTableState.tabs = data.tabs.map(t => ({
+ id: t.id,
+ name: t.name,
+ materialDir: t.materialDir || '',
+ lastRefreshTime: t.lastRefreshTime || null,
+ tasks: t.tasks || [],
+ }));
+ _batchTableState.activeTabId = data.activeTabId || _batchTableState.tabs[0].id;
+ _batchTableState.nextTabId = data.nextTabId || _batchTableState.tabs.length + 1;
+ // Load active tab's tasks
+ const activeTab = _getActiveTab();
+ if (activeTab && activeTab.tasks.length > 0) {
+ const existing = window._reelsState.tasks || [];
+ if (existing.length === 0) {
+ window._reelsState.tasks = activeTab.tasks.map(t => ({ ...t }));
+ }
+ }
+ console.log(`[BatchTable] Auto-restored ${_batchTableState.tabs.length} tabs from ${data.timestamp}`);
+ } else if (data.tasks && data.tasks.length > 0) {
+ // v1: legacy single-list format — migrate to tab
const existing = window._reelsState.tasks || [];
if (existing.length === 0) {
window._reelsState.tasks = data.tasks;
- console.log(`[BatchTable] Auto-restored ${data.tasks.length} tasks from ${data.timestamp}`);
+ _batchTableState.tabs[0].tasks = data.tasks;
+ console.log(`[BatchTable] Auto-restored ${data.tasks.length} tasks (v1) from ${data.timestamp}`);
}
}
} catch (e) {
@@ -2000,53 +2513,84 @@ function _batchAutoRestore() {
}
}
-/** 导出配置为 JSON 文件 */
+/** 导出所有标签页为 JSON 工程文件 */
function _batchExportConfig() {
- const tasks = window._reelsState.tasks || [];
- if (tasks.length === 0) {
+ _syncTasksToActiveTab();
+ const totalTasks = _batchTableState.tabs.reduce((sum, t) => sum + (t.tasks || []).length, 0);
+ if (totalTasks === 0) {
alert('没有任务可以保存');
return;
}
const data = {
- version: '1.0',
+ version: '2.0',
timestamp: new Date().toISOString(),
- tasks: _serializeTasks(tasks),
+ activeTabId: _batchTableState.activeTabId,
+ nextTabId: _batchTableState.nextTabId,
+ tabs: _batchTableState.tabs.map(tab => ({
+ id: tab.id,
+ name: tab.name,
+ materialDir: tab.materialDir || '',
+ lastRefreshTime: tab.lastRefreshTime || null,
+ tasks: _serializeTasks(tab.tasks),
+ })),
};
const json = JSON.stringify(data, null, 2);
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
- a.download = `batch_config_${new Date().toISOString().slice(0, 10)}.json`;
+ a.download = `batch_project_${new Date().toISOString().slice(0, 10)}.json`;
a.click();
URL.revokeObjectURL(url);
- console.log(`[BatchTable] Exported ${tasks.length} tasks`);
+ console.log(`[BatchTable] Exported project: ${_batchTableState.tabs.length} tabs, ${totalTasks} tasks`);
}
-/** 从 JSON 文件导入配置 */
+/** 从 JSON 文件导入工程配置 */
function _batchImportConfig(file) {
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = JSON.parse(e.target.result);
- const tasks = data.tasks;
- if (!Array.isArray(tasks) || tasks.length === 0) {
- alert('配置文件中没有任务数据');
- return;
- }
- const mode = (window._reelsState.tasks || []).length > 0
- ? confirm(`当前有 ${window._reelsState.tasks.length} 个任务。\n\n确定 = 替换全部\n取消 = 追加到末尾`)
- ? 'replace'
- : 'append'
- : 'replace';
-
- if (mode === 'replace') {
- window._reelsState.tasks = tasks;
+
+ if (data.version === '2.0' && data.tabs && data.tabs.length > 0) {
+ // v2: multi-tab project
+ if (!confirm(`将加载 ${data.tabs.length} 个标签页的工程文件。\n\n确定 = 替换全部\n取消 = 放弃`)) return;
+ _batchTableState.tabs = data.tabs.map(t => ({
+ id: t.id,
+ name: t.name,
+ materialDir: t.materialDir || '',
+ lastRefreshTime: t.lastRefreshTime || null,
+ tasks: t.tasks || [],
+ }));
+ _batchTableState.activeTabId = data.activeTabId || _batchTableState.tabs[0].id;
+ _batchTableState.nextTabId = data.nextTabId || _batchTableState.tabs.length + 1;
+ const activeTab = _getActiveTab();
+ _loadTabTasks(activeTab);
+ _renderBatchTable();
+ const total = _batchTableState.tabs.reduce((s, t) => s + (t.tasks || []).length, 0);
+ alert(`✅ 成功加载工程: ${_batchTableState.tabs.length} 个标签页, ${total} 个任务\n(${data.timestamp || ''})`);
} else {
- window._reelsState.tasks = (window._reelsState.tasks || []).concat(tasks);
+ // v1: legacy single-task-list
+ const tasks = data.tasks;
+ if (!Array.isArray(tasks) || tasks.length === 0) {
+ alert('配置文件中没有任务数据');
+ return;
+ }
+ const mode = (window._reelsState.tasks || []).length > 0
+ ? confirm(`当前有 ${window._reelsState.tasks.length} 个任务。\n\n确定 = 替换全部\n取消 = 追加到末尾`)
+ ? 'replace'
+ : 'append'
+ : 'replace';
+
+ if (mode === 'replace') {
+ window._reelsState.tasks = tasks;
+ } else {
+ window._reelsState.tasks = (window._reelsState.tasks || []).concat(tasks);
+ }
+ _syncTasksToActiveTab();
+ _renderBatchTable();
+ alert(`成功加载 ${tasks.length} 个任务 (${data.timestamp || ''})`);
}
- _renderBatchTable();
- alert(`成功加载 ${tasks.length} 个任务 (${data.timestamp || ''})`);
} catch (err) {
alert(`配置文件解析失败: ${err.message}`);
}
|