import SwiftUI import AVFoundation struct PlayerView: View { let book: AudiobookItem @EnvironmentObject var authManager: AuthManager @StateObject private var vm = PlayerViewModel() var body: some View { VStack(spacing: 24) { // Cover if let coverURL = book.coverURL, let url = URL(string: coverURL) { AsyncImage(url: url) { image in image.resizable().aspectRatio(contentMode: .fit) } placeholder: { Rectangle().fill(Color.gray.opacity(0.3)) .overlay(Image(systemName: "book.fill").font(.largeTitle).foregroundColor(.gray)) } .frame(maxWidth: 280, maxHeight: 280) .cornerRadius(12) .shadow(radius: 8) } else { Rectangle().fill(Color.gray.opacity(0.3)) .frame(width: 280, height: 280) .cornerRadius(12) .overlay(Image(systemName: "book.fill").font(.largeTitle).foregroundColor(.gray)) } // Titel & Autor VStack(spacing: 4) { Text(book.title) .font(.title2).bold() .multilineTextAlignment(.center) Text(book.author) .font(.subheadline) .foregroundColor(.secondary) } .padding(.horizontal) // Fortschrittsbalken VStack(spacing: 4) { Slider(value: $vm.currentTime, in: 0...max(vm.duration, 1)) { editing in if !editing { vm.seek(to: vm.currentTime) } } .padding(.horizontal) HStack { Text(formatTime(vm.currentTime)) Spacer() Text(formatTime(vm.duration)) } .font(.caption) .foregroundColor(.secondary) .padding(.horizontal) } // Steuerung HStack(spacing: 40) { Button(action: { vm.skip(-30) }) { Image(systemName: "gobackward.30") .font(.title) } Button(action: { vm.togglePlayPause() }) { Image(systemName: vm.isPlaying ? "pause.circle.fill" : "play.circle.fill") .font(.system(size: 64)) } Button(action: { vm.skip(30) }) { Image(systemName: "goforward.30") .font(.title) } } .foregroundColor(.accentColor) // Wiedergabegeschwindigkeit HStack { Text("Geschwindigkeit:") .font(.caption) Picker("", selection: $vm.playbackRate) { Text("0.75x").tag(Float(0.75)) Text("1.0x").tag(Float(1.0)) Text("1.25x").tag(Float(1.25)) Text("1.5x").tag(Float(1.5)) Text("2.0x").tag(Float(2.0)) } .pickerStyle(.segmented) } .padding(.horizontal) Spacer() } .padding(.top) .navigationTitle("Player") .navigationBarTitleDisplayMode(.inline) .onAppear { vm.setup(book: book, authManager: authManager) } .onDisappear { vm.saveProgress(authManager: authManager) } } func formatTime(_ seconds: Double) -> String { let s = Int(seconds) return String(format: "%d:%02d:%02d", s/3600, (s%3600)/60, s%60) } } // MARK: - PlayerViewModel class PlayerViewModel: ObservableObject { @Published var isPlaying = false @Published var currentTime: Double = 0 @Published var duration: Double = 0 @Published var playbackRate: Float = 1.0 { didSet { player?.rate = isPlaying ? playbackRate : 0 } } private var player: AVPlayer? private var playerItem: AVPlayerItem? private var timeObserver: Any? private var book: AudiobookItem? private var authManager: AuthManager? func setup(book: AudiobookItem, authManager: AuthManager) { self.book = book self.authManager = authManager guard let serverURL = authManager.serverURL, let token = authManager.token, let firstFile = book.mediaFiles.first else { return } let urlString = "\(serverURL)/api/items/\(book.id)/file/\(firstFile.ino)" guard let url = URL(string: urlString) else { return } var request = URLRequest(url: url) request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") let asset = AVURLAsset(url: url, options: ["AVURLAssetHTTPHeaderFieldsKey": ["Authorization": "Bearer \(token)"]]) playerItem = AVPlayerItem(asset: asset) player = AVPlayer(playerItem: playerItem) // Duration Task { if let dur = try? await asset.load(.duration) { await MainActor.run { self.duration = dur.seconds } } } // Fortschritt laden Task { if let progress = try? await ABSService.shared.fetchProgress( serverURL: serverURL, token: token, itemId: book.id) { await MainActor.run { self.currentTime = progress.currentTime self.player?.seek(to: CMTime(seconds: progress.currentTime, preferredTimescale: 1000)) } } } // Time Observer let interval = CMTime(seconds: 1, preferredTimescale: 1000) timeObserver = player?.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [weak self] time in self?.currentTime = time.seconds } } func togglePlayPause() { guard let player else { return } if isPlaying { player.pause() } else { player.rate = playbackRate } isPlaying.toggle() } func skip(_ seconds: Double) { guard let player else { return } let newTime = max(0, min(currentTime + seconds, duration)) player.seek(to: CMTime(seconds: newTime, preferredTimescale: 1000)) currentTime = newTime } func seek(to time: Double) { player?.seek(to: CMTime(seconds: time, preferredTimescale: 1000)) } func saveProgress(authManager: AuthManager) { guard let book, let serverURL = authManager.serverURL, let token = authManager.token else { return } Task { await ABSService.shared.updateProgress( serverURL: serverURL, token: token, itemId: book.id, currentTime: currentTime, duration: duration ) } } deinit { if let timeObserver { player?.removeTimeObserver(timeObserver) } player?.pause() } }