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

Compare commits

...

9 Commits

Author SHA1 Message Date
34f941017f
We don't need this container view 2020-03-07 15:13:11 -05:00
225d511543
Update to 0.17.0-alpha 2020-03-07 13:11:41 -05:00
47c273c7ad
Add missing edit menu 2020-03-07 13:10:41 -05:00
98e741d9fe
✂️ 2020-03-06 12:01:57 -05:00
c0da221074
Update to 0.17.0-prealpha 2020-03-06 11:36:16 -05:00
6ccaef91a1
Add more informative messages 2020-03-06 11:36:04 -05:00
c4e5f7408a
Handle connection/disconnection better
* Add menu options to connect/disconnect
* Add option to attempt reconnection on fatal error
* Add ability to reset connection to MPDClient
2020-03-03 11:11:10 -05:00
b46cbf229f
Refactor error and retain server connected state 2020-03-03 11:11:10 -05:00
063b8da202
Handle errors connecting to MPD 2020-03-03 11:11:09 -05:00
20 changed files with 327 additions and 82 deletions

View File

@ -101,6 +101,7 @@
E4B11BC02275EE150075461B /* QueueActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B11BBF2275EE150075461B /* QueueActions.swift */; };
E4B11BC22275EE410075461B /* AlbumListActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B11BC12275EE410075461B /* AlbumListActions.swift */; };
E4B3DF6523D66A4400728F6B /* QueueSongCoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B3DF6423D66A4400728F6B /* QueueSongCoverView.swift */; };
E4B46F8F2402E89800152157 /* MPDError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B46F8E2402E89800152157 /* MPDError.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 */; };
@ -120,6 +121,9 @@
E4F26F7723411AE300D45FF9 /* ArtistListActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4F26F7623411AE300D45FF9 /* ArtistListActions.swift */; };
E4F26F7923411B1500D45FF9 /* ArtistReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4F26F7823411B1500D45FF9 /* ArtistReducer.swift */; };
E4F26F7B23411D5400D45FF9 /* MPDClient+Artist.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4F26F7A23411D5400D45FF9 /* MPDClient+Artist.swift */; };
E4F2EFEE24076A2700198159 /* ServerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4F2EFED24076A2700198159 /* ServerState.swift */; };
E4F2EFF024076B0900198159 /* ServerActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4F2EFEF24076B0900198159 /* ServerActions.swift */; };
E4F2EFF224076B5E00198159 /* ServerReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4F2EFF124076B5E00198159 /* ServerReducer.swift */; };
E4F6B460221E119B00ACF42A /* QueueDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4F6B45F221E119B00ACF42A /* QueueDataSource.swift */; };
E4F6B463221E125900ACF42A /* QueueItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4F6B462221E125900ACF42A /* QueueItem.swift */; };
E4F6B467221E233200ACF42A /* AlbumDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4F6B466221E233200ACF42A /* AlbumDataSource.swift */; };
@ -300,6 +304,7 @@
E4B11BBF2275EE150075461B /* QueueActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueueActions.swift; sourceTree = "<group>"; };
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>"; };
E4B46F8E2402E89800152157 /* MPDError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPDError.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>"; };
@ -320,6 +325,9 @@
E4F26F7623411AE300D45FF9 /* ArtistListActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArtistListActions.swift; sourceTree = "<group>"; };
E4F26F7823411B1500D45FF9 /* ArtistReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArtistReducer.swift; sourceTree = "<group>"; };
E4F26F7A23411D5400D45FF9 /* MPDClient+Artist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MPDClient+Artist.swift"; sourceTree = "<group>"; };
E4F2EFED24076A2700198159 /* ServerState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerState.swift; sourceTree = "<group>"; };
E4F2EFEF24076B0900198159 /* ServerActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerActions.swift; sourceTree = "<group>"; };
E4F2EFF124076B5E00198159 /* ServerReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerReducer.swift; sourceTree = "<group>"; };
E4F6B45F221E119B00ACF42A /* QueueDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueueDataSource.swift; sourceTree = "<group>"; };
E4F6B462221E125900ACF42A /* QueueItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueueItem.swift; sourceTree = "<group>"; };
E4F6B466221E233200ACF42A /* AlbumDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumDataSource.swift; sourceTree = "<group>"; };
@ -660,6 +668,7 @@
E4B11B60226A4BFF0075461B /* PlayerReducer.swift */,
E4FF718F227601B400D4C412 /* PreferencesReducer.swift */,
E4B11B74226CC4D30075461B /* QueueReducer.swift */,
E4F2EFF124076B5E00198159 /* ServerReducer.swift */,
E440519D227BB0720090CD6F /* UIReducer.swift */,
);
path = Reducers;
@ -676,6 +685,7 @@
E4B11B65226A4F830075461B /* PlayerState.swift */,
E4FF718D2276010E00D4C412 /* PreferencesState.swift */,
E4B11B67226A4FA00075461B /* QueueState.swift */,
E4F2EFED24076A2700198159 /* ServerState.swift */,
E440519F227BB0AB0090CD6F /* UIState.swift */,
);
path = State;
@ -689,6 +699,7 @@
E4B11BBD2275EDAA0075461B /* PlayerActions.swift */,
E4FF71912276029000D4C412 /* PreferencesActions.swift */,
E4B11BBF2275EE150075461B /* QueueActions.swift */,
E4F2EFEF24076B0900198159 /* ServerActions.swift */,
E440519B227BAF2E0090CD6F /* UIActions.swift */,
);
path = Actions;
@ -707,6 +718,7 @@
children = (
E4EB237A220F7CF1008C70C0 /* MPDAlbum.swift */,
E45962C52241A78500FC1A1E /* MPDCommand.swift */,
E4B46F8E2402E89800152157 /* MPDError.swift */,
E4C8B53D22349002009A20F3 /* MPDIdle.swift */,
E4EB2378220F10B8008C70C0 /* MPDPair.swift */,
E4E8CC9922075D370024217A /* MPDSong.swift */,
@ -974,6 +986,8 @@
E450AD7E222620A10091BED3 /* Album.swift in Sources */,
E4DA820623D6236200C1EE58 /* NSSize.swift in Sources */,
E408D3B6220DD8970006D9BE /* Notification.swift in Sources */,
E4F2EFEE24076A2700198159 /* ServerState.swift in Sources */,
E4B46F8F2402E89800152157 /* MPDError.swift in Sources */,
E43AC1F822C7065A001E483C /* AlbumCoverButton.swift in Sources */,
E45962C62241A78500FC1A1E /* MPDCommand.swift in Sources */,
E408D3B9220DE98F0006D9BE /* NSUserInterfaceItemIdentifier.swift in Sources */,
@ -991,6 +1005,7 @@
E4FF71922276029000D4C412 /* PreferencesActions.swift in Sources */,
E489E39922B85D0400CA8CBD /* NSPasteboard.swift in Sources */,
E43AC1F122C68E6A001E483C /* NSPasteboardItem.swift in Sources */,
E4F2EFF224076B5E00198159 /* ServerReducer.swift in Sources */,
E465049A21E94DF500A70F4C /* WindowController.swift in Sources */,
E41B22C621FB932700D544F6 /* MPDClient.swift in Sources */,
E47E2FDD2220A6D100F747E6 /* Time.swift in Sources */,
@ -1021,6 +1036,7 @@
E41EA46C221636AF0068EF46 /* GeneralPrefsViewController.swift in Sources */,
E4E8CC902204EC7F0024217A /* Delegate.swift in Sources */,
E47E2FD5222071FD00F747E6 /* AlbumViewItem.swift in Sources */,
E4F2EFF024076B0900198159 /* ServerActions.swift in Sources */,
E408D3BE220E03EE0006D9BE /* RawRepresentable.swift in Sources */,
E4B11B66226A4F830075461B /* PlayerState.swift in Sources */,
E4B11BBE2275EDAA0075461B /* PlayerActions.swift in Sources */,
@ -1179,7 +1195,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "3rd Party Mac Developer Application";
CODE_SIGN_IDENTITY = "Mac Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
@ -1221,7 +1237,7 @@
"$(inherited)",
"$(PROJECT_DIR)/libmpdclient/output",
);
MARKETING_VERSION = "0.16.1-alpha";
MARKETING_VERSION = "0.17.0-alpha";
PRODUCT_BUNDLE_IDENTIFIER = me.danbarber.Persephone;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -1238,7 +1254,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "3rd Party Mac Developer Application";
CODE_SIGN_IDENTITY = "Mac Developer";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = 54DPC63K8R;
@ -1253,7 +1269,7 @@
"$(inherited)",
"$(PROJECT_DIR)/libmpdclient/output",
);
MARKETING_VERSION = "0.16.1-alpha";
MARKETING_VERSION = "0.17.0-alpha";
PRODUCT_BUNDLE_IDENTIFIER = me.danbarber.Persephone;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -1298,7 +1314,7 @@
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_IDENTITY = "3rd Party Mac Developer Application";
CODE_SIGN_IDENTITY = "Mac Developer";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = 54DPC63K8R;
@ -1348,7 +1364,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_IDENTITY = "3rd Party Mac Developer Application";
CODE_SIGN_IDENTITY = "Mac Developer";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = 54DPC63K8R;

View File

@ -16,6 +16,8 @@ class AppDelegate: NSObject,
MediaKeyTapDelegate {
var mediaKeyTap: MediaKeyTap?
@IBOutlet weak var connectMenuItem: NSMenuItem!
@IBOutlet weak var disconnectMenuItem: NSMenuItem!
@IBOutlet weak var mainWindowMenuItem: NSMenuItem!
@IBOutlet weak var updateDatabaseMenuItem: NSMenuItem!
@IBOutlet weak var playSelectedSongMenuItem: NSMenuItem!
@ -28,13 +30,13 @@ class AppDelegate: NSObject,
App.store.subscribe(self) {
$0.select {
$0.uiState
($0.serverState, $0.uiState)
}
}
_ = App.userNotificationsController
App.mpdServerController.connect()
_ = App.mpdServerController
}
func applicationWillTerminate(_ aNotification: Notification) {
@ -105,6 +107,11 @@ class AppDelegate: NSObject,
playSelectedSongNextMenuItem.isEnabled = selectedSong != nil
addSelectedSongToQueueMenuItem.isEnabled = selectedSong != nil
}
func setConnectMenuItemsState(connected: Bool) {
connectMenuItem.isEnabled = !connected
disconnectMenuItem.isEnabled = connected
}
func handle(mediaKey: MediaKey, event: KeyEvent) {
switch mediaKey {
@ -117,6 +124,13 @@ class AppDelegate: NSObject,
}
}
@IBAction func connectMenuAction(_ sender: NSMenuItem) {
App.mpdServerController.connect()
}
@IBAction func disconnectMenuAction(_ sender: NSMenuItem) {
App.mpdServerController.disconnect()
}
@IBAction func updateDatabase(_ sender: NSMenuItem) {
App.mpdClient.updateDatabase()
}
@ -182,11 +196,14 @@ class AppDelegate: NSObject,
}
extension AppDelegate: StoreSubscriber {
typealias StoreSubscriberStateType = UIState
typealias StoreSubscriberStateType = (
serverState: ServerState, uiState: UIState
)
func newState(state: UIState) {
updateDatabaseMenuItem.isEnabled = !state.databaseUpdating
setMainWindowStateMenuItem(state: state.mainWindowState)
setSongMenuItemsState(selectedSong: state.selectedSong)
func newState(state: StoreSubscriberStateType) {
updateDatabaseMenuItem.isEnabled = !state.uiState.databaseUpdating
setMainWindowStateMenuItem(state: state.uiState.mainWindowState)
setSongMenuItemsState(selectedSong: state.uiState.selectedSong)
setConnectMenuItemsState(connected: state.serverState.connected)
}
}

View File

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

View File

@ -6,15 +6,28 @@
// Copyright © 2019 Dan Barber. All rights reserved.
//
import Foundation
import AppKit
class MPDServerDelegate: MPDClientDelegate {
func didConnect(mpdClient: MPDClient) {
NotificationCenter.default.post(name: .didConnect, object: nil)
DispatchQueue.main.async {
App.store.dispatch(UpdateConnectedState(connected: true))
}
}
func willDisconnect(mpdClient: MPDClient) {
NotificationCenter.default.post(name: .willDisconnect, object: nil)
DispatchQueue.main.async {
App.store.dispatch(UpdateConnectedState(connected: false))
}
}
func didRaiseError(
mpdClient: MPDClient,
error: MPDClient.MPDError
) {
NotificationCenter.default.post(name: .didRaiseError, object: error)
}
func didUpdateStatus(mpdClient: MPDClient, status: MPDClient.MPDStatus) {

View File

@ -14,7 +14,7 @@
<items>
<menuItem title="Persephone" id="1Xt-HY-uBw">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Persephone" systemMenu="apple" id="uQy-DD-JDr">
<menu key="submenu" title="Persephone" systemMenu="apple" autoenablesItems="NO" id="uQy-DD-JDr">
<items>
<menuItem title="About Persephone" id="5kV-Vb-QxS">
<modifierMask key="keyEquivalentModifierMask"/>
@ -29,7 +29,19 @@
<segue destination="xYu-7w-E5x" kind="show" identifier="Preferences" id="OTW-56-v9E"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="wFC-TO-SCJ"/>
<menuItem title="Connect" keyEquivalent="c" id="VlA-Re-OZs">
<modifierMask key="keyEquivalentModifierMask" control="YES" option="YES" command="YES"/>
<connections>
<action selector="connectMenuAction:" target="Voe-Tx-rLC" id="Psl-MI-Dbx"/>
</connections>
</menuItem>
<menuItem title="Disconnect" enabled="NO" keyEquivalent="d" id="yjK-qU-C6d">
<modifierMask key="keyEquivalentModifierMask" control="YES" option="YES" command="YES"/>
<connections>
<action selector="disconnectMenuAction:" target="Voe-Tx-rLC" id="smu-DN-Ubp"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="vcx-U6-vzu"/>
<menuItem title="Services" id="NMo-om-nkz">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Services" systemMenu="services" id="hz9-B4-Xy5"/>
@ -61,6 +73,50 @@
</items>
</menu>
</menuItem>
<menuItem title="Edit" id="vqD-eY-QGd">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Edit" id="TuB-7j-hDK">
<items>
<menuItem title="Undo" keyEquivalent="z" id="Qfn-k5-qMJ">
<connections>
<action selector="undo:" target="Ady-hI-5gd" id="eEs-gZ-3LG"/>
</connections>
</menuItem>
<menuItem title="Redo" keyEquivalent="Z" id="jTO-Fc-mMc">
<connections>
<action selector="redo:" target="Ady-hI-5gd" id="VB9-9R-Kbp"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="iGg-SA-H7J"/>
<menuItem title="Cut" keyEquivalent="x" id="wMs-2i-IFp">
<connections>
<action selector="cut:" target="Ady-hI-5gd" id="kNV-s2-6rJ"/>
</connections>
</menuItem>
<menuItem title="Copy" keyEquivalent="c" id="6Xf-49-5o2">
<connections>
<action selector="copy:" target="Ady-hI-5gd" id="QzD-pM-sbg"/>
</connections>
</menuItem>
<menuItem title="Paste" keyEquivalent="v" id="FPx-H8-gor">
<connections>
<action selector="paste:" target="Ady-hI-5gd" id="zK6-ON-cCD"/>
</connections>
</menuItem>
<menuItem title="Delete" id="42c-Ut-F4e">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="delete:" target="Ady-hI-5gd" id="XbT-8a-cop"/>
</connections>
</menuItem>
<menuItem title="Select All" keyEquivalent="a" id="oF5-zh-KZt">
<connections>
<action selector="selectAll:" target="Ady-hI-5gd" id="3gW-FK-DJV"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Database" id="usv-UH-vkC">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Database" autoenablesItems="NO" id="rFP-zL-1X4">
@ -178,6 +234,8 @@
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="Persephone" customModuleProvider="target">
<connections>
<outlet property="addSelectedSongToQueueMenuItem" destination="JFH-jT-sBp" id="9dy-sJ-XYS"/>
<outlet property="connectMenuItem" destination="VlA-Re-OZs" id="Zzt-yq-mtp"/>
<outlet property="disconnectMenuItem" destination="yjK-qU-C6d" id="qel-dM-Gbl"/>
<outlet property="mainWindowMenuItem" destination="1Sq-L7-znT" id="dC6-yY-6Ss"/>
<outlet property="playSelectedSongMenuItem" destination="dyT-9E-DRY" id="UY2-SN-YMF"/>
<outlet property="playSelectedSongNextMenuItem" destination="Q8j-jr-IOp" id="Jqh-ia-sMK"/>
@ -375,7 +433,7 @@
<splitViewController id="fnD-7K-pHK" customClass="MainSplitViewController" customModule="Persephone" customModuleProvider="target" sceneMemberID="viewController">
<splitViewItems>
<splitViewItem holdingPriority="255" behavior="contentList" id="CWo-v7-gd2"/>
<splitViewItem id="Mdr-U0-Vci"/>
<splitViewItem id="bAy-ec-iQI"/>
</splitViewItems>
<splitView key="splitView" dividerStyle="thin" vertical="YES" id="g34-ef-XN0">
<rect key="frame" x="0.0" y="0.0" width="450" height="300"/>
@ -387,7 +445,7 @@
<connections>
<outlet property="splitView" destination="g34-ef-XN0" id="YEc-LL-DoS"/>
<segue destination="KIP-rq-4dM" kind="relationship" relationship="splitItems" id="Vmb-hY-d12"/>
<segue destination="SjO-VS-1bb" kind="relationship" relationship="splitItems" id="peC-gL-WJ1"/>
<segue destination="gPn-fP-LFc" kind="relationship" relationship="splitItems" id="eC9-b4-YWt"/>
</connections>
</splitViewController>
<customObject id="Dag-kO-ps3" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
@ -501,53 +559,6 @@
</objects>
<point key="canvasLocation" x="1411" y="-420"/>
</scene>
<!--View Controller-->
<scene sceneID="VvW-vT-alQ">
<objects>
<customObject id="MSG-y7-cKU" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
<viewController id="SjO-VS-1bb" sceneMemberID="viewController">
<view key="view" id="BRY-0R-F3u">
<rect key="frame" x="0.0" y="0.0" width="478" height="558"/>
<autoresizingMask key="autoresizingMask"/>
<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"/>
<tabViewItems>
<tabViewItem label="Albums" identifier="" id="XgS-cX-SDH">
<view key="view" id="hB7-hN-SbB">
<rect key="frame" x="0.0" y="0.0" width="478" height="558"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<containerView translatesAutoresizingMaskIntoConstraints="NO" id="moE-bb-Zvg">
<rect key="frame" x="-3" y="-3" width="481" height="561"/>
<connections>
<segue destination="gPn-fP-LFc" kind="embed" id="2iB-9y-I9h"/>
</connections>
</containerView>
</subviews>
<constraints>
<constraint firstItem="moE-bb-Zvg" firstAttribute="top" secondItem="hB7-hN-SbB" secondAttribute="top" id="DUI-jy-8D7"/>
<constraint firstItem="moE-bb-Zvg" firstAttribute="leading" secondItem="hB7-hN-SbB" secondAttribute="leading" constant="-3" id="dCS-Kx-2UP"/>
<constraint firstAttribute="trailing" secondItem="moE-bb-Zvg" secondAttribute="trailing" id="qey-4e-xfu"/>
<constraint firstAttribute="bottom" secondItem="moE-bb-Zvg" secondAttribute="bottom" constant="-3" id="ziA-Xh-dlz"/>
</constraints>
</view>
</tabViewItem>
</tabViewItems>
</tabView>
</subviews>
<constraints>
<constraint firstAttribute="trailing" secondItem="ARv-cj-xlz" secondAttribute="trailing" id="nXk-bi-ua4"/>
<constraint firstAttribute="bottom" secondItem="ARv-cj-xlz" secondAttribute="bottom" id="nzf-Jw-Bpk"/>
<constraint firstItem="ARv-cj-xlz" firstAttribute="top" secondItem="BRY-0R-F3u" secondAttribute="top" id="v6W-0L-kQ1"/>
<constraint firstItem="ARv-cj-xlz" firstAttribute="leading" secondItem="BRY-0R-F3u" secondAttribute="leading" id="w2Z-xv-Fwz"/>
</constraints>
</view>
</viewController>
</objects>
<point key="canvasLocation" x="1436" y="238"/>
</scene>
<!--General-->
<scene sceneID="xTC-Y5-Agk">
<objects>
@ -912,7 +923,7 @@
</viewController>
<customObject id="uex-Ws-5X4" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="2038" y="87"/>
<point key="canvasLocation" x="1485" y="182"/>
</scene>
</scenes>
<resources>

View File

@ -38,13 +38,14 @@ class WindowController: NSWindowController {
App.store.subscribe(self) {
$0.select {
($0.playerState, $0.uiState)
($0.serverState, $0.playerState, $0.uiState)
}
}
App.store.dispatch(MainWindowDidOpenAction())
NotificationCenter.default.addObserver(self, selector: #selector(willDisconnect), name: .willDisconnect, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(reportError), name: .didRaiseError, object: nil)
trackProgress.font = .timerFont
trackRemaining.font = .timerFont
@ -76,9 +77,18 @@ class WindowController: NSWindowController {
}
}
func setShuffleRepeatState(_ state: PlayerState) {
shuffleState.state = state.shuffleState ? .on : .off
repeatState.state = state.repeatState ? .on : .off
func setShuffleRepeatState(
_ serverState: ServerState,
_ playerState: PlayerState
) {
shuffleState.isEnabled = serverState.connected
repeatState.isEnabled = serverState.connected
shuffleState.state = playerState.shuffleState ? .on : .off
repeatState.state = playerState.repeatState ? .on : .off
}
func setSearchState(_ serverState: ServerState) {
searchQuery.isEnabled = serverState.connected
}
func setTrackProgressControls(_ playerState: PlayerState) {
@ -144,10 +154,56 @@ class WindowController: NSWindowController {
@objc func willDisconnect() {
DispatchQueue.main.async {
App.store.dispatch(SetSearchQuery(searchQuery: ""))
App.store.dispatch(ResetStatusAction())
self.searchQuery.stringValue = ""
}
}
@objc func reportError(_ notification: NSNotification) {
guard let error = notification.object as? MPDClient.MPDError
else { return }
DispatchQueue.main.async {
guard let window = NSApplication.shared.mainWindow ?? self.window
else { return }
let alert = NSAlert(error: error)
alert.messageText = error.message
alert.alertStyle = error.recovered ? .warning : .critical
print(error.mpdError)
switch error.mpdError {
case MPD_ERROR_MALFORMED,
MPD_ERROR_ARGUMENT:
alert.informativeText = "Please check the mpd log for more details."
case MPD_ERROR_SYSTEM,
MPD_ERROR_TIMEOUT:
alert.informativeText = "Is the mpd server running?"
case MPD_ERROR_RESOLVER:
alert.informativeText = "Check your network connection."
default:
break;
}
if !error.recovered {
alert.addButton(withTitle: "Reconnect")
alert.addButton(withTitle: "Dismiss")
}
alert.beginSheetModal(for: window) { response in
switch response {
case .alertFirstButtonReturn:
if !error.recovered {
App.mpdServerController.connect()
}
default:
break
}
}
}
}
// TODO: Refactor this using a gesture recognizer
@IBAction func changeTrackProgress(_ sender: NSSlider) {
@ -224,12 +280,15 @@ extension WindowController: NSWindowDelegate {
}
extension WindowController: StoreSubscriber {
typealias StoreSubscriberStateType = (playerState: PlayerState, uiState: UIState)
typealias StoreSubscriberStateType = (
serverState: ServerState, playerState: PlayerState, uiState: UIState
)
func newState(state: StoreSubscriberStateType) {
DispatchQueue.main.async {
self.setTransportControlState(state.playerState)
self.setShuffleRepeatState(state.playerState)
self.setShuffleRepeatState(state.serverState, state.playerState)
self.setSearchState(state.serverState)
self.setTrackProgressControls(state.playerState)
self.setDatabaseUpdatingIndicator(state.uiState)
self.setVolumeControlIcon(state.playerState)

View File

@ -13,6 +13,8 @@ extension MPDClient {
command: MPDCommand,
userData: Dictionary<String, Any> = [:]
) {
guard command == .connect || isConnected else { return }
switch command {
case .connect:
@ -157,7 +159,9 @@ extension MPDClient {
let commandOperation = BlockOperation() { [unowned self] in
self.sendCommand(command: command, userData: userData)
self.idle(forceIdle)
if self.checkError() {
self.idle(forceIdle)
}
}
commandOperation.queuePriority = priority

View File

@ -11,16 +11,19 @@ import mpdclient
extension MPDClient {
func createConnection(host: String, port: Int) {
guard let connection = mpd_connection_new(host, UInt32(port), 10000),
mpd_connection_get_error(connection) == MPD_ERROR_SUCCESS
else { return }
connection = mpd_connection_new(host, UInt32(port), 10000)
let error = mpd_connection_get_error(connection)
guard error == MPD_ERROR_SUCCESS else {
_ = handleError(mpdError: error)
return
}
self.isConnected = true
guard let status = mpd_run_status(connection)
else { return }
self.connection = connection
self.status = MPDStatus(status)
delegate?.didConnect(mpdClient: self)
@ -50,4 +53,10 @@ extension MPDClient {
func disconnect() {
enqueueCommand(command: .disconnect)
}
func resetConnection() {
delegate?.willDisconnect(mpdClient: self)
mpd_connection_free(connection)
self.isConnected = false;
}
}

View File

@ -10,6 +10,38 @@ import Foundation
import mpdclient
extension MPDClient {
func checkError() -> Bool {
let error = mpd_connection_get_error(connection)
if error != MPD_ERROR_SUCCESS {
return handleError(mpdError: error)
}
return true
}
func handleError(mpdError: mpd_error) -> Bool {
guard let errorMessage = mpd_connection_get_error_message(connection)
else { return true }
let message = String(cString: errorMessage)
let recovered = mpd_connection_clear_error(connection)
let error = MPDError(
mpdError: mpdError,
recovered: recovered,
message: message
)
delegate?.didRaiseError(mpdClient: self, error: error)
if !recovered {
resetConnection()
}
return recovered
}
func getLastErrorMessage() -> String? {
if mpd_connection_get_error(connection) == MPD_ERROR_SUCCESS {
return nil

View File

@ -11,6 +11,8 @@ import mpdclient
extension MPDClient {
func noIdle() {
guard isConnected else { return }
do {
idleLock.lock()
defer { idleLock.unlock() }
@ -22,6 +24,8 @@ extension MPDClient {
}
func idle(_ force: Bool = false) {
guard isConnected else { return }
let shouldIdle: Bool
do {
@ -50,8 +54,8 @@ extension MPDClient {
wasIdle = isIdle
isIdle = false
}
if wasIdle {
if checkError() && wasIdle {
if mpdIdle.contains(.database) {
self.fetchAllAlbums()
}

View File

@ -0,0 +1,18 @@
//
// MPDError.swift
// Persephone
//
// Created by Daniel Barber on 2/23/20.
// Copyright © 2020 Dan Barber. All rights reserved.
//
import Foundation
import mpdclient
extension MPDClient {
struct MPDError: Error {
let mpdError: mpd_error
let recovered: Bool
let message: String
}
}

View File

@ -11,6 +11,8 @@ import Foundation
protocol MPDClientDelegate {
func didConnect(mpdClient: MPDClient)
func willDisconnect(mpdClient: MPDClient)
func didRaiseError(mpdClient: MPDClient, error: MPDClient.MPDError)
func didUpdateStatus(mpdClient: MPDClient, status: MPDClient.MPDStatus)

View File

@ -6,7 +6,6 @@
// Copyright © 2019 Dan Barber. All rights reserved.
//
import AppKit
import ReSwift
struct UpdateCurrentSongAction: Action {
@ -17,6 +16,8 @@ struct UpdateElapsedTimeAction: Action {
var elapsedTimeMs: UInt = 0
}
struct ResetStatusAction: Action {}
struct UpdateStatusAction: Action {
var status: MPDClient.MPDStatus
}

View File

@ -0,0 +1,13 @@
//
// ServerActions.swift
// Persephone
//
// Created by Daniel Barber on 2/26/20.
// Copyright © 2020 Dan Barber. All rights reserved.
//
import ReSwift
struct UpdateConnectedState: Action {
var connected: Bool
}

View File

@ -9,6 +9,7 @@
import ReSwift
struct AppState: StateType {
var serverState = ServerState()
var playerState = PlayerState()
var queueState = QueueState()
var albumListState = AlbumListState()

View File

@ -6,7 +6,6 @@
// Copyright © 2019 Dan Barber. All rights reserved.
//
import AppKit
import ReSwift
struct PlayerState: StateType {

View File

@ -10,6 +10,7 @@ import ReSwift
func appReducer(action: Action, state: AppState?) -> AppState {
return AppState(
serverState: serverReducer(action: action, state: state?.serverState),
playerState: playerReducer(action: action, state: state?.playerState),
queueState: queueReducer(action: action, state: state?.queueState),
albumListState: albumListReducer(action: action, state: state?.albumListState),

View File

@ -13,6 +13,14 @@ func playerReducer(action: Action, state: PlayerState?) -> PlayerState {
var state = state ?? PlayerState()
switch action {
case is ResetStatusAction:
state.state = .unknown
state.totalTime = 0
state.elapsedTimeMs = 0
state.shuffleState = false
state.repeatState = false
state.volume = -1
case let action as UpdateStatusAction:
state.status = action.status
state.state = action.status.state

View File

@ -0,0 +1,23 @@
//
// ServerReducer.swift
// Persephone
//
// Created by Daniel Barber on 2/26/20.
// Copyright © 2020 Dan Barber. All rights reserved.
//
import ReSwift
func serverReducer(action: Action, state: ServerState?) -> ServerState {
var state = state ?? ServerState()
switch action {
case let action as UpdateConnectedState:
state.connected = action.connected
default:
break
}
return state
}

View File

@ -0,0 +1,13 @@
//
// ServerState.swift
// Persephone
//
// Created by Daniel Barber on 2/26/20.
// Copyright © 2020 Dan Barber. All rights reserved.
//
import ReSwift
struct ServerState: StateType {
var connected: Bool = false
}