Skip to content

[PC TV] Player ellipsis menu: mark as played / archive#4563

Open
david-gonzalez-a8c wants to merge 4 commits into
trunkfrom
player-actions
Open

[PC TV] Player ellipsis menu: mark as played / archive#4563
david-gonzalez-a8c wants to merge 4 commits into
trunkfrom
player-actions

Conversation

@david-gonzalez-a8c

@david-gonzalez-a8c david-gonzalez-a8c commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

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.

  • Mark Played and Archive confirm via alert first (matching the iOS player), because the underlying EpisodeManager calls stop playback as a side effect. Their reverses run directly.
  • After a destructive action's alert is confirmed, the view dismisses itself so any fullScreenCover presentation closes. The Now Playing tab is already swapped back to Home by MainTabRouter's existing playback observer, so no extra routing is needed.
  • All four actions are wrapped in requireAccount { … }, read from @Environment(\.requireAccount) inside NowPlayingPlayerRepresentable (where .requireAccountSupport() is applied), so logged-out users hit the create-account modal before anything runs.
  • The ellipsis popover shows only the action entries with no title header.

To test

  1. Play any episode and open the fullscreen player. Verify a new ellipsis round button appears to the right of Playback Effects.
  2. Focus it — the popover shows the entries matching the current episode's state (e.g. "Mark Played" + "Archive" for an unplayed, non-archived episode), with no title header.
  3. Tap Mark Played → confirmation alert appears → confirm → playback stops, toast "Mark as Played" shows, the player closes (fullScreenCover dismisses, the Now Playing tab swaps back to Home).
  4. Repeat for Archive → same behaviour with the archive confirmation alert.
  5. Tap Mark Unplayed / Unarchive (when the inverse state is true) → action runs immediately, toast confirms, player stays open.
  6. While logged out, tap any of the four actions → create-account modal appears instead of running the action.
  7. User-uploaded files: confirm the archive entry is hidden (no canArchive).

Checklist

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>
@dangermattic

dangermattic commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator
1 Warning
⚠️ View files have been modified, but no screenshot or video is included in the pull request. Consider adding some for clarity.

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>
@david-gonzalez-a8c david-gonzalez-a8c marked this pull request as ready for review June 18, 2026 18:52
@david-gonzalez-a8c david-gonzalez-a8c requested a review from a team as a code owner June 18, 2026 18:52
@david-gonzalez-a8c david-gonzalez-a8c requested review from Copilot and kean and removed request for a team June 18, 2026 18:52

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 NowPlayingViewModel with played/archived state accessors and action methods (mark played/unplayed, archive/unarchive).
  • Updates NowPlayingView / AVPlayerViewController transport bar to include a new custom UIMenu for 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.

Comment on lines +10 to +14
// `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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

That is the case. If you don't have an account and select on of the new options from the player, nothing happens.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 83d8e07

Comment on lines 65 to 71
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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 83d8e07

@kean kean added this to the 8.15 milestone Jun 18, 2026

@kean kean left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Comment on lines +10 to +14
// `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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Comment thread Pocket Casts TV App/UI/Player/NowPlayingView.swift
Comment thread podcasts/en.lproj/Localizable.strings Outdated
"tv_player_playback_effects" = "Playback effects";

/* tv player ellipsis menu title; sits above episode actions like Mark Played and Archive */
"tv_player_more" = "More";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

(nit) the title seems a bit redundant

Image

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 0f3aa00 — dropped the title so the popover shows just the actions.

david-gonzalez-a8c and others added 2 commits June 19, 2026 15:41
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>
@david-gonzalez-a8c

Copy link
Copy Markdown
Contributor Author

@kean Re: your review — the logged-out gating issue Copilot flagged is fixed in 83d8e07 (moved @Environment(\.requireAccount) into the representable where .requireAccountSupport() is actually applied).

@david-gonzalez-a8c

Copy link
Copy Markdown
Contributor Author

Review feedback addressed

  • NowPlayingView.swift:14 (@copilot): requireAccount env was read in NowPlayingView, where .requireAccountSupport() hadn't been applied yet, so the gated implementation was never seen — moved the @Environment(\.requireAccount) read into NowPlayingPlayerRepresentable. Done in 83d8e07.
  • NowPlayingView.swift:71 (@copilot): Same root cause as above — fixed by the same change. Done in 83d8e07.
  • Localizable.strings:6191 (@kean): "More" header above the ellipsis menu was redundant — dropped the title (and removed the unused tv_player_more string). Done in 0f3aa00.
  • Review (@kean): Logged-out gating issue you flagged is covered by the requireAccount fix above. Done in 83d8e07.

Not changed:

  • NowPlayingView.swift:18 (@kean): Animation observation during Archive from Now Playing — noted, but no actionable change requested.
  • NowPlayingView.swift:256 (@kean): "played episode in player" question — already answered inline (played episodes can still be played).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

Comment on lines +260 to +264
requireAccount {
AnalyticsEpisodeHelper.shared.currentSource = .player
model.markAsUnplayed()
ToastManager.shared.show(L10n.markUnplayedShort)
}
Comment on lines +286 to +290
requireAccount {
AnalyticsEpisodeHelper.shared.currentSource = .player
model.unarchive()
ToastManager.shared.show(L10n.unarchive)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants