feat(podcasts): played/unplayed queue, background generation, share-sheet audio
Turns the Podcasts tab into a proper podcast-app experience across three features that compose on a shared background-generation service. Background generation (#2) - New PodcastGenerationManager runs generate→poll→download off the player, so producing a new episode never stops current playback. - Headphone taps never interrupt: idle → generate-and-play in the foreground (unchanged); already playing → generate in the background, episode lands in the library. A "Generating…" banner surfaces active jobs. Played/unplayed + queue (#1) - PodcastEntry gains playedAt (old index.json decodes as unplayed). - Podcasts tab splits into Up Next / Played with a Play All queue that auto-advances; finishing marks played; swipe to toggle played state. Share-extension audio (#3) - "Create podcast" toggle on the save card, plus configurable Auto-Podcast Tags in Settings. The extension enqueues a request into the App Group; the main app drains it on launch/foreground and generates in the background. Closes #1 Closes #2 Closes #3 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -220,6 +220,7 @@ struct BrowserView: View {
|
||||
let initialTitle: String
|
||||
let claude: ClaudeService
|
||||
let podcastPlayer: PodcastPlayerViewModel
|
||||
let podcastGenerator: PodcastGenerationManager
|
||||
var onArchive: (() async -> Void)? = nil
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@@ -232,11 +233,12 @@ struct BrowserView: View {
|
||||
@State private var navForwardCount = 0
|
||||
@State private var toolbarHidden = false
|
||||
|
||||
init(url: URL, title: String, claude: ClaudeService, podcastPlayer: PodcastPlayerViewModel, onArchive: (() async -> Void)? = nil) {
|
||||
init(url: URL, title: String, claude: ClaudeService, podcastPlayer: PodcastPlayerViewModel, podcastGenerator: PodcastGenerationManager, onArchive: (() async -> Void)? = nil) {
|
||||
self.initialURL = url
|
||||
self.initialTitle = title
|
||||
self.claude = claude
|
||||
self.podcastPlayer = podcastPlayer
|
||||
self.podcastGenerator = podcastGenerator
|
||||
self.onArchive = onArchive
|
||||
self._state = State(initialValue: BrowserState(url: url))
|
||||
}
|
||||
@@ -375,13 +377,23 @@ struct BrowserView: View {
|
||||
Button {
|
||||
let currentUrl = (state.currentURL ?? initialURL).absoluteString
|
||||
let currentTitle = state.pageTitle.isEmpty ? initialTitle : state.pageTitle
|
||||
podcastPlayer.start(
|
||||
articleUrl: currentUrl,
|
||||
articleTitle: currentTitle,
|
||||
claude: claude,
|
||||
parentBookmarkUrl: initialURL.absoluteString
|
||||
)
|
||||
showPodcast = true
|
||||
// Never interrupt current playback: play-and-show only when idle,
|
||||
// otherwise generate in the background.
|
||||
if podcastPlayer.currentArticleUrl.isEmpty {
|
||||
podcastPlayer.start(
|
||||
articleUrl: currentUrl,
|
||||
articleTitle: currentTitle,
|
||||
claude: claude,
|
||||
parentBookmarkUrl: initialURL.absoluteString
|
||||
)
|
||||
showPodcast = true
|
||||
} else {
|
||||
podcastGenerator.enqueue(
|
||||
articleUrl: currentUrl,
|
||||
title: currentTitle,
|
||||
parentBookmarkUrl: initialURL.absoluteString
|
||||
)
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "headphones")
|
||||
.frame(width: 44, height: 44)
|
||||
|
||||
Reference in New Issue
Block a user