/* ===== Batch Selection & Actions ===== */ window.NAIBatch = { /** * Create a batch controller for a gallery page. * @param {object} opts * - gridSel: grid container selector * - toolbarSel: toolbar container selector * - getItems: () => [{id, ...}] all items * - renderCard: (item, isSelected, batchMode) => HTMLElement * - onDelete: async (ids) => void * - onDownload: async (ids) => void (optional, we provide default ZIP) * - getImageB64: async (id) => {b64, filename} * - getName: (item) => string filename */ create(opts) { const ctrl = { opts, batchMode: false, selected: new Set(), toggle() { this.batchMode = !this.batchMode; if (!this.batchMode) this.selected.clear(); this.updateToolbar(); this.refreshGrid(); }, selectAll() { const items = this.opts.getItems(); items.forEach(item => this.selected.add(item.id ?? item.path)); this.updateToolbar(); this.refreshGrid(); }, selectNone() { this.selected.clear(); this.updateToolbar(); this.refreshGrid(); }, toggleItem(id) { if (this.selected.has(id)) { this.selected.delete(id); } else { this.selected.add(id); } this.updateToolbar(); // Update just the card const card = document.querySelector(`[data-batch-id="${id}"]`); if (card) { card.classList.toggle('batch-selected', this.selected.has(id)); const cb = card.querySelector('.batch-checkbox'); if (cb) cb.checked = this.selected.has(id); } }, // Shift-click range select _lastClickedId: null, rangeSelect(id) { const items = this.opts.getItems(); const ids = items.map(i => i.id ?? i.path); const curIdx = ids.indexOf(id); const lastIdx = ids.indexOf(this._lastClickedId); if (lastIdx === -1 || curIdx === -1) { this.toggleItem(id); this._lastClickedId = id; return; } const start = Math.min(curIdx, lastIdx); const end = Math.max(curIdx, lastIdx); for (let i = start; i <= end; i++) { this.selected.add(ids[i]); } this._lastClickedId = id; this.updateToolbar(); this.refreshGrid(); }, updateToolbar() { const toolbar = document.querySelector(this.opts.toolbarSel); if (!toolbar) return; if (!this.batchMode) { toolbar.style.display = 'none'; return; } toolbar.style.display = 'flex'; const count = this.selected.size; const total = this.opts.getItems().length; const countEl = toolbar.querySelector('.batch-count'); if (countEl) countEl.textContent = `已选 ${count} / ${total}`; const allCb = toolbar.querySelector('.batch-select-all'); if (allCb) { allCb.checked = count > 0 && count === total; allCb.indeterminate = count > 0 && count < total; } // Disable actions when nothing selected toolbar.querySelectorAll('.batch-action-btn').forEach(btn => { btn.disabled = count === 0; }); }, refreshGrid() { // Re-render grid via the host page if (this.opts.onRefreshGrid) this.opts.onRefreshGrid(); }, async batchDelete() { const count = this.selected.size; if (count === 0) return; if (!confirm(`确定删除选中的 ${count} 张图片?`)) return; const ids = [...this.selected]; try { await this.opts.onDelete(ids); this.selected.clear(); this.updateToolbar(); } catch(e) { console.error('Batch delete failed:', e); alert('部分删除失败,请重试'); } }, async batchDownload() { const count = this.selected.size; if (count === 0) return; const ids = [...this.selected]; const toolbar = document.querySelector(this.opts.toolbarSel); const progressEl = toolbar?.querySelector('.batch-progress'); // Use JSZip to create ZIP in browser if (typeof JSZip === 'undefined') { alert('JSZip 未加载,无法打包下载'); return; } const zip = new JSZip(); let loaded = 0; if (progressEl) { progressEl.style.display = ''; progressEl.textContent = `打包中 0/${count}`; } for (const id of ids) { try { const result = await this.opts.getImageB64(id); if (result && result.b64) { zip.file(result.filename, result.b64, { base64: true }); } } catch(e) { console.error('Failed to fetch image:', id, e); } loaded++; if (progressEl) progressEl.textContent = `打包中 ${loaded}/${count}`; } if (progressEl) progressEl.textContent = '生成ZIP...'; try { const blob = await zip.generateAsync({ type: 'blob', compression: 'STORE', }, (meta) => { if (progressEl) progressEl.textContent = `压缩 ${Math.round(meta.percent)}%`; }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; const now = new Date(); a.download = `nai_batch_${now.getFullYear()}${String(now.getMonth()+1).padStart(2,'0')}${String(now.getDate()).padStart(2,'0')}.zip`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } catch(e) { console.error('ZIP generation failed:', e); alert('ZIP生成失败'); } if (progressEl) { progressEl.textContent = '完成'; setTimeout(() => { progressEl.style.display = 'none'; }, 2000); } }, }; return ctrl; }, };