feat(podcasts): played/unplayed queue, background generation, share-sheet audio #4

Merged
admin merged 5 commits from feat/podcast-app-experience into master 2026-07-01 18:18:57 +00:00
2 changed files with 62 additions and 4 deletions
Showing only changes of commit 5e69d99996 - Show all commits

View File

@@ -54,6 +54,10 @@ enum PodcastIndex {
all().filter { $0.parentBookmarkUrl == bookmarkUrl || $0.articleUrl == bookmarkUrl } 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. /// Mark an episode played (default) or back to unplayed. No-op if unknown.
static func setPlayed(articleUrl: String, _ played: Bool = true) { static func setPlayed(articleUrl: String, _ played: Bool = true) {
var entries = all() var entries = all()

View File

@@ -358,6 +358,13 @@ struct PodcastPlayerView: View {
var stopOnDismiss: Bool = false var stopOnDismiss: Bool = false
@Environment(\.dismiss) private var dismiss @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 { var body: some View {
NavigationStack { NavigationStack {
@@ -385,6 +392,10 @@ struct PodcastPlayerView: View {
} }
.onAppear { .onAppear {
vm.start(articleUrl: articleUrl, articleTitle: articleTitle, claude: claude) vm.start(articleUrl: articleUrl, articleTitle: articleTitle, claude: claude)
isPlayed = PodcastIndex.isPlayed(articleUrl: activeUrl)
}
.onChange(of: vm.currentArticleUrl) { _, _ in
isPlayed = PodcastIndex.isPlayed(articleUrl: activeUrl)
} }
.onDisappear { .onDisappear {
if stopOnDismiss { vm.stop() } if stopOnDismiss { vm.stop() }
@@ -495,6 +506,19 @@ struct PodcastPlayerView: View {
.glassEffect(in: Capsule()) .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) { if let url = URL(string: articleUrl) {
ShareLink(item: url, subject: Text(vm.currentArticleTitle.isEmpty ? "Marks Podcast" : vm.currentArticleTitle)) { ShareLink(item: url, subject: Text(vm.currentArticleTitle.isEmpty ? "Marks Podcast" : vm.currentArticleTitle)) {
Label("Share Episode", systemImage: "square.and.arrow.up") Label("Share Episode", systemImage: "square.and.arrow.up")
@@ -504,6 +528,7 @@ struct PodcastPlayerView: View {
} }
} }
} }
}
private func failedView(message: String) -> some View { private func failedView(message: String) -> some View {
VStack(spacing: 16) { VStack(spacing: 16) {
@@ -713,6 +738,12 @@ struct PodcastLibraryView: View {
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
Spacer() Spacer()
if entry.isPlayed {
Image(systemName: "checkmark.circle.fill")
.font(.system(size: 15))
.foregroundStyle(.green)
.accessibilityLabel("Played")
}
Button { Button {
play(entry) play(entry)
} label: { } label: {
@@ -743,6 +774,29 @@ struct PodcastLibraryView: View {
Label("Delete", systemImage: "trash") 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) { private func setPlayed(_ entry: PodcastEntry, _ played: Bool) {