diff --git a/Marks/Services/PodcastIndex.swift b/Marks/Services/PodcastIndex.swift index 23db190..a340bcd 100644 --- a/Marks/Services/PodcastIndex.swift +++ b/Marks/Services/PodcastIndex.swift @@ -54,6 +54,10 @@ enum PodcastIndex { all().filter { $0.parentBookmarkUrl == bookmarkUrl || $0.articleUrl == bookmarkUrl } } + static func isPlayed(articleUrl: String) -> Bool { + all().first { $0.articleUrl == articleUrl }?.isPlayed ?? false + } + /// Mark an episode played (default) or back to unplayed. No-op if unknown. static func setPlayed(articleUrl: String, _ played: Bool = true) { var entries = all() diff --git a/Marks/Views/PodcastPlayerView.swift b/Marks/Views/PodcastPlayerView.swift index 58eb995..df51ba4 100644 --- a/Marks/Views/PodcastPlayerView.swift +++ b/Marks/Views/PodcastPlayerView.swift @@ -358,6 +358,13 @@ struct PodcastPlayerView: View { var stopOnDismiss: Bool = false @Environment(\.dismiss) private var dismiss + @State private var isPlayed = false + + /// The episode currently loaded in the player (follows queue advances), + /// falling back to the URL this sheet was opened with. + private var activeUrl: String { + vm.currentArticleUrl.isEmpty ? articleUrl : vm.currentArticleUrl + } var body: some View { NavigationStack { @@ -385,6 +392,10 @@ struct PodcastPlayerView: View { } .onAppear { vm.start(articleUrl: articleUrl, articleTitle: articleTitle, claude: claude) + isPlayed = PodcastIndex.isPlayed(articleUrl: activeUrl) + } + .onChange(of: vm.currentArticleUrl) { _, _ in + isPlayed = PodcastIndex.isPlayed(articleUrl: activeUrl) } .onDisappear { if stopOnDismiss { vm.stop() } @@ -495,11 +506,25 @@ struct PodcastPlayerView: View { .glassEffect(in: Capsule()) } - if let url = URL(string: articleUrl) { - ShareLink(item: url, subject: Text(vm.currentArticleTitle.isEmpty ? "Marks Podcast" : vm.currentArticleTitle)) { - Label("Share Episode", systemImage: "square.and.arrow.up") + HStack(spacing: 28) { + Button { + isPlayed.toggle() + PodcastIndex.setPlayed(articleUrl: activeUrl, isPlayed) + } label: { + Label(isPlayed ? "Mark as Unplayed" : "Mark as Played", + systemImage: isPlayed ? "checkmark.circle.fill" : "checkmark.circle") .font(.system(size: 14, weight: .medium)) - .foregroundStyle(.secondary) + .foregroundStyle(isPlayed ? Color.green : .secondary) + } + .buttonStyle(.plain) + .contentTransition(.symbolEffect(.replace)) + + if let url = URL(string: articleUrl) { + ShareLink(item: url, subject: Text(vm.currentArticleTitle.isEmpty ? "Marks Podcast" : vm.currentArticleTitle)) { + Label("Share Episode", systemImage: "square.and.arrow.up") + .font(.system(size: 14, weight: .medium)) + .foregroundStyle(.secondary) + } } } } @@ -713,6 +738,12 @@ struct PodcastLibraryView: View { .foregroundStyle(.secondary) } Spacer() + if entry.isPlayed { + Image(systemName: "checkmark.circle.fill") + .font(.system(size: 15)) + .foregroundStyle(.green) + .accessibilityLabel("Played") + } Button { play(entry) } label: { @@ -743,6 +774,29 @@ struct PodcastLibraryView: View { Label("Delete", systemImage: "trash") } } + .contextMenu { + Button { + play(entry) + } label: { + Label(isCurrent(entry) && vm.isPlaying ? "Pause" : "Play", + systemImage: isCurrent(entry) && vm.isPlaying ? "pause" : "play") + } + Button { + setPlayed(entry, !entry.isPlayed) + } label: { + if entry.isPlayed { + Label("Mark as Unplayed", systemImage: "circle") + } else { + Label("Mark as Played", systemImage: "checkmark.circle") + } + } + Divider() + Button(role: .destructive) { + deleteEntry(entry) + } label: { + Label("Delete", systemImage: "trash") + } + } } private func setPlayed(_ entry: PodcastEntry, _ played: Bool) {