From f89ca2aceb9983bdf7d48ae7ed94eb2a4cedec70 Mon Sep 17 00:00:00 2001 From: rays Date: Tue, 15 Sep 2026 17:54:17 +0000 Subject: [PATCH] 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 --- CHANGELOG.md | 2 ++ tests/ui/app.spec.js | 21 ++++++++++++++------- web/index.html | 17 +++++++++++++++-- 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88bfddd..1fad47a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ The long form, with what was wrong before and how it was found, is in ### 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 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, diff --git a/tests/ui/app.spec.js b/tests/ui/app.spec.js index ccf4358..a7aa2d1 100644 --- a/tests/ui/app.spec.js +++ b/tests/ui/app.spec.js @@ -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, 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, // 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. @@ -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).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 expect(page.locator('#player')).toBeVisible(); await expect(page.locator('#ptitle')).toHaveText('Second Episode'); 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 }) => { diff --git a/web/index.html b/web/index.html index a6b4eb7..75a6910 100644 --- a/web/index.html +++ b/web/index.html @@ -1870,13 +1870,26 @@ async function renderListening(url,box){ `
${esc(e.title||'(untitled)')}`+ `${esc(feedName(e.feed_id))} ยท ${clock(e.position)} of ${e.duration?clock(e.duration):'?'}`+ `
`+ - ``; - el.onclick=()=>play(e); + ``+ + ``; + el.onclick=ev=>ev.target.closest('[data-a=remove]')?forget(e):play(e); box.appendChild(el); } 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. $('#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); };