diff --git a/Marks/Views/PodcastPlayerView.swift b/Marks/Views/PodcastPlayerView.swift index fb084bd..5dcb013 100644 --- a/Marks/Views/PodcastPlayerView.swift +++ b/Marks/Views/PodcastPlayerView.swift @@ -800,6 +800,7 @@ struct PodcastLibraryView: View { @State private var library = PodcastLibrary.shared @State private var showFullPlayer = false + @State private var detailEntry: PodcastEntry? var body: some View { NavigationStack { @@ -856,6 +857,9 @@ struct PodcastLibraryView: View { stopOnDismiss: false ) } + .sheet(item: $detailEntry) { entry in + EpisodeDetailView(entry: entry, vm: vm, claude: claude) + } } .onAppear { library.reload() } // When the current episode changes (finished, advanced, or stopped), @@ -896,6 +900,8 @@ struct PodcastLibraryView: View { .buttonStyle(.plain) } .padding(.vertical, 6) + .contentShape(Rectangle()) + .onTapGesture { detailEntry = entry } .swipeActions(edge: .leading) { Button { setPlayed(entry, !entry.isPlayed) @@ -922,6 +928,11 @@ struct PodcastLibraryView: View { Label(isCurrent(entry) && vm.isPlaying ? "Pause" : "Play", systemImage: isCurrent(entry) && vm.isPlaying ? "pause" : "play") } + Button { + detailEntry = entry + } label: { + Label("Episode Details", systemImage: "info.circle") + } Button { setPlayed(entry, !entry.isPlayed) } label: { @@ -970,6 +981,7 @@ struct EpisodePickerView: View { let claude: ClaudeService @State private var entries: [PodcastEntry] = [] + @State private var detailEntry: PodcastEntry? @Environment(\.dismiss) private var dismiss var body: some View { @@ -1005,6 +1017,8 @@ struct EpisodePickerView: View { .buttonStyle(.plain) } .padding(.vertical, 6) + .contentShape(Rectangle()) + .onTapGesture { detailEntry = entry } .swipeActions(edge: .trailing, allowsFullSwipe: true) { Button(role: .destructive) { deleteEntry(entry) } label: { Label("Delete", systemImage: "trash") @@ -1020,6 +1034,9 @@ struct EpisodePickerView: View { Button("Done") { dismiss() } } } + .sheet(item: $detailEntry) { entry in + EpisodeDetailView(entry: entry, vm: vm, claude: claude) + } } .onAppear { entries = PodcastIndex.find(for: bookmark.url) } } @@ -1030,3 +1047,103 @@ struct EpisodePickerView: View { if vm.currentArticleUrl == entry.articleUrl { vm.stop() } } } + +// MARK: - Episode detail + +/// Detail sheet for a single episode: reconnects the podcast to its source +/// bookmark and gathers per-episode actions (play, mark played, open source, +/// share, delete) in one place. +struct EpisodeDetailView: View { + let entry: PodcastEntry + let vm: PodcastPlayerViewModel + let claude: ClaudeService + + @Environment(\.dismiss) private var dismiss + @Environment(\.openURL) private var openURL + @State private var library = PodcastLibrary.shared + + private var isCurrent: Bool { vm.currentArticleUrl == entry.articleUrl } + private var isPlayed: Bool { + library.entries.first { $0.articleUrl == entry.articleUrl }?.isPlayed ?? entry.isPlayed + } + private var sourceURL: URL? { URL(string: entry.parentBookmarkUrl ?? entry.articleUrl) } + private var domain: String { URL(string: entry.articleUrl)?.host ?? entry.articleUrl } + + var body: some View { + NavigationStack { + List { + Section { + VStack(alignment: .leading, spacing: 8) { + Text(entry.title ?? entry.articleUrl) + .font(.system(size: 20, weight: .semibold)) + Label(domain, systemImage: "link") + .font(.subheadline) + .foregroundStyle(.secondary) + .lineLimit(1) + HStack(spacing: 12) { + Label(entry.createdAt.formatted(date: .abbreviated, time: .omitted), + systemImage: "calendar") + if isPlayed { + Label("Played", systemImage: "checkmark.circle.fill") + .foregroundStyle(.green) + } + } + .font(.caption) + .foregroundStyle(.secondary) + } + .padding(.vertical, 4) + } + + Section { + Button { + if isCurrent { + vm.togglePlayPause() + } else { + vm.start(articleUrl: entry.articleUrl, + articleTitle: entry.title ?? "", claude: claude) + } + } label: { + Label(isCurrent && vm.isPlaying ? "Pause" : "Play", + systemImage: isCurrent && vm.isPlaying ? "pause.fill" : "play.fill") + } + Button { + library.setPlayed(entry.articleUrl, !isPlayed) + } label: { + Label(isPlayed ? "Mark as Unplayed" : "Mark as Played", + systemImage: isPlayed ? "circle" : "checkmark.circle") + } + } + + if let sourceURL { + Section { + Button { + openURL(sourceURL) + } label: { + Label("Open Source", systemImage: "safari") + } + ShareLink(item: sourceURL) { + Label("Share Episode", systemImage: "square.and.arrow.up") + } + } + } + + Section { + Button(role: .destructive) { + library.remove(entry.articleUrl) + if vm.currentArticleUrl == entry.articleUrl { vm.stop() } + dismiss() + } label: { + Label("Delete Episode", systemImage: "trash") + } + } + } + .navigationTitle("Episode") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button("Done") { dismiss() } + } + } + } + } +}