/* ===== 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 = `
`; } }, 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 = '