feat: split source and terminal library navigation
This commit is contained in:
+3
-3
@@ -130,7 +130,7 @@ f9e777980ea5bb3ea8fb2357f23530802811ea2674d25100af9b728cbc9f83a9 pigway_frame_f
|
||||
8f37296f52a88c11a000a5a0767310354550da34e1d4c0c7260393ec00cd0d9b pigway_frame_flow/templates/quote.json
|
||||
82c17f4e7eaac95483592add181f549186dea9d7c0375fa9cad3554e3ccd1794 pigway_frame_flow/templates/system-alert.json
|
||||
d6f705b9d183b57d50759e283db2161f8493c4e3969459e74da635252014c925 pigway_frame_flow/templates/weather.json
|
||||
e7305a0e1870df824fd0d46663bf646f0105ce66308a0bd785baa95b6c1e99c5 pigway_frame_flow/web/app.js
|
||||
67542e813a803fccbaab17db8ad525c32219d6ba88d9c83e65aac95cf2325df6 pigway_frame_flow/web/app.js
|
||||
f332fac63f611572597812a06ae0cc266a59e5677efda0d6e9b6925acf9f9309 pigway_frame_flow/web/apple-touch-icon.png
|
||||
d7d55b06ead173b4c23a277fa39800cadfc18f1163c500e8617cfc1f4ee0dea9 pigway_frame_flow/web/extensions.js
|
||||
94558dbf524a1068e83283eb814c1b1501512d4b7e75e092b52e9aabfea720e7 pigway_frame_flow/web/favicon.ico
|
||||
@@ -138,13 +138,13 @@ d7d55b06ead173b4c23a277fa39800cadfc18f1163c500e8617cfc1f4ee0dea9 pigway_frame_f
|
||||
1f77efa9acfb40872593e3b7c475d972b960cb9f21e9afb936add03c3f2dd542 pigway_frame_flow/web/features.js
|
||||
a07ea444ab4ac75ea11e1188a431b48195353b1e08f7e80f3b931e8097ea6221 pigway_frame_flow/web/i18n.js
|
||||
25f975a560aab00b57c5527a674d52bc45bd6c4b42e6169f1db406f2040f2177 pigway_frame_flow/web/index.html
|
||||
6128df031931bea57c6fa6107c534872b5d866f24ce8f901fd2a870c82bda674 pigway_frame_flow/web/libraries.js
|
||||
7787ff064be5beddd18cd5c7271aa900b33f991ae95dd9ff8ffe95154f7b2a3a pigway_frame_flow/web/libraries.js
|
||||
0fa494a3fbd456c754bbea3f7f050be139e32e3b42b09a1ed953094087437210 pigway_frame_flow/web/logo-symbol.svg
|
||||
86ecbf86eb41a7a46b4878cbac679f83dc3de0ee6687bcc7779140689f0b5ec3 pigway_frame_flow/web/style.css
|
||||
cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30 pigway_frame_flow/web/swagger-LICENSE
|
||||
050bc415ee7048dcd881682678f720264e7da5e373f7461d7c58c755305255f7 pigway_frame_flow/web/swagger-ui-bundle.js
|
||||
1ac324f7dcd27e4b9386b4bd6421271ec147e922a22c05ba24b11515e9aa6321 pigway_frame_flow/web/swagger-ui.css
|
||||
ec7703151fe7c10e8bdeb0ee303e725376dffe148ea22df729564f72ed5759ff pigway_frame_flow/web/thumbnails.js
|
||||
ec920d7a083968ddfb15d23295499c64ace13cd2e65ca01a373a69158837bc3f pigway_frame_flow/web/thumbnails.js
|
||||
5f392f94636d261ae2982815763b794ae9595a1b9226946b792105634ec3f559 pyproject.toml
|
||||
d34ac82e4c3feacd38546aa2f0a4491ab17ddbf3ad8f489375783166d78923f0 requirements.lock
|
||||
008af5f4adf616c86e805febc016faad60be0008abf5dddad6e955f1d03606cf scripts/build-verify.sh
|
||||
|
||||
@@ -8,7 +8,7 @@ async function loadConnection(){
|
||||
state.settings=settings;state.devices=devices;state.templates=templates;state.device=devices[0]?.id||settings.device_id||state.device;
|
||||
if(settings.device_catalog&&state.page!=='dashboard')void api('/api/v1/devices').then(details=>{if(state.settings!==settings||!state.token)return;state.devices=details;shell();}).catch(error=>{if(state.settings===settings&&state.token)toast(error.message);});
|
||||
}
|
||||
const icons={dashboard:'◫',gallery:'▧','photo-sources':'▣',upload:'↥',templates:'▦',studio:'▦',thumbnails:'▧',automation:'◷',playlists:'▤',history:'↺',connections:'↔',settings:'⚙'};
|
||||
const icons={dashboard:'◫',gallery:'▧','terminal-gallery':'▧','photo-sources':'▣',upload:'↥',templates:'▦',studio:'▦',thumbnails:'▧',automation:'◷',playlists:'▤',history:'↺',connections:'↔',settings:'⚙'};
|
||||
const t=k=>window.MESSAGES[state.lang][k]||k;
|
||||
function operationErrorMessage(error){const reason=error?.details?.reason;if(reason&&t(reason)!==reason)return t(reason);return error?.code&&t(error.code)!==error.code?t(error.code):(error?.message||t('error'));}
|
||||
const esc=v=>String(v??'—').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||||
@@ -49,7 +49,7 @@ function operationResultHTML(o){
|
||||
}
|
||||
function operationErrorLabel(error){if(!error?.code)return '';const label=t(error.code);return label===error.code?t('error'):label;}
|
||||
function deviceDisplayName(device){const name=device.name||device.id;return device.id==='living-room'&&['living-room','Living room'].includes(name)?t('livingRoom'):name;}
|
||||
function shell(){document.documentElement.lang=state.lang==='zh'?'zh-CN':'en';$('#lang').textContent=state.lang==='zh'?'EN':'中文';$('#theme').title=t('theme');$('#nav').innerHTML=Object.entries(icons).filter(([id])=>id!=='legacy-plans').map(([id,icon])=>`<button data-page="${id}" class="${id===state.page?'active':''}" ${id===state.page?'aria-current="page"':''}><span class="nav-icon">${icon}</span><span>${t(id)}</span></button>`).join('');document.querySelectorAll('[data-page]').forEach(b=>b.onclick=()=>navigate(b.dataset.page));$('#page-title').textContent=t(state.page);const watchSelected=state.page==='gallery'&&state.tab==='watch';$('#device').innerHTML=(state.devices.length?state.devices:[{id:state.device,name:state.device}]).map(d=>`<option value="${esc(d.id)}" ${!watchSelected&&d.id===state.device?'selected':''}>${esc(deviceDisplayName(d))}</option>`).join('')+`<option value="@watch" ${watchSelected?'selected':''}>${tr('手表 · 410×502','Watch · 410×502')}</option>`;revealCurrentNavigation();}
|
||||
function shell(){document.documentElement.lang=state.lang==='zh'?'zh-CN':'en';$('#lang').textContent=state.lang==='zh'?'EN':'中文';$('#theme').title=t('theme');$('#nav').innerHTML=Object.entries(icons).filter(([id])=>!['legacy-plans','legacy-gallery'].includes(id)).map(([id,icon])=>`<button data-page="${id}" class="${id===state.page?'active':''}" ${id===state.page?'aria-current="page"':''}><span class="nav-icon">${icon}</span><span>${t(id)}</span></button>`).join('');document.querySelectorAll('[data-page]').forEach(b=>b.onclick=()=>navigate(b.dataset.page));$('#page-title').textContent=t(state.page);const watchSelected=['terminal-gallery','legacy-gallery'].includes(state.page)&&state.tab==='watch';$('#device').innerHTML=(state.devices.length?state.devices:[{id:state.device,name:state.device}]).map(d=>`<option value="${esc(d.id)}" ${!watchSelected&&d.id===state.device?'selected':''}>${esc(deviceDisplayName(d))}</option>`).join('')+`<option value="@watch" ${watchSelected?'selected':''}>${tr('手表 · 410×502','Watch · 410×502')}</option>`;revealCurrentNavigation();}
|
||||
// A destination appears only when its connection and read/write adapter are available.
|
||||
// Future cloud-drive destinations must implement their adapter before registration here.
|
||||
const galleryDestinationAdapters=new Set(['kokonna','local-assets','watch-thumbnails']);
|
||||
@@ -150,7 +150,7 @@ const views={
|
||||
$('.gallery')?.setAttribute('aria-busy','false');
|
||||
}
|
||||
},
|
||||
async upload(){state.preview=null;$('#main').innerHTML=notice()+`<div class="notice">${t('uploadNotice')}</div><div class="columns"><section class="card"><h2>${t('upload')}</h2><div class="dropzone"><strong>${t('selectFile')}</strong><p class="hint">${t('uploadHint')}</p><input id="file" type="file" accept="image/jpeg,image/png,image/webp" aria-label="${t('selectFile')}"></div><div class="form-grid" style="margin-top:24px"><div class="field"><label for="fit">${t('mode')}</label><select id="fit">${['contain','cover','smart'].map(x=>`<option value="${x}">${t(x)}</option>`).join('')}</select></div><div class="field"><label for="profile">${t('profile')}</label><select id="profile">${['color','grayscale'].map(x=>`<option value="${x}">${t(x)}</option>`).join('')}</select></div></div>${buttons('make-preview','preview',true)}<p class="hint">${t('previewHint')}</p></section><section class="card"><h3>${t('preview')}</h3><div id="preview-box" class="empty">${t('previewHint')}</div><div class="actions" style="margin-top:18px"><button id="save-local" disabled>${t('staging')}</button><button id="display-upload" class="primary" disabled>${t('display')}</button></div></section></div>`;bind('make-preview',async()=>{const file=$('#file').files[0];if(!file)throw new Error(t('selectFile'));const form=new FormData();form.append('file',file);form.append('fit',$('#fit').value);form.append('profile',$('#profile').value);state.preview=await api('/api/v1/preview',{method:'POST',body:form});$('#preview-box').className='';$('#preview-box').innerHTML='<img id="upload-preview" class="preview" alt="Preview">';await blobImage($('#upload-preview'),state.preview.url);$('#save-local').disabled=false;$('#display-upload').disabled=!liveWriteAllowed();});for(const id of ['file','fit','profile'])$('#'+id).onchange=()=>{state.preview=null;$('#save-local').disabled=true;$('#display-upload').disabled=true;};bind('save-local',async()=>{const id=await accepted('/api/v1/devices/'+state.device+'/upload',{type:'image',asset_id:state.preview.asset_id});await waitOperation(id);state.tab='local';return navigate('gallery');});bind('display-upload',()=>displayDialog({type:'image',asset_id:state.preview.asset_id}));},
|
||||
async upload(){state.preview=null;$('#main').innerHTML=notice()+`<div class="notice">${t('uploadNotice')}</div><div class="columns"><section class="card"><h2>${t('upload')}</h2><div class="dropzone"><strong>${t('selectFile')}</strong><p class="hint">${t('uploadHint')}</p><input id="file" type="file" accept="image/jpeg,image/png,image/webp" aria-label="${t('selectFile')}"></div><div class="form-grid" style="margin-top:24px"><div class="field"><label for="fit">${t('mode')}</label><select id="fit">${['contain','cover','smart'].map(x=>`<option value="${x}">${t(x)}</option>`).join('')}</select></div><div class="field"><label for="profile">${t('profile')}</label><select id="profile">${['color','grayscale'].map(x=>`<option value="${x}">${t(x)}</option>`).join('')}</select></div></div>${buttons('make-preview','preview',true)}<p class="hint">${t('previewHint')}</p></section><section class="card"><h3>${t('preview')}</h3><div id="preview-box" class="empty">${t('previewHint')}</div><div class="actions" style="margin-top:18px"><button id="save-local" disabled>${t('staging')}</button><button id="display-upload" class="primary" disabled>${t('display')}</button></div></section></div>`;bind('make-preview',async()=>{const file=$('#file').files[0];if(!file)throw new Error(t('selectFile'));const form=new FormData();form.append('file',file);form.append('fit',$('#fit').value);form.append('profile',$('#profile').value);state.preview=await api('/api/v1/preview',{method:'POST',body:form});$('#preview-box').className='';$('#preview-box').innerHTML='<img id="upload-preview" class="preview" alt="Preview">';await blobImage($('#upload-preview'),state.preview.url);$('#save-local').disabled=false;$('#display-upload').disabled=!liveWriteAllowed();});for(const id of ['file','fit','profile'])$('#'+id).onchange=()=>{state.preview=null;$('#save-local').disabled=true;$('#display-upload').disabled=true;};bind('save-local',async()=>{const id=await accepted('/api/v1/devices/'+state.device+'/upload',{type:'image',asset_id:state.preview.asset_id});await waitOperation(id);state.tab='local';return navigate('legacy-gallery');});bind('display-upload',()=>displayDialog({type:'image',asset_id:state.preview.asset_id}));},
|
||||
async templates(){state.templates=await api('/api/v1/templates');const selected=state.templates.find(x=>x.id===state.selectedTemplate)||state.templates[0];state.selectedTemplate=selected.id;if(state.pendingTemplateData?.template===selected.id){selected.defaults=state.pendingTemplateData.data;state.initialProfile=state.pendingTemplateData.profile;state.pendingTemplateData=null;}state.templatePreview=null;$('#main').innerHTML=notice()+`<div class="notice">${t('templateHint')}<p>${t('templateSteps')}</p></div><div class="template-grid">${state.templates.map((x,i)=>`<div class="card template-card ${x.id===selected.id?'selected':''}" role="button" tabindex="0" data-template="${x.id}"><span>${{'daily-poetry':'詩','calendar':'▦','daily-dashboard':'☀','photo-text':'▣','quote':'❝','system-alert':'◒','weather':'☁'}[x.id]||'▦'}</span><div><h3>${esc(x.name.split(' / ')[state.lang==='zh'?1:0]||x.name)}</h3><small>1200 × 1600 · ${esc(x.id)}</small></div></div>`).join('')}</div><div class="columns"><section class="card"><h3>${t('templateData')}</h3><p class="hint">${t('templateGuideTitle')}</p><p class="hint">${t({'photo-text':'guidePhotoText','daily-dashboard':'guideDashboard',weather:'guideWeather',calendar:'guideCalendar',quote:'guideQuote','daily-poetry':'guidePoetry','system-alert':'guideAlert'}[selected.id]||'templateContentHelp')}</p>${['weather','daily-dashboard'].includes(selected.id)?`<p class="hint">${t('templateWeatherHelp')}</p>`:''}<textarea id="template-data" aria-label="${t('templateData')}">${esc(JSON.stringify(selected.defaults,null,2))}</textarea><div class="field" style="margin-top:16px"><label for="template-profile">${t('profile')}</label><select id="template-profile"><option value="color">${t('color')}</option><option value="grayscale">${t('grayscale')}</option></select><p class="hint">${t('helpProfile')}</p></div><p class="hint">${t('templateActionHelp')}</p><p class="hint">${t('templateAutomationHelp')}</p><div class="actions">${buttons('render-template','render',true)}${buttons('save-template','saveTemplate')}</div></section><section class="card"><h3>${t('preview')}</h3><div id="template-preview" class="empty">${t('templatePreviewHint')}</div><button id="display-template" class="primary" style="margin-top:18px" disabled>${t('display')}</button></section></div>`;document.querySelectorAll('[data-template]').forEach(b=>{b.onclick=()=>{state.selectedTemplate=b.dataset.template;navigate(['collage','poster'].includes(b.dataset.template)?'studio':undefined);};b.onkeydown=e=>{if(e.key==='Enter')b.click();};});bind('render-template',async()=>{const content={type:'template',template:selected.id,data:JSON.parse($('#template-data').value),profile:$('#template-profile').value};if([...document.querySelectorAll('#template-fields textarea'),$('#template-data')].some(x=>!x.checkValidity())){toast(t('INVALID_REQUEST'));return;}const id=await accepted('/api/v1/render',content);$('#template-preview').textContent=t('previewLoading');const op=await waitOperation(id);state.templatePreview=op.result.asset_id;$('#template-preview').className='';$('#template-preview').innerHTML='<img id="template-image" class="preview" alt="Template preview">';await blobImage($('#template-image'),op.result.url);$('#display-template').disabled=!liveWriteAllowed();});bind('save-template',async()=>{await api('/api/v1/templates/'+selected.id+'/config',{method:'PUT',body:{data:JSON.parse($('#template-data').value)}});toast(t('done'));});$('#template-data').oninput=()=>$('#display-template').disabled=true;$('#template-profile').onchange=()=>$('#display-template').disabled=true;bind('display-template',()=>displayDialog({type:'image',asset_id:state.templatePreview}));await templateEditor(selected);if(state.initialProfile){$('#template-profile').value=state.initialProfile;state.initialProfile=null;}},
|
||||
async playlists(epoch){const playlists=await api('/api/v1/playlists');if(epoch!==state.epoch)return;$('#main').innerHTML=notice()+`<div class="notice">${t('playlistNotice')}</div><div class="section-heading"><h2>${t('playlists')}</h2>${buttons('new-playlist','newPlaylist',true)}</div><div class="gallery">${playlists.map((p,i)=>`<section class="card"><span class="badge">${t(p.enabled?'enabled':'disabled')}</span><h3 style="margin-top:16px">${esc(p.name)}</h3><p class="hint">${p.image_ids.length} ${t('count')} · ${p.interval_minutes} ${t('minutes')} · ${t(p.mode)}</p><p><code>${esc(p.image_ids.join(' → '))}</code></p><small>${t('lastScheduled')}: ${p.position>=0?esc(p.image_ids[p.position]):'—'}</small><div class="actions" style="margin-top:16px"><button data-pl-edit="${i}">${t('edit')}</button><button class="danger" data-pl-delete="${i}">${t('remove')}</button></div></section>`).join('')||`<div class="card empty"><strong>${t('noTasks')}</strong></div>`}</div>`;bind('new-playlist',()=>playlistEditor());document.querySelectorAll('[data-pl-edit]').forEach(b=>b.onclick=()=>playlistEditor(playlists[b.dataset.plEdit]));document.querySelectorAll('[data-pl-delete]').forEach(b=>b.onclick=()=>confirmDialog(t('removeConfirm'),async()=>{await api('/api/v1/playlists/'+playlists[b.dataset.plDelete].id,{method:'DELETE'});await navigate();}));},
|
||||
async history(epoch){const data=await api('/api/v1/operation-history?'+historyQuery());if(epoch!==state.epoch)return;state.historyPageData=data;state.historyPage=data.page;const ops=data.items;$('#main').innerHTML=`<div class="section-heading"><h2>${t('history')}</h2>${buttons('history-refresh','refresh')}</div>${ops.some(x=>x.status==='UNKNOWN')?`<div class="notice warning">${t('unknown')}</div>`:''}<div class="card table-wrap">${ops.length?`<table><thead><tr>${['created','action','status','result','client','requestId'].map(x=>`<th>${t(x)}</th>`).join('')}</tr></thead><tbody>${ops.map((o,i)=>`<tr data-operation-id="${esc(o.operation_id)}"><td>${fmt(o.created_at)}</td><td>${esc(operationActionLabel(o.action))}<br><small>${esc(o.action==='library-generate'?tr('按来源绑定终端','Source-bound terminals'):deviceNameForId(o.device_id))}</small></td><td><span class="history-status ${['FAILED','UNKNOWN'].includes(o.status)?'bad':''}">${t(o.status)}</span>${o.status==='UNKNOWN'?`<div><button data-reconcile="${i}">${t('reconcile')}</button></div>`:''}</td><td>${operationResultHTML(o)}</td><td>${esc(operationClientLabel(o.client_id))}</td><td><code>${esc(o.request_id)}<br>${esc(o.operation_id)}</code></td></tr>`).join('')}</tbody></table>`:`<div class="empty">${t('noHistory')}</div>`}</div>`;bind('history-refresh',()=>navigate());document.querySelectorAll('[data-reconcile]').forEach(b=>b.onclick=busy(async()=>{const o=ops[b.dataset.reconcile];const r=await api('/api/v1/operations/'+o.operation_id+'/reconcile',{method:'POST',body:{acknowledge:false}});showModal(`<h2>${t('reconcile')}</h2><p>${t('current')}: ${esc(r.current_image_id)}</p><p>${t('result')}: ${esc(r.expected_image_id)}</p><p class="hint">${t('unknown')}</p><div class="modal-actions">${buttons('modal-cancel','cancel')}${r.resolved?'':buttons('acknowledge','acknowledge')}</div>`);bind('acknowledge',async()=>{await api('/api/v1/operations/'+o.operation_id+'/reconcile',{method:'POST',body:{acknowledge:true}});$('#modal').close();await navigate();});}));},
|
||||
|
||||
@@ -1,28 +1,36 @@
|
||||
'use strict';
|
||||
// Keep older assets and APIs accessible without interpreting frame products as sources.
|
||||
Object.assign(MESSAGES.zh,{'legacy-plans':'旧版小图方案',libraryOutputPartial:'部分终端输出失败,源画面与成功版本已保留,可单独重试失败目标。',libraryGenerateFirst:'请先按终端生成方案生成成品,源画面不能直接推送。',librarySourceMismatch:'方案不属于所选来源图库,请重新选择。',librarySizeMismatch:'方案尺寸与终端屏幕不匹配;相框使用设备屏幕尺寸,当前手表为 410×502。',libraryPlanBound:'此方案正在被终端使用,请先解除绑定或更换方案。',libraryBindFirst:'请先在终端图库中绑定此来源及其中的生成方案。',libraryBindingChanged:'终端绑定已改变,此版本不再匹配,已停止推送。'});Object.assign(MESSAGES.en,{'legacy-plans':'Legacy thumbnail plans',libraryOutputPartial:'Some outputs failed. Source pixels and successful versions are kept; retry failed targets individually.',libraryGenerateFirst:'Generate a terminal version before sending source pixels.',librarySourceMismatch:'The plan belongs to another source library.',librarySizeMismatch:'Plan dimensions must match the terminal screen. The current watch uses 410×502.',libraryPlanBound:'Unbind terminals or select another plan first.',libraryBindFirst:'Bind a terminal to this source and one of its plans first.',libraryBindingChanged:'The terminal binding changed; this version was not sent.'});
|
||||
Object.assign(MESSAGES.zh,{gallery:'来源图库','terminal-gallery':'终端图库','legacy-gallery':'历史图库'});
|
||||
Object.assign(MESSAGES.en,{gallery:'Source library','terminal-gallery':'Terminal library','legacy-gallery':'Legacy gallery'});
|
||||
icons['legacy-gallery']='▧';
|
||||
const legacyGalleryView=views.gallery;
|
||||
const legacyPlanView=views.thumbnails;
|
||||
delete icons.thumbnails;
|
||||
icons['legacy-plans']='▧';
|
||||
views['legacy-plans']=legacyPlanView;
|
||||
const libraryUI={kind:'source',source:null,plan:null,terminal:null,offset:0,legacy:false};
|
||||
const libraryUI={kind:'source',source:null,plan:null,terminal:null,offset:0};
|
||||
|
||||
function libraryStatus(status){return ({GENERATED:tr('已生成','Generated'),QUEUED:tr('待推送','Queued'),RENDERING:tr('准备推送','Preparing'),UPLOADING:tr('上传中','Uploading'),SYNCING:tr('等待设备确认','Awaiting confirmation'),COMPLETED:tr('已确认显示','Display confirmed'),FAILED:tr('推送失败','Failed'),UNKNOWN:tr('结果待核对','Outcome unknown')})[status]||status;}
|
||||
function terminalLabel(terminal){return terminal.id==='watch'?tr('手表','Watch'):deviceNameForId(terminal.id);}
|
||||
function librarySelectOptions(items,selected,label){return `<option value="">${label}</option>`+items.map(p=>`<option value="${esc(p.id)}" ${p.id===selected?'selected':''}>${esc(p.name)}</option>`).join('');}
|
||||
function libraryLegacy(tab){libraryUI.legacy=true;state.tab=tab;return navigate('gallery');}
|
||||
function libraryLegacy(tab){state.tab=tab;return navigate('legacy-gallery');}
|
||||
|
||||
views.gallery=async function(epoch){
|
||||
if(libraryUI.legacy){await legacyGalleryView(epoch);if(epoch!==state.epoch)return;const back=document.createElement('button');back.textContent=tr('返回新图库','Back to libraries');back.onclick=()=>{libraryUI.legacy=false;return navigate('gallery');};$('#main').prepend(back);return;}
|
||||
views['legacy-gallery']=async function(epoch){
|
||||
await legacyGalleryView(epoch);if(epoch!==state.epoch)return;
|
||||
const back=document.createElement('button');back.textContent=tr('返回来源图库','Back to source library');back.onclick=()=>navigate('gallery');$('#main').prepend(back);
|
||||
};
|
||||
views.gallery=epoch=>libraryPage(epoch,'source');
|
||||
views['terminal-gallery']=epoch=>libraryPage(epoch,'terminal');
|
||||
async function libraryPage(epoch,kind){
|
||||
if(libraryUI.kind!==kind)libraryUI.offset=0;
|
||||
libraryUI.kind=kind;
|
||||
const [sources,terminals]=await Promise.all([api('/api/v1/source-libraries'),api('/api/v1/library-terminals')]);
|
||||
if(epoch!==state.epoch)return;
|
||||
$('#main').innerHTML=`<div class="section-heading"><div class="tabs"><button id="library-source-tab" class="${libraryUI.kind==='source'?'active':''}">${tr('来源图库','Source libraries')}</button><button id="library-terminal-tab" class="${libraryUI.kind==='terminal'?'active':''}">${tr('终端图库','Terminal libraries')}</button></div><button id="library-refresh">${t('refresh')}</button></div><div id="library-content"></div>`;
|
||||
$('#main').innerHTML=`<div class="section-heading"><span></span><button id="library-refresh">${t('refresh')}</button></div><div id="library-content"></div>`;
|
||||
bind('library-refresh',()=>navigate());
|
||||
bind('library-source-tab',()=>{libraryUI.kind='source';libraryUI.offset=0;state.tab='local';return navigate();});
|
||||
bind('library-terminal-tab',()=>{libraryUI.kind='terminal';libraryUI.offset=0;return navigate();});
|
||||
if(libraryUI.kind==='source')await sourceLibraryPage(epoch,sources,terminals);else await terminalLibraryPage(epoch,sources,terminals);
|
||||
};
|
||||
if(kind==='source')await sourceLibraryPage(epoch,sources,terminals);else await terminalLibraryPage(epoch,sources,terminals);
|
||||
}
|
||||
|
||||
async function sourceLibraryPage(epoch,sources,terminals){
|
||||
const panel=$('#library-content');
|
||||
@@ -85,7 +93,7 @@ async function terminalLibraryPage(epoch,sources,terminals){
|
||||
$('#library-content').innerHTML=`<section class="card"><div class="field"><label for="library-terminal">${tr('终端','Terminal')}</label><select id="library-terminal">${terminals.map(t=>`<option value="${esc(t.id)}" ${t.id===terminal.id?'selected':''}>${esc(terminalLabel(t))}</option>`).join('')}</select></div><h2>${esc(terminalLabel(terminal))}</h2><p>${tr('来源图库:','Source library: ')}${esc(source?.name||tr('未绑定','Not bound'))} · ${tr('生成方案:','Generation plan: ')}${esc(plan?.name||tr('未选择','Not selected'))}</p><p class="hint">${terminal.transport_supported?tr('生成、上传和设备确认分别记录;已生成不等于已显示。','Generation, upload and device confirmation are tracked separately. Generated does not mean displayed.'):tr('当前手表屏幕 410×502,传输尚未接入;这里管理生成成品。','Current watch screen: 410×502. Transfer is not connected; this library manages generated versions.')}</p><div class="actions"><button id="library-binding" class="primary">${tr('设置来源与方案(各单选)','Bind one source and one plan')}</button>${terminal.binding?`<button id="library-unbind">${tr('解除绑定','Unbind')}</button><button id="library-to-source">${tr('查看来源与生成版本','View source and versions')}</button>`:''}<button id="library-old-terminal">${terminal.kind==='watch'?tr('旧版小图图库','Legacy watch gallery'):tr('设备已有云端图片','Existing device cloud images')}</button></div></section><div class="actions"><button id="terminal-prev" ${libraryUI.offset===0?'disabled':''}>${tr('上一页','Previous')}</button><button id="terminal-next" ${libraryUI.offset+30>=page.total?'disabled':''}>${tr('下一页','Next')}</button><span>${tr('共','Total')} ${page.total} ${tr('个成品版本','generated versions')}</span></div><div class="gallery">${page.items.map((v,i)=>`<article class="card image-card"><button class="image-preview" data-terminal-preview="${i}"><img class="thumb" alt="${esc(v.name)}" data-src="${esc(v.url)}" style="object-fit:contain;aspect-ratio:${v.plan.width}/${v.plan.height}"></button><h3>${esc(v.name)}</h3><small>${v.plan.width} × ${v.plan.height} · ${esc(v.plan.name)}</small><small>${esc(libraryStatus(v.delivery_status))} · ${esc(fmt(v.created_at))}</small><div class="actions"><button data-terminal-preview="${i}">${tr('预览','Preview')}</button><button data-terminal-download="${i}">${tr('下载图片','Download image')}</button><button data-terminal-settings="${i}">${tr('生成参数','Generation settings')}</button>${v.operation?`<button data-terminal-operation="${i}">${tr('推送记录','Delivery history')}</button>`:''}<button data-terminal-send="${i}" ${!terminal.transport_supported||!liveWriteAllowed()||['QUEUED','RENDERING','UPLOADING','SYNCING','UNKNOWN'].includes(v.delivery_status)?'disabled':''} title="${terminal.transport_supported?(liveWriteAllowed()?'':esc(t('readOnlyWarning'))):tr('手表传输尚未接入','Watch transfer is not connected')}">${tr('推送到终端','Send to terminal')}</button></div></article>`).join('')}</div>${page.items.length?'':`<section class="card empty">${tr('尚无对应成品。绑定来源与方案后,在来源图库选入照片并生成。','No matching versions. Bind a source and plan, then import and generate in the source library.')}</section>`}`;
|
||||
$('#library-terminal').onchange=()=>{libraryUI.terminal=$('#library-terminal').value;libraryUI.offset=0;state.tab=libraryUI.terminal==='watch'?'watch':'cloud';if(libraryUI.terminal!=='watch')state.device=libraryUI.terminal;return navigate();};
|
||||
bind('library-binding',()=>terminalBindingEditor(terminal,sources));bind('library-unbind',()=>confirmDialog(tr('解除终端绑定?已有图片保留,后续来源任务不再向此终端输出。','Unbind this terminal? Images are kept; future source tasks will not output to it.'),async()=>{await api('/api/v1/library-terminals/'+encodeURIComponent(terminal.id)+'/binding',{method:'DELETE'});await navigate();}));
|
||||
bind('library-to-source',()=>{libraryUI.source=terminal.binding.source_id;libraryUI.plan=terminal.binding.plan_id;libraryUI.kind='source';libraryUI.offset=0;state.tab='local';return navigate();});
|
||||
bind('library-to-source',()=>{libraryUI.source=terminal.binding.source_id;libraryUI.plan=terminal.binding.plan_id;libraryUI.offset=0;state.tab='local';return navigate('gallery');});
|
||||
bind('library-old-terminal',()=>{if(terminal.id!=='watch')state.device=terminal.id;return libraryLegacy(terminal.id==='watch'?'watch':'cloud');});
|
||||
bind('terminal-prev',()=>{libraryUI.offset=Math.max(0,libraryUI.offset-30);return navigate();});bind('terminal-next',()=>{libraryUI.offset+=30;return navigate();});
|
||||
document.querySelectorAll('[data-terminal-preview]').forEach(b=>b.onclick=busy(()=>{const v=page.items[b.dataset.terminalPreview];return openImageViewer(v.url,v.name);}));
|
||||
@@ -102,7 +110,7 @@ async function terminalBindingEditor(terminal,sources){
|
||||
$('#binding-source').onchange=busy(()=>load(null));$('#terminal-binding-form').onsubmit=busy(async e=>{e.preventDefault();await api('/api/v1/library-terminals/'+encodeURIComponent(terminal.id)+'/binding',{method:'PUT',body:{source_id:$('#binding-source').value,plan_id:$('#binding-plan').value}});$('#modal').close();await navigate();});await load(terminal.binding?.plan_id);
|
||||
}
|
||||
|
||||
$('#device').onchange=()=>{const id=$('#device').value;if(id==='@watch'||state.page==='gallery'){libraryUI.kind='terminal';libraryUI.terminal=id==='@watch'?'watch':id;libraryUI.offset=0;libraryUI.legacy=false;state.tab=id==='@watch'?'watch':'cloud';if(id!=='@watch')state.device=id;return navigate('gallery');}state.device=id;return navigate();};
|
||||
$('#device').onchange=()=>{const id=$('#device').value;if(id==='@watch'||['terminal-gallery','legacy-gallery'].includes(state.page)){libraryUI.kind='terminal';libraryUI.terminal=id==='@watch'?'watch':id;libraryUI.offset=0;state.tab=id==='@watch'?'watch':'cloud';if(id!=='@watch')state.device=id;return navigate('terminal-gallery');}state.device=id;return navigate();};
|
||||
|
||||
async function libraryJobFields(job,isNew){
|
||||
const box=document.createElement('section');box.innerHTML=`<label class="check-row"><input id="job-library-pipeline" type="checkbox" ${job.library_pipeline||(isNew&&job.source?.type==='photo_source')?'checked':''}>${tr('按来源图库统一生成并推送','Generate and send through source-library bindings')}</label><p class="hint">${tr('选一次照片并保存源画面,按来源内的终端绑定分别生成;同一方案共用成品。相框继续走上传和显示确认,手表仅生成。此模式不再单独选择“小图方案”。旧任务默认保留原模式,勾选后切换。','Select one photo, save source pixels and generate each bound plan. Terminals sharing a plan share its version. Frames upload and confirm display; watches generate only. Legacy tasks keep their old mode until enabled.')}</p>`;
|
||||
|
||||
@@ -10,7 +10,7 @@ views.thumbnails=async function(epoch){
|
||||
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');});
|
||||
bind('thumb-gallery',()=>{state.tab='watch';state.watchOffset=0;return navigate('legacy-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();});
|
||||
@@ -38,7 +38,7 @@ async function watchGalleryPage(epoch,destinations){
|
||||
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');});
|
||||
document.querySelectorAll('[data-watch-tab]').forEach(b=>b.onclick=()=>{state.tab=b.dataset.watchTab;return navigate('legacy-gallery');});
|
||||
bind('watch-refresh',()=>navigate());bind('watch-plans',()=>navigate('legacy-plans'));
|
||||
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);}));
|
||||
|
||||
Reference in New Issue
Block a user