diff --git a/apple/OmnivoreKit/Sources/App/Views/AI/FullScreenDigestView.swift b/apple/OmnivoreKit/Sources/App/Views/AI/FullScreenDigestView.swift index f232375c7..51e015886 100644 --- a/apple/OmnivoreKit/Sources/App/Views/AI/FullScreenDigestView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/AI/FullScreenDigestView.swift @@ -5,10 +5,42 @@ import Views import MarkdownUI import Utils + +func getChapterData(digest: DigestResult) -> [DigestChapterData] { + let speed = 1.0 + var chapterData: [DigestChapterData] = [] + var currentAudioIndex = 0 + var currentWordCount = 0.0 + + for (index, speechFile) in digest.speechFiles.enumerated() { + let chapter = digest.chapters[index] + let duration = currentWordCount / SpeechDocument.averageWPM / speed * 60.0 + + chapterData.append(DigestChapterData( + time: formatTimeInterval(duration) ?? "00:00", + start: Int(currentAudioIndex), + end: currentAudioIndex + Int(speechFile.utterances.count) + )) + currentAudioIndex += Int(speechFile.utterances.count) + currentWordCount += chapter.wordCount + } + return chapterData +} + +func formatTimeInterval(_ time: TimeInterval) -> String? { + let componentFormatter = DateComponentsFormatter() + componentFormatter.unitsStyle = .positional + componentFormatter.allowedUnits = time >= 3600 ? [.second, .minute, .hour] : [.second, .minute] + componentFormatter.zeroFormattingBehavior = .pad + return componentFormatter.string(from: time) +} + + @MainActor public class FullScreenDigestViewModel: ObservableObject { @Published var isLoading = false @Published var digest: DigestResult? + @Published var chapterInfo: [(DigestChapter, DigestChapterData)]? @AppStorage(UserDefaultKey.lastVisitedDigestId.rawValue) var lastVisitedDigestId = "" func load(dataService: DataService, audioController: AudioController) async { @@ -24,7 +56,8 @@ public class FullScreenDigestViewModel: ObservableObject { if let playingDigest = audioController.itemAudioProperties as? DigestAudioItem, playingDigest.digest.id == digest.id { // Don't think we need to do anything here } else { - audioController.play(itemAudioProperties: DigestAudioItem(digest: digest)) + let chapters = getChapterData(digest: digest) + audioController.play(itemAudioProperties: DigestAudioItem(digest: digest, chapters: chapters)) } } } catch { @@ -114,35 +147,6 @@ struct FullScreenDigestView: View { .buttonStyle(.plain) } - func getChapterData(digest: DigestResult) -> [String:(time: String, start: Int, end: Int)] { - let speed = 1.0 - var chapterData: [String:(time: String, start: Int, end: Int)] = [:] - var currentAudioIndex = 0 - var currentWordCount = 0.0 - - for (index, speechFile) in digest.speechFiles.enumerated() { - let chapter = digest.chapters[index] - let duration = currentWordCount / SpeechDocument.averageWPM / speed * 60.0 - - chapterData[chapter.id] = ( - time: formatTimeInterval(duration) ?? "00:00", - start: Int(currentAudioIndex), - end: currentAudioIndex + Int(speechFile.utterances.count) - ) - currentAudioIndex += Int(speechFile.utterances.count) - currentWordCount += chapter.wordCount - } - return chapterData - } - - func formatTimeInterval(_ time: TimeInterval) -> String? { - let componentFormatter = DateComponentsFormatter() - componentFormatter.unitsStyle = .positional - componentFormatter.allowedUnits = time >= 3600 ? [.second, .minute, .hour] : [.second, .minute] - componentFormatter.zeroFormattingBehavior = .pad - return componentFormatter.string(from: time) - } - @available(iOS 17.0, *) var itemBody: some View { VStack { @@ -182,30 +186,31 @@ struct FullScreenDigestView: View { .background(Color.themeLabelBackground.opacity(0.6)) .cornerRadius(5) - if let digest = viewModel.digest { + if let chapters = viewModel.chapterInfo { VStack(alignment: .leading, spacing: 10) { Text("Chapters") .font(Font.system(size: 17, weight: .semibold)) .frame(maxWidth: .infinity, alignment: .leading) .padding(0) - let chapterData = getChapterData(digest: digest) - ForEach(digest.chapters, id: \.id) { chapter in - if let startTime = chapterData[chapter.id]?.time, let skipIndex = chapterData[chapter.id]?.start { - let currentChapter = audioController.currentAudioIndex >= (chapterData[chapter.id]?.start ?? 0) && - audioController.currentAudioIndex < (chapterData[chapter.id]?.end ?? 0) - ChapterView( - startTime: startTime, - skipIndex: skipIndex, - chapter: chapter - ) - .onTapGesture { - audioController.seek(toIdx: skipIndex) + ForEach(chapters, id: \.0.id) { chaps in + let (chapter, chapterData) = chaps + let currentChapter = (audioController.currentAudioIndex >= chapterData.start && audioController.currentAudioIndex < chapterData.end) + + ChapterView( + startTime: chapterData.time, + skipIndex: chapterData.start, + chapter: chapter + ) + .onTapGesture { + audioController.seek(toIdx: chapterData.start) + if audioController.state != .loading && !audioController.isPlaying { + audioController.unpause() } - .background( - currentChapter ? Color.themeLabelBackground.opacity(0.6) : Color.clear - ) - .cornerRadius(5) } + .background( + currentChapter ? Color.themeLabelBackground.opacity(0.6) : Color.clear + ) + .cornerRadius(5) } } .padding(.top, 20) diff --git a/apple/OmnivoreKit/Sources/Services/AudioSession/AudioController.swift b/apple/OmnivoreKit/Sources/Services/AudioSession/AudioController.swift index d48023ab9..a55cdd90a 100644 --- a/apple/OmnivoreKit/Sources/Services/AudioSession/AudioController.swift +++ b/apple/OmnivoreKit/Sources/Services/AudioSession/AudioController.swift @@ -33,16 +33,20 @@ public struct DigestAudioItem: AudioItemProperties { public let digest: DigestResult public let itemID: String public let title: String + public let chapters: [DigestChapterData] + public var byline: String? public var imageURL: URL? public var language: String? public var startIndex: Int = 0 public var startOffset: Double = 0.0 - public init(digest: DigestResult) { + public init(digest: DigestResult, chapters: [DigestChapterData]) { self.digest = digest self.itemID = digest.id self.title = digest.title + self.chapters = chapters + self.startIndex = 0 self.startOffset = 0 @@ -908,11 +912,30 @@ public struct DigestAudioItem: AudioItemProperties { return .commandFailed } + if let digest = self.itemAudioProperties as? DigestAudioItem { + commandCenter.nextTrackCommand.isEnabled = true + commandCenter.nextTrackCommand.addTarget { event -> MPRemoteCommandHandlerStatus in + let next = self.currentAudioIndex + 1 + if next < (self.document?.utterances.count ?? 0) { + self.seek(toIdx: self.currentAudioIndex + 1) + return .success + } + return .commandFailed + } + } + Task { await downloadAndSetArtwork() } } - + + func nextChapterIndex(digest: DigestResult, idx: Int) -> Int { +// for chapter in digest.chapters { +// if chapter. +// } + return 0 + } + func isoLangForCurrentVoice() -> String { // currentVoicePair should not ever be nil but if it is we return an empty string if let isoLang = currentVoicePair?.language { diff --git a/apple/OmnivoreKit/Sources/Services/DataService/AI/AITasks.swift b/apple/OmnivoreKit/Sources/Services/DataService/AI/AITasks.swift index 6ec0fe19e..7147501c5 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/AI/AITasks.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/AI/AITasks.swift @@ -36,6 +36,18 @@ public struct DigestChapter: Codable { } } +public struct DigestChapterData { + public let time: String + public let start: Int + public let end: Int + + public init(time: String, start: Int, end: Int) { + self.time = time + self.start = start + self.end = end + } +} + public struct RefreshDigestResult: Codable { public let jobId: String }