Added 'Save Artwork' toolbar button
This commit is contained in:
committed by
Kim Wittenburg
parent
257a7811d6
commit
3a400037ff
@@ -148,6 +148,71 @@ internal class MainViewController: NSViewController {
|
||||
return contents
|
||||
}
|
||||
|
||||
/// Returns all selected items. This property removes duplicate items from
|
||||
/// the returned array (for example a track is not included if the whole
|
||||
/// album the track belongs to is included itself).
|
||||
///
|
||||
/// This value is not cached. If you need to access this value often, you
|
||||
/// should consider caching it yourself in a local variable. The order in
|
||||
/// which the selected objects occur in the returned array is random.
|
||||
///
|
||||
/// - returns: An array of `SearchResult`s, `Album`s, `Track`s and
|
||||
/// `iTunesTrack`s.
|
||||
private var selectedItems: [AnyObject] {
|
||||
var selectedSearchResults = Set<SearchResult>()
|
||||
var selectedAlbums = Set<Album>()
|
||||
var selectedTracks = Set<Track>()
|
||||
var selectedITunesTracks = Set<iTunesTrack>()
|
||||
|
||||
for row in outlineView.selectedRowIndexes {
|
||||
let item = outlineView.itemAtRow(row)
|
||||
if let searchResult = item as? SearchResult {
|
||||
selectedSearchResults.insert(searchResult)
|
||||
} else if let album = item as? Album {
|
||||
selectedAlbums.insert(album)
|
||||
} else if let track = item as? Track {
|
||||
selectedTracks.insert(track)
|
||||
} else if let track = item as? iTunesTrack {
|
||||
selectedITunesTracks.insert(track)
|
||||
}
|
||||
}
|
||||
|
||||
for album in selectedAlbums {
|
||||
for track in album.tracks {
|
||||
for iTunesTrack in track.associatedTracks {
|
||||
selectedITunesTracks.remove(iTunesTrack)
|
||||
}
|
||||
selectedTracks.remove(track)
|
||||
}
|
||||
}
|
||||
|
||||
for track in selectedTracks {
|
||||
for iTunesTrack in track.associatedTracks {
|
||||
selectedITunesTracks.remove(iTunesTrack)
|
||||
}
|
||||
}
|
||||
|
||||
var selectedItems = [AnyObject]()
|
||||
selectedItems.appendContentsOf(Array(selectedSearchResults) as [AnyObject])
|
||||
selectedItems.appendContentsOf(Array(selectedAlbums) as [AnyObject])
|
||||
selectedItems.appendContentsOf(Array(selectedTracks) as [AnyObject])
|
||||
selectedItems.appendContentsOf(Array(selectedITunesTracks) as [AnyObject])
|
||||
return selectedItems
|
||||
}
|
||||
|
||||
internal func parentForTrack(track: iTunesTrack) -> Track? {
|
||||
return outlineView.parentForItem(track) as? Track
|
||||
}
|
||||
|
||||
internal func containsAlbumForSearchResult(searchResult: SearchResult) -> Bool {
|
||||
for album in albumCollection {
|
||||
if album.id == searchResult.id {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/// Returns the section of the specified row or `nil` if the row is not a
|
||||
/// valid row.
|
||||
internal func sectionOfRow(row: Int) -> Section? {
|
||||
@@ -178,29 +243,6 @@ internal class MainViewController: NSViewController {
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: Can this algorithm be improved?
|
||||
/// Returns the section the specified item resides in or `nil` if the item is
|
||||
/// not part of the outline view's contents.
|
||||
internal func sectionOfItem(item: AnyObject) -> Section? {
|
||||
if let album = item as? Album where albumCollection.contains(album) {
|
||||
return .Albums
|
||||
} else if let track = item as? Track where albumCollection.contains(track.album) {
|
||||
return .Albums
|
||||
} else if let track = item as? iTunesTrack {
|
||||
if let parentTrack = outlineView.parentForItem(track) as? Track where albumCollection.contains(parentTrack.album) {
|
||||
return .Albums
|
||||
} else {
|
||||
return unsortedTracks.contains(track) ? .UnsortedTracks : nil
|
||||
}
|
||||
} else if item === OutlineViewConstants.Items.loadingItem || item === OutlineViewConstants.Items.noResultsItem || item is NSError {
|
||||
return .SearchResults
|
||||
} else if let string = item as? String {
|
||||
return Section(rawValue: string)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Searching
|
||||
|
||||
/// Starts a search for the specified search term. Calling this method
|
||||
@@ -262,50 +304,89 @@ internal class MainViewController: NSViewController {
|
||||
searchResults.removeAll()
|
||||
showsSearch = false
|
||||
}
|
||||
var albumAlreadyPresent = false
|
||||
for album in albumCollection {
|
||||
if album == searchResult {
|
||||
albumAlreadyPresent = true
|
||||
}
|
||||
}
|
||||
if !albumAlreadyPresent {
|
||||
if !containsAlbumForSearchResult(searchResult) {
|
||||
let album = Album(searchResult: searchResult)
|
||||
albumCollection.addAlbum(album, beginLoading: true)
|
||||
}
|
||||
outlineView.reloadData()
|
||||
}
|
||||
|
||||
// MARK: Albums
|
||||
// MARK: Saving
|
||||
|
||||
private func saveTracks(tracks: [Track: [iTunesTrack]]) {
|
||||
let numberOfTracks = tracks.reduce(0) { (count: Int, element: (key: Track, value: [iTunesTrack])) -> Int in
|
||||
return count + element.value.count
|
||||
}
|
||||
private func saveItems(items: [AnyObject]) {
|
||||
let numberOfTracks = numberOfTracksInItems(items)
|
||||
let progress = NSProgress(totalUnitCount: Int64(numberOfTracks))
|
||||
NSProgress.currentProgress()?.localizedDescription = NSLocalizedString("Saving tracks…", comment: "Alert message indicating that the selected tracks are currently being saved")
|
||||
NSProgress.currentProgress()?.localizedAdditionalDescription = progress.localizedAdditionalDescription
|
||||
for (parentTrack, targetTracks) in tracks {
|
||||
for track in targetTracks {
|
||||
|
||||
let save: (parentTrack: Track, tracks: [iTunesTrack]) -> Bool = { parentTrack, tracks in
|
||||
for track in tracks {
|
||||
if progress.cancelled {
|
||||
return
|
||||
return false
|
||||
}
|
||||
parentTrack.saveToTrack(track)
|
||||
++progress.completedUnitCount
|
||||
NSProgress.currentProgress()?.localizedAdditionalDescription = progress.localizedAdditionalDescription
|
||||
}
|
||||
return !progress.cancelled
|
||||
}
|
||||
|
||||
for item in items {
|
||||
if let album = item as? Album {
|
||||
for parentTrack in album.tracks {
|
||||
if !save(parentTrack: parentTrack, tracks: parentTrack.associatedTracks) {
|
||||
return
|
||||
}
|
||||
}
|
||||
} else if let track = item as? Track {
|
||||
if !save(parentTrack: track, tracks: track.associatedTracks) {
|
||||
return
|
||||
}
|
||||
} else if let track = item as? iTunesTrack {
|
||||
if let parentTrack = parentForTrack(track) {
|
||||
if !save(parentTrack: parentTrack, tracks: [track]) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func saveArtworks(tracks: [Track: [iTunesTrack]]) {
|
||||
private func numberOfTracksInItems(items: [AnyObject]) -> Int {
|
||||
return items.reduce(0) { (count: Int, item: AnyObject) -> Int in
|
||||
if let album = item as? Album {
|
||||
return count + album.tracks.reduce(0) { $0 + $1.associatedTracks.count }
|
||||
} else if let track = item as? Track {
|
||||
return count + track.associatedTracks.count
|
||||
} else if let track = item as? iTunesTrack {
|
||||
return parentForTrack(track) == nil ? count : count + 1
|
||||
} else {
|
||||
return count
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func saveArtworksForItems(items: [AnyObject]) {
|
||||
var albums = Set<Album>()
|
||||
for (track, _) in tracks {
|
||||
albums.insert(track.album)
|
||||
for item in items {
|
||||
if let searchResult = item as? SearchResult {
|
||||
albums.insert(Album(searchResult: searchResult))
|
||||
} else if let album = item as? Album {
|
||||
albums.insert(album)
|
||||
} else if let track = item as? Track {
|
||||
albums.insert(track.album)
|
||||
} else if let track = item as? iTunesTrack {
|
||||
if let parentTrack = parentForTrack(track) {
|
||||
albums.insert(parentTrack.album)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let progress = NSProgress(totalUnitCount: Int64(albums.count))
|
||||
var errorCount = 0
|
||||
NSProgress.currentProgress()?.localizedDescription = NSLocalizedString("Saving artworks…", comment: "Alert message indicating that the artworks for the selected tracks are currently being saved")
|
||||
NSProgress.currentProgress()?.localizedAdditionalDescription = progress.localizedAdditionalDescription
|
||||
NSThread.sleepForTimeInterval(2)
|
||||
for album in albums {
|
||||
if progress.cancelled {
|
||||
return
|
||||
@@ -383,31 +464,12 @@ internal class MainViewController: NSViewController {
|
||||
/// Saves the selected items to iTunes. The saving process will be reported
|
||||
/// to the user in a progress sheet.
|
||||
@IBAction internal func performSave(sender: AnyObject?) {
|
||||
var itemsToBeSaved = [Track: [iTunesTrack]]()
|
||||
for row in outlineView.selectedRowIndexes where sectionOfRow(row) == .Albums {
|
||||
let item = outlineView.itemAtRow(row)
|
||||
if let album = item as? Album {
|
||||
for track in album.tracks where !track.associatedTracks.isEmpty {
|
||||
itemsToBeSaved[track] = track.associatedTracks
|
||||
}
|
||||
} else if let track = item as? Track {
|
||||
itemsToBeSaved[track] = track.associatedTracks
|
||||
} else if let track = item as? iTunesTrack {
|
||||
if let parentTrack = outlineView.parentForItem(track) as? Track {
|
||||
if itemsToBeSaved[parentTrack] != nil {
|
||||
itemsToBeSaved[parentTrack]?.append(track)
|
||||
} else {
|
||||
itemsToBeSaved[parentTrack] = [track]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
guard !itemsToBeSaved.isEmpty else {
|
||||
return
|
||||
}
|
||||
let selectedItems = self.selectedItems.filter { !($0 is SearchResult) }
|
||||
let progress = NSProgress(totalUnitCount: 100)
|
||||
progress.beginProgressSheetModalForWindow(self.view.window!) {
|
||||
reponse in
|
||||
let progressAlert = ProgressAlert(progress: progress)
|
||||
progressAlert.dismissesWhenCancelled = false
|
||||
progressAlert.beginSheetModalForWindow(self.view.window!) {
|
||||
response in
|
||||
self.outlineView.reloadData()
|
||||
}
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
|
||||
@@ -416,18 +478,85 @@ internal class MainViewController: NSViewController {
|
||||
} else {
|
||||
progress.becomeCurrentWithPendingUnitCount(100)
|
||||
}
|
||||
self.saveTracks(itemsToBeSaved)
|
||||
self.saveItems(selectedItems)
|
||||
progress.resignCurrent()
|
||||
if progress.cancelled {
|
||||
progressAlert.dismissWithResponse(NSModalResponseAbort)
|
||||
return
|
||||
}
|
||||
if Preferences.sharedPreferences.saveArtwork {
|
||||
progress.becomeCurrentWithPendingUnitCount(10)
|
||||
self.saveArtworks(itemsToBeSaved)
|
||||
self.saveArtworksForItems(selectedItems)
|
||||
progress.resignCurrent()
|
||||
if progress.cancelled {
|
||||
progressAlert.dismissWithResponse(NSModalResponseAbort)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Saves the artworks of the selected items to the folder specified in the
|
||||
/// preferences. If there is no folder specified this method prompts the user
|
||||
/// to select one.
|
||||
@IBAction internal func saveArtworks(sender: AnyObject?) {
|
||||
if Preferences.sharedPreferences.artworkTarget == nil {
|
||||
let alert = NSAlert()
|
||||
alert.messageText = NSLocalizedString("There is no folder set to save artworks to.", comment: "Error message informing the user that there is no directory set in the preferences that can be used to save artworks to.")
|
||||
alert.informativeText = NSLocalizedString("You must select a folder to save artworks to. The folder can be changed in the preferences.", comment: "Informative text for the 'no folder to save artworks to' error.")
|
||||
alert.addButtonWithTitle(NSLocalizedString("Use Downloads Folder", comment: "Button title offering the user to automatically use the downloads directory instead of manually choosing a directory."))
|
||||
alert.addButtonWithTitle(NSLocalizedString("Chose Folder…", comment: "Button title prompting the user to choose a folder."))
|
||||
alert.addButtonWithTitle(NSLocalizedString("Cancel", comment: "Button title"))
|
||||
alert.alertStyle = .WarningAlertStyle
|
||||
alert.beginSheetModalForWindow(view.window!) { response in
|
||||
switch response {
|
||||
case NSAlertFirstButtonReturn:
|
||||
let downloadsFolder = NSURL.fileURLWithPath(NSFileManager.defaultManager().URLsForDirectory(.DownloadsDirectory, inDomains: .UserDomainMask)[0].filePathURL!.path!, isDirectory: true)
|
||||
Preferences.sharedPreferences.artworkTarget = downloadsFolder
|
||||
self.performSaveArtworks()
|
||||
case NSAlertSecondButtonReturn:
|
||||
let openPanel = NSOpenPanel()
|
||||
openPanel.canChooseDirectories = true
|
||||
openPanel.canChooseFiles = false
|
||||
openPanel.canCreateDirectories = true
|
||||
openPanel.prompt = NSLocalizedString("Choose…", comment: "Button title in an open dialog prompting the user to choose a directory")
|
||||
openPanel.beginSheetModalForWindow(self.view.window!) { response in
|
||||
if response == NSFileHandlingPanelOKButton {
|
||||
Preferences.sharedPreferences.artworkTarget = openPanel.URL!.filePathURL!
|
||||
self.performSaveArtworks()
|
||||
}
|
||||
}
|
||||
case NSAlertThirdButtonReturn:
|
||||
fallthrough
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
performSaveArtworks()
|
||||
}
|
||||
}
|
||||
|
||||
/// Actually performs the action for `saveArtworks`.
|
||||
private func performSaveArtworks() {
|
||||
let progress = NSProgress(totalUnitCount: 100)
|
||||
let progressAlert = ProgressAlert(progress: progress)
|
||||
progressAlert.dismissesWhenCancelled = false
|
||||
progressAlert.beginSheetModalForWindow(self.view.window!) {
|
||||
response in
|
||||
self.outlineView.reloadData()
|
||||
}
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
|
||||
progress.becomeCurrentWithPendingUnitCount(100)
|
||||
self.saveArtworksForItems(self.selectedItems)
|
||||
progress.resignCurrent()
|
||||
if progress.cancelled {
|
||||
progressAlert.dismissWithResponse(NSModalResponseAbort)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes the selected items from the outline view.
|
||||
@IBAction internal func delete(sender: AnyObject?) {
|
||||
@IBAction internal func removeSelectedItems(sender: AnyObject?) {
|
||||
let items = outlineView.selectedRowIndexes.map { ($0, outlineView.itemAtRow($0)) }
|
||||
for (row, item) in items {
|
||||
if sectionOfRow(row)! != .SearchResults {
|
||||
@@ -436,7 +565,7 @@ internal class MainViewController: NSViewController {
|
||||
} else if let track = item as? Track {
|
||||
track.associatedTracks = []
|
||||
} else if let track = item as? iTunesTrack {
|
||||
if let parentTrack = outlineView.parentForItem(track) as? Track {
|
||||
if let parentTrack = parentForTrack(track) {
|
||||
parentTrack.associatedTracks.removeElement(track)
|
||||
} else {
|
||||
unsortedTracks.removeElement(track)
|
||||
@@ -522,19 +651,54 @@ extension MainViewController: NSUserInterfaceValidations {
|
||||
|
||||
internal func validateUserInterfaceItem(anItem: NSValidatedUserInterfaceItem) -> Bool {
|
||||
if anItem.action() == "performSave:" {
|
||||
for row in outlineView.selectedRowIndexes {
|
||||
return sectionOfRow(row) == .Albums
|
||||
}
|
||||
return canSave()
|
||||
} else if anItem.action() == "saveArtworks:" {
|
||||
return canSaveArtworks()
|
||||
} else if anItem.action() == "addITunesSelection:" {
|
||||
guard iTunes.running else {
|
||||
return false
|
||||
}
|
||||
return !(iTunes.selection.get() as! [AnyObject]).isEmpty
|
||||
} else if anItem.action() == "delete:" {
|
||||
for row in outlineView.selectedRowIndexes {
|
||||
if sectionOfRow(row) != .SearchResults {
|
||||
return canAddITunesSelection()
|
||||
} else if anItem.action() == "removeSelectedItems:" {
|
||||
return canRemoveSelectedItems()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private func canSave() -> Bool {
|
||||
for row in outlineView.selectedRowIndexes where sectionOfRow(row) == .Albums {
|
||||
let item = outlineView.itemAtRow(row)
|
||||
if let album = item as? Album {
|
||||
if !album.saved {
|
||||
return true
|
||||
}
|
||||
} else if let track = item as? Track {
|
||||
if !track.saved {
|
||||
return true
|
||||
}
|
||||
} else if let track = item as? iTunesTrack {
|
||||
if parentForTrack(track)?.saved == false {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private func canSaveArtworks() -> Bool {
|
||||
for row in outlineView.selectedRowIndexes {
|
||||
if sectionOfRow(row) != .UnsortedTracks {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private func canAddITunesSelection() -> Bool {
|
||||
return iTunes.running && !(iTunes.selection.get() as! [AnyObject]).isEmpty
|
||||
}
|
||||
|
||||
private func canRemoveSelectedItems() -> Bool {
|
||||
for row in outlineView.selectedRowIndexes {
|
||||
if sectionOfRow(row)! != .SearchResults {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
@@ -664,6 +828,18 @@ extension MainViewController: NSOutlineViewDataSource, NSOutlineViewDelegate {
|
||||
view?.setupForError(error, errorMessage: NSLocalizedString("Failed to load results", comment: "Error message informing the user that an error occured during searching."))
|
||||
return view
|
||||
}
|
||||
if let searchResult = item as? SearchResult {
|
||||
var view = outlineView.makeViewWithIdentifier(OutlineViewConstants.ViewIdentifiers.albumTableCellViewIdentifier, owner: nil) as? AlbumTableCellView
|
||||
if view == nil {
|
||||
view = AlbumTableCellView()
|
||||
view?.identifier = OutlineViewConstants.ViewIdentifiers.albumTableCellViewIdentifier
|
||||
}
|
||||
view?.button.target = self
|
||||
view?.button.action = "selectSearchResult:"
|
||||
let selectable = !containsAlbumForSearchResult(searchResult)
|
||||
view?.setupForSearchResult(searchResult, selectable: selectable)
|
||||
return view
|
||||
}
|
||||
if let album = item as? Album {
|
||||
var view = outlineView.makeViewWithIdentifier(OutlineViewConstants.ViewIdentifiers.albumTableCellViewIdentifier, owner: nil) as? AlbumTableCellView
|
||||
if view == nil {
|
||||
@@ -675,18 +851,6 @@ extension MainViewController: NSOutlineViewDataSource, NSOutlineViewDelegate {
|
||||
view?.errorButton?.action = "showErrorDetails:"
|
||||
return view
|
||||
}
|
||||
if let searchResult = item as? SearchResult {
|
||||
var view = outlineView.makeViewWithIdentifier(OutlineViewConstants.ViewIdentifiers.albumTableCellViewIdentifier, owner: nil) as? AlbumTableCellView
|
||||
if view == nil {
|
||||
view = AlbumTableCellView()
|
||||
view?.identifier = OutlineViewConstants.ViewIdentifiers.albumTableCellViewIdentifier
|
||||
}
|
||||
view?.button.target = self
|
||||
view?.button.action = "selectSearchResult:"
|
||||
let selectable = albumCollection.filter { $0.id == searchResult.id }.isEmpty
|
||||
view?.setupForSearchResult(searchResult, selectable: selectable)
|
||||
return view
|
||||
}
|
||||
if let track = item as? Track {
|
||||
var view = outlineView.makeViewWithIdentifier(OutlineViewConstants.ViewIdentifiers.trackTableCellViewIdentifier, owner: nil) as? TrackTableCellView
|
||||
if view == nil {
|
||||
@@ -775,7 +939,7 @@ extension MainViewController: NSOutlineViewDataSource, NSOutlineViewDelegate {
|
||||
track.associatedTracks.removeAll()
|
||||
} else if let track = item as? iTunesTrack {
|
||||
draggedTracks.insert(track)
|
||||
if let parentTrack = outlineView.parentForItem(track) as? Track {
|
||||
if let parentTrack = parentForTrack(track) {
|
||||
parentTrack.associatedTracks.removeElement(track)
|
||||
} else {
|
||||
unsortedTracks.removeElement(track)
|
||||
|
||||
Reference in New Issue
Block a user