Version 1.1 App Store

This commit is contained in:
Scarriffle
2026-05-25 10:21:11 +02:00
parent 7ca511d37f
commit 15d8e71d09
17 changed files with 478 additions and 71 deletions

View File

@@ -11,6 +11,12 @@ import AppKit
private typealias PlayerArtworkImage = NSImage
#endif
enum SleepTimerMode: Equatable, Hashable {
case off
case minutes(Int)
case endOfBook
}
@Observable
@MainActor
final class PlayerEngine {
@@ -21,6 +27,11 @@ final class PlayerEngine {
var isReady: Bool = false
var errorMessage: String?
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.
var sleepRemainingSeconds: Double = 0
private var player: AVQueuePlayer?
private var trackDurations: [Double] = []
private var trackPlayerItems: [AVPlayerItem] = []
@@ -28,6 +39,7 @@ final class PlayerEngine {
private var timeObserver: Any?
private var endObservers: [NSObjectProtocol] = []
private var isSeeking: Bool = false
private var sleepTickTask: Task<Void, Never>?
var itemId: String?
@@ -127,12 +139,16 @@ final class PlayerEngine {
player?.play()
player?.rate = rate
isPlaying = true
if case .minutes = sleepTimer, sleepRemainingSeconds > 0, sleepTickTask == nil {
startSleepTickTask()
}
updateNowPlayingInfo()
}
func pause() {
player?.pause()
isPlaying = false
cancelSleepTickTask()
updateNowPlayingInfo()
}
@@ -143,6 +159,9 @@ final class PlayerEngine {
func setRate(_ newRate: Float) {
rate = newRate
if isPlaying { player?.rate = newRate }
if case .endOfBook = sleepTimer {
sleepRemainingSeconds = wallclockRemainingUntilEndOfBook()
}
updateNowPlayingInfo()
}
@@ -221,9 +240,13 @@ final class PlayerEngine {
let wasPlaying = isPlaying
isPlaying = player.timeControlStatus == .playing
if wasPlaying != isPlaying { updateNowPlayingInfo() }
updateEndOfBookSleep()
}
func teardown() {
cancelSleepTickTask()
sleepTimer = .off
sleepRemainingSeconds = 0
if let token = timeObserver { player?.removeTimeObserver(token) }
timeObserver = nil
for obs in endObservers { NotificationCenter.default.removeObserver(obs) }
@@ -248,6 +271,57 @@ final class PlayerEngine {
clearNowPlayingInfo()
}
// MARK: - Sleep timer
func setSleepTimer(_ mode: SleepTimerMode) {
cancelSleepTickTask()
sleepTimer = mode
switch mode {
case .off:
sleepRemainingSeconds = 0
case .minutes(let m):
sleepRemainingSeconds = Double(m * 60)
if isPlaying { startSleepTickTask() }
case .endOfBook:
sleepRemainingSeconds = wallclockRemainingUntilEndOfBook()
}
}
private func startSleepTickTask() {
cancelSleepTickTask()
sleepTickTask = Task { @MainActor [weak self] in
while !Task.isCancelled {
try? await Task.sleep(nanoseconds: 500_000_000)
guard !Task.isCancelled, let self else { return }
self.sleepRemainingSeconds = max(0, self.sleepRemainingSeconds - 0.5)
if self.sleepRemainingSeconds <= 0 {
self.sleepTimer = .off
self.pause()
return
}
}
}
}
private func cancelSleepTickTask() {
sleepTickTask?.cancel()
sleepTickTask = nil
}
private func updateEndOfBookSleep() {
guard case .endOfBook = sleepTimer else { return }
sleepRemainingSeconds = wallclockRemainingUntilEndOfBook()
if sleepRemainingSeconds <= 0 {
sleepTimer = .off
}
}
private func wallclockRemainingUntilEndOfBook() -> Double {
let playback = max(0, totalDuration - absoluteCurrentTime)
let r = max(0.1, Double(rate))
return playback / r
}
// MARK: - Now-playing / remote commands
private func configureRemoteCommandsIfNeeded() {