diff --git a/Music/ViewModels/PlayerViewModel.swift b/Music/ViewModels/PlayerViewModel.swift new file mode 100644 index 0000000..55c8392 --- /dev/null +++ b/Music/ViewModels/PlayerViewModel.swift @@ -0,0 +1,104 @@ +import Foundation +import Observation + +@Observable +final class PlayerViewModel { + var currentTrack: Track? + var currentIndex: Int? + var isShuffled = false + + private(set) var queue: [Track] = [] + private var originalQueue: [Track] = [] + private let audio: AudioService + private let db: DatabaseService? + private var halfwayReported = false + + init(audio: AudioService, db: DatabaseService?) { + self.audio = audio + self.db = db + + audio.onTrackFinished = { [weak self] in + self?.trackDidFinish() + } + } + + func setQueue(_ tracks: [Track]) { + originalQueue = tracks + if isShuffled { + queue = buildShuffledQueue(from: tracks, startingWith: currentTrack) + } else { + queue = tracks + } + if let current = currentTrack { + currentIndex = queue.firstIndex(where: { $0.id == current.id }) + } + } + + func play(_ track: Track) { + currentTrack = track + currentIndex = queue.firstIndex(where: { $0.id == track.id }) + halfwayReported = false + + guard let url = URL(string: track.fileURL) else { return } + audio.play(url: url) + } + + func next() { + guard let idx = currentIndex else { return } + let nextIdx = idx + 1 + if nextIdx < queue.count { + play(queue[nextIdx]) + } else { + audio.stop() + currentTrack = nil + currentIndex = nil + } + } + + func previous() { + guard let idx = currentIndex else { return } + let prevIdx = max(0, idx - 1) + play(queue[prevIdx]) + } + + func toggleShuffle() { + isShuffled.toggle() + if isShuffled { + queue = buildShuffledQueue(from: originalQueue, startingWith: currentTrack) + } else { + queue = originalQueue + } + if let current = currentTrack { + currentIndex = queue.firstIndex(where: { $0.id == current.id }) + } + } + + func checkHalfway() { + guard !halfwayReported, + audio.duration > 0, + audio.currentTime >= audio.duration * 0.5, + let track = currentTrack, + let trackId = track.id else { return } + + halfwayReported = true + let newCount = track.playCount + 1 + try? db?.updatePlayStats(trackId: trackId, playCount: newCount, lastPlayedAt: Date()) + } + + private func trackDidFinish() { + if let track = currentTrack, let trackId = track.id, !halfwayReported { + let newCount = track.playCount + 1 + try? db?.updatePlayStats(trackId: trackId, playCount: newCount, lastPlayedAt: Date()) + } + next() + } + + private func buildShuffledQueue(from tracks: [Track], startingWith current: Track?) -> [Track] { + var shuffled = tracks.shuffled() + if let current, let idx = shuffled.firstIndex(where: { $0.id == current.id }) { + shuffled.remove(at: idx) + shuffled.insert(current, at: 0) + } + return shuffled + } +} diff --git a/MusicTests/PlayerViewModelTests.swift b/MusicTests/PlayerViewModelTests.swift new file mode 100644 index 0000000..3a973a5 --- /dev/null +++ b/MusicTests/PlayerViewModelTests.swift @@ -0,0 +1,105 @@ +import Testing +import Foundation +@testable import Music + +@MainActor +struct PlayerViewModelTests { + private func makeTracks(_ count: Int) -> [Track] { + (0..