From 447432120ac1b93a877c598a4b7537e12f8d6d7a Mon Sep 17 00:00:00 2001 From: Dan Barber Date: Sun, 10 Feb 2019 14:52:15 -0500 Subject: [PATCH] Add album view --- Persephone.xcodeproj/project.pbxproj | 30 ++++- .../Controllers/AlbumViewController.swift | 56 ++++++++++ .../Controllers/NotificationsController.swift | 7 ++ Persephone/Extensions/Notification.swift | 2 + Persephone/MPDClient/MPDClient.swift | 104 ++++++++++++++++-- Persephone/MPDClient/Models/Album.swift | 21 ++++ Persephone/MPDClient/Models/Pair.swift | 18 +++ Persephone/MPDClient/Protocols/Delegate.swift | 1 + .../blankAlbum.imageset/Contents.json | 12 ++ .../blankAlbum.imageset/blankAlbum.pdf | Bin 0 -> 4127 bytes Persephone/Views/AlbumItem.swift | 27 +++++ Persephone/Views/AlbumItem.xib | 60 ++++++++++ .../Base.lproj/Main.storyboard | 36 +++--- Resources/export/blankAlbum.pdf | Bin 0 -> 4127 bytes Resources/icons.sketch | Bin 76311 -> 76320 bytes 15 files changed, 347 insertions(+), 27 deletions(-) create mode 100644 Persephone/Controllers/AlbumViewController.swift create mode 100644 Persephone/MPDClient/Models/Album.swift create mode 100644 Persephone/MPDClient/Models/Pair.swift create mode 100644 Persephone/Resources/Assets.xcassets/blankAlbum.imageset/Contents.json create mode 100644 Persephone/Resources/Assets.xcassets/blankAlbum.imageset/blankAlbum.pdf create mode 100644 Persephone/Views/AlbumItem.swift create mode 100644 Persephone/Views/AlbumItem.xib rename Persephone/{Resources => Views}/Base.lproj/Main.storyboard (98%) create mode 100644 Resources/export/blankAlbum.pdf diff --git a/Persephone.xcodeproj/project.pbxproj b/Persephone.xcodeproj/project.pbxproj index 0f2dfbc..0e6aec4 100644 --- a/Persephone.xcodeproj/project.pbxproj +++ b/Persephone.xcodeproj/project.pbxproj @@ -15,6 +15,9 @@ E408D3B6220DD8970006D9BE /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = E408D3B5220DD8970006D9BE /* Notification.swift */; }; E408D3B9220DE98F0006D9BE /* NSUserInterfaceItemIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E408D3B8220DE98F0006D9BE /* NSUserInterfaceItemIdentifier.swift */; }; E408D3BE220E03EE0006D9BE /* RawRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E408D3BD220E03EE0006D9BE /* RawRepresentable.swift */; }; + E408D3C2220E134F0006D9BE /* AlbumViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E408D3C1220E134F0006D9BE /* AlbumViewController.swift */; }; + E408D3CA220E341D0006D9BE /* AlbumItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E408D3C8220E341D0006D9BE /* AlbumItem.swift */; }; + E408D3CB220E341D0006D9BE /* AlbumItem.xib in Resources */ = {isa = PBXBuildFile; fileRef = E408D3C9220E341D0006D9BE /* AlbumItem.xib */; }; E41B22C021FB6BBA00D544F6 /* libmpdclient.2.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = E41B22BF21FB6BBA00D544F6 /* libmpdclient.2.dylib */; settings = {ATTRIBUTES = (Required, ); }; }; E41B22C121FB6C3300D544F6 /* libmpdclient.2.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = E41B22BF21FB6BBA00D544F6 /* libmpdclient.2.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; E41B22C621FB932700D544F6 /* MPDClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = E41B22C521FB932700D544F6 /* MPDClient.swift */; }; @@ -24,6 +27,8 @@ E4E8CC922204F4B80024217A /* QueueViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4E8CC912204F4B80024217A /* QueueViewController.swift */; }; E4E8CC942206097F0024217A /* NotificationsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4E8CC932206097F0024217A /* NotificationsController.swift */; }; E4E8CC9A22075D370024217A /* Song.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4E8CC9922075D370024217A /* Song.swift */; }; + E4EB2379220F10B8008C70C0 /* Pair.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4EB2378220F10B8008C70C0 /* Pair.swift */; }; + E4EB237B220F7CF1008C70C0 /* Album.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4EB237A220F7CF1008C70C0 /* Album.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -73,6 +78,9 @@ E408D3B5220DD8970006D9BE /* Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notification.swift; sourceTree = ""; }; E408D3B8220DE98F0006D9BE /* NSUserInterfaceItemIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSUserInterfaceItemIdentifier.swift; sourceTree = ""; }; E408D3BD220E03EE0006D9BE /* RawRepresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RawRepresentable.swift; sourceTree = ""; }; + E408D3C1220E134F0006D9BE /* AlbumViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumViewController.swift; sourceTree = ""; }; + E408D3C8220E341D0006D9BE /* AlbumItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumItem.swift; sourceTree = ""; }; + E408D3C9220E341D0006D9BE /* AlbumItem.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AlbumItem.xib; sourceTree = ""; }; E41B22BF21FB6BBA00D544F6 /* libmpdclient.2.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libmpdclient.2.dylib; path = libmpdclient/output/libmpdclient.2.dylib; sourceTree = ""; }; E41B22C421FB715A00D544F6 /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; E41B22C521FB932700D544F6 /* MPDClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPDClient.swift; sourceTree = ""; }; @@ -117,6 +125,8 @@ E4E8CC912204F4B80024217A /* QueueViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueueViewController.swift; sourceTree = ""; }; E4E8CC932206097F0024217A /* NotificationsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsController.swift; sourceTree = ""; }; E4E8CC9922075D370024217A /* Song.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Song.swift; sourceTree = ""; }; + E4EB2378220F10B8008C70C0 /* Pair.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pair.swift; sourceTree = ""; }; + E4EB237A220F7CF1008C70C0 /* Album.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Album.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -172,6 +182,7 @@ E408D3B7220DE8CC0006D9BE /* Extensions */, E4D1B598220BA3C90026F233 /* Resources */, E4D1B597220BA3A20026F233 /* Controllers */, + E408D3C3220E138B0006D9BE /* Views */, E41B22C721FB966C00D544F6 /* include */, E407861B2110CE6E006887B1 /* AppDelegate.swift */, E40786242110CE70006887B1 /* Info.plist */, @@ -216,6 +227,16 @@ path = Extensions; sourceTree = ""; }; + E408D3C3220E138B0006D9BE /* Views */ = { + isa = PBXGroup; + children = ( + E40786212110CE70006887B1 /* Main.storyboard */, + E408D3C8220E341D0006D9BE /* AlbumItem.swift */, + E408D3C9220E341D0006D9BE /* AlbumItem.xib */, + ); + path = Views; + sourceTree = ""; + }; E41B22BE21FB6B3300D544F6 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -291,6 +312,8 @@ children = ( E4E8CC9922075D370024217A /* Song.swift */, E4A642D922090CBE00067D21 /* Status.swift */, + E4EB2378220F10B8008C70C0 /* Pair.swift */, + E4EB237A220F7CF1008C70C0 /* Album.swift */, ); path = Models; sourceTree = ""; @@ -306,6 +329,7 @@ E4D1B597220BA3A20026F233 /* Controllers */ = { isa = PBXGroup; children = ( + E408D3C1220E134F0006D9BE /* AlbumViewController.swift */, E4E8CC932206097F0024217A /* NotificationsController.swift */, E4E8CC912204F4B80024217A /* QueueViewController.swift */, E465049921E94DF500A70F4C /* WindowController.swift */, @@ -317,7 +341,6 @@ isa = PBXGroup; children = ( E407861F2110CE70006887B1 /* Assets.xcassets */, - E40786212110CE70006887B1 /* Main.storyboard */, ); path = Resources; sourceTree = ""; @@ -433,6 +456,7 @@ buildActionMask = 2147483647; files = ( E40786202110CE70006887B1 /* Assets.xcassets in Resources */, + E408D3CB220E341D0006D9BE /* AlbumItem.xib in Resources */, E40786232110CE70006887B1 /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -458,17 +482,21 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + E408D3C2220E134F0006D9BE /* AlbumViewController.swift in Sources */, E4A642DA22090CBE00067D21 /* Status.swift in Sources */, E4E8CC942206097F0024217A /* NotificationsController.swift in Sources */, E408D3B6220DD8970006D9BE /* Notification.swift in Sources */, E408D3B9220DE98F0006D9BE /* NSUserInterfaceItemIdentifier.swift in Sources */, + E4EB2379220F10B8008C70C0 /* Pair.swift in Sources */, E465049A21E94DF500A70F4C /* WindowController.swift in Sources */, E41B22C621FB932700D544F6 /* MPDClient.swift in Sources */, E407861C2110CE6E006887B1 /* AppDelegate.swift in Sources */, E4E8CC9A22075D370024217A /* Song.swift in Sources */, + E408D3CA220E341D0006D9BE /* AlbumItem.swift in Sources */, E4E8CC902204EC7F0024217A /* Delegate.swift in Sources */, E408D3BE220E03EE0006D9BE /* RawRepresentable.swift in Sources */, E4E8CC922204F4B80024217A /* QueueViewController.swift in Sources */, + E4EB237B220F7CF1008C70C0 /* Album.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Persephone/Controllers/AlbumViewController.swift b/Persephone/Controllers/AlbumViewController.swift new file mode 100644 index 0000000..8e67518 --- /dev/null +++ b/Persephone/Controllers/AlbumViewController.swift @@ -0,0 +1,56 @@ +// +// AlbumViewController.swift +// Persephone +// +// Created by Daniel Barber on 2019/2/08. +// Copyright © 2019 Dan Barber. All rights reserved. +// + +import Cocoa + +class AlbumViewController: NSViewController, + NSCollectionViewDataSource, + NSCollectionViewDelegate { + var albums: [MPDClient.Album] = [] + + override func viewDidLoad() { + super.viewDidLoad() + + NotificationCenter.default.addObserver( + self, + selector: #selector(updateAlbums(_:)), + name: Notification.loadedAlbums, + object: AppDelegate.mpdClient + ) + } + + @objc func updateAlbums(_ notification: Notification) { + guard let albums = notification.userInfo?[Notification.albumsKey] as? [MPDClient.Album] + else { return } + + print("Loaded albums") + self.albums = albums + + albumCollectionView.reloadData() + } + + func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int { + return albums.count + } + + func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem { + let item = collectionView.makeItem( + withIdentifier: NSUserInterfaceItemIdentifier("AlbumItem"), + for: indexPath + ) + guard let albumItem = item as? AlbumItem else { return item } + + albumItem.view.wantsLayer = true + albumItem.setAlbumTitle(albums[indexPath.item].title) + albumItem.setAlbumArtist(albums[indexPath.item].artist) + + return albumItem + } + + @IBOutlet var albumCollectionView: NSCollectionView! +} diff --git a/Persephone/Controllers/NotificationsController.swift b/Persephone/Controllers/NotificationsController.swift index 1087b0a..74a9d20 100644 --- a/Persephone/Controllers/NotificationsController.swift +++ b/Persephone/Controllers/NotificationsController.swift @@ -32,6 +32,13 @@ class NotificationsController: MPDClientDelegate { ) } + func didLoadAlbums(mpdClient: MPDClient, albums: [MPDClient.Album]) { + sendNotification( + name: Notification.loadedAlbums, + userInfo: [Notification.albumsKey: albums] + ) + } + private func sendNotification(name: Notification.Name, userInfo: [AnyHashable : Any]) { self.notificationQueue.async { NotificationCenter.default.post( diff --git a/Persephone/Extensions/Notification.swift b/Persephone/Extensions/Notification.swift index ad8e275..903f39a 100644 --- a/Persephone/Extensions/Notification.swift +++ b/Persephone/Extensions/Notification.swift @@ -12,8 +12,10 @@ extension Notification { static let stateChanged = Notification.Name("MPDClientStateChanged") static let queueChanged = Notification.Name("MPDClientQueueChanged") static let queuePosChanged = Notification.Name("MPDClientQueuePosChanged") + static let loadedAlbums = Notification.Name("MPDClientLoadedAlbums") static let stateKey = "state" static let queueKey = "queue" static let queuePosKey = "song" + static let albumsKey = "albums" } diff --git a/Persephone/MPDClient/MPDClient.swift b/Persephone/MPDClient/MPDClient.swift index 19d1c39..5a434bf 100644 --- a/Persephone/MPDClient/MPDClient.swift +++ b/Persephone/MPDClient/MPDClient.swift @@ -22,7 +22,8 @@ class MPDClient { private let commandQueue = DispatchQueue(label: "commandQueue") enum Command { - case prevTrack, nextTrack, playPause, stop, fetchStatus, fetchQueue + case prevTrack, nextTrack, playPause, stop, fetchStatus, fetchQueue, + fetchAllAlbums } struct Idle: OptionSet { @@ -57,6 +58,8 @@ class MPDClient { fetchQueue() + fetchAllAlbums() + self.delegate?.didUpdateState(mpdClient: self, state: self.status!.state) self.delegate?.didUpdateQueue(mpdClient: self, queue: self.queue) self.delegate?.didUpdateQueuePos(mpdClient: self, song: self.status!.song) @@ -78,6 +81,10 @@ class MPDClient { sendCommand(command: .fetchQueue) } + func fetchAllAlbums() { + sendCommand(command: .fetchAllAlbums) + } + func playPause() { queueCommand(command: .playPause) } @@ -103,6 +110,7 @@ class MPDClient { } func sendCommand(command: Command) { + print("Command:", command) switch command { // Transport commands @@ -114,19 +122,12 @@ class MPDClient { sendStop() case .playPause: sendPlay() - case .fetchStatus: - guard let status = mpd_run_status(connection) else { break } - self.status = Status(status) - + sendRunStatus() case .fetchQueue: - self.queue = [] - mpd_send_list_queue_meta(connection) - - while let mpdSong = mpd_recv_song(connection) { - let song = Song(mpdSong) - self.queue.append(song) - } + sendFetchQueue() + case .fetchAllAlbums: + allAlbums() } } @@ -158,6 +159,84 @@ class MPDClient { } } + func sendRunStatus() { + guard let status = mpd_run_status(connection) else { return } + self.status = Status(status) + } + + func sendFetchQueue() { + self.queue = [] + mpd_send_list_queue_meta(connection) + + while let mpdSong = mpd_recv_song(connection) { + let song = Song(mpdSong) + self.queue.append(song) + } + } + + func allAlbums() { + var albums: [Album] = [] + var artist: String = "" + + mpd_search_db_tags(self.connection, MPD_TAG_ALBUM) + mpd_search_add_group_tag(self.connection, MPD_TAG_ALBUM_ARTIST) + mpd_search_commit(self.connection) + while let mpdPair = mpd_recv_pair(self.connection) { + let name = String(cString: mpdPair.pointee.name) + let value = String(cString: mpdPair.pointee.value) + + switch name { + case "AlbumArtist": + artist = value + case "Album": + albums.append(Album(title: value, artist: artist)) + default: + break + } + + mpd_return_pair(self.connection, mpdPair) + } + + delegate?.didLoadAlbums(mpdClient: self, albums: albums) + } + + func albumsForArtist(_ artist: String) -> [Album] { + var albums: [Album] = [] + + mpd_search_db_tags(self.connection, MPD_TAG_ALBUM) + mpd_search_add_tag_constraint(self.connection, MPD_OPERATOR_DEFAULT, MPD_TAG_ARTIST, artist) + mpd_search_commit(self.connection) + while let mpdAlbumPair = mpd_recv_pair_tag(self.connection, MPD_TAG_ALBUM) { + print(artist, "-", String(cString: mpdAlbumPair.pointee.value)) + albums.append(Album(title: String(cString: mpdAlbumPair.pointee.value), artist: artist)) + mpd_return_pair(self.connection, mpdAlbumPair) + } + + return albums + } + + func allArtists() -> [String] { + var artists: [String] = [] + + mpd_search_db_tags(self.connection, MPD_TAG_ARTIST) + mpd_search_commit(self.connection) + while let mpdArtistPair = mpd_recv_pair_tag(self.connection, MPD_TAG_ARTIST) { + artists.append(String(cString: mpdArtistPair.pointee.value)) + mpd_return_pair(self.connection, mpdArtistPair) + } + + return artists + } + + func sendSearchDbTags(_ tagType: mpd_tag_type) { + mpd_search_db_tags(self.connection, tagType) + mpd_search_commit(self.connection) + while let mpdPair = mpd_recv_pair_tag(self.connection, tagType) { + print(String(cString: mpdPair.pointee.value)) + mpd_return_pair(self.connection, mpdPair) + } + } + func noIdle() { mpd_send_noidle(connection) } @@ -184,6 +263,7 @@ class MPDClient { self.delegate?.didUpdateQueuePos(mpdClient: self, song: self.status!.song) } if !mpdIdle.isEmpty { + print("Status") self.idle() } } diff --git a/Persephone/MPDClient/Models/Album.swift b/Persephone/MPDClient/Models/Album.swift new file mode 100644 index 0000000..2cb6f6c --- /dev/null +++ b/Persephone/MPDClient/Models/Album.swift @@ -0,0 +1,21 @@ +// +// Album.swift +// Persephone +// +// Created by Daniel Barber on 2019/2/09. +// Copyright © 2019 Dan Barber. All rights reserved. +// + +import Foundation + +extension MPDClient { + class Album { + let title: String + let artist: String + + init(title: String, artist: String) { + self.title = title + self.artist = artist + } + } +} diff --git a/Persephone/MPDClient/Models/Pair.swift b/Persephone/MPDClient/Models/Pair.swift new file mode 100644 index 0000000..012ba1c --- /dev/null +++ b/Persephone/MPDClient/Models/Pair.swift @@ -0,0 +1,18 @@ +// +// Pair.swift +// Persephone +// +// Created by Daniel Barber on 2019/2/09. +// Copyright © 2019 Dan Barber. All rights reserved. +// + +import Foundation +import mpdclient + +class Pair { + let mpdPair: UnsafeMutablePointer + + init(_ mpdPair: UnsafeMutablePointer) { + self.mpdPair = mpdPair + } +} diff --git a/Persephone/MPDClient/Protocols/Delegate.swift b/Persephone/MPDClient/Protocols/Delegate.swift index 1871f0e..5db155e 100644 --- a/Persephone/MPDClient/Protocols/Delegate.swift +++ b/Persephone/MPDClient/Protocols/Delegate.swift @@ -12,4 +12,5 @@ protocol MPDClientDelegate { func didUpdateState(mpdClient: MPDClient, state: MPDClient.Status.State) func didUpdateQueue(mpdClient: MPDClient, queue: [MPDClient.Song]) func didUpdateQueuePos(mpdClient: MPDClient, song: Int) + func didLoadAlbums(mpdClient: MPDClient, albums: [MPDClient.Album]) } diff --git a/Persephone/Resources/Assets.xcassets/blankAlbum.imageset/Contents.json b/Persephone/Resources/Assets.xcassets/blankAlbum.imageset/Contents.json new file mode 100644 index 0000000..f6c870a --- /dev/null +++ b/Persephone/Resources/Assets.xcassets/blankAlbum.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "blankAlbum.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Persephone/Resources/Assets.xcassets/blankAlbum.imageset/blankAlbum.pdf b/Persephone/Resources/Assets.xcassets/blankAlbum.imageset/blankAlbum.pdf new file mode 100644 index 0000000000000000000000000000000000000000..4c51e9d897f71b7c2a3e14ecf831aa552b2c5239 GIT binary patch literal 4127 zcmai1c{o&k8@5cAQI--}%1MTlm@zY!$~u@t*=4MaF&Nv3u_u(smOV>Jmh7acL}TAd zuSr4(NkjH5S(9%@_0{`+?{$6WnmOlp?%(sfujf3^{m1=4^fk0jz@*_|NHcYjI-S3C z?{#x47y&>5qOBA7^l3mw7w_gkas-ex$PkdxBDj$79<-+mmW0>96Kv@~=N%FvB zUBSMLUWRH;&CuP?H}{|9FB*6VQJko~mmjF=sD0St-Q0Foj+pl&4v5CBwnqruq?QON z=M15MWK|l_F{N3;lR3C2V^Uq^_;|o}kc#{bixVcdBQqcW5AzZNTNR+|xkEN*Zos1GfBfTXO8)cst z_kI_0JIVF*hH1+TTG=nJC3`Om*|?nTLQ$7UJIkxp@kqC8)xPZyG2!32NTHQYV7!~% z&uY_F^d`|eLw~}NNctvyDf>tNjQ!m4fQ&xYVdw6FcOwDtAH6ihdlJ1oaClEZ?gxM( zx{+x6o&df7hV;Jv2frm#?PZ;f4AygZVocEe24*wL7{DP~J3~R}#~_~to$iUdVy+vf4~r4p#0+wC_NDvZ``VaJo2&W+yi)uk*~?!F1-4}L(r zrO61vxTZPA&z1_$&SHAU9oOsnM-E*v+EQxw-!xeepP}4Ui@u8)ahWNRA@eb;h?(C$ zSW%m7!cghTqDmbt(VGVt7I!gJ4&PZi6y{=`(EM??1V_}&jEg5@rnB_%&+?m@SBWp{ z!W382I_5S_Pv!BegdEY{565d#^YZg_G+w>h&6z^6+gjz`9;;U*{Pm(q>P}`o|HAdp zCI+9A*h3>6Yp$OTF^iM71BV9nj0PQ$lVBCNlHttj=w-tYcwJGyN}IKvsKkIe!Yt;d z;?EQ~ZNUu#x7R**ERBEQ87u4mePTm|!!pusR98*ZXnbXNtepoWTS_2jB-EvWX>S}W z=^hvnA`NAdy0}L;81BwI3^0TSpW$Y{#Vmb>iJ%I4q^A6i0jvgl$LM{{$(`YCi2rNQ z`Kxm7ES8`P^?Teob_b(X!C?(LP{!d90^8Lu-21cDisJZkxzf~oxrNNx{7`E83cSoM zsE9c31l9G|;spneT`S5$=_@TCAqiHqSzX(>$NCyTuxv2~hVrB`bz}FuV|WtWUw%rM z4c*xBLBn_d9ajGO!O^o-e9D|p8~I0tzA#8#w`ojdQ*CEs?$R=26I#$vk24cvmd zFPh6#sF@zenJe@x=1lyhPs}XgS=iJAe60dw;h5-ATT^VW?OA9~DlXMjmw#N&o*N#r ze)Ya>&%-=3i4qlWb~&EgF(;Z$FUDT9C{KCF{0=*FN|>{)Uf`p!`<1E#ma=ZhGSE}5 zzUZ3Z>83rS;tRgdj}7zAU*&4>{9=3ga4k4UIOxzwtVnB}L>#+0$oVSQ;ah;1id9}mD_zlG*(<(!ZwVgz*#FfO! zhZA3%v_pFwz^Gd5m5F5nHW01^75${6;=Foig*Dat&nupNrs}8ahq`aLr@-y4Sy5_r zhPfVDBk*jX3_1~>vXnCRqBOBM-Z^?DxEHRZIpAstraV zIpwBVTBDVXO4qeD)3cLpZ`h{WM%Z5ah-@){JWmKoc)C}wP<68W4dnh|LG|wYes8KO z&sClrzz;wVqUCcpyAvl{mPJ03m)Y?Uyan|rRhTMBt$1n3(g?k{1~(MAADtLo6CJyR z7=K0?qYTPDAXsldmrPzi=639IvS_k>GGlUVk**1`hqouA$hAoCE!0We3G3AFwAmwF z97D;c_}ryr*dVq)eG8u|ua5Aq+Hm^XG^I3^D3~r-D<~jnBA9|`MGB<5rN2n0rcWT+ zDjm%^9)rxO<`a*tp~-1;uZ-;>6;X;=rDe1EC%Tk#hl-U-aRn7-4msy5I`>&6NL$FO zJ+*vlqBV#6G^T1#}OW7c^G&8pjU1t&Wvi>zNa4|~lc0)l{WL6#aE;eTrccdu3 zxa(MsdCsX;1>BQMkqgRW7UJa*8QKZjF)z*;y?Z-se6gBRN!jf@?kJ8ZM%J`tm%qng zO0;_KDK?#MzntTDd;K_vQj}R#2Hy~$jnu4^(TQm(5sV~emzitjRQX2JKy$jY6)vzX z%xMf)(U~_i{O=w^b z&-CZfs?LSzF;bLJ#oOJ>(XeIut&VKopiK~uY1;8 zlfmRM^XZLO=lWtMyqjH{hu$gmZ&hs`=hESdM@O|KhhQVik6Xc&B*-#Nh#V-Y`X!-f&SYPx?j&ki`|A? z!*EnMJsNgE<1=3xyy6*O{*?a7t>fT}r5Br5koHI)FNa$1?_u8>kaNzQrPq7QEC-*R z_pxfI9{pN79qS6Xp6UP6q16$C7aDPk9DZwX&!D{aqGQGUc&qv|b%RmziB1bnmll`( zF0(`S6{SvtDWcY0Uu`=+Mt;~GUBA!HwFUcrw4{kLojp}u=MZEQq%nJDA)bQ1 zj=ta480*z@>TREVfqar&V@yKp!?8 z-!C$-Xi{P;bLGiv^GNfo-uJx`Pjcl50lEPrlfm5?2Rjd*P`SN*c{zF|ExqEUgAOIW zbN`*&ikUuF7S&s?FGNM~ho8JaQm(H2^d_UMyll85B;|qi!ZVM;LGtD|laykIGDWLX zff<41*|4pc-a|EaZ`sUTAxu7=cO_4{S8UxHE4^8}D{x}NYEx!-r8{t7i%GxwaP1hg)PS|+{{?E1X%Cb-6ECJt)8_$`$F>a%)P#gl!_Y; zv0+sxneF8D2YOArD20^cT9sN;=u$K#dOdo`|KW1WmyYO^?y>82lHZ@KCU+HQUh&x^ zJJ`Nx{^HyXqj$x1!tV2@ZXd#YUVh|oajnWq$X3=SZn-Ufaz<}WFF7+t z#pg2nw?$uZSNrji)zGEQ|Mkyw7rJAbC%eV83RDqZ%h=p)f_LEK~ueh?IjN&7n{U+V6Md zA4$@N0Xr`oZ6Mr9gf<_dXmcQmmVpM{$?tH|z;i^qKi_|+@9TlL2h(Z<2SfjR0dhzr z90}M1KQXu*?QLflaQlTp5%RQB(XZba9IikcQ~r)Yp|Z5k`Ui$U(u(#E42k>~zJH06 zgV9v*_p@*~{D1i12sxUn{t<_S{R^f@lhLm@k_VRHg7?^&x{V0_wEEHJB10mPCK|dh zXob{vvnSFN^kc0-)6i*o1)LlVW)HQKRe;Ij;4pav9* + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Persephone/Resources/Base.lproj/Main.storyboard b/Persephone/Views/Base.lproj/Main.storyboard similarity index 98% rename from Persephone/Resources/Base.lproj/Main.storyboard rename to Persephone/Views/Base.lproj/Main.storyboard index 634fbc1..2d1dc43 100644 --- a/Persephone/Resources/Base.lproj/Main.storyboard +++ b/Persephone/Views/Base.lproj/Main.storyboard @@ -908,47 +908,55 @@ - + - + - + - + - + - - + + + + + + + - - - - - + + + + + + + diff --git a/Resources/export/blankAlbum.pdf b/Resources/export/blankAlbum.pdf new file mode 100644 index 0000000000000000000000000000000000000000..4c51e9d897f71b7c2a3e14ecf831aa552b2c5239 GIT binary patch literal 4127 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