From e43699747d89b42822f95384825008a09dc6c6a0 Mon Sep 17 00:00:00 2001 From: Admin Date: Sun, 8 Mar 2026 14:20:49 +0500 Subject: [PATCH] feat: make prev/next chapter buttons absolute navigation Previously, prev/next buttons were relative to the currently playing chapter and only worked for one navigation step. They relied on audioPlayer.prevChapter and audioPlayer.nextChapter which were set when loading audio and never updated. Now buttons use absolute chapter navigation: - Previous: always navigate to (current chapter - 1) - Next: always navigate to (current chapter + 1) - Bounded by chapter 1 and max chapter number - Works continuously without stopping after one navigation Added AudioPlayerService.absolutePrevChapter and .absoluteNextChapter computed properties that calculate the prev/next chapter numbers based on the current chapter and total chapters list. Updated both mini player and full player to use absolute navigation. --- .../Services/AudioPlayerService.swift | 14 +++++++++++ .../LibNovel/Views/Player/PlayerViews.swift | 24 +++++++++---------- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/ios/LibNovel/LibNovel/Services/AudioPlayerService.swift b/ios/LibNovel/LibNovel/Services/AudioPlayerService.swift index bd7c7bd..e1c92d0 100644 --- a/ios/LibNovel/LibNovel/Services/AudioPlayerService.swift +++ b/ios/LibNovel/LibNovel/Services/AudioPlayerService.swift @@ -47,6 +47,20 @@ final class AudioPlayerService: ObservableObject { default: return true } } + + /// Absolute previous chapter number (current - 1), or nil if at first chapter + var absolutePrevChapter: Int? { + guard chapter > 1 else { return nil } + return chapter - 1 + } + + /// Absolute next chapter number (current + 1), or nil if at last chapter + var absoluteNextChapter: Int? { + guard !chapters.isEmpty else { return nil } + let maxChapter = chapters.map(\.number).max() ?? chapter + guard chapter < maxChapter else { return nil } + return chapter + 1 + } // MARK: - Private diff --git a/ios/LibNovel/LibNovel/Views/Player/PlayerViews.swift b/ios/LibNovel/LibNovel/Views/Player/PlayerViews.swift index e09501f..3458f14 100644 --- a/ios/LibNovel/LibNovel/Views/Player/PlayerViews.swift +++ b/ios/LibNovel/LibNovel/Views/Player/PlayerViews.swift @@ -77,7 +77,7 @@ struct MiniPlayerView: View { // Previous chapter button if audioPlayer.status == .ready { Button { - if let prev = audioPlayer.prevChapter { + if let prev = audioPlayer.absolutePrevChapter { NotificationCenter.default.post( name: .skipToPrevChapter, object: nil, @@ -92,8 +92,8 @@ struct MiniPlayerView: View { .contentShape(Rectangle()) } .buttonStyle(.plain) - .disabled(audioPlayer.prevChapter == nil) - .opacity(audioPlayer.prevChapter == nil ? 0.4 : 1.0) + .disabled(audioPlayer.absolutePrevChapter == nil) + .opacity(audioPlayer.absolutePrevChapter == nil ? 0.4 : 1.0) } // Status indicator or play/pause control @@ -126,7 +126,7 @@ struct MiniPlayerView: View { // Next chapter button if audioPlayer.status == .ready { Button { - if let next = audioPlayer.nextChapter { + if let next = audioPlayer.absoluteNextChapter { NotificationCenter.default.post( name: .skipToNextChapter, object: nil, @@ -158,8 +158,8 @@ struct MiniPlayerView: View { .contentShape(Rectangle()) } .buttonStyle(.plain) - .disabled(audioPlayer.nextChapter == nil) - .opacity(audioPlayer.nextChapter == nil ? 0.4 : 1.0) + .disabled(audioPlayer.absoluteNextChapter == nil) + .opacity(audioPlayer.absoluteNextChapter == nil ? 0.4 : 1.0) } } } @@ -456,7 +456,7 @@ struct FullPlayerView: View { // ← previous chapter Button { - if let prev = audioPlayer.prevChapter { + if let prev = audioPlayer.absolutePrevChapter { onDismiss() NotificationCenter.default.post( name: .skipToPrevChapter, @@ -471,8 +471,8 @@ struct FullPlayerView: View { .frame(maxWidth: .infinity) } .buttonStyle(.plain) - .disabled(audioPlayer.prevChapter == nil) - .opacity(audioPlayer.prevChapter == nil ? 0.5 : 1.0) + .disabled(audioPlayer.absolutePrevChapter == nil) + .opacity(audioPlayer.absolutePrevChapter == nil ? 0.5 : 1.0) // play / pause Button { audioPlayer.togglePlayPause() } label: { @@ -495,7 +495,7 @@ struct FullPlayerView: View { // → next chapter Button { - if let next = audioPlayer.nextChapter { + if let next = audioPlayer.absoluteNextChapter { onDismiss() NotificationCenter.default.post( name: .skipToNextChapter, @@ -527,8 +527,8 @@ struct FullPlayerView: View { .frame(maxWidth: .infinity) } .buttonStyle(.plain) - .disabled(audioPlayer.nextChapter == nil) - .opacity(audioPlayer.nextChapter == nil ? 0.5 : 1.0) + .disabled(audioPlayer.absoluteNextChapter == nil) + .opacity(audioPlayer.absoluteNextChapter == nil ? 0.5 : 1.0) // → skip forward 15s Button { audioPlayer.skip(by: 15) } label: {