/* ===== Cloud Image Browser Page ===== */ window.NAICloud = { dates: [], images: [], displayedCount: 0, BATCH_SIZE: 30, selectedPath: null, selectedInfo: null, currentDate: '', syncing: false, batch: null, searchQuery: '', searchTimeout: null, _scrollHandler: null, async loadDates() { try { const resp = await fetch('/api/cloud/dates'); const data = await resp.json(); this.dates = data.dates || []; } catch (e) { this.dates = []; } this.renderDateList(); }, async loadImages(date) { this.currentDate = date || ''; try { let url; if (this.searchQuery.trim()) { url = `/api/cloud/search?q=${encodeURIComponent(this.searchQuery)}&date=${encodeURIComponent(date)}`; } else { url = `/api/cloud/images?date=${encodeURIComponent(date)}`; } const resp = await fetch(url); const data = await resp.json(); this.images = data.images || []; } catch (e) { this.images = []; } this.displayedCount = 0; this.renderGrid(); }, onSearchInput(value) { this.searchQuery = value; clearTimeout(this.searchTimeout); this.searchTimeout = this.currentDate ? setTimeout(() => this.loadImages(this.currentDate), 300) : null; }, async selectImage(path) { this.selectedPath = path; $$('.cloud-card').forEach(c => { c.classList.toggle('selected', c.dataset.path === path); }); const detail = $('#cloud-detail'); detail.style.display = 'flex'; $('#cloud-empty-detail').style.display = 'none'; // Load full image const imgEl = $('#cloud-detail-img'); imgEl.src = ''; imgEl.alt = 'Loading...'; try { const resp = await fetch(`/api/cloud/image?path=${encodeURIComponent(path)}`); const data = await resp.json(); if (data.image) { const dataUrl = 'data:image/png;base64,' + data.image; imgEl.src = dataUrl; imgEl.addEventListener('click', () => { if (document.fullscreenElement) { document.exitFullscreen(); } else { imgEl.requestFullscreen(); } }); // Parse PNG info const binary = atob(data.image); const bytes = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i); const textChunks = await NAIPngInfo.readPngTextChunks(bytes.buffer); const img = await NAIUtils.loadImageFromDataUrl(dataUrl); const canvas = document.createElement('canvas'); canvas.width = img.naturalWidth; canvas.height = img.naturalHeight; const ctx = canvas.getContext('2d', { willReadFrequently: true }); ctx.drawImage(img, 0, 0); const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const stealthInfo = await NAIPngInfo.readStealthInfo(imageData, canvas.width, canvas.height); const parsed = NAIPngInfo.parseInfo(textChunks, stealthInfo, img.naturalWidth, img.naturalHeight); this.selectedInfo = parsed; this._selectedB64 = data.image; this.renderParams(parsed); } } catch (e) { console.error('Failed to load cloud image:', e); } // Meta const item = this.images.find(i => i.path === path); const metaEl = $('#cloud-detail-meta'); if (item) { metaEl.innerHTML = `
文件:${item.name}
日期:${item.date}
`; } }, renderDateList() { const list = $('#cloud-date-list'); list.innerHTML = ''; // "All" option const allItem = document.createElement('div'); allItem.className = 'cloud-date-item' + (!this.currentDate ? ' active' : ''); allItem.textContent = `全部`; allItem.addEventListener('click', () => { this.currentDate = ''; this.loadImages(''); $$('.cloud-date-item').forEach(d => d.classList.remove('active')); allItem.classList.add('active'); }); list.appendChild(allItem); for (const d of this.dates) { const item = document.createElement('div'); item.className = 'cloud-date-item' + (d.date === this.currentDate ? ' active' : ''); item.innerHTML = `${d.date}${d.count}`; item.addEventListener('click', () => { this.currentDate = d.date; this.loadImages(d.date); $$('.cloud-date-item').forEach(di => di.classList.remove('active')); item.classList.add('active'); }); list.appendChild(item); } }, _renderCards(items) { const grid = $('#cloud-grid'); for (const item of items) { const card = document.createElement('div'); card.className = 'cloud-card history-card' + (item.path === this.selectedPath ? ' selected' : ''); card.dataset.path = item.path; // Use thumbnail endpoint (lazy: load full only on select) const img = document.createElement('img'); img.loading = 'lazy'; img.src = `/api/cloud/image?path=${encodeURIComponent(item.path)}`; img.alt = item.name; img.style.background = 'var(--bg-input)'; // Fetch thumbnail via API fetch(`/api/cloud/image?path=${encodeURIComponent(item.path)}`) .then(r => r.json()) .then(data => { if (data.image) img.src = 'data:image/png;base64,' + data.image; }) .catch(() => {}); card.appendChild(img); const badge = document.createElement('div'); badge.className = 'history-card-badge'; badge.textContent = item.name.split('-').pop()?.replace('.png', '') || ''; card.appendChild(badge); card.dataset.batchId = item.path; if (this.batch?.batchMode) { const cbWrap = document.createElement('div'); cbWrap.className = 'batch-checkbox-wrap'; const cb = document.createElement('input'); cb.type = 'checkbox'; cb.className = 'batch-checkbox'; cb.checked = this.batch.selected.has(item.path); cbWrap.appendChild(cb); card.appendChild(cbWrap); if (this.batch.selected.has(item.path)) { card.classList.add('batch-selected'); } card.addEventListener('click', (e) => { if (e.shiftKey) { this.batch.rangeSelect(item.path); } else { this.batch.toggleItem(item.path); } }); } else { card.addEventListener('click', () => this.selectImage(item.path)); } grid.appendChild(card); } }, loadMoreImages() { if (this.displayedCount >= this.images.length) return; const nextBatch = this.images.slice(this.displayedCount, this.displayedCount + this.BATCH_SIZE); this._renderCards(nextBatch); this.displayedCount += nextBatch.length; // Update load-more indicator this._updateLoadMoreIndicator(); }, _updateLoadMoreIndicator() { const grid = $('#cloud-grid'); const existing = grid.parentElement.querySelector('.cloud-load-more-indicator'); if (existing) existing.remove(); if (this.displayedCount < this.images.length) { const indicator = document.createElement('div'); indicator.className = 'cloud-load-more-indicator'; indicator.style.cssText = 'text-align:center;padding:12px;color:var(--text-secondary,#888);font-size:13px;'; indicator.textContent = `已加载 ${this.displayedCount} / ${this.images.length} 张,下滑加载更多...`; grid.parentElement.appendChild(indicator); } }, _onScrollContainer() { const container = $('.cloud-grid-body'); if (!container) return; const scrollTop = container.scrollTop; const scrollHeight = container.scrollHeight; const clientHeight = container.clientHeight; // When scrolled to within 200px of the bottom, load more if (scrollTop + clientHeight >= scrollHeight - 200) { this.loadMoreImages(); } }, _setupScrollListener() { const container = $('.cloud-grid-body'); if (!container) return; // Remove old listener if exists if (this._scrollHandler) { container.removeEventListener('scroll', this._scrollHandler); } this._scrollHandler = () => this._onScrollContainer(); container.addEventListener('scroll', this._scrollHandler); }, renderGrid() { const grid = $('#cloud-grid'); const countEl = $('#cloud-count'); grid.innerHTML = ''; // Remove any existing load-more indicator const existingIndicator = grid.parentElement?.querySelector('.cloud-load-more-indicator'); if (existingIndicator) existingIndicator.remove(); if (countEl) countEl.textContent = `${this.images.length} 张`; if (this.images.length === 0) { grid.innerHTML = '
暂无图片
'; return; } // Reset displayed count and load first batch this.displayedCount = 0; this.loadMoreImages(); // Setup scroll listener for infinite scroll this._setupScrollListener(); }, renderParams(parsed) { const paramsEl = $('#cloud-detail-params'); paramsEl.innerHTML = ''; if (parsed.params && Object.keys(parsed.params).length > 0) { const order = ['prompt', 'uc', 'negative prompt', 'model', 'steps', 'sampler', 'scale', 'seed', 'width', 'height']; const shown = new Set(); for (const key of order) { if (parsed.params[key] !== undefined) { paramsEl.appendChild(this._row(key, parsed.params[key])); shown.add(key); } } for (const [k, v] of Object.entries(parsed.params)) { if (shown.has(k)) continue; paramsEl.appendChild(this._row(k, typeof v === 'object' ? JSON.stringify(v) : v)); } $('#btn-cloud-send-params').style.display = ''; } else { paramsEl.innerHTML = '
无可解析参数
'; $('#btn-cloud-send-params').style.display = 'none'; } }, _row(key, value) { const row = document.createElement('div'); row.className = 'pi-param-row'; const vs = String(value); row.innerHTML = `
${key}
100 ? 'title="' + vs.replace(/"/g, '"') + '"' : ''}>${vs}
`; return row; }, // ── Actions ── sendParamsToGenerate() { if (!this.selectedInfo) return; const backup = NAIPngInfo.currentInfo; NAIPngInfo.currentInfo = this.selectedInfo; NAIPngInfo.sendToGenerate(); NAIPngInfo.currentInfo = backup; }, _getSelectedDataUrl() { if (!this._selectedB64) return null; return 'data:image/png;base64,' + this._selectedB64; }, sendToExtraInput(mode) { const du = this._getSelectedDataUrl(); if (!du) return; NAIExtraInput.clear(); const r = document.querySelector(`input[name="extra-mode"][value="${mode}"]`); if (r) { r.checked = true; NAIExtraInput.setMode(mode); } NAIExtraInput.handleUploadFromDataUrl(du); const c = $('#extra-input-content'); if (c && !c.classList.contains('open')) { c.classList.add('open'); $('#extra-input-arrow').textContent = '▼'; } document.querySelector('.nav-item[data-page="generate"]')?.click(); }, sendToVibe() { const du = this._getSelectedDataUrl(); if (!du) return; fetch(du).then(r => r.blob()).then(blob => { NAIVibe.handleRefUpload([new File([blob], 'cloud.png', { type: 'image/png' })]); const c = $('#vibe-content'); if (c && !c.classList.contains('open')) { c.classList.add('open'); $('#vibe-arrow').textContent = '▼'; } document.querySelector('.nav-item[data-page="generate"]')?.click(); }); }, sendToDirectorTools() { const du = this._getSelectedDataUrl(); if (!du) return; NAIDirectorTools._setInputFromDataUrl(du); document.querySelector('.nav-item[data-page="inpaint"]')?.click(); }, downloadSelected() { if (!this._selectedB64 || !this.selectedPath) return; const a = document.createElement('a'); a.href = 'data:image/png;base64,' + this._selectedB64; a.download = this.selectedPath.split('/').pop() || 'image.png'; document.body.appendChild(a); a.click(); document.body.removeChild(a); }, async deleteSelected() { if (!this.selectedPath) return; if (!confirm(`确定删除 ${this.selectedPath}?将同时从云端删除。`)) return; try { await fetch('/api/cloud/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path: this.selectedPath }), }); this.selectedPath = null; this.selectedInfo = null; this._selectedB64 = null; $('#cloud-detail').style.display = 'none'; $('#cloud-empty-detail').style.display = 'flex'; if (this.currentDate) await this.loadImages(this.currentDate); await this.loadDates(); } catch (e) { console.error(e); } }, async syncFromCloud() { if (this.syncing) return; this.syncing = true; const btn = $('#btn-cloud-sync'); btn.disabled = true; btn.textContent = '同步中...'; try { await fetch('/api/cloud/sync', { method: 'POST' }); await this.loadDates(); if (this.currentDate) await this.loadImages(this.currentDate); } catch (e) { console.error(e); } this.syncing = false; btn.disabled = false; btn.textContent = '🔄 同步'; }, init() { $('#btn-cloud-sync').addEventListener('click', () => this.syncFromCloud()); $('#btn-cloud-send-params').addEventListener('click', () => this.sendParamsToGenerate()); $('#btn-cloud-send-i2i').addEventListener('click', () => this.sendToExtraInput('img2img')); $('#btn-cloud-send-inp').addEventListener('click', () => this.sendToExtraInput('inpaint')); $('#btn-cloud-send-vib').addEventListener('click', () => this.sendToVibe()); $('#btn-cloud-send-dt').addEventListener('click', () => this.sendToDirectorTools()); $('#btn-cloud-download').addEventListener('click', () => this.downloadSelected()); $('#btn-cloud-delete').addEventListener('click', () => this.deleteSelected()); // Search $('#cloud-search-input').addEventListener('input', (e) => this.onSearchInput(e.target.value)); $('#btn-cloud-search-clear').addEventListener('click', () => { $('#cloud-search-input').value = ''; this.onSearchInput(''); }); // Batch mode this.batch = NAIBatch.create({ gridSel: '#cloud-grid', toolbarSel: '#cloud-batch-toolbar', getItems: () => this.images, onDelete: async (paths) => { for (const path of paths) { await fetch('/api/cloud/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path }), }); } this.selectedPath = null; this.selectedInfo = null; this._selectedB64 = null; $('#cloud-detail').style.display = 'none'; $('#cloud-empty-detail').style.display = 'flex'; if (this.currentDate) await this.loadImages(this.currentDate); await this.loadDates(); }, getImageB64: async (path) => { const resp = await fetch(`/api/cloud/image?path=${encodeURIComponent(path)}`); const data = await resp.json(); if (!data.image) return null; return { b64: data.image, filename: path.split('/').pop() || 'image.png' }; }, onRefreshGrid: () => this.renderGrid(), }); $('#btn-cloud-batch-toggle').addEventListener('click', () => { this.batch.toggle(); $('#btn-cloud-batch-toggle').classList.toggle('active', this.batch.batchMode); }); const cbSelectAll = document.querySelector('#cloud-batch-toolbar .batch-select-all'); if (cbSelectAll) { cbSelectAll.addEventListener('change', () => { cbSelectAll.checked ? this.batch.selectAll() : this.batch.selectNone(); }); } $('#btn-cloud-batch-download')?.addEventListener('click', () => this.batch.batchDownload()); $('#btn-cloud-batch-delete')?.addEventListener('click', () => this.batch.batchDelete()); document.querySelector('.nav-item[data-page="cloud"]')?.addEventListener('click', () => { this.loadDates(); if (this.currentDate) this.loadImages(this.currentDate); }); }, };