From 45f664cb10c9ee79f67ca6f4d068826a265ea9c9 Mon Sep 17 00:00:00 2001 From: Kim Wittenburg Date: Fri, 4 Sep 2015 17:23:45 +0200 Subject: [PATCH] Added 'Tags' preferences with saving behaviors for each tag --- TagTunes.xcodeproj/project.pbxproj | 12 +- TagTunes/Base.lproj/Main.storyboard | 169 ++++++++++++++++++++ TagTunes/Preference Controllers.swift | 119 ++++++++++++++ TagTunes/Preferences.swift | 94 +++++++++-- TagTunes/PreferencesTabViewController.swift | 46 ------ TagTunes/Track.swift | 94 ++++++++--- 6 files changed, 453 insertions(+), 81 deletions(-) create mode 100644 TagTunes/Preference Controllers.swift delete mode 100644 TagTunes/PreferencesTabViewController.swift diff --git a/TagTunes.xcodeproj/project.pbxproj b/TagTunes.xcodeproj/project.pbxproj index c8eb7e7..fc907bb 100644 --- a/TagTunes.xcodeproj/project.pbxproj +++ b/TagTunes.xcodeproj/project.pbxproj @@ -7,7 +7,7 @@ objects = { /* Begin PBXBuildFile section */ - 3B285DB81B9128C100F0A2F1 /* PreferencesTabViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B285DB71B9128C100F0A2F1 /* PreferencesTabViewController.swift */; }; + 3B285DB81B9128C100F0A2F1 /* Preference Controllers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B285DB71B9128C100F0A2F1 /* Preference Controllers.swift */; }; 3B285DBF1B912AB700F0A2F1 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B285DBE1B912AB700F0A2F1 /* Preferences.swift */; }; 3B489DBF1B90B055002B7EB3 /* TrackTableCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B489DBD1B90B055002B7EB3 /* TrackTableCellView.swift */; }; 3B489DC31B90B116002B7EB3 /* Artwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B489DC01B90B116002B7EB3 /* Artwork.swift */; }; @@ -44,7 +44,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - 3B285DB71B9128C100F0A2F1 /* PreferencesTabViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencesTabViewController.swift; sourceTree = ""; }; + 3B285DB71B9128C100F0A2F1 /* Preference Controllers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Preference Controllers.swift"; sourceTree = ""; }; 3B285DBE1B912AB700F0A2F1 /* Preferences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = ""; }; 3B489DBD1B90B055002B7EB3 /* TrackTableCellView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = TrackTableCellView.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 3B489DC01B90B116002B7EB3 /* Artwork.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Artwork.swift; sourceTree = ""; }; @@ -178,7 +178,7 @@ children = ( 3B76C7721B909B280025D550 /* AppDelegate.swift */, 3B76C7741B909B280025D550 /* MainViewController.swift */, - 3B285DB71B9128C100F0A2F1 /* PreferencesTabViewController.swift */, + 3B285DB71B9128C100F0A2F1 /* Preference Controllers.swift */, ); name = Controller; sourceTree = ""; @@ -341,7 +341,7 @@ 3B489DC41B90B116002B7EB3 /* Album.swift in Sources */, 3B489DC51B90B116002B7EB3 /* Track.swift in Sources */, 3B489DC71B90B38C002B7EB3 /* iTunes.swift in Sources */, - 3B285DB81B9128C100F0A2F1 /* PreferencesTabViewController.swift in Sources */, + 3B285DB81B9128C100F0A2F1 /* Preference Controllers.swift in Sources */, 3B489DCB1B90B3E3002B7EB3 /* iTunes.m in Sources */, 3B489DD61B90E0D8002B7EB3 /* AlbumTableCellView.swift in Sources */, ); @@ -476,12 +476,14 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; INFOPLIST_FILE = TagTunes/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = wittenburg.kim.TagTunes; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "TagTunes/TagTunes-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; }; @@ -491,11 +493,13 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; INFOPLIST_FILE = TagTunes/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = wittenburg.kim.TagTunes; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "TagTunes/TagTunes-Bridging-Header.h"; + VERSIONING_SYSTEM = "apple-generic"; }; name = Release; }; diff --git a/TagTunes/Base.lproj/Main.storyboard b/TagTunes/Base.lproj/Main.storyboard index 4c88da7..0100286 100644 --- a/TagTunes/Base.lproj/Main.storyboard +++ b/TagTunes/Base.lproj/Main.storyboard @@ -725,6 +725,7 @@ CA + @@ -738,6 +739,7 @@ CA + @@ -838,6 +840,172 @@ CA + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -925,6 +1093,7 @@ CA + diff --git a/TagTunes/Preference Controllers.swift b/TagTunes/Preference Controllers.swift new file mode 100644 index 0000000..ef75003 --- /dev/null +++ b/TagTunes/Preference Controllers.swift @@ -0,0 +1,119 @@ +// +// PreferencesTabViewController.swift +// Harmony +// +// Created by Kim Wittenburg on 26.01.15. +// Copyright (c) 2015 Das Code Kollektiv. All rights reserved. +// + +import Cocoa + +internal class GeneralPreferencesViewController: NSViewController { + + // MARK: IBOutlets + + @IBOutlet internal weak var artworkPathControl: NSPathControl! + @IBOutlet internal weak var chooseArtworkButton: NSButton! + + override internal func viewDidLoad() { + super.viewDidLoad() + artworkPathControl.URL = Preferences.sharedPreferences.artworkTarget + saveArtworkStateChanged(self) + } + + @IBAction internal func saveArtworkStateChanged(sender: AnyObject) { + artworkPathControl.enabled = Preferences.sharedPreferences.saveArtwork + chooseArtworkButton.enabled = Preferences.sharedPreferences.saveArtwork + } + + @IBAction internal func chooseArtworkPath(sender: AnyObject) { + let openPanel = NSOpenPanel() + openPanel.canChooseDirectories = true + openPanel.canChooseFiles = false + openPanel.canCreateDirectories = true + openPanel.prompt = NSLocalizedString("Choose…", comment: "Button title prompting the user to choose a directory") + openPanel.beginSheetModalForWindow(view.window!) { + result in + if result == NSModalResponseOK { + Preferences.sharedPreferences.artworkTarget = openPanel.URL!.filePathURL! + self.artworkPathControl.URL = openPanel.URL + } + } + } + +} + +internal class TagsPreferencesViewController: NSViewController, NSTableViewDataSource, NSTableViewDelegate, NSMenuDelegate { + + // MARK: Types + + private struct TableViewConstants { + + static let textTableCellViewIdentifier = "textCell" + + static let popupTableCellViewIdentifier = "popupCell" + + static let tagTableColumnIdentifier = "tagColumn" + + static let savingBehaviorTableColumnIdentifier = "savingBehaviorColumn" + + } + + // MARK: Properties + + @IBOutlet weak var tableView: NSTableView! + + // MARK: Table View + + internal func numberOfRowsInTableView(tableView: NSTableView) -> Int { + return Track.Tag.allTags.count + } + + func tableView(tableView: NSTableView, viewForTableColumn tableColumn: NSTableColumn?, row: Int) -> NSView? { + let tag = Track.Tag.allTags[row] + if tableColumn?.identifier == TableViewConstants.tagTableColumnIdentifier { + let view = tableView.makeViewWithIdentifier(TableViewConstants.textTableCellViewIdentifier, owner: nil) as? NSTableCellView + view?.textField?.stringValue = tag.localizedName + return view + } else if tableColumn?.identifier == TableViewConstants.savingBehaviorTableColumnIdentifier { + let popupButton = tableView.makeViewWithIdentifier(TableViewConstants.popupTableCellViewIdentifier, owner: nil) as? NSPopUpButton + popupButton?.removeAllItems() + if tag.isReturnedBySearchAPI { + popupButton?.addItemWithTitle(NSLocalizedString("Save", comment: "Menu item title for a tag that is going to be saved")) + } + popupButton?.addItemsWithTitles([ + NSLocalizedString("Clear", comment: "Menu item title for a tag that is going to be cleared"), + NSLocalizedString("Ignore", comment: "Menu item title for a tag that is not going to be saved")]) + var selectedIndex: Int + switch Preferences.sharedPreferences.tagSavingBehaviors[tag]! { + case .Save: + selectedIndex = 0 + case .Clear: + selectedIndex = tag.isReturnedBySearchAPI ? 1 : 0 + case .Ignore: + selectedIndex = tag.isReturnedBySearchAPI ? 2 : 1 + } + popupButton?.selectItemAtIndex(selectedIndex) + return popupButton + } + return nil + } + + @IBAction private func savingBehaviorChanged(sender: NSPopUpButton) { + let tag = Track.Tag.allTags[tableView.rowForView(sender)] + let selectedIndex = sender.indexOfItem(sender.selectedItem!) + var savingBehavior = Preferences.sharedPreferences.tagSavingBehaviors[tag]! + switch selectedIndex { + case 0: + savingBehavior = tag.isReturnedBySearchAPI ? .Save : .Clear + case 1: + savingBehavior = tag.isReturnedBySearchAPI ? .Clear : .Ignore + case 2: + savingBehavior = .Ignore + default: + break + } + Preferences.sharedPreferences.tagSavingBehaviors[tag] = savingBehavior + } + +} \ No newline at end of file diff --git a/TagTunes/Preferences.swift b/TagTunes/Preferences.swift index 9e44c3a..79a9262 100644 --- a/TagTunes/Preferences.swift +++ b/TagTunes/Preferences.swift @@ -15,6 +15,38 @@ import Cocoa /// All properties in this class are KCO compliant. @objc public class Preferences: NSObject { + // MARK: Types + + private struct UserDefaultsConstants { + + static let saveArtworkKey = "Save Artwork" + + static let keepSearchResultsKey = "Keep Search Results" + + static let removeSavedAlbumsKey = "Remove Saved Albums" + + static let artworkTargetKey = "Artwork Target" + + static let useCensoredNamesKey = "Use Censored Names" + + static let tagSavingBehaviorsKey = "Tag Saving Behaviors" + } + + public enum TagSavingBehavior: String { + + /// Sets the tag's value to the value returned from the Search API. + case Save = "save" + + /// Sets the tag's value to an empty string. + case Clear = "clear" + + /// Does not alter the tag's value. + case Ignore = "ignore" + + } + + // MARK: Initialization + public static var sharedPreferences = Preferences() /// Initializes the default preferences. This method must be called the very @@ -22,24 +54,35 @@ import Cocoa /// this method every time the application launches. Existing values are not /// overridden. public func initializeDefaultValues() { - let initialArtworkFolder = NSURL.fileURLWithPath(NSHomeDirectory(), isDirectory: true) NSUserDefaults.standardUserDefaults().registerDefaults([ - "Save Artwork": false, - "Keep Search Results": false, - "Remove Saved Albums": false]) - if NSUserDefaults.standardUserDefaults().URLForKey("Artwork Target") == nil { - NSUserDefaults.standardUserDefaults().setURL(initialArtworkFolder, forKey: "Artwork Target") + UserDefaultsConstants.saveArtworkKey: false, + UserDefaultsConstants.keepSearchResultsKey: false, + UserDefaultsConstants.removeSavedAlbumsKey: false, + UserDefaultsConstants.useCensoredNamesKey: false + ]) + if NSUserDefaults.standardUserDefaults().dictionaryForKey(UserDefaultsConstants.tagSavingBehaviorsKey) == nil { + var savingBehaviors: [Track.Tag: TagSavingBehavior] = [:] + for tag in Track.Tag.allTags { + savingBehaviors[tag] = tag.isReturnedBySearchAPI ? .Save : .Clear + } + tagSavingBehaviors = savingBehaviors + } + let initialArtworkFolder = NSURL.fileURLWithPath(NSFileManager.defaultManager().URLsForDirectory(.DownloadsDirectory, inDomains: .UserDomainMask)[0].filePathURL!.path!, isDirectory: true) + if NSUserDefaults.standardUserDefaults().URLForKey(UserDefaultsConstants.artworkTargetKey) == nil { + artworkTarget = initialArtworkFolder } } + // MARK: General Preferences + /// If `true` the album artwork should be saved to the `artworkTarget` URL /// when an item is saved. public dynamic var saveArtwork: Bool { set { - NSUserDefaults.standardUserDefaults().setBool(newValue, forKey: "Save Artwork") + NSUserDefaults.standardUserDefaults().setBool(newValue, forKey: UserDefaultsConstants.saveArtworkKey) } get { - return NSUserDefaults.standardUserDefaults().boolForKey("Save Artwork") + return NSUserDefaults.standardUserDefaults().boolForKey(UserDefaultsConstants.saveArtworkKey) } } @@ -48,10 +91,10 @@ import Cocoa /// The URL must be a valid file URL pointing to a directory. public dynamic var artworkTarget: NSURL { set { - NSUserDefaults.standardUserDefaults().setURL(newValue, forKey: "Artwork Target") + NSUserDefaults.standardUserDefaults().setURL(newValue, forKey: UserDefaultsConstants.artworkTargetKey) } get { - return NSUserDefaults.standardUserDefaults().URLForKey("Artwork Target")! + return NSUserDefaults.standardUserDefaults().URLForKey(UserDefaultsConstants.artworkTargetKey)! } } @@ -59,11 +102,36 @@ import Cocoa /// when the user selects a result. public dynamic var keepSearchResults: Bool { set { - NSUserDefaults.standardUserDefaults().setBool(newValue, forKey: "Keep Search Results") + NSUserDefaults.standardUserDefaults().setBool(newValue, forKey: UserDefaultsConstants.keepSearchResultsKey) } get { - return NSUserDefaults.standardUserDefaults().boolForKey("Keep Search Results") + return NSUserDefaults.standardUserDefaults().boolForKey(UserDefaultsConstants.keepSearchResultsKey) } } - + + // MARK: Tag Preferences + + /// If `true` TagTunes displays and saves censored names instead of the + /// original names. + public dynamic var useCensoredNames: Bool { + set { + NSUserDefaults.standardUserDefaults().setBool(newValue, forKey: UserDefaultsConstants.useCensoredNamesKey) + } + get { + return NSUserDefaults.standardUserDefaults().boolForKey(UserDefaultsConstants.useCensoredNamesKey) + } + } + + /// The ways different tags are saved (or not saved). + public var tagSavingBehaviors: [Track.Tag: TagSavingBehavior] { + set { + let savableData = newValue.map { ($0.rawValue, $1.rawValue) } + NSUserDefaults.standardUserDefaults().setObject(savableData, forKey: UserDefaultsConstants.tagSavingBehaviorsKey) + } + get { + let savableData = NSUserDefaults.standardUserDefaults().dictionaryForKey(UserDefaultsConstants.tagSavingBehaviorsKey)! + return savableData.map { (Track.Tag(rawValue: $0)!, TagSavingBehavior(rawValue: $1 as! String)!) } + } + } + } diff --git a/TagTunes/PreferencesTabViewController.swift b/TagTunes/PreferencesTabViewController.swift deleted file mode 100644 index e303cfc..0000000 --- a/TagTunes/PreferencesTabViewController.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// PreferencesTabViewController.swift -// Harmony -// -// Created by Kim Wittenburg on 26.01.15. -// Copyright (c) 2015 Das Code Kollektiv. All rights reserved. -// - -import Cocoa - -internal class GeneralPreferencesViewController: NSViewController { - - // MARK: IBOutlets - - @IBOutlet internal weak var artworkPathControl: NSPathControl! - @IBOutlet internal weak var chooseArtworkButton: NSButton! - - override internal func viewDidLoad() { - super.viewDidLoad() - artworkPathControl.URL = Preferences.sharedPreferences.artworkTarget - saveArtworkStateChanged(self) - } - - @IBAction internal func saveArtworkStateChanged(sender: AnyObject) { - artworkPathControl.enabled = Preferences.sharedPreferences.saveArtwork - chooseArtworkButton.enabled = Preferences.sharedPreferences.saveArtwork - } - - @IBAction internal func chooseArtworkPath(sender: AnyObject) { - let openPanel = NSOpenPanel() - openPanel.canChooseDirectories = true - openPanel.canChooseFiles = false - openPanel.canCreateDirectories = true - openPanel.prompt = NSLocalizedString("Choose…", comment: "Button title prompting the user to choose a directory") - openPanel.beginSheetModalForWindow(view.window!) { - result in - if result == NSModalResponseOK { - Preferences.sharedPreferences.artworkTarget = openPanel.URL!.filePathURL! - self.artworkPathControl.URL = openPanel.URL - } - } - } - - - -} \ No newline at end of file diff --git a/TagTunes/Track.swift b/TagTunes/Track.swift index 1b658e6..1fdcf9c 100644 --- a/TagTunes/Track.swift +++ b/TagTunes/Track.swift @@ -12,6 +12,46 @@ import Cocoa /// [Search API](https://www.apple.com/itunes/affiliates/resources/documentation/itunes-store-web-service-search-api.html). public class Track: iTunesType { + // MARK: Types + + public enum Tag: String { + case Name = "name", Artist = "artist", Year = "year", TrackNumber = "trackNumber", TrackCount = "trackCount", DiscNumber = "discNumber", DiscCount = "discCount", Genre = "genre", AlbumName = "album", AlbumArtist = "albumArtist" + case SortName = "sortName", SortArtist = "sortArtist", SortAlbumName = "sortAlbum", SortAlbumArtist = "sortAlbumArtist", Composer = "composer", SortComposer = "sortComposer", Comment = "comment" + + /// Returns `true` for tags that are returned from the + /// [Search API](https://www.apple.com/itunes/affiliates/resources/documentation/itunes-store-web-service-search-api.html). + public var isReturnedBySearchAPI: Bool { + switch self { + case .Name, .Artist, .Year, .TrackNumber, .TrackCount, .DiscNumber, .DiscCount, .Genre, .AlbumName, .AlbumArtist: + return true + default: + return false + } + } + + /// Returns a string identifying the respective tag that can be displayed + /// to the user. + public var localizedName: String { + return NSLocalizedString(self.rawValue, comment: "") + } + + /// Returns the object that should be saved to *clear* the tag. + public var clearedValue: AnyObject? { + switch self { + case .Year, .TrackNumber, .TrackCount, .DiscNumber, .DiscCount: + return "" + default: + return "" + } + } + + /// Returns an array of all tags. + public static var allTags: [Tag] { + return [.Name, .Artist, .Year, .TrackNumber, .TrackCount, .DiscNumber, .DiscCount, .Genre, .AlbumName, .AlbumArtist, .SortName, .SortArtist, .SortAlbumName, .SortAlbumArtist, .Composer, .SortComposer, .Comment] + } + + } + // MARK: Properties public let id: iTunesId @@ -82,24 +122,42 @@ public class Track: iTunesType { /// This method respects the user's preferences (See `Preferences` class). public func saveToTrack(track: iTunesTrack) { let components = NSCalendar.currentCalendar().components(.Year, fromDate: releaseDate) - track.name = name - track.artist = artistName - track.year = components.year - track.trackNumber = trackNumber - track.trackCount = trackCount - track.discNumber = discNumber - track.discCount = discCount - track.genre = genre - track.album = album?.name - track.albumArtist = album?.artistName - track.sortName = "" - track.sortAlbum = "" - track.sortAlbumArtist = "" - track.sortArtist = "" - track.composer = "" - track.sortComposer = "" - track.comment = "" - track.artworks().removeAllObjects() + let trackName = Preferences.sharedPreferences.useCensoredNames ? censoredName : name + saveTag(.Name, toTrack: track, value: trackName) + saveTag(.Artist, toTrack: track, value: artistName) + saveTag(.Year, toTrack: track, value: components.year) + saveTag(.TrackNumber, toTrack: track, value: trackNumber) + saveTag(.TrackCount, toTrack: track, value: trackCount) + saveTag(.DiscNumber, toTrack: track, value: discNumber) + saveTag(.DiscCount, toTrack: track, value: discCount) + saveTag(.Genre, toTrack: track, value: genre) + let albumName = Preferences.sharedPreferences.useCensoredNames ? album.censoredName : album.name + saveTag(.AlbumName, toTrack: track, value: albumName) + saveTag(.AlbumArtist, toTrack: track, value: album.artistName) + saveTag(.SortName, toTrack: track, value: nil) + saveTag(.SortArtist, toTrack: track, value: nil) + saveTag(.SortAlbumName, toTrack: track, value: nil) + saveTag(.SortAlbumArtist, toTrack: track, value: nil) + saveTag(.Composer, toTrack: track, value: nil) + saveTag(.SortComposer, toTrack: track, value: nil) + saveTag(.Comment, toTrack: track, value: nil) + + // TODO: Deal with artworks +// if Preferences.sharedPreferences.clearArtworks { +// track.artworks().removeAllObjects() +// } + } + + private func saveTag(tag: Tag, toTrack track: iTunesTrack, value: AnyObject?) { + switch Preferences.sharedPreferences.tagSavingBehaviors[tag]! { + case .Save: + track.setValue(value, forKey: tag.rawValue) + case .Clear: + track.setValue(tag.clearedValue, forKey: tag.rawValue) + case .Ignore: + break + } + track.setValue(value, forKey: tag.rawValue) } /// Returns `true` if all `associatedTrack`s contain the same values as the