Add Tonuino Manager website and Sparkle updates

This commit is contained in:
maddin committed 2026-10-31 16:40:48 +01:00
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
}
}