Add Tonuino Manager website and Sparkle updates
This commit is contained in:
commit
1cac427ff2
44 files changed
+5861
No files matched your search
@@ -0,0 +1,153 @@
|
||||
import AVFoundation
|
||||
import Combine
|
||||
import Foundation
|
||||
import MediaPlayer
|
||||
|
||||
@MainActor
|
||||
final class AudioPlayerModel: ObservableObject {
|
||||
@Published private(set) var title = ""
|
||||
@Published private(set) var artist = ""
|
||||
@Published private(set) var duration: Double = 0
|
||||
@Published private(set) var position: Double = 0
|
||||
@Published private(set) var isPlaying = false
|
||||
@Published private(set) var isLoading = false
|
||||
@Published private(set) var statusMessage = "Bereit"
|
||||
@Published private(set) var currentURL: URL?
|
||||
@Published var volume: Double = 0.8 { didSet { player.volume = Float(volume) } }
|
||||
|
||||
private let player = AVPlayer()
|
||||
private let commandCenter = MPRemoteCommandCenter.shared()
|
||||
private var observer: Any?
|
||||
private var itemObservation: NSKeyValueObservation?
|
||||
private var endObserver: NSObjectProtocol?
|
||||
private var remoteCommandTargets: [(MPRemoteCommand, Any)] = []
|
||||
private var queue: [MusicTrack] = []
|
||||
private var currentIndex = 0
|
||||
|
||||
init() {
|
||||
player.automaticallyWaitsToMinimizeStalling = true
|
||||
installRemoteCommands()
|
||||
observer = player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 0.25, preferredTimescale: 600), queue: .main) { [weak self] time in
|
||||
Task { @MainActor [weak self] in
|
||||
guard let self else { return }
|
||||
self.position = time.seconds.isFinite ? time.seconds : 0
|
||||
let seconds = self.player.currentItem?.duration.seconds ?? 0
|
||||
if seconds.isFinite { self.duration = seconds }
|
||||
self.isPlaying = self.player.rate > 0
|
||||
self.updateNowPlayingInfo()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
if let observer { player.removeTimeObserver(observer) }
|
||||
if let endObserver { NotificationCenter.default.removeObserver(endObserver) }
|
||||
for (command, target) in remoteCommandTargets { command.removeTarget(target) }
|
||||
}
|
||||
|
||||
func play(_ tracks: [MusicTrack], startingAt index: Int) {
|
||||
guard tracks.indices.contains(index) else { return }
|
||||
queue = tracks; currentIndex = index
|
||||
loadCurrent(autoplay: true)
|
||||
}
|
||||
|
||||
func isCurrentTrack(in tracks: [MusicTrack]) -> Bool {
|
||||
guard let currentURL else { return false }
|
||||
return tracks.contains { $0.url.standardizedFileURL == currentURL.standardizedFileURL }
|
||||
}
|
||||
|
||||
func toggle() {
|
||||
guard currentURL != nil else { statusMessage = "Wähle zuerst einen Track aus."; return }
|
||||
if isPlaying { player.pause(); isPlaying = false; statusMessage = "Pausiert" }
|
||||
else if player.currentItem?.status == .failed { loadCurrent(autoplay: true) }
|
||||
else { player.play(); isPlaying = true; statusMessage = "Wiedergabe wird gestartet…" }
|
||||
updateNowPlayingInfo()
|
||||
}
|
||||
|
||||
func seek(_ value: Double) { player.seek(to: CMTime(seconds: value, preferredTimescale: 600)) }
|
||||
func next() { guard currentIndex + 1 < queue.count else { return }; currentIndex += 1; loadCurrent(autoplay: true) }
|
||||
func previous() { guard currentIndex > 0 else { return }; currentIndex -= 1; loadCurrent(autoplay: true) }
|
||||
|
||||
private func loadCurrent(autoplay: Bool) {
|
||||
let track = queue[currentIndex]
|
||||
title = track.displayTitle; artist = track.artist; currentURL = track.url
|
||||
duration = 0; position = 0; isPlaying = false; isLoading = true
|
||||
statusMessage = "Lade \(track.url.lastPathComponent)…"
|
||||
updateNowPlayingInfo()
|
||||
|
||||
if let endObserver { NotificationCenter.default.removeObserver(endObserver); self.endObserver = nil }
|
||||
itemObservation = nil
|
||||
let item = AVPlayerItem(url: track.url)
|
||||
itemObservation = item.observe(\.status, options: [.initial, .new]) { [weak self] item, _ in
|
||||
Task { @MainActor in
|
||||
guard let self, self.currentURL == track.url else { return }
|
||||
switch item.status {
|
||||
case .readyToPlay:
|
||||
self.isLoading = false
|
||||
let seconds = item.duration.seconds
|
||||
if seconds.isFinite { self.duration = seconds }
|
||||
self.statusMessage = autoplay ? "Wiedergabe" : "Bereit zum Abspielen"
|
||||
if autoplay { self.player.play(); self.isPlaying = true }
|
||||
self.updateNowPlayingInfo()
|
||||
case .failed:
|
||||
self.isLoading = false; self.isPlaying = false
|
||||
self.statusMessage = "Wiedergabe fehlgeschlagen: \(item.error?.localizedDescription ?? "Unbekannter Fehler")"
|
||||
self.updateNowPlayingInfo()
|
||||
case .unknown: break
|
||||
@unknown default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
player.replaceCurrentItem(with: item)
|
||||
endObserver = NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: item, queue: .main) { [weak self] _ in
|
||||
Task { @MainActor in self?.next() }
|
||||
}
|
||||
}
|
||||
|
||||
private func installRemoteCommands() {
|
||||
let toggleTarget = commandCenter.togglePlayPauseCommand.addTarget { [weak self] _ in
|
||||
Task { @MainActor [weak self] in self?.toggle() }
|
||||
return .success
|
||||
}
|
||||
let playTarget = commandCenter.playCommand.addTarget { [weak self] _ in
|
||||
Task { @MainActor [weak self] in
|
||||
guard let self, self.currentURL != nil, !self.isPlaying else { return }
|
||||
self.toggle()
|
||||
}
|
||||
return .success
|
||||
}
|
||||
let pauseTarget = commandCenter.pauseCommand.addTarget { [weak self] _ in
|
||||
Task { @MainActor [weak self] in
|
||||
guard let self, self.isPlaying else { return }
|
||||
self.toggle()
|
||||
}
|
||||
return .success
|
||||
}
|
||||
let nextTarget = commandCenter.nextTrackCommand.addTarget { [weak self] _ in
|
||||
Task { @MainActor [weak self] in self?.next() }
|
||||
return .success
|
||||
}
|
||||
let previousTarget = commandCenter.previousTrackCommand.addTarget { [weak self] _ in
|
||||
Task { @MainActor [weak self] in self?.previous() }
|
||||
return .success
|
||||
}
|
||||
remoteCommandTargets = [
|
||||
(commandCenter.togglePlayPauseCommand, toggleTarget),
|
||||
(commandCenter.playCommand, playTarget),
|
||||
(commandCenter.pauseCommand, pauseTarget),
|
||||
(commandCenter.nextTrackCommand, nextTarget),
|
||||
(commandCenter.previousTrackCommand, previousTarget)
|
||||
]
|
||||
}
|
||||
|
||||
private func updateNowPlayingInfo() {
|
||||
var info: [String: Any] = [
|
||||
MPMediaItemPropertyTitle: title,
|
||||
MPMediaItemPropertyArtist: artist,
|
||||
MPNowPlayingInfoPropertyElapsedPlaybackTime: position,
|
||||
MPNowPlayingInfoPropertyPlaybackRate: isPlaying ? 1.0 : 0.0
|
||||
]
|
||||
if duration > 0 { info[MPMediaItemPropertyPlaybackDuration] = duration }
|
||||
MPNowPlayingInfoCenter.default().nowPlayingInfo = info
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user