Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
Lee Jun Kit committed Aug 14, 2021
1 parent 74509e6 commit 8595377
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 108 deletions.
12 changes: 12 additions & 0 deletions Spottie.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
4764B39F2684001A00AE471E /* TokenObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4764B39E2684001A00AE471E /* TokenObject.swift */; };
4764B3A1268407C600AE471E /* TrackListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4764B3A0268407C600AE471E /* TrackListItem.swift */; };
4764B3A526842ECA00AE471E /* WebAPIImageCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4764B3A426842ECA00AE471E /* WebAPIImageCollection.swift */; };
4764B3A82684867600AE471E /* DurationFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4764B3A72684867600AE471E /* DurationFormatter.swift */; };
47C56F602679A789003EA20A /* PlayerCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47C56F5F2679A789003EA20A /* PlayerCommands.swift */; };
/* End PBXBuildFile section */

Expand Down Expand Up @@ -155,6 +156,7 @@
4764B39E2684001A00AE471E /* TokenObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenObject.swift; sourceTree = "<group>"; };
4764B3A0268407C600AE471E /* TrackListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackListItem.swift; sourceTree = "<group>"; };
4764B3A426842ECA00AE471E /* WebAPIImageCollection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebAPIImageCollection.swift; sourceTree = "<group>"; };
4764B3A72684867600AE471E /* DurationFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DurationFormatter.swift; sourceTree = "<group>"; };
47C56F5F2679A789003EA20A /* PlayerCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerCommands.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

Expand Down Expand Up @@ -190,6 +192,7 @@
isa = PBXGroup;
children = (
4730616B26565EB8001E3A1F /* Backend */,
4764B3A62684865B00AE471E /* Utilities */,
4730617526588C40001E3A1F /* ViewModels */,
4730615D26565706001E3A1F /* Views */,
47C56F5E2679A779003EA20A /* Commands */,
Expand Down Expand Up @@ -354,6 +357,14 @@
path = WebAPI;
sourceTree = "<group>";
};
4764B3A62684865B00AE471E /* Utilities */ = {
isa = PBXGroup;
children = (
4764B3A72684867600AE471E /* DurationFormatter.swift */,
);
path = Utilities;
sourceTree = "<group>";
};
47C56F5E2679A779003EA20A /* Commands */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -459,6 +470,7 @@
470201BF265BB4320030ECA9 /* PreviousTrackButton.swift in Sources */,
470201C3265CF29B0030ECA9 /* NowPlaying.swift in Sources */,
475798C3266E309A00AADF2F /* RecommendationItem.swift in Sources */,
4764B3A82684867600AE471E /* DurationFormatter.swift in Sources */,
4730616A26565BB7001E3A1F /* BottomBar.swift in Sources */,
473061722656629F001E3A1F /* Nothing.swift in Sources */,
474BAFBC2668B0170006EB16 /* RepeatMode.swift in Sources */,
Expand Down
24 changes: 24 additions & 0 deletions Spottie/Utilities/DurationFormatter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// DurationFormatter.swift
// Spottie
//
// Created by Lee Jun Kit on 24/6/21.
//

import Foundation

class DurationFormatter {
static let shared = DurationFormatter()

private let formatter: DateComponentsFormatter
private init() {
formatter = DateComponentsFormatter()
formatter.allowedUnits = [.minute, .second]
formatter.unitsStyle = .positional
formatter.zeroFormattingBehavior = .pad
}

func format(_ from: TimeInterval) -> String {
return formatter.string(from: from)!
}
}
64 changes: 46 additions & 18 deletions Spottie/ViewModels/PlayerViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,39 +13,67 @@ class PlayerViewModel: PlayerStateProtocol {
@Published var isPlaying = false
@Published var durationMs = 1
@Published var progressMs = 0
@Published var progressPercent = 0.0
@Published var trackName = ""
@Published var artistName = ""
@Published var artworkURL: URL?
@Published var isShuffling = false
@Published var repeatMode = RepeatMode.none
@Published var isScrubbing = false

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

private var timerPublisher = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
private var timerSubscription: AnyCancellable?

init(_ eventBroker: EventBroker) {
self.eventBroker = eventBroker
let token = SpotifyAPI.currentPlayerState().sink(receiveCompletion: {[weak self] status in
if case .failure(let error) = status {
print(error)
} else {
guard let self = self else { return }

// 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 }
SpotifyAPI.currentPlayerState().sink(receiveCompletion: {_ in
// subscribe to events
eventBroker.onEventReceived.sink(receiveValue: { [weak self] event in
self?.onEventReceived(event: event)
}).store(in: &self.cancellables)
}) { [weak self] context in
if let ctx = context {
self.handleInitialStateUpdate(context: ctx)
self?.handleInitialStateUpdate(context: ctx)
}
}

token.store(in: &cancellables)
}.store(in: &cancellables)

// progress timer to activate only when isPlaying is true
$isPlaying.sink { [weak self] playing in
guard let strongSelf = self else { return }

if playing {
if strongSelf.timerSubscription == nil {
strongSelf.timerSubscription = strongSelf.timerPublisher.sink { _ in
if !strongSelf.isScrubbing {
strongSelf.progressMs += 1000
}
}
}
} else {
strongSelf.timerSubscription?.cancel()
strongSelf.timerSubscription = nil
}
}.store(in: &cancellables)

// derive percent progress
Publishers.CombineLatest($progressMs, $durationMs)
.map({ progressMs, durationMs -> Double in
if durationMs == 0 {
return 0.0
} else {
return Double(progressMs) / Double(durationMs)
}
})
.sink { [weak self] pc in
guard let strongSelf = self else { return }
if !strongSelf.isScrubbing {
strongSelf.progressPercent = pc
}
}
.store(in: &cancellables)
}

func handleInitialStateUpdate(context ctx: CurrentlyPlayingContextObject) {
Expand Down
5 changes: 2 additions & 3 deletions Spottie/Views/BottomBar/PlayerControls.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,8 @@ struct PlayerControls<M: PlayerStateProtocol>: View {
onRepeatButtonTapped: viewModel.cycleRepeatMode
)
}
TrackProgressSlider(viewModel: TrackProgressSlider.ViewModel(isPlaying: viewModel.isPlaying, progressMs: viewModel.progressMs, durationMs: viewModel.durationMs, onScrubToNewProgressPercent: viewModel.seek))
.padding(.leading)
.padding(.trailing)
TrackProgressSlider()
.padding([.leading, .trailing])
}
}
}
Expand Down
88 changes: 18 additions & 70 deletions Spottie/Views/BottomBar/TrackProgressSlider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,31 @@
//

import SwiftUI
import Combine

struct TrackProgressSlider: View {
@ObservedObject var viewModel: TrackProgressSlider.ViewModel

let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
@EnvironmentObject var viewModel: PlayerViewModel

var prettyProgress: String {
get {
let formatter = TrackProgressSlider.DurationFormatter.shared()
if (self.viewModel.isScrubbing) {
let scrubbedProgress = self.viewModel.progressPercent * Double(self.viewModel.durationMs)
return formatter.string(from: scrubbedProgress / 1000.0)!
if (viewModel.isScrubbing) {
let scrubbedProgress = viewModel.progressPercent * Double(viewModel.durationMs)
return DurationFormatter.shared.format(scrubbedProgress / 1000.0)
} else {
return formatter.string(from: Double(self.viewModel.progressMs) / 1000.0)!
return DurationFormatter.shared.format(Double(viewModel.progressMs) / 1000.0)
}
}
}

var prettyDuration: String {
get {
return TrackProgressSlider.DurationFormatter.shared().string(from: Double(self.viewModel.durationMs) / 1000.0)!
return DurationFormatter.shared.format(Double(viewModel.durationMs) / 1000.0)
}
}

var body: some View {
HStack {
VStack(alignment: .trailing) {
VStack {
Text(prettyProgress).foregroundColor(.secondary)
}
.frame(width: 40)
Expand All @@ -42,14 +40,12 @@ struct TrackProgressSlider: View {
in: 0...1,
onEditingChanged: { editing in
if (!editing) {
viewModel.onScrubToNewProgressPercent(viewModel.progressPercent)
viewModel.seek(toPercent: viewModel.progressPercent)
}

viewModel.isScrubbing = editing
}
)
.onReceive(timer) { _ in
viewModel.updateProgress()
}

VStack(alignment: .leading) {
Text(prettyDuration).foregroundColor(.secondary)
Expand All @@ -59,59 +55,11 @@ struct TrackProgressSlider: View {
}
}

extension TrackProgressSlider {
class DurationFormatter {
private static var sharedDurationFormatter: DateComponentsFormatter = {
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .positional
formatter.allowedUnits = [ .minute, .second ]
formatter.zeroFormattingBehavior = [ .pad ]
return formatter
}()

class func shared() -> DateComponentsFormatter {
return sharedDurationFormatter
}
}

class ViewModel: ObservableObject {
@Published var isPlaying: Bool
@Published var progressMs: Int
@Published var durationMs: Int
@Published var progressPercent: Double
var isScrubbing = false
var onScrubToNewProgressPercent: (_ progressPercent: Double) -> Void

init(isPlaying: Bool, progressMs: Int, durationMs: Int, onScrubToNewProgressPercent: @escaping (_ progressPercent: Double) -> Void) {
self.isPlaying = isPlaying
self.progressMs = progressMs
self.durationMs = durationMs
self.progressPercent = Double(progressMs) / Double(durationMs)
self.onScrubToNewProgressPercent = onScrubToNewProgressPercent
}

func calculateProgressPercent() {
self.progressPercent = Double(self.progressMs) / Double(self.durationMs)
}

func updateProgress() {
if (self.isPlaying) {
if (self.progressMs < self.durationMs) {
self.progressMs += 1000
if (!self.isScrubbing) {
self.calculateProgressPercent()
}
}
}
}
}
}

struct TrackProgressSlider_Previews: PreviewProvider {
static let viewModel = TrackProgressSlider.ViewModel(isPlaying: true, progressMs: 20000, durationMs: 120000) { progressPercent in

}
static var previews: some View {
TrackProgressSlider(viewModel: viewModel)
}
}
//struct TrackProgressSlider_Previews: PreviewProvider {
// static let viewModel = TrackProgressSlider.ViewModel(isPlaying: true, progressMs: 20000, durationMs: 120000) { progressPercent in
//
// }
// static var previews: some View {
// TrackProgressSlider(viewModel: viewModel)
// }
//}
17 changes: 0 additions & 17 deletions Spottie/Views/Components/TrackListItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,23 +44,6 @@ struct TrackListItem: View {
}
}

extension TrackListItem {
class DurationFormatter {
static let shared = DurationFormatter()

private let formatter: DateComponentsFormatter
private init() {
formatter = DateComponentsFormatter()
formatter.allowedUnits = [.hour, .minute, .second]
formatter.unitsStyle = .positional
}

func format(_ from: TimeInterval) -> String {
return formatter.string(from: from)!
}
}
}

struct TrackListItem_Previews: PreviewProvider {
static let viewModel = CarouselRowItem.ViewModel(
id: "abc",
Expand Down

0 comments on commit 8595377

Please sign in to comment.