Enlarge image viewer with fit and actual-size modes
This commit is contained in:
@@ -42,7 +42,19 @@ function login(){ $('#main').innerHTML=`<div class="card" style="max-width:560px
|
|||||||
function clientUUID(){if(typeof crypto.randomUUID==='function')return crypto.randomUUID();const bytes=crypto.getRandomValues(new Uint8Array(16));bytes[6]=(bytes[6]&15)|64;bytes[8]=(bytes[8]&63)|128;const hex=Array.from(bytes,b=>b.toString(16).padStart(2,'0')).join('');return `${hex.slice(0,8)}-${hex.slice(8,12)}-${hex.slice(12,16)}-${hex.slice(16,20)}-${hex.slice(20)}`;}
|
function clientUUID(){if(typeof crypto.randomUUID==='function')return crypto.randomUUID();const bytes=crypto.getRandomValues(new Uint8Array(16));bytes[6]=(bytes[6]&15)|64;bytes[8]=(bytes[8]&63)|128;const hex=Array.from(bytes,b=>b.toString(16).padStart(2,'0')).join('');return `${hex.slice(0,8)}-${hex.slice(8,12)}-${hex.slice(12,16)}-${hex.slice(16,20)}-${hex.slice(20)}`;}
|
||||||
async function accepted(path,payload){const key='web-'+clientUUID();const result=await api(path,{method:'POST',body:payload,headers:{'Idempotency-Key':key}});toast(t('accepted'));return result.operation_id;}
|
async function accepted(path,payload){const key='web-'+clientUUID();const result=await api(path,{method:'POST',body:payload,headers:{'Idempotency-Key':key}});toast(t('accepted'));return result.operation_id;}
|
||||||
async function waitOperation(id){for(let n=0;n<120;n++){const operation=await api('/api/v1/operations/'+id);if(operation.status==='COMPLETED')return operation;if(['FAILED','UNKNOWN'].includes(operation.status))throw new Error(operation.status==='UNKNOWN'?t('unknown'):(operation.error?.message||t('error')));await new Promise(resolve=>setTimeout(resolve,2000));}throw new Error(t('syncing'));}
|
async function waitOperation(id){for(let n=0;n<120;n++){const operation=await api('/api/v1/operations/'+id);if(operation.status==='COMPLETED')return operation;if(['FAILED','UNKNOWN'].includes(operation.status))throw new Error(operation.status==='UNKNOWN'?t('unknown'):(operation.error?.message||t('error')));await new Promise(resolve=>setTimeout(resolve,2000));}throw new Error(t('syncing'));}
|
||||||
function showModal(html){$('#modal-content').innerHTML=html;$('#modal').showModal();bind('modal-cancel',()=>$('#modal').close());}
|
function showModal(html){
|
||||||
|
const dialog=$('#modal'),content=$('#modal-content');content.innerHTML=html;
|
||||||
|
const image=content.querySelector('img');dialog.classList.toggle('image-viewer',!!image);
|
||||||
|
if(image){
|
||||||
|
const viewport=document.createElement('div');viewport.className='image-viewer-viewport';image.before(viewport);viewport.append(image);
|
||||||
|
const toolbar=document.createElement('div');toolbar.className='actions image-viewer-toolbar';
|
||||||
|
const fit=document.createElement('button'),original=document.createElement('button');fit.textContent=t('fitScreen');original.textContent=t('actualSize');
|
||||||
|
fit.type=original.type='button';fit.setAttribute('aria-pressed','true');original.setAttribute('aria-pressed','false');toolbar.append(fit,original);viewport.before(toolbar);
|
||||||
|
const setSize=actual=>{viewport.classList.toggle('actual-size',actual);fit.setAttribute('aria-pressed',String(!actual));original.setAttribute('aria-pressed',String(actual));};
|
||||||
|
fit.onclick=()=>setSize(false);original.onclick=()=>setSize(true);
|
||||||
|
}
|
||||||
|
dialog.showModal();bind('modal-cancel',()=>dialog.close());
|
||||||
|
}
|
||||||
async function confirmDialog(message,fn){showModal(`<h2>${t('confirm')}</h2><p>${esc(message)}</p><div class="modal-actions">${buttons('modal-cancel','cancel')}${buttons('modal-confirm','confirm',true)}</div>`);bind('modal-confirm',async()=>{await fn();$('#modal').close();});}
|
async function confirmDialog(message,fn){showModal(`<h2>${t('confirm')}</h2><p>${esc(message)}</p><div class="modal-actions">${buttons('modal-cancel','cancel')}${buttons('modal-confirm','confirm',true)}</div>`);bind('modal-confirm',async()=>{await fn();$('#modal').close();});}
|
||||||
function detail(label,value){return `<div><small>${t(label)}</small><strong>${esc(value)}</strong></div>`;}
|
function detail(label,value){return `<div><small>${t(label)}</small><strong>${esc(value)}</strong></div>`;}
|
||||||
function liveWriteAllowed(){return state.settings?.mock||state.settings?.write_enabled;}
|
function liveWriteAllowed(){return state.settings?.mock||state.settings?.write_enabled;}
|
||||||
|
|||||||
@@ -40,3 +40,6 @@ Object.assign(window.MESSAGES.zh,{cloudKeepLatest:'随机任务云端仅保留
|
|||||||
Object.assign(window.MESSAGES.en,{cloudKeepLatest:'Each random task keeps its latest confirmed cloud image. Uploaded local favorites stay. Failed replacements never evict the last good image.',taskOrigin:'Task generated',originFilter:'Filter by origin',origin_all:'All origins',origin_manual:'Manual upload',origin_task:'Task generated',origin_external:'External push',origin_template:'Template generated',origin_unknown:'Unknown origin',allTasks:'All tasks',selectImage:'Select image',selectAll:'Select / deselect all',deleteSelected:'Delete selected',selectImagesFirst:'Select images first',deleteSelectedConfirm:'Delete selected local images? NAS originals remain untouched.',batchFailed:'Failed items:'});
|
Object.assign(window.MESSAGES.en,{cloudKeepLatest:'Each random task keeps its latest confirmed cloud image. Uploaded local favorites stay. Failed replacements never evict the last good image.',taskOrigin:'Task generated',originFilter:'Filter by origin',origin_all:'All origins',origin_manual:'Manual upload',origin_task:'Task generated',origin_external:'External push',origin_template:'Template generated',origin_unknown:'Unknown origin',allTasks:'All tasks',selectImage:'Select image',selectAll:'Select / deselect all',deleteSelected:'Delete selected',selectImagesFirst:'Select images first',deleteSelectedConfirm:'Delete selected local images? NAS originals remain untouched.',batchFailed:'Failed items:'});
|
||||||
|
|
||||||
Object.assign(window.MESSAGES.zh,{previousPage:'上一页',nextPage:'下一页'});Object.assign(window.MESSAGES.en,{previousPage:'Previous page',nextPage:'Next page'});
|
Object.assign(window.MESSAGES.zh,{previousPage:'上一页',nextPage:'下一页'});Object.assign(window.MESSAGES.en,{previousPage:'Previous page',nextPage:'Next page'});
|
||||||
|
|
||||||
|
Object.assign(window.MESSAGES.zh,{fitScreen:'适应屏幕',actualSize:'原始尺寸(1:1)'});
|
||||||
|
Object.assign(window.MESSAGES.en,{fitScreen:'Fit to screen',actualSize:'Actual size (1:1)'});
|
||||||
|
|||||||
@@ -54,3 +54,12 @@ input[type="file"]::file-selector-button{margin-inline-end:12px}
|
|||||||
.source-items button small{display:block;margin-top:8px;color:var(--muted)}
|
.source-items button small{display:block;margin-top:8px;color:var(--muted)}
|
||||||
|
|
||||||
.history-table [data-reconcile]{white-space:nowrap;overflow-wrap:normal;margin-top:8px}
|
.history-table [data-reconcile]{white-space:nowrap;overflow-wrap:normal;margin-top:8px}
|
||||||
|
|
||||||
|
/* Image dialogs use the viewport; forms keep their existing compact width. */
|
||||||
|
dialog.image-viewer{width:calc(100vw - 32px);max-width:1600px;max-height:calc(100dvh - 32px);padding:20px;overflow:auto}
|
||||||
|
.image-viewer-toolbar{margin:12px 0}
|
||||||
|
.image-viewer-toolbar button[aria-pressed="true"]{background:var(--accent);color:var(--panel)}
|
||||||
|
.image-viewer-viewport{max-height:calc(100dvh - 220px);overflow:auto;text-align:center;background:var(--soft);border-radius:12px}
|
||||||
|
.image-viewer-viewport img{display:block;width:auto;height:auto;max-width:100%;max-height:calc(100dvh - 220px);object-fit:contain;margin:0 auto}
|
||||||
|
.image-viewer-viewport.actual-size img{width:auto;height:auto;max-width:none;max-height:none;margin:0}
|
||||||
|
@media(max-width:600px){dialog.image-viewer{width:calc(100vw - 16px);padding:12px}.image-viewer-viewport,.image-viewer-viewport img{max-height:calc(100dvh - 240px)}.image-viewer-viewport.actual-size img{max-height:none}}
|
||||||
|
|||||||
Reference in New Issue
Block a user