feat(podcasts): played/unplayed queue, background generation, share-sheet audio #4
@@ -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()
|
||||||
|
|||||||
@@ -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,11 +506,25 @@ struct PodcastPlayerView: View {
|
|||||||
.glassEffect(in: Capsule())
|
.glassEffect(in: Capsule())
|
||||||
}
|
}
|
||||||
|
|
||||||
if let url = URL(string: articleUrl) {
|
HStack(spacing: 28) {
|
||||||
ShareLink(item: url, subject: Text(vm.currentArticleTitle.isEmpty ? "Marks Podcast" : vm.currentArticleTitle)) {
|
Button {
|
||||||
Label("Share Episode", systemImage: "square.and.arrow.up")
|
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))
|
.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)
|
.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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user