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>
62 lines
2.2 KiB
Swift
62 lines
2.2 KiB
Swift
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()) }
|
|
}
|
|
}
|