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:
@@ -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,
|
- 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.
|
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
|
- 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.
|
picks up its category without waiting for the publisher to change something.
|
||||||
|
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ const drive = [
|
|||||||
['opmlModal', () => ctx.opmlModal()],
|
['opmlModal', () => ctx.opmlModal()],
|
||||||
['selectFeed (directory)', () => ctx.selectFeed(':directory')],
|
['selectFeed (directory)', () => ctx.selectFeed(':directory')],
|
||||||
['selectFeed (popular)', () => ctx.selectFeed(':popular')],
|
['selectFeed (popular)', () => ctx.selectFeed(':popular')],
|
||||||
|
['selectFeed (currently listening)', () => ctx.selectFeed(':listening')],
|
||||||
['selectFeed (all subscriptions)', () => ctx.selectFeed(':all')],
|
['selectFeed (all subscriptions)', () => ctx.selectFeed(':all')],
|
||||||
['logsModal', () => ctx.logsModal()],
|
['logsModal', () => ctx.logsModal()],
|
||||||
// `const S` is not reachable from here: top-level const/let do not become properties
|
// `const S` is not reachable from here: top-level const/let do not become properties
|
||||||
|
|||||||
@@ -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, 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
|
// 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
|
// 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.
|
// 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(() =>
|
await page.evaluate(() =>
|
||||||
api('/api/entries/test-show/ui-2/position', { method: 'POST', body: JSON.stringify({ secs: 42 }) }));
|
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 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' });
|
const row = page.locator('#listening .childrow', { hasText: 'Second Episode' });
|
||||||
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');
|
||||||
@@ -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.
|
// Finished (read) drops it from the list, however far it got.
|
||||||
await page.evaluate(() =>
|
await page.evaluate(() =>
|
||||||
api('/api/entries/test-show/ui-2/flags', { method: 'POST', body: JSON.stringify({ read: true }) }));
|
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 });
|
await expect(page.locator('#listening')).not.toContainText('Second Episode', { timeout: 20_000 });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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.'},
|
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',
|
':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.'},
|
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},
|
':all':{title:'All Subscriptions',icon:ICON.all},
|
||||||
};
|
};
|
||||||
function renderFeeds(){
|
function renderFeeds(){
|
||||||
@@ -1745,30 +1747,26 @@ async function renderListed(v){
|
|||||||
$('#tbRemove').disabled=true;
|
$('#tbRemove').disabled=true;
|
||||||
syncTools(null);
|
syncTools(null);
|
||||||
$('#epSearch').placeholder='Search items…';
|
$('#epSearch').placeholder='Search items…';
|
||||||
const listening=v===VIEWS[':popular'], grid=v===VIEWS[':directory'];
|
const listening=v===VIEWS[':listening'], grid=v===VIEWS[':directory'];
|
||||||
box.innerHTML=`
|
box.innerHTML=`
|
||||||
<div class="fhead slim">
|
<div class="fhead slim">
|
||||||
<div class="art">${v.icon}</div>
|
<div class="art">${v.icon}</div>
|
||||||
<div class="meta"><h2>${v.title}</h2>
|
<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>
|
</div>
|
||||||
${grid?'<div class="dirbar" id="dirbar"></div>':''}
|
${grid?'<div class="dirbar" id="dirbar"></div>':''}
|
||||||
<div class="${grid?'tiles':'childlist'}" id="popular"><p class="hint">Loading…</p></div>
|
<div class="${grid?'tiles':'childlist'}" id="${listening?'listening':'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>`:''}`;
|
|
||||||
$('#count').textContent=v.title;
|
$('#count').textContent=v.title;
|
||||||
const n=await (grid?renderDirectory:listFeeds)(v.url,$('#popular',box));
|
const n=await (listening?renderListening:grid?renderDirectory:listFeeds)(v.url,$(listening?'#listening':'#popular',box));
|
||||||
if(VIEWS[S.feed]===v) $('#count').textContent=`${v.title}: ${n} feed${n===1?'':'s'}`;
|
if(VIEWS[S.feed]===v) $('#count').textContent=`${v.title}: ${plural(n,listening?'episode':'feed')}`;
|
||||||
if(listening) renderListening();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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
|
/// 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.
|
/// where you left off, not another way to browse.
|
||||||
async function renderListening(){
|
async function renderListening(url,box){
|
||||||
const box=$('#listening');
|
|
||||||
let rows=[];
|
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>';
|
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){
|
for(const e of rows){
|
||||||
const el=document.createElement('div');
|
const el=document.createElement('div');
|
||||||
@@ -1782,6 +1780,7 @@ async function renderListening(){
|
|||||||
el.onclick=()=>play(e);
|
el.onclick=()=>play(e);
|
||||||
box.appendChild(el);
|
box.appendChild(el);
|
||||||
}
|
}
|
||||||
|
return rows.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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.
|
||||||
|
|||||||
Reference in New Issue
Block a user