/* ---------------- feed page ---------------- */ function renderFeed(){ const box=$('#content'); box.classList.remove('plain'); const v=VIEWS[S.feed]; if(v&&v.url) return renderListed(v); const f=v?null:S.feeds.find(x=>x.id===S.feed); $('#tbRemove').disabled=!f; syncTools(null); if(!v&&!f){ box.innerHTML='

Add a feed to get started.

'; $('#count').textContent=''; return; } const name=f?(f.title||f.id):v.title; $('#epSearch').placeholder=`Search ${name}…`; const kids=f?S.feeds.filter(c=>c.group===f.id):[]; if(kids.length){ box.classList.add('plain'); renderGroup(f,kids); return; } const unreadAll=S.feeds.reduce((n,x)=>n+(x.unread||0),0); box.innerHTML = (f ? `
${artHTML(f.image,name)}

${esc(name)}

${ plural(f.entries,'item')}, ${f.downloaded} downloaded · checked ${ago(f.last_checked)}${ f.subscribers>1?` · shared with ${f.subscribers-1} other ${f.subscribers===2?'person':'people'}`:''}
${failBannerHTML(f)} ${f.orphaned?`
This feed is no longer listed in its OPML subscription. It was kept rather than removed because it has downloaded items.
`:''} ${f.group?`
From the OPML subscription ${esc(f.group)}
`:''}
` : `
${v.icon}

${v.title}

Every item from the ${S.feeds.length} feed${S.feeds.length===1?'':'s'} you subscribe to, newest first · ${unreadAll} unread
`) + `
${[['all','All'],['unread','Unread'],['downloaded','Downloaded'],['flagged','Pinned']].map(([t,label])=> ``).join('')}
`; const pane=document.createElement('div'); pane.id='split'; if(f) pane.className='one'; pane.innerHTML='
'+sortHead()+ '
'; $$('.ephead [data-sort]',pane).forEach(b=>b.onclick=()=>sortBy(b.dataset.sort)); box.appendChild(pane); pane.style.setProperty('--listh', localStorage.getItem('ipx.listh') || '60%'); dragSplit(pane); // Nothing is open any more, so nothing is kept on the Unread tab for being open. S.sel=null; showDetail(null); $$('#content .acts .btn').forEach(b=>b.onclick=()=>f?feedAction(b.dataset.a,f):allAction(b.dataset.a)); $$('#content .tabs button').forEach(b=>b.onclick=()=>{ S.filter=b.dataset.f; S.offset=0; try{ localStorage.setItem('ipx.filter',S.filter); }catch{} renderFeed(); loadEntries(); }); if(f) wireFailBanner(box,f); } /// The item table's headings, each a button that sorts by its column. The first click goes the /// natural way round (A to Z; newest, largest and kept first) and the next one reverses it. const COLS=[['kept','Pinned',ICON.pin],['title','Title'],['feed','Feed'],['type','File'],['size','Size'],['published','Published']]; function sortHead(){ return '
'+COLS.map(([k,label,icon])=>{ const on=S.sort.col===k; return ``; }).join('')+'
'; } function sortBy(col){ const first=['published','size','kept'].includes(col)?'desc':'asc'; S.sort={col,dir:S.sort.col===col?(S.sort.dir==='asc'?'desc':'asc'):first}; try{ localStorage.setItem('ipx.sort',JSON.stringify(S.sort)); }catch{} S.offset=0; renderFeed(); loadEntries(); } /// An OPML subscription's page lists the feeds inside it rather than items, but keeps /// every action a normal feed has -- it is still an ordinary feed entry underneath. // A Patreon creator split into its shows is drawn like an OPML, and named for what it is. const isPatreon=f=>/patreon\.com\//.test(f&&f.url||''); function renderGroup(f,kids){ const unread=kids.reduce((n,c)=>n+c.unread,0); const saved=kids.reduce((n,c)=>n+c.downloaded,0); const gone=kids.filter(c=>c.orphaned).length; $('#count').textContent=`${f.title||f.id}: ${kids.length} feed${kids.length===1?'':'s'}, ${unread} unread`; // The same header as a feed's, buttons in the same places: it is a feed underneath. $('#content').innerHTML = `
${folderArt(f,kids)}

${esc(f.title||f.id)}

${isPatreon(f)?'Patreon creator':'OPML subscription'} · ${plural(kids.length,'feed')}, ${unread} unread, ${saved} downloaded · checked ${ago(f.last_checked)}
${failBannerHTML(f)} ${gone?`
${gone} feed${gone===1?' is':'s are'} no longer listed but kept because ${gone===1?'it has':'they have'} downloads.
`:''}
${esc(f.url)}
`; $$('#content .acts .btn').forEach(b=>b.onclick=()=>feedAction(b.dataset.a,f)); wireFailBanner($('#content'),f); const draw=()=>{ const q=($('#kidSearch').value||'').trim().toLowerCase(); const box=$('#kidlist'); box.innerHTML=''; const rows=kids.filter(c=>!q||(c.title||c.id).toLowerCase().includes(q)).sort(unreadFirst); if(!rows.length){ box.innerHTML='

Nothing matches.

'; return; } for(const c of rows){ const el=document.createElement('div'); el.className='childrow'; el.innerHTML = artHTML(c.image,c.title||c.id)+ `
${esc(c.title||c.id)}`+ `${plural(c.entries,'item')} · ${c.downloaded} downloaded`+ (c.failing?` · error` :c.last_error?` · error`:'')+`
`+ (c.orphaned?'Gone':'')+ (c.failing?`Error`:'')+ `${c.unread}`; el.onclick=()=>selectFeed(c.id); box.appendChild(el); } }; $('#kidSearch').oninput=draw; draw(); } async function feedAction(a,f){ if(a==='scan'){ toast('Scanning '+(f.title||f.id)+'…'); await api('/api/fetch',{method:'POST',body:JSON.stringify({feed:f.id,force:true})}); } if(a==='read'){ const r=await api(`/api/feeds/${encodeURIComponent(f.id)}/read-all`,{method:'POST'}); toast(`Marked ${r.marked} read`); await loadFeeds(true); renderFeed(); loadEntries(); } if(a==='rm') removeFeed(f); if(a==='pin'){ try{ await api(`/api/feeds/${encodeURIComponent(f.id)}`,{method:'PATCH',body:JSON.stringify({pinned:!f.pinned})}); await loadFeeds(true); renderFeed(); }catch(e){ toast(e.message,true); } } if(a==='settings') settingsModal(f); if(a==='dl') downloadLatestModal(f); } /// All Subscriptions' own buttons: a feed's, across every feed you read. async function allAction(a){ if(a==='scanall') return scanAll(); if(a==='readall'){ const n=S.feeds.reduce((k,x)=>k+(x.unread||0),0); if(!n){ toast('Nothing unread'); return; } // One click across every feed is a lot to take back, so this one asks first. if(!confirm(`Mark all ${n} unread item${n===1?'':'s'} read, in every feed you subscribe to?`)) return; try{ const r=await api('/api/read-all',{method:'POST'}); toast(`Marked ${r.marked} read`); await loadFeeds(true); renderFeed(); loadEntries(); }catch(e){ toast(e.message,true); } } }