2026-10-02 09:19:25 +08:00
'use strict' ;
// Page features share the same authenticated API and display operation pipeline.
async function currentPanel ( container ){
const content = await api ( '/api/v1/devices/' + state . device + '/current-content' );
state . currentDisplay = content ;
const known = content . source_known && content . source ? . type === 'template' ;
container . innerHTML = `<h3> ${ t ( 'currentContent' ) } </h3><p># ${ esc ( content . image_id ?? '—' ) } · ${ t ( content . synced ? 'synced' : 'syncing' ) } </p><p> ${ known ? ` ${ t ( 'currentTemplate' ) } : <strong> ${ esc ( content . source . template ) } </strong>` : content . source_known ? t ( 'sourceKnown' ) : t ( 'sourceUnknown' ) } </p> ${ known ? buttons ( 'edit-current-template' , 'editCurrent' ) : '' }${ content . overrides . length ? `<div class="notice"><strong> ${ t ( 'temporaryDisplay' ) } </strong> ${ content . overrides . map ( x => `<p># ${ esc ( x . image_id ) } · ${ t ( 'priority' ) } : ${ x . priority } · ${ fmt ( x . expires_at ) } </p>` ). join ( '' ) } </div>` : '' } ` ;
bind ( 'edit-current-template' ,()=> editTemplateSource ( content . source ));
return content ;
}
function editTemplateSource ( source ){ state . selectedTemplate = source . template ; state . pendingTemplateData = structuredClone ( source ); return navigate ( 'templates' );}
async function enhanceDashboard (){
const epoch = state . epoch ; const frame = $ ( '.frame-demo' ); const panel = document . createElement ( 'section' ); panel . className = 'card' ; $ ( '#main' ). append ( panel );
const current = await currentPanel ( panel ); if ( epoch !== state . epoch ) return ;
if ( current . preview_url ){ frame . innerHTML = `<img id="current-frame-image" alt=" ${ t ( 'currentContent' ) } " style="width:100%;height:100%;object-fit:contain"><span class="frame-label"> ${ t ( 'currentContent' ) } # ${ current . image_id } </span>` ; await blobImage ( $ ( '#current-frame-image' ), current . preview_url );}
const weather = await api ( '/api/v1/providers/weather' ); if ( epoch !== state . epoch ) return ; const box = document . createElement ( 'section' ); box . className = 'card' ; box . innerHTML = `<h3> ${ t ( 'weather' ) } </h3><p> ${ esc ( weather . current . location ) } · ${ esc ( weather . current . temperature ) } · ${ esc ( weather . current . summary ) } </p><p class="hint"> ${ weather . current . mock ? t ( 'weatherMock' ) : t ( 'weatherAttribution' ) }${ weather . current . stale ? ' · ' + t ( 'stale' ) : '' } </p>` ; $ ( '#main' ). append ( box );
}
async function enhanceTemplates (){
const panel = document . createElement ( 'section' ); panel . className = 'card' ; $ ( '.template-grid' ). before ( panel ); await currentPanel ( panel ); if ( ! panel . isConnected ) return ;
2026-10-02 14:52:57 +08:00
const download = document . createElement ( 'button' ); download . id = 'download-template' ; download . textContent = t ( 'download' ); const previewActions = document . createElement ( 'div' ); previewActions . className = 'actions template-preview-actions' ; $ ( '#display-template' ). before ( previewActions ); previewActions . append ( $ ( '#display-template' ), download );
2026-10-02 09:19:25 +08:00
download . onclick = busy ( async ()=>{ if ( ! state . templatePreview ){ toast ( t ( 'previewHint' )); return ;} await downloadImage ( '/api/v1/assets/' + state . templatePreview , 'frame-flow-template.jpg' );});
2026-10-02 09:57:42 +08:00
await poetryControls ();
2026-10-02 14:52:57 +08:00
officialContentInfo ();
2026-10-02 09:19:25 +08:00
const photo = $ ( '#field-photo_id' ); if ( photo ){ const file = document . createElement ( 'input' ); file . type = 'file' ; file . id = 'template-photo-upload' ; file . accept = 'image/jpeg,image/png,image/webp' ; file . setAttribute ( 'aria-label' , t ( 'selectFile' )); photo . after ( file ); file . onchange = busy ( async ()=>{ if ( ! file . files [ 0 ]) return ; const body = new FormData (); body . append ( 'file' , file . files [ 0 ]); body . append ( 'fit' , 'cover' ); body . append ( 'profile' , 'color' ); const asset = await api ( '/api/v1/preview' ,{ method : 'POST' , body }); const accepted = await api ( '/api/v1/devices/' + state . device + '/upload' ,{ method : 'POST' , body : { type : 'image' , asset_id : asset . asset_id }, headers : { 'Idempotency-Key' : 'photo-' + clientUUID ()}}); await waitOperation ( accepted . operation_id ); const option = document . createElement ( 'option' ); option . value = asset . asset_id ; option . textContent = asset . name ; photo . append ( option ); photo . value = asset . asset_id ; photo . oninput (); toast ( t ( 'done' ));});}
}
async function downloadImage ( path , name ){ const blob = await api ( path ,{ binary : true }); const url = URL . createObjectURL ( blob ); const link = document . createElement ( 'a' ); link . href = url ; link . download = name ; document . body . append ( link ); link . click (); link . remove (); setTimeout (()=> URL . revokeObjectURL ( url ), 10000 );}
function displayDialog ( content ){
if ( ! liveWriteAllowed ()){ toast ( t ( 'readOnlyWarning' )); return ;}
showModal ( `<h2> ${ t ( 'displayOptions' ) } </h2><form id="display-options"><label class="check-row"><input id="override-restore" type="checkbox"> ${ t ( 'restoreAfter' ) } </label><div class="form-grid"><div class="field"><label for="override-duration"> ${ t ( 'duration' ) } </label><input id="override-duration" type="number" min="1" max="604800" value="1800"></div><div class="field"><label for="override-priority"> ${ t ( 'priority' ) } </label><input id="override-priority" type="number" min="0" max="100" value="50"></div></div><div class="modal-actions"> ${ buttons ( 'modal-cancel' , 'cancel' ) } <button id="confirm-display" type="submit" class="primary"> ${ t ( 'display' ) } </button></div></form>` );
$ ( '#display-options' ). onsubmit = busy ( async e =>{ e . preventDefault (); const restore = $ ( '#override-restore' ). checked ; const options = { duration : restore ? Number ( $ ( '#override-duration' ). value ) : 0 , restore_previous : restore , priority : Number ( $ ( '#override-priority' ). value )}; await accepted ( '/api/v1/display' ,{ device_id : state . device , content , options }); $ ( '#modal' ). close (); await navigate ( 'history' );});
}
function enhanceGallery (){
const grid = $ ( '.gallery' ); if ( ! grid ) return ;
const controls = document . createElement ( 'div' ); controls . className = 'form-grid' ; controls . innerHTML = `<div class="field"><label for="gallery-search"> ${ t ( 'searchImages' ) } </label><input id="gallery-search" type="search"></div><div class="field"><label for="gallery-sort"> ${ t ( 'mode' ) } </label><select id="gallery-sort"><option value="newest"> ${ t ( 'sortNewest' ) } </option><option value="oldest"> ${ t ( 'sortOldest' ) } </option><option value="name"> ${ t ( 'sortName' ) } </option></select></div>` ; grid . before ( controls );
const cards = [... grid . querySelectorAll ( '.image-card' )]; $ ( '#gallery-search' ). oninput = ()=>{ const query = $ ( '#gallery-search' ). value . toLowerCase (); cards . forEach ( card => card . hidden =! card . textContent . toLowerCase (). includes ( query ));}; $ ( '#gallery-sort' ). onchange = ()=>{ const order = $ ( '#gallery-sort' ). value ; const indexes = cards . map (( card , index )=>({ card , item : state . gallery [ index ]})); indexes . sort (( a , b )=> order === 'name' ? a . item . name . localeCompare ( b . item . name ) : order === 'oldest' ? String ( a . item . created_at ). localeCompare ( String ( b . item . created_at )) : String ( b . item . created_at ). localeCompare ( String ( a . item . created_at ))); indexes . forEach ( x => grid . append ( x . card ));};
cards . forEach (( card , i )=>{ const item = state . gallery [ i ]; if ( item . width && item . height ){ const dimensions = document . createElement ( 'small' ); dimensions . textContent = item . width + ' × ' + item . height ; card . querySelector ( 'h3' ). after ( dimensions );} const download = document . createElement ( 'button' ); download . textContent = t ( 'download' ); card . querySelector ( '.actions' ). append ( download ); download . onclick = busy (()=> downloadImage ( item . url , item . name )); if ( state . tab === 'local' ){ const edit = document . createElement ( 'button' ); edit . textContent = t ( 'continueEdit' ); card . querySelector ( '.actions' ). append ( edit ); edit . onclick = busy ( async ()=>{ const meta = await api ( '/api/v1/assets/' + item . asset_id + '/source' ); if ( meta . source ? . type === 'template' ) await editTemplateSource ( meta . source ); else toast ( t ( 'sourceUnknown' ));});}});
}
async function weatherSettings (){
const weather = await api ( '/api/v1/providers/weather' ); const box = document . createElement ( 'section' ); box . className = 'card' ; box . innerHTML = `<h3> ${ t ( 'weatherSettings' ) } </h3><form id="weather-form"><div class="field"><label for="weather-provider">Provider</label><select id="weather-provider"><option value="mock"> ${ t ( 'weatherMock' ) } </option><option value="open-meteo">Open-Meteo</option></select></div><div class="field"><label for="weather-location"> ${ t ( 'location' ) } </label><input id="weather-location" required maxlength="120" value=" ${ esc ( weather . config . location ) } "></div><div class="form-grid"> ${ [ 'latitude' , 'longitude' ]. map ( k => `<div class="field"><label for="weather- ${ k } "> ${ t ( k ) } </label><input id="weather- ${ k } " required type="number" step="any" min=" ${ k === 'latitude' ?- 90 :- 180 } " max=" ${ k === 'latitude' ? 90 : 180 } " value=" ${ weather . config [ k ] } "></div>` ). join ( '' ) } </div><button type="submit" class="primary"> ${ t ( 'save' ) } </button></form><div id="weather-result"></div>` ; $ ( '#main' ). append ( box ); $ ( '#weather-provider' ). value = weather . config . provider ;
function result ( w ){ $ ( '#weather-result' ). innerHTML = `<p> ${ esc ( w . current . temperature ) } · ${ esc ( w . current . summary ) } </p><p class="hint"> ${ w . current . mock ? t ( 'weatherMock' ) : t ( 'weatherAttribution' ) }${ w . current . stale ? ' · ' + t ( 'stale' ) : '' } </p><h4> ${ t ( 'forecast' ) } </h4> ${ w . forecast . map ( x => `<p> ${ esc ( x . date ) } · ${ esc ( x . summary ) } · ${ x . minimum_c } – ${ x . maximum_c } °C · ${ x . rain_probability } %</p>` ). join ( '' ) } ` ;}
result ( weather ); $ ( '#weather-form' ). onsubmit = busy ( async e =>{ e . preventDefault (); await api ( '/api/v1/settings/weather' ,{ method : 'PUT' , body : { provider : $ ( '#weather-provider' ). value , location : $ ( '#weather-location' ). value , latitude : Number ( $ ( '#weather-latitude' ). value ), longitude : Number ( $ ( '#weather-longitude' ). value )}}); result ( await api ( '/api/v1/providers/weather' )); toast ( t ( 'done' ));});
}
async function enhanceHistory (){
const rows = [... document . querySelectorAll ( 'tbody tr' )]; const ops = await api ( '/api/v1/operations' ); rows . forEach (( row , i )=>{ const op = ops [ i ]; if ( ! op ) return ; const button = document . createElement ( 'button' ); button . textContent = t ( 'details' ); row . lastElementChild . append ( button ); button . onclick = busy ( async ()=>{ const current = await api ( '/api/v1/operations/' + op . operation_id ); showModal ( `<h2> ${ t ( 'details' ) } </h2><p> ${ t ( current . status ) } · ${ esc ( current . action ) } </p><p> ${ esc ( current . error ? . message || '' ) } </p><pre style="white-space:pre-wrap;overflow-wrap:anywhere"> ${ esc ( JSON . stringify ({ operation_id : current . operation_id , request_id : current . request_id , created_at : current . created_at , updated_at : current . updated_at , result : current . result } ,null,2))}</pre> ${ current . result ? . url ? '<img id="operation-preview" class="preview" alt="Preview">' : '' } <div class="modal-actions"> ${ buttons ( 'modal-cancel' , 'cancel' ) } </div>` ); if ( current . result ? . url ) await blobImage ( $ ( '#operation-preview' ), current . result . url );});});}
async function enhanceAutomation (){
const jobs = await api ( '/api/v1/jobs' ); const rows = [... document . querySelectorAll ( 'tbody tr' )]; rows . forEach (( row , i )=>{ const job = jobs [ i ]; if ( ! job ) return ; const actions = row . querySelector ( '.row-actions' ); const run = document . createElement ( 'button' ); run . textContent = t ( 'runNow' ); run . disabled =! liveWriteAllowed (); run . title = run . disabled ? t ( 'readOnlyWarning' ) : '' ; actions . append ( run ); run . onclick = ()=> confirmDialog ( t ( 'runConfirm' ), async ()=>{ await accepted ( '/api/v1/jobs/' + job . id + '/run' ,{}); await navigate ( 'history' );}); if ( job . source . type === 'template' ){ const preview = document . createElement ( 'button' ); preview . textContent = t ( 'jobPreview' ); actions . append ( preview ); preview . onclick = busy ( async ()=>{ const id = await accepted ( '/api/v1/jobs/' + job . id + '/preview' ,{}); const op = await waitOperation ( id ); showModal ( `<h2> ${ t ( 'jobPreview' ) } </h2><img id="job-preview-image" class="preview" alt="Preview"><div class="modal-actions"> ${ buttons ( 'modal-cancel' , 'cancel' ) } </div>` ); await blobImage ( $ ( '#job-preview-image' ), op . result . url );});}});
}
async function jobSourceEditor ( job ){
const raw = $ ( '#job-source' ); const advanced = document . createElement ( 'details' ); advanced . innerHTML = `<summary> ${ t ( 'advanced' ) } </summary>` ; raw . parentElement . before ( advanced ); advanced . append ( raw . parentElement );
const box = document . createElement ( 'div' ); advanced . before ( box ); let source = structuredClone ( job . source ); let images = [], assets = []; try {[ images , assets ] = await Promise . all ([ api ( '/api/v1/devices/' + state . device + '/images' ), api ( '/api/v1/assets' )]);} catch ( e ){ toast ( e . message );} if ( ! raw . isConnected ) return ;
box . innerHTML = `<div class="field"><label for="job-content-type"> ${ t ( 'sourceType' ) } </label><select id="job-content-type"> ${ [ 'template' , 'image' , 'image_url' , 'restore' ]. map ( k => `<option value=" ${ k } "> ${ k === 'template' ? t ( 'template' ) : k === 'image' ? t ( 'imageChoice' ) : k === 'restore' ? t ( 'restoreSource' ) : t ( 'imageURL' ) } </option>` ). join ( '' ) } </select></div><div id="job-content-fields"></div>` ;
$ ( '#job-content-type' ). value = source . type ; const old = $ ( '#job-template' ) ? . parentElement ; if ( old ) old . remove ();
function update (){ raw . value = JSON . stringify ( source , null , 2 );}
function fields (){ const type = $ ( '#job-content-type' ). value ; const panel = $ ( '#job-content-fields' ); if ( type === 'restore' ){ source = { type }; panel . innerHTML = '' ; $ ( '#job-restore' ). checked = false ; $ ( '#job-duration' ). value = 0 ; update (); return ;}
if ( type === 'template' ){ const selected = state . templates . find ( tpl => tpl . id === source . template ) || state . templates [ 0 ]; source = { type , template : selected . id , data : source . type === 'template' ? source . data || {} : {}, profile : source . profile || 'color' }; panel . innerHTML = `<div class="field"><label for="job-template"> ${ t ( 'template' ) } </label><select id="job-template"> ${ state . templates . map ( x => `<option value=" ${ x . id } " ${ selected . id === x . id ? 'selected' : '' } > ${ esc ( x . name ) } </option>` ). join ( '' ) } </select></div><div class="field"><label for="job-source-profile"> ${ t ( 'profile' ) } </label><select id="job-source-profile"><option value="color"> ${ t ( 'color' ) } </option><option value="grayscale"> ${ t ( 'grayscale' ) } </option></select></div><div id="job-data-fields"></div>` ; $ ( '#job-source-profile' ). value = source . profile ; $ ( '#job-source-profile' ). onchange = ()=>{ source . profile = $ ( '#job-source-profile' ). value ; update ();}; const defaults = {... selected . defaults ,... source . data }; const keys = [... new Set ([... Object . keys ( defaults ). filter ( k => typeof defaults [ k ] !== 'object' || Array . isArray ( defaults [ k ])),...([ 'calendar' , 'daily-dashboard' ]. includes ( selected . id ) ? [ 'events' ] : [])])]; $ ( '#job-data-fields' ). innerHTML = keys . map ( k => `<div class="field"><label for="job-field- ${ k } "> ${ esc ({ title : state . lang === 'zh' ? '标题' : 'Title' , message : state . lang === 'zh' ? '正文' : 'Message' , quote : state . lang === 'zh' ? '一句话' : 'Quote' , author : state . lang === 'zh' ? '作者' : 'Author' , events : t ( 'eventsHelp' ) } [k]||k)}</label><textarea id="job-field- ${ k } " data-job-field=" ${ k } " rows="2"> ${ esc ( Array . isArray ( defaults [ k ]) ? defaults [ k ]. map ( x => typeof x === 'object' ? x . title : x ). join ( '\n' ) : defaults [ k ] || '' ) } </textarea></div>` ). join ( '' ); $ ( '#job-template' ). onchange = ()=>{ source = { type : 'template' , template : $ ( '#job-template' ). value , data : {}}; fields ();}; panel . querySelectorAll ( '[data-job-field]' ). forEach ( el => el . oninput = ()=>{ source . data [ el . dataset . jobField ] = el . dataset . jobField === 'events' ? el . value . split ( '\n' ). filter ( x => x . trim ()) : el . value ; update ();}); update ();}
else if ( type === 'image' ){ const selected = source . image_id ? 'cloud:' + source . image_id : source . asset_id ? 'local:' + source . asset_id : '' ; panel . innerHTML = `<div class="field"><label for="job-image"> ${ t ( 'imageChoice' ) } </label><select id="job-image" required><option value="">—</option> ${ images . map ( i => `<option value="cloud: ${ i . image_id } "> ${ esc ( i . name ) } · # ${ i . image_id } </option>` ). join ( '' ) }${ assets . map ( a => `<option value="local: ${ a . asset_id } "> ${ esc ( a . name ) } · LOCAL</option>` ). join ( '' ) } </select></div>` ; $ ( '#job-image' ). value = selected ; $ ( '#job-image' ). onchange = ()=>{ const [ kind , id ] = $ ( '#job-image' ). value . split ( ':' ); source = kind === 'cloud' ? { type : 'image' , image_id : Number ( id )} : { type : 'image' , asset_id : id }; update ();};}
else { panel . innerHTML = `<div class="field"><label for="job-url"> ${ t ( 'imageURL' ) } </label><input id="job-url" type="url" placeholder="https://…" value=" ${ esc ( source . type === 'image_url' ? source . url : '' ) } " required></div>` ; $ ( '#job-url' ). oninput = ()=>{ source = { type : 'image_url' , url : $ ( '#job-url' ). value }; update ();};}}
$ ( '#job-content-type' ). onchange = ()=>{ source = { type : $ ( '#job-content-type' ). value }; update (); fields ();}; raw . oninput = ()=>{ try { source = JSON . parse ( raw . value ); raw . setCustomValidity ( '' ); $ ( '#job-content-type' ). value = source . type ; fields ();} catch { raw . setCustomValidity ( t ( 'INVALID_REQUEST' ));}}; fields ();
}
async function playlistImagePicker ( playlist ){
const raw = $ ( '#playlist-images' ); const box = document . createElement ( 'div' ); raw . parentElement . after ( box ); box . innerHTML = `<p> ${ t ( 'imageChoice' ) } </p><div id="playlist-image-choices"> ${ t ( 'loading' ) } </div>` ; const images = await api ( '/api/v1/devices/' + state . device + '/images' ); if ( ! box . isConnected ) return ; const chosen = new Set ( playlist . image_ids );
$ ( '#playlist-image-choices' ). innerHTML = images . map ( image => `<label class="check-row"><input type="checkbox" data-playlist-image=" ${ image . image_id } " ${ chosen . has ( image . image_id ) ? 'checked' : '' } > ${ esc ( image . name ) } · # ${ image . image_id }${ image . current ? ' · ' + t ( 'current' ) : '' } </label>` ). join ( '' ); box . querySelectorAll ( '[data-playlist-image]' ). forEach ( el => el . onchange = ()=>{ const id = Number ( el . dataset . playlistImage ); if ( el . checked ) chosen . add ( id ); else chosen . delete ( id ); raw . value = [... chosen ]. join ( ',' );}); raw . oninput = ()=>{ chosen . clear (); raw . value . split ( ',' ). map ( Number ). filter ( x => x > 0 ). forEach ( x => chosen . add ( x )); box . querySelectorAll ( '[data-playlist-image]' ). forEach ( el => el . checked = chosen . has ( Number ( el . dataset . playlistImage )));};
}
async function enhanceSettings (){ await weatherSettings (); const config = await api ( '/api/v1/settings/image-url' ); const box = document . createElement ( 'section' ); box . className = 'card' ; box . innerHTML = `<h3> ${ t ( 'urlSettings' ) } </h3><form id="image-host-form"><div class="field"><label for="image-hosts"> ${ t ( 'allowedHosts' ) } </label><textarea id="image-hosts" rows="3"> ${ esc ( config . allowed_hosts . join ( '\n' )) } </textarea><p class="hint"> ${ t ( 'urlHint' ) } </p></div><button type="submit"> ${ t ( 'save' ) } </button></form>` ; $ ( '#main' ). append ( box ); $ ( '#image-host-form' ). onsubmit = busy ( async e =>{ e . preventDefault (); await api ( '/api/v1/settings/image-url' ,{ method : 'PUT' , body : { allowed_hosts : $ ( '#image-hosts' ). value . split ( '\n' ). map ( x => x . trim ()). filter ( Boolean )}}); toast ( t ( 'done' ));});}
function enhanceUpload (){ const box = document . createElement ( 'div' ); box . innerHTML = `<h3> ${ t ( 'uploadURL' ) } </h3><div class="field"><label for="upload-url"> ${ t ( 'imageURL' ) } </label><input id="upload-url" type="url" placeholder="https://…"><p class="hint"> ${ t ( 'urlHint' ) } </p></div> ${ buttons ( 'preview-url' , 'uploadURL' ) } ` ; $ ( '#make-preview' ). parentElement . append ( box ); bind ( 'preview-url' , async ()=>{ if ( ! $ ( '#upload-url' ). checkValidity ()) return ; const id = await accepted ( '/api/v1/devices/' + state . device + '/upload' ,{ type : 'image_url' , url : $ ( '#upload-url' ). value , fit : $ ( '#fit' ). value , profile : $ ( '#profile' ). value }); const op = await waitOperation ( id ); state . preview = { asset_id : op . result . asset_id , url : op . result . url }; $ ( '#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 ();});}
2026-10-02 09:57:42 +08:00
async function poetryControls (){
if ( state . selectedTemplate !== 'daily-poetry' ) return ;
const provider = await api ( '/api/v1/providers/poetry' ); if ( ! $ ( '#template-fields' )) return ;
const box = document . createElement ( 'div' ); box . className = 'poetry-controls' ; box . innerHTML = `<div class="notice"> ${ t ( 'poetryNotice' ) } </div><div class="field"><label for="poem-choice"> ${ t ( 'poemChoice' ) } </label><select id="poem-choice"><option value=""> ${ t ( 'dailyAutomatic' ) } </option> ${ provider . catalog . map ( p => `<option value=" ${ p . id } "> ${ esc ( p . title ) } · ${ esc ( p . author ) } </option>` ). join ( '' ) } </select></div><p id="poem-description" class="hint"></p> ${ buttons ( 'daily-poetry-job' , 'dailyPoetryJob' ) } ` ; $ ( '#template-fields' ). before ( box );
const values = JSON . parse ( $ ( '#template-data' ). value ); $ ( '#poem-choice' ). value = values . poem_id || '' ;
function description (){ const selected = provider . catalog . find ( p => p . id === $ ( '#poem-choice' ). value ) || provider . current ; $ ( '#poem-description' ). textContent = selected . title + ' · ' + selected . author + ' · ' + t ( 'poetryCatalog' ) + provider . catalog . length ; const raw = $ ( '#field-poem_id' ); if ( raw ) raw . parentElement . hidden = true ;}
description (); $ ( '#poem-choice' ). onchange = ()=>{ const data = JSON . parse ( $ ( '#template-data' ). value ); delete data . poem_title ; delete data . poem_author ; delete data . poem_lines ; delete data . poem_source ; delete data . poem_edition ; if ( $ ( '#poem-choice' ). value ) data . poem_id = $ ( '#poem-choice' ). value ; else { delete data . poem_id ; delete data . date ;} $ ( '#template-data' ). value = JSON . stringify ( data , null , 2 ); $ ( '#template-data' ). oninput (); description ();};
bind ( 'daily-poetry-job' ,()=>{ showModal ( `<h2> ${ t ( 'dailyPoetryJob' ) } </h2><form id="poetry-job-form"><div class="field"><label for="poetry-job-time"> ${ t ( 'time' ) } </label><input id="poetry-job-time" type="time" required value="07:00"></div><label class="check-row"><input id="poetry-job-enabled" type="checkbox"> ${ t ( 'enabled' ) } </label><p class="hint"> ${ t ( 'poetryScheduleHint' ) } </p><div class="modal-actions"> ${ buttons ( 'modal-cancel' , 'cancel' ) } <button type="submit" class="primary"> ${ t ( 'save' ) } </button></div></form>` ); $ ( '#poetry-job-form' ). onsubmit = busy ( async e =>{ e . preventDefault (); await api ( '/api/v1/jobs' ,{ method : 'POST' , body : { device_id : state . device , name : t ( 'dailyPoetryJob' ), enabled : $ ( '#poetry-job-enabled' ). checked , schedule : { type : 'daily' , time : $ ( '#poetry-job-time' ). value }, source : { type : 'template' , template : 'daily-poetry' , data : {}, profile : $ ( '#template-profile' ). value }, display_duration : 0 , restore_previous : false , priority : 50 }}); $ ( '#modal' ). close (); await navigate ( 'automation' );});});
}
2026-10-02 14:52:57 +08:00
2026-10-02 16:20:54 +08:00
async function officialContentInfo (){
2026-10-02 14:52:57 +08:00
const info = document . createElement ( 'details' ); info . className = 'official-content-info' ;
2026-10-02 16:20:54 +08:00
const zh = state . lang === 'zh' ;
info . innerHTML = `<summary> ${ zh ? '官方小组件' : 'Official widgets' } </summary><p> ${ zh ? '正在读取…' : 'Loading…' } </p>` ;
2026-10-02 14:52:57 +08:00
$ ( '.template-grid' ). before ( info );
2026-10-02 16:20:54 +08:00
try {
const data = await api ( '/api/v1/devices/' + state . device + '/widgets' );
const label = data . source === 'live' ? ( zh ? '实时目录' : 'Live catalog' ) : data . source === 'mock' ? 'Mock' : ( zh ? '历史目录 · ' + data . observed_at : 'Catalog snapshot · ' + data . observed_at );
info . innerHTML = `<summary> ${ zh ? '官方小组件' : 'Official widgets' } · ${ data . catalog . length } </summary><p> ${ esc ( label ) } </p><p class="notice"> ${ data . current_verified ? esc (( zh ? '当前小组件:' : 'Current widget: ' ) + data . current . name ) : ( zh ? '当前小组件尚未核实。官方微信会话与设备 API Key 是不同凭据。' : 'Current widget is unverified. The official session differs from the device API Key.' ) } </p><p> ${ zh ? '此入口只读,不会切换或取消官方订阅。' : 'Read only. This does not change official subscriptions.' } </p><input type="search" aria-label=" ${ zh ? '搜索小组件' : 'Search widgets' } " placeholder=" ${ zh ? '搜索名称或分类' : 'Search name or category' } "><div class="widget-list"></div>` ;
const render = ()=>{ const query = info . querySelector ( 'input' ). value . toLowerCase (); info . querySelector ( '.widget-list' ). innerHTML = data . catalog . filter ( x =>( x . name + ' ' + x . tags . join ( ' ' )). toLowerCase (). includes ( query )). map ( x => `<article class="card"><strong> ${ esc ( x . name ) } </strong><p> ${ esc ( x . tags . join ( ' · ' )) } </p><small> ${ esc ( x . arg_keys . join ( ' · ' )) } </small></article>` ). join ( '' );};
info . querySelector ( 'input' ). oninput = render ; render ();
} catch ( e ){ info . querySelector ( 'p' ). textContent = e . message ;}
2026-10-02 14:52:57 +08:00
}