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:
61
Marks/Services/PodcastRequests.swift
Normal file
61
Marks/Services/PodcastRequests.swift
Normal file
@@ -0,0 +1,61 @@
|
||||
import Foundation
|
||||
|
||||
/// Cross-process (main app ⇆ share extension) queue of podcast-generation
|
||||
/// requests, plus the user's "auto-generate for these tags" rule. Lives in the
|
||||
/// shared App Group so the share extension can enqueue work the main app runs —
|
||||
/// the extension itself is too short-lived to generate audio.
|
||||
enum PodcastRequests {
|
||||
private static let appGroupId = "group.com.magicive.marks"
|
||||
private static let pendingKey = "pendingPodcastRequests"
|
||||
private static let autoTagsKey = "podcastAutoTags"
|
||||
|
||||
private static var defaults: UserDefaults? { UserDefaults(suiteName: appGroupId) }
|
||||
|
||||
struct Request: Codable {
|
||||
let url: String
|
||||
let title: String
|
||||
}
|
||||
|
||||
// MARK: - Pending queue
|
||||
|
||||
/// Enqueue a request. De-duplicates on URL so repeated saves don't pile up.
|
||||
static func enqueue(url: String, title: String) {
|
||||
var list = pending()
|
||||
guard !list.contains(where: { $0.url == url }) else { return }
|
||||
list.append(Request(url: url, title: title))
|
||||
save(list)
|
||||
}
|
||||
|
||||
static func pending() -> [Request] {
|
||||
guard let data = defaults?.data(forKey: pendingKey),
|
||||
let list = try? JSONDecoder().decode([Request].self, from: data) else { return [] }
|
||||
return list
|
||||
}
|
||||
|
||||
/// Return everything pending and clear the queue.
|
||||
static func drain() -> [Request] {
|
||||
let list = pending()
|
||||
defaults?.removeObject(forKey: pendingKey)
|
||||
return list
|
||||
}
|
||||
|
||||
private static func save(_ list: [Request]) {
|
||||
guard let data = try? JSONEncoder().encode(list) else { return }
|
||||
defaults?.set(data, forKey: pendingKey)
|
||||
}
|
||||
|
||||
// MARK: - Auto-tag rule
|
||||
|
||||
/// Tags that auto-trigger podcast generation when a bookmark is saved.
|
||||
static var autoTags: [String] {
|
||||
get { defaults?.stringArray(forKey: autoTagsKey) ?? [] }
|
||||
set { defaults?.set(newValue, forKey: autoTagsKey) }
|
||||
}
|
||||
|
||||
/// Whether saving a bookmark with `tags` should auto-generate a podcast.
|
||||
static func matchesAutoTag(_ tags: [String]) -> Bool {
|
||||
let auto = Set(autoTags.map { $0.lowercased() })
|
||||
guard !auto.isEmpty else { return false }
|
||||
return tags.contains { auto.contains($0.lowercased()) }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user