From 37bd73651a39e7d8e995b8ed8ad8c85cc0253832 Mon Sep 17 00:00:00 2001 From: Admin Date: Sun, 8 Mar 2026 13:14:26 +0500 Subject: [PATCH] iOS: replace shuffle/repeat with time skip and add chapter navigation to full player - 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 --- .../LibNovel/Services/AudioPlayerService.swift | 11 +++++++++-- .../LibNovel/ViewModels/ChapterReaderViewModel.swift | 4 +++- .../LibNovel/Views/BookDetail/BookDetailView.swift | 2 +- .../Views/ChapterReader/ChapterReaderView.swift | 8 ++++++++ 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/ios/LibNovel/LibNovel/Services/AudioPlayerService.swift b/ios/LibNovel/LibNovel/Services/AudioPlayerService.swift index df34be2..a18b686 100644 --- a/ios/LibNovel/LibNovel/Services/AudioPlayerService.swift +++ b/ios/LibNovel/LibNovel/Services/AudioPlayerService.swift @@ -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") } diff --git a/ios/LibNovel/LibNovel/ViewModels/ChapterReaderViewModel.swift b/ios/LibNovel/LibNovel/ViewModels/ChapterReaderViewModel.swift index 4b9112f..00e4fd5 100644 --- a/ios/LibNovel/LibNovel/ViewModels/ChapterReaderViewModel.swift +++ b/ios/LibNovel/LibNovel/ViewModels/ChapterReaderViewModel.swift @@ -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 ) } } diff --git a/ios/LibNovel/LibNovel/Views/BookDetail/BookDetailView.swift b/ios/LibNovel/LibNovel/Views/BookDetail/BookDetailView.swift index 384e416..8dd8637 100644 --- a/ios/LibNovel/LibNovel/Views/BookDetail/BookDetailView.swift +++ b/ios/LibNovel/LibNovel/Views/BookDetail/BookDetailView.swift @@ -86,7 +86,7 @@ struct BookDetailView: View { Button(summaryExpanded ? "Less" : "More") { withAnimation { summaryExpanded.toggle() } } - .font(.caption.bold()) + .font(.caption.bold())  .foregroundStyle(.amber) } } diff --git a/ios/LibNovel/LibNovel/Views/ChapterReader/ChapterReaderView.swift b/ios/LibNovel/LibNovel/Views/ChapterReader/ChapterReaderView.swift index e7723b3..873441d 100644 --- a/ios/LibNovel/LibNovel/Views/ChapterReader/ChapterReaderView.swift +++ b/ios/LibNovel/LibNovel/Views/ChapterReader/ChapterReaderView.swift @@ -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 } }