import AppIntents // Siri and Shortcuts (#427). Each is an App Intent, and IPXShortcuts below gives the common ones // phrases that work with nothing set up: "Resume iPX", "Pause iPX", "How many unread in iPX". // The playback ones are AudioPlaybackIntents, which iOS runs in the app and lets start audio // from the background, so they work with the phone locked and the app closed. struct ResumeIntent: AudioPlaybackIntent { static let title: LocalizedStringResource = "Resume Listening" static let description = IntentDescription( "Carries on with what iPX was playing, or the last episode, from where you left off.") @MainActor func perform() async throws -> some IntentResult { guard Playback.shared.resumeLast() else { throw NothingToResume() } return .result() } } /// What Siri says when there is no episode to carry on with. struct NothingToResume: Error, CustomLocalizedStringResourceConvertible { var localizedStringResource: LocalizedStringResource { "There is nothing to resume. Play an episode in iPX first." } } struct PauseIntent: AudioPlaybackIntent { static let title: LocalizedStringResource = "Pause" static let description = IntentDescription("Pauses what iPX is playing.") @MainActor func perform() async throws -> some IntentResult { Playback.shared.pause() return .result() } } struct SkipForwardIntent: AudioPlaybackIntent { static let title: LocalizedStringResource = "Skip Forward" static let description = IntentDescription("Skips 30 seconds ahead, as the player's button does.") @MainActor func perform() async throws -> some IntentResult { Playback.shared.seek(to: Playback.shared.currentTime + 30) return .result() } } struct SkipBackIntent: AudioPlaybackIntent { static let title: LocalizedStringResource = "Skip Back" static let description = IntentDescription("Goes back 15 seconds, as the player's button does.") @MainActor func perform() async throws -> some IntentResult { Playback.shared.seek(to: Playback.shared.currentTime - 15) return .result() } } struct SleepTimerIntent: AudioPlaybackIntent { static let title: LocalizedStringResource = "Set Sleep Timer" static let description = IntentDescription( "Pauses iPX after a number of minutes, fading out over the last ten seconds. Zero turns it off.") @Parameter(title: "Minutes", default: 30, inclusiveRange: (0, 600), requestValueDialog: "For how many minutes?") var minutes: Int @MainActor func perform() async throws -> some IntentResult & ProvidesDialog { Playback.shared.sleep(after: Double(minutes) * 60) return .result(dialog: minutes == 0 ? "The sleep timer is off." : "iPX will pause in \(minutes) minutes.") } } struct UnreadIntent: AppIntent { static let title: LocalizedStringResource = "Unread Count" static let description = IntentDescription("How many items are unread in All Subscriptions.") func perform() async throws -> some IntentResult & ReturnsValue & ProvidesDialog { guard let n = await Badge.fetch() else { return .result(value: 0, dialog: "iPX could not reach your server.") } await MainActor.run { Badge.set(n) } return .result(value: n, dialog: n == 0 ? "Nothing is unread." : "\(n) unread in All Subscriptions.") } } struct IPXShortcuts: AppShortcutsProvider { static var appShortcuts: [AppShortcut] { AppShortcut(intent: ResumeIntent(), phrases: ["Resume \(.applicationName)", "Play \(.applicationName)", "Carry on listening in \(.applicationName)"], shortTitle: "Resume", systemImageName: "play.fill") AppShortcut(intent: PauseIntent(), phrases: ["Pause \(.applicationName)"], shortTitle: "Pause", systemImageName: "pause.fill") AppShortcut(intent: SkipForwardIntent(), phrases: ["Skip forward in \(.applicationName)", "Skip ahead in \(.applicationName)"], shortTitle: "Skip Forward", systemImageName: "goforward.30") AppShortcut(intent: SkipBackIntent(), phrases: ["Skip back in \(.applicationName)"], shortTitle: "Skip Back", systemImageName: "gobackward.15") AppShortcut(intent: SleepTimerIntent(), phrases: ["Set a sleep timer in \(.applicationName)", "\(.applicationName) sleep timer"], shortTitle: "Sleep Timer", systemImageName: "moon.zzz") AppShortcut(intent: UnreadIntent(), phrases: ["How many unread in \(.applicationName)", "What's new in \(.applicationName)"], shortTitle: "Unread", systemImageName: "tray.full") } }