Files pane plays through the player bar, once
The pane drew its own <audio controls> for a downloaded file, and its onplay also started the player bar, so one click played the same file twice at once. The pane now has a play button (with the file's type icon, like the other rows) that hands that exact file to the player bar, the only player. play() takes the file, so another of an item's files starts from its top instead of resuming the first. Dead CSS for the pane's <audio> removed. Test: play in the Files pane leaves one <audio> on the page, the bar's. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0173mGu6rK18Ne7UGTwAaVJV
This commit is contained in:
@@ -61,6 +61,8 @@ The long form, with what was wrong before and how it was found, is in
|
|||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
- Playing a file from the Files pane played it twice at once, in the pane and in the player bar.
|
||||||
|
The pane has a play button now, and the player bar is the only player.
|
||||||
- The password box in Manage users was white in the dark theme.
|
- The password box in Manage users was white in the dark theme.
|
||||||
- An item with no date showed a stray dot in its details.
|
- An item with no date showed a stray dot in its details.
|
||||||
- Escape did not close a dialog while the cursor was in one of its boxes, so Add feed, which opens
|
- Escape did not close a dialog while the cursor was in one of its boxes, so Add feed, which opens
|
||||||
|
|||||||
@@ -89,13 +89,13 @@ test('the three panes are there and the item text lands in the bottom one', asyn
|
|||||||
// its chip rather than assuming which episode the daemon happened to fetch.
|
// its chip rather than assuming which episode the daemon happened to fetch.
|
||||||
const downloaded = page.locator('.ep', { has: page.locator('.kind.here') }).first();
|
const downloaded = page.locator('.ep', { has: page.locator('.kind.here') }).first();
|
||||||
await downloaded.click();
|
await downloaded.click();
|
||||||
await expect(page.locator('#files audio')).toBeVisible();
|
await expect(page.locator('#files [data-a="play"]')).toBeVisible();
|
||||||
await expect(page.locator('#files .encbox [title="Save to this computer"]')).toBeVisible();
|
await expect(page.locator('#files .encbox [title="Save to this computer"]')).toBeVisible();
|
||||||
|
|
||||||
// Selecting another item replaces the pane rather than stacking.
|
// Selecting another item replaces the pane rather than stacking.
|
||||||
await page.locator('.ep', { hasText: 'First Episode' }).click();
|
await page.locator('.ep', { hasText: 'First Episode' }).click();
|
||||||
await expect(page.locator('#detail .dt')).toHaveText('First Episode');
|
await expect(page.locator('#detail .dt')).toHaveText('First Episode');
|
||||||
await expect(page.locator('#files audio')).toHaveCount(0);
|
await expect(page.locator('#files [data-a="play"]')).toHaveCount(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('a downloaded file that is not audio gets no player', async ({ page }) => {
|
test('a downloaded file that is not audio gets no player', async ({ page }) => {
|
||||||
@@ -109,7 +109,7 @@ test('a downloaded file that is not audio gets no player', async ({ page }) => {
|
|||||||
await row.click();
|
await row.click();
|
||||||
|
|
||||||
await expect(page.locator('#detail .dt')).toHaveText('An Article');
|
await expect(page.locator('#detail .dt')).toHaveText('An Article');
|
||||||
await expect(page.locator('#files audio')).toHaveCount(0);
|
await expect(page.locator('#files [data-a="play"]')).toHaveCount(0);
|
||||||
// What it is and that it is here: one icon, green, with the words in its tooltip.
|
// What it is and that it is here: one icon, green, with the words in its tooltip.
|
||||||
await expect(page.locator('#files .encbox .kind.here')).toHaveAttribute('title', 'image, downloaded');
|
await expect(page.locator('#files .encbox .kind.here')).toHaveAttribute('title', 'image, downloaded');
|
||||||
// Still offered as a file, just not as an episode: viewable and keepable.
|
// Still offered as a file, just not as an episode: viewable and keepable.
|
||||||
@@ -767,3 +767,14 @@ test('the item table sorts by any column, both ways, and remembers', async ({ pa
|
|||||||
await expect(page.locator('#eps .ep .size', { hasText: /\d/ }).first()).toBeVisible();
|
await expect(page.locator('#eps .ep .size', { hasText: /\d/ }).first()).toBeVisible();
|
||||||
await expect(page.locator('#eps .ep .file', { hasText: /\d/ })).toHaveCount(0);
|
await expect(page.locator('#eps .ep .file', { hasText: /\d/ })).toHaveCount(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('play in the Files pane plays once, in the player bar', async ({ page }) => {
|
||||||
|
// Regression: the pane had an <audio> of its own, and playing it started the player bar too,
|
||||||
|
// so the same file played twice at once.
|
||||||
|
await page.locator('#feedlist .feed', { hasText: 'Test Show' }).first().click();
|
||||||
|
await page.locator('.ep', { has: page.locator('.kind.here') }).first().click();
|
||||||
|
await page.locator('#files [data-a="play"]').click();
|
||||||
|
await expect(page.locator('#player')).toBeVisible();
|
||||||
|
await expect(page.locator('audio')).toHaveCount(1); // the player bar's, and nothing else
|
||||||
|
await page.locator('#pclose').click();
|
||||||
|
});
|
||||||
|
|||||||
@@ -191,7 +191,6 @@ input:focus,select:focus{outline:0;border-color:var(--accent)}
|
|||||||
/* The selected item's files, beside the list, as the original's Files pane was. */
|
/* The selected item's files, beside the list, as the original's Files pane was. */
|
||||||
#files{grid-area:files;overflow-y:auto;padding:8px 12px;border-left:1px solid var(--line);background:var(--panel)}
|
#files{grid-area:files;overflow-y:auto;padding:8px 12px;border-left:1px solid var(--line);background:var(--panel)}
|
||||||
#files .encbox{margin-top:8px}
|
#files .encbox{margin-top:8px}
|
||||||
#files .encbox audio{min-width:0;width:100%;flex-basis:100%}
|
|
||||||
#files .empty{padding:24px 0}
|
#files .empty{padding:24px 0}
|
||||||
.fhd{font-size:10.5px;text-transform:uppercase;letter-spacing:.05em;color:var(--faint)}
|
.fhd{font-size:10.5px;text-transform:uppercase;letter-spacing:.05em;color:var(--faint)}
|
||||||
#grab{grid-area:grab;cursor:row-resize;background:var(--line)}
|
#grab{grid-area:grab;cursor:row-resize;background:var(--line)}
|
||||||
@@ -208,7 +207,6 @@ input:focus,select:focus{outline:0;border-color:var(--accent)}
|
|||||||
display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-top:14px;
|
display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-top:14px;
|
||||||
padding:10px 12px;border:1px solid var(--line);border-radius:9px;background:var(--panel2);
|
padding:10px 12px;border:1px solid var(--line);border-radius:9px;background:var(--panel2);
|
||||||
}
|
}
|
||||||
.encbox audio{flex:1;min-width:200px;margin:0}
|
|
||||||
.fhead{display:flex;gap:18px;margin-bottom:18px}
|
.fhead{display:flex;gap:18px;margin-bottom:18px}
|
||||||
.fhead .art{width:118px;height:118px;font-size:34px;box-shadow:var(--shadow)}
|
.fhead .art{width:118px;height:118px;font-size:34px;box-shadow:var(--shadow)}
|
||||||
.fhead .meta{min-width:0;flex:1;display:flex;flex-direction:column}
|
.fhead .meta{min-width:0;flex:1;display:flex;flex-direction:column}
|
||||||
@@ -1192,13 +1190,9 @@ function showDetail(e){
|
|||||||
$('#dback').onclick=()=>showDetail(null);
|
$('#dback').onclick=()=>showDetail(null);
|
||||||
for(const root of [box,files]) if(root) $$('button[data-a]',root).forEach(b=>
|
for(const root of [box,files]) if(root) $$('button[data-a]',root).forEach(b=>
|
||||||
b.onclick=()=>epAction(b.dataset.a,e,null,b.dataset.enc?Number(b.dataset.enc):null));
|
b.onclick=()=>epAction(b.dataset.a,e,null,b.dataset.enc?Number(b.dataset.enc):null));
|
||||||
for(const x of e.enclosures){
|
|
||||||
const a=$(`#audio-${x.id}`);
|
|
||||||
if(a) a.onplay=()=>play(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One enclosure: a player when the file is here, otherwise what it is and a way to get it.
|
/// One enclosure: a play button when the file is here, otherwise what it is and a way to get it.
|
||||||
function encBox(x){
|
function encBox(x){
|
||||||
const size=x.length?mb(x.length):'';
|
const size=x.length?mb(x.length):'';
|
||||||
// One file serves everyone reading the feed, so deleting is not a private act.
|
// One file serves everyone reading the feed, so deleting is not a private act.
|
||||||
@@ -1221,9 +1215,12 @@ function encBox(x){
|
|||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
if(x.path){
|
if(x.path){
|
||||||
|
// One player, the bar at the bottom. This pane had an <audio> of its own, and playing it
|
||||||
|
// started the bar as well, so the same file played twice at once.
|
||||||
return `<div class="encbox">
|
return `<div class="encbox">
|
||||||
<audio id="audio-${x.id}" controls preload="none" src="/media/${x.id}"></audio>
|
${kindIcon(x)}
|
||||||
<span class="meta">${size}</span>
|
<span class="meta" style="flex:1">${size}</span>
|
||||||
|
<button class="btn ico" data-a="play" data-enc="${x.id}" title="Play" aria-label="Play">${ICON.play}</button>
|
||||||
${saveBtn}
|
${saveBtn}
|
||||||
${delBtn}
|
${delBtn}
|
||||||
</div>`;
|
</div>`;
|
||||||
@@ -1247,7 +1244,7 @@ async function epAction(a,e,el,encId){
|
|||||||
const enc=(encId!=null && e.enclosures.find(x=>x.id===encId)) || e.enclosures[0];
|
const enc=(encId!=null && e.enclosures.find(x=>x.id===encId)) || e.enclosures[0];
|
||||||
const path=`/api/entries/${encodeURIComponent(e.feed_id)}/${encodeURIComponent(e.guid)}`;
|
const path=`/api/entries/${encodeURIComponent(e.feed_id)}/${encodeURIComponent(e.guid)}`;
|
||||||
try{
|
try{
|
||||||
if(a==='play') play(e);
|
if(a==='play') play(e, encId!=null ? enc : undefined);
|
||||||
if(a==='flag'){ e.flagged=!e.flagged; await api(path+'/flags',{method:'POST',body:JSON.stringify({flagged:e.flagged})}); redraw(); }
|
if(a==='flag'){ e.flagged=!e.flagged; await api(path+'/flags',{method:'POST',body:JSON.stringify({flagged:e.flagged})}); redraw(); }
|
||||||
if(a==='read'){ e.read=!e.read; await api(path+'/flags',{method:'POST',body:JSON.stringify({read:e.read})}); redraw(); loadFeeds(true); }
|
if(a==='read'){ e.read=!e.read; await api(path+'/flags',{method:'POST',body:JSON.stringify({read:e.read})}); redraw(); loadFeeds(true); }
|
||||||
if(a==='get'){
|
if(a==='get'){
|
||||||
@@ -1294,15 +1291,16 @@ function markPlayed(){
|
|||||||
{method:'POST',body:JSON.stringify({read:true})}).then(()=>loadFeeds(true)).catch(()=>{});
|
{method:'POST',body:JSON.stringify({read:true})}).then(()=>loadFeeds(true)).catch(()=>{});
|
||||||
}
|
}
|
||||||
|
|
||||||
function play(e){
|
/// Plays one of the item's files in the player bar: the one asked for, or its first playable one.
|
||||||
const enc=e.enclosures.find(isPlayable);
|
function play(e,enc=e.enclosures.find(isPlayable)){
|
||||||
if(!enc){
|
if(!isPlayable(enc)){
|
||||||
toast(e.enclosures.some(x=>x.path) ? 'That file is not audio or video' : 'Not downloaded yet', true);
|
toast(e.enclosures.some(x=>x.path) ? 'That file is not audio or video' : 'Not downloaded yet', true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const resuming = player.guid===e.guid;
|
// The same file carries on where it was; another of the item's files starts from its top.
|
||||||
|
const resuming = player.guid===e.guid && player.enc===enc.id;
|
||||||
if(!resuming){
|
if(!resuming){
|
||||||
player.guid=e.guid; player.feed=e.feed_id; player.entry=e;
|
player.guid=e.guid; player.feed=e.feed_id; player.entry=e; player.enc=enc.id;
|
||||||
audio.src=`/media/${enc.id}`;
|
audio.src=`/media/${enc.id}`;
|
||||||
audio.currentTime=0;
|
audio.currentTime=0;
|
||||||
if(e.position>5) audio.addEventListener('loadedmetadata',()=>{audio.currentTime=e.position},{once:true});
|
if(e.position>5) audio.addEventListener('loadedmetadata',()=>{audio.currentTime=e.position},{once:true});
|
||||||
|
|||||||
Reference in New Issue
Block a user