|
|
|
|
@@ -34,114 +34,217 @@ $('#addFeed').onclick=()=>{
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// What everyone here reads, you included, as a place to start. The rows carry an id, never a
|
|
|
|
|
// URL, so a key in someone's feed address never reaches this page.
|
|
|
|
|
// The rows carry an id, never a URL, so a key in someone's feed address never reaches this page.
|
|
|
|
|
const NONE_LISTED='<p class="hint">Nothing yet. Feeds people here subscribe to show up here.</p>';
|
|
|
|
|
async function listFeeds(url,box){
|
|
|
|
|
let rows=[];
|
|
|
|
|
try{ rows=await api(url)||[]; }catch{}
|
|
|
|
|
box.innerHTML=rows.length?'':NONE_LISTED;
|
|
|
|
|
for(const p of rows) box.appendChild(listedFeed(p,'childrow'));
|
|
|
|
|
return rows.length;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// One listed feed: a row in Popular and the Add a feed dialog, a tile in Directory's grid. The
|
|
|
|
|
/// parts are the same either way; the class lays them out.
|
|
|
|
|
function listedFeed(p,cls){
|
|
|
|
|
/// One listed feed: a tile in the Directory's grid or, given its place, a row in its chart. The
|
|
|
|
|
/// parts are the same either way; the class lays them out. Yours opens its feed; any other, its
|
|
|
|
|
/// page in the Directory, to see what it is before you take it (#128).
|
|
|
|
|
function listedFeed(p,cls,rank?: number){
|
|
|
|
|
const el=document.createElement('div');
|
|
|
|
|
el.className=cls;
|
|
|
|
|
el.innerHTML=artHTML(p.image,p.title||p.id)+
|
|
|
|
|
`<div class="txt"><b>${esc(p.title||p.id)}</b>`+
|
|
|
|
|
`<small class="meta">${p.subscribers} subscriber${p.subscribers===1?'':'s'}</small></div>`+
|
|
|
|
|
el.className=cls+(p.subscribed?' mine':'');
|
|
|
|
|
el.tabIndex=0; el.dataset.id=p.id;
|
|
|
|
|
// As the feed list's rows: Enter or Space opens it, and stops there, or Space would also
|
|
|
|
|
// play or pause.
|
|
|
|
|
el.onkeydown=ev=>{
|
|
|
|
|
if((ev.key==='Enter'||ev.key===' ')&&ev.target===el){ ev.preventDefault(); ev.stopPropagation(); el.click(); }
|
|
|
|
|
};
|
|
|
|
|
// A chart says what a show is about, as Apple's does; the grid says who here reads it, when
|
|
|
|
|
// anyone does. "0 subscribers" under most of the grid said nothing.
|
|
|
|
|
const meta=rank?(p.subcategory||p.category):p.subscribers?plural(p.subscribers,'subscriber'):'';
|
|
|
|
|
el.innerHTML=(rank?`<span class="no">${rank}</span>`:'')+artHTML(p.image,NAME(p))+
|
|
|
|
|
`<div class="txt"><b>${esc(NAME(p))}</b>${meta?`<small class="meta">${esc(meta)}</small>`:''}</div>`+
|
|
|
|
|
// Green, as a downloaded file is: it is already yours. Plus, beside it, is the way to get one.
|
|
|
|
|
(p.subscribed?`<span class="subbed" title="Subscribed: click to open it" aria-label="Subscribed">${ICON.subbed}</span>`
|
|
|
|
|
:`<button class="btn ico" data-a="sub" title="Subscribe" aria-label="Subscribe">${ICON.plus}</button>`);
|
|
|
|
|
// Yours already: the row opens it instead.
|
|
|
|
|
if(p.subscribed){ el.onclick=()=>{ closeModal(); selectFeed(p.id); }; return el; }
|
|
|
|
|
$('[data-a="sub"]',el).onclick=async()=>{
|
|
|
|
|
try{
|
|
|
|
|
await api(`/api/popular/${encodeURIComponent(p.id)}`,{method:'POST'});
|
|
|
|
|
closeModal(); toast(`Subscribed to ${p.title||p.id}`);
|
|
|
|
|
await loadFeeds(true); selectFeed(p.id);
|
|
|
|
|
}catch(e){ toast(e.message,true); }
|
|
|
|
|
};
|
|
|
|
|
if(p.subscribed){ el.onclick=()=>selectFeed(p.id); return el; }
|
|
|
|
|
el.onclick=()=>{ dirShow=p.id; $('#content').scrollTop=0; drawDirectory(undefined,'.dback'); };
|
|
|
|
|
// Its own click, not the tile's as well.
|
|
|
|
|
$('[data-a="sub"]',el).onclick=ev=>{ ev.stopPropagation(); subscribeListed(p); };
|
|
|
|
|
return el;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Directory's filters. Kept out here because a finished scan redraws the pane, which would
|
|
|
|
|
// otherwise clear them.
|
|
|
|
|
let dirKind='All', dirCat=null, dirSub=null, dirSort='az';
|
|
|
|
|
/// The Directory's orders. It opens A to Z, as the server sends it.
|
|
|
|
|
const NAME=p=>(p.title||p.id);
|
|
|
|
|
const DIR_SORTS={
|
|
|
|
|
subscribers:['Most subscribers',(a,b)=>b.subscribers-a.subscribers||NAME(a).localeCompare(NAME(b),undefined,{sensitivity:'base',numeric:true})],
|
|
|
|
|
az:['Name, A to Z',(a,b)=>NAME(a).localeCompare(NAME(b),undefined,{sensitivity:'base',numeric:true})],
|
|
|
|
|
za:['Name, Z to A',(a,b)=>NAME(b).localeCompare(NAME(a),undefined,{sensitivity:'base',numeric:true})],
|
|
|
|
|
};
|
|
|
|
|
const KINDS={All:()=>true,Podcasts:p=>p.podcast,Blogs:p=>!p.podcast};
|
|
|
|
|
/// Directory: every listed feed as its cover art, under two filters that combine: what a feed is
|
|
|
|
|
/// (Podcasts, anything with audio or video, or Blogs, the rest) and what it is about (its iTunes
|
|
|
|
|
/// category, as chips, then once one is picked its subcategories, as Apple's directory does).
|
|
|
|
|
/// They filter in place, without asking the server again.
|
|
|
|
|
async function renderDirectory(url,box){
|
|
|
|
|
let rows=[];
|
|
|
|
|
try{ rows=await api(url)||[]; }catch{}
|
|
|
|
|
if(!rows.length){ box.innerHTML=NONE_LISTED; return 0; }
|
|
|
|
|
const bar=$('#dirbar');
|
|
|
|
|
// Only a filter when the server has both kinds.
|
|
|
|
|
const both=rows.some(KINDS.Podcasts)&&rows.some(KINDS.Blogs);
|
|
|
|
|
const btn=(k,v,on)=>`<button type="button" data-${k}="${esc(v)}" class="${on?'on':''}" aria-pressed="${on}">${esc(v)}</button>`;
|
|
|
|
|
const draw=()=>{
|
|
|
|
|
if(!both) dirKind='All';
|
|
|
|
|
const ofKind=rows.filter(KINDS[dirKind]);
|
|
|
|
|
// No empty chips: only the categories among the feeds the kind lets through.
|
|
|
|
|
const cats=[...new Set(ofKind.map(p=>p.category).filter(Boolean))].sort();
|
|
|
|
|
if(!cats.includes(dirCat)) dirCat=null;
|
|
|
|
|
const ofCat=ofKind.filter(p=>!dirCat||p.category===dirCat);
|
|
|
|
|
const subs=dirCat?[...new Set(ofCat.map(p=>p.subcategory).filter(Boolean))].sort():[];
|
|
|
|
|
if(!subs.includes(dirSub)) dirSub=null;
|
|
|
|
|
bar.innerHTML=
|
|
|
|
|
(both?`<div class="tabs" role="group" aria-label="Kind">${Object.keys(KINDS).map(k=>btn('kind',k,k===dirKind)).join('')}</div>`:'')+
|
|
|
|
|
(cats.length?`<div class="chips" role="group" aria-label="Category">${cats.map(c=>btn('cat',c,c===dirCat)).join('')}</div>`:'')+
|
|
|
|
|
`<select id="dirsort" aria-label="Sort">${Object.entries(DIR_SORTS).map(([k,[label]])=>
|
|
|
|
|
`<option value="${k}"${k===dirSort?' selected':''}>${label}</option>`).join('')}</select>`+
|
|
|
|
|
(subs.length?`<div class="chips subs" role="group" aria-label="Subcategory">${subs.map(c=>btn('sub',c,c===dirSub)).join('')}</div>`:'');
|
|
|
|
|
// A picked chip lifts on a second press. Everything is redrawn, so the keyboard goes back to
|
|
|
|
|
// the button just pressed.
|
|
|
|
|
for(const b of $$('button',bar)) b.onclick=()=>{
|
|
|
|
|
const k=b.dataset.kind!=null?'kind':b.dataset.cat!=null?'cat':'sub', v=b.dataset[k];
|
|
|
|
|
if(k==='kind') dirKind=v;
|
|
|
|
|
else if(k==='cat'){ dirCat=dirCat===v?null:v; dirSub=null; }
|
|
|
|
|
else dirSub=dirSub===v?null:v;
|
|
|
|
|
draw(); $(`[data-${k}="${CSS.escape(v)}"]`,bar)?.focus();
|
|
|
|
|
};
|
|
|
|
|
$('#dirsort',bar).onchange=e=>{ dirSort=e.target.value; draw(); $('#dirsort',bar).focus(); };
|
|
|
|
|
box.innerHTML='';
|
|
|
|
|
for(const p of ofCat.filter(p=>!dirSub||p.subcategory===dirSub).sort(DIR_SORTS[dirSort][1])) box.appendChild(listedFeed(p,'tile'));
|
|
|
|
|
};
|
|
|
|
|
draw();
|
|
|
|
|
return rows.length;
|
|
|
|
|
async function subscribeListed(p){
|
|
|
|
|
try{
|
|
|
|
|
await api(`/api/popular/${encodeURIComponent(p.id)}`,{method:'POST'});
|
|
|
|
|
toast(`Subscribed to ${NAME(p)}`);
|
|
|
|
|
await loadFeeds(true); selectFeed(p.id);
|
|
|
|
|
}catch(e){ toast(e.message,true); }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Directory and Popular open in the main pane, as the original's Directory did.
|
|
|
|
|
// Where you are in the Directory: what kind of feed, which category and subcategory, in what
|
|
|
|
|
// order. Out here because a search redraws the page, and the kind and order hold across visits.
|
|
|
|
|
let dirKind='All', dirCat=null, dirSub=null, dirSort='az', dirRows=[];
|
|
|
|
|
// The feed whose page is open over all that, by id; going back leaves the rest as it was.
|
|
|
|
|
let dirShow=null;
|
|
|
|
|
// The two pages that are not a category: every feed, and the feeds that name none. A category is
|
|
|
|
|
// Apple's or one an admin typed, neither of which starts with a colon.
|
|
|
|
|
const EVERY=':every', OTHER=':other';
|
|
|
|
|
const NAME=p=>(p.title||p.id);
|
|
|
|
|
const byName=(a,b)=>NAME(a).localeCompare(NAME(b),undefined,{sensitivity:'base',numeric:true});
|
|
|
|
|
const bySubs=(a,b)=>b.subscribers-a.subscribers||byName(a,b);
|
|
|
|
|
/// The grid's orders. It opens A to Z, as the server sends it.
|
|
|
|
|
const DIR_SORTS={
|
|
|
|
|
subscribers:['Most subscribers',bySubs],
|
|
|
|
|
az:['Name, A to Z',byName],
|
|
|
|
|
za:['Name, Z to A',(a,b)=>byName(b,a)],
|
|
|
|
|
};
|
|
|
|
|
const KINDS={All:()=>true,Podcasts:p=>p.podcast,Blogs:p=>!p.podcast};
|
|
|
|
|
const KIND_WORD={All:'feed',Podcasts:'podcast',Blogs:'blog'};
|
|
|
|
|
|
|
|
|
|
/// The Directory, as Apple's is laid out: it opens on the most subscribed feeds and the
|
|
|
|
|
/// categories, and a category has a page of its own, its subcategories along the top of its
|
|
|
|
|
/// grid. What a feed is (Podcasts, anything with audio or video, or Blogs, the rest) holds across
|
|
|
|
|
/// every page, and the search box looks through the lot. One fetch draws all of it.
|
|
|
|
|
async function renderDirectory(url,box){
|
|
|
|
|
// Each visit starts at the front, as going back to a tab does.
|
|
|
|
|
dirCat=null; dirSub=null; dirShow=null;
|
|
|
|
|
try{ dirRows=await api(url)||[]; }catch{ dirRows=[]; }
|
|
|
|
|
drawDirectory(box);
|
|
|
|
|
return dirRows.length;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// `focus` is the control to put the keyboard back on, since the whole page is redrawn.
|
|
|
|
|
function drawDirectory(box=$('#directory'), focus?: string){
|
|
|
|
|
if(!box) return;
|
|
|
|
|
if(!dirRows.length){ box.innerHTML=NONE_LISTED; return; }
|
|
|
|
|
// Only a choice when the server has both kinds.
|
|
|
|
|
const both=dirRows.some(KINDS.Podcasts)&&dirRows.some(KINDS.Blogs);
|
|
|
|
|
if(!both) dirKind='All';
|
|
|
|
|
const rows=dirRows.filter(KINDS[dirKind]), word=KIND_WORD[dirKind];
|
|
|
|
|
const count=n=>`${n.toLocaleString()} ${word}${n===1?'':'s'}`;
|
|
|
|
|
const of=c=>p=>c===EVERY||(c===OTHER?!p.category:p.category===c);
|
|
|
|
|
// A kind can leave a category with nothing in it; that goes back to the front.
|
|
|
|
|
if(dirCat&&!rows.some(of(dirCat))) dirCat=null;
|
|
|
|
|
const catName=c=>c===EVERY?`All ${word}s`:c===OTHER?'Other':c;
|
|
|
|
|
const top=list=>list.filter(p=>p.subscribers).sort(bySubs).slice(0,10);
|
|
|
|
|
const btn=(k,v,label,on)=>`<button type="button" data-${k}="${esc(v)}" aria-pressed="${on}"${on?' class="on"':''}>${label}</button>`;
|
|
|
|
|
// back: where its button goes, '' for the front; none on the front itself.
|
|
|
|
|
const head=(title,sub,back?)=>`<div class="dhead">${back==null?'':
|
|
|
|
|
`<button type="button" class="dback" data-go="${esc(back)}">${ICON.left}${esc(back?catName(back):'Directory')}</button>`}
|
|
|
|
|
<div class="dtitle"><h2>${esc(title)}</h2>${both?`<div class="tabs" role="group" aria-label="Kind">${
|
|
|
|
|
Object.keys(KINDS).map(k=>btn('kind',k,k,k===dirKind)).join('')}</div>`:''}</div>
|
|
|
|
|
<p class="sub">${esc(sub)}</p></div>`;
|
|
|
|
|
const sec=(title,body,extra='')=>`<section class="dsec">${title?`<div class="dsech"><h3>${esc(title)}</h3>${extra}</div>`:''}${body}</section>`;
|
|
|
|
|
const tools=(pills='')=>`<div class="dtools">${pills||'<span class="grow"></span>'}<select id="dirsort" aria-label="Sort">${
|
|
|
|
|
Object.entries(DIR_SORTS).map(([k,[label]])=>`<option value="${k}"${k===dirSort?' selected':''}>${label}</option>`).join('')}</select></div>`;
|
|
|
|
|
const q=S.q.trim(), grid='<div class="tiles" id="dtiles"></div>', board='<div class="chart" id="dchart"></div>';
|
|
|
|
|
let html, chart=[], tiles=null;
|
|
|
|
|
const show=dirShow&&dirRows.find(p=>p.id===dirShow);
|
|
|
|
|
if(show){
|
|
|
|
|
const n=show.subscribers;
|
|
|
|
|
html=`<div class="dhead"><button type="button" class="dback" data-back>${ICON.left}${esc(q?'Results':dirCat?catName(dirCat):'Directory')}</button></div>
|
|
|
|
|
<div class="show">${artHTML(show.image,NAME(show))}<div class="meta"><h2>${esc(NAME(show))}</h2>${
|
|
|
|
|
// Its category's page is one click away from here.
|
|
|
|
|
show.category?`<button type="button" class="dlink" data-go="${esc(show.category)}">${esc(show.subcategory||show.category)}</button>`:''}
|
|
|
|
|
<p class="sub">${n?`${n} ${n===1?'person here subscribes':'people here subscribe'}`:'Nobody here subscribes yet'}</p>
|
|
|
|
|
<div class="acts"><button class="btn ico primary" data-a="sub" title="Subscribe" aria-label="Subscribe">${ICON.plus}</button></div></div></div>`
|
|
|
|
|
+sec(show.podcast?'Latest episodes':'Latest posts','<div class="latest" id="dshow"><p class="hint">Loading…</p></div>');
|
|
|
|
|
}else if(q){
|
|
|
|
|
// Names only: a category is a tap away, and "news" matching every News feed buried the
|
|
|
|
|
// one called that.
|
|
|
|
|
tiles=rows.filter(p=>NAME(p).toLocaleLowerCase().includes(q.toLocaleLowerCase()));
|
|
|
|
|
html=head(`Results for “${q}”`,tiles.length?count(tiles.length):`No ${word} here has that in its name.`,dirCat||'')+tools()+grid;
|
|
|
|
|
}else if(!dirCat){
|
|
|
|
|
chart=top(rows);
|
|
|
|
|
const cats=new Map<string,any[]>();
|
|
|
|
|
for(const p of rows){ const c=p.category||OTHER; if(!cats.has(c)) cats.set(c,[]); cats.get(c).push(p); }
|
|
|
|
|
// Other, the feeds with no category, goes last.
|
|
|
|
|
const order=[...cats.keys()].sort((a,b)=>Number(a===OTHER)-Number(b===OTHER)||a.localeCompare(b));
|
|
|
|
|
html=head('Directory',`${count(rows.length)} on this server. Private feeds are never listed.`)
|
|
|
|
|
+(chart.length?sec('Most subscribed',board):'')
|
|
|
|
|
+sec('Categories',`<div class="cats">${order.map(c=>catTile(c,catName(c),count(cats.get(c).length),cats.get(c))).join('')}</div>`,
|
|
|
|
|
`<button type="button" class="dall" data-go="${EVERY}">See all ${rows.length.toLocaleString()}</button>`);
|
|
|
|
|
}else{
|
|
|
|
|
const inCat=rows.filter(of(dirCat));
|
|
|
|
|
const subs=dirCat===EVERY?[]:[...new Set(inCat.map(p=>p.subcategory).filter(Boolean))].sort();
|
|
|
|
|
if(!subs.includes(dirSub)) dirSub=null;
|
|
|
|
|
if(dirCat!==EVERY) chart=top(inCat);
|
|
|
|
|
tiles=inCat.filter(p=>!dirSub||p.subcategory===dirSub);
|
|
|
|
|
const pill=(v,label,n)=>btn('sub',v,`${esc(label)}<small>${n}</small>`,(dirSub||'')===v);
|
|
|
|
|
html=head(catName(dirCat),dirCat===OTHER?`${count(inCat.length)} that name no category`:count(inCat.length),'')
|
|
|
|
|
+(chart.length?sec('Most subscribed',board):'')
|
|
|
|
|
// Its own heading only when the chart is above it, to say where one list ends.
|
|
|
|
|
+sec(chart.length?`${word[0].toUpperCase()+word.slice(1)}s`:'',tools(subs.length?`<div class="pills" role="group" aria-label="Subcategory">${
|
|
|
|
|
pill('','All',inCat.length)+subs.map(s=>pill(s,s,inCat.filter(p=>p.subcategory===s).length)).join('')}</div>`:'')+grid);
|
|
|
|
|
}
|
|
|
|
|
box.innerHTML=html;
|
|
|
|
|
if(show){ $('.show [data-a="sub"]',box).onclick=()=>subscribeListed(show); fillShow($('#dshow',box),show); }
|
|
|
|
|
const c=$('#dchart',box);
|
|
|
|
|
if(c){ c.style.setProperty('--rows',String(Math.ceil(chart.length/2))); chart.forEach((p,i)=>c.appendChild(listedFeed(p,'rank',i+1))); }
|
|
|
|
|
const t=$('#dtiles',box);
|
|
|
|
|
if(t) for(const p of tiles.sort(DIR_SORTS[dirSort][1])) t.appendChild(listedFeed(p,'tile'));
|
|
|
|
|
box.onclick=ev=>{
|
|
|
|
|
const b=(ev.target as Element).closest<HTMLElement>('[data-go],[data-kind],[data-sub],[data-back]');
|
|
|
|
|
if(!b) return;
|
|
|
|
|
// Back from a feed's page, onto the tile or row it was opened from.
|
|
|
|
|
if(b.dataset.back!=null){ const id=dirShow; dirShow=null; return drawDirectory(box,`[data-id="${CSS.escape(id)}"]`); }
|
|
|
|
|
if(b.dataset.kind!=null){ dirKind=b.dataset.kind; return drawDirectory(box,`[data-kind="${CSS.escape(dirKind)}"]`); }
|
|
|
|
|
if(b.dataset.sub!=null){ dirSub=b.dataset.sub||null; return drawDirectory(box,`[data-sub="${CSS.escape(b.dataset.sub)}"]`); }
|
|
|
|
|
// Going anywhere ends a search.
|
|
|
|
|
if(S.q){ S.q=''; $('#epSearch').value=''; }
|
|
|
|
|
const from=dirCat;
|
|
|
|
|
dirCat=b.dataset.go||null; dirSub=null; dirShow=null;
|
|
|
|
|
$('#content').scrollTop=0;
|
|
|
|
|
// Into a page, onto its way back; back out, onto the tile you went in by.
|
|
|
|
|
drawDirectory(box,dirCat?'.dback':from?`[data-go="${CSS.escape(from)}"]:not(.dback)`:undefined);
|
|
|
|
|
};
|
|
|
|
|
const s=$('#dirsort',box);
|
|
|
|
|
if(s) s.onchange=()=>{ dirSort=s.value; drawDirectory(box,'#dirsort'); };
|
|
|
|
|
if(focus) $(focus,box)?.focus();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A listed feed's latest items, on its page: each one's title, opening its post on the
|
|
|
|
|
/// publisher's site where it has one, its first lines, and when and how long.
|
|
|
|
|
async function fillShow(list,p){
|
|
|
|
|
let items;
|
|
|
|
|
try{ items=await api(`/api/directory/${encodeURIComponent(p.id)}`); }
|
|
|
|
|
catch(e){ list.innerHTML=`<p class="hint">${esc(e.message)}</p>`; return; }
|
|
|
|
|
// Gone back, or on to another, while it loaded.
|
|
|
|
|
if(dirShow!==p.id||!list.isConnected) return;
|
|
|
|
|
list.innerHTML=items.length?items.map(e=>{
|
|
|
|
|
const name=entryName(e), text=name.derived?'':plainText(e.description).slice(0,500);
|
|
|
|
|
const when=[dateOf(e.published),e.duration&&clock(e.duration)].filter(Boolean);
|
|
|
|
|
return `<article class="lep"><b>${e.link?`<a href="${esc(e.link)}" target="_blank" rel="noopener">${esc(name.text)}</a>`:esc(name.text)}</b>`
|
|
|
|
|
+(text?`<p>${esc(text)}</p>`:'')
|
|
|
|
|
+(when.length?`<small>${when.map(s=>`<span>${s}</span>`).join('<span class="dot"></span>')}</small>`:'')+'</article>';
|
|
|
|
|
}).join(''):`<p class="hint">Nothing read from it yet. A feed nobody here subscribes to is checked once a day.</p>`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A category as a tile: its name, how many feeds, and the covers of its three most subscribed
|
|
|
|
|
/// fanned in the corner, so each one is coloured by what is in it, as Apple's are.
|
|
|
|
|
function catTile(c,name,count,rows){
|
|
|
|
|
// Covers before initials, which are all Other had; the likeliest first, and first is in front:
|
|
|
|
|
// the fan is drawn back to front.
|
|
|
|
|
const art=[...rows].sort((a,b)=>Number(!a.image)-Number(!b.image)||bySubs(a,b)).slice(0,3).reverse();
|
|
|
|
|
return `<button type="button" class="cat" data-go="${esc(c)}" style="--tint:${tint(c)}"><b>${esc(name)}</b><small>${count}</small>`+
|
|
|
|
|
`<span class="fan" aria-hidden="true">${art.map(p=>artHTML(p.image,NAME(p))).join('')}</span></button>`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The Directory and Currently Listening open in the main pane, as the original's Directory did.
|
|
|
|
|
async function renderListed(v){
|
|
|
|
|
const box=$('#content');
|
|
|
|
|
box.classList.add('plain');
|
|
|
|
|
$('#tbRemove').disabled=true;
|
|
|
|
|
syncTools(null);
|
|
|
|
|
if(v===VIEWS[':directory']){
|
|
|
|
|
$('#epSearch').placeholder='Find a feed…';
|
|
|
|
|
box.innerHTML='<div id="directory"><p class="hint">Loading…</p></div>';
|
|
|
|
|
$('#count').textContent=v.title;
|
|
|
|
|
const n=await renderDirectory(v.url,$('#directory'));
|
|
|
|
|
if(VIEWS[S.feed]===v) $('#count').textContent=`${v.title}: ${plural(n,'feed')}`;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
$('#epSearch').placeholder='Search items…';
|
|
|
|
|
const listening=v===VIEWS[':listening'], grid=v===VIEWS[':directory'];
|
|
|
|
|
box.innerHTML=`
|
|
|
|
|
<div class="fhead slim">
|
|
|
|
|
<div class="art">${v.icon}</div>
|
|
|
|
|
<div class="meta"><h2>${v.title}</h2>
|
|
|
|
|
<div class="sub">${v.blurb}${listening?'':' Everyone counts, you included. Private feeds are never listed.'}</div></div>
|
|
|
|
|
<div class="meta"><h2>${v.title}</h2><div class="sub">${v.blurb}</div></div>
|
|
|
|
|
</div>
|
|
|
|
|
${grid?'<div class="dirbar" id="dirbar"></div>':''}
|
|
|
|
|
<div class="${grid?'tiles':'childlist'}" id="${listening?'listening':'popular'}"><p class="hint">Loading…</p></div>`;
|
|
|
|
|
<div class="childlist" id="listening"><p class="hint">Loading…</p></div>`;
|
|
|
|
|
$('#count').textContent=v.title;
|
|
|
|
|
const n=await (listening?renderListening:grid?renderDirectory:listFeeds)(v.url,$(listening?'#listening':'#popular',box));
|
|
|
|
|
if(VIEWS[S.feed]===v) $('#count').textContent=`${v.title}: ${plural(n,listening?'episode':'feed')}`;
|
|
|
|
|
const n=await renderListening(v.url,$('#listening',box));
|
|
|
|
|
if(VIEWS[S.feed]===v) $('#count').textContent=`${v.title}: ${plural(n,'episode')}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Currently Listening: episodes you started and have not finished, across every feed you
|
|
|
|
|
@@ -210,7 +313,9 @@ $('#tbRead').onclick=()=>{ const e=cur(); if(e) epAction('read',e,null); };
|
|
|
|
|
$('#tbFlag').onclick=()=>{ const e=cur(); if(e) epAction('flag',e,null); };
|
|
|
|
|
let searchT;
|
|
|
|
|
$('#epSearch').oninput=ev=>{ clearTimeout(searchT);
|
|
|
|
|
searchT=setTimeout(()=>{ S.q=ev.target.value; S.offset=0; loadEntries(); },250); };
|
|
|
|
|
// The Directory looks through what it already has; the rest ask the server (#126).
|
|
|
|
|
searchT=setTimeout(()=>{ S.q=ev.target.value; S.offset=0;
|
|
|
|
|
if(S.feed===':directory'){ dirShow=null; drawDirectory(); } else loadEntries(); },250); };
|
|
|
|
|
// Crossing the phone breakpoint moves the files between their pane and the text.
|
|
|
|
|
window.matchMedia?.('(max-width:820px)')?.addEventListener?.('change',()=>{ const e=cur(); if(e) showDetail(e); });
|
|
|
|
|
|
|
|
|
|
|