diff --git a/Marks/Intents/IntentSupport.swift b/Marks/Intents/IntentSupport.swift index 878120a..7b071f9 100644 --- a/Marks/Intents/IntentSupport.swift +++ b/Marks/Intents/IntentSupport.swift @@ -3,7 +3,7 @@ import AppIntents /// Which top-level tab the app is showing. Used so an intent can switch tabs. enum AppTab: Hashable { - case bookmarks, tags, search + case bookmarks, tags, podcasts, search } /// Bridges App Intents (which run in the main app process, since there is no diff --git a/Marks/MarksApp.swift b/Marks/MarksApp.swift index 0dea39c..e6a6818 100644 --- a/Marks/MarksApp.swift +++ b/Marks/MarksApp.swift @@ -55,6 +55,9 @@ struct MainContainer: View { Tab("Tags", systemImage: "tag", value: AppTab.tags) { TagsView(viewModel: viewModel) } + Tab("Podcasts", systemImage: "headphones", value: AppTab.podcasts) { + PodcastLibraryView(vm: viewModel.podcastPlayer, claude: viewModel.claude) + } Tab(value: AppTab.search, role: .search) { SearchView(viewModel: viewModel) } diff --git a/Marks/Views/BookmarksView.swift b/Marks/Views/BookmarksView.swift index 8053b19..a2ccb98 100644 --- a/Marks/Views/BookmarksView.swift +++ b/Marks/Views/BookmarksView.swift @@ -58,7 +58,6 @@ struct BookmarksView: View { @State private var editingBookmark: Bookmark? @State private var browsingBookmark: Bookmark? @State private var showFullPlayer = false - @State private var showPodcastLibrary = false @State private var showAsk = false @State private var readingProgress: [String: Double] = ReadingProgress.all() @@ -112,11 +111,6 @@ struct BookmarksView: View { } label: { Label("Enrich All with AI", systemImage: "wand.and.stars") } - Button { - showPodcastLibrary = true - } label: { - Label("Podcasts", systemImage: "headphones") - } } label: { Image(systemName: "sparkles") } @@ -202,9 +196,6 @@ struct BookmarksView: View { stopOnDismiss: false ) } - .sheet(isPresented: $showPodcastLibrary) { - PodcastLibraryView(vm: viewModel.podcastPlayer, claude: viewModel.claude) - } .refreshable { await viewModel.load() } diff --git a/Marks/Views/PodcastPlayerView.swift b/Marks/Views/PodcastPlayerView.swift index 2890df6..6b44100 100644 --- a/Marks/Views/PodcastPlayerView.swift +++ b/Marks/Views/PodcastPlayerView.swift @@ -597,12 +597,15 @@ struct MiniPlayerView: View { // MARK: - Podcast library +/// The Podcasts tab: lists every podcast generated in the app (from +/// `PodcastIndex`) and plays them on demand through the shared player, with a +/// now-playing bar that opens the full player. struct PodcastLibraryView: View { let vm: PodcastPlayerViewModel let claude: ClaudeService @State private var entries: [PodcastEntry] = [] - @Environment(\.dismiss) private var dismiss + @State private var showFullPlayer = false var body: some View { NavigationStack { @@ -611,7 +614,7 @@ struct PodcastLibraryView: View { ContentUnavailableView( "No Podcasts", systemImage: "headphones", - description: Text("Podcasts you generate will appear here.") + description: Text("Podcasts you generate from bookmarks will appear here.") ) } else { List { @@ -627,14 +630,12 @@ struct PodcastLibraryView: View { } Spacer() Button { - vm.start(articleUrl: entry.articleUrl, - articleTitle: entry.title ?? "", - claude: claude) - dismiss() + play(entry) } label: { - Image(systemName: "play.circle.fill") + Image(systemName: isCurrent(entry) && vm.isPlaying ? "pause.circle.fill" : "play.circle.fill") .font(.system(size: 36)) .foregroundStyle(.blue) + .contentTransition(.symbolEffect(.replace)) } .buttonStyle(.plain) } @@ -655,16 +656,45 @@ struct PodcastLibraryView: View { } } .navigationTitle("Podcasts") - .navigationBarTitleDisplayMode(.inline) .toolbar { - ToolbarItem(placement: .topBarTrailing) { - Button("Done") { dismiss() } + if !entries.isEmpty { + ToolbarItem(placement: .topBarTrailing) { EditButton() } } } + .overlay(alignment: .bottom) { + if !vm.currentArticleUrl.isEmpty { + MiniPlayerView(vm: vm) { showFullPlayer = true } + .padding(.horizontal) + .padding(.bottom, 8) + .transition(.move(edge: .bottom).combined(with: .opacity)) + } + } + .animation(.spring(duration: 0.3), value: vm.currentArticleUrl.isEmpty) + .sheet(isPresented: $showFullPlayer) { + PodcastPlayerView( + vm: vm, + articleUrl: vm.currentArticleUrl, + articleTitle: vm.currentArticleTitle, + claude: claude, + stopOnDismiss: false + ) + } } .onAppear { entries = PodcastIndex.all() } } + private func isCurrent(_ entry: PodcastEntry) -> Bool { + vm.currentArticleUrl == entry.articleUrl + } + + private func play(_ entry: PodcastEntry) { + if isCurrent(entry) { + vm.togglePlayPause() + } else { + vm.start(articleUrl: entry.articleUrl, articleTitle: entry.title ?? "", claude: claude) + } + } + private func deleteEntry(_ entry: PodcastEntry) { PodcastIndex.remove(articleUrl: entry.articleUrl) entries.removeAll { $0.articleUrl == entry.articleUrl }