Skip to content

Commit

Permalink
implement volume slider
Browse files Browse the repository at this point in the history
  • Loading branch information
Lee Jun Kit committed Jun 3, 2021
1 parent b315146 commit 066142d
Show file tree
Hide file tree
Showing 10 changed files with 174 additions and 58 deletions.
8 changes: 8 additions & 0 deletions Spottie.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@
473061A12659218F001E3A1F /* SpottieError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 473061A02659218F001E3A1F /* SpottieError.swift */; };
473061A42659FFF5001E3A1F /* CurrentlyPlayingContextObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 473061A32659FFF5001E3A1F /* CurrentlyPlayingContextObject.swift */; };
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 */; };
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
Expand Down Expand Up @@ -108,6 +110,8 @@
473061A02659218F001E3A1F /* SpottieError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpottieError.swift; sourceTree = "<group>"; };
473061A32659FFF5001E3A1F /* CurrentlyPlayingContextObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentlyPlayingContextObject.swift; sourceTree = "<group>"; };
473061A5265B4A10001E3A1F /* WebAPITrackObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebAPITrackObject.swift; sourceTree = "<group>"; };
474BAFB7266876030006EB16 /* VolumeSlider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VolumeSlider.swift; sourceTree = "<group>"; };
474BAFB926687AB60006EB16 /* WebAPIDeviceObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebAPIDeviceObject.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -186,6 +190,7 @@
470201C2265CF29B0030ECA9 /* NowPlaying.swift */,
470201C7265CF8D90030ECA9 /* PlayerControls.swift */,
470201CD265DE8720030ECA9 /* TrackProgressSlider.swift */,
474BAFB7266876030006EB16 /* VolumeSlider.swift */,
);
path = Components;
sourceTree = "<group>";
Expand Down Expand Up @@ -254,6 +259,7 @@
470201B2265B55F30030ECA9 /* WebAPISimplifiedArtistObject.swift */,
470201B4265B56350030ECA9 /* WebAPIImageObject.swift */,
470201B6265B56860030ECA9 /* WebAPIArtistObject.swift */,
474BAFB926687AB60006EB16 /* WebAPIDeviceObject.swift */,
);
path = Base;
sourceTree = "<group>";
Expand Down Expand Up @@ -346,6 +352,7 @@
470201BD265B80970030ECA9 /* FakePlayerViewModel.swift in Sources */,
4730618326590931001E3A1F /* TrackObject.swift in Sources */,
470201BB265B7C190030ECA9 /* PlayerStateProtocol.swift in Sources */,
474BAFBA26687AB60006EB16 /* WebAPIDeviceObject.swift in Sources */,
4730615F2656573B001E3A1F /* Sidebar.swift in Sources */,
470201CC265CF9610030ECA9 /* RepeatButton.swift in Sources */,
4730618B26591E36001E3A1F /* CoverGroupObject.swift in Sources */,
Expand Down Expand Up @@ -377,6 +384,7 @@
470201B7265B56860030ECA9 /* WebAPIArtistObject.swift in Sources */,
473061A6265B4A10001E3A1F /* WebAPITrackObject.swift in Sources */,
473061852659164E001E3A1F /* AlbumObject.swift in Sources */,
474BAFB8266876030006EB16 /* VolumeSlider.swift in Sources */,
4730619D26592023001E3A1F /* PlaybackPausedEvent.swift in Sources */,
4730618F26591E9C001E3A1F /* ContextChangedEvent.swift in Sources */,
4730616526565841001E3A1F /* Library.swift in Sources */,
Expand Down
11 changes: 11 additions & 0 deletions Spottie/Backend/SpotifyAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,15 @@ extension SpotifyAPI {
req.httpMethod = "POST"
return client.run(req).print().map(\.value).eraseToAnyPublisher()
}

static func setVolume(volumePercent: Float) -> AnyPublisher<Nothing?, Error> {
let rawVolume = Int(volumePercent * 65535)
let queryItems = [URLQueryItem(name: "volume", value: "\(rawVolume)")]
var urlComponents = URLComponents(url: base.appendingPathComponent("/player/set-volume"), resolvingAgainstBaseURL: false)!
urlComponents.queryItems = queryItems

var req = URLRequest(url: urlComponents.url!)
req.httpMethod = "POST"
return client.run(req).print().map(\.value).eraseToAnyPublisher()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ enum RepeatState: String, Codable {
}

struct CurrentlyPlayingContextObject: Codable {
var device: WebAPIDeviceObject
var isPlaying: Bool
var shuffleState: Bool
var repeatState: RepeatState
Expand Down
16 changes: 16 additions & 0 deletions Spottie/Backend/Types/Base/WebAPIDeviceObject.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//
// WebAPIDeviceObject.swift
// Spottie
//
// Created by Lee Jun Kit on 3/6/21.
//

import Foundation

struct WebAPIDeviceObject: Codable {
var id: String
var isActive: Bool
var name: String
var type: String
var volumePercent: Float
}
2 changes: 2 additions & 0 deletions Spottie/ViewModels/FakePlayerViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import Foundation

final class FakePlayerViewModel: PlayerStateProtocol {
var volumePercent : Float = 0.5
var isPlaying = false
var durationMs = 1200
var progressMs = 3600
Expand All @@ -20,4 +21,5 @@ final class FakePlayerViewModel: PlayerStateProtocol {
func nextTrack() {}
func togglePlayPause() {}
func seek(toPercent: Double) {}
func setVolume(volumePercent: Float) {}
}
2 changes: 2 additions & 0 deletions Spottie/ViewModels/PlayerStateProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import Foundation
import Combine

protocol PlayerStateProtocol: ObservableObject {
var volumePercent: Float { get set }
var isPlaying: Bool { get set }
var trackName: String { get set }
var artistName: String { get set }
Expand All @@ -20,4 +21,5 @@ protocol PlayerStateProtocol: ObservableObject {
func nextTrack() -> Void
func previousTrack() -> Void
func seek(toPercent: Double) -> Void
func setVolume(volumePercent: Float) -> Void
}
99 changes: 55 additions & 44 deletions Spottie/ViewModels/PlayerViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ import Foundation
import Combine

class PlayerViewModel: PlayerStateProtocol {
@Published var volumePercent: Float = 0.0
@Published var isPlaying = false
@Published var durationMs = 0
@Published var progressMs = 0
@Published var trackName = ""
@Published var artistName = ""
@Published var artworkURL: URL?


private var cancellables = [AnyCancellable]()
private var eventBroker: EventBroker

Expand All @@ -25,60 +25,66 @@ class PlayerViewModel: PlayerStateProtocol {
let token = SpotifyAPI.currentPlayerState().sink(receiveCompletion: {[weak self] status in
if case .failure(let error) = status {
print(error)
}

guard let self = self else { return }

// subscribe to events
eventBroker.onEventReceived.sink(receiveValue: {[weak self] event in
} else {
guard let self = self else { return }

switch (event.data) {
case .playbackEnded(_):
self.isPlaying = false
case let .playbackPaused(playbackPausedEvent):
self.progressMs = playbackPausedEvent.trackTime
self.isPlaying = false
case let .playbackResumed(playbackResumedEvent):
self.isPlaying = true
self.progressMs = playbackResumedEvent.trackTime
case let .trackSeeked(trackSeekedEvent):
self.progressMs = trackSeekedEvent.trackTime
case let .trackChanged(trackChangedEvent):
self.progressMs = 0
self.isPlaying = true
if let track = trackChangedEvent.track {
self.trackName = track.name
self.artistName = track.artist[0].name
}
case let .metadataAvailable(metadataAvailableEvent):
self.trackName = metadataAvailableEvent.track.name
self.artistName = metadataAvailableEvent.track.artist[0].name
self.artworkURL = metadataAvailableEvent.track.album.coverGroup.getArtworkURL()
self.durationMs = metadataAvailableEvent.track.duration
default:
break;
}

print(" in event recevied: self.durationMs: \(self.durationMs) self.progressMs: \(self.progressMs)")
}).store(in: &self.cancellables)
// subscribe to events
eventBroker.onEventReceived.sink(receiveValue: {[weak self] event in
guard let self = self else { return }
self.onEventReceived(event: event)
}).store(in: &self.cancellables)
}
}) {[weak self] context in
guard let self = self else { return }
if let ctx = context {
self.isPlaying = ctx.isPlaying
self.trackName = ctx.item.name
self.artistName = ctx.item.artists[0].name
self.artworkURL = ctx.item.album.getArtworkURL()
self.durationMs = ctx.item.durationMs
self.progressMs = ctx.progressMs
self.handleInitialStateUpdate(context: ctx)
}

print(" in value recevied: self.durationMs: \(self.durationMs) self.progressMs: \(self.progressMs)")
}

token.store(in: &cancellables)
}

func handleInitialStateUpdate(context ctx: CurrentlyPlayingContextObject) {
self.volumePercent = ctx.device.volumePercent
self.isPlaying = ctx.isPlaying
self.trackName = ctx.item.name
self.artistName = ctx.item.artists[0].name
self.artworkURL = ctx.item.album.getArtworkURL()
self.durationMs = ctx.item.durationMs
self.progressMs = ctx.progressMs
}

func onEventReceived(event: SpotifyEvent) {
switch (event.data) {
case let .volumeChanged(volumeChangedEvent):
self.volumePercent = volumeChangedEvent.value
case .playbackEnded(_):
self.isPlaying = false
case let .playbackPaused(playbackPausedEvent):
self.progressMs = playbackPausedEvent.trackTime
self.isPlaying = false
case let .playbackResumed(playbackResumedEvent):
self.isPlaying = true
self.progressMs = playbackResumedEvent.trackTime
case let .trackSeeked(trackSeekedEvent):
self.progressMs = trackSeekedEvent.trackTime
case let .trackChanged(trackChangedEvent):
self.progressMs = 0
self.isPlaying = true
if let track = trackChangedEvent.track {
self.trackName = track.name
self.artistName = track.artist[0].name
}
case let .metadataAvailable(metadataAvailableEvent):
self.trackName = metadataAvailableEvent.track.name
self.artistName = metadataAvailableEvent.track.artist[0].name
self.artworkURL = metadataAvailableEvent.track.album.coverGroup.getArtworkURL()
self.durationMs = metadataAvailableEvent.track.duration
default:
break;
}
}

func togglePlayPause() {
var apiCall: AnyPublisher<Nothing?, Error>;
if self.isPlaying {
Expand Down Expand Up @@ -106,4 +112,9 @@ class PlayerViewModel: PlayerStateProtocol {
let posMs = Int(Double(self.durationMs) * toPercent)
SpotifyAPI.seek(posMs: posMs).sink { _ in } receiveValue: { _ in }.store(in: &cancellables)
}

func setVolume(volumePercent: Float) {
self.volumePercent = volumePercent
SpotifyAPI.setVolume(volumePercent: volumePercent).sink { _ in } receiveValue: { _ in }.store(in: &cancellables)
}
}
5 changes: 5 additions & 0 deletions Spottie/Views/BottomBar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ struct BottomBar<M: PlayerStateProtocol>: View {
)
.frame(width: 240, alignment: .leading)
PlayerControls<M>()
VolumeSlider(
volumePercent: viewModel.volumePercent,
onVolumeChanged: viewModel.setVolume
)
.frame(width: 160, alignment: .trailing)
}
}
}
Expand Down
37 changes: 23 additions & 14 deletions Spottie/Views/Components/TrackProgressSlider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,21 +31,30 @@ struct TrackProgressSlider: View {
}

var body: some View {
Slider(
value: $viewModel.progressPercent,
onEditingChanged: { editing in
if (!editing) {
viewModel.onScrubToNewProgressPercent(viewModel.progressPercent)
HStack {
VStack(alignment: .trailing) {
Text(prettyProgress).foregroundColor(.secondary)
}
.frame(width: 40)

Slider(
value: $viewModel.progressPercent,
in: 0...1,
onEditingChanged: { editing in
if (!editing) {
viewModel.onScrubToNewProgressPercent(viewModel.progressPercent)
}
viewModel.isScrubbing = editing
}
viewModel.isScrubbing = editing
},
minimumValueLabel: Text(prettyProgress).foregroundColor(.secondary),
maximumValueLabel: Text(prettyDuration).foregroundColor(.secondary)
) {
EmptyView()
}
.onReceive(timer) { _ in
viewModel.updateProgress()
)
.onReceive(timer) { _ in
viewModel.updateProgress()
}
VStack(alignment: .leading) {
Text(prettyDuration).foregroundColor(.secondary)
}
.frame(width: 40)
}
}
}
Expand Down
51 changes: 51 additions & 0 deletions Spottie/Views/Components/VolumeSlider.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
//
// VolumeSlider.swift
// Spottie
//
// Created by Lee Jun Kit on 3/6/21.
//

import SwiftUI

struct VolumeSlider: View {
var volumePercent: Float
var onVolumeChanged: (Float) -> Void

var imageName: String {
get {
if (volumePercent > 0.8) {
return "speaker.wave.3.fill"
} else if (volumePercent > 0.5) {
return "speaker.wave.2.fill"
} else if (volumePercent > 0.3) {
return "speaker.wave.1.fill"
} else {
return "speaker.fill"
}
}
}

var body: some View {
Slider(value: Binding(get: {
self.volumePercent
}, set: { newVolumePercent in
self.onVolumeChanged(newVolumePercent)
}), in: 0...1) {
VStack(alignment: .trailing) {
Image(systemName: imageName)
.foregroundColor(.secondary)
}
.frame(width: 26, height: 26)
}
}
}

struct VolumeSlider_Previews: PreviewProvider {
static func onVolumeChanged(volumePercent: Float) {
print("new volume: \(volumePercent)")
}

static var previews: some View {
VolumeSlider(volumePercent: Float(0.5), onVolumeChanged: onVolumeChanged)
}
}

0 comments on commit 066142d

Please sign in to comment.