Page type-check: noImplicitAny, every parameter and variable typed (#235)
290 places the type-check could not see into, mostly parameters, now typed with web/src/types.ts (feeds, items, files, accounts, log lines, the directory's rows and items). Two changes run differently in form only: the seek and volume inputs divide Number(value) where they divided the string, and esc's table has a name. Null checks are #236. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
/* ---------------- player ---------------- */
|
||||
const audio=$('#audio');
|
||||
const player: {guid: string|null, feed: string|null, entry: any, enc?: number, src?: string, moved?: boolean,
|
||||
const player: {guid: string|null, feed: string|null, entry: Entry|null, enc?: number, src?: string, moved?: boolean,
|
||||
saveAt: number, marked: boolean}={guid:null,feed:null,entry:null,saveAt:0,marked:false};
|
||||
|
||||
/// Where a file plays from: DeadAir's playlist when the server says this person plays it
|
||||
/// shortened (#171, #181), whose times the entry is already given in, else the file itself.
|
||||
const mediaSrc=enc=>enc.deadair!=null?`/media/${enc.id}/deadair.m3u8`:`/media/${enc.id}`;
|
||||
const mediaSrc=(enc: {id: number, deadair?: number|null})=>enc.deadair!=null?`/media/${enc.id}/deadair.m3u8`:`/media/${enc.id}`;
|
||||
|
||||
/// hls.js, playing DeadAir's playlist where the browser cannot itself. Fetched the first time it
|
||||
/// is needed. Only Apple's browsers play it themselves: Chromium says it can ("maybe"), then fails
|
||||
@@ -31,7 +31,7 @@ function setSrc(url: string): Promise<void>|null{
|
||||
return ready.then(()=>{
|
||||
// Its playlist and audio requests join the trace of the tap that played them (#192).
|
||||
hls=new w.Hls({xhrSetup:(xhr: XMLHttpRequest)=>xhr.setRequestHeader('traceparent',traceparent())});
|
||||
hls.on(w.Hls.Events.ERROR,(_,d)=>{ if(d.fatal){ toast('Playback failed: '+d.details,true);
|
||||
hls.on(w.Hls.Events.ERROR,(_: string,d: {fatal: boolean, type: string, details: string})=>{ if(d.fatal){ toast('Playback failed: '+d.details,true);
|
||||
report('player_error',{msg:`hls.js: ${d.type} ${d.details}`,feed:player.feed,enclosure:player.enc}); } });
|
||||
hls.loadSource(url); hls.attachMedia(audio);
|
||||
});
|
||||
@@ -49,7 +49,7 @@ function markPlayed(){
|
||||
}
|
||||
|
||||
/// Plays one of the item's files in the player bar: the one asked for, or its first playable one.
|
||||
function play(e,enc=e.enclosures.find(isPlayable)){
|
||||
function play(e: Entry,enc=e.enclosures.find(isPlayable)){
|
||||
if(!isPlayable(enc)){
|
||||
toast(e.enclosures.some(x=>x.path) ? 'That file is not audio or video' : 'Not downloaded yet', true);
|
||||
return;
|
||||
@@ -76,17 +76,17 @@ function play(e,enc=e.enclosures.find(isPlayable)){
|
||||
player.marked=false;
|
||||
paintDead();
|
||||
}
|
||||
const go=()=>audio.play().catch(err=>toast('Playback failed: '+err.message,true));
|
||||
const go=()=>audio.play().catch((err: Error)=>toast('Playback failed: '+err.message,true));
|
||||
if(wait) wait.then(go,err=>toast('Playback failed: '+err.message,true)); else go();
|
||||
renderEntries();
|
||||
}
|
||||
function mediaSession(e,f){
|
||||
function mediaSession(e: Entry,f: Feed|undefined){
|
||||
if(!('mediaSession' in navigator)) return;
|
||||
navigator.mediaSession.metadata=new MediaMetadata({
|
||||
title:entryName(e).text, artist:f?(f.title||f.id):'', album:f?(f.title||''):'',
|
||||
artwork:(e.image||(f&&f.image))?[{src:artSrc(e.image||f.image),sizes:'512x512'}]:[],
|
||||
});
|
||||
const h={play:()=>audio.play(),pause:()=>audio.pause(),
|
||||
const h: Record<string,()=>void>={play:()=>audio.play(),pause:()=>audio.pause(),
|
||||
seekbackward:()=>audio.currentTime-=15,seekforward:()=>audio.currentTime+=30};
|
||||
for(const k in h){ try{navigator.mediaSession.setActionHandler(k as MediaSessionAction,h[k])}catch{} }
|
||||
}
|
||||
@@ -130,8 +130,8 @@ 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
|
||||
/// pane's, the toolbar's. Only the bar's used to change, so the others said play while it played.
|
||||
function syncPlayButtons(){
|
||||
const on=(guid,enc?)=>!audio.paused&&player.guid===guid&&(enc==null||player.enc===enc);
|
||||
const paint=(b,now,idle)=>{
|
||||
const on=(guid: string,enc?: number)=>!audio.paused&&player.guid===guid&&(enc==null||player.enc===enc);
|
||||
const paint=(b: HTMLElement,now: boolean,idle: string)=>{
|
||||
const label=now?'Pause':idle;
|
||||
if(b.title===label) return;
|
||||
b.title=label; b.setAttribute('aria-label',label); b.innerHTML=now?ICON.pause:ICON.play;
|
||||
@@ -160,9 +160,9 @@ audio.addEventListener('error',()=>{ if(audio.error) report('player_error',{msg:
|
||||
$('#pplay').onclick=()=>audio.paused?audio.play():audio.pause();
|
||||
$('#pback').onclick=()=>audio.currentTime-=15;
|
||||
$('#pfwd').onclick=()=>audio.currentTime+=30;
|
||||
$('#seek').oninput=e=>{const d=audio.duration;if(d)audio.currentTime=d*e.target.value/1000};
|
||||
$('#rate').onchange=e=>{audio.playbackRate=+e.target.value;localStorage.setItem('ipx.rate',e.target.value)};
|
||||
$('#vol').oninput=e=>{audio.volume=e.target.value/100;localStorage.setItem('ipx.vol',e.target.value)};
|
||||
$('#seek').oninput=(e: Event & {target: HTMLInputElement})=>{const d=audio.duration;if(d)audio.currentTime=d*Number(e.target.value)/1000};
|
||||
$('#rate').onchange=(e: Event & {target: HTMLSelectElement})=>{audio.playbackRate=+e.target.value;localStorage.setItem('ipx.rate',e.target.value)};
|
||||
$('#vol').oninput=(e: Event & {target: HTMLInputElement})=>{audio.volume=Number(e.target.value)/100;localStorage.setItem('ipx.vol',e.target.value)};
|
||||
/// DeadAir's switch beside the speed: on or off for the show that is playing. The state is in the
|
||||
/// icon's shape and aria-pressed, never a colour.
|
||||
function paintDead(){
|
||||
@@ -195,7 +195,7 @@ async function switchVersion(){
|
||||
secs:Math.floor(audio.currentTime),duration:isFinite(audio.duration)?Math.floor(audio.duration):null})}).catch(()=>{});
|
||||
player.moved=false;
|
||||
}
|
||||
const e=await api(`/api/entries/${feed}/${guid}`).catch(()=>null);
|
||||
const e: Entry|null=await api(`/api/entries/${feed}/${guid}`).catch((): null=>null);
|
||||
const enc=e&&e.enclosures.find(x=>x.id===player.enc);
|
||||
if(!enc||mediaSrc(enc)===player.src) return false;
|
||||
const was=!audio.paused;
|
||||
@@ -230,22 +230,22 @@ document.addEventListener('keydown',ev=>{
|
||||
// Feedly's keys, vim's j and k among them: a letter to move through items or feeds, g and a
|
||||
// letter to go somewhere, ? to list them. None fire with Ctrl, Alt or Cmd held, so the browser's
|
||||
// own shortcuts still work, or while a dialog is open.
|
||||
const GO={a:':all',d:':directory',l:':listening'};
|
||||
const GO: Record<string,string>={a:':all',d:':directory',l:':listening'};
|
||||
let gAt=0;
|
||||
function stepEntry(by){
|
||||
function stepEntry(by: number){
|
||||
if(VIEWS[S.feed]?.url||!S.entries.length) return;
|
||||
const i=S.entries.findIndex(x=>x.guid===S.sel);
|
||||
const e=S.entries[i<0?0:Math.min(S.entries.length-1,Math.max(0,i+by))];
|
||||
selectEntry(e,true);
|
||||
$(`#eps .ep[data-guid="${CSS.escape(e.guid)}"]`)?.scrollIntoView({block:'nearest'});
|
||||
}
|
||||
function stepFeed(by){
|
||||
function stepFeed(by: number){
|
||||
const rows=$$('#feedlist [data-id]'), i=rows.findIndex(r=>r.dataset.id===S.feed), id=rows[i+by]?.dataset.id;
|
||||
if(!id) return;
|
||||
selectFeed(id);
|
||||
$(`#feedlist [data-id="${CSS.escape(id)}"]`)?.scrollIntoView({block:'nearest'});
|
||||
}
|
||||
const KEYS={
|
||||
const KEYS: Record<string,()=>void>={
|
||||
j:()=>stepEntry(1), n:()=>stepEntry(1), k:()=>stepEntry(-1), p:()=>stepEntry(-1),
|
||||
J:()=>stepFeed(1), K:()=>stepFeed(-1),
|
||||
// The toolbar's own buttons, so a key does exactly what the click does, and nothing while
|
||||
@@ -259,7 +259,7 @@ const KEYS={
|
||||
'?':()=>keysModal(),
|
||||
g:()=>{ gAt=Date.now(); },
|
||||
};
|
||||
function typed(ev){
|
||||
function typed(ev: KeyboardEvent){
|
||||
// The second key of a g pair counts only if it follows within a second and a half.
|
||||
const pair=Date.now()-gAt<1500; gAt=0;
|
||||
const fn=pair ? (GO[ev.key]&&(()=>selectFeed(GO[ev.key])))||(ev.key==='s'&&prefsModal) : KEYS[ev.key];
|
||||
@@ -268,7 +268,7 @@ function typed(ev){
|
||||
}
|
||||
/// What ? shows: every key, grouped as Feedly's own list is.
|
||||
function keysModal(){
|
||||
const k=s=>`<kbd>${esc(s)}</kbd>`, g=c=>k('g')+' '+k(c);
|
||||
const k=(s: string)=>`<kbd>${esc(s)}</kbd>`, g=(c: string)=>k('g')+' '+k(c);
|
||||
const rows=[
|
||||
['Go to'],
|
||||
[g('a'),'All Subscriptions'],[g('d'),'Directory'],
|
||||
|
||||
Reference in New Issue
Block a user