$('#main').innerHTML=`<div class="notice">${tr('一次选择 1–12 张图片,生成多图拼版或海报。AI 只接收文字与尺寸,不接收照片。预览不会上传到画框。','Choose 1–12 photos for a collage or poster. AI receives text and dimensions, never photos. Preview does not upload to the frame.')}</div><div class="columns"><section class="card"><h3>${tr('选择照片','Choose photos')}</h3><input id="studio-files" type="file" multiple accept="image/jpeg,image/png,image/webp"><p class="hint">${tr('可以一次上传多张,保留原始比例。或勾选已有本地照片。','Upload several photos at once, preserving aspect ratio, or select local photos.')}</p><div id="studio-assets" class="studio-assets">${assets.map(a=>`<label class="check-row"><input type="checkbox" value="${a.asset_id}">${esc(a.name)}</label>`).join('')}</div><div class="field"><label for="studio-title">${tr('标题','Title')}</label><input id="studio-title" maxlength="100"></div><div class="field"><label for="studio-message">${tr('底部文字','Footer text')}</label><textarea id="studio-message" maxlength="500"></textarea></div><div class="form-grid"><div class="field"><label for="studio-style">${tr('布局','Layout')}</label><select id="studio-style"><option value="grid">${tr('均匀拼版','Grid')}</option><option value="hero">${tr('一张主图+多张附图','Hero with supporting photos')}</option><option value="poster">${tr('海报','Poster')}</option></select></div><div class="field"><label for="studio-orientation">${tr('方向','Orientation')}</label><select id="studio-orientation"><option value="portrait">${tr('竖向','Portrait')}</option><option value="landscape">${tr('横向','Landscape')}</option></select></div></div><div class="field"><label for="studio-profile">${t('profile')}</label><select id="studio-profile"><option value="color">${t('color')}</option><option value="grayscale">${t('grayscale')}</option></select></div><label class="check-row"><input id="studio-ai" type="checkbox">${tr('使用 AI 排版(先在连接中心配置)','Use AI layout (configure in Connections first)')}</label><div class="field"><label for="studio-prompt">${tr('AI 排版要求','AI layout instructions')}</label><textarea id="studio-prompt" maxlength="1500" placeholder="${tr('例如:第一张作为主图,其余大小错落','Example: feature the first photo with smaller supporting photos')}"></textarea></div><div class="actions"><button id="studio-plan" class="primary">${tr('规划布局','Plan layout')}</button><button id="studio-render" disabled>${t('render')}</button></div><p class="hint">${tr('规划后可拖动照片区域或修改坐标尺寸。区域不能重叠,超出范围的布局会被拒绝。','After planning, drag photo regions or edit coordinates and sizes. Overlapping or out-of-bounds layouts are rejected.')}</p><div id="studio-region-editor"></div></section><section class="card"><h3>${t('preview')}</h3><div id="studio-layout"></div><div id="studio-preview" class="empty">${tr('先规划布局,再生成最终图片','Plan the layout, then render the final image')}</div><div class="actions"><button id="studio-display" disabled>${t('display')}</button><button id="studio-download" disabled>${t('download')}</button></div></section></div>`;
$('#main').innerHTML=`<div class="notice">${tr('可选连接不会影响 NAS 照片和现有设备。账号密钥保存在受限配置文件中,外部客户端密钥仅显示一次。','Optional connections do not affect NAS photos or existing devices. Provider keys use restricted config files; client tokens are shown once.')}</div>`;
const[clients,scopes]=awaitPromise.all([api('/api/v1/clients'),api('/api/v1/client-scopes')]);constcard=document.createElement('section');card.className='card';card.innerHTML=`<h3>${tr('外部客户端权限','External client permissions')}</h3><p class="hint">${tr('网页继续使用账号登录。这里只为其他程序创建独立凭据,可随时撤销。','The web UI still uses account login. These credentials are for other programs and can be revoked.')}</p>${clients.map(c=>`<p>${esc(c.name)} · ${esc(c.client_id)} · ${t(c.enabled?'enabled':'disabled')}${c.client_id!=='admin'&&c.enabled?`<button data-revoke="${esc(c.client_id)}">${tr('撤销','Revoke')}</button>`:''}</p>`).join('')}<form id="client-form"><div class="field"><label for="client-id">Client ID</label><input id="client-id" required pattern="[a-z0-9-]{1,60}"></div><div class="field"><label for="client-name">${t('name')}</label><input id="client-name" required></div>${scopes.map(s=>`<label class="check-row"><input type="checkbox" name="client-scope" value="${s}" ${['devices:read','display:write','templates:read','render:write'].includes(s)?'checked':''}>${s}</label>`).join('')}<button type="submit">${tr('创建客户端','Create client')}</button><div id="client-secret" class="notice" hidden></div></form>`;$('#main').append(card);document.querySelectorAll('[data-revoke]').forEach(b=>b.onclick=busy(async()=>{awaitapi('/api/v1/clients/'+b.dataset.revoke,{method:'DELETE'});awaitnavigate();}));$('#client-form').onsubmit=busy(asynce=>{e.preventDefault();constr=awaitapi('/api/v1/clients',{method:'POST',body:{client_id:$('#client-id').value,name:$('#client-name').value,permissions:[...document.querySelectorAll('[name="client-scope"]:checked')].map(x=>x.value)}});$('#client-secret').hidden=false;$('#client-secret').textContent=tr('仅显示一次,请保存:','Shown once; save it: ')+r.token;});
consthooks=awaitapi('/api/v1/webhooks');constcard=document.createElement('section');card.className='card';card.innerHTML=`<h3>Webhook</h3><p class="hint">${tr('HTTPS 地址须先加入设置中的允许主机。事件带 HMAC 签名,最多重试三次;接收端应按事件 ID 去重。','Add the HTTPS host to the Settings allowlist first. Events are HMAC signed, with up to three attempts; deduplicate by event ID.')}</p>${hooks.map(h=>`<p>${esc(h.callback_url)} <button data-hook-delete="${h.id}">${t('delete')}</button></p>`).join('')}<form id="hook-form"><div class="field"><label for="hook-url">Callback URL</label><input id="hook-url" type="url" required></div><div class="field"><label for="hook-events">${tr('事件(逗号分隔)','Events (comma separated)')}</label><input id="hook-events" value="operation.completed,operation.failed"></div><button type="submit">${t('save')}</button><p id="hook-secret"></p></form>`;$('#main').append(card);document.querySelectorAll('[data-hook-delete]').forEach(b=>b.onclick=busy(async()=>{awaitapi('/api/v1/webhooks/'+b.dataset.hookDelete,{method:'DELETE'});awaitnavigate();}));$('#hook-form').onsubmit=busy(asynce=>{e.preventDefault();constr=awaitapi('/api/v1/webhooks',{method:'POST',body:{callback_url:$('#hook-url').value,event_types:$('#hook-events').value.split(',').map(x=>x.trim()).filter(Boolean)}});$('#hook-secret').textContent=tr('签名密钥仅显示一次:','Signing key shown once: ')+r.signing_secret;});
constpanel=document.createElement('section');panel.className='card';panel.innerHTML=`<h3>${tr('订阅管理','Subscription management')}</h3><p class="hint">${tr('这与本地模板独立。请选择官方目录中的小组件,填写其参数后再确认。取消订阅不会自动恢复之前的照片。','This is separate from local templates. Choose a widget from the official catalog and fill its parameters before confirming. Cancelling does not automatically restore a previous photo.')}</p><div class="field"><label for="official-widget-choice">${tr('官方小组件','Official widget')}</label><select id="official-widget-choice">${data.catalog.map(w=>`<option value="${w.id}">${esc(w.name)}</option>`).join('')}</select></div><div id="official-widget-args"></div><div class="actions"><button id="official-widget-use" class="primary">${tr('确认使用','Apply widget')}</button><button id="official-widget-cancel">${tr('取消订阅','Cancel subscription')}</button></div>`;info.append(panel);constdisabled=!data.configured||!liveWriteAllowed();$('#official-widget-use').disabled=disabled;$('#official-widget-cancel').disabled=disabled;
constsend=cover_id=>confirmDialog(tr('此操作会改变官方订阅和画框内容,是否继续?','This changes the official subscription and frame content. Continue?'),async()=>{constargs=cover_id?Object.fromEntries([...document.querySelectorAll('[data-widget-arg]')].filter(x=>x.value).map(x=>[x.dataset.widgetArg,x.value])):{};constr=awaitapi('/api/v1/devices/'+state.device+'/widgets/selection',{method:'POST',body:{cover_id,args},headers:{'Idempotency-Key':'widget-'+clientUUID()}});awaitwaitOperation(r.operation_id);awaitnavigate('templates');});if(!disabled){$('#official-widget-use').onclick=busy(()=>send(Number($('#official-widget-choice').value)));$('#official-widget-cancel').onclick=busy(()=>send(null));}
card.innerHTML=`<h2>${tr('自动拼图','Automatic collage')}</h2><p class="hint">${tr('选择照片来源和数量,系统每次随机选取不同照片,并自动排版生成一张图片。无需手动拼接。预览不会上传或切换画框。','Choose a photo source and count. Each run randomly selects distinct photos and automatically creates one composition. Preview never uploads to or changes the frame.')}</p><form id="auto-collage-form"><div class="form-grid"><div class="field"><label for="collage-source">${tr('照片来源','Photo source')}</label><select id="collage-source"><option value="">${tr('本地手动上传照片','Locally uploaded photos')}</option>${sources.filter(x=>x.enabled).map(x=>`<option value="${esc(x.id)}">${esc(x.name)}</option>`).join('')}</select></div><div class="field"><label for="collage-count">${tr('拼图数量(1–12)','Photo count (1–12)')}</label><input id="collage-count" type="number" min="1" max="12" value="4" required></div></div><div class="field"><label for="collage-title">${tr('标题(可选)','Title (optional)')}</label><input id="collage-title" maxlength="100"></div><div class="field"><label for="collage-prompt">${tr('排版要求(可选)','Layout instructions (optional)')}</label><textarea id="collage-prompt" maxlength="1500"></textarea></div><label class="check-row"><input id="collage-ai" type="checkbox" checked>${tr('AI 自动排版','AI layout')}</label><p class="hint">${tr('AI 需先在连接中心配置,只接收尺寸与文字,不发送照片。未配置时可取消勾选,使用自动网格。','Configure AI in Connections first. Only dimensions and text are sent, never photos. Uncheck AI to use an automatic grid.')}</p><div class="actions"><button type="submit" class="primary">${tr('随机选图并生成预览','Pick photos and create preview')}</button><button id="collage-job" type="button">${tr('创建自动化任务','Create automation')}</button></div></form><div id="collage-preview"></div><div class="actions"><button id="collage-display" hidden>${t('display')}</button></div>`;
bind('collage-job',async()=>{if(!$('#auto-collage-form').reportValidity())return;awaitapi('/api/v1/jobs',{method:'POST',body:{device_id:state.device,name:$('#collage-title').value||tr('自动随机拼图','Automatic random collage'),enabled:false,schedule:{type:'daily',time:'08:00'},source:content(),display_duration:0,restore_previous:false,priority:50}});awaitnavigate('automation');toast(tr('任务已创建并暂停;编辑时间后启用。每次执行重新随机选图。','Created paused. Edit the schedule, then enable. Each run picks new photos.'));});
constmanual=document.createElement('details');manual.className='card manual-collage';manual.innerHTML=`<summary>${tr('高级:手动选图与调整布局','Advanced: manual photos and layout')}</summary>`;constcolumns=$('#main>.columns');columns.before(manual);manual.append(columns);