Skip to content

Carplay Homescreen and Now Playing Updates - #1713

Open
APIUM wants to merge 12 commits into
finamp-app:redesignfrom
APIUM:carplay-redesign
Open

APIUM wants to merge 12 commits into
finamp-app:redesignfrom
APIUM:carplay-redesign

Conversation

@APIUM

@APIUM APIUM commented Aug 5, 2026

Copy link
Copy Markdown
Member

Changes

Updates to Carplay including:

  • Favourite, Radio and Shuffle buttons on the Now Playing screen
  • Homescreen changes:
  • Add support for horizontal album art lists
  • Add recent queues
  • Change recently added to horiz album art list
  • Match order to main app homescreen
  • Change Now Playing screens icons to Tabler
  • Update Now Playing state workaround
  • Update album art placeholder graphics
nowplaying-buttons

<img width="800" height="480" alt="finamp-home-imagerow" src="https://github.com/user-attachments/assets/5d3594ba-49aa-4a3a
rebased-finamp-home-imagerow
-bb4c-81ea7e5d122c" />

az-letter-picker letter-list-artwork

Todo before merging

I've forked audio_service and flutter_carplay and moved them into finamp org. I will try to get these changes upstreamed but they are OK where they are for now while that occurs until the next update.

Related Issues

APIUM added 9 commits July 6, 2026 20:03
- Point flutter_carplay at our fork (upstream v1.6.3 plus Now Playing
  buttons, sfsymbol, and a shuffle state shim)
- Drop sectionIndexEnabled from the Home template, the only 1.2.11 to
  1.6.3 API break (param moved to CPListSection)
- Hide favourite and mix while offline or without a current track

Closes finamp-app#1588
- Fetch through the same home screen path as the main UI
- Show a collage of covers from each queue's next albums, falling back
  to the current track's artwork then a placeholder so tap indices
  stay aligned
- Hide the row when there is no queue history
Replace the Recently Added list rows with a single art row of album
covers, matching the Recent Queues presentation. The fetch limit
rises from 3 to 6 albums to fill the wider row, clamped to the
runtime image limit.
Finamp would lose last-playing status to other applications such as
Apple Podcasts even when it was the last playing application. Set that
it was last playing in more spots and handle play requests earlier in
app lifecycle.
- Render icon font glyphs to cached PNGs so CarPlay buttons can show
  the same Tabler icons as the main UI
- Use the main UI's radio, heart, and playback order glyphs for the
  mix, favourite, and shuffle buttons, with SF Symbols fallbacks
- Replace the native shuffle button with an image button so the icon
  matches the main UI's linear and shuffled states
Show Recently Added, then Recently Played, then Recent Queues below
the quick actions, and reuse the main UI home's Shuffle Tracks and
Surprise Me translation strings for the quick actions.
- audio_service now sets MPNowPlayingInfoCenter.playbackState on iOS,
  merged upstream via ryanheise/audio_service#1140 (0.18.20)
- Rebase the audio_service fork onto the latest upstream, dropping our
  copy of that fix and keeping the command registration and
  iosKeepNowPlayingOnStop commits
- Remove the playback_state method channel and its Dart caller

Closes finamp-app#1590
Albums and queues without artwork showed a bare SF symbol, which
CarPlay renders as a tiny glyph in the corner of the tile. Render the
main UI's album glyph placeholder onto a card coloured tile instead.
Lists push 30 items straight away then fill in the background up to the head
unit's cap, replacing one blocking 250-item fetch. Oversized views get a
letter picker, except online Tracks, see _enableOnlineTracksLetterPicker.
Needs flutter_carplay cf997048.
@APIUM APIUM changed the title Carplay redesign Carplay Homescreen and Now Playing Updates Aug 5, 2026
@Chaphasilor

Copy link
Copy Markdown
Member

This looks pretty sweet, thanks @APIUM!
I think you still need an in-car test from @UnicornsOnLSD?

We'll save this PR for the 1.1 update of Finamp, so that we can get any critical changes in before end of August and the Jellyfin 12 release, I hope that's fine with you!

@Chaphasilor Chaphasilor added the stable-version Issues related to the stable version of Finamp label Aug 15, 2026
@APIUM

APIUM commented Aug 15, 2026

Copy link
Copy Markdown
Member Author

This looks pretty sweet, thanks @APIUM! I think you still need an in-car test from @UnicornsOnLSD?

We'll save this PR for the 1.1 update of Finamp, so that we can get any critical changes in before end of August and the Jellyfin 12 release, I hope that's fine with you!

Yep no worries, definitely need to test in car. It’s super annoying not being able to do it even with a dev account! Maybe with the changes @UnicornsOnLSD did for CI builds we can extend to dev builds.

@UnicornsOnLSD

Copy link
Copy Markdown
Collaborator

Making a build now, I foolishly upgraded my Flutter and now it's freaking out about our deployment target being below iOS 15 (the new minimum :( ), currently resurrecting flutterw

@APIUM
APIUM force-pushed the carplay-redesign branch 6 times, most recently from e9fbad1 to 9a84374 Compare August 17, 2026 12:32
@APIUM

APIUM commented Aug 17, 2026

Copy link
Copy Markdown
Member Author

Making a build now, I foolishly upgraded my Flutter and now it's freaking out about our deployment target being below iOS 15 (the new minimum :( ), currently resurrecting flutterw

I accidentally left the letter picker part out :/ Everything else is working well. Would you be able to do another build please?

@APIUM
APIUM force-pushed the carplay-redesign branch from 9a84374 to 7cfce1f Compare August 17, 2026 12:47
@APIUM

APIUM commented Sep 1, 2026

Copy link
Copy Markdown
Member Author

With the latest build this is working well for me on hardware too, same as the sim - ready for post 1.0.0 review and merge.

Comment thread lib/main.dart
// whichever app had it before. Requires the audio_service fork
// overrides in pubspec.yaml (the flag spans audio_service and
// audio_service_platform_interface).
iosKeepNowPlayingOnStop: true,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this actually needed/desirable? As far as I can tell, the stopService command this is intercepting only occurs when we actually call stop() against just_audio, and we only do that on explicit user request. I would imagine if the user has stopped and cleared the queue, they don't actually want us to be taking the now-playing status like this. Or is there something more complex going on?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was because I kept finding Finamp would lose the now playing/last playing status to other apps which are grabby such as Apple's Podcast app. It appears that audio_service calls stopService on transitions to idle, so then we lose the resume. This change did fix it for me, and I've not really noticed any issues in use. I might otherwise be able to just intercept remote stops and make them a pause or something?

Comment thread ios/Runner/AppDelegate.swift Outdated
return sceneConfig
}

// UIApplicationSupportsMultipleScenes is required for CarPlay, but it

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem to have been implemented before. Why do we need it now?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've looked at this again, still working in the sim without it so I've removed

Comment thread lib/services/carplay_helper.dart Outdated
final track = _queueService.getCurrentTrack()?.baseItem;
if (track == null) return;
try {
_carPlayLogger.info("Mix button pressed, starting an instant mix from '${track.name}'");

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instant mix and similar radio mode are not quite the same thing.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed this

Comment thread lib/services/carplay_helper.dart Outdated
break;
}
scanned++;
final track = await providerRef.read(itemByIdProvider(id).future);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're only using 20 tracks, it might be faster to just batch lookup them all instead of trying to do them one at a time.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah done

Comment thread lib/services/carplay_helper.dart Outdated
if (albumId == null || !seenAlbumIds.add(albumId)) {
continue;
}
final image = await _resolveCollageTileImage(track!);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe ui.Image has native resources, so we need to make sure we reliably dispose of them once we're done. You might also want to double check if any of the other image resolving objects also need disposal.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now done, thanks for the pickup - I did

ui.Picture
ui.Image
ImageInfo
TextPainter

Comment thread lib/services/carplay_helper.dart Outdated
/// queue, falling back to the current track's own artwork, then to a
/// placeholder icon, so a missing track or missing artwork doesn't shift
/// indices out of alignment with the queue list.
Future<String> _getRecentQueueImage(FinampStorableQueueInfo info) async {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be worth moving all these image helper methods to a dedicated file 'carplay_image_helper.dart' or something to help keep the size of this file down.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

"play() received with no current item; awaiting saved-queue restore before starting playback",
);
try {
await queueService.performInitialQueueLoad();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't particularly like the idea of performInitialQueueLoad being called from other places, and I think the saved queue state should remain internal to the queue service. My recommendation would be creating an ensureQueueLoaded function in the queue service which can wait for the initial load via a completer and then load the latest queue itself if appropriate.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

/// current track or while offline.
///
/// Overlapping calls are ignored.
Future<void> _updateNowPlayingButtons() async {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you put any thought into getting the now playing buttons code upstreamed so we don't need to maintain a fork?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, they've been receptive in the past, I was planning to confirm we're happy with everything here, get it upstreamed, then come back and update to point back to them

@@ -18,11 +17,6 @@ let flutterEngine = FlutterEngine(name: "SharedEngine", project: nil, allowHeadl
flutterEngine.run()
GeneratedPluginRegistrant.register(with: flutterEngine)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you know anything about the UIScene migration? The carplay code seems to have left us in a strange, partially migrated state, and I'd like to try to bring us closer to the flutter guidelines.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fairly weak on it. I've had a look now, and I see I did make it a bit worse last time. I can do a follow up commit later to combine the plists and get this cleaned up. Keen to not expand this MR too much more - I'll make a ticket for it.

Comment thread lib/services/queue_service.dart Outdated
/// Performs the one-time startup queue restore, loading the last "latest"
/// queue into the player, paused, per [FinampSettings.autoloadLastQueueOnStartup].
/// Every caller awaits the same [Future].
Future<void> performInitialQueueLoad() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd really prefer if we keep performInitialQueueLoad being exclusively called in main(), and just add a completer if other functions want be sure it has already ran.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

The radio button replaced the queue at once, so the user never saw the
tracks first. Now it shows the suggested tracks in a list. Start Radio
replaces the queue with them. Back keeps the queue and the radio mode
setting unchanged.

If CarPlay refuses the push because its template stack is full, the
radio starts at once with the same tracks.
Start Radio replaced the whole queue, so the playing track restarted.
Now the radio tracks replace only the tracks after the current one.
The old queue is saved to Recent Queues first. The queue source becomes
the radio, and later refills use the track as the seed.
Make the queue service the only owner of the startup queue restore. Other
callers wait for it with ensureQueueLoaded or initialQueueLoaded.

Fetch the collage tracks in one request and read the cached collage before
the covers are decoded. Release each decoded image, picture and text
painter after use.

Move the CarPlay image code to carplay_image_helper.dart.
@APIUM
APIUM force-pushed the carplay-redesign branch 2 times, most recently from f95e694 to 7b7dbcb Compare September 18, 2026 02:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

stable-version Issues related to the stable version of Finamp

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants