client / static /js /cloud.js
P01yH3dr0n's picture
launch
774fe36
Raw
History Blame Contribute Delete
18.6 kB
/* ===== 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 = `
<div class="history-meta-row"><span class="history-meta-key">文件:</span><span>${item.name}</span></div>
<div class="history-meta-row"><span class="history-meta-key">日期:</span><span>${item.date}</span></div>
`;
}
},
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 = `<span>${d.date}</span><span class="cloud-date-count">${d.count}</span>`;
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 = '<div class="history-empty">暂无图片</div>';
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 = '<div class="history-no-params">无可解析参数</div>';
$('#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 = `<div class="pi-param-key">${key}</div><div class="pi-param-value" ${vs.length > 100 ? 'title="' + vs.replace(/"/g, '&quot;') + '"' : ''}>${vs}</div>`;
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);
});
},
};