iOS CarPlay: the phone's bar takes up the car's episode; only playable shows (#442, #443, #444)

#442: a car's pick told the page `replaced`, and the page closed its player bar, so leaving the
car with the episode playing left nothing on the phone to pause it. And when the car launched the
app, the page loaded later and was never told anything played. Now the app sends `nowPlaying`
(the item, its place, playing or not) on a car pick, at the page's `ready`, and on coming
forward, and the page's bar takes it up and drives the phone's player. `replaced` is still
handled for builds before this one.

#443: Shows listed every feed with items, blogs included. Now only feeds with a downloaded file,
and an episode only with a downloaded audio file: /media/{id} answers 404 before the download.

#444: tests/native-bridge.ts read its failure count before the CarPlay checks, so those could
fail and still exit 0. The count is read last now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RqkrVARreS9wxzH5o2rF7N
This commit is contained in:
2026-11-11 23:08:14 +00:00
parent a55f08d4ff
commit 6d6bfe6e1d
8 changed files with 94 additions and 16 deletions

View File

@@ -14,7 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- In the iOS app, the page registers the phone with ipx for its icon's count, and signing out removes it (#433).
- In the iOS app, the player bar closes when CarPlay starts a different episode, instead of going on as the old one (#420).
- In the iOS app, the player bar shows what CarPlay plays, so it can be paused on the phone after leaving the car, including when the app was opened by the car (#420, #442).
- In the iOS app, the icon's unread count follows the page's: reading and new items change it at once, not the next time the app is opened (#421).
- In the iOS app, presses are felt: play, pause, flag, pull to refresh, marking read and errors each give the phone's haptic for it (#430).
- A sleep timer in the player: 5 minutes to an hour, then playback fades out and pauses. In the iOS app the phone keeps the time, so it works with the screen locked; the app and Safari also get an AirPlay button. On a phone the title has the player's top line to itself, with the controls either side of play below it (#429).

View File

@@ -151,6 +151,34 @@ function installNativePlayback(){
return realRemoveAttribute(name);
}});
/// What the phone plays that the page did not choose (#442): the car's pick, or an episode already
/// playing when the page loads (the app opened from CarPlay, a reload). The bar takes it up, so it
/// can be paused here; it used to close, and the sound went on with nothing on the phone to stop it.
function adopt(m: any){
if(!(N.on && player.guid===m.guid && player.feed===m.feedId)){
// A video the element was playing for itself stops: the phone's player has the sound now.
realPause.call(audio); realRemoveAttribute('src'); realLoad.call(audio);
document.body.classList.remove('has-video');
const e=S.entries.find(x=>x.guid===m.guid&&x.feed_id===m.feedId)||null;
const enc=e?.enclosures.find(x=>x.id===m.enc);
// Not moved: nothing here played it, so there is no place of the page's own to save.
player.guid=m.guid; player.feed=m.feedId; player.entry=e; player.enc=m.enc;
player.src=enc?mediaSrc(enc):undefined; player.moved=false; player.marked=false; player.saveAt=m.cur||0;
$('#partwrap').innerHTML=artHTML(m.artwork||feedArt(m.feedId),feedName(m.feedId));
$('#ptitle').textContent=m.title||'';
$('#pfeed').textContent=m.feedTitle||'';
$('#player').classList.add('on');
N.on=true; N.paused=true; N.dur=NaN; N.ready=0;
}
N.cur=m.cur||0;
if(m.dur!=null){ N.dur=m.dur; N.ready=1; }
N.paused=!m.playing;
fire('timeupdate');
fire(m.playing ? 'play' : 'pause');
renderEntries();
syncListening();
}
function stop(){
if(!N.on) return;
N.on = false; N.paused = true; N.cur = 0; N.dur = NaN; N.ready = 0;
@@ -179,6 +207,7 @@ function installNativePlayback(){
if(m.t==='host'){ hostCan.clear(); for(const c of m.can||[]) hostCan.add(c); paintRoute(); return; }
if(m.t==='sleep'||m.t==='slept'){ hostSleep(m); return; }
if(m.t==='push'){ hostPush(m); return; }
if(m.t==='nowPlaying'){ adopt(m); return; }
if(!N.on) return;
switch(m.t){
case 'time':
@@ -201,7 +230,7 @@ function installNativePlayback(){
fire('ended');
break;
case 'replaced':
// The car chose another episode (#420). The bar would go on as this one, saving its
// From app builds before nowPlaying (#442): the car chose another episode (#420). The bar would go on as this one, saving its
// place and marking it read by the other's clock, so it closes. No pause event: that
// saves, and N.on is off, so it would write the page's stale time to ipx itself.
N.on = false; N.paused = true;