// // ViewController.swift // TagTunes // // Created by Kim Wittenburg on 28.08.15. // Copyright © 2015 Kim Wittenburg. All rights reserved. // import SearchAPI import AppKitPlus class MainViewController: NSViewController, SearchDelegate, LookupQueueDelegate { // MARK: Types /// This struct contains *magic numbers* like references to storyboard /// identifiers. private struct Constants { /// The identifier of the segue that embeds the /// `OutlineContentViewController` in its parent view. static let OutlineContentViewControllerEmbedSegueIdentifier = "embedOutlineContentViewController" /// The identifier of the segue that embeds the `LookupViewController` in /// its parent view. static let LookupViewControllerEmbedSegueIdentifier = "embedLookupViewController" /// The space between the lookup panel and its superview when it's /// visible. static let LookupPanelExpandedBottomSpace: CGFloat = -4 /// The space between the superview and the lookup panel, if it's not /// visible. static let LookupPanelCollapsedBottomSpace: CGFloat = -82 /// The delay between the lookup controller completing and it being /// hidden. static let LookupPanelHideDelay: NSTimeInterval = 2 /// This constant is used for KVO observations on a `Preferences` /// instance. static var PreferencesKVOContext = "PreferencesKVOContext" } // MARK: Properties /// The view controller that manages the display of the content. internal var contentViewController: ContentViewController! // MARK: View Life Cycle override func viewDidLoad() { super.viewDidLoad() Preferences.sharedPreferences.addObserver(self, forKeyPath: "useCensoredNames", options: [], context: &Constants.PreferencesKVOContext) Preferences.sharedPreferences.addObserver(self, forKeyPath: "caseSensitive", options: [], context: &Constants.PreferencesKVOContext) } override func prepareForSegue(segue: NSStoryboardSegue, sender: AnyObject?) { if segue.identifier == Constants.OutlineContentViewControllerEmbedSegueIdentifier { contentViewController = segue.destinationController as? ContentViewController } } deinit { Preferences.sharedPreferences.removeObserver(self, forKeyPath: "useCensoredNames") Preferences.sharedPreferences.removeObserver(self, forKeyPath: "caseSensitiv") } // MARK: Notifications override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer) { if context == &Constants.PreferencesKVOContext { contentViewController.updateAllItems() } } // MARK: Searching func searchController(searchController: SearchController, didSelectSearchResult searchResult: TagTunesItem) { contentViewController.addItem(searchResult) } // MARK: Lookup Panel func lookupQueue(lookupQueue: LookupQueue, willBeginLookupForTracks tracks: [TagTunesTrack]) { } func lookupQueue(lookupQueue: LookupQueue, completedLookupForTracks tracks: [TagTunesTrack]) { var newAlbumItems = [AlbumItem]() for track in tracks { if case .Found(let result) = track.lookupState { if let album = result.collection.map(contentViewController.itemForEntity) as? AlbumItem { album.addAssociatedTrack(track, forChildEntity: result) } else if let collection = result.collection { let album = AlbumItem(entity: collection) album.addAssociatedTrack(track, forChildEntity: result) contentViewController.addItem(album) newAlbumItems.append(album) } else { fatalError("Lookup returned a song without album.") } } else { NSApp.unsortedTracksController.addTracks([track]) // TODO: Show failure reason } } for album in newAlbumItems { album.beginLoadingChildren() } } func lookupQueueDidFinishLookup(lookupQueue: LookupQueue) { } // MARK: Saving @IBAction func save(sender: AnyObject?) { var items = [AnyObject]() for item in contentViewController.selectedItems { if let group = item as? TagTunesGroupItem { for child in group.children { for track in child.associatedTracks { saveQueue.addOperation(SaveOperation(track: track, entity: child)) } } items.append(group) } else if let entity = item as? TagTunesEntityItem { for track in entity.associatedTracks { saveQueue.addOperation(SaveOperation(track: track, entity: entity)) } items.append(entity) } else if let track = item as? TagTunesTrack { if let entity = track.entity { saveQueue.addOperation(SaveOperation(track: track, entity: entity)) items.append(track) } } } contentViewController.removeItems(items) } // TODO: Use Operations for this /// Exports the artwork of the first selected item to a file. This methods /// presents a `NSSavePanel` so the user can specify where to save the /// artwork. @IBAction func exportArtwork(sender: AnyObject?) { let item: TagTunesItem let object = contentViewController.selectedItems.first if let track = object as? TagTunesTrack { if let theItem = track.entity { item = theItem } else { return } } else if let theItem = object as? TagTunesItem { item = theItem } else { return } var album: Album switch item { case let songItem as SongItem: album = songItem.album case let albumItem as AlbumItem: album = albumItem.album default: return } let savePanel = NSSavePanel() savePanel.allowedFileTypes = ["jpg"] savePanel.nameFieldStringValue = Preferences.sharedPreferences.useCensoredNames ? album.censoredName : album.name savePanel.beginSheetModalForWindow(view.window!) { response -> Void in if response == NSModalResponseOK { let bitmapRep = album.artwork.optimalArtworkImageForSize(CGFloat.max)?.representations[0] as? NSBitmapImageRep guard bitmapRep != nil else { let alert = NSAlert() alert.messageText = NSLocalizedString("The artwork could not be saved.", comment: "Error message informing the user that an artwork could not be saved to a file.") alert.informativeText = NSLocalizedString("Please check your network connection. Also make sure that you have write permissions to the destination you selected.", comment: "Informative text for the 'The artwork could not be saved.' error.") alert.addButtonWithTitle(NSLocalizedString("OK", comment: "Button title")) alert.beginSheetModalForWindow(self.view.window!, completionHandler: nil) return } let data = bitmapRep!.representationUsingType(.NSJPEGFileType, properties: [:]) data?.writeToURL(savePanel.URL!, atomically: false) } } } // MARK: Other Actions /// Removes the selected items from the outline view. @IBAction internal func delete(sender: AnyObject?) { self.contentViewController.removeSelectedItems() } /// Shows the currently selected item in the iTunes store. @IBAction internal func showInITunesStore(sender: AnyObject?) { if let item = contentViewController.clickedItems.first as? TagTunesItem { NSWorkspace.sharedWorkspace().openURL(item.entity.viewURL) } } } // MARK: - User Interface Validations extension MainViewController: NSUserInterfaceValidations { func validateUserInterfaceItem(anItem: NSValidatedUserInterfaceItem) -> Bool { if anItem.action() == #selector(MainViewController.save(_:)) { return canSave } else if anItem.action() == #selector(MainViewController.exportArtwork(_:)) { return canExportArtworks } else if anItem.action() == #selector(MainViewController.delete(_:)) { return canDelete } else if anItem.action() == #selector(MainViewController.showInITunesStore(_:)) { return canShowInITunesStore } return false } /// Returns whether it is currently valid to invoke `save(_:)`. private var canSave: Bool { return !contentViewController.selectedItems.isEmpty } /// Returns whether it is currently valid to invoke `exportArtworks(_:)`. private var canExportArtworks: Bool { return contentViewController.selectedItems.count == 1 } /// Returns whether it is currently valid to invoke /// `delete(_:)`. private var canDelete: Bool { return !contentViewController.selectedItems.isEmpty } private var canShowInITunesStore: Bool { return contentViewController.clickedItems.count == 1 && contentViewController.clickedItems.first is TagTunesItem } }