feat(podcasts): #8 follow-ups — AI summary + in-app Open Bookmark
Resolves the two deviations from the episode detail sheet: - AI summary now shows in the detail sheet. Added a URL-keyed AISummaryStore that BookmarksViewModel populates as it enriches/loads bookmarks, so the decoupled Podcasts tab can surface a summary from just the episode URL — without recoupling to BookmarksViewModel. - "Open Bookmark" opens the in-app BrowserView as a nested sheet (generator threaded through PodcastLibraryView / EpisodePickerView), alongside the existing "Open in Safari" and Share. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -85,7 +85,7 @@ struct BookmarkListRow: View {
|
||||
)
|
||||
}
|
||||
.sheet(item: $episodePickerBookmark) { b in
|
||||
EpisodePickerView(bookmark: b, vm: viewModel.podcastPlayer, claude: viewModel.claude)
|
||||
EpisodePickerView(bookmark: b, vm: viewModel.podcastPlayer, claude: viewModel.claude, podcastGenerator: viewModel.podcastGenerator)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -299,6 +299,8 @@ final class BookmarksViewModel {
|
||||
"tags": bookmark.aiTags ?? []
|
||||
]
|
||||
UserDefaults.standard.set(store, forKey: "aiData")
|
||||
// Mirror by URL so the decoupled Podcasts tab can show summaries.
|
||||
AISummaryStore.set(bookmark.aiSummary, for: bookmark.url)
|
||||
}
|
||||
|
||||
private func restoreAIData() {
|
||||
@@ -311,6 +313,8 @@ final class BookmarksViewModel {
|
||||
if let d = store["\(list[i].id)"] {
|
||||
list[i].aiSummary = (d["summary"] as? String).flatMap { $0.isEmpty ? nil : $0 }
|
||||
list[i].aiTags = d["tags"] as? [String]
|
||||
// Keep the URL-keyed summary cache populated for existing bookmarks.
|
||||
if let summary = list[i].aiSummary { AISummaryStore.set(summary, for: list[i].url) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -797,6 +797,7 @@ struct MiniPlayerView: View {
|
||||
struct PodcastLibraryView: View {
|
||||
let vm: PodcastPlayerViewModel
|
||||
let claude: ClaudeService
|
||||
let podcastGenerator: PodcastGenerationManager
|
||||
|
||||
@State private var library = PodcastLibrary.shared
|
||||
@State private var showFullPlayer = false
|
||||
@@ -858,7 +859,7 @@ struct PodcastLibraryView: View {
|
||||
)
|
||||
}
|
||||
.sheet(item: $detailEntry) { entry in
|
||||
EpisodeDetailView(entry: entry, vm: vm, claude: claude)
|
||||
EpisodeDetailView(entry: entry, vm: vm, claude: claude, podcastGenerator: podcastGenerator)
|
||||
}
|
||||
}
|
||||
.onAppear { library.reload() }
|
||||
@@ -979,6 +980,7 @@ struct EpisodePickerView: View {
|
||||
let bookmark: Bookmark
|
||||
let vm: PodcastPlayerViewModel
|
||||
let claude: ClaudeService
|
||||
let podcastGenerator: PodcastGenerationManager
|
||||
|
||||
@State private var entries: [PodcastEntry] = []
|
||||
@State private var detailEntry: PodcastEntry?
|
||||
@@ -1035,7 +1037,7 @@ struct EpisodePickerView: View {
|
||||
}
|
||||
}
|
||||
.sheet(item: $detailEntry) { entry in
|
||||
EpisodeDetailView(entry: entry, vm: vm, claude: claude)
|
||||
EpisodeDetailView(entry: entry, vm: vm, claude: claude, podcastGenerator: podcastGenerator)
|
||||
}
|
||||
}
|
||||
.onAppear { entries = PodcastIndex.find(for: bookmark.url) }
|
||||
@@ -1057,10 +1059,12 @@ struct EpisodeDetailView: View {
|
||||
let entry: PodcastEntry
|
||||
let vm: PodcastPlayerViewModel
|
||||
let claude: ClaudeService
|
||||
let podcastGenerator: PodcastGenerationManager
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Environment(\.openURL) private var openURL
|
||||
@State private var library = PodcastLibrary.shared
|
||||
@State private var showBrowser = false
|
||||
|
||||
private var isCurrent: Bool { vm.currentArticleUrl == entry.articleUrl }
|
||||
private var isPlayed: Bool {
|
||||
@@ -1068,6 +1072,10 @@ struct EpisodeDetailView: View {
|
||||
}
|
||||
private var sourceURL: URL? { URL(string: entry.parentBookmarkUrl ?? entry.articleUrl) }
|
||||
private var domain: String { URL(string: entry.articleUrl)?.host ?? entry.articleUrl }
|
||||
private var summary: String? {
|
||||
AISummaryStore.summary(for: entry.parentBookmarkUrl ?? entry.articleUrl)
|
||||
?? AISummaryStore.summary(for: entry.articleUrl)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
@@ -1094,6 +1102,14 @@ struct EpisodeDetailView: View {
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
|
||||
if let summary {
|
||||
Section("Summary") {
|
||||
Text(summary)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
Button {
|
||||
if isCurrent {
|
||||
@@ -1116,10 +1132,15 @@ struct EpisodeDetailView: View {
|
||||
|
||||
if let sourceURL {
|
||||
Section {
|
||||
Button {
|
||||
showBrowser = true
|
||||
} label: {
|
||||
Label("Open Bookmark", systemImage: "book")
|
||||
}
|
||||
Button {
|
||||
openURL(sourceURL)
|
||||
} label: {
|
||||
Label("Open Source", systemImage: "safari")
|
||||
Label("Open in Safari", systemImage: "safari")
|
||||
}
|
||||
ShareLink(item: sourceURL) {
|
||||
Label("Share Episode", systemImage: "square.and.arrow.up")
|
||||
@@ -1144,6 +1165,15 @@ struct EpisodeDetailView: View {
|
||||
Button("Done") { dismiss() }
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showBrowser) {
|
||||
if let sourceURL {
|
||||
BrowserView(url: sourceURL,
|
||||
title: entry.title ?? domain,
|
||||
claude: claude,
|
||||
podcastPlayer: vm,
|
||||
podcastGenerator: podcastGenerator)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user