1
1
mirror of https://github.com/danbee/persephone synced 2025-03-04 08:39:11 +00:00

Compare commits

...

23 Commits

Author SHA1 Message Date
770a01c604
Update to 0.16.0-alpha 2020-02-19 18:15:18 -05:00
91c82bbc6f
Add volume control 2020-02-19 18:08:22 -05:00
123f9c1e4a
Tweak priorities of commands 2020-02-16 14:39:12 -05:00
cd2e5efc95
Cache the small image when we get the browser art
We use the RawImageDataProvider here to pass the already downloaded
image onto this transform, to avoid fetching it from MPD again. The
180x180 size artwork is plenty big enough to use as source for the 32x32
queue/dragged song artwork.
2020-02-16 12:51:11 -05:00
a663315013
💄 2020-02-16 12:50:56 -05:00
6236884195
Fix a nasty deadlock 2020-02-16 12:50:32 -05:00
cff3e7bfc6
Bump version to 0.16.0-prealpha 2020-02-15 21:47:30 -05:00
f22b5b022c
We were chasing a red herring
It should actually be perfectly ok to send `noidle` before the call to
`recv_idle`. It just means the `recv_idle` will return immediately.
2020-02-15 21:36:41 -05:00
cacb0b0124
Trying to lock when we already have the lock is bad 2020-02-15 21:36:41 -05:00
f39c2a4f99
Remove redundant selfs 2020-02-15 21:36:41 -05:00
abe2c293eb
This does not need to be async on the main thread
In fact doing so causes us to idle excessively.
2020-02-15 21:36:40 -05:00
abf1579789
Hold onto idleLock while we handle idle 2020-02-15 21:36:40 -05:00
2f59eaeecf
Woops, we were reading this outside of the lock! 2020-02-15 21:36:40 -05:00
fa338ee790
Address some PR feedback 2020-02-15 21:36:40 -05:00
a7e7620f68
Attempt to fix album detail view crash
Very occasionally the album detail view fails to get the album tracks
and this causes a crash. This extra guard avoids the crash, but doesn't
fix the underlying problem.

The problem itself is very hard to track down because it's incredibly
hard to reproduce.
2020-02-15 21:36:39 -05:00
eb5cee2a75
Fix potential race 2020-02-15 21:36:39 -05:00
f39f69b4dc
We can just reuse the same provider 2020-02-15 21:36:39 -05:00
8d24d9df19
Update all the packages 2020-02-15 21:36:39 -05:00
7c99a9a712
We never mutate this 2020-02-15 21:36:39 -05:00
7f8e209970
Self *could* get deallocated here 2020-02-15 21:36:38 -05:00
c0f8badbb5
Fix deprecation warning 2020-02-15 21:36:38 -05:00
3b4b5f5b5c
Refactor cover art refresh using Promises 2020-02-15 21:36:38 -05:00
ff3c7c4856
Fetching album art via MPD connection now
- Remove music dir prefs
+ Add refresh album art option to album list context menu
+ Wire up album view context menu
+ Force an idle after transport commands
+ Add "clear cache" button
2020-02-15 21:36:38 -05:00
77 changed files with 921 additions and 367 deletions

View File

@ -53,9 +53,11 @@
E44051A0227BB0AB0090CD6F /* UIState.swift in Sources */ = {isa = PBXBuildFile; fileRef = E440519F227BB0AB0090CD6F /* UIState.swift */; };
E442CCCD2347E73C00004E0C /* Artist.swift in Sources */ = {isa = PBXBuildFile; fileRef = E442CCCC2347E73C00004E0C /* Artist.swift */; };
E450AD7E222620A10091BED3 /* Album.swift in Sources */ = {isa = PBXBuildFile; fileRef = E450AD7D222620A10091BED3 /* Album.swift */; };
E450AD9522262DF10091BED3 /* CoverArtQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = E450AD9422262DF10091BED3 /* CoverArtQueue.swift */; };
E451E36B22BD214D008BE9B2 /* DraggedSongType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E451E36A22BD214D008BE9B2 /* DraggedSongType.swift */; };
E451E36E22BD2501008BE9B2 /* DraggedSong.swift in Sources */ = {isa = PBXBuildFile; fileRef = E451E36C22BD23DB008BE9B2 /* DraggedSong.swift */; };
E453825223FA0186007F6BFC /* VolumeControlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E453825023FA0186007F6BFC /* VolumeControlView.swift */; };
E453825323FA0186007F6BFC /* VolumeControlView.xib in Resources */ = {isa = PBXBuildFile; fileRef = E453825123FA0186007F6BFC /* VolumeControlView.xib */; };
E453825523FA347C007F6BFC /* MPDClient+Mixer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E453825423FA347C007F6BFC /* MPDClient+Mixer.swift */; };
E45878382296173C00586A1C /* AlbumDetailSongRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E45878372296173C00586A1C /* AlbumDetailSongRowView.swift */; };
E45962C62241A78500FC1A1E /* MPDCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = E45962C52241A78500FC1A1E /* MPDCommand.swift */; };
E45E4FDA22515D87004B537F /* CHANGELOG.md in Resources */ = {isa = PBXBuildFile; fileRef = E45E4FD722515D87004B537F /* CHANGELOG.md */; };
@ -81,6 +83,8 @@
E4A642DA22090CBE00067D21 /* MPDStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4A642D922090CBE00067D21 /* MPDStatus.swift */; };
E4A83BEF2221F8CF0098FED6 /* CoverArtPrefsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4A83BEE2221F8CF0098FED6 /* CoverArtPrefsController.swift */; };
E4A83BF12221FAA00098FED6 /* PreferencesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4A83BF02221FAA00098FED6 /* PreferencesViewController.swift */; };
E4B079C723E5E0AD0044B6D3 /* libmpdclient.2.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = E41B22BF21FB6BBA00D544F6 /* libmpdclient.2.dylib */; };
E4B079C823E5E0AD0044B6D3 /* libmpdclient.2.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = E41B22BF21FB6BBA00D544F6 /* libmpdclient.2.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
E4B11B53226928F20075461B /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B11B52226928F20075461B /* AppState.swift */; };
E4B11B61226A4C000075461B /* PlayerReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B11B60226A4BFF0075461B /* PlayerReducer.swift */; };
E4B11B63226A4C510075461B /* AppReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B11B62226A4C510075461B /* AppReducer.swift */; };
@ -90,8 +94,6 @@
E4B11B73226A6C770075461B /* TrackTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B11B72226A6C770075461B /* TrackTimer.swift */; };
E4B11B75226CC4D30075461B /* QueueReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B11B74226CC4D30075461B /* QueueReducer.swift */; };
E4B11B79226D346B0075461B /* AlbumListReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B11B78226D346B0075461B /* AlbumListReducer.swift */; };
E4B11BA62274E44A0075461B /* libmpdclient.2.dylib in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = E41B22BF21FB6BBA00D544F6 /* libmpdclient.2.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
E4B11BA72274E4500075461B /* libmpdclient.2.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = E41B22BF21FB6BBA00D544F6 /* libmpdclient.2.dylib */; };
E4B11BA92274EDE30075461B /* Loading.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B11BA82274EDE30075461B /* Loading.swift */; };
E4B11BB62275374B0075461B /* UserNotificationsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B11BB52275374B0075461B /* UserNotificationsController.swift */; };
E4B11BB8227538FA0075461B /* CurrentCoverArtView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B11BB7227538FA0075461B /* CurrentCoverArtView.swift */; };
@ -100,11 +102,14 @@
E4B11BC22275EE410075461B /* AlbumListActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B11BC12275EE410075461B /* AlbumListActions.swift */; };
E4B3DF6523D66A4400728F6B /* QueueSongCoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B3DF6423D66A4400728F6B /* QueueSongCoverView.swift */; };
E4B5AE7E22F4C49600CCEC65 /* MPDServerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B5AE7D22F4C49600CCEC65 /* MPDServerDelegate.swift */; };
E4BB7F8F23E5E7BC00906E2F /* MPDAlbumArtImageDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4BB7F8E23E5E7BC00906E2F /* MPDAlbumArtImageDataProvider.swift */; };
E4BB7F9323E9150A00906E2F /* CoverArtService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4BB7F9223E9150A00906E2F /* CoverArtService.swift */; };
E4BBD2F323357C0700702C16 /* ArtistListState.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4BBD2F223357C0700702C16 /* ArtistListState.swift */; };
E4C8B53C22342005009A20F3 /* PreferencesWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4C8B53B22342005009A20F3 /* PreferencesWindowController.swift */; };
E4C8B53E22349002009A20F3 /* MPDIdle.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4C8B53D22349002009A20F3 /* MPDIdle.swift */; };
E4D3BFA622B419C000C56F48 /* QueueViewController+NSOutlineViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4D3BFA522B419C000C56F48 /* QueueViewController+NSOutlineViewDelegate.swift */; };
E4DA820623D6236200C1EE58 /* NSSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4DA820523D6236200C1EE58 /* NSSize.swift */; };
E4DCCFAE23E4DB5D009A8113 /* MPDClientWrapper.c in Sources */ = {isa = PBXBuildFile; fileRef = E4DCCFAD23E4DB5D009A8113 /* MPDClientWrapper.c */; };
E4E7A6AD22AAAF98006D566C /* AlbumDetailView+NSTableViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4E7A6AC22AAAF98006D566C /* AlbumDetailView+NSTableViewDelegate.swift */; };
E4E8CC902204EC7F0024217A /* Delegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4E8CC8F2204EC7F0024217A /* Delegate.swift */; };
E4E8CC922204F4B80024217A /* QueueViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4E8CC912204F4B80024217A /* QueueViewController.swift */; };
@ -151,15 +156,15 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
E4B11BA52274E43B0075461B /* Embed Frameworks */ = {
E4B079C923E5E0AD0044B6D3 /* Embed Libraries */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
E4B11BA62274E44A0075461B /* libmpdclient.2.dylib in Embed Frameworks */,
E4B079C823E5E0AD0044B6D3 /* libmpdclient.2.dylib in Embed Libraries */,
);
name = "Embed Frameworks";
name = "Embed Libraries";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
@ -254,10 +259,12 @@
E440519F227BB0AB0090CD6F /* UIState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIState.swift; sourceTree = "<group>"; };
E442CCCC2347E73C00004E0C /* Artist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Artist.swift; sourceTree = "<group>"; };
E450AD7D222620A10091BED3 /* Album.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Album.swift; sourceTree = "<group>"; };
E450AD9422262DF10091BED3 /* CoverArtQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoverArtQueue.swift; sourceTree = "<group>"; };
E450AD9E2229B9BC0091BED3 /* PersephoneBridgingHeader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PersephoneBridgingHeader.h; sourceTree = "<group>"; };
E451E36A22BD214D008BE9B2 /* DraggedSongType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraggedSongType.swift; sourceTree = "<group>"; };
E451E36C22BD23DB008BE9B2 /* DraggedSong.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraggedSong.swift; sourceTree = "<group>"; };
E453825023FA0186007F6BFC /* VolumeControlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VolumeControlView.swift; sourceTree = "<group>"; };
E453825123FA0186007F6BFC /* VolumeControlView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = VolumeControlView.xib; sourceTree = "<group>"; };
E453825423FA347C007F6BFC /* MPDClient+Mixer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MPDClient+Mixer.swift"; sourceTree = "<group>"; };
E45878372296173C00586A1C /* AlbumDetailSongRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumDetailSongRowView.swift; sourceTree = "<group>"; };
E45962C52241A78500FC1A1E /* MPDCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPDCommand.swift; sourceTree = "<group>"; };
E45E4FD722515D87004B537F /* CHANGELOG.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = SOURCE_ROOT; };
@ -294,11 +301,16 @@
E4B11BC12275EE410075461B /* AlbumListActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumListActions.swift; sourceTree = "<group>"; };
E4B3DF6423D66A4400728F6B /* QueueSongCoverView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueueSongCoverView.swift; sourceTree = "<group>"; };
E4B5AE7D22F4C49600CCEC65 /* MPDServerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPDServerDelegate.swift; sourceTree = "<group>"; };
E4BB7F8E23E5E7BC00906E2F /* MPDAlbumArtImageDataProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPDAlbumArtImageDataProvider.swift; sourceTree = "<group>"; };
E4BB7F9223E9150A00906E2F /* CoverArtService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoverArtService.swift; sourceTree = "<group>"; };
E4BBD2F223357C0700702C16 /* ArtistListState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArtistListState.swift; sourceTree = "<group>"; };
E4C8B53B22342005009A20F3 /* PreferencesWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesWindowController.swift; sourceTree = "<group>"; };
E4C8B53D22349002009A20F3 /* MPDIdle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPDIdle.swift; sourceTree = "<group>"; };
E4D3BFA522B419C000C56F48 /* QueueViewController+NSOutlineViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "QueueViewController+NSOutlineViewDelegate.swift"; sourceTree = "<group>"; };
E4DA820523D6236200C1EE58 /* NSSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSSize.swift; sourceTree = "<group>"; };
E4DCCFAB23E4DB5D009A8113 /* Persephone-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Persephone-Bridging-Header.h"; sourceTree = "<group>"; };
E4DCCFAC23E4DB5D009A8113 /* MPDClientWrapper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MPDClientWrapper.h; sourceTree = "<group>"; };
E4DCCFAD23E4DB5D009A8113 /* MPDClientWrapper.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = MPDClientWrapper.c; sourceTree = "<group>"; };
E4E7A6AC22AAAF98006D566C /* AlbumDetailView+NSTableViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AlbumDetailView+NSTableViewDelegate.swift"; sourceTree = "<group>"; };
E4E8CC8F2204EC7F0024217A /* Delegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Delegate.swift; sourceTree = "<group>"; };
E4E8CC912204F4B80024217A /* QueueViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueueViewController.swift; sourceTree = "<group>"; };
@ -322,12 +334,12 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
E4B11BA72274E4500075461B /* libmpdclient.2.dylib in Frameworks */,
E49A5485233E5ADC00EED353 /* Differ in Frameworks */,
E4677C48233E60E70041474F /* MediaKeyTap in Frameworks */,
E43BECA0238835DC00CAF1EB /* Kingfisher in Frameworks */,
E49A548E233E5B6000EED353 /* SwiftyJSON in Frameworks */,
E4E96D13233E630800AFD36F /* PMKFoundation in Frameworks */,
E4B079C723E5E0AD0044B6D3 /* libmpdclient.2.dylib in Frameworks */,
E49A5482233E580800EED353 /* PromiseKit in Frameworks */,
E49A548B233E5B2D00EED353 /* CryptoSwift in Frameworks */,
E49A5488233E5B0000EED353 /* ReSwift in Frameworks */,
@ -387,7 +399,6 @@
E40786242110CE70006887B1 /* Info.plist */,
E4F6B461221E124700ACF42A /* Models */,
E4A642DB220912FA00067D21 /* MPDClient */,
E450AD8922262B420091BED3 /* Operations */,
E40786252110CE70006887B1 /* Persephone.entitlements */,
E450AD9E2229B9BC0091BED3 /* PersephoneBridgingHeader.h */,
E4A83BF2222207BE0098FED6 /* Services */,
@ -441,11 +452,15 @@
E42410B52241B956005ED6DF /* MPDClient+Database.swift */,
E41E5304223BFB0700173814 /* MPDClient+Error.swift */,
E41E5302223BF9C300173814 /* MPDClient+Idle.swift */,
E453825423FA347C007F6BFC /* MPDClient+Mixer.swift */,
E41E5300223BF99300173814 /* MPDClient+Queue.swift */,
E42A4D5022E2167E001C6CAD /* MPDClient+Songs.swift */,
E408D3BD220E03EE0006D9BE /* RawRepresentable.swift */,
E41E5306223C019100173814 /* MPDClient+Status.swift */,
E41E52FE223BF95E00173814 /* MPDClient+Transport.swift */,
E4DCCFAD23E4DB5D009A8113 /* MPDClientWrapper.c */,
E4DCCFAC23E4DB5D009A8113 /* MPDClientWrapper.h */,
E4DCCFAB23E4DB5D009A8113 /* Persephone-Bridging-Header.h */,
E408D3BD220E03EE0006D9BE /* RawRepresentable.swift */,
);
path = Extensions;
sourceTree = "<group>";
@ -512,6 +527,7 @@
E442CCC42347D5B900004E0C /* Components */ = {
isa = PBXGroup;
children = (
E453824D23F9F700007F6BFC /* VolumeControl */,
E442CCCB2347D77A00004E0C /* Browser */,
E4A83BEC2221F5DD0098FED6 /* Preferences */,
E442CCC62347D5E700004E0C /* Queue */,
@ -574,6 +590,7 @@
E442CCC92347D6FD00004E0C /* Shared */ = {
isa = PBXGroup;
children = (
E4BB7F8D23E5E7A300906E2F /* ImageDataProviders */,
E4E13C2C2350D8CB00092A6E /* Layouts */,
E408D3B7220DE8CC0006D9BE /* Extensions */,
E489E3A222B9D31800CA8CBD /* DraggedSongView.swift */,
@ -594,12 +611,13 @@
path = Browser;
sourceTree = "<group>";
};
E450AD8922262B420091BED3 /* Operations */ = {
E453824D23F9F700007F6BFC /* VolumeControl */ = {
isa = PBXGroup;
children = (
E450AD9422262DF10091BED3 /* CoverArtQueue.swift */,
E453825023FA0186007F6BFC /* VolumeControlView.swift */,
E453825123FA0186007F6BFC /* VolumeControlView.xib */,
);
path = Operations;
path = VolumeControl;
sourceTree = "<group>";
};
E4A642DB220912FA00067D21 /* MPDClient */ = {
@ -628,6 +646,7 @@
isa = PBXGroup;
children = (
E439109722640213002982E9 /* SongNotifierService.swift */,
E4BB7F9223E9150A00906E2F /* CoverArtService.swift */,
);
path = Services;
sourceTree = "<group>";
@ -675,6 +694,14 @@
path = Actions;
sourceTree = "<group>";
};
E4BB7F8D23E5E7A300906E2F /* ImageDataProviders */ = {
isa = PBXGroup;
children = (
E4BB7F8E23E5E7BC00906E2F /* MPDAlbumArtImageDataProvider.swift */,
);
path = ImageDataProviders;
sourceTree = "<group>";
};
E4D1B594220BA2490026F233 /* Models */ = {
isa = PBXGroup;
children = (
@ -735,7 +762,7 @@
E40786162110CE6E006887B1 /* Resources */,
E42A98F122430936004D8180 /* ShellScript */,
E421AC9B221F7319008B2449 /* CopyFiles */,
E4B11BA52274E43B0075461B /* Embed Frameworks */,
E4B079C923E5E0AD0044B6D3 /* Embed Libraries */,
);
buildRules = (
);
@ -804,7 +831,7 @@
TargetAttributes = {
E40786172110CE6E006887B1 = {
CreatedOnToolsVersion = 9.4.1;
LastSwiftMigration = 1020;
LastSwiftMigration = 1130;
SystemCapabilities = {
com.apple.HardenedRuntime = {
enabled = 1;
@ -863,6 +890,7 @@
files = (
E45E4FDB22515D87004B537F /* Brewfile in Resources */,
E42A8F3B22176D6400A13ED9 /* LICENSE.md in Resources */,
E453825323FA0186007F6BFC /* VolumeControlView.xib in Resources */,
E45E4FDA22515D87004B537F /* CHANGELOG.md in Resources */,
E43B67AB22909793007DCF55 /* AlbumDetailView.xib in Resources */,
E489E3A522B9D31800CA8CBD /* DraggedSongView.xib in Resources */,
@ -933,13 +961,14 @@
E4B11B63226A4C510075461B /* AppReducer.swift in Sources */,
E41E5307223C019100173814 /* MPDClient+Status.swift in Sources */,
E442CCCD2347E73C00004E0C /* Artist.swift in Sources */,
E4DCCFAE23E4DB5D009A8113 /* MPDClientWrapper.c in Sources */,
E41E530B223C033700173814 /* MPDClient+Album.swift in Sources */,
E42410B62241B956005ED6DF /* MPDClient+Database.swift in Sources */,
E4BB7F8F23E5E7BC00906E2F /* MPDAlbumArtImageDataProvider.swift in Sources */,
E4235640228623D2001216D6 /* QueueSongInfoView.swift in Sources */,
E451E36E22BD2501008BE9B2 /* DraggedSong.swift in Sources */,
E4A642DA22090CBE00067D21 /* MPDStatus.swift in Sources */,
E4B11BC02275EE150075461B /* QueueActions.swift in Sources */,
E450AD9522262DF10091BED3 /* CoverArtQueue.swift in Sources */,
E43B67AD229194CD007DCF55 /* AlbumTracksDataSource.swift in Sources */,
E41E52FD223BF87300173814 /* MPDClient+Connection.swift in Sources */,
E450AD7E222620A10091BED3 /* Album.swift in Sources */,
@ -968,6 +997,7 @@
E419E2872249B96600216A8C /* Song.swift in Sources */,
E451E36B22BD214D008BE9B2 /* DraggedSongType.swift in Sources */,
E4BBD2F323357C0700702C16 /* ArtistListState.swift in Sources */,
E453825223FA0186007F6BFC /* VolumeControlView.swift in Sources */,
E44051A0227BB0AB0090CD6F /* UIState.swift in Sources */,
E4FF718E2276010E00D4C412 /* PreferencesState.swift in Sources */,
E439109822640213002982E9 /* SongNotifierService.swift in Sources */,
@ -978,6 +1008,7 @@
E4F26F7923411B1500D45FF9 /* ArtistReducer.swift in Sources */,
E4B11B73226A6C770075461B /* TrackTimer.swift in Sources */,
E44051942278765A0090CD6F /* App.swift in Sources */,
E4BB7F9323E9150A00906E2F /* CoverArtService.swift in Sources */,
E4B11B79226D346B0075461B /* AlbumListReducer.swift in Sources */,
E47E2FE52220AA0700F747E6 /* FlexibleGridViewLayout.swift in Sources */,
E41E52FF223BF95E00173814 /* MPDClient+Transport.swift in Sources */,
@ -985,6 +1016,7 @@
E47E2FD122205C4600F747E6 /* MainSplitViewController.swift in Sources */,
E4B5AE7E22F4C49600CCEC65 /* MPDServerDelegate.swift in Sources */,
E4B11BB8227538FA0075461B /* CurrentCoverArtView.swift in Sources */,
E453825523FA347C007F6BFC /* MPDClient+Mixer.swift in Sources */,
E4E8CC9A22075D370024217A /* MPDSong.swift in Sources */,
E41EA46C221636AF0068EF46 /* GeneralPrefsViewController.swift in Sources */,
E4E8CC902204EC7F0024217A /* Delegate.swift in Sources */,
@ -1173,6 +1205,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
@ -1188,10 +1221,12 @@
"$(inherited)",
"$(PROJECT_DIR)/libmpdclient/output",
);
MARKETING_VERSION = "0.15.2-alpha";
MARKETING_VERSION = "0.16.0-alpha";
PRODUCT_BUNDLE_IDENTIFIER = me.danbarber.Persephone;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Persephone/MPDClient/Extensions/Persephone-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
SYSTEM_HEADER_SEARCH_PATHS = Persephone/include;
USER_HEADER_SEARCH_PATHS = libmpdclient/output;
@ -1202,6 +1237,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
@ -1217,10 +1253,11 @@
"$(inherited)",
"$(PROJECT_DIR)/libmpdclient/output",
);
MARKETING_VERSION = "0.15.2-alpha";
MARKETING_VERSION = "0.16.0-alpha";
PRODUCT_BUNDLE_IDENTIFIER = me.danbarber.Persephone;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Persephone/MPDClient/Extensions/Persephone-Bridging-Header.h";
SWIFT_VERSION = 5.0;
SYSTEM_HEADER_SEARCH_PATHS = Persephone/include;
USER_HEADER_SEARCH_PATHS = libmpdclient/output;

View File

@ -6,8 +6,8 @@
"repositoryURL": "https://github.com/krzyzanowskim/CryptoSwift",
"state": {
"branch": null,
"revision": "3a2acbb32ab68215ee1596ee6004da8e90c3721b",
"version": "1.0.0"
"revision": "a44caef0550c346e0ab9172f7c9a3852c1833599",
"version": "1.3.0"
}
},
{
@ -15,8 +15,8 @@
"repositoryURL": "https://github.com/tonyarnold/Differ",
"state": {
"branch": null,
"revision": "e2cca36e7258dd8add88ae46b5ea56509b066e21",
"version": "1.4.3"
"revision": "dd5d4bfb1c27012d4790e877b29847d2ab9d989b",
"version": "1.4.4"
}
},
{
@ -24,8 +24,8 @@
"repositoryURL": "https://github.com/PromiseKit/Foundation",
"state": {
"branch": null,
"revision": "ee06d95342a5007de2fffd898f4f35de026842ac",
"version": "3.3.3"
"revision": "1a276e598dac59489ed904887e0740fa75e571e0",
"version": "3.3.4"
}
},
{
@ -33,8 +33,8 @@
"repositoryURL": "https://github.com/onevcat/Kingfisher.git",
"state": {
"branch": null,
"revision": "8ef6ca8b1b767ac2400762ed2f2bf75ddea3de5b",
"version": "5.10.1"
"revision": "44bfa76787a0f07e4d5ae3304e9d4f631fd519bd",
"version": "5.13.0"
}
},
{
@ -51,8 +51,8 @@
"repositoryURL": "https://github.com/mxcl/PromiseKit",
"state": {
"branch": null,
"revision": "4d8d1287d2e50c53a9f8430ffe88925292838c57",
"version": "6.11.0"
"revision": "f14f16cc2602afec1030e4f492100d6d43dca544",
"version": "6.13.1"
}
},
{

View File

@ -27,6 +27,15 @@
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "E40786172110CE6E006887B1"
BuildableName = "Persephone.app"
BlueprintName = "Persephone"
ReferencedContainer = "container:Persephone.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO">
@ -49,17 +58,6 @@
</BuildableReference>
</TestableReference>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "E40786172110CE6E006887B1"
BuildableName = "Persephone.app"
BlueprintName = "Persephone"
ReferencedContainer = "container:Persephone.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
@ -81,8 +79,6 @@
ReferencedContainer = "container:Persephone.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"

View File

@ -0,0 +1,25 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "speakerDisabled.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "speakerDisabled@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 566 B

View File

@ -0,0 +1,25 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "speakerHigh.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "speakerHigh@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 428 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 800 B

View File

@ -0,0 +1,25 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "speakerLow.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "speakerLow@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 531 B

View File

@ -0,0 +1,25 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "speakerMid.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "speakerMid@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 357 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 648 B

View File

@ -0,0 +1,25 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "speakerOff.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "speakerOff@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 B

View File

@ -56,10 +56,14 @@ class AlbumViewItem: NSCollectionViewItem {
}
func setAlbumCover(_ album: Album) {
guard let imagePath = album.coverArtFilePath else { return }
guard let song = album.mpdAlbum.firstSong
else { return }
let provider = MPDAlbumArtImageDataProvider(
songUri: song.uriString,
cacheKey: album.hash
)
let imageURL = URL(fileURLWithPath: imagePath)
let provider = LocalFileImageDataProvider(fileURL: imageURL)
albumCoverView.kf.setImage(
with: .provider(provider),
placeholder: NSImage.defaultCoverArt,
@ -67,7 +71,34 @@ class AlbumViewItem: NSCollectionViewItem {
.processor(DownsamplingImageProcessor(size: .albumListCoverSize)),
.scaleFactor(2),
]
)
) { result in
switch result {
case .success(let imageResult):
guard let imageData = imageResult.image.tiffRepresentation
else { return }
let rawProvider = RawImageDataProvider(
data: imageData,
cacheKey: album.hash
)
self.cacheSmallCover(provider: rawProvider)
case .failure(_):
break
}
}
}
func cacheSmallCover(provider: ImageDataProvider) {
_ = KingfisherManager.shared.retrieveImage(
with: .provider(provider),
options: [
.memoryCacheExpiration(.never),
.processor(DownsamplingImageProcessor(size: .queueSongCoverSize)),
.scaleFactor(2),
]
) { result in }
}
func setAppearance(selected isSelected: Bool) {
@ -88,6 +119,18 @@ class AlbumViewItem: NSCollectionViewItem {
boxLayer.backgroundColor = albumCoverBox.layer?.borderColor
}
}
func refreshAlbumArt() {
guard let album = album,
let mpdSong = album.mpdAlbum.firstSong
else { return }
let song = Song(mpdSong: mpdSong)
CoverArtService(song: song).refresh {
self.setAlbumCover(album)
}
}
@IBAction func showAlbumDetail(_ sender: NSButton) {
guard let album = album else { return }
@ -121,4 +164,8 @@ class AlbumViewItem: NSCollectionViewItem {
App.mpdClient.addAlbumToQueue(album: album.mpdAlbum, at: queueLength)
}
@IBAction func refreshAlbumArtMenuAction(_ sender: NSMenuItem) {
refreshAlbumArt()
}
}

View File

@ -35,7 +35,7 @@
</shadow>
<buttonCell key="cell" type="square" bezelStyle="shadowlessSquare" image="defaultCoverArt" imagePosition="only" alignment="center" lineBreakMode="truncatingTail" refusesFirstResponder="YES" state="on" transparent="YES" imageScaling="proportionallyUpOrDown" inset="2" id="t8A-Hz-L38">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="label" size="13"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="showAlbumDetail:" target="-2" id="A4Q-gb-B45"/>
@ -54,7 +54,7 @@
<textField identifier="albumTitle" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="KEh-NL-c2W">
<rect key="frame" x="8" y="28" width="142" height="17"/>
<textFieldCell key="cell" lineBreakMode="truncatingTail" title="Label" id="pDs-0t-e1j">
<font key="font" metaFont="label" size="13"/>
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
@ -62,7 +62,7 @@
<textField identifier="albumArtist" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="5Uu-j1-qyT">
<rect key="frame" x="8" y="10" width="142" height="16"/>
<textFieldCell key="cell" lineBreakMode="truncatingTail" title="Label" id="yZn-e9-zyP">
<font key="font" metaFont="label" size="13"/>
<font key="font" metaFont="system"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
@ -98,6 +98,13 @@
<action selector="addAlbumToQueueMenuAction:" target="-2" id="6wW-oR-ykh"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="g9v-9R-xDB"/>
<menuItem title="Refresh album art" id="4Ld-db-wka">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="refreshAlbumArtMenuAction:" target="-2" id="wyO-Dn-k9r"/>
</connections>
</menuItem>
</items>
<point key="canvasLocation" x="191" y="38"/>
</menu>

View File

@ -116,27 +116,27 @@ class AlbumDetailView: NSViewController {
App.mpdClient.getAlbumSongs(for: album.mpdAlbum) { [weak self] (mpdSongs: [MPDClient.MPDSong]) in
guard let self = self else { return }
self.dataSource.setAlbumSongs(
mpdSongs.map { Song(mpdSong: $0) }
)
DispatchQueue.main.async {
self.dataSource.setAlbumSongs(
mpdSongs.map { Song(mpdSong: $0) }
)
self.albumTracksView.reloadData()
guard let mpdSong = album.mpdAlbum.firstSong
else { return }
self.getBigCoverArt(song: Song(mpdSong: mpdSong), album: album)
}
guard let song = self.dataSource.albumSongs.first?.song ??
self.dataSource.albumSongs[1].song
else { return }
self.getBigCoverArt(song: song, album: album)
}
}
func getBigCoverArt(song: Song, album: Album) {
guard let imagePath = album.coverArtFilePath else { return }
let provider = MPDAlbumArtImageDataProvider(
songUri: song.mpdSong.uriString,
cacheKey: album.hash
)
let imageURL = URL(fileURLWithPath: imagePath)
let provider = LocalFileImageDataProvider(fileURL: imageURL)
albumCoverView.kf.setImage(
with: .provider(provider),
placeholder: NSImage.defaultCoverArt,
@ -145,18 +145,6 @@ class AlbumDetailView: NSViewController {
.scaleFactor(2),
]
)
cacheSmallCover(provider: provider)
}
func cacheSmallCover(provider: ImageDataProvider) {
_ = KingfisherManager.shared.retrieveImage(
with: .provider(provider),
options: [
.processor(DownsamplingImageProcessor(size: .queueSongCoverSize)),
.scaleFactor(2),
]
) { result in }
}
func setAppearance() {

View File

@ -50,7 +50,8 @@ class AlbumTracksDataSource: NSObject, NSTableViewDataSource {
type: .albumSongItem(song.mpdSong.uriString),
title: song.title,
artist: song.artist,
cover: song.album.coverArtFilePath
album: song.album.title,
uri: song.mpdSong.uriString
),
ofType: .songPasteboardType
)
@ -69,10 +70,11 @@ class AlbumTracksDataSource: NSObject, NSTableViewDataSource {
) { draggingItem, index, stop in
guard let item = draggingItem.item as? NSPasteboardItem,
let draggedSong = item.draggedSong(forType: .songPasteboardType),
case let (title?, artist?, cover?) = (
case let (title, artist, album, uri) = (
draggedSong.title,
draggedSong.artist,
draggedSong.cover
draggedSong.album,
draggedSong.uri
)
else { return }
@ -81,7 +83,8 @@ class AlbumTracksDataSource: NSObject, NSTableViewDataSource {
let draggedSongView = DraggedSongView(
title: title,
artist: artist,
cover: cover
album: album,
uri: uri
)
component.contents = draggedSongView.view.image()

View File

@ -7,15 +7,12 @@
//
import AppKit
import Kingfisher
class CoverArtPrefsController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
if let mpdLibraryDir = App.store.state.preferencesState.mpdLibraryDir {
mpdLibraryDirField.stringValue = mpdLibraryDir
}
if App.store.state.preferencesState.fetchMissingArtworkFromInternet {
fetchMissingArtworkFromInternet.state = .on
} else {
@ -33,12 +30,6 @@ class CoverArtPrefsController: NSViewController {
self.parent?.view.window?.title = title
}
@IBAction func updateMpdLibraryDir(_ sender: NSTextField) {
App.store.dispatch(UpdateMPDLibraryDir(mpdLibraryDir: sender.stringValue))
}
@IBOutlet var mpdLibraryDirField: NSTextField!
@IBAction func updateFetchMissingArtworkFromInternet(_ sender: NSButton) {
App.store.dispatch(
UpdateFetchMissingArtworkFromInternet(
@ -47,5 +38,10 @@ class CoverArtPrefsController: NSViewController {
)
}
@IBAction func clearAlbumArtCache(_ sender: NSButton) {
KingfisherManager.shared.cache.clearDiskCache()
KingfisherManager.shared.cache.clearMemoryCache()
}
@IBOutlet var fetchMissingArtworkFromInternet: NSButton!
}

View File

@ -17,13 +17,23 @@ class CurrentCoverArtView: NSImageView {
App.store.subscribe(self) {
$0.select { $0.playerState.currentSong }
}
NotificationCenter.default.addObserver(self, selector: #selector(didReloadAlbumArt), name: .didReloadAlbumArt, object: nil)
}
@objc func didReloadAlbumArt() {
guard let song = App.store.state.playerState.currentSong
else { return }
setSongImage(song)
}
func setAlbumImage(_ album: Album) {
guard let imagePath = album.coverArtFilePath else { return }
func setSongImage(_ song: Song) {
let provider = MPDAlbumArtImageDataProvider(
songUri: song.mpdSong.uriString,
cacheKey: song.album.hash
)
let imageURL = URL(fileURLWithPath: imagePath)
let provider = LocalFileImageDataProvider(fileURL: imageURL)
kf.setImage(
with: .provider(provider),
placeholder: NSImage.defaultCoverArt,
@ -44,6 +54,6 @@ extension CurrentCoverArtView: StoreSubscriber {
return
}
setAlbumImage(song.album)
setSongImage(song)
}
}

View File

@ -44,7 +44,8 @@ class QueueDataSource: NSObject, NSOutlineViewDataSource {
type: .queueItem(queueItem.queuePos),
title: queueItem.song.title,
artist: queueItem.song.artist,
cover: queueItem.song.album.coverArtFilePath
album: queueItem.song.album.title,
uri: queueItem.song.mpdSong.uriString
),
ofType: .songPasteboardType
)
@ -128,10 +129,11 @@ class QueueDataSource: NSObject, NSOutlineViewDataSource {
guard let item = draggingItem.item as? NSPasteboardItem,
let data = item.data(forType: .songPasteboardType),
let draggedSong = try? PropertyListDecoder().decode(DraggedSong.self, from: data),
case let (title?, artist?, cover?) = (
case let (title, artist, album, uri) = (
draggedSong.title,
draggedSong.artist,
draggedSong.cover
draggedSong.album,
draggedSong.uri
)
else { return }
@ -140,7 +142,8 @@ class QueueDataSource: NSObject, NSOutlineViewDataSource {
let draggedSongView = DraggedSongView(
title: title,
artist: artist,
cover: cover
album: album,
uri: uri
)
let view = draggedSongView.view

View File

@ -60,14 +60,15 @@ class QueueSongCoverView: NSTableCellView {
}
func setSong(_ queueItem: QueueItem, queueIcon: NSImage?) {
guard let imagePath = queueItem.song.album.coverArtFilePath
else { return }
let song = queueItem.song
isPlaying = queueItem.isPlaying
let imageURL = URL(fileURLWithPath: imagePath)
let provider = LocalFileImageDataProvider(fileURL: imageURL)
let provider = MPDAlbumArtImageDataProvider(
songUri: song.mpdSong.uriString,
cacheKey: song.album.hash
)
queueSongCover.kf.setImage(
with: .provider(provider),
placeholder: NSImage.defaultCoverArt,

View File

@ -24,6 +24,7 @@ class QueueViewController: NSViewController {
NotificationCenter.default.addObserver(self, selector: #selector(didConnect), name: .didConnect, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(willDisconnect), name: .willDisconnect, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(didReloadAlbumArt), name: .didReloadAlbumArt, object: nil)
queueView.dataSource = dataSource
queueView.columnAutoresizingStyle = .sequentialColumnAutoresizingStyle
@ -61,6 +62,10 @@ class QueueViewController: NSViewController {
App.store.dispatch(UpdateQueueAction(queue: []))
}
}
@objc func didReloadAlbumArt() {
queueView.reloadData()
}
@IBAction func playTrack(_ sender: Any) {
let queuePos = queueView.selectedRow

View File

@ -16,12 +16,14 @@ class DraggedSongView: NSViewController {
private let songTitle: String
private let songArtist: String
private let songCover: String?
private let songAlbum: String
private let songUri: String
init(title: String, artist: String, cover: String? = nil) {
init(title: String, artist: String, album: String, uri: String) {
songTitle = title
songArtist = artist
songCover = cover
songAlbum = album
songUri = uri
super.init(nibName: nil, bundle: nil)
}
@ -58,11 +60,13 @@ class DraggedSongView: NSViewController {
}
func setCoverArt() {
guard let imagePath = songCover else { return }
let mpdAlbum = MPDClient.MPDAlbum(title: songAlbum, artist: songArtist)
let provider = MPDAlbumArtImageDataProvider(
songUri: songUri,
cacheKey: Album(mpdAlbum: mpdAlbum).hash
)
let imageURL = URL(fileURLWithPath: imagePath)
let provider = LocalFileImageDataProvider(fileURL: imageURL)
coverImage.kf.setImage(
with: .provider(provider),
placeholder: NSImage.defaultCoverArt,

View File

@ -15,34 +15,10 @@ extension NSImage {
static let queuePauseIcon = NSImage(named: "queuePauseButton")
static let defaultCoverArt = NSImage(named: "defaultCoverArt")
func toFitBox(size: NSSize) -> NSImage {
var newSize: NSSize = NSSize.zero
let aspectRatio = self.size.width / self.size.height
let boxAspectRatio = size.width / size.height
if aspectRatio > boxAspectRatio {
newSize = NSSize(width: size.width, height: size.width / aspectRatio)
} else {
newSize = NSSize(width: size.height * aspectRatio, height: size.height)
}
let newImage = NSImage(size: newSize)
newImage.lockFocus()
self.draw(in: newImage.alignmentRect)
newImage.unlockFocus()
return newImage
}
func jpegData(compressionQuality: CGFloat) -> Data? {
guard let image = cgImage(forProposedRect: nil, context: nil, hints: nil)
else { return nil }
let bitmapImageRep = NSBitmapImageRep(cgImage: image)
return bitmapImageRep.representation(
using: .jpeg,
properties: [.compressionFactor: compressionQuality]
)
}
static let speakerDisabled = NSImage(named: "speakerDisabled")
static let speakerOff = NSImage(named: "speakerOff")
static let speakerLow = NSImage(named: "speakerLow")
static let speakerMid = NSImage(named: "speakerMid")
static let speakerHigh = NSImage(named: "speakerHigh")
}

View File

@ -11,4 +11,5 @@ import Foundation
extension Notification.Name {
static let didConnect = Notification.Name("MPDClientDidConnect")
static let willDisconnect = Notification.Name("MPDClientWillDisconnect")
static let didReloadAlbumArt = Notification.Name("MPDDidReloadAlbumArt")
}

View File

@ -0,0 +1,34 @@
//
// MPDAlbumArtImageDataProvider.swift
// Persephone
//
// Created by Daniel Barber on 2/1/20.
// Copyright © 2020 Dan Barber. All rights reserved.
//
import Foundation
import Kingfisher
public struct MPDAlbumArtImageDataProvider: ImageDataProvider {
let songUri: String
init(songUri: String, cacheKey: String) {
self.songUri = songUri
self.cacheKey = cacheKey
}
public var cacheKey: String
public func data(handler: @escaping (Result<Data, Error>) -> Void) {
App.mpdClient.fetchAlbumArt(songUri: songUri, imageData: nil) { imageData in
guard let imageData = imageData
else { return }
handler(.success(imageData))
}
}
public var contentURL: String? {
return songUri
}
}

View File

@ -18,14 +18,16 @@ class UserNotificationsController {
}
func notifyTrack(_ state: Song?) {
guard let currentSong = state,
let coverArtFilePath = currentSong.album.coverArtFilePath,
guard let song = state,
let status = App.mpdClient.status,
status.state == .playing
else { return }
let provider = MPDAlbumArtImageDataProvider(
songUri: song.mpdSong.uriString,
cacheKey: song.album.hash
)
let imageURL = URL(fileURLWithPath: coverArtFilePath)
let provider = LocalFileImageDataProvider(fileURL: imageURL)
_ = KingfisherManager.shared.retrieveImage(
with: .provider(provider),
options: [
@ -35,7 +37,7 @@ class UserNotificationsController {
) { result in
switch result {
case .success(let value):
SongNotifierService(song: currentSong, image: value.image)
SongNotifierService(song: song, image: value.image)
.deliver()
case .failure:
break

View File

@ -0,0 +1,45 @@
//
// VolumeControlView.swift
// Persephone
//
// Created by Daniel Barber on 2/16/20.
// Copyright © 2020 Dan Barber. All rights reserved.
//
import AppKit
import ReSwift
class VolumeControlView: NSViewController {
static let shared = VolumeControlView()
static let popover = NSPopover()
var currentVolume: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
App.store.subscribe(self) {
$0.select { $0.playerState }
}
}
@IBAction func volumeSliderAction(_ sender: NSSlider) {
let newVolume = sender.integerValue
if newVolume != currentVolume {
App.mpdClient.setVolume(to: newVolume)
currentVolume = newVolume
}
}
@IBOutlet var volumeSlider: NSSlider!
}
extension VolumeControlView: StoreSubscriber {
typealias StoreSubscriberStateType = PlayerState
func newState(state: StoreSubscriberStateType) {
volumeSlider.integerValue = state.volume
currentVolume = state.volume
}
}

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="15705" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="15705"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="VolumeControlView" customModule="Persephone" customModuleProvider="target">
<connections>
<outlet property="view" destination="Hz6-mo-xeY" id="0bl-1N-x8E"/>
<outlet property="volumeSlider" destination="E78-vZ-qV0" id="7qP-Fm-MDY"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<customView id="Hz6-mo-xeY">
<rect key="frame" x="0.0" y="0.0" width="36" height="145"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<subviews>
<slider horizontalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="E78-vZ-qV0" userLabel="Volume Slider">
<rect key="frame" x="9" y="12" width="19" height="121"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<sliderCell key="cell" continuous="YES" alignment="left" maxValue="100" doubleValue="50" tickMarkPosition="right" sliderType="linear" id="22u-9w-IXT"/>
<connections>
<action selector="volumeSliderAction:" target="-2" id="1yh-vH-sgN"/>
</connections>
</slider>
</subviews>
<point key="canvasLocation" x="-83" y="90.5"/>
</customView>
</objects>
</document>

View File

@ -208,7 +208,7 @@
<rect key="frame" x="0.0" y="14" width="153" height="24"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<segmentedCell key="cell" borderStyle="border" alignment="left" style="texturedRounded" trackingMode="momentary" id="EBk-sD-nG7">
<font key="font" metaFont="system"/>
<font key="font" metaFont="label" size="13"/>
<segments>
<segment image="prevTrackButton" width="32" enabled="NO"/>
<segment image="playButton" width="48" enabled="NO" tag="1"/>
@ -230,7 +230,7 @@
<rect key="frame" x="16" y="14" width="55" height="17"/>
<autoresizingMask key="autoresizingMask"/>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="right" placeholderString="8:88:88" id="g0c-k5-wCA">
<font key="font" metaFont="system"/>
<font key="font" metaFont="label" size="13"/>
<color key="textColor" name="tertiaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
@ -257,7 +257,7 @@
<rect key="frame" x="16" y="14" width="60" height="17"/>
<autoresizingMask key="autoresizingMask"/>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="left" placeholderString="-8:88:88" id="XUa-pD-s5c">
<font key="font" metaFont="system"/>
<font key="font" metaFont="label" size="13"/>
<color key="textColor" name="tertiaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
@ -277,9 +277,9 @@
<button key="view" verticalHuggingPriority="750" id="E8L-uK-XT0">
<rect key="frame" x="2" y="14" width="42" height="24"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="roundTextured" bezelStyle="texturedRounded" image="shuffleButton" imagePosition="only" alignment="center" lineBreakMode="truncatingTail" borderStyle="border" inset="2" id="YNb-hd-ax8">
<buttonCell key="cell" type="roundTextured" bezelStyle="texturedRounded" image="shuffleButton" imagePosition="overlaps" alignment="center" lineBreakMode="truncatingTail" borderStyle="border" inset="2" id="YNb-hd-ax8">
<behavior key="behavior" pushIn="YES" changeContents="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
<font key="font" metaFont="label" size="13"/>
</buttonCell>
<connections>
<action selector="handleShuffleButton:" target="B8D-0N-5wS" id="THd-0g-fmb"/>
@ -295,7 +295,7 @@
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="roundTextured" bezelStyle="texturedRounded" image="repeatButton" imagePosition="only" alignment="center" lineBreakMode="truncatingTail" borderStyle="border" inset="2" id="1bu-vK-3Hb">
<behavior key="behavior" pushIn="YES" changeContents="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
<font key="font" metaFont="label" size="13"/>
</buttonCell>
<connections>
<action selector="handleRepeatButton:" target="B8D-0N-5wS" id="EN2-u4-DNl"/>
@ -308,7 +308,7 @@
<rect key="frame" x="0.0" y="14" width="96" height="22"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<searchFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" borderStyle="bezel" usesSingleLineMode="YES" bezelStyle="round" id="F3N-3P-tS3">
<font key="font" metaFont="system"/>
<font key="font" metaFont="label" size="13"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</searchFieldCell>
@ -317,6 +317,20 @@
</connections>
</searchField>
</toolbarItem>
<toolbarItem implicitItemIdentifier="01E8DA80-2BDE-49F9-B311-69876CF0AE8E" label="Volume" paletteLabel="Volume" image="speakerHigh" sizingBehavior="auto" id="cMg-Mj-j7q">
<nil key="toolTip"/>
<button key="view" verticalHuggingPriority="750" id="cfN-LI-Cab">
<rect key="frame" x="2" y="14" width="42" height="24"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="roundTextured" bezelStyle="texturedRounded" image="speakerHigh" imagePosition="overlaps" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="bJh-X9-7q0">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="label" size="13"/>
</buttonCell>
<connections>
<action selector="showVolumeControl:" target="B8D-0N-5wS" id="UoW-fa-jBM"/>
</connections>
</button>
</toolbarItem>
</allowedToolbarItems>
<defaultToolbarItems>
<toolbarItem reference="p3r-ty-Pxf"/>
@ -329,6 +343,8 @@
<toolbarItem reference="s1h-EC-nvL"/>
<toolbarItem reference="5U7-UV-xn2"/>
<toolbarItem reference="9ol-aR-mzv"/>
<toolbarItem reference="cMg-Mj-j7q"/>
<toolbarItem reference="mhg-16-CNM"/>
<toolbarItem reference="FRe-rR-Ulo"/>
</defaultToolbarItems>
</toolbar>
@ -345,6 +361,7 @@
<outlet property="trackProgressBar" destination="KMy-xf-rmN" id="a67-JU-cyQ"/>
<outlet property="trackRemaining" destination="9WZ-ij-lrb" id="0pH-d7-wvD"/>
<outlet property="transportControls" destination="EBk-sD-nG7" id="yOo-58-Fby"/>
<outlet property="volumeState" destination="cfN-LI-Cab" id="hrE-SY-J9E"/>
<segue destination="fnD-7K-pHK" kind="relationship" relationship="window.shadowedContentViewController" id="fQQ-kB-KVc"/>
</connections>
</windowController>
@ -409,7 +426,7 @@
<tabView key="tabView" type="noTabsNoBorder" id="6dC-M0-oC5">
<rect key="frame" x="0.0" y="0.0" width="418" height="300"/>
<autoresizingMask key="autoresizingMask"/>
<font key="font" metaFont="system"/>
<font key="font" metaFont="label" size="13"/>
<connections>
<outlet property="delegate" destination="zhe-qh-Mal" id="LUL-qN-JlP"/>
</connections>
@ -429,93 +446,47 @@
<objects>
<viewController title="Cover Art" id="3C9-vU-zjZ" userLabel="Cover Art" customClass="CoverArtPrefsController" customModule="Persephone" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" id="PyK-v2-kus">
<rect key="frame" x="0.0" y="0.0" width="524" height="168"/>
<rect key="frame" x="0.0" y="0.0" width="420" height="103"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="zZn-Rm-e1f">
<rect key="frame" x="53" y="129" width="104" height="16"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="Music Directory:" id="sPn-V6-CfK">
<font key="font" usesAppearanceFont="YES"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="gDk-ca-eOa">
<rect key="frame" x="162" y="125" width="288" height="21"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" placeholderString="~/Music" drawsBackground="YES" id="7WZ-b7-GUs">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<connections>
<action selector="updateMpdLibraryDir:" target="3C9-vU-zjZ" id="3Ta-fH-5Zh"/>
</connections>
</textField>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="pRL-MG-1Be">
<rect key="frame" x="160" y="94" width="264" height="18"/>
<rect key="frame" x="82" y="67" width="264" height="18"/>
<constraints>
<constraint firstAttribute="width" constant="260" id="PJN-iZ-3RV"/>
</constraints>
<buttonCell key="cell" type="check" title="Fetch missing artwork from MusicBrainz" bezelStyle="regularSquare" imagePosition="left" enabled="NO" inset="2" id="LpD-Ew-HMd">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
<font key="font" metaFont="label" size="13"/>
</buttonCell>
<connections>
<action selector="updateFetchMissingArtworkFromInternet:" target="3C9-vU-zjZ" id="I7x-9V-xJr"/>
</connections>
</button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="z1g-nP-ksw">
<rect key="frame" x="160" y="63" width="264" height="18"/>
<constraints>
<constraint firstAttribute="width" constant="260" id="gK0-aW-CJy"/>
</constraints>
<buttonCell key="cell" type="check" title="Save fetched artwork to music directory" bezelStyle="regularSquare" imagePosition="left" enabled="NO" inset="2" id="ZeZ-O4-vjS">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="mXh-kY-tMC">
<rect key="frame" x="78" y="21" width="185" height="32"/>
<buttonCell key="cell" type="push" title="Clear album art cache..." bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="l81-SG-7mf">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="label" size="13"/>
</buttonCell>
<connections>
<action selector="clearAlbumArtCache:" target="3C9-vU-zjZ" id="tXg-rz-lvh"/>
</connections>
</button>
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="xgS-Kg-8KR">
<rect key="frame" x="162" y="26" width="144" height="21"/>
<constraints>
<constraint firstAttribute="width" constant="144" id="DSX-th-Wn1"/>
</constraints>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" enabled="NO" sendsActionOnEndEditing="YES" borderStyle="bezel" placeholderString="folder.jpg" drawsBackground="YES" id="nKF-YI-xBL">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="SmH-w6-5QI">
<rect key="frame" x="37" y="30" width="119" height="16"/>
<textFieldCell key="cell" lineBreakMode="clipping" enabled="NO" alignment="right" title="Cover art filename:" id="b4u-u7-iWD">
<font key="font" metaFont="system"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<constraints>
<constraint firstItem="pRL-MG-1Be" firstAttribute="centerX" secondItem="z1g-nP-ksw" secondAttribute="centerX" id="0Ev-Ia-XBO"/>
<constraint firstItem="gDk-ca-eOa" firstAttribute="leading" secondItem="pRL-MG-1Be" secondAttribute="leading" id="1jd-wZ-a4Q"/>
<constraint firstItem="pRL-MG-1Be" firstAttribute="leading" secondItem="z1g-nP-ksw" secondAttribute="leading" id="3tZ-Ub-RaT"/>
<constraint firstItem="gDk-ca-eOa" firstAttribute="leading" secondItem="zZn-Rm-e1f" secondAttribute="trailing" constant="7" id="C0m-yx-gXh"/>
<constraint firstItem="z1g-nP-ksw" firstAttribute="leading" secondItem="xgS-Kg-8KR" secondAttribute="leading" id="Dkv-ai-X2G"/>
<constraint firstItem="xgS-Kg-8KR" firstAttribute="leading" secondItem="SmH-w6-5QI" secondAttribute="trailing" constant="8" symbolic="YES" id="KQk-nr-H8y"/>
<constraint firstItem="zZn-Rm-e1f" firstAttribute="leading" secondItem="PyK-v2-kus" secondAttribute="leading" constant="55" id="OzK-MR-zuB"/>
<constraint firstAttribute="bottom" secondItem="SmH-w6-5QI" secondAttribute="bottom" constant="30" id="aBY-Ny-jPe"/>
<constraint firstItem="pRL-MG-1Be" firstAttribute="top" secondItem="gDk-ca-eOa" secondAttribute="bottom" constant="15" id="dKy-uC-r43"/>
<constraint firstItem="xgS-Kg-8KR" firstAttribute="top" secondItem="z1g-nP-ksw" secondAttribute="bottom" constant="18" id="lfR-Im-bd4"/>
<constraint firstAttribute="trailing" secondItem="gDk-ca-eOa" secondAttribute="trailing" constant="74" id="n8X-T2-tXA"/>
<constraint firstAttribute="bottom" secondItem="xgS-Kg-8KR" secondAttribute="bottom" constant="26" id="oXZ-qo-HwX"/>
<constraint firstItem="SmH-w6-5QI" firstAttribute="top" secondItem="zZn-Rm-e1f" secondAttribute="bottom" constant="83" id="qhC-mD-Bvw"/>
<constraint firstItem="z1g-nP-ksw" firstAttribute="top" secondItem="pRL-MG-1Be" secondAttribute="bottom" constant="17" id="sTP-hk-zfU"/>
<constraint firstItem="pRL-MG-1Be" firstAttribute="leading" secondItem="PyK-v2-kus" secondAttribute="leading" constant="84" id="81s-YL-o8A"/>
<constraint firstAttribute="bottom" secondItem="pRL-MG-1Be" secondAttribute="bottom" constant="69" id="L7b-jE-keG"/>
<constraint firstItem="mXh-kY-tMC" firstAttribute="leading" secondItem="pRL-MG-1Be" secondAttribute="leading" id="b9X-hO-aYJ"/>
<constraint firstAttribute="bottom" secondItem="mXh-kY-tMC" secondAttribute="bottom" constant="28" id="ras-nE-Oq8"/>
</constraints>
</view>
<connections>
<outlet property="fetchMissingArtworkFromInternet" destination="pRL-MG-1Be" id="Xcp-sb-iZm"/>
<outlet property="mpdLibraryDirField" destination="gDk-ca-eOa" id="myi-BQ-0NS"/>
</connections>
</viewController>
<customObject id="KzD-E3-lpA" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1459" y="-242"/>
<point key="canvasLocation" x="1407" y="-274.5"/>
</scene>
<!--View Controller-->
<scene sceneID="VvW-vT-alQ">
@ -528,7 +499,7 @@
<subviews>
<tabView type="noTabsNoBorder" initialItem="XgS-cX-SDH" translatesAutoresizingMaskIntoConstraints="NO" id="ARv-cj-xlz">
<rect key="frame" x="0.0" y="0.0" width="478" height="558"/>
<font key="font" metaFont="system"/>
<font key="font" metaFont="label" size="13"/>
<tabViewItems>
<tabViewItem label="Albums" identifier="" id="XgS-cX-SDH">
<view key="view" id="hB7-hN-SbB">
@ -593,7 +564,7 @@
<real key="minimum" value="0.0"/>
<real key="maximum" value="65535"/>
</numberFormatter>
<font key="font" metaFont="system"/>
<font key="font" metaFont="label" size="13"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
@ -604,7 +575,7 @@
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="kvB-99-zwY">
<rect key="frame" x="76" y="61" width="80" height="16"/>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="right" title="Server Host:" id="AVi-g9-Irz">
<font key="font" metaFont="system"/>
<font key="font" metaFont="label" size="13"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
@ -615,7 +586,7 @@
<constraint firstAttribute="width" constant="72" id="Of6-Ls-knP"/>
</constraints>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="right" title="Server Port:" id="DgA-xT-2ir">
<font key="font" metaFont="system"/>
<font key="font" metaFont="label" size="13"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
@ -647,18 +618,18 @@
<objects>
<viewController id="KIP-rq-4dM" customClass="QueueViewController" customModule="Persephone" customModuleProvider="target" sceneMemberID="viewController">
<splitView key="view" misplaced="YES" dividerStyle="thin" id="84I-w3-Mxl">
<rect key="frame" x="0.0" y="0.0" width="329" height="543"/>
<rect key="frame" x="0.0" y="0.0" width="329" height="498"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<subviews>
<scrollView borderType="none" autohidesScrollers="YES" horizontalLineScroll="42" horizontalPageScroll="10" verticalLineScroll="42" verticalPageScroll="10" usesPredominantAxisScrolling="NO" id="S3o-nF-NN7">
<rect key="frame" x="0.0" y="0.0" width="329" height="217"/>
<rect key="frame" x="0.0" y="0.0" width="329" height="198"/>
<autoresizingMask key="autoresizingMask" flexibleMaxY="YES"/>
<clipView key="contentView" drawsBackground="NO" id="WI8-Pw-03L">
<rect key="frame" x="0.0" y="0.0" width="329" height="217"/>
<rect key="frame" x="0.0" y="0.0" width="329" height="198"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<outlineView verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnAutoresizingStyle="lastColumnOnly" selectionHighlightStyle="sourceList" multipleSelection="NO" autosaveColumns="NO" rowHeight="42" rowSizeStyle="automatic" viewBased="YES" indentationMarkerFollowsCell="NO" outlineTableColumn="0Co-uF-CCB" id="jEJ-jg-fll">
<rect key="frame" x="0.0" y="0.0" width="329" height="217"/>
<rect key="frame" x="0.0" y="0.0" width="329" height="198"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<size key="intercellSpacing" width="3" height="0.0"/>
<color key="backgroundColor" name="_sourceListBackgroundColor" catalog="System" colorSpace="catalog"/>
@ -666,12 +637,11 @@
<tableColumns>
<tableColumn identifier="songCoverColumn" width="43" minWidth="42" maxWidth="1000" id="0Co-uF-CCB" userLabel="Position">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border">
<font key="font" metaFont="message" size="11"/>
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="headerColor" catalog="System" colorSpace="catalog"/>
</tableHeaderCell>
<textFieldCell key="dataCell" lineBreakMode="truncatingTail" selectable="YES" editable="YES" title="Text Cell" id="99v-Rb-3kv">
<font key="font" metaFont="system"/>
<font key="font" metaFont="label" size="13"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
@ -712,12 +682,11 @@
</tableColumn>
<tableColumn identifier="songInfoColumn" width="213" minWidth="10" maxWidth="3.4028234663852886e+38" id="HP0-ty-PFY" userLabel="Song Info">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="left">
<font key="font" metaFont="message" size="11"/>
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</tableHeaderCell>
<textFieldCell key="dataCell" lineBreakMode="truncatingTail" selectable="YES" editable="YES" alignment="left" title="Text Cell" id="zb2-QK-DhK">
<font key="font" metaFont="system"/>
<font key="font" metaFont="label" size="13"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
@ -738,7 +707,7 @@
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="i0h-bn-auJ" userLabel="Song Title View">
<rect key="frame" x="1" y="23" width="211" height="16"/>
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="Song Title" id="ei8-1e-ErK">
<font key="font" metaFont="system"/>
<font key="font" metaFont="label" size="13"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
@ -761,12 +730,11 @@
</tableColumn>
<tableColumn identifier="songDurationColumn" width="64" minWidth="64" maxWidth="64" id="8O6-ox-kx2" userLabel="Duration">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="left">
<font key="font" metaFont="message" size="11"/>
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</tableHeaderCell>
<textFieldCell key="dataCell" lineBreakMode="truncatingTail" selectable="YES" editable="YES" alignment="left" title="Text Cell" id="JOa-Mc-ceQ">
<font key="font" metaFont="system"/>
<font key="font" metaFont="label" size="13"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
@ -783,7 +751,7 @@
<constraint firstAttribute="height" constant="17" id="grB-CG-1vJ"/>
</constraints>
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" alignment="right" title="88:88" id="JnJ-sF-vCP">
<font key="font" metaFont="system"/>
<font key="font" metaFont="label" size="13"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
@ -824,7 +792,7 @@
</scroller>
</scrollView>
<customView misplaced="YES" id="iUb-eV-Qws">
<rect key="frame" x="0.0" y="218" width="329" height="325"/>
<rect key="frame" x="0.0" y="199" width="329" height="299"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="250" verticalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="Dw3-M5-tWY" customClass="CurrentCoverArtView" customModule="Persephone" customModuleProvider="target">
@ -932,6 +900,7 @@
<image name="prevTrackButton" width="17" height="17"/>
<image name="repeatButton" width="17" height="17"/>
<image name="shuffleButton" width="17" height="17"/>
<image name="speakerHigh" width="21" height="17"/>
<image name="stopButton" width="17" height="17"/>
</resources>
</document>

View File

@ -27,6 +27,8 @@ class WindowController: NSWindowController {
@IBOutlet var shuffleState: NSButton!
@IBOutlet var repeatState: NSButton!
@IBOutlet var volumeState: NSButton!
@IBOutlet weak var searchQuery: NSSearchField!
override func windowDidLoad() {
@ -120,6 +122,25 @@ class WindowController: NSWindowController {
trackRemaining.stringValue = time.formattedTime
}
func setVolumeControlIcon(_ state: PlayerState) {
volumeState.isEnabled = state.volume != -1
switch state.volume {
case -1:
volumeState.image = .speakerDisabled
case 0..<5:
volumeState.image = .speakerOff
case 5..<40:
volumeState.image = .speakerLow
case 40..<70:
volumeState.image = .speakerMid
case 70...100:
volumeState.image = .speakerHigh
default:
break
}
}
@objc func willDisconnect() {
DispatchQueue.main.async {
@ -176,6 +197,16 @@ class WindowController: NSWindowController {
@IBAction func handleSearchQuery(_ sender: NSSearchField) {
App.store.dispatch(SetSearchQuery(searchQuery: sender.stringValue))
}
@IBAction func showVolumeControl(_ sender: NSButton) {
VolumeControlView.popover.contentViewController = VolumeControlView.shared
VolumeControlView.popover.behavior = .transient
VolumeControlView.popover.show(
relativeTo: sender.bounds,
of: sender,
preferredEdge: .maxY
)
}
}
extension WindowController: NSWindowDelegate {
@ -201,6 +232,7 @@ extension WindowController: StoreSubscriber {
self.setShuffleRepeatState(state.playerState)
self.setTrackProgressControls(state.playerState)
self.setDatabaseUpdatingIndicator(state.uiState)
self.setVolumeControlIcon(state.playerState)
}
}
}

View File

@ -74,8 +74,8 @@ extension MPDClient {
let mpdAlbum = MPDAlbum(
title: mpdSong.album.title,
artist: mpdSong.artist,
date: mpdSong.date,
path: mpdSong.path
firstSong: mpdSong,
date: mpdSong.date
)
if (mpdAlbum != albums.last) {
albums.append(mpdAlbum)

View File

@ -46,6 +46,11 @@ extension MPDClient {
guard let repeatState = userData["repeatState"] as? Bool
else { return }
sendRepeatState(repeatState: repeatState)
case .setVolume:
guard let volume = userData["volume"] as? Int
else { return }
sendSetVolume(to: volume)
// Database commands
case .updateDatabase:
@ -119,6 +124,22 @@ extension MPDClient {
else { return }
albumSongs(for: album, callback: callback)
// Song commands
case .fetchAlbumArt:
guard let songUri = userData["songUri"] as? String,
let offset = userData["offset"] as? Int32,
let callback = userData["callback"] as? (Data?) -> Void
else { return }
let imageData = userData["imageData"] as? Data? ?? nil
sendFetchAlbumArt(
forUri: songUri,
imageData: imageData,
offset: offset,
callback: callback
)
}
}
@ -126,6 +147,7 @@ extension MPDClient {
func enqueueCommand(
command: MPDCommand,
priority: BlockOperation.QueuePriority = .normal,
forceIdle: Bool = false,
userData: Dictionary<String, Any> = [:]
) {
guard isConnected else { return }
@ -135,8 +157,9 @@ extension MPDClient {
let commandOperation = BlockOperation() { [unowned self] in
self.sendCommand(command: command, userData: userData)
self.idle()
self.idle(forceIdle)
}
commandOperation.queuePriority = priority
commandQueue.addOperation(commandOperation)
}

View File

@ -11,58 +11,81 @@ import mpdclient
extension MPDClient {
func noIdle() {
if isIdle {
mpd_send_noidle(connection)
isIdle = false
do {
idleLock.lock()
defer { idleLock.unlock() }
if isIdle {
mpd_send_noidle(connection)
isIdle = false
}
}
}
func idle() {
if !self.isIdle && self.commandQueue.operationCount == 1 {
mpd_send_idle(self.connection)
self.isIdle = true
func idle(_ force: Bool = false) {
let shouldIdle: Bool
do {
idleLock.lock()
defer { idleLock.unlock() }
shouldIdle = (!isIdle && commandQueue.operationCount == 1) || force
if shouldIdle {
mpd_send_idle(connection)
self.isIdle = true
}
}
let result = mpd_recv_idle(self.connection, true)
self.handleIdleResult(result)
if shouldIdle {
let result = mpd_recv_idle(connection, true)
handleIdleResult(result)
}
}
func handleIdleResult(_ result: mpd_idle) {
isIdle = false
let mpdIdle = MPDIdle(rawValue: result.rawValue)
let wasIdle: Bool
if mpdIdle.contains(.database) {
self.fetchAllAlbums()
do {
idleLock.lock()
defer { idleLock.unlock() }
wasIdle = isIdle
isIdle = false
}
if mpdIdle.contains(.queue) {
self.fetchQueue()
self.fetchStatus()
self.delegate?.didUpdateQueue(mpdClient: self, queue: self.queue)
if let status = self.status {
self.delegate?.didUpdateQueuePos(mpdClient: self, song: status.song)
if wasIdle {
if mpdIdle.contains(.database) {
self.fetchAllAlbums()
}
}
if mpdIdle.contains(.player) || mpdIdle.contains(.options) {
self.fetchStatus()
if mpdIdle.contains(.queue) {
self.fetchQueue()
self.fetchStatus()
if let status = self.status {
self.delegate?.didUpdateStatus(mpdClient: self, status: status)
self.delegate?.didUpdateQueuePos(mpdClient: self, song: status.song)
self.delegate?.didUpdateQueue(mpdClient: self, queue: self.queue)
if let status = self.status {
self.delegate?.didUpdateQueuePos(mpdClient: self, song: status.song)
}
}
}
if mpdIdle.contains(.update) {
self.fetchStatus()
if mpdIdle.contains(.player) ||
mpdIdle.contains(.options) ||
mpdIdle.contains(.mixer) {
self.fetchStatus()
if self.status?.updating ?? false {
self.delegate?.willStartDatabaseUpdate(mpdClient: self)
} else {
self.delegate?.didFinishDatabaseUpdate(mpdClient: self)
if let status = self.status {
self.delegate?.didUpdateStatus(mpdClient: self, status: status)
self.delegate?.didUpdateQueuePos(mpdClient: self, song: status.song)
}
}
if mpdIdle.contains(.update) {
self.fetchStatus()
if self.status?.updating ?? false {
self.delegate?.willStartDatabaseUpdate(mpdClient: self)
} else {
self.delegate?.didFinishDatabaseUpdate(mpdClient: self)
}
}
if !mpdIdle.isEmpty {
self.idle()
}
}
if !mpdIdle.isEmpty {
self.idle()
}
}
}

View File

@ -0,0 +1,24 @@
//
// MPDClient+Mixer.swift
// Persephone
//
// Created by Daniel Barber on 2/16/20.
// Copyright © 2020 Dan Barber. All rights reserved.
//
import Foundation
import mpdclient
extension MPDClient {
func setVolume(to volume: Int) {
enqueueCommand(
command: .setVolume,
priority: .high,
userData: ["volume": volume]
)
}
func sendSetVolume(to volume: Int) {
mpd_run_set_volume(connection, UInt32(volume))
}
}

View File

@ -19,27 +19,51 @@ extension MPDClient {
}
func playTrack(at queuePos: Int) {
enqueueCommand(command: .playTrack, userData: ["queuePos": queuePos])
enqueueCommand(
command: .playTrack,
forceIdle: true,
userData: ["queuePos": queuePos]
)
}
func appendSong(_ song: MPDSong) {
enqueueCommand(command: .appendSong, userData: ["song": song])
enqueueCommand(
command: .appendSong,
forceIdle: true,
userData: ["song": song]
)
}
func removeSong(at queuePos: Int) {
enqueueCommand(command: .removeSong, userData: ["queuePos": queuePos])
enqueueCommand(
command: .removeSong,
forceIdle: true,
userData: ["queuePos": queuePos]
)
}
func moveSongInQueue(at queuePos: Int, to newQueuePos: Int) {
enqueueCommand(command: .moveSongInQueue, userData: ["oldQueuePos": queuePos, "newQueuePos": newQueuePos])
enqueueCommand(
command: .moveSongInQueue,
forceIdle: true,
userData: ["oldQueuePos": queuePos, "newQueuePos": newQueuePos]
)
}
func addSongToQueue(songUri: String, at queuePos: Int) {
enqueueCommand(command: .addSongToQueue, userData: ["uri": songUri, "queuePos": queuePos])
enqueueCommand(
command: .addSongToQueue,
forceIdle: true,
userData: ["uri": songUri, "queuePos": queuePos]
)
}
func addAlbumToQueue(album: MPDAlbum, at queuePos: Int) {
enqueueCommand(command: .addAlbumToQueue, userData: ["album": album, "queuePos": queuePos])
enqueueCommand(
command: .addAlbumToQueue,
forceIdle: true,
userData: ["album": album, "queuePos": queuePos]
)
}
func sendPlayTrack(at queuePos: Int) {

View File

@ -10,19 +10,80 @@ import Foundation
import mpdclient
extension MPDClient {
func fetchAlbumArt(
songUri: String,
imageData: Data?,
offset: Int32 = 0,
callback: @escaping (Data?) -> Void
) {
enqueueCommand(
command: .fetchAlbumArt,
priority: .low,
userData: [
"songUri": songUri,
"callback": callback,
"imageData": imageData as Any,
"offset": offset,
]
)
}
func searchSongs(_ terms: [MPDClient.MPDTag: String]) -> [MPDSong] {
var songs: [MPDSong] = []
mpd_search_db_songs(self.connection, true)
mpd_search_db_songs(connection, true)
for (tag, term) in terms {
mpd_search_add_tag_constraint(self.connection, MPD_OPERATOR_DEFAULT, tag.mpdTag(), term)
mpd_search_add_tag_constraint(connection, MPD_OPERATOR_DEFAULT, tag.mpdTag(), term)
}
mpd_search_commit(self.connection)
mpd_search_commit(connection)
while let song = mpd_recv_song(self.connection) {
while let song = mpd_recv_song(connection) {
songs.append(MPDSong(song))
}
return songs
}
func sendFetchAlbumArt(
forUri songUri: String,
imageData: Data?,
offset: Int32,
callback: @escaping (Data?) -> Void
) -> Void {
var size: Int?
mpd_send_albumart(connection, songUri, String(offset))
guard let sizePair = mpd_recv_pair(connection) else {
mpd_connection_clear_error(connection)
return
}
size = Int(MPDPair(sizePair).value)
mpd_return_pair(connection, sizePair)
var data = imageData ?? Data(count: size!)
let binaryPair = MPDPair(mpd_recv_pair(connection))
let chunkSize = Int(binaryPair.value)!
mpd_return_pair(connection, binaryPair.pair)
_ = data[offset...].withUnsafeMutableBytes { (pointer) in
mpd_recv_binary(connection, pointer.baseAddress, chunkSize)
}
guard mpd_response_finish(connection) else { return }
let newOffset = offset + Int32(chunkSize)
if newOffset < size! {
fetchAlbumArt(
songUri: songUri,
imageData: data,
offset: newOffset,
callback: callback
)
} else {
callback(data)
}
}
}

View File

@ -11,24 +11,42 @@ import mpdclient
extension MPDClient {
func playPause() {
enqueueCommand(command: .playPause)
enqueueCommand(
command: .playPause,
priority: .high,
forceIdle: true
)
}
func stop() {
enqueueCommand(command: .stop)
enqueueCommand(
command: .stop,
priority: .high,
forceIdle: true
)
}
func prevTrack() {
enqueueCommand(command: .prevTrack)
enqueueCommand(
command: .prevTrack,
priority: .high,
forceIdle: true
)
}
func nextTrack() {
enqueueCommand(command: .nextTrack)
enqueueCommand(
command: .nextTrack,
priority: .high,
forceIdle: true
)
}
func seekCurrentSong(timeInSeconds: Float) {
enqueueCommand(
command: .seekCurrentSong,
priority: .high,
forceIdle: true,
userData: ["timeInSeconds": timeInSeconds]
)
}
@ -36,6 +54,8 @@ extension MPDClient {
func setShuffleState(shuffleState: Bool) {
enqueueCommand(
command: .setShuffleState,
priority: .high,
forceIdle: true,
userData: ["shuffleState": shuffleState]
)
}
@ -43,6 +63,8 @@ extension MPDClient {
func setRepeatState(repeatState: Bool) {
enqueueCommand(
command: .setRepeatState,
priority: .high,
forceIdle: true,
userData: ["repeatState": repeatState]
)
}

View File

@ -0,0 +1,15 @@
//
// MPDClientWrapper.c
// Persephone
//
// Created by Daniel Barber on 1/31/20.
// Copyright © 2020 Dan Barber. All rights reserved.
//
#include "MPDClientWrapper.h"
int
mpd_send_albumart(struct mpd_connection *connection, const char * uri, const char * offset)
{
return mpd_send_command(connection, "albumart", uri, offset, NULL);
}

View File

@ -0,0 +1,12 @@
//
// MPDClientWrapper.h
// Persephone
//
// Created by Daniel Barber on 1/31/20.
// Copyright © 2020 Dan Barber. All rights reserved.
//
#include <mpd/client.h>
int
mpd_send_albumart(struct mpd_connection *connection, const char * uri, const char * offset);

View File

@ -0,0 +1,5 @@
//
// Use this file to import your target's public headers that you would like to expose to Swift.
//
#import "MPDClientWrapper.h"

View File

@ -21,6 +21,8 @@ class MPDClient {
var queue: [MPDSong] = []
let commandQueue = OperationQueue()
let idleLock = NSLock()
init(withDelegate delegate: MPDClientDelegate?) {
commandQueue.maxConcurrentOperationCount = 1

View File

@ -12,8 +12,8 @@ extension MPDClient {
struct MPDAlbum: Equatable {
let title: String
let artist: String
var firstSong: MPDSong?
var date: String?
var path: String?
static func == (lhs: MPDAlbum, rhs: MPDAlbum) -> Bool {
return lhs.title == rhs.title &&

View File

@ -22,6 +22,8 @@ extension MPDClient {
case setShuffleState
case setRepeatState
case setVolume
// Database commands
case updateDatabase
@ -48,5 +50,8 @@ extension MPDClient {
case playAlbum
case getAlbumFirstSong
case getAlbumSongs
// Song commands
case fetchAlbumArt
}
}

View File

@ -37,8 +37,7 @@ extension MPDClient {
return MPDAlbum(
title: getTag(.album),
artist: artist,
date: date,
path: path
date: date
)
}
@ -54,11 +53,6 @@ extension MPDClient {
return getTag(.date)
}
var path: String {
return NSString(string: uriString)
.deletingLastPathComponent
}
func getTag(_ tagType: MPDTag) -> String {
guard let tag = mpd_song_get_tag(song, tagType.mpdTag(), 0)
else { return "" }

View File

@ -57,6 +57,10 @@ extension MPDClient {
var repeatState: Bool {
return mpd_status_get_repeat(status)
}
var volume: Int {
return Int(mpd_status_get_volume(status))
}
var updating: Bool {
let updating = mpd_status_get_update_id(status)

View File

@ -32,29 +32,6 @@ struct Album {
var hash: String {
return "\(title) - \(artist)".sha1()
}
var coverArtFilenames: [String] {
return [
"cover.jpg",
"folder.jpg",
"\(artist) - \(title).jpg",
"cover.png",
"folder.png",
"\(artist) - \(title ).png",
]
}
var coverArtFilePath: String? {
let musicDir = App.store.state.preferencesState.expandedMpdLibraryDir
guard let albumPath = mpdAlbum.path else { return nil }
return coverArtFilenames
.lazy
.map { "\(musicDir)/\(albumPath)/\($0)" }
.first {
FileManager.default.fileExists(atPath: $0)
}
}
}
extension Album: Equatable {

View File

@ -8,7 +8,8 @@
struct DraggedSong: Codable {
var type: DraggedSongType
var title: String?
var artist: String?
var cover: String?
var title: String
var artist: String
var album: String
var uri: String
}

View File

@ -1,23 +0,0 @@
//
// CoverArtQueue.swift
// Persephone
//
// Created by Daniel Barber on 2019/2/26.
// Copyright © 2019 Dan Barber. All rights reserved.
//
import AppKit
class CoverArtQueue {
static let shared = CoverArtQueue()
let queue = DispatchQueue(label: "CoverArtQueue")
var lastDispatchedTime = DispatchTime(uptimeNanoseconds: 0) - 1
func addToQueue(workItem: DispatchWorkItem) {
let dispatchTime = max(lastDispatchedTime + 1, DispatchTime(uptimeNanoseconds: 0))
lastDispatchedTime = dispatchTime
queue.asyncAfter(deadline: dispatchTime, execute: workItem)
}
}

View File

@ -0,0 +1,83 @@
//
// CoverArtService.swift
// Persephone
//
// Created by Daniel Barber on 2/3/20.
// Copyright © 2020 Dan Barber. All rights reserved.
//
import Foundation
import PromiseKit
import Kingfisher
struct CoverArtService {
let song: Song
let provider: MPDAlbumArtImageDataProvider
init(song: Song) {
self.song = song
provider = MPDAlbumArtImageDataProvider(
songUri: song.mpdSong.uriString,
cacheKey: song.album.hash
)
}
func refreshAlbumListArt() -> Promise<Any> {
return Promise<Any> { seal in
_ = KingfisherManager.shared.retrieveImage(
with: .provider(provider),
options: [
.forceRefresh,
.memoryCacheExpiration(.never),
.processor(DownsamplingImageProcessor(size: .albumListCoverSize)),
.scaleFactor(2),
]
) { result in
seal.fulfill(result)
}
}
}
func refreshCurrentlyPlayingArt() -> Promise<Any> {
return Promise<Any> { seal in
_ = KingfisherManager.shared.retrieveImage(
with: .provider(provider),
options: [
.forceRefresh,
.memoryCacheExpiration(.never),
.processor(DownsamplingImageProcessor(size: .currentlyPlayingCoverSize)),
.scaleFactor(2),
.callbackQueue(.mainAsync)
]
) { result in
seal.fulfill(result)
}
}
}
func refreshQueueSongArt() -> Promise<Any> {
return Promise<Any> { seal in
_ = KingfisherManager.shared.retrieveImage(
with: .provider(provider),
options: [
.forceRefresh,
.memoryCacheExpiration(.never),
.processor(DownsamplingImageProcessor(size: .queueSongCoverSize)),
.scaleFactor(2),
]
) { result in
seal.fulfill(result)
}
}
}
func refresh(callback: @escaping () -> Void?) {
_ = firstly {
when(fulfilled: refreshAlbumListArt(), refreshQueueSongArt(), refreshCurrentlyPlayingArt())
}.done { _, _, _ in
NotificationCenter.default.post(name: .didReloadAlbumArt, object: nil)
callback()
}
}
}

View File

@ -20,3 +20,7 @@ struct UpdateElapsedTimeAction: Action {
struct UpdateStatusAction: Action {
var status: MPDClient.MPDStatus
}
struct UpdateVolumeAction: Action {
var volume: Int
}

View File

@ -16,10 +16,6 @@ struct UpdateServerPort: Action {
var port: Int?
}
struct UpdateMPDLibraryDir: Action {
var mpdLibraryDir: String?
}
struct UpdateFetchMissingArtworkFromInternet: Action {
var fetchMissingArtworkFromInternet: Bool
}

View File

@ -16,6 +16,8 @@ struct PlayerState: StateType {
var state: MPDClient.MPDStatus.State?
var shuffleState: Bool = false
var repeatState: Bool = false
var volume: Int = 0
var totalTime: UInt?
var elapsedTimeMs: UInt?
@ -23,10 +25,11 @@ struct PlayerState: StateType {
extension PlayerState: Equatable {
static func == (lhs: PlayerState, rhs: PlayerState) -> Bool {
return (lhs.state == rhs.state) &&
(lhs.totalTime == rhs.totalTime) &&
(lhs.elapsedTimeMs == rhs.elapsedTimeMs) &&
(lhs.shuffleState == rhs.shuffleState) &&
(lhs.repeatState == rhs.repeatState)
return lhs.state == rhs.state &&
lhs.totalTime == rhs.totalTime &&
lhs.elapsedTimeMs == rhs.elapsedTimeMs &&
lhs.shuffleState == rhs.shuffleState &&
lhs.repeatState == rhs.repeatState &&
lhs.volume == rhs.volume
}
}

View File

@ -13,17 +13,6 @@ struct PreferencesState: StateType, Equatable {
let preferences = UserDefaults.standard
var mpdServer: MPDServer
let mpdLibraryDirDefault = "~/Music"
var mpdLibraryDir: String?
var mpdLibraryDirOrDefault: String {
return mpdLibraryDir ?? mpdLibraryDirDefault
}
var expandedMpdLibraryDir: String {
return NSString(string: mpdLibraryDirOrDefault).expandingTildeInPath
}
var fetchMissingArtworkFromInternet: Bool
@ -32,7 +21,6 @@ struct PreferencesState: StateType, Equatable {
host: preferences.string(forKey: "mpdHost"),
port: preferences.value(forKey: "mpdPort") as? Int
)
self.mpdLibraryDir = preferences.string(forKey: "mpdLibraryDir")
self.fetchMissingArtworkFromInternet = preferences.bool(
forKey: "fetchMissingArtworkFromInternet"
)
@ -45,7 +33,6 @@ struct PreferencesState: StateType, Equatable {
} else {
preferences.removeObject(forKey: "mpdPort")
}
preferences.set(mpdLibraryDir, forKey: "mpdLibraryDir")
preferences.set(fetchMissingArtworkFromInternet, forKey: "fetchMissingArtworkFromInternet")
}
}

View File

@ -20,6 +20,7 @@ func playerReducer(action: Action, state: PlayerState?) -> PlayerState {
state.elapsedTimeMs = action.status.elapsedTimeMs
state.shuffleState = action.status.shuffleState
state.repeatState = action.status.repeatState
state.volume = action.status.volume
if state.state == .playing {
App.trackTimer.start(elapsedTimeMs: state.elapsedTimeMs)

View File

@ -20,9 +20,6 @@ func preferencesReducer(action: Action, state: PreferencesState?) -> Preferences
case let action as UpdateServerPort:
state.mpdServer.port = action.port
case let action as UpdateMPDLibraryDir:
state.mpdLibraryDir = action.mpdLibraryDir
case is SavePreferences:
state.save()

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 566 B

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 428 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 800 B

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 531 B

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 357 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 648 B

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 B

Binary file not shown.