Browse the Directory by category, then subcategory, as Apple does (#118)
The Directory had one row of chips holding whatever each feed's category was, a category (Technology) or a subcategory (Tech News, Video Games) side by side: a podcast's own <itunes:category> is stored as its subcategory, and Jev's answers (#117) are often subcategories too, so after the first forced scan the row held 22 chips. /api/directory now gives each feed's Apple category and subcategory, worked out from Apple's list, and the page shows the categories, then a picked one's subcategories on a line of their own. A category that is not Apple's, one an admin typed, stands as a category of its own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -70,11 +70,12 @@ function listedFeed(p,cls){
|
||||
|
||||
// Directory's filters. Kept out here because a finished scan redraws the pane, which would
|
||||
// otherwise clear them.
|
||||
let dirKind='All', dirCat=null;
|
||||
let dirKind='All', dirCat=null, dirSub=null;
|
||||
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). Both filter in place, without asking the server again.
|
||||
/// 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{}
|
||||
@@ -89,18 +90,24 @@ async function renderDirectory(url,box){
|
||||
// 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>`:'');
|
||||
(cats.length?`<div class="chips" role="group" aria-label="Category">${cats.map(c=>btn('cat',c,c===dirCat)).join('')}</div>`:'')+
|
||||
(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':'cat', v=b.dataset[k];
|
||||
if(k==='kind') dirKind=v; else dirCat=dirCat===v?null:v;
|
||||
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();
|
||||
};
|
||||
box.innerHTML='';
|
||||
for(const p of ofKind.filter(p=>!dirCat||p.category===dirCat)) box.appendChild(listedFeed(p,'tile'));
|
||||
for(const p of ofCat.filter(p=>!dirSub||p.subcategory===dirSub)) box.appendChild(listedFeed(p,'tile'));
|
||||
};
|
||||
draw();
|
||||
return rows.length;
|
||||
@@ -307,7 +314,7 @@ function settingsModal(f, newUrl?: string){
|
||||
// Offer the categories the Directory already shows, so a blog about games joins Games rather
|
||||
// than starting a second chip beside it.
|
||||
if($('#scats')) api('/api/directory').then(rows=>{ $('#scats').innerHTML=[...new Set((rows||[])
|
||||
.map(p=>p.category).filter(Boolean))].sort().map(c=>`<option value="${esc(c)}">`).join(''); }).catch(()=>{});
|
||||
.flatMap(p=>[p.category,p.subcategory]).filter(Boolean))].sort().map(c=>`<option value="${esc(c)}">`).join(''); }).catch(()=>{});
|
||||
$('#ssave').onclick=async()=>{
|
||||
const max=$('#smax').value;
|
||||
try{
|
||||
|
||||
Reference in New Issue
Block a user