Commit 9dc91392 authored by Kourser's avatar Kourser
Browse files

feat(macos): keep the Touch Bar and the app panes in sync

A shared visiblePodcastID on the CommandCenter becomes the single
source of truth for 'which podcast page is showing':

- Sidebar selection writes it, so opening a podcast in the app switches
  the Touch Bar to that podcast's episode level (and leaving the page —
  inbox, queue, settings — returns the bar to the library strip).
- Tapping an artwork on the Touch Bar writes the same state, so the
  detail pane opens the podcast at the same time; the bar's back button
  clears it, which also closes the page in the app.
- Launching an episode from the bar shows the now-playing level while
  the app stays on the podcast page; the synced id is forgotten so
  re-tapping the same artwork drills in again. Radios keep playing
  directly. Both directions are change-guarded, so no update loops.

Co-Authored-By: Claude (RCA)
parent 8590ac30
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -12,4 +12,8 @@ final class CommandCenter {
    /// Podcast picked outside the view hierarchy (Touch Bar scrubber):
    /// RootView opens it (or plays it, for a radio) then clears the value.
    var openPodcastID: UUID?
    /// The podcast whose page is showing, shared source of truth keeping the
    /// app's detail pane and the Touch Bar level in sync (both sides write it,
    /// both react to it). `nil` when no podcast page is visible.
    var visiblePodcastID: UUID?
}
+18 −0
Original line number Diff line number Diff line
@@ -106,6 +106,24 @@ struct RootView: View {
            }
            commands.openPodcastID = nil
        }
        // Keep the shared "visible podcast" in sync, both directions: the
        // sidebar drives the Touch Bar level, and the Touch Bar (artwork tap,
        // back button) drives the detail pane.
        .onChange(of: selection) { _, newSelection in
            var id: UUID?
            if case .podcast(let podcastID) = newSelection { id = podcastID }
            if commands.visiblePodcastID != id { commands.visiblePodcastID = id }
        }
        .onChange(of: commands.visiblePodcastID) { _, id in
            var currentID: UUID?
            if case .podcast(let podcastID) = selection { currentID = podcastID }
            guard id != currentID else { return }
            if let id, model.podcasts.first(where: { $0.id == id })?.isRadio == false {
                selection = .podcast(id)
            } else if id == nil, case .podcast = selection {
                selection = nil
            }
        }
        .sheet(isPresented: $commands.showAddPodcast) { AddPodcastView() }
        .sheet(isPresented: $commands.showAddRadio) { AddRadioView() }
        .sheet(isPresented: $showingSearch) { SearchView() }
+30 −2
Original line number Diff line number Diff line
@@ -39,6 +39,8 @@ final class TouchBarController: NSObject {
    private var artwork: [UUID: NSImage] = [:]
    private var artworkRequests: Set<UUID> = []
    private var hadEpisode = false
    /// Last `commands.visiblePodcastID` applied to the bar's mode.
    private var syncedPodcastID: UUID?

    private static let podcastCell = NSUserInterfaceItemIdentifier("podcast-artwork")
    private static let episodeCell = NSUserInterfaceItemIdentifier("episode-title")
@@ -84,6 +86,7 @@ final class TouchBarController: NSObject {

    /// Drill-down: shows the latest episodes of `podcast`.
    private func showEpisodes(of podcast: Podcast) {
        if case .episodes(let current) = mode, current.id == podcast.id { return }
        mode = .episodes(podcast)
        episodes = []
        episodeScrubber?.reloadData()
@@ -98,6 +101,12 @@ final class TouchBarController: NSObject {
    }

    @objc private func backToLibrary() {
        // Routed through the shared state so the app's detail pane follows.
        commands.visiblePodcastID = nil
    }

    private func returnToLibrary() {
        guard case .episodes = mode else { return }
        mode = .library
        episodes = []
        applyMode()
@@ -112,6 +121,7 @@ final class TouchBarController: NSObject {
            _ = playback.currentTime
            _ = playback.duration
            _ = model.podcasts
            _ = commands.visiblePodcastID
        } onChange: { [weak self] in
            Task { @MainActor [weak self] in
                guard let self else { return }
@@ -143,6 +153,20 @@ final class TouchBarController: NSObject {
            if case .library = mode { applyMode() }
        }

        // Follow the app's detail pane (and our own taps, routed through the
        // same shared state): podcast page visible -> its episode level.
        let visible = commands.visiblePodcastID
        if visible != syncedPodcastID {
            syncedPodcastID = visible
            if let visible,
               let podcast = podcasts.first(where: { $0.id == visible }),
               !podcast.isRadio {
                showEpisodes(of: podcast)
            } else {
                returnToLibrary()
            }
        }

        if podcasts.map(\.id) != model.podcasts.map(\.id) {
            podcasts = model.podcasts
            podcastScrubber?.reloadData()
@@ -282,14 +306,18 @@ extension TouchBarController: NSScrubberDataSource, NSScrubberDelegate, NSScrubb
            guard episodes.indices.contains(index) else { return }
            let episode = episodes[index]
            Task { await model.play(episode) }
            backToLibrary()
            // Show the now-playing level; the app stays on the podcast page.
            // Forget the synced id so re-tapping the same artwork drills in.
            returnToLibrary()
            syncedPodcastID = nil
        } else {
            guard podcasts.indices.contains(index) else { return }
            let podcast = podcasts[index]
            if podcast.isRadio {
                commands.openPodcastID = podcast.id // plays the station
            } else {
                showEpisodes(of: podcast)
                // Shared state: opens the page in the app AND our episode level.
                commands.visiblePodcastID = podcast.id
            }
        }
    }