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 autoNext: Bool = false
|
||||||
@Published var nextChapter: Int? = nil
|
@Published var nextChapter: Int? = nil
|
||||||
|
@Published var prevChapter: Int? = nil
|
||||||
|
|
||||||
@Published var nextPrefetchStatus: NextPrefetchStatus = .none
|
@Published var nextPrefetchStatus: NextPrefetchStatus = .none
|
||||||
@Published var nextAudioURL: String = ""
|
@Published var nextAudioURL: String = ""
|
||||||
@@ -73,7 +74,7 @@ final class AudioPlayerService: ObservableObject {
|
|||||||
/// Load audio for a specific chapter. Triggers TTS generation if not cached.
|
/// Load audio for a specific chapter. Triggers TTS generation if not cached.
|
||||||
func load(slug: String, chapter: Int, chapterTitle: String,
|
func load(slug: String, chapter: Int, chapterTitle: String,
|
||||||
bookTitle: String, coverURL: String, voice: String, speed: Double,
|
bookTitle: String, coverURL: String, voice: String, speed: Double,
|
||||||
chapters: [ChapterIndexBrief], nextChapter: Int?) {
|
chapters: [ChapterIndexBrief], nextChapter: Int?, prevChapter: Int?) {
|
||||||
generationTask?.cancel()
|
generationTask?.cancel()
|
||||||
prefetchTask?.cancel()
|
prefetchTask?.cancel()
|
||||||
stop()
|
stop()
|
||||||
@@ -87,6 +88,7 @@ final class AudioPlayerService: ObservableObject {
|
|||||||
self.speed = speed
|
self.speed = speed
|
||||||
self.chapters = chapters
|
self.chapters = chapters
|
||||||
self.nextChapter = nextChapter
|
self.nextChapter = nextChapter
|
||||||
|
self.prevChapter = prevChapter
|
||||||
self.nextPrefetchStatus = .none
|
self.nextPrefetchStatus = .none
|
||||||
self.nextAudioURL = ""
|
self.nextAudioURL = ""
|
||||||
self.nextPrefetchedChapter = nil
|
self.nextPrefetchedChapter = nil
|
||||||
@@ -328,6 +330,7 @@ final class AudioPlayerService: ObservableObject {
|
|||||||
|
|
||||||
let nextTitle = chapters.first(where: { $0.number == next })?.title ?? ""
|
let nextTitle = chapters.first(where: { $0.number == next })?.title ?? ""
|
||||||
let nextNextChapter = chapters.first(where: { $0.number > next })?.number
|
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
|
// 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.
|
// playback and kick off generation in the background for the one after.
|
||||||
@@ -338,6 +341,7 @@ final class AudioPlayerService: ObservableObject {
|
|||||||
chapter = next
|
chapter = next
|
||||||
chapterTitle = nextTitle
|
chapterTitle = nextTitle
|
||||||
nextChapter = nextNextChapter
|
nextChapter = nextNextChapter
|
||||||
|
prevChapter = nextPrevChapter
|
||||||
nextPrefetchStatus = .none
|
nextPrefetchStatus = .none
|
||||||
nextAudioURL = ""
|
nextAudioURL = ""
|
||||||
nextPrefetchedChapter = nil
|
nextPrefetchedChapter = nil
|
||||||
@@ -360,7 +364,8 @@ final class AudioPlayerService: ObservableObject {
|
|||||||
voice: voice,
|
voice: voice,
|
||||||
speed: speed,
|
speed: speed,
|
||||||
chapters: chapters,
|
chapters: chapters,
|
||||||
nextChapter: nextNextChapter
|
nextChapter: nextNextChapter,
|
||||||
|
prevChapter: nextPrevChapter
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -462,4 +467,6 @@ enum AudioPlayerStatus: Equatable {
|
|||||||
|
|
||||||
extension Notification.Name {
|
extension Notification.Name {
|
||||||
static let audioDidFinishChapter = Notification.Name("audioDidFinishChapter")
|
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()
|
audioPlayer.togglePlayPause()
|
||||||
} else {
|
} else {
|
||||||
let nextChapter: Int? = content.next
|
let nextChapter: Int? = content.next
|
||||||
|
let prevChapter: Int? = content.prev
|
||||||
audioPlayer.load(
|
audioPlayer.load(
|
||||||
slug: slug,
|
slug: slug,
|
||||||
chapter: chapter,
|
chapter: chapter,
|
||||||
@@ -56,7 +57,8 @@ final class ChapterReaderViewModel: ObservableObject {
|
|||||||
voice: settings.voice,
|
voice: settings.voice,
|
||||||
speed: settings.speed,
|
speed: settings.speed,
|
||||||
chapters: content.chapters,
|
chapters: content.chapters,
|
||||||
nextChapter: nextChapter
|
nextChapter: nextChapter,
|
||||||
|
prevChapter: prevChapter
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ struct BookDetailView: View {
|
|||||||
Button(summaryExpanded ? "Less" : "More") {
|
Button(summaryExpanded ? "Less" : "More") {
|
||||||
withAnimation { summaryExpanded.toggle() }
|
withAnimation { summaryExpanded.toggle() }
|
||||||
}
|
}
|
||||||
.font(.caption.bold())
|
.font(.caption.bold())
|
||||||
.foregroundStyle(.amber)
|
.foregroundStyle(.amber)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,14 @@ struct ChapterReaderView: View {
|
|||||||
vm.navigateTo = next
|
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(
|
.navigationDestination(isPresented: Binding(
|
||||||
get: { vm.navigateTo != nil },
|
get: { vm.navigateTo != nil },
|
||||||
set: { if !$0 { vm.navigateTo = nil } }
|
set: { if !$0 { vm.navigateTo = nil } }
|
||||||
|
|||||||
Reference in New Issue
Block a user