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:
@@ -18,6 +18,7 @@ final class BookmarksViewModel {
|
||||
private let api: LinkdingAPI
|
||||
let claude = ClaudeService()
|
||||
let podcastPlayer = PodcastPlayerViewModel()
|
||||
let podcastGenerator = PodcastGenerationManager()
|
||||
private var enrichTask: Task<Void, Never>?
|
||||
private let cacheFileUrl: URL
|
||||
|
||||
@@ -29,6 +30,32 @@ final class BookmarksViewModel {
|
||||
self.cacheFileUrl = dir.appendingPathComponent("\(name).json")
|
||||
}
|
||||
|
||||
/// True while the player is showing an episode (playing or generating one).
|
||||
var isPlayerBusy: Bool { !podcastPlayer.currentArticleUrl.isEmpty }
|
||||
|
||||
/// Handle a headphone tap without ever interrupting current playback.
|
||||
/// If the player is idle, generate-and-play in the foreground and return
|
||||
/// `true` (caller should present the full player). If the player is busy,
|
||||
/// generate in the background — the finished episode lands in the library —
|
||||
/// and return `false`.
|
||||
@discardableResult
|
||||
func playOrGeneratePodcast(articleUrl: String, title: String, parentBookmarkUrl: String? = nil) -> Bool {
|
||||
if isPlayerBusy {
|
||||
podcastGenerator.enqueue(articleUrl: articleUrl, title: title, parentBookmarkUrl: parentBookmarkUrl)
|
||||
return false
|
||||
}
|
||||
podcastPlayer.start(articleUrl: articleUrl, articleTitle: title, claude: claude, parentBookmarkUrl: parentBookmarkUrl)
|
||||
return true
|
||||
}
|
||||
|
||||
/// Drain podcast requests queued by the share extension and start generating
|
||||
/// them in the background. Safe to call repeatedly (the queue is cleared).
|
||||
func processPendingPodcastRequests() {
|
||||
for req in PodcastRequests.drain() {
|
||||
podcastGenerator.enqueue(articleUrl: req.url, title: req.title)
|
||||
}
|
||||
}
|
||||
|
||||
func load(useCache: Bool = true) async {
|
||||
if useCache { loadFromCache() }
|
||||
isLoading = true
|
||||
|
||||
Reference in New Issue
Block a user