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.
This commit is contained in:
Admin
2026-03-08 14:20:49 +05:00
parent 1e85f1c0bc
commit e43699747d
2 changed files with 26 additions and 12 deletions

View File

@@ -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

View File

@@ -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: {