The page tells two feeds' items with one guid apart (#361)
Rows, the selection, the items turned past and the player were matched by guid alone, so in All Subscriptions two feeds giving a post the same guid (a blog's RSS and Atom) were one item to the page: clicking the second selected and marked the first, and the second never read. They are matched by feed and guid now (entryKey, the key readWrites already used); data-guid stays on the row for the tests, data-key is matched. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Two items from different feeds that share an id, as a blog's RSS and Atom feeds' do, are told apart: in All Subscriptions the second can be opened and marked read, where the first was marked whichever was clicked (#361).
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- An item's title and text take four fifths of the reading pane's width, no longer running to its far edge (#360).
|
- An item's title and text take four fifths of the reading pane's width, no longer running to its far edge (#360).
|
||||||
|
|||||||
@@ -362,7 +362,7 @@ function drawListening(box: HTMLElement,rows: Entry[]){
|
|||||||
|
|
||||||
/// One row's time left, progress and play button, taken from the player when it is the one in it.
|
/// One row's time left, progress and play button, taken from the player when it is the one in it.
|
||||||
function paintListenRow(el: HTMLElement & {entry?: Entry}){
|
function paintListenRow(el: HTMLElement & {entry?: Entry}){
|
||||||
const e=el.entry!, now=player.guid===e.guid&&player.feed===e.feed_id;
|
const e=el.entry!, now=inPlayer(e);
|
||||||
// Zero until the player has sought to where you left off; the saved position stands till then.
|
// Zero until the player has sought to where you left off; the saved position stands till then.
|
||||||
if(now&&audio.currentTime) e.position=Math.floor(audio.currentTime);
|
if(now&&audio.currentTime) e.position=Math.floor(audio.currentTime);
|
||||||
// The player's own length first: a feed's can be minutes out.
|
// The player's own length first: a feed's can be minutes out.
|
||||||
@@ -378,14 +378,14 @@ function paintListenRow(el: HTMLElement & {entry?: Entry}){
|
|||||||
/// Keeps the list in step with the player. Only a row that is, or was, the one in it changes.
|
/// Keeps the list in step with the player. Only a row that is, or was, the one in it changes.
|
||||||
function syncListening(){
|
function syncListening(){
|
||||||
for(const el of $$<HTMLElement & {entry?: Entry}>('#listening .childrow'))
|
for(const el of $$<HTMLElement & {entry?: Entry}>('#listening .childrow'))
|
||||||
if(el.entry&&(el.classList.contains('now')||player.guid===el.entry.guid)) paintListenRow(el);
|
if(el.entry&&(el.classList.contains('now')||inPlayer(el.entry))) paintListenRow(el);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Takes an episode off Currently Listening by forgetting where you got to: the list is every
|
/// 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.
|
/// episode with a saved position short of the end, so the position is what has to go.
|
||||||
async function forget(e: Entry){
|
async function forget(e: Entry){
|
||||||
// Closed without saving first, or the player's next save would put it straight back.
|
// Closed without saving first, or the player's next save would put it straight back.
|
||||||
if(player.guid===e.guid){ player.guid=null; $<HTMLButtonElement>('#pclose').click(); }
|
if(inPlayer(e)){ player.guid=null; $<HTMLButtonElement>('#pclose').click(); }
|
||||||
try{
|
try{
|
||||||
await api(`/api/entries/${encodeURIComponent(e.feed_id)}/${encodeURIComponent(e.guid)}/position`,
|
await api(`/api/entries/${encodeURIComponent(e.feed_id)}/${encodeURIComponent(e.guid)}/position`,
|
||||||
{method:'POST',body:JSON.stringify({secs:0})});
|
{method:'POST',body:JSON.stringify({secs:0})});
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ document.addEventListener('touchmove', ev => {
|
|||||||
}else if(Math.abs(touch.dx) > 10){
|
}else if(Math.abs(touch.dx) > 10){
|
||||||
// The reader follows the finger, so it is plain before letting go whether this will move on;
|
// The reader follows the finger, so it is plain before letting go whether this will move on;
|
||||||
// with nothing that way it only gives a little.
|
// with nothing that way it only gives a little.
|
||||||
const i = S.entries.findIndex(x => x.guid === S.sel);
|
const i = S.entries.findIndex(x => entryKey(x) === S.sel);
|
||||||
const end = touch.dx < 0 && i >= S.entries.length - 1;
|
const end = touch.dx < 0 && i >= S.entries.length - 1;
|
||||||
slide(end ? touch.dx / 3 : touch.dx, 0);
|
slide(end ? touch.dx / 3 : touch.dx, 0);
|
||||||
}
|
}
|
||||||
@@ -100,7 +100,7 @@ document.addEventListener('touchmove', ev => {
|
|||||||
/// (issue #52). Kept outside #content, which every feed render rebuilds, and inert, since its
|
/// (issue #52). Kept outside #content, which every feed render rebuilds, and inert, since its
|
||||||
/// copy of the reader's markup repeats the reader's ids.
|
/// copy of the reader's markup repeats the reader's ids.
|
||||||
function beside(dx: number){
|
function beside(dx: number){
|
||||||
const i = S.entries.findIndex(x => x.guid === S.sel), e = S.entries[i + (dx < 0 ? 1 : -1)];
|
const i = S.entries.findIndex(x => entryKey(x) === S.sel), e = S.entries[i + (dx < 0 ? 1 : -1)];
|
||||||
const kind = dx < 0 ? (e ? 'next' : 'end') : (e ? 'prev' : 'dim');
|
const kind = dx < 0 ? (e ? 'next' : 'end') : (e ? 'prev' : 'dim');
|
||||||
let p = $('#dpeek');
|
let p = $('#dpeek');
|
||||||
if(!p){
|
if(!p){
|
||||||
@@ -160,7 +160,7 @@ document.addEventListener('touchend', () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Across, mostly sideways, and not a slow drag while selecting text.
|
// Across, mostly sideways, and not a slow drag while selecting text.
|
||||||
const i = S.entries.findIndex(x => x.guid === S.sel);
|
const i = S.entries.findIndex(x => entryKey(x) === S.sel);
|
||||||
if(Math.abs(g.dx) < swipeMin() || Math.abs(g.dx) < 2 * Math.abs(g.dy) || Date.now() - g.t > 800
|
if(Math.abs(g.dx) < swipeMin() || Math.abs(g.dx) < 2 * Math.abs(g.dy) || Date.now() - g.t > 800
|
||||||
|| (g.dx < 0 && i >= S.entries.length - 1)) return slide(0);
|
|| (g.dx < 0 && i >= S.entries.length - 1)) return slide(0);
|
||||||
if(g.dx < 0) return slideOn(g.dx, () => stepEntry(1));
|
if(g.dx < 0) return slideOn(g.dx, () => stepEntry(1));
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
/* ---------------- items ---------------- */
|
/* ---------------- items ---------------- */
|
||||||
|
/// An item is its feed and its guid: two feeds can give one post the same guid, as a blog's RSS
|
||||||
|
/// and Atom feeds do, and keyed by guid alone, All Subscriptions marked the first of the two
|
||||||
|
/// whichever was clicked (#361).
|
||||||
|
const entryKey=(e: Entry)=>e.feed_id+'\n'+e.guid;
|
||||||
|
/// Whether this item is the one in the player.
|
||||||
|
const inPlayer=(e: Entry)=>player.guid===e.guid&&player.feed===e.feed_id;
|
||||||
let loadSeq=0;
|
let loadSeq=0;
|
||||||
type ListPage={total: number, entries: Entry[]};
|
type ListPage={total: number, entries: Entry[]};
|
||||||
/// The first page of each list opened or fetched ahead, by what it shows, so a list seen before is
|
/// The first page of each list opened or fetched ahead, by what it shows, so a list seen before is
|
||||||
@@ -99,7 +105,7 @@ async function loadEntries(append?: boolean, quiet?: boolean){
|
|||||||
function showList(r: ListPage, append: boolean, asked: number){
|
function showList(r: ListPage, append: boolean, asked: number){
|
||||||
S.total=r.total;
|
S.total=r.total;
|
||||||
for(const e of r.entries){
|
for(const e of r.entries){
|
||||||
const w=readWrites.get(readKey(e));
|
const w=readWrites.get(entryKey(e));
|
||||||
if(w && !(w.done!==undefined&&w.done<asked)) e.read=w.read;
|
if(w && !(w.done!==undefined&&w.done<asked)) e.read=w.read;
|
||||||
}
|
}
|
||||||
let entries = append ? S.entries.concat(r.entries) : r.entries;
|
let entries = append ? S.entries.concat(r.entries) : r.entries;
|
||||||
@@ -107,10 +113,10 @@ function showList(r: ListPage, append: boolean, asked: number){
|
|||||||
// tab -- would drop the item you have open the moment reading it took it off the filter.
|
// tab -- would drop the item you have open the moment reading it took it off the filter.
|
||||||
// Keep it until you pick a different one; the next refresh after that no longer protects it.
|
// Keep it until you pick a different one; the next refresh after that no longer protects it.
|
||||||
// The items turned past on the way to it are kept too, so a swipe back still finds them.
|
// The items turned past on the way to it are kept too, so a swipe back still finds them.
|
||||||
const kept=(e: Entry)=>e.guid===S.sel||turned.has(e.guid);
|
const kept=(e: Entry)=>entryKey(e)===S.sel||turned.has(entryKey(e));
|
||||||
if(S.filter==='unread') entries=entries.filter(e=>!e.read||kept(e));
|
if(S.filter==='unread') entries=entries.filter(e=>!e.read||kept(e));
|
||||||
if(!append && S.sel){
|
if(!append && S.sel){
|
||||||
const gone=S.entries.filter(e=>kept(e)&&!entries.some(n=>n.guid===e.guid));
|
const gone=S.entries.filter(e=>kept(e)&&!entries.some(n=>entryKey(n)===entryKey(e)));
|
||||||
entries=[...gone,...entries];
|
entries=[...gone,...entries];
|
||||||
}
|
}
|
||||||
S.entries = entries;
|
S.entries = entries;
|
||||||
@@ -157,8 +163,8 @@ function epEl(e: Entry){
|
|||||||
const playable=isPlayable(enc);
|
const playable=isPlayable(enc);
|
||||||
const others=e.enclosures.length-1;
|
const others=e.enclosures.length-1;
|
||||||
const el=document.createElement('div');
|
const el=document.createElement('div');
|
||||||
el.className='ep'+(e.read?' read':'')+(S.sel===e.guid?' sel':'');
|
el.className='ep'+(e.read?' read':'')+(S.sel===entryKey(e)?' sel':'');
|
||||||
el.dataset.guid=e.guid;
|
el.dataset.guid=e.guid; el.dataset.key=entryKey(e);
|
||||||
const num=[e.season?`S${e.season}`:'',e.episode?`E${e.episode}`:''].filter(Boolean).join('');
|
const num=[e.season?`S${e.season}`:'',e.episode?`E${e.episode}`:''].filter(Boolean).join('');
|
||||||
const left = e.position>10 && e.duration ? `${clock(e.duration-e.position)} left` : (e.duration?clock(e.duration):'');
|
const left = e.position>10 && e.duration ? `${clock(e.duration-e.position)} left` : (e.duration?clock(e.duration):'');
|
||||||
const name=entryName(e);
|
const name=entryName(e);
|
||||||
@@ -166,7 +172,7 @@ function epEl(e: Entry){
|
|||||||
// the row alone was a click target only a mouse could reach (#298).
|
// the row alone was a click target only a mouse could reach (#298).
|
||||||
el.innerHTML=`
|
el.innerHTML=`
|
||||||
<button class="st" data-a="read" title="Mark ${e.read?'unread':'read'}" aria-label="Mark ${e.read?'unread':'read'}">${
|
<button class="st" data-a="read" title="Mark ${e.read?'unread':'read'}" aria-label="Mark ${e.read?'unread':'read'}">${
|
||||||
player.guid===e.guid?EQ:(e.read?'':'<span aria-hidden="true">●</span>')}</button>
|
inPlayer(e)?EQ:(e.read?'':'<span aria-hidden="true">●</span>')}</button>
|
||||||
<button class="fl${e.flagged?' on':''}" data-a="flag" aria-pressed="${!!e.flagged}" title="${
|
<button class="fl${e.flagged?' on':''}" data-a="flag" aria-pressed="${!!e.flagged}" title="${
|
||||||
e.flagged?'Unpin':'Pin, so it is never deleted'}">${e.flagged?ICON.pinOn:ICON.pin}</button>
|
e.flagged?'Unpin':'Pin, so it is never deleted'}">${e.flagged?ICON.pinOn:ICON.pin}</button>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
@@ -262,7 +268,7 @@ function entryName(e: Partial<Entry>): {text: string, derived: boolean}{
|
|||||||
/// Selecting an item shows it in the pane below, rather than expanding the row.
|
/// Selecting an item shows it in the pane below, rather than expanding the row.
|
||||||
/// Replaces one row with a fresh one, leaving the rest of the list and its scroll alone.
|
/// Replaces one row with a fresh one, leaving the rest of the list and its scroll alone.
|
||||||
function swapRow(e: Entry){
|
function swapRow(e: Entry){
|
||||||
const row=$(`#eps .ep[data-guid="${CSS.escape(e.guid)}"]`);
|
const row=$(`#eps .ep[data-key="${CSS.escape(entryKey(e))}"]`);
|
||||||
if(row){ row.replaceWith(epEl(e)); syncPlayButtons(); }
|
if(row){ row.replaceWith(epEl(e)); syncPlayButtons(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,12 +276,11 @@ function swapRow(e: Entry){
|
|||||||
/// before then answers with the old state: it put the dot back on an item just read, until
|
/// before then answers with the old state: it put the dot back on an item just read, until
|
||||||
/// the next refresh took it off again (loadEntries).
|
/// the next refresh took it off again (loadEntries).
|
||||||
const readWrites=new Map<string, {read: boolean, done?: number}>();
|
const readWrites=new Map<string, {read: boolean, done?: number}>();
|
||||||
const readKey=(e: Entry)=>e.feed_id+'\n'+e.guid;
|
|
||||||
function setRead(e: Entry,read: boolean){
|
function setRead(e: Entry,read: boolean){
|
||||||
e.read=read;
|
e.read=read;
|
||||||
// Kept lists of its feed hold their own copy of it, still as it was.
|
// Kept lists of its feed hold their own copy of it, still as it was.
|
||||||
forgetLists(e.feed_id);
|
forgetLists(e.feed_id);
|
||||||
const w: {read: boolean, done?: number}={read}; readWrites.set(readKey(e),w);
|
const w: {read: boolean, done?: number}={read}; readWrites.set(entryKey(e),w);
|
||||||
return api(`/api/entries/${encodeURIComponent(e.feed_id)}/${encodeURIComponent(e.guid)}/flags`,
|
return api(`/api/entries/${encodeURIComponent(e.feed_id)}/${encodeURIComponent(e.guid)}/flags`,
|
||||||
{method:'POST',body:JSON.stringify({read})})
|
{method:'POST',body:JSON.stringify({read})})
|
||||||
.then(row=>{ w.done=performance.now(); if(row) patchFeed(row); if(S.signals.length) refreshSignals(); });
|
.then(row=>{ w.done=performance.now(); if(row) patchFeed(row); if(S.signals.length) refreshSignals(); });
|
||||||
@@ -285,7 +290,7 @@ function setRead(e: Entry,read: boolean){
|
|||||||
/// reloaded, so an item does not vanish from under the pointer on the Unread tab.
|
/// reloaded, so an item does not vanish from under the pointer on the Unread tab.
|
||||||
function markRead(e: Entry){
|
function markRead(e: Entry){
|
||||||
if(e.read) return;
|
if(e.read) return;
|
||||||
setRead(e,true).catch(err=>{ e.read=false; readWrites.delete(readKey(e)); toast(why(err),true); });
|
setRead(e,true).catch(err=>{ e.read=false; readWrites.delete(entryKey(e)); toast(why(err),true); });
|
||||||
}
|
}
|
||||||
|
|
||||||
/// On the Unread tab, the items read while turning from one to the next (a swipe, j and k), kept
|
/// On the Unread tab, the items read while turning from one to the next (a swipe, j and k), kept
|
||||||
@@ -293,23 +298,23 @@ function markRead(e: Entry){
|
|||||||
/// was left, a swipe back had nothing to go back to: the item just read was already gone.
|
/// was left, a swipe back had nothing to go back to: the item just read was already gone.
|
||||||
const turned=new Set<string>();
|
const turned=new Set<string>();
|
||||||
function dropTurned(keep: string|null){
|
function dropTurned(keep: string|null){
|
||||||
const gone=S.entries.filter(x=>turned.has(x.guid)&&x.read&&x.guid!==keep);
|
const gone=S.entries.filter(x=>turned.has(entryKey(x))&&x.read&&entryKey(x)!==keep);
|
||||||
turned.clear();
|
turned.clear();
|
||||||
if(!gone.length) return;
|
if(!gone.length) return;
|
||||||
S.entries=S.entries.filter(x=>!gone.includes(x)); S.total-=gone.length;
|
S.entries=S.entries.filter(x=>!gone.includes(x)); S.total-=gone.length;
|
||||||
for(const x of gone) $(`#eps .ep[data-guid="${CSS.escape(x.guid)}"]`)?.remove();
|
for(const x of gone) $(`#eps .ep[data-key="${CSS.escape(entryKey(x))}"]`)?.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectEntry(e: Entry,turning=false){
|
function selectEntry(e: Entry,turning=false){
|
||||||
// On the Unread tab the item you were reading goes as you move on, not whenever a refresh
|
// On the Unread tab the item you were reading goes as you move on, not whenever a refresh
|
||||||
// next happens to come along, which left a few read ones in the list for a while.
|
// next happens to come along, which left a few read ones in the list for a while.
|
||||||
if(S.filter==='unread' && S.sel && S.sel!==e.guid){
|
if(S.filter==='unread' && S.sel && S.sel!==entryKey(e)){
|
||||||
turned.add(S.sel);
|
turned.add(S.sel);
|
||||||
if(!turning) dropTurned(e.guid);
|
if(!turning) dropTurned(entryKey(e));
|
||||||
}
|
}
|
||||||
S.sel=e.guid;
|
S.sel=entryKey(e);
|
||||||
markRead(e);
|
markRead(e);
|
||||||
$$('#eps .ep').forEach(x=>x.classList.toggle('sel', x.dataset.guid===e.guid));
|
$$('#eps .ep').forEach(x=>x.classList.toggle('sel', x.dataset.key===entryKey(e)));
|
||||||
swapRow(e);
|
swapRow(e);
|
||||||
showDetail(e);
|
showDetail(e);
|
||||||
const d=$('#detail'); if(d) d.scrollTop=0;
|
const d=$('#detail'); if(d) d.scrollTop=0;
|
||||||
@@ -339,9 +344,7 @@ function dragSplit(pane: HTMLElement){
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Which item the toolbar's play, read and keep buttons act on.
|
/// Which item the toolbar's play, read and keep buttons act on.
|
||||||
// ponytail: matched by guid alone; two feeds sharing a guid in All Subscriptions would pick
|
const cur=()=>S.entries.find(x=>entryKey(x)===S.sel);
|
||||||
// the first. Key rows by feed as well if that ever happens.
|
|
||||||
const cur=()=>S.entries.find(x=>x.guid===S.sel);
|
|
||||||
function syncTools(e: Entry|null){
|
function syncTools(e: Entry|null){
|
||||||
$<HTMLButtonElement>('#tbPlay').disabled=!(e&&e.enclosures.some(isPlayable));
|
$<HTMLButtonElement>('#tbPlay').disabled=!(e&&e.enclosures.some(isPlayable));
|
||||||
$<HTMLButtonElement>('#tbRead').disabled=$<HTMLButtonElement>('#tbFlag').disabled=!e;
|
$<HTMLButtonElement>('#tbRead').disabled=$<HTMLButtonElement>('#tbFlag').disabled=!e;
|
||||||
@@ -489,7 +492,7 @@ function encBox(x: Enclosure){
|
|||||||
|
|
||||||
async function epAction(a: string, e: Entry, el: HTMLElement|null, encId?: number){
|
async function epAction(a: string, e: Entry, el: HTMLElement|null, encId?: number){
|
||||||
// Swap the row in place and refresh the text below when it is the one being read.
|
// Swap the row in place and refresh the text below when it is the one being read.
|
||||||
const redraw=()=>{ swapRow(e); if(!el || S.sel===e.guid) showDetail(e); };
|
const redraw=()=>{ swapRow(e); if(!el || S.sel===entryKey(e)) showDetail(e); };
|
||||||
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{
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ function play(e: Entry,enc=e.enclosures.find(isPlayable)){
|
|||||||
}
|
}
|
||||||
// The same file carries on where it was; another of the item's files starts from its top. So
|
// The same file carries on where it was; another of the item's files starts from its top. So
|
||||||
// does the other version of the same file, from the place the entry gives in its clock.
|
// does the other version of the same file, from the place the entry gives in its clock.
|
||||||
const resuming = player.guid===e.guid && player.enc===enc.id && player.src===mediaSrc(enc);
|
const resuming = inPlayer(e) && player.enc===enc.id && player.src===mediaSrc(enc);
|
||||||
// Every play button is a pause button for what is playing, as the player bar's is.
|
// Every play button is a pause button for what is playing, as the player bar's is.
|
||||||
if(resuming && !audio.paused){ audio.pause(); return; }
|
if(resuming && !audio.paused){ audio.pause(); return; }
|
||||||
let wait: Promise<void>|null=null;
|
let wait: Promise<void>|null=null;
|
||||||
@@ -140,16 +140,16 @@ for(const ev of ['play','pause','ended']) audio.addEventListener(ev,syncPlayButt
|
|||||||
/// Every play button for what is playing shows pause, like the player bar's: a row's, the files
|
/// Every play button for what is playing shows pause, like the player bar's: a row's, the files
|
||||||
/// pane's, the toolbar's. Only the bar's used to change, so the others said play while it played.
|
/// pane's, the toolbar's. Only the bar's used to change, so the others said play while it played.
|
||||||
function syncPlayButtons(){
|
function syncPlayButtons(){
|
||||||
const on=(guid: string|null,enc?: number)=>!audio.paused&&player.guid===guid&&(enc==null||player.enc===enc);
|
const on=(key: string|null|undefined,enc?: number)=>!audio.paused&&player.feed+'\n'+player.guid===key&&(enc==null||player.enc===enc);
|
||||||
const paint=(b: HTMLElement,now: boolean,idle: string)=>{
|
const paint=(b: HTMLElement,now: boolean,idle: string)=>{
|
||||||
const label=now?'Pause':idle;
|
const label=now?'Pause':idle;
|
||||||
if(b.title===label) return;
|
if(b.title===label) return;
|
||||||
b.title=label; b.setAttribute('aria-label',label); b.innerHTML=now?ICON.pause:ICON.play;
|
b.title=label; b.setAttribute('aria-label',label); b.innerHTML=now?ICON.pause:ICON.play;
|
||||||
};
|
};
|
||||||
for(const b of $$('#eps .ep [data-a=play]')) paint(b,on(b.closest<HTMLElement>('.ep')!.dataset.guid!),'Play');
|
for(const b of $$('#eps .ep [data-a=play]')) paint(b,on(b.closest<HTMLElement>('.ep')!.dataset.key),'Play');
|
||||||
for(const b of $$('#files [data-a=play][data-enc], #detail [data-a=play][data-enc]'))
|
for(const b of $$('#files [data-a=play][data-enc], #detail [data-a=play][data-enc]'))
|
||||||
paint(b,on(S.sel,Number(b.dataset.enc)),'Play');
|
paint(b,on(S.sel,Number(b.dataset.enc)),'Play');
|
||||||
const e=cur(); paint($<HTMLButtonElement>('#tbPlay'),!!e&&on(e.guid),'Play the selected item');
|
const e=cur(); paint($<HTMLButtonElement>('#tbPlay'),!!e&&on(entryKey(e)),'Play the selected item');
|
||||||
}
|
}
|
||||||
for(const ev of ['play','pause','timeupdate']) audio.addEventListener(ev,syncListening);
|
for(const ev of ['play','pause','timeupdate']) audio.addEventListener(ev,syncListening);
|
||||||
window.addEventListener('beforeunload',savePos);
|
window.addEventListener('beforeunload',savePos);
|
||||||
@@ -244,10 +244,10 @@ const GO: Record<string,string>={a:':all',d:':directory',l:':listening'};
|
|||||||
let gAt=0;
|
let gAt=0;
|
||||||
function stepEntry(by: number){
|
function stepEntry(by: number){
|
||||||
if(VIEWS[S.feed ?? '']?.url||!S.entries.length) return;
|
if(VIEWS[S.feed ?? '']?.url||!S.entries.length) return;
|
||||||
const i=S.entries.findIndex(x=>x.guid===S.sel);
|
const i=S.entries.findIndex(x=>entryKey(x)===S.sel);
|
||||||
const e=S.entries[i<0?0:Math.min(S.entries.length-1,Math.max(0,i+by))];
|
const e=S.entries[i<0?0:Math.min(S.entries.length-1,Math.max(0,i+by))];
|
||||||
selectEntry(e,true);
|
selectEntry(e,true);
|
||||||
$(`#eps .ep[data-guid="${CSS.escape(e.guid)}"]`)?.scrollIntoView({block:'nearest'});
|
$(`#eps .ep[data-key="${CSS.escape(entryKey(e))}"]`)?.scrollIntoView({block:'nearest'});
|
||||||
}
|
}
|
||||||
function stepFeed(by: number){
|
function stepFeed(by: number){
|
||||||
const rows=$$('#feedlist [data-id]'), i=rows.findIndex(r=>r.dataset.id===S.feed), id=rows[i+by]?.dataset.id;
|
const rows=$$('#feedlist [data-id]'), i=rows.findIndex(r=>r.dataset.id===S.feed), id=rows[i+by]?.dataset.id;
|
||||||
|
|||||||
Reference in New Issue
Block a user