Refresh buttons turn from the press to the end of their scan; quick checks keep the count (#232, #233)
The buttons followed the feeds being checked, so they stopped between one feed and the next;
7ecbe82 held them for 600 ms over each gap. Now pressing one starts it, and scan_done stops
it only when it is the end of the scan asked for: scan_done carries `only` and `forced`, so a
scheduled scan finishing first does not stop a button whose scan is queued behind it. An
error event, a job that failed without a scan_done, stops them all.
A row's spinner stood in for its unread count from the moment its feed was checked; most
checks take milliseconds, so each count blinked as the scan went by. It now waits 300 ms.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -729,7 +729,11 @@ function opmlModal(){
|
||||
}
|
||||
|
||||
// No toast: the spinners on the rows being checked say it (issue #37).
|
||||
async function scanAll(){ await api('/api/fetch',{method:'POST',body:JSON.stringify({force:true})}); }
|
||||
async function scanAll(){
|
||||
spinStart(null);
|
||||
try{ await api('/api/fetch',{method:'POST',body:JSON.stringify({force:true})}); }
|
||||
catch(e){ spinEnd(); throw e; }
|
||||
}
|
||||
$('#scanAll').onclick=scanAll;
|
||||
$('#prefs').onclick=prefsModal;
|
||||
// Someone the proxy signed in is signed out by the proxy: ipx's own sign-out cannot stick while
|
||||
|
||||
@@ -66,7 +66,9 @@ function connect(){
|
||||
// every open page reloaded the whole list each time (#103).
|
||||
// Rows came as feed_row events. ponytail: a feed an OPML drops stays in the list until the
|
||||
// page reloads; reload the list here when a scan synced an OPML if that ever matters.
|
||||
else if(ev.ev==='scan_done'){ scanning.clear(); paintScanning(); if(ev.feeds) refreshEntries(); }
|
||||
else if(ev.ev==='scan_done'){ scanning.clear(); paintScanning(); spinEnd(ev); if(ev.feeds) refreshEntries(); }
|
||||
// The worker's job failed, so the scan someone is waiting on will send no scan_done.
|
||||
else if(ev.ev==='error') spinEnd();
|
||||
};
|
||||
sse.onerror=()=>{ sse.close(); dropped=true; setTimeout(connect,4000); };
|
||||
}
|
||||
|
||||
@@ -159,7 +159,11 @@ function renderGroup(f,kids){
|
||||
}
|
||||
|
||||
async function feedAction(a,f){
|
||||
if(a==='scan'){ await api('/api/fetch',{method:'POST',body:JSON.stringify({feed:f.id,force:true})}); }
|
||||
if(a==='scan'){
|
||||
spinStart(f.id);
|
||||
try{ await api('/api/fetch',{method:'POST',body:JSON.stringify({feed:f.id,force:true})}); }
|
||||
catch(e){ spinEnd(); throw e; }
|
||||
}
|
||||
if(a==='read'){ const r=await api(`/api/feeds/${encodeURIComponent(f.id)}/read-all`,{method:'POST'}); toast(`Marked ${r.marked} read`); await loadFeeds(true); renderFeed(); loadEntries(); }
|
||||
if(a==='rm') removeFeed(f);
|
||||
if(a==='pin'){
|
||||
|
||||
@@ -34,34 +34,42 @@ const VIEWS={
|
||||
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},
|
||||
};
|
||||
/// Feeds being checked right now, from the event stream: their rows, and the row of a folder
|
||||
/// holding one, carry a spinner.
|
||||
const scanning=new Set<string>();
|
||||
/// Feeds being checked right now, from the event stream, and since when: their rows, and the row
|
||||
/// of a folder holding one, carry a spinner.
|
||||
const scanning=new Map<string, number>();
|
||||
/// A row's spinner takes the place of its unread count, so it waits this long: most feeds are
|
||||
/// checked in a few milliseconds, and each count blinked out and back as its feed went by.
|
||||
const SPIN_AFTER=300;
|
||||
function setScanning(id: string, on: boolean){
|
||||
if(on) scanning.add(id); else scanning.delete(id);
|
||||
if(on){ scanning.set(id,Date.now()); setTimeout(paintScanning,SPIN_AFTER); } else scanning.delete(id);
|
||||
paintScanning();
|
||||
}
|
||||
function paintScanning(){
|
||||
const slow=(id: string)=>scanning.has(id)&&Date.now()-scanning.get(id)>=SPIN_AFTER;
|
||||
for(const row of $$('#feedlist .feed')){
|
||||
const id=row.dataset.id;
|
||||
const on=scanning.has(id)||S.feeds.some(c=>c.group===id&&scanning.has(c.id));
|
||||
const on=slow(id)||S.feeds.some(c=>c.group===id&&slow(c.id));
|
||||
row.classList.toggle('scanning',on);
|
||||
if(on) row.title='Checking for new items…'; else row.removeAttribute('title');
|
||||
}
|
||||
// The refresh buttons turn while what they check is being checked (#78). On <body>, because
|
||||
// a feed's page is redrawn as its items come in and would take a class on the button with it.
|
||||
const busy=id=>scanning.has(id)||S.feeds.some(c=>c.group===id&&scanning.has(c.id));
|
||||
spin('scan-this',busy(S.feed));
|
||||
spin('scan-any',S.feeds.some(f=>scanning.has(f.id)));
|
||||
// A feed's button turns on its own page only, wherever the person has gone since pressing it.
|
||||
document.body.classList.toggle('scan-this',spinFeed!==null&&spinFeed===S.feed);
|
||||
}
|
||||
/// Starts a refresh button turning at once, and stops it only after a moment with nothing being
|
||||
/// checked: a scan takes its feeds one at a time, and in the gap between two the class came off,
|
||||
/// the icon snapped back upright and started again, many times a second.
|
||||
const spinStop: Record<string, number>={};
|
||||
function spin(cls: string, on: boolean){
|
||||
clearTimeout(spinStop[cls]);
|
||||
if(on) document.body.classList.add(cls);
|
||||
else spinStop[cls]=setTimeout(()=>document.body.classList.remove(cls),600);
|
||||
/// The refresh buttons turn from the press until that scan's scan_done (#78, #233). On <body>,
|
||||
/// because a feed's page is redrawn as its items come in and would take a class on the button
|
||||
/// with it. Following the feeds being checked instead, the icon snapped back upright in the gap
|
||||
/// between one feed and the next, many times a second (#231).
|
||||
let spinFeed: string|null=null;
|
||||
function spinStart(feed: string|null){
|
||||
if(feed===null) document.body.classList.add('scan-any');
|
||||
else { spinFeed=feed; paintScanning(); }
|
||||
}
|
||||
/// `only` and `forced` from scan_done: a scheduled scan, or one limited to another feed, leaves
|
||||
/// a pressed button turning, since the scan it asked for is still queued behind it. Nothing
|
||||
/// (an error ended the job) stops them all.
|
||||
function spinEnd(done?: {only: string|null, forced: boolean}){
|
||||
if(!done||done.forced&&done.only==null) document.body.classList.remove('scan-any');
|
||||
if(!done||done.forced&&done.only===spinFeed){ spinFeed=null; paintScanning(); }
|
||||
}
|
||||
|
||||
function renderFeeds(){
|
||||
|
||||
Reference in New Issue
Block a user