Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,12 @@ public struct UpNextItem {
public var title: String?
public var url: String
public var published: Date

public init(podcastUuid: String, episodeUuid: String, title: String? = nil, url: String, published: Date) {
self.podcastUuid = podcastUuid
self.episodeUuid = episodeUuid
self.title = title
self.url = url
self.published = published
}
}
90 changes: 90 additions & 0 deletions podcasts.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

84 changes: 81 additions & 3 deletions podcasts/Base.lproj/Intents.intentdefinition
Original file line number Diff line number Diff line change
Expand Up @@ -53,28 +53,47 @@
<key>INIntentDefinitionNamespace</key>
<string>DOik2X</string>
<key>INIntentDefinitionSystemVersion</key>
<string>21A559</string>
<string>24F74</string>
<key>INIntentDefinitionToolsBuildVersion</key>
<string>13A1030d</string>
<string>17A5241e</string>
<key>INIntentDefinitionToolsVersion</key>
<string>13.1</string>
<string>26.0</string>
<key>INIntents</key>
<array>
<dict>
<key>INIntentCategory</key>
<string>generic</string>
<key>INIntentClassPrefix</key>
<string>SJ</string>
<key>INIntentConfigurable</key>
<true/>
<key>INIntentDescription</key>
<string>Skip chapter</string>
<key>INIntentDescriptionID</key>
<string>dBTobY</string>
<key>INIntentLastParameterTag</key>
<integer>7</integer>
<key>INIntentManagedParameterCombinations</key>
<dict>
<key></key>
<dict>
<key>INIntentParameterCombinationSupportsBackgroundExecution</key>
<true/>
<key>INIntentParameterCombinationUpdatesLinked</key>
<true/>
</dict>
</dict>
<key>INIntentName</key>
<string>Chapter</string>
<key>INIntentParameterCombinations</key>
<dict>
<key></key>
<dict>
<key>INIntentParameterCombinationIsLinked</key>
<true/>
<key>INIntentParameterCombinationSupportsBackgroundExecution</key>
<true/>
</dict>
<key>skipForward</key>
<dict>
<key>INIntentParameterCombinationIsPrimary</key>
Expand Down Expand Up @@ -161,16 +180,35 @@
<string>set</string>
<key>INIntentClassPrefix</key>
<string>SJ</string>
<key>INIntentConfigurable</key>
<true/>
<key>INIntentDescription</key>
<string>Set Sleep Timer</string>
<key>INIntentDescriptionID</key>
<string>CXbd65</string>
<key>INIntentLastParameterTag</key>
<integer>2</integer>
<key>INIntentManagedParameterCombinations</key>
<dict>
<key></key>
<dict>
<key>INIntentParameterCombinationSupportsBackgroundExecution</key>
<true/>
<key>INIntentParameterCombinationUpdatesLinked</key>
<true/>
</dict>
</dict>
<key>INIntentName</key>
<string>SleepTimer</string>
<key>INIntentParameterCombinations</key>
<dict>
<key></key>
<dict>
<key>INIntentParameterCombinationIsLinked</key>
<true/>
<key>INIntentParameterCombinationSupportsBackgroundExecution</key>
<true/>
</dict>
<key>minutes</key>
<dict>
<key>INIntentParameterCombinationIsPrimary</key>
Expand Down Expand Up @@ -248,16 +286,35 @@
<string>set</string>
<key>INIntentClassPrefix</key>
<string>SJ</string>
<key>INIntentConfigurable</key>
<true/>
<key>INIntentDescription</key>
<string>Extend Sleep Timer</string>
<key>INIntentDescriptionID</key>
<string>EwNFJ5</string>
<key>INIntentLastParameterTag</key>
<integer>2</integer>
<key>INIntentManagedParameterCombinations</key>
<dict>
<key></key>
<dict>
<key>INIntentParameterCombinationSupportsBackgroundExecution</key>
<true/>
<key>INIntentParameterCombinationUpdatesLinked</key>
<true/>
</dict>
</dict>
<key>INIntentName</key>
<string>ExtendSleepTimer</string>
<key>INIntentParameterCombinations</key>
<dict>
<key></key>
<dict>
<key>INIntentParameterCombinationIsLinked</key>
<true/>
<key>INIntentParameterCombinationSupportsBackgroundExecution</key>
<true/>
</dict>
<key>minutes</key>
<dict>
<key>INIntentParameterCombinationIsPrimary</key>
Expand Down Expand Up @@ -335,16 +392,37 @@
<string>information</string>
<key>INIntentClassPrefix</key>
<string>SJ</string>
<key>INIntentConfigurable</key>
<true/>
<key>INIntentDescription</key>
<string>Open Filter</string>
<key>INIntentDescriptionID</key>
<string>QektJm</string>
<key>INIntentLastParameterTag</key>
<integer>2</integer>
<key>INIntentManagedParameterCombinations</key>
<dict>
<key></key>
<dict>
<key>INIntentParameterCombinationSupportsBackgroundExecution</key>
<true/>
<key>INIntentParameterCombinationUpdatesLinked</key>
<true/>
</dict>
</dict>
<key>INIntentName</key>
<string>OpenFilter</string>
<key>INIntentParameterCombinations</key>
<dict>
<key></key>
<dict>
<key>INIntentParameterCombinationIsLinked</key>
<true/>
<key>INIntentParameterCombinationIsPrimary</key>
<true/>
<key>INIntentParameterCombinationSupportsBackgroundExecution</key>
<true/>
</dict>
<key>filterUuid,filterName</key>
<dict>
<key>INIntentParameterCombinationSupportsBackgroundExecution</key>
Expand Down
1 change: 1 addition & 0 deletions podcasts/Bookmarks/Bookmarks+Analytics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ enum BookmarkAnalyticsSource: String, AnalyticsDescribable {
case files
case headphones
case whatsNew = "whats_new"
case appIntent = "app_intent"

case unknown

Expand Down
17 changes: 17 additions & 0 deletions podcasts/DownloadManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,23 @@ class DownloadManager: NSObject, FilePathProtocol {
addToQueue(episodeUuid: episodeUuid, fireNotification: false, autoDownloadStatus: .playerDownloadedForStreaming)
}

func download(episodeUuid: String) async -> String? {
addToQueue(episodeUuid: episodeUuid)
return await withCheckedContinuation { continuation in
NotificationCenter.default.addObserver(forName: Constants.Notifications.episodeDownloadStatusChanged, object: nil, queue: nil) { [weak self] notification in
let uuid = notification.object as? String
if uuid == episodeUuid {
if let episode = self?.dataManager.findEpisode(uuid: episodeUuid) {
continuation.resume(returning: episode.downloadUrl)
}
else {
fatalError("Episode should be found")
}
}
}
}
}

func addToQueue(episodeUuid: String, fireNotification: Bool, autoDownloadStatus: AutoDownloadStatus) {
// if this episode is already downloading, ignore it
if !shouldAddDownload(episodeUuid, autoDownloadStatus: autoDownloadStatus) { return }
Expand Down
181 changes: 181 additions & 0 deletions podcasts/Intents/AddBookmarkIntent.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import AppIntents
import PocketCastsDataModel
import PocketCastsUtils
import SwiftUI

@available(iOS 16.0, *)
enum AddBookmarkError: Error, CustomLocalizedStringResourceConvertible {
case episodeNotFound
case bookmarksNotUnlocked
case invalidTimestamp
case addBookmarkFailed

var localizedStringResource: LocalizedStringResource {
switch self {
case .episodeNotFound:
"Episode not found"
case .bookmarksNotUnlocked:
"Bookmarks feature not unlocked"
case .invalidTimestamp:
"Invalid timestamp provided"
case .addBookmarkFailed:
"Failed to add bookmark"
}
}
}

@available(iOS 16.0, *)
struct AddBookmarkIntent: AppIntent {
static var title: LocalizedStringResource = "Add Bookmark"
static var description = IntentDescription("Add a bookmark to the current episode or at a specific timestamp")

@Parameter(title: "Episode")
var episode: EpisodeSearchEntity?

@Parameter(title: "Timestamp (seconds)", description: "Optional timestamp to bookmark (uses current time if not provided)")
var timestamp: Double?

@Parameter(title: "Title", description: "Optional title for the bookmark")
var title: String?

static var parameterSummary: some ParameterSummary {
Summary("Add bookmark to \(\.$episode)") {
\.$timestamp
\.$title
}
}

func perform() async throws -> some IntentResult & ShowsSnippetView & ReturnsValue<String> {
#if APPCLIP
throw AddBookmarkError.bookmarksNotUnlocked
#else

guard PaidFeature.bookmarks.isUnlocked else {
throw AddBookmarkError.bookmarksNotUnlocked
}

let targetEpisode: BaseEpisode
let bookmarkTime: TimeInterval

if let episode = episode {
// Find the specific episode
guard let dbEpisode = DataManager.sharedManager.findBaseEpisode(uuid: episode.episodeUuid) else {
throw AddBookmarkError.episodeNotFound
}
targetEpisode = dbEpisode

// Use provided timestamp or current playback time
if let timestamp = timestamp {
guard timestamp >= 0 else {
throw AddBookmarkError.invalidTimestamp
}
bookmarkTime = timestamp
} else {
// Use current playback time if this episode is playing
if PlaybackManager.shared.isNowPlayingEpisode(episodeUuid: episode.episodeUuid) {
bookmarkTime = PlaybackManager.shared.currentTime()
} else {
bookmarkTime = 0
}
}
} else {
// Use currently playing episode
guard let currentEpisode = PlaybackManager.shared.currentEpisode() else {
throw AddBookmarkError.episodeNotFound
}
targetEpisode = currentEpisode

// Use provided timestamp or current playback time
if let timestamp = timestamp {
guard timestamp >= 0 else {
throw AddBookmarkError.invalidTimestamp
}
bookmarkTime = timestamp
} else {
bookmarkTime = PlaybackManager.shared.currentTime()
}
}

let bookmarkTitle = title ?? L10n.bookmarkDefaultTitle
let timeString = TimeFormatter.shared.multipleUnitFormattedShortTime(time: bookmarkTime)

// Request confirmation with detailed context
try await requestConfirmation(
result: .result(
dialog: IntentDialog(stringLiteral: "Add bookmark '\(bookmarkTitle)' at \(timeString) in '\(targetEpisode.displayableTitle())'?")
)
)

// Add the bookmark
guard let bookmark = PlaybackManager.shared.bookmarkManager.add(
to: targetEpisode,
at: bookmarkTime,
title: bookmarkTitle
) else {
throw AddBookmarkError.addBookmarkFailed
}

// Track analytics
Analytics.track(.bookmarkCreated, source: BookmarkAnalyticsSource.appIntent, properties: [
"episode_uuid": targetEpisode.uuid,
"podcast_uuid": (targetEpisode as? Episode)?.podcastUuid ?? "user_file",
"time": Int(bookmarkTime)
])

return .result(
value: "Bookmark added at \(timeString)",
view: AddBookmarkSnippetView(
episodeTitle: targetEpisode.displayableTitle(),
bookmarkTime: timeString,
bookmarkTitle: bookmarkTitle
)
)
#endif
}
}

@available(iOS 16.0, *)
struct AddBookmarkSnippetView: View {
let episodeTitle: String
let bookmarkTime: String
let bookmarkTitle: String

var body: some View {
VStack(alignment: .leading, spacing: 12) {
Label("Bookmark Added", systemImage: "bookmark.fill")
.font(.headline)
.foregroundStyle(.primary)

VStack(alignment: .leading, spacing: 8) {
HStack {
Image(systemName: "tag")
.foregroundStyle(.green)
Text(bookmarkTitle)
.font(.headline)
.fontWeight(.medium)
.lineLimit(2)
}

HStack {
Image(systemName: "clock")
.foregroundStyle(.orange)
Text(bookmarkTime)
.font(.subheadline)
.fontWeight(.medium)
}

HStack {
Image(systemName: "play.circle.fill")
.foregroundStyle(.blue)
Text(episodeTitle)
.font(.subheadline)
.foregroundStyle(.secondary)
.lineLimit(2)
}
}
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(12)
}
}
Loading