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>
108 lines
3.9 KiB
Swift
108 lines
3.9 KiB
Swift
import SwiftUI
|
|
|
|
/// Full-featured list row used by BookmarksView, TagBookmarksView, and SearchView.
|
|
/// Owns podcast and episode-picker sheet state; parent owns BrowserView sheet.
|
|
struct BookmarkListRow: View {
|
|
let bookmark: Bookmark
|
|
let viewModel: BookmarksViewModel
|
|
var readingProgress: Double = 0
|
|
let onOpen: () -> Void
|
|
var onEdit: (() -> Void)? = nil
|
|
|
|
@Environment(\.openURL) private var openURL
|
|
@State private var showFullPlayer = false
|
|
@State private var episodePickerBookmark: Bookmark?
|
|
|
|
var body: some View {
|
|
BookmarkRow(
|
|
bookmark: bookmark,
|
|
readingProgress: readingProgress,
|
|
onPodcast: handlePodcast
|
|
)
|
|
.contentShape(Rectangle())
|
|
.onTapGesture { onOpen() }
|
|
.listRowInsets(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16))
|
|
.listRowSeparator(.visible)
|
|
// Delete is destructive and not undoable — require an explicit tap on the
|
|
// revealed button rather than letting a single full swipe delete instantly.
|
|
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
|
|
Button(role: .destructive) {
|
|
Task { await viewModel.delete(bookmark) }
|
|
} label: {
|
|
Label("Delete", systemImage: "trash")
|
|
}
|
|
}
|
|
.swipeActions(edge: .leading) {
|
|
Button {
|
|
Task { await viewModel.archive(bookmark) }
|
|
} label: {
|
|
Label("Archive", systemImage: "archivebox")
|
|
}
|
|
.tint(.orange)
|
|
if let onEdit {
|
|
Button { onEdit() } label: {
|
|
Label("Edit", systemImage: "pencil")
|
|
}
|
|
.tint(.blue)
|
|
}
|
|
}
|
|
.contextMenu {
|
|
if let onEdit {
|
|
Button { onEdit() } label: {
|
|
Label("Edit", systemImage: "pencil")
|
|
}
|
|
}
|
|
Button { onOpen() } label: {
|
|
Label("Open", systemImage: "globe")
|
|
}
|
|
Button {
|
|
if let url = URL(string: bookmark.url) { openURL(url) }
|
|
} label: {
|
|
Label("Open in Safari", systemImage: "safari")
|
|
}
|
|
Button { handlePodcast() } label: {
|
|
Label("Convert to Podcast", systemImage: "headphones")
|
|
}
|
|
Divider()
|
|
Button {
|
|
Task { await viewModel.archive(bookmark) }
|
|
} label: {
|
|
Label("Archive", systemImage: "archivebox")
|
|
}
|
|
Button(role: .destructive) {
|
|
Task { await viewModel.delete(bookmark) }
|
|
} label: {
|
|
Label("Delete", systemImage: "trash")
|
|
}
|
|
}
|
|
.sheet(isPresented: $showFullPlayer) {
|
|
PodcastPlayerView(
|
|
vm: viewModel.podcastPlayer,
|
|
articleUrl: viewModel.podcastPlayer.currentArticleUrl,
|
|
articleTitle: viewModel.podcastPlayer.currentArticleTitle,
|
|
claude: viewModel.claude,
|
|
stopOnDismiss: false
|
|
)
|
|
}
|
|
.sheet(item: $episodePickerBookmark) { b in
|
|
EpisodePickerView(bookmark: b, vm: viewModel.podcastPlayer, claude: viewModel.claude, podcastGenerator: viewModel.podcastGenerator)
|
|
}
|
|
}
|
|
|
|
private func handlePodcast() {
|
|
let episodes = PodcastIndex.find(for: bookmark.url)
|
|
if episodes.count >= 2 {
|
|
episodePickerBookmark = bookmark
|
|
} else if let ep = episodes.first {
|
|
viewModel.podcastPlayer.start(
|
|
articleUrl: ep.articleUrl,
|
|
articleTitle: ep.title ?? bookmark.displayTitle,
|
|
claude: viewModel.claude
|
|
)
|
|
showFullPlayer = true
|
|
} else if viewModel.playOrGeneratePodcast(articleUrl: bookmark.url, title: bookmark.displayTitle) {
|
|
showFullPlayer = true
|
|
}
|
|
}
|
|
}
|