[PC TV] Player ellipsis menu: mark as played / archive#4563
[PC TV] Player ellipsis menu: mark as played / archive#4563david-gonzalez-a8c wants to merge 4 commits into
Conversation
Adds a "More" menu on the fullscreen player's transport bar (third
round button after Playback Speed and Playback Effects) holding the
two state-aware episode actions:
- Mark Played / Mark Unplayed
- Archive / Unarchive
Mirroring the iOS player, Mark Played and Archive both confirm via
alert first ("Mark this episode as played?" / "Archive this
episode?") because the underlying EpisodeManager calls stop the
current playback as a side effect. Their reverses (Mark Unplayed /
Unarchive) run directly.
After confirming a destructive action the view dismisses itself, so
fullScreenCover presentations close immediately; the tab variant is
already swapped back to Home by MainTabRouter's existing playback
observer.
All four actions are gated by `requireAccount`, so logged-out users
hit the create-account modal before anything runs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Generated by 🚫 Danger |
Self-review follow-up: every EpisodeManager invocation funnels through AnalyticsEpisodeHelper, which reads the last value written to `currentSource`. Without an explicit assignment from the player path, those events inherited whatever source the previous caller had set — usually `.unknown`. Set `currentSource = .player` before each markAsPlayed/markAsUnplayed/archive/unarchive call so the events match what the iOS player records. Also unify the played-side toast to L10n.markPlayedShort so it's symmetric with the unplayed-side toast (which was already short). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds a new “More” (ellipsis) transport-bar menu to the tvOS fullscreen player that exposes episode actions (Mark Played/Unplayed and Archive/Unarchive), including confirmation alerts for the destructive variants and dismissal behavior after those actions.
Changes:
- Adds a new localized title for the tvOS player ellipsis menu (“More”).
- Extends
NowPlayingViewModelwith played/archived state accessors and action methods (mark played/unplayed, archive/unarchive). - Updates
NowPlayingView/AVPlayerViewControllertransport bar to include a new customUIMenufor episode actions, plus confirmation alerts for Mark Played and Archive.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| podcasts/en.lproj/Localizable.strings | Adds tv_player_more string used as the ellipsis menu title. |
| Pocket Casts TV App/UI/Player/NowPlayingViewModel.swift | Adds state + action helpers used by the new ellipsis menu entries. |
| Pocket Casts TV App/UI/Player/NowPlayingView.swift | Adds the “More” transport-bar menu, account-gated action handling, and confirmation alerts/dismissal behavior. |
| // `NowPlayingView` is hosted inside a `fullScreenCover`, which doesn't | ||
| // inherit the requireAccount environment from its presenter — read it | ||
| // back here so the ellipsis-menu actions can gate themselves. | ||
| @Environment(\.requireAccount) private var requireAccount | ||
| // Mark Played and Archive both stop playback, which leaves the player |
There was a problem hiding this comment.
That is the case. If you don't have an account and select on of the new options from the player, nothing happens.
| private struct NowPlayingPlayerRepresentable: UIViewControllerRepresentable { | ||
| @Bindable var model: NowPlayingViewModel | ||
| @Binding var isShowingDescription: Bool | ||
| @Binding var isShowingMarkAsPlayedConfirmation: Bool | ||
| @Binding var isShowingArchiveConfirmation: Bool | ||
| let requireAccount: RequireAccountAction | ||
| @State private var isTransportBarVisible = true |
kean
left a comment
There was a problem hiding this comment.
I tested multiple scenarios including "Now Playing" and the "Player", which are different environments, and both worked well. There is a one issue with logged-out experience that Copilot also flagged.
| // `NowPlayingView` is hosted inside a `fullScreenCover`, which doesn't | ||
| // inherit the requireAccount environment from its presenter — read it | ||
| // back here so the ellipsis-menu actions can gate themselves. | ||
| @Environment(\.requireAccount) private var requireAccount | ||
| // Mark Played and Archive both stop playback, which leaves the player |
There was a problem hiding this comment.
That is the case. If you don't have an account and select on of the new options from the player, nothing happens.
| // empty. Dismiss closes the fullScreenCover variants; the Now Playing | ||
| // *tab* is separately swapped back to Home by `MainTabRouter`'s | ||
| // existing playback observer. | ||
| @Environment(\.dismiss) private var dismiss |
There was a problem hiding this comment.
I tested the scenario where you are on "Now Playing", use "Archive" and the queue has more items or the queue is empty. It was a bit abrupt in terms of animations, but it worked ok. For the "no items" scenarios, it removed the "Now Playing" tab and returned me to "Home".
Screen.Recording.2026-06-18.at.5.39.33.PM.mov
| "tv_player_playback_effects" = "Playback effects"; | ||
|
|
||
| /* tv player ellipsis menu title; sits above episode actions like Mark Played and Archive */ | ||
| "tv_player_more" = "More"; |
There was a problem hiding this comment.
Done in 0f3aa00 — dropped the title so the popover shows just the actions.
Move @Environment(\.requireAccount) into NowPlayingPlayerRepresentable, where .requireAccountSupport() is actually applied. Reading it in the parent NowPlayingView resolved to the default no-op (actions run immediately) since the modifier installs the gated implementation on its content's environment, not its parent's. Co-Authored-By: Claude <noreply@anthropic.com>
The popover header showed "More" above Mark Played / Archive, repeating what the ellipsis icon already implies. Drop the title (and the unused tv_player_more string) so the popover only shows the actions. Co-Authored-By: Claude <noreply@anthropic.com>
Review feedback addressed
Not changed: |
| requireAccount { | ||
| AnalyticsEpisodeHelper.shared.currentSource = .player | ||
| model.markAsUnplayed() | ||
| ToastManager.shared.show(L10n.markUnplayedShort) | ||
| } |
| requireAccount { | ||
| AnalyticsEpisodeHelper.shared.currentSource = .player | ||
| model.unarchive() | ||
| ToastManager.shared.show(L10n.unarchive) | ||
| } |

Fixes PCIOS-757
Summary
Adds a third More round button to the fullscreen player's transport bar (after Playback Speed and Playback Effects) holding two state-aware episode actions: Mark Played / Mark Unplayed and Archive / Unarchive.
EpisodeManagercalls stop playback as a side effect. Their reverses run directly.fullScreenCoverpresentation closes. The Now Playing tab is already swapped back to Home byMainTabRouter's existing playback observer, so no extra routing is needed.requireAccount { … }, read from@Environment(\.requireAccount)insideNowPlayingPlayerRepresentable(where.requireAccountSupport()is applied), so logged-out users hit the create-account modal before anything runs.To test
canArchive).Checklist
CHANGELOG.mdif necessary.