All checks were successful
CI / build-and-deploy (push) Successful in 34s
Replace Button wrapper with onTapGesture so swipe gestures don't compete with the row tap recognizer and accidentally open bookmarks. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
111 lines
3.8 KiB
Swift
111 lines
3.8 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)
|
|
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
|
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)
|
|
}
|
|
}
|
|
|
|
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 {
|
|
viewModel.podcastPlayer.start(
|
|
articleUrl: bookmark.url,
|
|
articleTitle: bookmark.displayTitle,
|
|
claude: viewModel.claude
|
|
)
|
|
showFullPlayer = true
|
|
}
|
|
}
|
|
}
|