${
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'}`:''}
`;
}
/// The open feed's counts, last check and error, from a row that came after its page was drawn:
/// the page kept an error a later check had cleared (#316). Not the whole page, which would close
/// the open item.
function patchFeedHead(f: Feed){
const meta=document.querySelector('#content .fhead .meta'); if(!meta) return;
meta.querySelector('.fail')?.remove();
const stat=meta.querySelector('.stat'); if(!stat) return;
// An OPML's header has a stat line of its own.
if(!S.feeds.some(c=>c.group===f.id)) stat.outerHTML=statHTML(f);
meta.querySelector('.stat')!.insertAdjacentHTML('afterend',failBannerHTML(f));
wireFailBanner(meta,f);
}
function renderFeed(){
paintScanning(); // another feed's page may be the one open now
const box=$('#content');
box.classList.remove('plain');
const v=VIEWS[S.feed ?? ''];
if(v&&v.url) return renderListed(v);
if(S.feed===SIGNALS) return renderSignals();
const f=v?null:S.feeds.find(x=>x.id===S.feed);
const sg=signalOf(S.feed);
$('#tbRemove').disabled=!f;
syncTools(null);
if(!v&&!f&&!sg){ box.innerHTML='
Add a feed to get started.
'; $('#count').textContent=''; return; }
const name=f?(f.title||f.id):sg?sg.name:v.title;
// The box says only Search: a list's name ran past a phone's narrow box (#417). What it
// searches is in its name for a screen reader.
$('#epSearch').setAttribute('aria-label',`Search ${name}`);
const kids=f?S.feeds.filter(c=>c.group===f.id):[];
const unreadAll=unreadInAll();
box.innerHTML = (f&&kids.length ? groupHead(f,kids) : f ? `
`;
const pane=document.createElement('div');
pane.id='split';
// One feed's list leaves out the feed column; an OPML's shows are told apart by it.
if(f&&!kids.length) 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):sg?signalAction(b.dataset.a!,sg):allAction(b.dataset.a!));
$$('#content .tabs button').forEach(b=>b.onpointerenter=b.ontouchstart=()=>{ if(S.feed) prefetch(S.feed,b.dataset.f!); });
$$('#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 '
';
}
function sortBy(col: string){
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 items of the shows inside it together, as All
/// Subscriptions does every feed's (#283), under the header a feed has, its buttons in the
/// same places: 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: {url: string}|null)=>/patreon\.com\//.test(f&&f.url||'');
function groupHead(f: Feed,kids: Feed[]){
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;
return `
${gone} show${gone===1?' is':'s are'} no longer
listed but kept because ${gone===1?'it has':'they have'} downloads.
`:''}
`;
}
/// Keep, for a show from an OPML or a Patreon creator: it stays yours, a show of its own, should
/// the list drop it or you leave the list (#280). A plus to keep it, a check once kept.
function keepHTML(f: Feed){
if(!f.managed||!f.group) return '';
const list=S.feeds.find(p=>p.id===f.group);
const name=esc(list?(list.title||list.id):f.group);
const title=f.kept?`Kept: stays on your list if ${name} drops it. Stop keeping it`:`Keep it on your list if ${name} drops it`;
return ``;
}
async function feedAction(a: string,f: Feed){
if(a==='scan'){
spinStart(f.id);
try{ await api('/api/fetch',{method:'POST',body:JSON.stringify({feed:f.id,force:true})}); }
catch(e){ spinEnd(); throw e; }
}
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(why(e),true); }
}
if(a==='keep'){
try{
await api(`/api/feeds/${encodeURIComponent(f.id)}`,{method:'PATCH',body:JSON.stringify({kept:!f.kept})});
await loadFeeds(true); renderFeed();
}catch(e){ toast(why(e),true); }
}
if(a==='settings') settingsModal(f);
if(a==='share') share(f.title||f.id, f.url, $('#content .acts [data-a="share"]'));
if(a==='dl') downloadLatestModal(f);
}
/// All Subscriptions' own buttons: a feed's, across every feed you read.
async function allAction(a: string){
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(why(e),true); }
}
}