Files
pigway-frame-flow/pigway_frame_flow/web/thumbnails.js
T
渡口浪人 081d8b6ee0
Community checks / verify (push) Skipped
Verify Frame Flow / verify (push) Failing after 6m12s
feat: add a separate watch thumbnail gallery and logical device
2026-10-23 15:00:29 +08:00

49 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
Object.assign(MESSAGES.zh, {thumbnails:'小图方案',thumbnailPlanInUse:'此方案已被任务或待处理操作使用,请先取消关联或更换方案。'});
Object.assign(MESSAGES.en, {thumbnails:'Thumbnail plans',thumbnailPlanInUse:'This plan is used by a task or pending operation. Unlink it or select another plan first.'});
views.thumbnails=async function(epoch){
const [plans,sources]=await Promise.all([api('/api/v1/thumbnail-plans'),api('/api/v1/photo-sources')]);
if(epoch!==state.epoch)return;
$('#main').innerHTML=`<section class="card"><div class="actions"><select id="thumb-plan"><option value="">${tr('新建方案','New plan')}</option>${plans.map(p=>`<option value="${esc(p.id)}">${esc(p.name)} · ${p.width}×${p.height}</option>`).join('')}</select><button id="thumb-delete" class="danger">${t('delete')}</button></div><form id="thumb-form"><div class="field"><label for="thumb-name">${t('name')}</label><input id="thumb-name" required maxlength="120"></div><div class="form-grid">${[['width',tr('宽度(像素)','Width (px)'),410,16,2048,1],['height',tr('高度(像素)','Height (px)'),502,16,2048,1],['center_x',tr('水平裁切位置(0 左 / 1 右)','Crop X (0 left / 1 right)'),0.5,0,1,0.01],['center_y',tr('垂直裁切位置(0 上 / 1 下)','Crop Y (0 top / 1 bottom)'),0.5,0,1,0.01],['quality',tr('JPEG 质量','JPEG quality'),85,40,95,1]].map(([key,label,value,min,max,step])=>`<div class="field"><label for="thumb-${key}">${label}</label><input id="thumb-${key}" type="number" required min="${min}" max="${max}" step="${step}" value="${value}"></div>`).join('')}<div class="field"><label for="thumb-fit">${tr('适配方式','Fit')}</label><select id="thumb-fit"><option value="cover">${tr('等比裁切铺满(默认)','Fill with proportional crop (default)')}</option><option value="contain">${tr('完整画面(必要时补白)','Full picture (white padding if needed)')}</option></select></div></div><p class="hint">${tr('不拉伸、不叠加文字;裁切位置仅在铺满时生效。Photos 使用可取得的源预览。','No stretching or text. Crop positions apply to fill mode. Photos uses available source previews.')}</p><button type="submit" class="primary">${tr('保存方案','Save plan')}</button></form></section><section class="card"><h2>${tr('预览与生成','Preview and generate')}</h2><div class="field"><label for="thumb-source">${t('source')}</label><select id="thumb-source"><option value="">${tr('选择照片数据源','Choose photo source')}</option>${sources.map(s=>`<option value="${esc(s.id)}">${esc(s.name)}</option>`).join('')}</select></div><div class="field"><label for="thumb-item">${tr('源图片','Source image')}</label><select id="thumb-item"></select></div><div class="actions"><button id="thumb-prev">${tr('上一页','Previous page')}</button><button id="thumb-next">${tr('下一页','Next page')}</button><button id="thumb-preview">${tr('预览小图','Preview thumbnail')}</button><button id="thumb-generate" class="primary">${tr('生成并保存到小图图库','Generate and save to thumbnail gallery')}</button></div><button id="thumb-gallery" type="button">${tr('查看手表小图图库','Open watch thumbnail gallery')}</button><p id="thumb-result" class="hint" aria-live="polite"></p><img id="thumb-image" alt="${tr('小图预览','Thumbnail preview')}" hidden style="max-width:100%;max-height:502px;width:auto;height:auto"></section>`;
const defaults={name:'',width:410,height:502,center_x:0.5,center_y:0.5,quality:85,fit:'cover'};
const fill=p=>{for(const key of Object.keys(defaults))$('#thumb-'+key).value=p[key];};
const read=()=>{const p={};for(const key of Object.keys(defaults))p[key]=['name','fit'].includes(key)?$('#thumb-'+key).value:Number($('#thumb-'+key).value);return p;};
fill(defaults);$('#thumb-delete').disabled=true;
bind('thumb-gallery',()=>{state.tab='watch';state.watchOffset=0;return navigate('gallery');});
$('#thumb-form').oninput=()=>{$('#thumb-image').hidden=true;$('#thumb-result').textContent=tr('参数已修改,请重新预览','Parameters changed; preview again');};
$('#thumb-plan').onchange=()=>{fill(plans.find(p=>p.id===$('#thumb-plan').value)||defaults);$('#thumb-delete').disabled=!$('#thumb-plan').value;$('#thumb-image').hidden=true;};
$('#thumb-form').onsubmit=busy(async e=>{e.preventDefault();const id=$('#thumb-plan').value;await api('/api/v1/thumbnail-plans'+(id?'/'+id:''),{method:id?'PUT':'POST',body:read()});toast(t('save'));await navigate();});
bind('thumb-delete',()=>confirmDialog(t('removeConfirm'),async()=>{await api('/api/v1/thumbnail-plans/'+$('#thumb-plan').value,{method:'DELETE'});await navigate();}));
let offset=0,version=0;
async function images(){const source=$('#thumb-source').value;const ticket=++version;$('#thumb-item').innerHTML='';if(!source)return;const page=await api('/api/v1/photo-sources/'+encodeURIComponent(source)+'/images?offset='+offset+'&limit=50');if(ticket!==version||epoch!==state.epoch)return;$('#thumb-item').innerHTML=page.items.map(i=>`<option value="${esc(i.id)}">${esc(i.name)}</option>`).join('');$('#thumb-prev').disabled=offset===0;$('#thumb-next').disabled=offset+50>=page.total;}
$('#thumb-source').onchange=busy(async()=>{offset=0;await images();});
bind('thumb-prev',async()=>{offset=Math.max(0,offset-50);await images();});bind('thumb-next',async()=>{offset+=50;await images();});
function payload(){if(!$('#thumb-form').reportValidity())throw new Error(tr('请补全方案参数','Complete the plan parameters'));if(!$('#thumb-item').value)throw new Error(tr('请选择源图片','Choose a source image'));return {source_id:$('#thumb-source').value,item_id:$('#thumb-item').value,plan:read()};}
bind('thumb-preview',async()=>{const blob=await api('/api/v1/thumbnails/preview',{method:'POST',body:payload(),binary:true});if(epoch!==state.epoch)return;const url=URL.createObjectURL(blob);state.blobs.push(url);$('#thumb-image').src=url;$('#thumb-image').hidden=false;$('#thumb-result').textContent=tr('预览未保存','Preview is not saved');});
bind('thumb-generate',async()=>{const asset=await api('/api/v1/thumbnails/generate',{method:'POST',body:payload()});if(epoch!==state.epoch)return;$('#thumb-image').hidden=false;await blobImage($('#thumb-image'),asset.url);$('#thumb-result').textContent=tr('已保存到手表小图图库:','Saved to watch thumbnail gallery: ')+asset.name;});
};
async function thumbnailJobFields(job){
const plans=await api('/api/v1/thumbnail-plans');if(!$('#job-form'))return;
const box=document.createElement('section');box.innerHTML=`<label class="check-row"><input type="checkbox" id="job-thumbnail" ${job.generate_thumbnail?'checked':''}>${tr('生成小图','Generate thumbnail')}</label><div class="field"><label for="job-thumbnail-plan">${tr('小图生成方案(单选)','Thumbnail plan (choose one)')}</label><select id="job-thumbnail-plan"><option value="">${tr('请选择已保存方案','Choose a saved plan')}</option>${plans.map(p=>`<option value="${esc(p.id)}" ${job.thumbnail_plan_id===p.id?'selected':''}>${esc(p.name)} · ${p.width}×${p.height}</option>`).join('')}</select></div><p class="hint">${tr('仅支持照片数据源任务;上传成功后使用源画面生成。请先在“小图方案”中保存方案。','For photo-source tasks only; generates from source pixels after upload succeeds. Save a plan in Thumbnail plans first.')}</p>`;
$('#job-form .modal-actions').before(box);
const update=()=>{const supported=$('#job-content-type')?.value==='photo_source';$('#job-thumbnail').disabled=!supported;if(!supported)$('#job-thumbnail').checked=false;$('#job-thumbnail-plan').disabled=!$('#job-thumbnail').checked;$('#job-thumbnail-plan').required=$('#job-thumbnail').checked;};
$('#job-thumbnail').onchange=update;$('#job-content-type')?.addEventListener('change',update);update();
}
async function watchGalleryPage(epoch,destinations){
const offset=state.watchOffset||0;
const [page,devices]=await Promise.all([api('/api/v1/thumbnails?limit=50&offset='+offset),api('/api/v1/thumbnail-devices')]);
if(epoch!==state.epoch)return;
const watch=devices.find(d=>d.id==='watch');
$('#main').innerHTML=`<div class="section-heading"><div class="tabs gallery-tabs">${destinations.map(d=>`<button data-watch-tab="${esc(d.id)}" class="${d.id==='watch'?'active':''}">${esc(galleryDestinationName(d))}</button>`).join('')}</div><button id="watch-refresh">${t('refresh')}</button></div><section class="card"><div class="section-heading"><div><h2>${tr('手表','Watch')} · ${watch.width} × ${watch.height}</h2><p class="hint">${tr('独立小图设备 · 图片已保存在服务器;设备传输尚未接入。','Separate thumbnail device · Images are saved on the server; device transfer is not connected.')}</p></div><button id="watch-plans" class="primary">${tr('管理小图方案 / 生成图片','Manage plans / Generate images')}</button></div><div class="actions"><button id="watch-prev" ${offset===0?'disabled':''}>${tr('上一页','Previous page')}</button><button id="watch-next" ${offset+page.limit>=page.total?'disabled':''}>${tr('下一页','Next page')}</button><span class="hint">${tr('共','Total')} ${page.total} ${tr('张小图','thumbnails')}</span></div></section><div class="gallery">${page.items.map((x,i)=>`<article class="card image-card"><button class="image-preview" data-watch-preview="${i}" aria-label="${tr('预览','Preview')} · ${esc(x.name)}"><img class="thumb" data-src="${esc(x.url)}" alt="${esc(x.name)}" style="aspect-ratio:${x.thumbnail.width}/${x.thumbnail.height};object-fit:contain"></button><h3>${esc(x.name)}</h3><small>${x.thumbnail.width} × ${x.thumbnail.height} · ${esc(x.thumbnail.plan?.name||tr('默认手表方案','Default watch plan'))}</small><small>${esc(fmt(x.thumbnail.generated_at||x.created_at))}</small><div class="actions"><button data-watch-preview="${i}">${tr('预览','Preview')}</button><button data-watch-download="${i}">${tr('下载图片','Download image')}</button><button class="danger" data-watch-delete="${i}">${t('delete')}</button></div></article>`).join('')}</div>${page.items.length?'':`<section class="card empty"><strong>${tr('还没有小图','No thumbnails yet')}</strong><p>${tr('在“小图方案”中生成,或为照片任务勾选生成小图。旧版手动生成的小图仍在原本地图库。','Generate one in Thumbnail plans or enable thumbnails on a photo task. Older manually generated images remain in the original local gallery.')}</p></section>`}`;
document.querySelectorAll('[data-watch-tab]').forEach(b=>b.onclick=()=>{state.tab=b.dataset.watchTab;return navigate('gallery');});
bind('watch-refresh',()=>navigate());bind('watch-plans',()=>navigate('thumbnails'));
bind('watch-prev',()=>{state.watchOffset=Math.max(0,offset-50);return navigate();});bind('watch-next',()=>{state.watchOffset=offset+50;return navigate();});
document.querySelectorAll('[data-watch-preview]').forEach(b=>b.onclick=busy(()=>{const x=page.items[b.dataset.watchPreview];return openImageViewer(x.url,x.name);}));
document.querySelectorAll('[data-watch-download]').forEach(b=>b.onclick=busy(()=>{const x=page.items[b.dataset.watchDownload];return downloadImage(x.url,x.name);}));
document.querySelectorAll('[data-watch-delete]').forEach(b=>b.onclick=()=>{const x=page.items[b.dataset.watchDelete];return confirmDialog(tr('删除这张手表小图?不会删除源照片。','Delete this watch thumbnail? The source photo is kept.'),async()=>{await api('/api/v1/assets/'+x.asset_id,{method:'DELETE'});state.watchOffset=Math.max(0,offset-(page.items.length===1?50:0));await navigate();});});
hydrateImages();
}