From 79188d549aed26e19269c7753e058c2a2b54d2b6 Mon Sep 17 00:00:00 2001 From: Lee Jun Kit Date: Thu, 3 Jun 2021 15:28:15 +0800 Subject: [PATCH] wire up repeat and shuffle buttons --- Spottie.xcodeproj/project.pbxproj | 4 +++ Spottie/Backend/SpotifyAPI.swift | 20 +++++++++++ Spottie/Backend/Types/Base/RepeatMode.swift | 10 ++++++ Spottie/ViewModels/FakePlayerViewModel.swift | 5 ++- Spottie/ViewModels/PlayerStateProtocol.swift | 4 +++ Spottie/ViewModels/PlayerViewModel.swift | 32 +++++++++++++++--- Spottie/Views/Components/PlayerControls.swift | 10 ++++-- Spottie/Views/Components/RepeatButton.swift | 33 ++++++++++++++++--- Spottie/Views/Components/ShuffleButton.swift | 8 ++++- 9 files changed, 114 insertions(+), 12 deletions(-) create mode 100644 Spottie/Backend/Types/Base/RepeatMode.swift diff --git a/Spottie.xcodeproj/project.pbxproj b/Spottie.xcodeproj/project.pbxproj index 3e53245..737e813 100644 --- a/Spottie.xcodeproj/project.pbxproj +++ b/Spottie.xcodeproj/project.pbxproj @@ -57,6 +57,7 @@ 473061A6265B4A10001E3A1F /* WebAPITrackObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 473061A5265B4A10001E3A1F /* WebAPITrackObject.swift */; }; 474BAFB8266876030006EB16 /* VolumeSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 474BAFB7266876030006EB16 /* VolumeSlider.swift */; }; 474BAFBA26687AB60006EB16 /* WebAPIDeviceObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 474BAFB926687AB60006EB16 /* WebAPIDeviceObject.swift */; }; + 474BAFBC2668B0170006EB16 /* RepeatMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 474BAFBB2668B0170006EB16 /* RepeatMode.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -112,6 +113,7 @@ 473061A5265B4A10001E3A1F /* WebAPITrackObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebAPITrackObject.swift; sourceTree = ""; }; 474BAFB7266876030006EB16 /* VolumeSlider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VolumeSlider.swift; sourceTree = ""; }; 474BAFB926687AB60006EB16 /* WebAPIDeviceObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebAPIDeviceObject.swift; sourceTree = ""; }; + 474BAFBB2668B0170006EB16 /* RepeatMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepeatMode.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -260,6 +262,7 @@ 470201B4265B56350030ECA9 /* WebAPIImageObject.swift */, 470201B6265B56860030ECA9 /* WebAPIArtistObject.swift */, 474BAFB926687AB60006EB16 /* WebAPIDeviceObject.swift */, + 474BAFBB2668B0170006EB16 /* RepeatMode.swift */, ); path = Base; sourceTree = ""; @@ -364,6 +367,7 @@ 470201C3265CF29B0030ECA9 /* NowPlaying.swift in Sources */, 4730616A26565BB7001E3A1F /* BottomBar.swift in Sources */, 473061722656629F001E3A1F /* Nothing.swift in Sources */, + 474BAFBC2668B0170006EB16 /* RepeatMode.swift in Sources */, 473061A12659218F001E3A1F /* SpottieError.swift in Sources */, 4730616F26566076001E3A1F /* SpotifyAPI.swift in Sources */, 473061A42659FFF5001E3A1F /* CurrentlyPlayingContextObject.swift in Sources */, diff --git a/Spottie/Backend/SpotifyAPI.swift b/Spottie/Backend/SpotifyAPI.swift index 464ac69..2d8f4a7 100644 --- a/Spottie/Backend/SpotifyAPI.swift +++ b/Spottie/Backend/SpotifyAPI.swift @@ -65,4 +65,24 @@ extension SpotifyAPI { req.httpMethod = "POST" return client.run(req).print().map(\.value).eraseToAnyPublisher() } + + static func setShuffle(shuffle: Bool) -> AnyPublisher { + let queryItems = [URLQueryItem(name: "val", value: shuffle ? "true" : "false")] + var urlComponents = URLComponents(url: base.appendingPathComponent("/player/shuffle"), resolvingAgainstBaseURL: false)! + urlComponents.queryItems = queryItems + + var req = URLRequest(url: urlComponents.url!) + req.httpMethod = "POST" + return client.run(req).print().map(\.value).eraseToAnyPublisher() + } + + static func setRepeatMode(mode: RepeatMode) -> AnyPublisher { + let queryItems = [URLQueryItem(name: "val", value: mode.rawValue)] + var urlComponents = URLComponents(url: base.appendingPathComponent("/player/repeat"), resolvingAgainstBaseURL: false)! + urlComponents.queryItems = queryItems + + var req = URLRequest(url: urlComponents.url!) + req.httpMethod = "POST" + return client.run(req).print().map(\.value).eraseToAnyPublisher() + } } diff --git a/Spottie/Backend/Types/Base/RepeatMode.swift b/Spottie/Backend/Types/Base/RepeatMode.swift new file mode 100644 index 0000000..8a49402 --- /dev/null +++ b/Spottie/Backend/Types/Base/RepeatMode.swift @@ -0,0 +1,10 @@ +// +// RepeatMode.swift +// Spottie +// +// Created by Lee Jun Kit on 3/6/21. +// + +enum RepeatMode: String, Codable { + case none, track, context +} diff --git a/Spottie/ViewModels/FakePlayerViewModel.swift b/Spottie/ViewModels/FakePlayerViewModel.swift index 13476b5..f750025 100644 --- a/Spottie/ViewModels/FakePlayerViewModel.swift +++ b/Spottie/ViewModels/FakePlayerViewModel.swift @@ -12,7 +12,8 @@ final class FakePlayerViewModel: PlayerStateProtocol { var isPlaying = false var durationMs = 1200 var progressMs = 3600 - + var isShuffling = false + var repeatMode = RepeatMode.none var trackName = "Track Name" var artistName = "Artist Name" var artworkURL = URL(string: "https://i.scdn.co/image/ab67616d00004851a48964b5d9a3d6968ae3e0de") @@ -22,4 +23,6 @@ final class FakePlayerViewModel: PlayerStateProtocol { func togglePlayPause() {} func seek(toPercent: Double) {} func setVolume(volumePercent: Float) {} + func toggleShuffle() {} + func cycleRepeatMode() {} } diff --git a/Spottie/ViewModels/PlayerStateProtocol.swift b/Spottie/ViewModels/PlayerStateProtocol.swift index 2db044d..aa3ebf8 100644 --- a/Spottie/ViewModels/PlayerStateProtocol.swift +++ b/Spottie/ViewModels/PlayerStateProtocol.swift @@ -16,10 +16,14 @@ protocol PlayerStateProtocol: ObservableObject { var artworkURL: URL? { get set } var durationMs: Int { get set } var progressMs: Int { get set } + var isShuffling: Bool { get set } + var repeatMode: RepeatMode { get set } func togglePlayPause() -> Void func nextTrack() -> Void func previousTrack() -> Void func seek(toPercent: Double) -> Void func setVolume(volumePercent: Float) -> Void + func toggleShuffle() -> Void + func cycleRepeatMode() -> Void } diff --git a/Spottie/ViewModels/PlayerViewModel.swift b/Spottie/ViewModels/PlayerViewModel.swift index b29a6c5..31b6cf2 100644 --- a/Spottie/ViewModels/PlayerViewModel.swift +++ b/Spottie/ViewModels/PlayerViewModel.swift @@ -16,6 +16,8 @@ class PlayerViewModel: PlayerStateProtocol { @Published var trackName = "" @Published var artistName = "" @Published var artworkURL: URL? + @Published var isShuffling = false + @Published var repeatMode = RepeatMode.none private var cancellables = [AnyCancellable]() private var eventBroker: EventBroker @@ -85,6 +87,10 @@ class PlayerViewModel: PlayerStateProtocol { } } + private func connectNothingPublisher(_ publisher: AnyPublisher) { + publisher.sink { _ in } receiveValue: { _ in }.store(in: &cancellables) + } + func togglePlayPause() { var apiCall: AnyPublisher; if self.isPlaying { @@ -93,11 +99,11 @@ class PlayerViewModel: PlayerStateProtocol { apiCall = SpotifyAPI.resume() } - apiCall.print().sink { _ in } receiveValue: { _ in }.store(in: &cancellables) + self.connectNothingPublisher(apiCall) } func nextTrack() { - SpotifyAPI.nextTrack().sink { _ in } receiveValue: { _ in }.store(in: &cancellables) + self.connectNothingPublisher(SpotifyAPI.nextTrack()) } func previousTrack() { @@ -110,11 +116,29 @@ class PlayerViewModel: PlayerStateProtocol { func seek(toPercent: Double) { // calculate posMs let posMs = Int(Double(self.durationMs) * toPercent) - SpotifyAPI.seek(posMs: posMs).sink { _ in } receiveValue: { _ in }.store(in: &cancellables) + self.connectNothingPublisher(SpotifyAPI.seek(posMs: posMs)) } func setVolume(volumePercent: Float) { self.volumePercent = volumePercent - SpotifyAPI.setVolume(volumePercent: volumePercent).sink { _ in } receiveValue: { _ in }.store(in: &cancellables) + self.connectNothingPublisher(SpotifyAPI.setVolume(volumePercent: volumePercent)) + } + + func toggleShuffle() { + self.isShuffling = !self.isShuffling + self.connectNothingPublisher(SpotifyAPI.setShuffle(shuffle: self.isShuffling)) + } + + func cycleRepeatMode() { + switch self.repeatMode { + case .none: + self.repeatMode = .track + case .track: + self.repeatMode = .context + case .context: + self.repeatMode = .none + } + + self.connectNothingPublisher(SpotifyAPI.setRepeatMode(mode: self.repeatMode)) } } diff --git a/Spottie/Views/Components/PlayerControls.swift b/Spottie/Views/Components/PlayerControls.swift index f5cba61..afd8117 100644 --- a/Spottie/Views/Components/PlayerControls.swift +++ b/Spottie/Views/Components/PlayerControls.swift @@ -13,7 +13,10 @@ struct PlayerControls: View { var body: some View { VStack { HStack(spacing: 28) { - ShuffleButton() + ShuffleButton( + isShuffling: viewModel.isShuffling, + toggle: viewModel.toggleShuffle + ) PreviousTrackButton( previousTrackButtonTapped: viewModel.previousTrack ) @@ -24,7 +27,10 @@ struct PlayerControls: View { NextTrackButton( nextTrackButtonTapped: viewModel.nextTrack ) - RepeatButton() + RepeatButton( + repeatMode: viewModel.repeatMode, + onRepeatButtonTapped: viewModel.cycleRepeatMode + ) } TrackProgressSlider(viewModel: TrackProgressSlider.ViewModel(isPlaying: viewModel.isPlaying, progressMs: viewModel.progressMs, durationMs: viewModel.durationMs, onScrubToNewProgressPercent: viewModel.seek)) .padding(.leading) diff --git a/Spottie/Views/Components/RepeatButton.swift b/Spottie/Views/Components/RepeatButton.swift index 9269d61..00a3dad 100644 --- a/Spottie/Views/Components/RepeatButton.swift +++ b/Spottie/Views/Components/RepeatButton.swift @@ -8,19 +8,44 @@ import SwiftUI struct RepeatButton: View { + var repeatMode: RepeatMode + var onRepeatButtonTapped: () -> Void + var imageName: String { + get { + switch repeatMode { + case .none: + return "repeat" + case .track: + return "repeat.1" + case .context: + return "repeat" + } + } + } + var body: some View { Button(action: { + onRepeatButtonTapped() }) { - Image(systemName: "repeat") - .resizable() - .frame(width: 12, height: 12) + HStack { + Image(systemName: imageName) + .resizable() + .frame(width: 12, height: 12) + if repeatMode == .context { + Text("ALL") + .font(.footnote) + } + } + } .buttonStyle(BorderlessButtonStyle()) + .foregroundColor(repeatMode == .none ? .secondary : .green) } } struct RepeatButton_Previews: PreviewProvider { + static func onRepeatButtonTapped() {} static var previews: some View { - RepeatButton() + RepeatButton(repeatMode: .none, onRepeatButtonTapped: onRepeatButtonTapped) } } diff --git a/Spottie/Views/Components/ShuffleButton.swift b/Spottie/Views/Components/ShuffleButton.swift index cce8233..6c84a8e 100644 --- a/Spottie/Views/Components/ShuffleButton.swift +++ b/Spottie/Views/Components/ShuffleButton.swift @@ -8,19 +8,25 @@ import SwiftUI struct ShuffleButton: View { + var isShuffling: Bool + var toggle: () -> Void + var body: some View { Button(action: { + toggle() }) { Image(systemName: "shuffle") .resizable() .frame(width: 12, height: 12) } .buttonStyle(BorderlessButtonStyle()) + .foregroundColor(isShuffling ? .green : .secondary) } } struct ShuffleButton_Previews: PreviewProvider { + static func toggle() {} static var previews: some View { - ShuffleButton() + ShuffleButton(isShuffling: false, toggle: toggle) } }