Refactor chapter data to allow for click to skip to next chapter

This commit is contained in:
Jackson Harper
2024-05-03 09:20:29 +08:00
parent 767b3dc77f
commit e4ae644c7d
3 changed files with 89 additions and 49 deletions

View File

@ -5,10 +5,42 @@ import Views
import MarkdownUI import MarkdownUI
import Utils 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 @MainActor
public class FullScreenDigestViewModel: ObservableObject { public class FullScreenDigestViewModel: ObservableObject {
@Published var isLoading = false @Published var isLoading = false
@Published var digest: DigestResult? @Published var digest: DigestResult?
@Published var chapterInfo: [(DigestChapter, DigestChapterData)]?
@AppStorage(UserDefaultKey.lastVisitedDigestId.rawValue) var lastVisitedDigestId = "" @AppStorage(UserDefaultKey.lastVisitedDigestId.rawValue) var lastVisitedDigestId = ""
func load(dataService: DataService, audioController: AudioController) async { 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 { if let playingDigest = audioController.itemAudioProperties as? DigestAudioItem, playingDigest.digest.id == digest.id {
// Don't think we need to do anything here // Don't think we need to do anything here
} else { } else {
audioController.play(itemAudioProperties: DigestAudioItem(digest: digest)) let chapters = getChapterData(digest: digest)
audioController.play(itemAudioProperties: DigestAudioItem(digest: digest, chapters: chapters))
} }
} }
} catch { } catch {
@ -114,35 +147,6 @@ struct FullScreenDigestView: View {
.buttonStyle(.plain) .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, *) @available(iOS 17.0, *)
var itemBody: some View { var itemBody: some View {
VStack { VStack {
@ -182,30 +186,31 @@ struct FullScreenDigestView: View {
.background(Color.themeLabelBackground.opacity(0.6)) .background(Color.themeLabelBackground.opacity(0.6))
.cornerRadius(5) .cornerRadius(5)
if let digest = viewModel.digest { if let chapters = viewModel.chapterInfo {
VStack(alignment: .leading, spacing: 10) { VStack(alignment: .leading, spacing: 10) {
Text("Chapters") Text("Chapters")
.font(Font.system(size: 17, weight: .semibold)) .font(Font.system(size: 17, weight: .semibold))
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
.padding(0) .padding(0)
let chapterData = getChapterData(digest: digest) ForEach(chapters, id: \.0.id) { chaps in
ForEach(digest.chapters, id: \.id) { chapter in let (chapter, chapterData) = chaps
if let startTime = chapterData[chapter.id]?.time, let skipIndex = chapterData[chapter.id]?.start { let currentChapter = (audioController.currentAudioIndex >= chapterData.start && audioController.currentAudioIndex < chapterData.end)
let currentChapter = audioController.currentAudioIndex >= (chapterData[chapter.id]?.start ?? 0) &&
audioController.currentAudioIndex < (chapterData[chapter.id]?.end ?? 0) ChapterView(
ChapterView( startTime: chapterData.time,
startTime: startTime, skipIndex: chapterData.start,
skipIndex: skipIndex, chapter: chapter
chapter: chapter )
) .onTapGesture {
.onTapGesture { audioController.seek(toIdx: chapterData.start)
audioController.seek(toIdx: skipIndex) 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) .padding(.top, 20)

View File

@ -33,16 +33,20 @@ public struct DigestAudioItem: AudioItemProperties {
public let digest: DigestResult public let digest: DigestResult
public let itemID: String public let itemID: String
public let title: String public let title: String
public let chapters: [DigestChapterData]
public var byline: String? public var byline: String?
public var imageURL: URL? public var imageURL: URL?
public var language: String? public var language: String?
public var startIndex: Int = 0 public var startIndex: Int = 0
public var startOffset: Double = 0.0 public var startOffset: Double = 0.0
public init(digest: DigestResult) { public init(digest: DigestResult, chapters: [DigestChapterData]) {
self.digest = digest self.digest = digest
self.itemID = digest.id self.itemID = digest.id
self.title = digest.title self.title = digest.title
self.chapters = chapters
self.startIndex = 0 self.startIndex = 0
self.startOffset = 0 self.startOffset = 0
@ -908,11 +912,30 @@ public struct DigestAudioItem: AudioItemProperties {
return .commandFailed 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 { Task {
await downloadAndSetArtwork() await downloadAndSetArtwork()
} }
} }
func nextChapterIndex(digest: DigestResult, idx: Int) -> Int {
// for chapter in digest.chapters {
// if chapter.
// }
return 0
}
func isoLangForCurrentVoice() -> String { func isoLangForCurrentVoice() -> String {
// currentVoicePair should not ever be nil but if it is we return an empty string // currentVoicePair should not ever be nil but if it is we return an empty string
if let isoLang = currentVoicePair?.language { if let isoLang = currentVoicePair?.language {

View File

@ -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 struct RefreshDigestResult: Codable {
public let jobId: String public let jobId: String
} }