Commit ddc11633 authored by Kourser's avatar Kourser
Browse files

feat: name the show on the lock screen

Artwork alone left the panel reading as an untitled file. The show's name
now goes out as MPMediaItemPropertyArtist, which is the line iOS puts
under the title.

The injected closure returns both pieces rather than growing a second
one, and the episode-to-show lookup that artworkURL(for:) was doing
inline is now AppModel.podcast(for:), shared by the two.

Verified on an iOS 26 simulator: title, artiste=Le Collimateur, and the
artwork, all published together.

Co-Authored-By: default avatarClaude Opus 5 <noreply@anthropic.com>
parent 7f16ad05
Loading
Loading
Loading
Loading
+4 −4
Original line number Diff line number Diff line
@@ -41,10 +41,10 @@ adopte le [versionnage sémantique](https://semver.org/lang/fr/).
  nouveaux épisodes ne déclenchent plus de notification.

### Corrigé
- **Affiche sur l'écran verrouillé et le centre de contrôle** : le panneau
  « En cours de lecture » du système ne recevait que le titre, la position et la
  durée — jamais d'image. L'affiche de l'épisode (ou celle du podcast à défaut)
  y est désormais publiée, téléchargée une fois par épisode.
- **Affiche et nom du podcast sur l'écran verrouillé** : le panneau « En cours
  de lecture » du système ne recevait que le titre, la position et la durée. Il
  affiche désormais l'image de l'épisode (ou celle du podcast à défaut),
  téléchargée une fois par épisode, ainsi que le nom de l'émission sous le titre.
- **iOS — affiche manquante dans le lecteur** : la plupart des flux ne publient
  d'affiche qu'au niveau du podcast, pas par épisode. Le lecteur et la barre de
  lecture n'utilisaient que celle de l'épisode et restaient donc vides. Ils
+5 −4
Original line number Diff line number Diff line
@@ -297,11 +297,12 @@ final class AppModel {
    /// Artwork to show for an episode in cross-podcast lists (the inbox): the
    /// owning podcast's image, falling back to the episode's own image.
    func artworkURL(for episode: Episode) -> URL? {
        if let id = episode.podcastId,
           let podcast = podcasts.first(where: { $0.id == id }) {
            return podcast.imageURL ?? episode.imageURL
        podcast(for: episode)?.imageURL ?? episode.imageURL
    }
        return episode.imageURL

    /// The show an episode belongs to, when its subscription is loaded.
    func podcast(for episode: Episode) -> Podcast? {
        episode.podcastId.flatMap { id in podcasts.first { $0.id == id } }
    }

    /// Artwork for the player, the mini bar and the system Now Playing panel:
+18 −6
Original line number Diff line number Diff line
@@ -10,6 +10,13 @@ import UIKit
private typealias PlatformImage = UIImage
#endif

/// What the system panel shows around the episode itself: the show's name
/// under the title, and the picture beside it.
struct NowPlayingMetadata {
    var artworkURL: URL?
    var showTitle: String?
}

/// Bridges ``PlaybackController`` to the system: lock-screen / Control Center
/// remote commands in, Now Playing info out. Lives in the app layer because
/// `MediaPlayer` is platform-specific.
@@ -17,9 +24,9 @@ private typealias PlatformImage = UIImage
final class NowPlayingCoordinator: PlaybackObserver {
    private let controller: PlaybackController
    private let settings: PlaybackSettings
    /// Resolves the picture to publish. Injected so this stays clear of the
    /// library model, which is where the episode-then-show precedence lives.
    private let artworkURL: @MainActor (Episode) -> URL?
    /// Resolves what to show around the episode. Injected so this stays clear
    /// of the library model, which is where an episode's show is looked up.
    private let metadata: @MainActor (Episode) -> NowPlayingMetadata

    /// The artwork currently published, and the URL it was fetched from.
    /// `playbackStateDidChange` fires on every play, pause and seek, so the
@@ -31,11 +38,11 @@ final class NowPlayingCoordinator: PlaybackObserver {
    init(
        controller: PlaybackController,
        settings: PlaybackSettings,
        artworkURL: @escaping @MainActor (Episode) -> URL?
        metadata: @escaping @MainActor (Episode) -> NowPlayingMetadata
    ) {
        self.controller = controller
        self.settings = settings
        self.artworkURL = artworkURL
        self.metadata = metadata
        controller.observer = self
        configureRemoteCommands()
    }
@@ -107,6 +114,11 @@ final class NowPlayingCoordinator: PlaybackObserver {
        if controller.duration > 0 {
            info[MPMediaItemPropertyPlaybackDuration] = controller.duration
        }
        // The show's name is what the lock screen puts under the title; without
        // it the panel reads as an untitled file.
        if let showTitle = metadata(episode).showTitle {
            info[MPMediaItemPropertyArtist] = showTitle
        }
        if let artwork {
            info[MPMediaItemPropertyArtwork] = artwork
        }
@@ -117,7 +129,7 @@ final class NowPlayingCoordinator: PlaybackObserver {
    /// is silent on purpose: the lock screen simply keeps its default glyph,
    /// which is better than interrupting playback over a missing image.
    private func loadArtworkIfNeeded(for episode: Episode) {
        let url = artworkURL(episode)
        let url = metadata(episode).artworkURL
        guard url != loadedArtworkURL else { return }
        loadedArtworkURL = url
        artwork = nil
+6 −1
Original line number Diff line number Diff line
@@ -55,7 +55,12 @@ struct SkingomzApp: App {
                    nowPlaying = NowPlayingCoordinator(
                        controller: playback,
                        settings: playbackSettings,
                        artworkURL: { model.playerArtworkURL(for: $0) }
                        metadata: { episode in
                            NowPlayingMetadata(
                                artworkURL: model.playerArtworkURL(for: episode),
                                showTitle: model.podcast(for: episode)?.title
                            )
                        }
                    )
                    #if os(macOS)
                    MacAppDelegate.playback = playback