diff --git a/Cartfile b/Cartfile index 11c695b..ef29de1 100644 --- a/Cartfile +++ b/Cartfile @@ -2,3 +2,4 @@ github "SwiftyJSON/SwiftyJSON" ~> 4.0 github "PromiseKit/Foundation" ~> 3.0 github "nhurden/MediaKeyTap" github "krzyzanowskim/CryptoSwift" +github "ReSwift/ReSwift" "mjarvis/swift-5.0" diff --git a/Cartfile.resolved b/Cartfile.resolved index 813257d..79f86bd 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1,5 +1,6 @@ github "PromiseKit/Foundation" "3.3.2" -github "SwiftyJSON/SwiftyJSON" "4.2.0" +github "ReSwift/ReSwift" "bc5943ad9493fc7fbad5200f690be538db8e86fb" +github "SwiftyJSON/SwiftyJSON" "4.3.0" github "krzyzanowskim/CryptoSwift" "1.0.0" github "mxcl/PromiseKit" "6.8.4" github "nhurden/MediaKeyTap" "2.2.1" diff --git a/Persephone.xcodeproj/project.pbxproj b/Persephone.xcodeproj/project.pbxproj index 70eef21..19535fb 100644 --- a/Persephone.xcodeproj/project.pbxproj +++ b/Persephone.xcodeproj/project.pbxproj @@ -20,7 +20,7 @@ E40F41F3221EDE27004B6CB8 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = E40F41F2221EDE27004B6CB8 /* Preferences.swift */; }; E40FE71B221B904300A4223F /* NSEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E40FE71A221B904300A4223F /* NSEvent.swift */; }; E419E2872249B96600216A8C /* Song.swift in Sources */ = {isa = PBXBuildFile; fileRef = E419E2862249B96600216A8C /* Song.swift */; }; - E41B22C021FB6BBA00D544F6 /* libmpdclient.2.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = E41B22BF21FB6BBA00D544F6 /* libmpdclient.2.dylib */; settings = {ATTRIBUTES = (Required, ); }; }; + E41B22C021FB6BBA00D544F6 /* libmpdclient.2.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = E41B22BF21FB6BBA00D544F6 /* libmpdclient.2.dylib */; }; E41B22C121FB6C3300D544F6 /* libmpdclient.2.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = E41B22BF21FB6BBA00D544F6 /* libmpdclient.2.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; E41B22C621FB932700D544F6 /* MPDClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = E41B22C521FB932700D544F6 /* MPDClient.swift */; }; E41E52FD223BF87300173814 /* MPDClient+Connection.swift in Sources */ = {isa = PBXBuildFile; fileRef = E41E52FC223BF87300173814 /* MPDClient+Connection.swift */; }; @@ -60,7 +60,6 @@ E45E4FDB22515D87004B537F /* Brewfile in Resources */ = {isa = PBXBuildFile; fileRef = E45E4FD822515D87004B537F /* Brewfile */; }; E45E4FDC22515D87004B537F /* Cartfile in Resources */ = {isa = PBXBuildFile; fileRef = E45E4FD922515D87004B537F /* Cartfile */; }; E45E4FDF225168DA004B537F /* CryptoSwift.framework.dSYM in Resources */ = {isa = PBXBuildFile; fileRef = E45E4FDD225168DA004B537F /* CryptoSwift.framework.dSYM */; }; - E45E4FE0225168DA004B537F /* CryptoSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E45E4FDE225168DA004B537F /* CryptoSwift.framework */; }; E45E4FE122516953004B537F /* CryptoSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E45E4FDE225168DA004B537F /* CryptoSwift.framework */; }; E45E4FE222516953004B537F /* CryptoSwift.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = E45E4FDE225168DA004B537F /* CryptoSwift.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; E465049A21E94DF500A70F4C /* WindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E465049921E94DF500A70F4C /* WindowController.swift */; }; @@ -76,6 +75,19 @@ E4A83BEF2221F8CF0098FED6 /* AlbumArtPrefsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4A83BEE2221F8CF0098FED6 /* AlbumArtPrefsController.swift */; }; E4A83BF12221FAA00098FED6 /* PreferencesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4A83BF02221FAA00098FED6 /* PreferencesViewController.swift */; }; E4A83BF4222207D50098FED6 /* AlbumArtService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4A83BF3222207D50098FED6 /* AlbumArtService.swift */; }; + E4B11B53226928F20075461B /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B11B52226928F20075461B /* AppState.swift */; }; + E4B11B5A2269296C0075461B /* ReSwift.framework.dSYM in Resources */ = {isa = PBXBuildFile; fileRef = E4B11B582269296C0075461B /* ReSwift.framework.dSYM */; }; + E4B11B5B226929730075461B /* ReSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E4B11B572269296C0075461B /* ReSwift.framework */; }; + E4B11B5C226929730075461B /* ReSwift.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = E4B11B572269296C0075461B /* ReSwift.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + E4B11B61226A4C000075461B /* PlayerReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B11B60226A4BFF0075461B /* PlayerReducer.swift */; }; + E4B11B63226A4C510075461B /* AppReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B11B62226A4C510075461B /* AppReducer.swift */; }; + E4B11B66226A4F830075461B /* PlayerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B11B65226A4F830075461B /* PlayerState.swift */; }; + E4B11B68226A4FA00075461B /* QueueState.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B11B67226A4FA00075461B /* QueueState.swift */; }; + E4B11B6A226A4FBC0075461B /* AlbumListState.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B11B69226A4FBC0075461B /* AlbumListState.swift */; }; + E4B11B6D226A5B180075461B /* UpdateQueueAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B11B6C226A5B180075461B /* UpdateQueueAction.swift */; }; + E4B11B6F226A5C7A0075461B /* UpdateStatusAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B11B6E226A5C7A0075461B /* UpdateStatusAction.swift */; }; + E4B11B71226A64E60075461B /* UpdateElapsedTimeAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B11B70226A64E60075461B /* UpdateElapsedTimeAction.swift */; }; + E4B11B73226A6C770075461B /* TrackTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B11B72226A6C770075461B /* TrackTimer.swift */; }; E4C8B53C22342005009A20F3 /* PreferencesWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4C8B53B22342005009A20F3 /* PreferencesWindowController.swift */; }; E4C8B53E22349002009A20F3 /* MPDIdle.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4C8B53D22349002009A20F3 /* MPDIdle.swift */; }; E4E8CC902204EC7F0024217A /* Delegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4E8CC8F2204EC7F0024217A /* Delegate.swift */; }; @@ -113,6 +125,7 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( + E4B11B5C226929730075461B /* ReSwift.framework in Embed Libraries */, E41B22C121FB6C3300D544F6 /* libmpdclient.2.dylib in Embed Libraries */, E450ADA42229E7E00091BED3 /* PMKFoundation.framework in Embed Libraries */, E421ACA4221F73C4008B2449 /* MediaKeyTap.framework in Embed Libraries */, @@ -242,6 +255,18 @@ E4A83BEE2221F8CF0098FED6 /* AlbumArtPrefsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumArtPrefsController.swift; sourceTree = ""; }; E4A83BF02221FAA00098FED6 /* PreferencesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesViewController.swift; sourceTree = ""; }; E4A83BF3222207D50098FED6 /* AlbumArtService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumArtService.swift; sourceTree = ""; }; + E4B11B52226928F20075461B /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = ""; }; + E4B11B572269296C0075461B /* ReSwift.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ReSwift.framework; path = Carthage/Build/Mac/ReSwift.framework; sourceTree = ""; }; + E4B11B582269296C0075461B /* ReSwift.framework.dSYM */ = {isa = PBXFileReference; lastKnownFileType = wrapper.dsym; name = ReSwift.framework.dSYM; path = Carthage/Build/Mac/ReSwift.framework.dSYM; sourceTree = ""; }; + E4B11B60226A4BFF0075461B /* PlayerReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerReducer.swift; sourceTree = ""; }; + E4B11B62226A4C510075461B /* AppReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppReducer.swift; sourceTree = ""; }; + E4B11B65226A4F830075461B /* PlayerState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerState.swift; sourceTree = ""; }; + E4B11B67226A4FA00075461B /* QueueState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueueState.swift; sourceTree = ""; }; + E4B11B69226A4FBC0075461B /* AlbumListState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumListState.swift; sourceTree = ""; }; + E4B11B6C226A5B180075461B /* UpdateQueueAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateQueueAction.swift; sourceTree = ""; }; + E4B11B6E226A5C7A0075461B /* UpdateStatusAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateStatusAction.swift; sourceTree = ""; }; + E4B11B70226A64E60075461B /* UpdateElapsedTimeAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateElapsedTimeAction.swift; sourceTree = ""; }; + E4B11B72226A6C770075461B /* TrackTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackTimer.swift; sourceTree = ""; }; E4C8B53B22342005009A20F3 /* PreferencesWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesWindowController.swift; sourceTree = ""; }; E4C8B53D22349002009A20F3 /* MPDIdle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPDIdle.swift; sourceTree = ""; }; E4E8CC8F2204EC7F0024217A /* Delegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Delegate.swift; sourceTree = ""; }; @@ -260,12 +285,12 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - E45E4FE122516953004B537F /* CryptoSwift.framework in Frameworks */, + E4B11B5B226929730075461B /* ReSwift.framework in Frameworks */, E41B22C021FB6BBA00D544F6 /* libmpdclient.2.dylib in Frameworks */, - E421ACA3221F73C4008B2449 /* MediaKeyTap.framework in Frameworks */, - E450AD8622262AE60091BED3 /* SwiftyJSON.framework in Frameworks */, - E45E4FE0225168DA004B537F /* CryptoSwift.framework in Frameworks */, E450ADA32229E7E00091BED3 /* PMKFoundation.framework in Frameworks */, + E421ACA3221F73C4008B2449 /* MediaKeyTap.framework in Frameworks */, + E45E4FE122516953004B537F /* CryptoSwift.framework in Frameworks */, + E450AD8622262AE60091BED3 /* SwiftyJSON.framework in Frameworks */, E450AD9222262C970091BED3 /* PromiseKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -316,6 +341,7 @@ E407861A2110CE6E006887B1 /* Persephone */ = { isa = PBXGroup; children = ( + E4B11B6B226A5AF50075461B /* Actions */, E407861B2110CE6E006887B1 /* AppDelegate.swift */, E407861F2110CE70006887B1 /* Assets.xcassets */, E4D1B597220BA3A20026F233 /* Controllers */, @@ -330,8 +356,10 @@ E40786252110CE70006887B1 /* Persephone.entitlements */, E450AD9E2229B9BC0091BED3 /* PersephoneBridgingHeader.h */, E4A83BEC2221F5DD0098FED6 /* Preferences */, + E4B11B5F226A4BED0075461B /* Reducers */, E4D1B598220BA3C90026F233 /* Resources */, E4A83BF2222207BE0098FED6 /* Services */, + E4B11B64226A4F460075461B /* State */, E408D3C3220E138B0006D9BE /* Views */, ); path = Persephone; @@ -397,6 +425,8 @@ E41B22BE21FB6B3300D544F6 /* Frameworks */ = { isa = PBXGroup; children = ( + E4B11B572269296C0075461B /* ReSwift.framework */, + E4B11B582269296C0075461B /* ReSwift.framework.dSYM */, E45E4FDE225168DA004B537F /* CryptoSwift.framework */, E45E4FDD225168DA004B537F /* CryptoSwift.framework.dSYM */, E450ADA02229E7C90091BED3 /* PMKFoundation.framework */, @@ -531,6 +561,36 @@ path = Services; sourceTree = ""; }; + E4B11B5F226A4BED0075461B /* Reducers */ = { + isa = PBXGroup; + children = ( + E4B11B60226A4BFF0075461B /* PlayerReducer.swift */, + E4B11B62226A4C510075461B /* AppReducer.swift */, + ); + path = Reducers; + sourceTree = ""; + }; + E4B11B64226A4F460075461B /* State */ = { + isa = PBXGroup; + children = ( + E4B11B52226928F20075461B /* AppState.swift */, + E4B11B65226A4F830075461B /* PlayerState.swift */, + E4B11B67226A4FA00075461B /* QueueState.swift */, + E4B11B69226A4FBC0075461B /* AlbumListState.swift */, + ); + path = State; + sourceTree = ""; + }; + E4B11B6B226A5AF50075461B /* Actions */ = { + isa = PBXGroup; + children = ( + E4B11B6C226A5B180075461B /* UpdateQueueAction.swift */, + E4B11B6E226A5C7A0075461B /* UpdateStatusAction.swift */, + E4B11B70226A64E60075461B /* UpdateElapsedTimeAction.swift */, + ); + path = Actions; + sourceTree = ""; + }; E4D1B594220BA2490026F233 /* Models */ = { isa = PBXGroup; children = ( @@ -591,6 +651,7 @@ E4F6B462221E125900ACF42A /* QueueItem.swift */, E419E2862249B96600216A8C /* Song.swift */, E47E2FDC2220A6D100F747E6 /* Time.swift */, + E4B11B72226A6C770075461B /* TrackTimer.swift */, ); path = Models; sourceTree = ""; @@ -724,6 +785,7 @@ E42A8F3C22176D6400A13ED9 /* README.md in Resources */, E450AD8F22262C620091BED3 /* PromiseKit.framework.dSYM in Resources */, E408D3CB220E341D0006D9BE /* AlbumViewItem.xib in Resources */, + E4B11B5A2269296C0075461B /* ReSwift.framework.dSYM in Resources */, E40786232110CE70006887B1 /* Main.storyboard in Resources */, E450ADA12229E7C90091BED3 /* PMKFoundation.framework.dSYM in Resources */, E47E2FCC2220573500F747E6 /* MediaKeyTap.framework.dSYM in Resources */, @@ -775,10 +837,14 @@ E4F6B467221E233200ACF42A /* AlbumDataSource.swift in Sources */, E408D3C2220E134F0006D9BE /* AlbumViewController.swift in Sources */, E40FE71B221B904300A4223F /* NSEvent.swift in Sources */, + E4B11B6D226A5B180075461B /* UpdateQueueAction.swift in Sources */, + E4B11B68226A4FA00075461B /* QueueState.swift in Sources */, E4928E0B2218D62A001D4BEA /* CGColor.swift in Sources */, E4C8B53E22349002009A20F3 /* MPDIdle.swift in Sources */, + E4B11B6F226A5C7A0075461B /* UpdateStatusAction.swift in Sources */, E4F6B460221E119B00ACF42A /* QueueDataSource.swift in Sources */, E4C8B53C22342005009A20F3 /* PreferencesWindowController.swift in Sources */, + E4B11B63226A4C510075461B /* AppReducer.swift in Sources */, E41E5307223C019100173814 /* MPDClient+Status.swift in Sources */, E41E5310223EF6CE00173814 /* AlbumArtService+Remote.swift in Sources */, E41E530B223C033700173814 /* MPDClient+Album.swift in Sources */, @@ -795,7 +861,9 @@ E4EB2379220F10B8008C70C0 /* MPDPair.swift in Sources */, E4F6B463221E125900ACF42A /* QueueItem.swift in Sources */, E4A83BF12221FAA00098FED6 /* PreferencesViewController.swift in Sources */, + E4B11B61226A4C000075461B /* PlayerReducer.swift in Sources */, E465049A21E94DF500A70F4C /* WindowController.swift in Sources */, + E4B11B71226A64E60075461B /* UpdateElapsedTimeAction.swift in Sources */, E41B22C621FB932700D544F6 /* MPDClient.swift in Sources */, E40F41F3221EDE27004B6CB8 /* Preferences.swift in Sources */, E47E2FDD2220A6D100F747E6 /* Time.swift in Sources */, @@ -803,6 +871,7 @@ E439109822640213002982E9 /* SongNotifierService.swift in Sources */, E407861C2110CE6E006887B1 /* AppDelegate.swift in Sources */, E41E5309223C020400173814 /* MPDClient+Command.swift in Sources */, + E4B11B73226A6C770075461B /* TrackTimer.swift in Sources */, E47E2FE52220AA0700F747E6 /* AlbumViewLayout.swift in Sources */, E41E52FF223BF95E00173814 /* MPDClient+Transport.swift in Sources */, E47E2FD322205D2500F747E6 /* MainWindow.swift in Sources */, @@ -813,13 +882,16 @@ E4A83BF4222207D50098FED6 /* AlbumArtService.swift in Sources */, E47E2FD5222071FD00F747E6 /* AlbumViewItem.swift in Sources */, E408D3BE220E03EE0006D9BE /* RawRepresentable.swift in Sources */, + E4B11B66226A4F830075461B /* PlayerState.swift in Sources */, E41E530E223EF4CF00173814 /* AlbumArtService+Caching.swift in Sources */, E4E8CC922204F4B80024217A /* QueueViewController.swift in Sources */, E41E5312223EF74A00173814 /* AlbumArtService+Filesystem.swift in Sources */, E41E5301223BF99300173814 /* MPDClient+Queue.swift in Sources */, E4EB237B220F7CF1008C70C0 /* MPDAlbum.swift in Sources */, E41E5303223BF9C300173814 /* MPDClient+Idle.swift in Sources */, + E4B11B53226928F20075461B /* AppState.swift in Sources */, E435E3E4221CD75D00184CFC /* NSImage.swift in Sources */, + E4B11B6A226A4FBC0075461B /* AlbumListState.swift in Sources */, E41E5305223BFB0700173814 /* MPDClient+Error.swift in Sources */, E435E3E2221CD4E200184CFC /* NSFont.swift in Sources */, ); diff --git a/Persephone/Actions/UpdateElapsedTimeAction.swift b/Persephone/Actions/UpdateElapsedTimeAction.swift new file mode 100644 index 0000000..cc5e8e5 --- /dev/null +++ b/Persephone/Actions/UpdateElapsedTimeAction.swift @@ -0,0 +1,13 @@ +// +// UpdateElapsedTimeAction.swift +// Persephone +// +// Created by Daniel Barber on 2019/4/19. +// Copyright © 2019 Dan Barber. All rights reserved. +// + +import ReSwift + +struct UpdateElapsedTimeAction: Action { + var elapsedTimeMs: UInt = 0 +} diff --git a/Persephone/Actions/UpdateQueueAction.swift b/Persephone/Actions/UpdateQueueAction.swift new file mode 100644 index 0000000..3ad54e0 --- /dev/null +++ b/Persephone/Actions/UpdateQueueAction.swift @@ -0,0 +1,13 @@ +// +// UpdateQueueAction.swift +// Persephone +// +// Created by Daniel Barber on 2019/4/19. +// Copyright © 2019 Dan Barber. All rights reserved. +// + +import ReSwift + +struct UpdateQueueAction: Action { + var queue: [MPDClient.MPDSong] +} diff --git a/Persephone/Actions/UpdateStatusAction.swift b/Persephone/Actions/UpdateStatusAction.swift new file mode 100644 index 0000000..1be9f13 --- /dev/null +++ b/Persephone/Actions/UpdateStatusAction.swift @@ -0,0 +1,13 @@ +// +// UpdateStatusAction.swift +// Persephone +// +// Created by Daniel Barber on 2019/4/19. +// Copyright © 2019 Dan Barber. All rights reserved. +// + +import ReSwift + +struct UpdateStatusAction: Action { + var status: MPDClient.MPDStatus +} diff --git a/Persephone/AppDelegate.swift b/Persephone/AppDelegate.swift index 31a0e5d..5aa5286 100644 --- a/Persephone/AppDelegate.swift +++ b/Persephone/AppDelegate.swift @@ -7,6 +7,7 @@ // import Cocoa +import ReSwift import MediaKeyTap @NSApplicationMain @@ -18,6 +19,10 @@ class AppDelegate: NSObject, NSApplicationDelegate, MediaKeyTapDelegate { withDelegate: NotificationsController() ) + static let trackTimer = TrackTimer() + + static let store = Store(reducer: appReducer, state: nil) + func applicationDidFinishLaunching(_ aNotification: Notification) { connect() diff --git a/Persephone/Controllers/NotificationsController.swift b/Persephone/Controllers/NotificationsController.swift index c82519e..ed53617 100644 --- a/Persephone/Controllers/NotificationsController.swift +++ b/Persephone/Controllers/NotificationsController.swift @@ -1,5 +1,5 @@ // -// MPDClientNotificationHandler.swift +// NotificationsController.swift // Persephone // // Created by Daniel Barber on 2019/2/02. @@ -19,21 +19,23 @@ class NotificationsController: MPDClientDelegate { sendNotification(name: Notification.willDisconnect) } - func didUpdateState(mpdClient: MPDClient, state: MPDClient.MPDStatus.State) { + func didUpdateStatus(mpdClient: MPDClient, status: MPDClient.MPDStatus) { + AppDelegate.store.dispatch(UpdateStatusAction(status: status)) sendNotification( name: Notification.stateChanged, - userInfo: [Notification.stateKey: state] + userInfo: [Notification.stateKey: status.state] + ) + sendNotification( + name: Notification.timeChanged, + userInfo: [ + Notification.totalTimeKey: status.totalTime, + Notification.elapsedTimeMsKey: status.elapsedTimeMs + ] ) } func didUpdateTime(mpdClient: MPDClient, total: UInt, elapsedMs: UInt) { - sendNotification( - name: Notification.timeChanged, - userInfo: [ - Notification.totalTimeKey: total, - Notification.elapsedTimeMsKey: elapsedMs - ] - ) + } func willStartDatabaseUpdate(mpdClient: MPDClient) { @@ -45,6 +47,7 @@ class NotificationsController: MPDClientDelegate { } func didUpdateQueue(mpdClient: MPDClient, queue: [MPDClient.MPDSong]) { + //AppDelegate.store.dispatch(UpdateQueueAction(queue: queue)) sendNotification( name: Notification.queueChanged, userInfo: [Notification.queueKey: queue] diff --git a/Persephone/Controllers/WindowController.swift b/Persephone/Controllers/WindowController.swift index 63d347f..88cf393 100644 --- a/Persephone/Controllers/WindowController.swift +++ b/Persephone/Controllers/WindowController.swift @@ -7,34 +7,23 @@ // import Cocoa +import ReSwift + +class WindowController: NSWindowController, StoreSubscriber { + typealias StoreSubscriberStateType = AppState -class WindowController: NSWindowController { enum TransportAction: Int { case prevTrack, playPause, stop, nextTrack } var state: MPDClient.MPDStatus.State? - var totalTime: UInt? - var elapsedTimeMs: UInt? var trackTimer: Timer? override func windowDidLoad() { super.windowDidLoad() window?.titleVisibility = .hidden - NotificationCenter.default.addObserver( - self, - selector: #selector(stateChanged(_:)), - name: Notification.stateChanged, - object: AppDelegate.mpdClient - ) - - NotificationCenter.default.addObserver( - self, - selector: #selector(timeChanged(_:)), - name: Notification.timeChanged, - object: AppDelegate.mpdClient - ) + AppDelegate.store.subscribe(self) NotificationCenter.default.addObserver( self, @@ -54,6 +43,15 @@ class WindowController: NSWindowController { trackRemaining.font = .timerFont } + func newState(state: WindowController.StoreSubscriberStateType) { + self.state = state.playerState.state + + DispatchQueue.main.async { + self.setTransportControlState(state.playerState) + self.setTrackProgressControls(state.playerState) + } + } + override func keyDown(with event: NSEvent) { switch event.keyCode { case NSEvent.keyCodeSpace: @@ -63,17 +61,8 @@ class WindowController: NSWindowController { } } - @objc func stateChanged(_ notification: Notification) { - guard let state = notification.userInfo?[Notification.stateKey] as? MPDClient.MPDStatus.State - else { return } - - self.state = state - - setTransportControlState() - } - - func setTransportControlState() { - guard let state = state else { return } + func setTransportControlState(_ state: PlayerState) { + guard let state = state.state else { return } transportControls.setEnabled(state.isOneOf([.playing, .paused]), forSegment: 0) transportControls.setEnabled(state.isOneOf([.playing, .paused, .stopped]), forSegment: 1) @@ -87,64 +76,37 @@ class WindowController: NSWindowController { } } - @objc func timeChanged(_ notification: Notification) { - guard let totalTime = notification.userInfo?[Notification.totalTimeKey] as? UInt, - let elapsedTimeMs = notification.userInfo?[Notification.elapsedTimeMsKey] as? UInt + func setTrackProgressControls(_ playerState: PlayerState) { + guard let state = playerState.state, + let totalTime = playerState.totalTime, + let elapsedTimeMs = playerState.elapsedTimeMs else { return } - self.totalTime = totalTime - self.elapsedTimeMs = elapsedTimeMs - - setTrackProgressControls() - } - - func setTrackProgressControls() { - guard let totalTime = totalTime, - let elapsedTimeMs = elapsedTimeMs - else { return } - - trackProgressBar.isEnabled = [.playing, .paused].contains(state) + trackProgressBar.isEnabled = state.isOneOf([.playing, .paused]) trackProgressBar.maxValue = Double(totalTime * 1000) + trackProgressBar.integerValue = Int(elapsedTimeMs) - if state == .playing { - trackTimer?.invalidate() - - trackTimer = Timer.scheduledTimer( - timeInterval: 0.25, - target: self, - selector: #selector(updateProgress(_:)), - userInfo: [ - "startTime": CACurrentMediaTime(), - "startElapsed": Double(elapsedTimeMs) / 1000 - ], - repeats: true - ) - } else { - trackTimer?.invalidate() - - trackProgressBar.integerValue = Int(elapsedTimeMs) - setTimeElapsed() - setTimeRemaining() - } + setTimeElapsed(elapsedTimeMs) + setTimeRemaining(elapsedTimeMs, totalTime * 1000) } - @objc func updateProgress(_ timer: Timer) { - let currentTime = CACurrentMediaTime() - - guard let userInfo = timer.userInfo as? Dictionary, - let startTime = userInfo["startTime"] as? Double, - let startElapsed = userInfo["startElapsed"] as? Double - else { return } - - let timeDiff = currentTime - startTime - let newElapsedTimeMs = (startElapsed + timeDiff) * 1000 - - self.elapsedTimeMs = UInt(newElapsedTimeMs) - trackProgressBar.integerValue = Int(newElapsedTimeMs) - - setTimeElapsed() - setTimeRemaining() - } +// func updateProgressState() { +// let currentTime = CACurrentMediaTime() +// +// guard let userInfo = timer.userInfo as? Dictionary, +// let startTime = userInfo["startTime"] as? Double, +// let startElapsed = userInfo["startElapsed"] as? Double +// else { return } +// +// let timeDiff = currentTime - startTime +// let newElapsedTimeMs = (startElapsed + timeDiff) * 1000 +// +// self.elapsedTimeMs = UInt(newElapsedTimeMs) +// trackProgressBar.integerValue = Int(newElapsedTimeMs) +// +// setTimeElapsed() +// setTimeRemaining() +// } @objc func startDatabaseUpdatingIndicator() { databaseUpdatingIndicator.startAnimation(self) @@ -154,7 +116,7 @@ class WindowController: NSWindowController { databaseUpdatingIndicator.stopAnimation(self) } - func setTimeElapsed() { + func setTimeElapsed(_ elapsedTimeMs: UInt?) { guard let elapsedTimeMs = elapsedTimeMs else { return } let time = Time(timeInSeconds: Int(elapsedTimeMs) / 1000) @@ -162,38 +124,40 @@ class WindowController: NSWindowController { trackProgress.stringValue = time.formattedTime } - func setTimeRemaining() { + func setTimeRemaining(_ elapsedTimeMs: UInt?, _ totalTime: UInt?) { guard let elapsedTimeMs = elapsedTimeMs, let totalTime = totalTime else { return } - let time = Time(timeInSeconds: -(Int(totalTime) - Int(elapsedTimeMs) / 1000)) + let time = Time( + timeInSeconds: -(Int(totalTime) - Int(elapsedTimeMs)) / 1000 + ) trackRemaining.stringValue = time.formattedTime } // TODO: Refactor this using a gesture recognizer - @IBAction func changeTrackProgress(_ sender: NSSlider) { - guard let event = NSApplication.shared.currentEvent else { - return - } - - switch event.type { - case .leftMouseDown: - trackTimer?.invalidate() - case .leftMouseDragged: - self.elapsedTimeMs = UInt(sender.integerValue) - - setTimeElapsed() - setTimeRemaining() - case .leftMouseUp: - let seekTime = Float(sender.integerValue) / 1000 - - AppDelegate.mpdClient.seekCurrentSong(timeInSeconds: seekTime) - default: - break - } - } +// @IBAction func changeTrackProgress(_ sender: NSSlider) { +// guard let event = NSApplication.shared.currentEvent else { +// return +// } +// +// switch event.type { +// case .leftMouseDown: +// trackTimer?.invalidate() +// case .leftMouseDragged: +// self.elapsedTimeMs = UInt(sender.integerValue) +// +// setTimeElapsed() +// setTimeRemaining() +// case .leftMouseUp: +// let seekTime = Float(sender.integerValue) / 1000 +// +// AppDelegate.mpdClient.seekCurrentSong(timeInSeconds: seekTime) +// default: +// break +// } +// } @IBAction func handleTransportControl(_ sender: NSSegmentedControl) { guard let transportAction = TransportAction(rawValue: sender.selectedSegment) diff --git a/Persephone/MPDClient/Extensions/MPDClient+Connection.swift b/Persephone/MPDClient/Extensions/MPDClient+Connection.swift index 50f4b27..9e6175e 100644 --- a/Persephone/MPDClient/Extensions/MPDClient+Connection.swift +++ b/Persephone/MPDClient/Extensions/MPDClient+Connection.swift @@ -29,8 +29,7 @@ extension MPDClient { self.idle() self.delegate?.didConnect(mpdClient: self) - self.delegate?.didUpdateState(mpdClient: self, state: self.status!.state) - self.delegate?.didUpdateTime(mpdClient: self, total: self.status!.totalTime, elapsedMs: self.status!.elapsedTimeMs) + self.delegate?.didUpdateStatus(mpdClient: self, status: self.status!) self.delegate?.didUpdateQueue(mpdClient: self, queue: self.queue) self.delegate?.didUpdateQueuePos(mpdClient: self, song: self.status!.song) } diff --git a/Persephone/MPDClient/Extensions/MPDClient+Idle.swift b/Persephone/MPDClient/Extensions/MPDClient+Idle.swift index abfd9ea..9d02f96 100644 --- a/Persephone/MPDClient/Extensions/MPDClient+Idle.swift +++ b/Persephone/MPDClient/Extensions/MPDClient+Idle.swift @@ -43,9 +43,8 @@ extension MPDClient { self.fetchStatus() if let status = self.status { + self.delegate?.didUpdateStatus(mpdClient: self, status: status) self.delegate?.didUpdateQueuePos(mpdClient: self, song: status.song) - self.delegate?.didUpdateState(mpdClient: self, state: status.state) - self.delegate?.didUpdateTime(mpdClient: self, total: status.totalTime, elapsedMs: status.elapsedTimeMs) } } if mpdIdle.contains(.update) { diff --git a/Persephone/MPDClient/Protocols/Delegate.swift b/Persephone/MPDClient/Protocols/Delegate.swift index 373a7a6..576695e 100644 --- a/Persephone/MPDClient/Protocols/Delegate.swift +++ b/Persephone/MPDClient/Protocols/Delegate.swift @@ -12,8 +12,7 @@ protocol MPDClientDelegate { func didConnect(mpdClient: MPDClient) func willDisconnect(mpdClient: MPDClient) - func didUpdateState(mpdClient: MPDClient, state: MPDClient.MPDStatus.State) - func didUpdateTime(mpdClient: MPDClient, total: UInt, elapsedMs: UInt) + func didUpdateStatus(mpdClient: MPDClient, status: MPDClient.MPDStatus) func willStartDatabaseUpdate(mpdClient: MPDClient) func didFinishDatabaseUpdate(mpdClient: MPDClient) diff --git a/Persephone/Models/TrackTimer.swift b/Persephone/Models/TrackTimer.swift new file mode 100644 index 0000000..69d6b49 --- /dev/null +++ b/Persephone/Models/TrackTimer.swift @@ -0,0 +1,55 @@ +// +// TrackTimer.swift +// Persephone +// +// Created by Daniel Barber on 2019/4/19. +// Copyright © 2019 Dan Barber. All rights reserved. +// + +import Cocoa + +class TrackTimer: NSObject { + var timer: Timer? + var startTime: CFTimeInterval = CACurrentMediaTime() + var startElapsed: Double = 0 + + func start(elapsedTimeMs: UInt?) { + print("Starting timer") + guard let elapsedTimeMs = elapsedTimeMs else { return } + print(elapsedTimeMs) + + timer?.invalidate() + + startTime = CACurrentMediaTime() + startElapsed = Double(elapsedTimeMs) / 1000 + + timer = Timer.scheduledTimer( + withTimeInterval: 0.25, + repeats: true + ) { _ in + let currentTime = CACurrentMediaTime() + + let timeDiff = currentTime - self.startTime + let newElapsedTimeMs = UInt((self.startElapsed + timeDiff) * 1000) + + DispatchQueue.main.async { + AppDelegate.store.dispatch( + UpdateElapsedTimeAction(elapsedTimeMs: newElapsedTimeMs) + ) + } + } + } + + func stop(elapsedTimeMs: UInt?) { + print("Stopping timer") + guard let elapsedTimeMs = elapsedTimeMs else { return } + + timer?.invalidate() + + DispatchQueue.main.async { + AppDelegate.store.dispatch( + UpdateElapsedTimeAction(elapsedTimeMs: elapsedTimeMs) + ) + } + } +} diff --git a/Persephone/Reducers/AppReducer.swift b/Persephone/Reducers/AppReducer.swift new file mode 100644 index 0000000..f0d72cf --- /dev/null +++ b/Persephone/Reducers/AppReducer.swift @@ -0,0 +1,16 @@ +// +// AppReducer.swift +// Persephone +// +// Created by Daniel Barber on 2019/4/19. +// Copyright © 2019 Dan Barber. All rights reserved. +// + +import Foundation +import ReSwift + +func appReducer(action: Action, state: AppState?) -> AppState { + return AppState( + playerState: playerReducer(action: action, state: state?.playerState) + ) +} diff --git a/Persephone/Reducers/PlayerReducer.swift b/Persephone/Reducers/PlayerReducer.swift new file mode 100644 index 0000000..c31609d --- /dev/null +++ b/Persephone/Reducers/PlayerReducer.swift @@ -0,0 +1,46 @@ +// +// PlayerReducer.swift +// Persephone +// +// Created by Daniel Barber on 2019/4/19. +// Copyright © 2019 Dan Barber. All rights reserved. +// + +import Cocoa +import ReSwift + +func playerReducer(action: Action, state: PlayerState?) -> PlayerState { + var state = state ?? PlayerState() + + switch action { + case let action as UpdateStatusAction: + state.status = action.status + state.state = action.status.state + state.totalTime = action.status.totalTime + state.elapsedTimeMs = action.status.elapsedTimeMs + + if state.state == .playing { + AppDelegate.trackTimer.start(elapsedTimeMs: state.elapsedTimeMs) + } else { + AppDelegate.trackTimer.stop(elapsedTimeMs: state.elapsedTimeMs) + } + + case let action as UpdateElapsedTimeAction: + state.elapsedTimeMs = action.elapsedTimeMs + + default: + break + } + + return state +} + +func updateElapsedTime(_ timer: Timer) { + guard let userInfo = timer.userInfo as? Dictionary, + let elapsedTimeMs = userInfo["elapsedTimeMs"] as? UInt + else { return } + + AppDelegate.store.dispatch( + UpdateElapsedTimeAction(elapsedTimeMs: elapsedTimeMs) + ) +} diff --git a/Persephone/State/AlbumListState.swift b/Persephone/State/AlbumListState.swift new file mode 100644 index 0000000..56f38e7 --- /dev/null +++ b/Persephone/State/AlbumListState.swift @@ -0,0 +1,13 @@ +// +// AlbumListState.swift +// Persephone +// +// Created by Daniel Barber on 2019/4/19. +// Copyright © 2019 Dan Barber. All rights reserved. +// + +import ReSwift + +struct AlbumListState: StateType { + var albums: [Album] = [] +} diff --git a/Persephone/State/AppState.swift b/Persephone/State/AppState.swift new file mode 100644 index 0000000..56f38a2 --- /dev/null +++ b/Persephone/State/AppState.swift @@ -0,0 +1,15 @@ +// +// AppState.swift +// Persephone +// +// Created by Daniel Barber on 2019/4/18. +// Copyright © 2019 Dan Barber. All rights reserved. +// + +import ReSwift + +struct AppState: StateType { + var playerState = PlayerState() +// var queueState = QueueState() +// var albumListState = AlbumListState() +} diff --git a/Persephone/State/PlayerState.swift b/Persephone/State/PlayerState.swift new file mode 100644 index 0000000..30a6d6e --- /dev/null +++ b/Persephone/State/PlayerState.swift @@ -0,0 +1,19 @@ +// +// PlayerState.swift +// Persephone +// +// Created by Daniel Barber on 2019/4/19. +// Copyright © 2019 Dan Barber. All rights reserved. +// + +import Cocoa +import ReSwift + +struct PlayerState: StateType { + var status: MPDClient.MPDStatus? + + var state: MPDClient.MPDStatus.State? + + var totalTime: UInt? + var elapsedTimeMs: UInt? +} diff --git a/Persephone/State/QueueState.swift b/Persephone/State/QueueState.swift new file mode 100644 index 0000000..af692f8 --- /dev/null +++ b/Persephone/State/QueueState.swift @@ -0,0 +1,14 @@ +// +// QueueState.swift +// Persephone +// +// Created by Daniel Barber on 2019/4/19. +// Copyright © 2019 Dan Barber. All rights reserved. +// + +import ReSwift + +struct QueueState: StateType { + var queue: [QueueItem] = [] + var queuePos: Int = -1 +}