Give Currently Listening its own place, below Popular

It was a section at the bottom of the Popular page, so finding the
episode you were halfway through meant opening a list of feeds first.
It is a place in the feed list now, between Popular and All
Subscriptions, with its own page and count.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 16:49:11 +00:00
parent b8d22904f1
commit eaea520020
4 changed files with 23 additions and 14 deletions

View File

@@ -20,6 +20,8 @@ The long form, with what was wrong before and how it was found, is in
- Directory shows each feed as its cover art in a grid, title and subscriber count underneath,
instead of a list. Popular and the Add a feed dialog keep their rows.
- Currently Listening is its own place in the feed list, below Popular, instead of a section at
the bottom of the Popular page.
- The first scan after upgrading fetches every feed in full once, on its usual schedule, so each
picks up its category without waiting for the publisher to change something.

View File

@@ -88,6 +88,7 @@ const drive = [
['opmlModal', () => ctx.opmlModal()],
['selectFeed (directory)', () => ctx.selectFeed(':directory')],
['selectFeed (popular)', () => ctx.selectFeed(':popular')],
['selectFeed (currently listening)', () => ctx.selectFeed(':listening')],
['selectFeed (all subscriptions)', () => ctx.selectFeed(':all')],
['logsModal', () => ctx.logsModal()],
// `const S` is not reachable from here: top-level const/let do not become properties

View File

@@ -155,7 +155,7 @@ test('an item with several enclosures lists them all', async ({ page }) => {
await expect(page.locator('#files .encbox').nth(1).locator('.kind[title^="image"]')).toBeVisible();
});
test('Currently Listening, below Popular, resumes an episode you started', async ({ page }) => {
test('Currently Listening, its own place below Popular, resumes an episode you started', async ({ page }) => {
// Second Episode (900s) is 42 seconds in and unfinished. Which of Test Show's two episodes
// the daemon auto-downloaded is not fixed (see the three-panes test above), so an earlier
// test may have opened -- and so read -- this one already; reset it before relying on it.
@@ -164,7 +164,14 @@ test('Currently Listening, below Popular, resumes an episode you started', async
await page.evaluate(() =>
api('/api/entries/test-show/ui-2/position', { method: 'POST', body: JSON.stringify({ secs: 42 }) }));
// Popular lists feeds and nothing else; the episodes have a place of their own under it.
await page.locator('#feedlist .place', { hasText: 'Popular' }).click();
await expect(page.locator('#popular')).toBeVisible();
await expect(page.locator('#listening')).toHaveCount(0);
const places = await page.locator('#feedlist .place b').allTextContents();
expect(places.indexOf('Currently Listening')).toBe(places.indexOf('Popular') + 1);
await page.locator('#feedlist .place', { hasText: 'Currently Listening' }).click();
const row = page.locator('#listening .childrow', { hasText: 'Second Episode' });
await expect(row).toBeVisible({ timeout: 20_000 });
await expect(row).toContainText('0:42 of 15:00');
@@ -177,7 +184,7 @@ test('Currently Listening, below Popular, resumes an episode you started', async
// Finished (read) drops it from the list, however far it got.
await page.evaluate(() =>
api('/api/entries/test-show/ui-2/flags', { method: 'POST', body: JSON.stringify({ read: true }) }));
await page.locator('#feedlist .place', { hasText: 'Popular' }).click();
await page.locator('#feedlist .place', { hasText: 'Currently Listening' }).click();
await expect(page.locator('#listening')).not.toContainText('Second Episode', { timeout: 20_000 });
});

View File

@@ -855,6 +855,8 @@ const VIEWS={
blurb:'Every feed anyone on this server subscribes to, A to Z. The feeds inside an OPML are listed one by one, not the OPML.'},
':popular':{title:'Popular',icon:ICON.popular,url:'/api/popular',
blurb:'The ten feeds with the most subscribers here. The feeds inside an OPML count one by one, not the OPML.'},
':listening':{title:'Currently Listening',icon:ICON.audio,url:'/api/entries?filter=in_progress&limit=50',
blurb:'Episodes you started and have not finished, across every feed you subscribe to. Pick one up where you left off.'},
':all':{title:'All Subscriptions',icon:ICON.all},
};
function renderFeeds(){
@@ -1745,30 +1747,26 @@ async function renderListed(v){
$('#tbRemove').disabled=true;
syncTools(null);
$('#epSearch').placeholder='Search items…';
const listening=v===VIEWS[':popular'], grid=v===VIEWS[':directory'];
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} Everyone counts, you included. Private feeds are never listed.</div></div>
<div class="sub">${v.blurb}${listening?'':' Everyone counts, you included. Private feeds are never listed.'}</div></div>
</div>
${grid?'<div class="dirbar" id="dirbar"></div>':''}
<div class="${grid?'tiles':'childlist'}" id="popular"><p class="hint">Loading…</p></div>
${listening?`<div class="sub" style="margin:18px 0 8px;font-weight:600;color:var(--fg)">Currently Listening</div>
<div class="childlist" id="listening"><p class="hint">Loading…</p></div>`:''}`;
<div class="${grid?'tiles':'childlist'}" id="${listening?'listening':'popular'}"><p class="hint">Loading…</p></div>`;
$('#count').textContent=v.title;
const n=await (grid?renderDirectory:listFeeds)(v.url,$('#popular',box));
if(VIEWS[S.feed]===v) $('#count').textContent=`${v.title}: ${n} feed${n===1?'':'s'}`;
if(listening) renderListening();
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')}`;
}
/// Below Popular: episodes you started and have not finished, across every feed you
/// Currently Listening: episodes you started and have not finished, across every feed you
/// subscribe to. A row resumes the episode in the player bar on click -- a shortcut back to
/// where you left off, not another way to browse.
async function renderListening(){
const box=$('#listening');
async function renderListening(url,box){
let rows=[];
try{ rows=(await api('/api/entries?filter=in_progress&limit=10')).entries||[]; }catch{}
try{ rows=(await api(url)).entries||[]; }catch{}
box.innerHTML=rows.length?'':'<p class="hint">Nothing in progress. Episodes you start and do not finish show up here.</p>';
for(const e of rows){
const el=document.createElement('div');
@@ -1782,6 +1780,7 @@ async function renderListening(){
el.onclick=()=>play(e);
box.appendChild(el);
}
return rows.length;
}
// The toolbar acts on whatever is selected: the feed on the left, the item in the table.