Archived
1

Added 'Save Artwork' toolbar button

This commit is contained in:
Kim Wittenburg
2015-09-15 13:43:18 +02:00
committed by Kim Wittenburg
parent 257a7811d6
commit 3a400037ff
9 changed files with 335 additions and 131 deletions

View File

@@ -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)