Currently Listening: a cross takes an episode off the list

It forgets the saved position, which is what puts an episode on the
list. The one in the player is closed without saving first, or its
next save would put it straight back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 17:54:17 +00:00
parent 57a6fb5daa
commit f89ca2aceb
3 changed files with 31 additions and 9 deletions

View File

@@ -12,6 +12,8 @@ The long form, with what was wrong before and how it was found, is in
### Added ### Added
- Each episode in Currently Listening has a cross that takes it off the list. It forgets where you
got to, so playing it again starts from the beginning.
- An admin can give a feed a Directory category in its settings (`category` in config.toml), for - An admin can give a feed a Directory category in its settings (`category` in config.toml), for
the blogs and other feeds that name none of their own. A feed's own iTunes category still wins. the blogs and other feeds that name none of their own. A feed's own iTunes category still wins.
- Keyboard shortcuts after Feedly's: j and k through items, Shift-J and Shift-K through feeds, - Keyboard shortcuts after Feedly's: j and k through items, Shift-J and Shift-K through feeds,

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(); await expect(page.locator('#files .encbox').nth(1).locator('.kind[title^="image"]')).toBeVisible();
}); });
test('Currently Listening, its own place below Popular, resumes an episode you started', async ({ page }) => { test('Currently Listening, its own place below Popular, resumes an episode you started or forgets it', async ({ page }) => {
// Second Episode (900s) is 42 seconds in and unfinished. An earlier test may have opened it, // Second Episode (900s) is 42 seconds in and unfinished. An earlier test may have opened it,
// and opening marks it read; it is listed all the same, because read is not finished. This // and opening marks it read; it is listed all the same, because read is not finished. This
// test used to set it unread first, which hid exactly the bug in issue #14. // test used to set it unread first, which hid exactly the bug in issue #14.
@@ -176,16 +176,23 @@ test('Currently Listening, its own place below Popular, resumes an episode you s
await expect(row).toBeVisible({ timeout: 20_000 }); await expect(row).toBeVisible({ timeout: 20_000 });
await expect(row).toContainText('0:42 of 15:00'); await expect(row).toContainText('0:42 of 15:00');
// Removing it forgets where you got to, so it is still gone on the next visit.
await row.locator('[data-a=remove]').click();
await expect(row).toHaveCount(0);
await expect(page.locator('#player')).not.toBeVisible();
await page.locator('#feedlist .place', { hasText: 'Currently Listening' }).click();
await expect(page.locator('#listening')).not.toContainText('Second Episode', { timeout: 20_000 });
// Started again, it is back, and clicking the row resumes it. Finishing it (90%) is
// covered in the Rust tests; here the player's own save on close would race it.
await page.evaluate(() =>
api('/api/entries/test-show/ui-2/position', { method: 'POST', body: JSON.stringify({ secs: 42 }) }));
await page.locator('#feedlist .place', { hasText: 'Currently Listening' }).click();
await expect(row).toBeVisible({ timeout: 20_000 });
await row.click(); await row.click();
await expect(page.locator('#player')).toBeVisible(); await expect(page.locator('#player')).toBeVisible();
await expect(page.locator('#ptitle')).toHaveText('Second Episode'); await expect(page.locator('#ptitle')).toHaveText('Second Episode');
await page.locator('#pclose').click(); await page.locator('#pclose').click();
// Finished, 90% of the way or more, drops it from the list.
await page.evaluate(() =>
api('/api/entries/test-show/ui-2/position', { method: 'POST', body: JSON.stringify({ secs: 850 }) }));
await page.locator('#feedlist .place', { hasText: 'Currently Listening' }).click();
await expect(page.locator('#listening')).not.toContainText('Second Episode', { timeout: 20_000 });
}); });
test('the filter tabs change what is listed', async ({ page }) => { test('the filter tabs change what is listed', async ({ page }) => {

View File

@@ -1870,13 +1870,26 @@ async function renderListening(url,box){
`<div class="txt"><b>${esc(e.title||'(untitled)')}</b>`+ `<div class="txt"><b>${esc(e.title||'(untitled)')}</b>`+
`<small class="meta">${esc(feedName(e.feed_id))} · ${clock(e.position)} of ${e.duration?clock(e.duration):'?'}</small>`+ `<small class="meta">${esc(feedName(e.feed_id))} · ${clock(e.position)} of ${e.duration?clock(e.duration):'?'}</small>`+
`<div class="dlbar live"><i style="width:${pct}%"></i></div></div>`+ `<div class="dlbar live"><i style="width:${pct}%"></i></div></div>`+
`<button class="btn ico primary" title="Resume" aria-label="Resume">${ICON.play}</button>`; `<button class="btn ico primary" title="Resume" aria-label="Resume">${ICON.play}</button>`+
el.onclick=()=>play(e); `<button class="btn ico" data-a="remove" title="Remove from Currently Listening" aria-label="Remove from Currently Listening">${ICON.close}</button>`;
el.onclick=ev=>ev.target.closest('[data-a=remove]')?forget(e):play(e);
box.appendChild(el); box.appendChild(el);
} }
return rows.length; return rows.length;
} }
/// Takes an episode off Currently Listening by forgetting where you got to: the list is every
/// episode with a saved position short of the end, so the position is what has to go.
async function forget(e){
// Closed without saving first, or the player's next save would put it straight back.
if(player.guid===e.guid){ player.guid=null; $('#pclose').click(); }
try{
await api(`/api/entries/${encodeURIComponent(e.feed_id)}/${encodeURIComponent(e.guid)}/position`,
{method:'POST',body:JSON.stringify({secs:0})});
}catch(err){ toast(err.message,true); }
if(S.feed===':listening') renderListed(VIEWS[':listening']);
}
// The toolbar acts on whatever is selected: the feed on the left, the item in the table. // The toolbar acts on whatever is selected: the feed on the left, the item in the table.
$('#tbRemove').onclick=()=>{ const f=S.feeds.find(x=>x.id===S.feed); if(f) removeFeed(f); }; $('#tbRemove').onclick=()=>{ const f=S.feeds.find(x=>x.id===S.feed); if(f) removeFeed(f); };
$('#tbPlay').onclick=()=>{ const e=cur(); if(e) play(e); }; $('#tbPlay').onclick=()=>{ const e=cur(); if(e) play(e); };