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); };