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

WIP: Add album detail view

TODO: Fix crash!
Unexpected outstanding background CATransaction
This commit is contained in:
Daniel Barber 2019-05-19 13:09:37 -04:00
parent 4beddf4a63
commit 63c55e1bd4
Signed by: danbarber
GPG Key ID: 931D8112E0103DD8
12 changed files with 456 additions and 17 deletions

View File

@ -39,6 +39,9 @@
E435E3E2221CD4E200184CFC /* NSFont.swift in Sources */ = {isa = PBXBuildFile; fileRef = E435E3E1221CD4E200184CFC /* NSFont.swift */; };
E435E3E4221CD75D00184CFC /* NSImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E435E3E3221CD75D00184CFC /* NSImage.swift */; };
E439109822640213002982E9 /* SongNotifierService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E439109722640213002982E9 /* SongNotifierService.swift */; };
E43B67AA22909793007DCF55 /* AlbumDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E43B67A822909793007DCF55 /* AlbumDetailView.swift */; };
E43B67AB22909793007DCF55 /* AlbumDetailView.xib in Resources */ = {isa = PBXBuildFile; fileRef = E43B67A922909793007DCF55 /* AlbumDetailView.xib */; };
E43B67AD229194CD007DCF55 /* AlbumTracksDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = E43B67AC229194CD007DCF55 /* AlbumTracksDataSource.swift */; };
E4405192227644340090CD6F /* MPDServerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4405191227644340090CD6F /* MPDServerController.swift */; };
E44051942278765A0090CD6F /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = E44051932278765A0090CD6F /* App.swift */; };
E4405196227879960090CD6F /* MPDActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4405195227879960090CD6F /* MPDActions.swift */; };
@ -245,6 +248,9 @@
E435E3E1221CD4E200184CFC /* NSFont.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSFont.swift; sourceTree = "<group>"; };
E435E3E3221CD75D00184CFC /* NSImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSImage.swift; sourceTree = "<group>"; };
E439109722640213002982E9 /* SongNotifierService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SongNotifierService.swift; sourceTree = "<group>"; };
E43B67A822909793007DCF55 /* AlbumDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumDetailView.swift; sourceTree = "<group>"; };
E43B67A922909793007DCF55 /* AlbumDetailView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AlbumDetailView.xib; sourceTree = "<group>"; };
E43B67AC229194CD007DCF55 /* AlbumTracksDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumTracksDataSource.swift; sourceTree = "<group>"; };
E4405191227644340090CD6F /* MPDServerController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPDServerController.swift; sourceTree = "<group>"; };
E44051932278765A0090CD6F /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = "<group>"; };
E4405195227879960090CD6F /* MPDActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPDActions.swift; sourceTree = "<group>"; };
@ -670,6 +676,7 @@
E4D1B597220BA3A20026F233 /* Controllers */ = {
isa = PBXGroup;
children = (
E43B67A822909793007DCF55 /* AlbumDetailView.swift */,
E408D3C1220E134F0006D9BE /* AlbumViewController.swift */,
E47E2FD4222071FD00F747E6 /* AlbumViewItem.swift */,
E47E2FD022205C4600F747E6 /* MainSplitViewController.swift */,
@ -685,6 +692,7 @@
isa = PBXGroup;
children = (
E40786212110CE70006887B1 /* Main.storyboard */,
E43B67A922909793007DCF55 /* AlbumDetailView.xib */,
E408D3C9220E341D0006D9BE /* AlbumViewItem.xib */,
);
path = Resources;
@ -695,6 +703,7 @@
children = (
E4F6B466221E233200ACF42A /* AlbumDataSource.swift */,
E4F6B45F221E119B00ACF42A /* QueueDataSource.swift */,
E43B67AC229194CD007DCF55 /* AlbumTracksDataSource.swift */,
);
path = DataSources;
sourceTree = "<group>";
@ -835,6 +844,7 @@
E450AD9122262C780091BED3 /* SwiftyJSON.framework.dSYM in Resources */,
E42A8F3B22176D6400A13ED9 /* LICENSE.md in Resources */,
E45E4FDA22515D87004B537F /* CHANGELOG.md in Resources */,
E43B67AB22909793007DCF55 /* AlbumDetailView.xib in Resources */,
E45E4FDC22515D87004B537F /* Cartfile in Resources */,
E450AD98222633920091BED3 /* Alamofire.framework.dSYM in Resources */,
E40786202110CE70006887B1 /* Assets.xcassets in Resources */,
@ -915,6 +925,7 @@
E4B11BC02275EE150075461B /* QueueActions.swift in Sources */,
E47E2FD72220720300F747E6 /* AlbumItemView.swift in Sources */,
E450AD9522262DF10091BED3 /* CoverArtQueue.swift in Sources */,
E43B67AD229194CD007DCF55 /* AlbumTracksDataSource.swift in Sources */,
E41E52FD223BF87300173814 /* MPDClient+Connection.swift in Sources */,
E450AD7E222620A10091BED3 /* Album.swift in Sources */,
E408D3B6220DD8970006D9BE /* Notification.swift in Sources */,
@ -922,6 +933,7 @@
E408D3B9220DE98F0006D9BE /* NSUserInterfaceItemIdentifier.swift in Sources */,
E4EB2379220F10B8008C70C0 /* MPDPair.swift in Sources */,
E440519E227BB0720090CD6F /* UIReducer.swift in Sources */,
E43B67AA22909793007DCF55 /* AlbumDetailView.swift in Sources */,
E4FF7190227601B400D4C412 /* PreferencesReducer.swift in Sources */,
E4F6B463221E125900ACF42A /* QueueItem.swift in Sources */,
E4A83BF12221FAA00098FED6 /* PreferencesViewController.swift in Sources */,

View File

@ -0,0 +1,109 @@
//
// AlbumDetailView.swift
// Persephone
//
// Created by Daniel Barber on 2019/5/18.
// Copyright © 2019 Dan Barber. All rights reserved.
//
import Cocoa
class AlbumDetailView: NSViewController {
var album: Album?
var dataSource = AlbumTracksDataSource()
@IBOutlet var albumTracksView: NSTableView!
@IBOutlet var albumTitle: NSTextField!
@IBOutlet var albumArtist: NSTextField!
@IBOutlet var albumCoverView: NSImageView!
override func viewDidLoad() {
super.viewDidLoad()
albumTracksView.dataSource = dataSource
albumTracksView.delegate = self
albumCoverView.wantsLayer = true
albumCoverView.layer?.cornerRadius = 5
albumCoverView.layer?.borderWidth = 1
setAppearance()
guard let album = album else { return }
getAlbumSongs(for: album)
albumTitle.stringValue = album.title
albumArtist.stringValue = album.artist
switch album.coverArt {
case .loaded(let coverArt):
albumCoverView.image = coverArt ?? .defaultCoverArt
default:
albumCoverView.image = .defaultCoverArt
}
}
func getAlbumSongs(for album: Album) {
App.mpdClient.getAlbumSongs(for: album.mpdAlbum) { (mpdSongs: [MPDClient.MPDSong]) in
self.dataSource.albumTracks = mpdSongs.map {
return Song(mpdSong: $0)
}
DispatchQueue.main.async {
self.albumTracksView.reloadData()
}
}
}
func setAppearance() {
if #available(OSX 10.14, *) {
let darkMode = NSApp.effectiveAppearance.bestMatch(from:
[.darkAqua, .aqua]) == .darkAqua
albumCoverView.layer?.borderColor = darkMode ? .albumBorderColorDark : .albumBorderColorLight
} else {
albumCoverView.layer?.borderColor = .albumBorderColorLight
}
}
func setAlbum(_ album: Album) {
self.album = album
}
}
extension AlbumDetailView: NSTableViewDelegate {
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
let song = dataSource.albumTracks[row]
switch tableColumn?.identifier.rawValue {
case "trackNumberColumn":
return cellForTrackNumber(tableView, with: song)
case "trackTitleColumn":
return cellForSongTitle(tableView, with: song)
default:
return nil
}
}
func cellForTrackNumber(_ tableView: NSTableView, with song: Song) -> NSView {
let cellView = tableView.makeView(
withIdentifier: .trackNumber,
owner: self
) as! NSTableCellView
cellView.textField?.stringValue = "\(song.trackNumber)."
return cellView
}
func cellForSongTitle(_ tableView: NSTableView, with song: Song) -> NSView {
let cellView = tableView.makeView(
withIdentifier: .songTitle,
owner: self
) as! NSTableCellView
cellView.textField?.stringValue = song.title
return cellView
}
}

View File

@ -51,13 +51,29 @@ class AlbumViewItem: NSCollectionViewItem {
}
}
@IBAction func playAlbum(_ sender: Any) {
@IBAction func playAlbum(_ sender: NSButton) {
guard let album = album else { return }
App.store.dispatch(MPDPlayAlbum(album: album.mpdAlbum))
}
@IBOutlet var albumCoverView: NSImageView!
@IBAction func showAlbumDetail(_ sender: NSButton) {
guard let album = album else { return }
let detailViewController = AlbumDetailView()
detailViewController.setAlbum(album)
let popoverView = NSPopover()
popoverView.contentViewController = detailViewController
popoverView.behavior = .transient
popoverView.show(
relativeTo: sender.bounds,
of: sender,
preferredEdge: .maxY
)
}
@IBOutlet var albumCoverView: NSButton!
@IBOutlet var albumTitle: NSTextField!
@IBOutlet var albumArtist: NSTextField!
}

View File

@ -0,0 +1,17 @@
//
// AlbumTracksDataSource.swift
// Persephone
//
// Created by Daniel Barber on 2019/5/19.
// Copyright © 2019 Dan Barber. All rights reserved.
//
import AppKit
class AlbumTracksDataSource: NSObject, NSTableViewDataSource {
var albumTracks: [Song] = []
func numberOfRows(in tableView: NSTableView) -> Int {
return albumTracks.count
}
}

View File

@ -17,4 +17,8 @@ extension NSUserInterfaceItemIdentifier {
static let queueSongTitle = NSUserInterfaceItemIdentifier("songTitleCell")
static let albumViewItem = NSUserInterfaceItemIdentifier("AlbumViewItem")
static let trackNumber = NSUserInterfaceItemIdentifier("trackNumberCell")
static let songTitle = NSUserInterfaceItemIdentifier("songTitleCell")
static let songDuration = NSUserInterfaceItemIdentifier("durationCell")
}

View File

@ -26,21 +26,27 @@ extension MPDClient {
)
}
func sendPlayAlbum(_ album: MPDAlbum) {
var songs: [MPDSong] = []
func getAlbumSongs(for album: MPDAlbum, callback: @escaping ([MPDSong]) -> Void) {
enqueueCommand(
command: .getAlbumSongs,
priority: .normal,
userData: ["album": album, "callback": callback]
)
}
mpd_run_clear(self.connection)
mpd_search_db_songs(self.connection, true)
mpd_search_add_tag_constraint(self.connection, MPD_OPERATOR_DEFAULT, MPD_TAG_ALBUM, album.title)
mpd_search_add_tag_constraint(self.connection, MPD_OPERATOR_DEFAULT, MPD_TAG_ALBUM_ARTIST, album.artist)
mpd_search_commit(self.connection)
while let song = mpd_recv_song(self.connection) {
songs.append(MPDSong(song))
func sendPlayAlbum(_ album: MPDAlbum) {
getAlbumSongs(for: album) { songs in
self.enqueueCommand(
command: .replaceQueue,
priority: .normal,
userData: ["songs": songs]
)
self.enqueueCommand(
command: .playTrack,
priority: .normal,
userData: ["queuePos": 0]
)
}
for song in songs {
mpd_run_add(self.connection, song.uri)
}
mpd_run_play_pos(self.connection, 0)
}
func allAlbums() {
@ -91,4 +97,21 @@ extension MPDClient {
callback(firstSong)
}
func albumSongs(for album: MPDAlbum, callback: ([MPDSong]) -> Void) {
guard isConnected else { return }
var songs: [MPDSong] = []
mpd_search_db_songs(self.connection, true)
mpd_search_add_tag_constraint(self.connection, MPD_OPERATOR_DEFAULT, MPD_TAG_ALBUM, album.title)
mpd_search_add_tag_constraint(self.connection, MPD_OPERATOR_DEFAULT, MPD_TAG_ALBUM_ARTIST, album.artist)
mpd_search_commit(self.connection)
while let song = mpd_recv_song(self.connection) {
songs.append(MPDSong(song))
}
callback(songs)
}
}

View File

@ -54,6 +54,10 @@ extension MPDClient {
guard let queuePos = userData["queuePos"] as? Int
else { return }
sendPlayTrack(at: queuePos)
case .replaceQueue:
guard let songs = userData["songs"] as? [MPDSong]
else { return }
sendReplaceQueue(songs)
// Album commands
case .fetchAllAlbums:
@ -67,6 +71,13 @@ extension MPDClient {
else { return }
albumFirstSong(for: album, callback: callback)
case .getAlbumSongs:
guard let album = userData["album"] as? MPDAlbum,
let callback = userData["callback"] as? ([MPDSong]) -> Void
else { return }
albumSongs(for: album, callback: callback)
}
}

View File

@ -31,4 +31,13 @@ extension MPDClient {
self.queue.append(song)
}
}
func sendReplaceQueue(_ songs: [MPDSong]) {
mpd_run_clear(self.connection)
for song in songs {
mpd_run_add(self.connection, song.uri)
}
mpd_run_play_pos(self.connection, 0)
}
}

View File

@ -29,10 +29,12 @@ extension MPDClient {
// Queue commands
case fetchQueue
case playTrack
case replaceQueue
// Album commands
case fetchAllAlbums
case playAlbum
case getAlbumFirstSong
case getAlbumSongs
}
}

View File

@ -11,6 +11,10 @@ import Foundation
struct Song {
var mpdSong: MPDClient.MPDSong
var trackNumber: String {
return mpdSong.getTag(.track)
}
var title: String {
return mpdSong.getTag(.title)
}

View File

@ -0,0 +1,218 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14490.70"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="AlbumDetailView" customModule="Persephone" customModuleProvider="target">
<connections>
<outlet property="albumArtist" destination="4Jx-I5-Nkv" id="mct-x3-yYC"/>
<outlet property="albumCoverView" destination="FWd-vZ-5CT" id="aHh-Bz-XQW"/>
<outlet property="albumTitle" destination="m2v-pR-e9v" id="M5i-u6-Nev"/>
<outlet property="albumTracksView" destination="ehr-qh-87Q" id="fSa-Di-CqI"/>
<outlet property="view" destination="Hz6-mo-xeY" id="0bl-1N-x8E"/>
</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="823" height="561"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="m2v-pR-e9v">
<rect key="frame" x="357" y="512" width="448" height="29"/>
<constraints>
<constraint firstAttribute="width" constant="444" id="64T-pe-ww2"/>
</constraints>
<textFieldCell key="cell" title="Album Title" id="URb-mh-vZz">
<font key="font" metaFont="systemSemibold" size="24"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="4Jx-I5-Nkv">
<rect key="frame" x="357" y="485" width="448" height="19"/>
<textFieldCell key="cell" title="Artist Name" id="ztJ-4E-qvI">
<font key="font" metaFont="system" size="16"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="FWd-vZ-5CT">
<rect key="frame" x="31" y="241" width="300" height="300"/>
<constraints>
<constraint firstAttribute="height" constant="300" id="WkH-QY-IM1"/>
<constraint firstAttribute="width" secondItem="FWd-vZ-5CT" secondAttribute="height" multiplier="1:1" id="m2r-ee-czT"/>
</constraints>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyUpOrDown" image="defaultCoverArt" id="scE-kj-gex"/>
</imageView>
<button verticalHuggingPriority="750" fixedFrame="YES" imageHugsTitle="YES" translatesAutoresizingMaskIntoConstraints="NO" id="jMU-bv-TNF">
<rect key="frame" x="31" y="182" width="119" height="35"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="smallSquare" title="Play Album" bezelStyle="smallSquare" image="playButton" imagePosition="left" alignment="center" lineBreakMode="truncatingTail" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="Rtg-Zd-JYc">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
</button>
<scrollView borderType="none" autohidesScrollers="YES" horizontalLineScroll="19" horizontalPageScroll="10" verticalLineScroll="19" verticalPageScroll="10" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="BOb-Lr-10M">
<rect key="frame" x="359" y="20" width="444" height="436"/>
<clipView key="contentView" drawsBackground="NO" copiesOnScroll="NO" id="9QN-UB-b4l">
<rect key="frame" x="0.0" y="0.0" width="444" height="436"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<tableView verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnAutoresizingStyle="none" columnSelection="YES" multipleSelection="NO" autosaveColumns="NO" rowSizeStyle="automatic" viewBased="YES" id="ehr-qh-87Q">
<rect key="frame" x="0.0" y="0.0" width="444" height="436"/>
<autoresizingMask key="autoresizingMask"/>
<size key="intercellSpacing" width="3" height="2"/>
<color key="backgroundColor" red="0.11764705882352941" green="0.11764705882352941" blue="0.11764705882352941" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
<color key="gridColor" name="gridColor" catalog="System" colorSpace="catalog"/>
<tableColumns>
<tableColumn identifier="trackNumberColumn" width="40" minWidth="40" maxWidth="40" id="cwb-jE-CEP">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" title="Track No.">
<font key="font" metaFont="smallSystem"/>
<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="7b7-6s-u1U">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
<prototypeCellViews>
<tableCellView identifier="trackNumberCell" id="bVN-zt-KW7">
<rect key="frame" x="1" y="1" width="40" height="17"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="6eU-Jx-HDR">
<rect key="frame" x="0.0" y="0.0" width="40" height="17"/>
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="1." id="Z5y-oS-Qm8">
<font key="font" metaFont="system"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<constraints>
<constraint firstItem="6eU-Jx-HDR" firstAttribute="centerY" secondItem="bVN-zt-KW7" secondAttribute="centerY" id="5cb-M0-OIZ"/>
<constraint firstItem="6eU-Jx-HDR" firstAttribute="leading" secondItem="bVN-zt-KW7" secondAttribute="leading" constant="2" id="KXb-Ua-LaU"/>
<constraint firstItem="6eU-Jx-HDR" firstAttribute="centerX" secondItem="bVN-zt-KW7" secondAttribute="centerX" id="MGU-H7-mAj"/>
</constraints>
<connections>
<outlet property="textField" destination="6eU-Jx-HDR" id="DWy-vj-9Eq"/>
</connections>
</tableCellView>
</prototypeCellViews>
</tableColumn>
<tableColumn identifier="trackTitleColumn" width="325" minWidth="40" maxWidth="1000" id="7yp-QQ-EzC">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" title="Song Title">
<font key="font" metaFont="smallSystem"/>
<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="wRS-GW-ubu">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
<prototypeCellViews>
<tableCellView identifier="songTitleCell" id="41U-5i-Oot">
<rect key="frame" x="44" y="1" width="325" height="17"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="R8t-bV-9LI">
<rect key="frame" x="0.0" y="0.0" width="325" height="17"/>
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="My Song Title" id="Sdi-jJ-EOM">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<constraints>
<constraint firstItem="R8t-bV-9LI" firstAttribute="centerX" secondItem="41U-5i-Oot" secondAttribute="centerX" id="0Hz-cI-Y32"/>
<constraint firstItem="R8t-bV-9LI" firstAttribute="centerY" secondItem="41U-5i-Oot" secondAttribute="centerY" id="7iv-Qw-o7d"/>
<constraint firstItem="R8t-bV-9LI" firstAttribute="leading" secondItem="41U-5i-Oot" secondAttribute="leading" constant="2" id="UzY-kH-q4q"/>
</constraints>
<connections>
<outlet property="textField" destination="R8t-bV-9LI" id="b79-el-ZAY"/>
</connections>
</tableCellView>
</prototypeCellViews>
</tableColumn>
<tableColumn identifier="trackDurationColumn" width="70" minWidth="40" maxWidth="1000" id="ha5-ff-2az">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" title="Duration">
<font key="font" metaFont="smallSystem"/>
<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="mxh-1M-IMh">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
<prototypeCellViews>
<tableCellView id="Lbx-5u-OFw">
<rect key="frame" x="372" y="1" width="70" height="17"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="pCr-f1-wNs">
<rect key="frame" x="0.0" y="0.0" width="70" height="17"/>
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" alignment="right" title="0:00" id="Qe2-WO-eXr">
<font key="font" metaFont="system"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<constraints>
<constraint firstItem="pCr-f1-wNs" firstAttribute="leading" secondItem="Lbx-5u-OFw" secondAttribute="leading" constant="2" id="DDY-eP-HQ2"/>
<constraint firstItem="pCr-f1-wNs" firstAttribute="centerY" secondItem="Lbx-5u-OFw" secondAttribute="centerY" id="M35-oF-zNB"/>
<constraint firstItem="pCr-f1-wNs" firstAttribute="centerX" secondItem="Lbx-5u-OFw" secondAttribute="centerX" id="j50-OZ-wXu"/>
</constraints>
<connections>
<outlet property="textField" destination="pCr-f1-wNs" id="r9M-8L-FoO"/>
</connections>
</tableCellView>
</prototypeCellViews>
</tableColumn>
</tableColumns>
</tableView>
</subviews>
<nil key="backgroundColor"/>
</clipView>
<scroller key="horizontalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" horizontal="YES" id="w9p-lE-zXP">
<rect key="frame" x="0.0" y="420" width="444" height="16"/>
<autoresizingMask key="autoresizingMask"/>
</scroller>
<scroller key="verticalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" horizontal="NO" id="n7u-af-H0a">
<rect key="frame" x="224" y="17" width="15" height="102"/>
<autoresizingMask key="autoresizingMask"/>
</scroller>
</scrollView>
</subviews>
<constraints>
<constraint firstAttribute="bottom" secondItem="BOb-Lr-10M" secondAttribute="bottom" constant="20" symbolic="YES" id="AaR-Nq-vZD"/>
<constraint firstItem="4Jx-I5-Nkv" firstAttribute="top" secondItem="m2v-pR-e9v" secondAttribute="bottom" constant="8" symbolic="YES" id="P10-cj-Iaz"/>
<constraint firstItem="m2v-pR-e9v" firstAttribute="leading" secondItem="4Jx-I5-Nkv" secondAttribute="leading" id="PVJ-Rk-9K5"/>
<constraint firstItem="m2v-pR-e9v" firstAttribute="trailing" secondItem="4Jx-I5-Nkv" secondAttribute="trailing" id="PlO-jD-clF"/>
<constraint firstItem="FWd-vZ-5CT" firstAttribute="leading" secondItem="Hz6-mo-xeY" secondAttribute="leading" constant="31" id="Pyj-e4-N8r"/>
<constraint firstItem="FWd-vZ-5CT" firstAttribute="top" secondItem="Hz6-mo-xeY" secondAttribute="top" constant="20" symbolic="YES" id="Yvv-MA-LPf"/>
<constraint firstItem="m2v-pR-e9v" firstAttribute="leading" secondItem="BOb-Lr-10M" secondAttribute="leading" id="aLT-36-8oT"/>
<constraint firstItem="m2v-pR-e9v" firstAttribute="trailing" secondItem="BOb-Lr-10M" secondAttribute="trailing" id="dpm-La-xDl"/>
<constraint firstItem="BOb-Lr-10M" firstAttribute="top" secondItem="4Jx-I5-Nkv" secondAttribute="bottom" constant="29" id="f3Q-VP-2Y2"/>
<constraint firstAttribute="trailing" secondItem="m2v-pR-e9v" secondAttribute="trailing" constant="20" symbolic="YES" id="sEh-fo-Kqa"/>
<constraint firstItem="m2v-pR-e9v" firstAttribute="top" secondItem="FWd-vZ-5CT" secondAttribute="top" id="zLA-y0-JQ9"/>
</constraints>
<point key="canvasLocation" x="262.5" y="117.5"/>
</customView>
</objects>
<resources>
<image name="defaultCoverArt" width="128" height="128"/>
<image name="playButton" width="17" height="17"/>
</resources>
</document>

View File

@ -37,10 +37,20 @@
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<imageView identifier="albumArtwork" horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="250" verticalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="Kfb-8f-ean">
<imageView identifier="albumArtwork" horizontalHuggingPriority="750" verticalHuggingPriority="750" placeholderIntrinsicWidth="128" placeholderIntrinsicHeight="128" translatesAutoresizingMaskIntoConstraints="NO" id="Kfb-8f-ean">
<rect key="frame" x="0.0" y="39" width="128" height="128"/>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyUpOrDown" image="defaultCoverArt" id="FsA-JX-BFh"/>
</imageView>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="x5e-56-uVO" userLabel="Album Detail Button">
<rect key="frame" x="0.0" y="39" width="128" height="128"/>
<buttonCell key="cell" type="square" bezelStyle="shadowlessSquare" alignment="center" lineBreakMode="truncatingTail" state="on" imageScaling="proportionallyDown" inset="2" id="MTh-fn-aCH">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="showAlbumDetail:" target="-2" id="nO1-4H-LHS"/>
</connections>
</button>
<button hidden="YES" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="n8W-do-HyG">
<rect key="frame" x="43" y="81" width="42" height="43"/>
<constraints>
@ -57,16 +67,20 @@
</button>
</subviews>
<constraints>
<constraint firstItem="x5e-56-uVO" firstAttribute="leading" secondItem="Kfb-8f-ean" secondAttribute="leading" id="1Hi-Uk-rkL"/>
<constraint firstItem="5Uu-j1-qyT" firstAttribute="trailing" secondItem="KEh-NL-c2W" secondAttribute="trailing" id="64z-uz-4nY"/>
<constraint firstAttribute="bottom" secondItem="KEh-NL-c2W" secondAttribute="bottom" constant="18" id="8Kg-1r-wNp"/>
<constraint firstItem="x5e-56-uVO" firstAttribute="trailing" secondItem="Kfb-8f-ean" secondAttribute="trailing" id="BYd-Fg-DVb"/>
<constraint firstItem="Kfb-8f-ean" firstAttribute="leading" secondItem="Hz6-mo-xeY" secondAttribute="leading" id="JMi-4i-dgs"/>
<constraint firstAttribute="trailing" secondItem="Kfb-8f-ean" secondAttribute="trailing" id="KQC-Wz-Bsg"/>
<constraint firstItem="n8W-do-HyG" firstAttribute="centerX" secondItem="KEh-NL-c2W" secondAttribute="centerX" id="Kf1-ws-d4q"/>
<constraint firstItem="n8W-do-HyG" firstAttribute="centerX" secondItem="Kfb-8f-ean" secondAttribute="centerX" id="Kf1-ws-d4q"/>
<constraint firstItem="5Uu-j1-qyT" firstAttribute="leading" secondItem="KEh-NL-c2W" secondAttribute="leading" id="MUo-0i-fX9"/>
<constraint firstItem="Kfb-8f-ean" firstAttribute="top" secondItem="Hz6-mo-xeY" secondAttribute="top" id="Qbk-jx-zAi"/>
<constraint firstItem="KEh-NL-c2W" firstAttribute="trailing" secondItem="Kfb-8f-ean" secondAttribute="trailing" id="U0w-G4-ggX"/>
<constraint firstItem="KEh-NL-c2W" firstAttribute="leading" secondItem="Kfb-8f-ean" secondAttribute="leading" id="V8r-Rc-Dx7"/>
<constraint firstAttribute="bottom" secondItem="5Uu-j1-qyT" secondAttribute="bottom" id="gci-4h-pDZ"/>
<constraint firstItem="x5e-56-uVO" firstAttribute="top" secondItem="Kfb-8f-ean" secondAttribute="top" id="hw2-ik-6VW"/>
<constraint firstItem="x5e-56-uVO" firstAttribute="bottom" secondItem="Kfb-8f-ean" secondAttribute="bottom" id="iVQ-Vn-dSV"/>
<constraint firstItem="n8W-do-HyG" firstAttribute="centerY" secondItem="Kfb-8f-ean" secondAttribute="centerY" id="pgP-oA-Nxa"/>
<constraint firstAttribute="bottom" secondItem="Kfb-8f-ean" secondAttribute="bottom" constant="39" id="sid-zJ-YMA"/>
</constraints>