/* ---------------- items ---------------- */ let loadSeq=0; async function loadEntries(append?: boolean){ // Directory and Popular list feeds, not items. if(!S.feed || VIEWS[S.feed]?.url) return; const p=new URLSearchParams({offset:String(S.offset),limit:String(LIMIT),filter:S.filter,sort:S.sort.col,dir:S.sort.dir}); if(S.q) p.set('q',S.q); const asked=performance.now(), seq=++loadSeq; const r=await api(S.feed===':all' ? `/api/entries?${p}` : `/api/feeds/${encodeURIComponent(S.feed)}/entries?${p}`); // Only the latest list counts: marking everything read reloads the All tab, and clicking // Unread straight after could have that answer land last and replace the Unread one. if(seq!==loadSeq) return; S.total=r.total; for(const e of r.entries){ const w=readWrites.get(readKey(e)); if(w && !(w.done!e.read||e.guid===S.sel); if(!append && S.sel && !entries.some(e=>e.guid===S.sel)){ const open=S.entries.find(e=>e.guid===S.sel); if(open) entries=[open,...entries]; } S.entries = entries; renderEntries(); } function renderEntries(){ const box=$('#eps'); if(!box) return; const f=S.feeds.find(x=>x.id===S.feed), v=VIEWS[S.feed]; const unread=f?f.unread:S.feeds.reduce((n,x)=>n+(x.unread||0),0); $('#count').textContent= `${f?(f.title||f.id):v?v.title:''}: ${S.total} item${S.total===1?'':'s'}, ${unread} unread`; const pane=$('#list'), top=pane?pane.scrollTop:0; box.innerHTML=''; if(!S.entries.length){ box.innerHTML=`

${S.q?'Nothing matches that search.':'Nothing here yet — try Scan now.'}

`; return; } for(const e of S.entries) box.appendChild(epEl(e)); syncPlayButtons(); if(pane) pane.scrollTop=top; if(S.entries.length < S.total){ const b=document.createElement('button'); b.className='btn'; b.id='more'; b.textContent=`Load more (${S.entries.length} of ${S.total})`; b.onclick=()=>{S.offset+=LIMIT;loadEntries(true)}; box.appendChild(b); } } function epEl(e){ // An item may carry several files. The row summarises the one you would act on -- // the playable one, else anything already downloaded, else the first -- and says how // many others there are; the pane below lists them all. const enc=e.enclosures.find(isPlayable) || e.enclosures.find(x=>x.path) || e.enclosures[0]; const has=!!(enc&&enc.path); const playable=isPlayable(enc); const others=e.enclosures.length-1; const el=document.createElement('div'); el.className='ep'+(e.read?' read':'')+(S.sel===e.guid?' sel':''); el.dataset.guid=e.guid; const num=[e.season?`S${e.season}`:'',e.episode?`E${e.episode}`:''].filter(Boolean).join(''); const left = e.position>10 && e.duration ? `${clock(e.duration-e.position)} left` : (e.duration?clock(e.duration):''); el.innerHTML=`
${esc(e.title||'(untitled)')}
${[ num&&`${num}`, left&&`${left}`, others>0&&`+${others} more file${others===1?'':'s'}`, enc&&enc.last_error&&enc.state==='error'&&`${esc(enc.last_error)}`, ].filter(Boolean).join('')}
${esc(feedName(e.feed_id))}
${enc?kindIcon(enc):''} ${enc&&!has?`
`:''}
${enc?mb(enc.length):''} ${dateOf(e.published)}
${playable?``: (enc&&!has?``:'')} ${has?``:''}
`; el.onclick=()=>selectEntry(e); $$('button[data-a]',el).forEach(b=>b.onclick=ev=>{ev.stopPropagation();epAction(b.dataset.a,e,el)}); return el; } /// Whether the browser can play it. Having a file is not the same as being playable: /// blog feeds put article images in enclosures, and an