Add chapters, history, bookmarks, live download progress, and i18n
- Chapter navigation with auto-scroll to current chapter and end-of-chapter sleep timer - Opt-in listening history (local-only) with XML export and per-item quick menu - Bookmarks with server sync via Audiobookshelf API - Live MB counter during downloads via URLSessionDownloadTask delegate - In-progress downloads shown in "Heruntergeladen" with dimmed cover + ring overlay - Cover image cache (50 MB memory / 500 MB disk URLCache) - German/English localization (de.lproj, en.lproj) - Loading spinner now triggers immediately on view switch instead of waiting for the network Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,7 @@ enum SleepTimerMode: Equatable, Hashable {
|
||||
case off
|
||||
case minutes(Int)
|
||||
case endOfBook
|
||||
case endOfChapter
|
||||
}
|
||||
|
||||
@Observable
|
||||
@@ -29,8 +30,13 @@ final class PlayerEngine {
|
||||
|
||||
var sleepTimer: SleepTimerMode = .off
|
||||
/// Verbleibende Wallclock-Sekunden bis der Sleep-Timer auslöst (0 wenn off).
|
||||
/// Pausiert mit der Wiedergabe; bei `.endOfBook` rate-skaliert aus der Restspielzeit.
|
||||
/// Pausiert mit der Wiedergabe; bei `.endOfBook`/`.endOfChapter` rate-skaliert aus der Restspielzeit.
|
||||
var sleepRemainingSeconds: Double = 0
|
||||
var chapters: [Chapter] = []
|
||||
|
||||
var currentChapter: Chapter? {
|
||||
chapters.last { $0.start <= absoluteCurrentTime }
|
||||
}
|
||||
|
||||
private var player: AVQueuePlayer?
|
||||
private var trackDurations: [Double] = []
|
||||
@@ -118,6 +124,7 @@ final class PlayerEngine {
|
||||
}
|
||||
}
|
||||
|
||||
self.chapters = item.chapters
|
||||
currentTitle = item.title
|
||||
currentAuthor = item.author
|
||||
currentCoverURL = client.coverURL(itemId: item.id)
|
||||
@@ -159,8 +166,10 @@ final class PlayerEngine {
|
||||
func setRate(_ newRate: Float) {
|
||||
rate = newRate
|
||||
if isPlaying { player?.rate = newRate }
|
||||
if case .endOfBook = sleepTimer {
|
||||
sleepRemainingSeconds = wallclockRemainingUntilEndOfBook()
|
||||
switch sleepTimer {
|
||||
case .endOfBook: sleepRemainingSeconds = wallclockRemainingUntilEndOfBook()
|
||||
case .endOfChapter: sleepRemainingSeconds = wallclockRemainingUntilEndOfChapter()
|
||||
default: break
|
||||
}
|
||||
updateNowPlayingInfo()
|
||||
}
|
||||
@@ -241,6 +250,7 @@ final class PlayerEngine {
|
||||
isPlaying = player.timeControlStatus == .playing
|
||||
if wasPlaying != isPlaying { updateNowPlayingInfo() }
|
||||
updateEndOfBookSleep()
|
||||
updateEndOfChapterSleep()
|
||||
}
|
||||
|
||||
func teardown() {
|
||||
@@ -264,6 +274,7 @@ final class PlayerEngine {
|
||||
itemId = nil
|
||||
errorMessage = nil
|
||||
isSeeking = false
|
||||
chapters = []
|
||||
currentTitle = ""
|
||||
currentAuthor = ""
|
||||
currentCoverURL = nil
|
||||
@@ -284,6 +295,8 @@ final class PlayerEngine {
|
||||
if isPlaying { startSleepTickTask() }
|
||||
case .endOfBook:
|
||||
sleepRemainingSeconds = wallclockRemainingUntilEndOfBook()
|
||||
case .endOfChapter:
|
||||
sleepRemainingSeconds = wallclockRemainingUntilEndOfChapter()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -322,6 +335,22 @@ final class PlayerEngine {
|
||||
return playback / r
|
||||
}
|
||||
|
||||
private func updateEndOfChapterSleep() {
|
||||
guard case .endOfChapter = sleepTimer else { return }
|
||||
sleepRemainingSeconds = wallclockRemainingUntilEndOfChapter()
|
||||
if sleepRemainingSeconds <= 0 {
|
||||
sleepTimer = .off
|
||||
pause()
|
||||
}
|
||||
}
|
||||
|
||||
private func wallclockRemainingUntilEndOfChapter() -> Double {
|
||||
let chapterEnd = currentChapter?.end ?? totalDuration
|
||||
let playback = max(0, chapterEnd - absoluteCurrentTime)
|
||||
let r = max(0.1, Double(rate))
|
||||
return playback / r
|
||||
}
|
||||
|
||||
// MARK: - Now-playing / remote commands
|
||||
|
||||
private func configureRemoteCommandsIfNeeded() {
|
||||
|
||||
Reference in New Issue
Block a user