iOS: replace shuffle/repeat with time skip and add chapter navigation to full player
Some checks failed
CI / Scraper / Test (pull_request) Failing after 8s
CI / UI / Build (pull_request) Failing after 8s
CI / Scraper / Lint (pull_request) Failing after 11s
CI / Scraper / Build (pull_request) Has been skipped
iOS CI / Build (push) Has been cancelled
iOS CI / Test (push) Has been cancelled
iOS CI / Build (pull_request) Failing after 1s
iOS CI / Test (pull_request) Has been skipped
Some checks failed
CI / Scraper / Test (pull_request) Failing after 8s
CI / UI / Build (pull_request) Failing after 8s
CI / Scraper / Lint (pull_request) Failing after 11s
CI / Scraper / Build (pull_request) Has been skipped
iOS CI / Build (push) Has been cancelled
iOS CI / Test (push) Has been cancelled
iOS CI / Build (pull_request) Failing after 1s
iOS CI / Test (pull_request) Has been skipped
- Leftmost button: 15-second backward skip (gobackward.15) - Second button: previous chapter navigation (backward.end.fill) - Center: play/pause (unchanged) - Fourth button: next chapter navigation (forward.end.fill) - Rightmost button: 15-second forward skip (goforward.15) - Added prevChapter tracking in AudioPlayerService - Added skipToPrevChapter notification support - Chapter nav buttons disabled when no prev/next available (0.5 opacity) - Updated load() signature to accept prevChapter parameter - Auto-next properly tracks prevChapter when advancing chapters
This commit is contained in:
@@ -33,6 +33,7 @@ final class AudioPlayerService: ObservableObject {
|
||||
|
||||
@Published var autoNext: Bool = false
|
||||
@Published var nextChapter: Int? = nil
|
||||
@Published var prevChapter: Int? = nil
|
||||
|
||||
@Published var nextPrefetchStatus: NextPrefetchStatus = .none
|
||||
@Published var nextAudioURL: String = ""
|
||||
@@ -73,7 +74,7 @@ final class AudioPlayerService: ObservableObject {
|
||||
/// Load audio for a specific chapter. Triggers TTS generation if not cached.
|
||||
func load(slug: String, chapter: Int, chapterTitle: String,
|
||||
bookTitle: String, coverURL: String, voice: String, speed: Double,
|
||||
chapters: [ChapterIndexBrief], nextChapter: Int?) {
|
||||
chapters: [ChapterIndexBrief], nextChapter: Int?, prevChapter: Int?) {
|
||||
generationTask?.cancel()
|
||||
prefetchTask?.cancel()
|
||||
stop()
|
||||
@@ -87,6 +88,7 @@ final class AudioPlayerService: ObservableObject {
|
||||
self.speed = speed
|
||||
self.chapters = chapters
|
||||
self.nextChapter = nextChapter
|
||||
self.prevChapter = prevChapter
|
||||
self.nextPrefetchStatus = .none
|
||||
self.nextAudioURL = ""
|
||||
self.nextPrefetchedChapter = nil
|
||||
@@ -328,6 +330,7 @@ final class AudioPlayerService: ObservableObject {
|
||||
|
||||
let nextTitle = chapters.first(where: { $0.number == next })?.title ?? ""
|
||||
let nextNextChapter = chapters.first(where: { $0.number > next })?.number
|
||||
let nextPrevChapter: Int? = chapter // Current chapter becomes previous for the next one
|
||||
|
||||
// If we already prefetched a URL for the next chapter, skip straight to
|
||||
// playback and kick off generation in the background for the one after.
|
||||
@@ -338,6 +341,7 @@ final class AudioPlayerService: ObservableObject {
|
||||
chapter = next
|
||||
chapterTitle = nextTitle
|
||||
nextChapter = nextNextChapter
|
||||
prevChapter = nextPrevChapter
|
||||
nextPrefetchStatus = .none
|
||||
nextAudioURL = ""
|
||||
nextPrefetchedChapter = nil
|
||||
@@ -360,7 +364,8 @@ final class AudioPlayerService: ObservableObject {
|
||||
voice: voice,
|
||||
speed: speed,
|
||||
chapters: chapters,
|
||||
nextChapter: nextNextChapter
|
||||
nextChapter: nextNextChapter,
|
||||
prevChapter: nextPrevChapter
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -462,4 +467,6 @@ enum AudioPlayerStatus: Equatable {
|
||||
|
||||
extension Notification.Name {
|
||||
static let audioDidFinishChapter = Notification.Name("audioDidFinishChapter")
|
||||
static let skipToNextChapter = Notification.Name("skipToNextChapter")
|
||||
static let skipToPrevChapter = Notification.Name("skipToPrevChapter")
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ final class ChapterReaderViewModel: ObservableObject {
|
||||
audioPlayer.togglePlayPause()
|
||||
} else {
|
||||
let nextChapter: Int? = content.next
|
||||
let prevChapter: Int? = content.prev
|
||||
audioPlayer.load(
|
||||
slug: slug,
|
||||
chapter: chapter,
|
||||
@@ -56,7 +57,8 @@ final class ChapterReaderViewModel: ObservableObject {
|
||||
voice: settings.voice,
|
||||
speed: settings.speed,
|
||||
chapters: content.chapters,
|
||||
nextChapter: nextChapter
|
||||
nextChapter: nextChapter,
|
||||
prevChapter: prevChapter
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ struct BookDetailView: View {
|
||||
Button(summaryExpanded ? "Less" : "More") {
|
||||
withAnimation { summaryExpanded.toggle() }
|
||||
}
|
||||
.font(.caption.bold())
|
||||
.font(.caption.bold())
|
||||
.foregroundStyle(.amber)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,14 @@ struct ChapterReaderView: View {
|
||||
vm.navigateTo = next
|
||||
}
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .skipToNextChapter)) { note in
|
||||
guard let next = note.userInfo?["next"] as? Int else { return }
|
||||
vm.navigateTo = next
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .skipToPrevChapter)) { note in
|
||||
guard let prev = note.userInfo?["prev"] as? Int else { return }
|
||||
vm.navigateTo = prev
|
||||
}
|
||||
.navigationDestination(isPresented: Binding(
|
||||
get: { vm.navigateTo != nil },
|
||||
set: { if !$0 { vm.navigateTo = nil } }
|
||||
|
||||
Reference in New Issue
Block a user