Expose iPhone playback controls
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import Foundation
|
||||
import VelodyDomain
|
||||
import VelodyNetworking
|
||||
import VelodyPlayback
|
||||
import VelodyPersistence
|
||||
import VelodySync
|
||||
import VelodyUtilities
|
||||
@@ -15,57 +16,22 @@ final class TestPlayer: iPhoneLocalAudioPlaying {
|
||||
var missingTrackIDs = Set<String>()
|
||||
|
||||
private var catalogTracksByID: [String: LibraryTrack] = [:]
|
||||
private var queue = PlaybackQueue()
|
||||
|
||||
func setCatalogTracks(_ tracks: [LibraryTrack]) {
|
||||
catalogTracksByID = Dictionary(uniqueKeysWithValues: tracks.map { ($0.id, $0) })
|
||||
|
||||
guard let trackID = state.trackID,
|
||||
let currentTrack = catalogTracksByID[trackID]
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
state = Self.makeState(
|
||||
for: currentTrack,
|
||||
playbackState: state.playbackState,
|
||||
currentTime: state.currentTime,
|
||||
errorMessage: state.errorMessage
|
||||
queue.replaceTrackIDs(
|
||||
tracks.map(\.id),
|
||||
currentTrackID: state.trackID,
|
||||
queuedTrackIDs: state.queueTrackIDs
|
||||
)
|
||||
refreshState()
|
||||
onStateChange?(state)
|
||||
}
|
||||
|
||||
func play(trackID: String) {
|
||||
guard let track = catalogTracksByID[trackID] else {
|
||||
return
|
||||
}
|
||||
|
||||
let currentTime = state.trackID == trackID ? state.currentTime : 0
|
||||
|
||||
if missingTrackIDs.contains(trackID) ||
|
||||
track.localFilePath.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
state = Self.makeState(
|
||||
for: track,
|
||||
playbackState: .missingFile,
|
||||
currentTime: currentTime,
|
||||
errorMessage: "The local file could not be found: \(track.localFilePath)"
|
||||
)
|
||||
} else if failingTrackIDs.contains(trackID) {
|
||||
state = Self.makeState(
|
||||
for: track,
|
||||
playbackState: .failed,
|
||||
currentTime: currentTime,
|
||||
errorMessage: "Playback could not be started."
|
||||
)
|
||||
} else {
|
||||
state = Self.makeState(
|
||||
for: track,
|
||||
playbackState: .playing,
|
||||
currentTime: currentTime,
|
||||
errorMessage: nil
|
||||
)
|
||||
}
|
||||
|
||||
onStateChange?(state)
|
||||
startPlayback(trackID: trackID, currentTime: currentTime)
|
||||
}
|
||||
|
||||
func pause() {
|
||||
@@ -75,6 +41,7 @@ final class TestPlayer: iPhoneLocalAudioPlaying {
|
||||
|
||||
state = Self.makeState(
|
||||
for: currentTrack,
|
||||
queue: queue,
|
||||
playbackState: .paused,
|
||||
currentTime: state.currentTime,
|
||||
errorMessage: nil
|
||||
@@ -84,13 +51,14 @@ final class TestPlayer: iPhoneLocalAudioPlaying {
|
||||
|
||||
func stop() {
|
||||
guard let currentTrack else {
|
||||
state = .empty
|
||||
state = Self.makeEmptyState(queue: queue)
|
||||
onStateChange?(state)
|
||||
return
|
||||
}
|
||||
|
||||
state = Self.makeState(
|
||||
for: currentTrack,
|
||||
queue: queue,
|
||||
playbackState: .stopped,
|
||||
currentTime: 0,
|
||||
errorMessage: nil
|
||||
@@ -105,6 +73,7 @@ final class TestPlayer: iPhoneLocalAudioPlaying {
|
||||
|
||||
state = Self.makeState(
|
||||
for: currentTrack,
|
||||
queue: queue,
|
||||
playbackState: state.playbackState,
|
||||
currentTime: time,
|
||||
errorMessage: state.errorMessage
|
||||
@@ -112,6 +81,41 @@ final class TestPlayer: iPhoneLocalAudioPlaying {
|
||||
onStateChange?(state)
|
||||
}
|
||||
|
||||
func previousTrack() {
|
||||
if state.currentTime > 5 {
|
||||
seek(to: 0)
|
||||
return
|
||||
}
|
||||
|
||||
guard let previousTrackID = queue.moveToPreviousTrack() else {
|
||||
seek(to: 0)
|
||||
return
|
||||
}
|
||||
|
||||
startPlayback(trackID: previousTrackID, currentTime: 0)
|
||||
}
|
||||
|
||||
func nextTrack() {
|
||||
guard let nextTrackID = queue.advanceToNextTrack() else {
|
||||
stop()
|
||||
return
|
||||
}
|
||||
|
||||
startPlayback(trackID: nextTrackID, currentTime: 0)
|
||||
}
|
||||
|
||||
func toggleShuffle() {
|
||||
queue.toggleShuffle()
|
||||
refreshState()
|
||||
onStateChange?(state)
|
||||
}
|
||||
|
||||
func cycleRepeatMode() {
|
||||
queue.cycleRepeatMode()
|
||||
refreshState()
|
||||
onStateChange?(state)
|
||||
}
|
||||
|
||||
func advanceProgress(by timeDelta: Double) {
|
||||
guard state.isPlaying,
|
||||
let currentTrack
|
||||
@@ -131,6 +135,7 @@ final class TestPlayer: iPhoneLocalAudioPlaying {
|
||||
|
||||
state = Self.makeState(
|
||||
for: currentTrack,
|
||||
queue: queue,
|
||||
playbackState: playbackState,
|
||||
currentTime: updatedTime,
|
||||
errorMessage: nil
|
||||
@@ -138,6 +143,43 @@ final class TestPlayer: iPhoneLocalAudioPlaying {
|
||||
onStateChange?(state)
|
||||
}
|
||||
|
||||
private func startPlayback(trackID: String, currentTime: Double) {
|
||||
guard let track = catalogTracksByID[trackID] else {
|
||||
return
|
||||
}
|
||||
|
||||
queue.selectTrack(trackID)
|
||||
|
||||
if missingTrackIDs.contains(trackID) ||
|
||||
track.localFilePath.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
state = Self.makeState(
|
||||
for: track,
|
||||
queue: queue,
|
||||
playbackState: .missingFile,
|
||||
currentTime: currentTime,
|
||||
errorMessage: "The local file could not be found: \(track.localFilePath)"
|
||||
)
|
||||
} else if failingTrackIDs.contains(trackID) {
|
||||
state = Self.makeState(
|
||||
for: track,
|
||||
queue: queue,
|
||||
playbackState: .failed,
|
||||
currentTime: currentTime,
|
||||
errorMessage: "Playback could not be started."
|
||||
)
|
||||
} else {
|
||||
state = Self.makeState(
|
||||
for: track,
|
||||
queue: queue,
|
||||
playbackState: .playing,
|
||||
currentTime: currentTime,
|
||||
errorMessage: nil
|
||||
)
|
||||
}
|
||||
|
||||
onStateChange?(state)
|
||||
}
|
||||
|
||||
private var currentTrack: LibraryTrack? {
|
||||
guard let trackID = state.trackID else {
|
||||
return nil
|
||||
@@ -146,8 +188,24 @@ final class TestPlayer: iPhoneLocalAudioPlaying {
|
||||
return catalogTracksByID[trackID]
|
||||
}
|
||||
|
||||
private func refreshState() {
|
||||
guard let currentTrack else {
|
||||
state = Self.makeEmptyState(queue: queue)
|
||||
return
|
||||
}
|
||||
|
||||
state = Self.makeState(
|
||||
for: currentTrack,
|
||||
queue: queue,
|
||||
playbackState: state.playbackState,
|
||||
currentTime: state.currentTime,
|
||||
errorMessage: state.errorMessage
|
||||
)
|
||||
}
|
||||
|
||||
private static func makeState(
|
||||
for track: LibraryTrack,
|
||||
queue: PlaybackQueue,
|
||||
playbackState: iPhonePlaybackState,
|
||||
currentTime: Double,
|
||||
errorMessage: String?
|
||||
@@ -165,12 +223,30 @@ final class TestPlayer: iPhoneLocalAudioPlaying {
|
||||
trackID: track.id,
|
||||
title: track.title,
|
||||
artist: track.artist,
|
||||
queueTrackIDs: queue.queuedTrackIDs,
|
||||
isShuffleEnabled: queue.isShuffleEnabled,
|
||||
repeatMode: queue.repeatMode,
|
||||
playbackState: playbackState,
|
||||
currentTime: clampedTime,
|
||||
duration: duration,
|
||||
errorMessage: errorMessage
|
||||
)
|
||||
}
|
||||
|
||||
private static func makeEmptyState(queue: PlaybackQueue) -> iPhoneNowPlayingState {
|
||||
iPhoneNowPlayingState(
|
||||
trackID: nil,
|
||||
title: nil,
|
||||
artist: nil,
|
||||
queueTrackIDs: queue.queuedTrackIDs,
|
||||
isShuffleEnabled: queue.isShuffleEnabled,
|
||||
repeatMode: queue.repeatMode,
|
||||
playbackState: .stopped,
|
||||
currentTime: 0,
|
||||
duration: 0,
|
||||
errorMessage: nil
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private actor TestRemoteLibraryRepository: RemoteLibraryRepository {
|
||||
|
||||
Reference in New Issue
Block a user