feat(podcasts): surface mark-played in context menu, full player, and row badge
- Add a long-press context menu on each library row (Play/Pause, Mark as Played/Unplayed, Delete). - Add a Mark as Played/Unplayed control next to Share Episode in the full player, tracking the loaded episode across queue advances. - Show a green checkmark on played rows, on top of the existing Played section + dimmed title, so played state reads at a glance. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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,6 +506,19 @@ struct PodcastPlayerView: View {
|
||||
.glassEffect(in: Capsule())
|
||||
}
|
||||
|
||||
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(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")
|
||||
@@ -504,6 +528,7 @@ struct PodcastPlayerView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func failedView(message: String) -> some View {
|
||||
VStack(spacing: 16) {
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user