Added 'Save Artwork' toolbar button
This commit is contained in:
committed by
Kim Wittenburg
parent
257a7811d6
commit
3a400037ff
@@ -9,7 +9,9 @@ Added:
|
||||
+ Options for tags not to be saved
|
||||
+ Option to use censored names
|
||||
+ Option for case (in)sensitivity
|
||||
+ Toolbar button to just save the artwork of the selected items
|
||||
|
||||
Fixed:
|
||||
* Added support for OS X 10.11 El Capitan
|
||||
* Error descriptions are now more understandable
|
||||
* Error descriptions are now more understandable
|
||||
* Cancelling the saving process of tracks does not crash the application on slow network connections anymore
|
||||
14
Credits.rtf
Normal file
14
Credits.rtf
Normal file
@@ -0,0 +1,14 @@
|
||||
{\rtf1\ansi\ansicpg1252\cocoartf1404
|
||||
{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
|
||||
{\colortbl;\red255\green255\blue255;}
|
||||
\paperw12240\paperh15840\margl1440\margr1440\vieww9000\viewh8400\viewkind0
|
||||
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural\partightenfactor0
|
||||
|
||||
\f0\b\fs24 \cf0 Design an Development
|
||||
\b0 \
|
||||
Kim Wittenburg\
|
||||
\
|
||||
|
||||
\b Icon Attributions
|
||||
\b0 \
|
||||
The Save Artwork icon was made by {\field{\*\fldinst{HYPERLINK "http://www.freepik.com"}}{\fldrslt Freepik}} from {\field{\*\fldinst{HYPERLINK "http://www.flaticon.com"}}{\fldrslt Flaticon}}. It is licensed under {\field{\*\fldinst{HYPERLINK "http://creativecommons.org/licenses/by/3.0/"}}{\fldrslt CC BY 3.0}}.}
|
||||
@@ -16,14 +16,15 @@
|
||||
3B489DC71B90B38C002B7EB3 /* iTunes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B489DC61B90B38C002B7EB3 /* iTunes.swift */; };
|
||||
3B489DCB1B90B3E3002B7EB3 /* iTunes.m in Sources */ = {isa = PBXBuildFile; fileRef = 3B489DCA1B90B3E3002B7EB3 /* iTunes.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; };
|
||||
3B489DD61B90E0D8002B7EB3 /* AlbumTableCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B489DD51B90E0D8002B7EB3 /* AlbumTableCellView.swift */; };
|
||||
3B66275F1BA767C500483219 /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 3B66275E1BA767C500483219 /* Credits.rtf */; settings = {ASSET_TAGS = (); }; };
|
||||
3B76C7731B909B280025D550 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B76C7721B909B280025D550 /* AppDelegate.swift */; };
|
||||
3B76C7751B909B280025D550 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B76C7741B909B280025D550 /* MainViewController.swift */; };
|
||||
3B76C7771B909B280025D550 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3B76C7761B909B280025D550 /* Assets.xcassets */; };
|
||||
3B76C77A1B909B280025D550 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3B76C7781B909B280025D550 /* Main.storyboard */; };
|
||||
3B76C7851B909B280025D550 /* TagTunesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B76C7841B909B280025D550 /* TagTunesTests.swift */; };
|
||||
3B76C7901B909B280025D550 /* TagTunesUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B76C78F1B909B280025D550 /* TagTunesUITests.swift */; };
|
||||
3B96BD661B9CA24100CC4101 /* DescriptiveError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B96BD651B9CA24100CC4101 /* DescriptiveError.swift */; };
|
||||
3BAD17CD1B9F0F6800FEF908 /* AlbumCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BAD17CC1B9F0F6800FEF908 /* AlbumCollection.swift */; };
|
||||
3BB1C97D1BA76F5F0083301F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3BB1C97C1BA76F5F0083301F /* Assets.xcassets */; settings = {ASSET_TAGS = (); }; };
|
||||
3BB8C5431BA2EEE800031021 /* Changelog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 3BB8C5421BA2EEE800031021 /* Changelog.txt */; };
|
||||
3BBF6FA01B946B7000BB1EDB /* SearchResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BBF6F9F1B946B7000BB1EDB /* SearchResult.swift */; };
|
||||
3BBF710B1B95E00F00BB1EDB /* AppKitPlus.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3BBF710A1B95E00F00BB1EDB /* AppKitPlus.framework */; };
|
||||
@@ -58,10 +59,10 @@
|
||||
3B489DC91B90B3E3002B7EB3 /* iTunes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = iTunes.h; sourceTree = "<group>"; };
|
||||
3B489DCA1B90B3E3002B7EB3 /* iTunes.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = iTunes.m; sourceTree = "<group>"; };
|
||||
3B489DD51B90E0D8002B7EB3 /* AlbumTableCellView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = AlbumTableCellView.swift; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
|
||||
3B66275E1BA767C500483219 /* Credits.rtf */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.rtf; path = Credits.rtf; sourceTree = "<group>"; };
|
||||
3B76C76F1B909B280025D550 /* TagTunes.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TagTunes.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
3B76C7721B909B280025D550 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
3B76C7741B909B280025D550 /* MainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = "<group>"; };
|
||||
3B76C7761B909B280025D550 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
3B76C7791B909B280025D550 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
3B76C77B1B909B280025D550 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
3B76C7801B909B280025D550 /* TagTunesTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TagTunesTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
@@ -72,6 +73,7 @@
|
||||
3B76C7911B909B280025D550 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
3B96BD651B9CA24100CC4101 /* DescriptiveError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DescriptiveError.swift; sourceTree = "<group>"; };
|
||||
3BAD17CC1B9F0F6800FEF908 /* AlbumCollection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlbumCollection.swift; sourceTree = "<group>"; };
|
||||
3BB1C97C1BA76F5F0083301F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
3BB8C5421BA2EEE800031021 /* Changelog.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Changelog.txt; sourceTree = "<group>"; };
|
||||
3BBF6F9F1B946B7000BB1EDB /* SearchResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchResult.swift; sourceTree = "<group>"; };
|
||||
3BBF710A1B95E00F00BB1EDB /* AppKitPlus.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKitPlus.framework; path = "../../../../Library/Developer/Xcode/DerivedData/TagTunes-ahlftzbggvvcneeglkkowfbohpzh/Build/Products/Debug/AppKitPlus.framework"; sourceTree = "<group>"; };
|
||||
@@ -116,6 +118,7 @@
|
||||
3B76C7661B909B280025D550 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3B66275E1BA767C500483219 /* Credits.rtf */,
|
||||
3BB8C5421BA2EEE800031021 /* Changelog.txt */,
|
||||
3BBF71161B98FB4200BB1EDB /* README.md */,
|
||||
3B76C7711B909B280025D550 /* TagTunes */,
|
||||
@@ -196,7 +199,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3B76C7781B909B280025D550 /* Main.storyboard */,
|
||||
3B76C7761B909B280025D550 /* Assets.xcassets */,
|
||||
3BB1C97C1BA76F5F0083301F /* Assets.xcassets */,
|
||||
3B489DD51B90E0D8002B7EB3 /* AlbumTableCellView.swift */,
|
||||
3B489DBD1B90B055002B7EB3 /* TrackTableCellView.swift */,
|
||||
);
|
||||
@@ -315,9 +318,10 @@
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
3B76C7771B909B280025D550 /* Assets.xcassets in Resources */,
|
||||
3B76C77A1B909B280025D550 /* Main.storyboard in Resources */,
|
||||
3BB8C5431BA2EEE800031021 /* Changelog.txt in Resources */,
|
||||
3BB1C97D1BA76F5F0083301F /* Assets.xcassets in Resources */,
|
||||
3B66275F1BA767C500483219 /* Credits.rtf in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -489,6 +493,10 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/TagTunes",
|
||||
);
|
||||
INFOPLIST_FILE = TagTunes/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = wittenburg.kim.TagTunes;
|
||||
@@ -506,6 +514,10 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/TagTunes",
|
||||
);
|
||||
INFOPLIST_FILE = TagTunes/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = wittenburg.kim.TagTunes;
|
||||
|
||||
@@ -109,8 +109,9 @@ public class Album: iTunesType {
|
||||
/// Saves the album's artwork to the directory specified in the user's
|
||||
/// preferences (See `Preferences` for details).
|
||||
public func saveArtwork() throws {
|
||||
let url = Preferences.sharedPreferences.artworkTarget
|
||||
try artwork.saveToURL(url, filename: name)
|
||||
if let url = Preferences.sharedPreferences.artworkTarget {
|
||||
try artwork.saveToURL(url, filename: name)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if all tracks of the album are saved.
|
||||
|
||||
@@ -675,19 +675,26 @@ CA
|
||||
<action selector="performSave:" target="Oky-zY-oP4" id="9BZ-UY-ffJ"/>
|
||||
</connections>
|
||||
</toolbarItem>
|
||||
<toolbarItem implicitItemIdentifier="85CB266F-AF83-4345-A711-06F6A276F0AA" label="Remove" paletteLabel="Remove" tag="-1" image="Cross" id="RU6-wj-p8A">
|
||||
<toolbarItem implicitItemIdentifier="0BAA5124-A8B4-49FB-8522-C426773E90C7" label="Save Artwork" paletteLabel="Save Artwork" tag="-1" image="SaveArtwork" id="PKp-tG-6Fu">
|
||||
<connections>
|
||||
<action selector="delete:" target="Oky-zY-oP4" id="SyO-WS-R4p"/>
|
||||
<action selector="saveArtworks:" target="Oky-zY-oP4" id="Sdk-EP-Qhk"/>
|
||||
</connections>
|
||||
</toolbarItem>
|
||||
<toolbarItem implicitItemIdentifier="NSToolbarSpaceItem" id="Bqq-8n-9JT"/>
|
||||
<toolbarItem implicitItemIdentifier="NSToolbarFlexibleSpaceItem" id="uaV-9O-6iw"/>
|
||||
<toolbarItem implicitItemIdentifier="E49F1F29-A83B-4580-B02C-D394E2FE36E6" label="Remove" paletteLabel="Remove" tag="-1" image="Cross" id="Fst-WO-GlQ">
|
||||
<connections>
|
||||
<action selector="removeSelectedItems:" target="Oky-zY-oP4" id="Uui-oo-jyX"/>
|
||||
</connections>
|
||||
</toolbarItem>
|
||||
<toolbarItem implicitItemIdentifier="NSToolbarSpaceItem" id="Wck-Nn-UcZ"/>
|
||||
<toolbarItem implicitItemIdentifier="NSToolbarFlexibleSpaceItem" id="HCz-XZ-F4F"/>
|
||||
</allowedToolbarItems>
|
||||
<defaultToolbarItems>
|
||||
<toolbarItem reference="vfK-go-3oR"/>
|
||||
<toolbarItem reference="Bqq-8n-9JT"/>
|
||||
<toolbarItem reference="Wck-Nn-UcZ"/>
|
||||
<toolbarItem reference="ax1-DR-t4v"/>
|
||||
<toolbarItem reference="RU6-wj-p8A"/>
|
||||
<toolbarItem reference="PKp-tG-6Fu"/>
|
||||
<toolbarItem reference="Wck-Nn-UcZ"/>
|
||||
<toolbarItem reference="Fst-WO-GlQ"/>
|
||||
</defaultToolbarItems>
|
||||
</toolbar>
|
||||
</window>
|
||||
@@ -757,9 +764,9 @@ CA
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<button translatesAutoresizingMaskIntoConstraints="NO" id="kl3-n8-T9b">
|
||||
<rect key="frame" x="18" y="92" width="103" height="18"/>
|
||||
<rect key="frame" x="18" y="92" width="188" height="18"/>
|
||||
<animations/>
|
||||
<buttonCell key="cell" type="check" title="Save Artwork" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="q2l-4t-mL0">
|
||||
<buttonCell key="cell" type="check" title="Automatically Save Artwork" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="q2l-4t-mL0">
|
||||
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
@@ -796,7 +803,7 @@ CA
|
||||
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" setsMaxLayoutWidthAtFirstLayout="YES" translatesAutoresizingMaskIntoConstraints="NO" id="QNf-Uy-bMT">
|
||||
<rect key="frame" x="30" y="20" width="402" height="14"/>
|
||||
<animations/>
|
||||
<textFieldCell key="cell" controlSize="small" sendsActionOnEndEditing="YES" title="If checked the search results are not removed if a result is added." id="Lnf-PQ-PX4">
|
||||
<textFieldCell key="cell" controlSize="small" sendsActionOnEndEditing="YES" title="If checked the search results are not hidden if a result is added." id="Lnf-PQ-PX4">
|
||||
<font key="font" metaFont="smallSystem"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
@@ -810,7 +817,11 @@ CA
|
||||
<url key="url" string="file:///Applications/"/>
|
||||
</pathCell>
|
||||
<connections>
|
||||
<binding destination="aSx-iH-PLA" name="value" keyPath="sharedPreferences.artworkTarget" id="k2W-Kf-xpK"/>
|
||||
<binding destination="aSx-iH-PLA" name="value" keyPath="sharedPreferences.artworkTarget" id="eFO-dQ-CMc">
|
||||
<dictionary key="options">
|
||||
<string key="NSNullPlaceholder">Choose an artwork directory…</string>
|
||||
</dictionary>
|
||||
</binding>
|
||||
</connections>
|
||||
</pathControl>
|
||||
</subviews>
|
||||
@@ -898,7 +909,7 @@ CA
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<tableView verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnAutoresizingStyle="lastColumnOnly" alternatingRowBackgroundColors="YES" multipleSelection="NO" autosaveColumns="NO" rowHeight="21" rowSizeStyle="automatic" viewBased="YES" id="WCb-HH-YPh">
|
||||
<rect key="frame" x="0.0" y="0.0" width="408" height="211"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="408" height="0.0"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<animations/>
|
||||
<size key="intercellSpacing" width="3" height="2"/>
|
||||
@@ -1138,5 +1149,6 @@ CA
|
||||
<image name="Note" width="1024" height="1024"/>
|
||||
<image name="PreferenceTags" width="730" height="730"/>
|
||||
<image name="Save" width="1024" height="1024"/>
|
||||
<image name="SaveArtwork" width="1024" height="1024"/>
|
||||
</resources>
|
||||
</document>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -18,12 +18,12 @@ internal class GeneralPreferencesViewController: NSViewController {
|
||||
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
|
||||
if Preferences.sharedPreferences.saveArtwork && Preferences.sharedPreferences.artworkTarget == nil {
|
||||
chooseArtworkPath(sender)
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction internal func chooseArtworkPath(sender: AnyObject) {
|
||||
@@ -36,7 +36,8 @@ internal class GeneralPreferencesViewController: NSViewController {
|
||||
result in
|
||||
if result == NSModalResponseOK {
|
||||
Preferences.sharedPreferences.artworkTarget = openPanel.URL!.filePathURL!
|
||||
self.artworkPathControl.URL = openPanel.URL
|
||||
} else if Preferences.sharedPreferences.artworkTarget == nil {
|
||||
Preferences.sharedPreferences.saveArtwork = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,12 +30,12 @@ import Cocoa
|
||||
|
||||
static let saveArtworkKey = "Save Artwork"
|
||||
|
||||
static let artworkTargetKey = "Artwork Target"
|
||||
|
||||
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 caseSensitive = "Case Sensitive"
|
||||
@@ -43,6 +43,7 @@ import Cocoa
|
||||
static let tagSavingBehaviorsKey = "Tag Saving Behaviors"
|
||||
}
|
||||
|
||||
/// Specifies the way a tag is saved to iTunes.
|
||||
public enum TagSavingBehavior: String {
|
||||
|
||||
/// Sets the tag's value to the value returned from the Search API.
|
||||
@@ -79,10 +80,7 @@ import Cocoa
|
||||
}
|
||||
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
|
||||
}
|
||||
artworkTarget = nil
|
||||
}
|
||||
|
||||
// MARK: General Preferences
|
||||
@@ -101,12 +99,12 @@ import Cocoa
|
||||
/// The URL of the folder album artwork is saved to.
|
||||
///
|
||||
/// The URL must be a valid file URL pointing to a directory.
|
||||
public dynamic var artworkTarget: NSURL {
|
||||
public dynamic var artworkTarget: NSURL? {
|
||||
set {
|
||||
NSUserDefaults.standardUserDefaults().setURL(newValue, forKey: UserDefaultsConstants.artworkTargetKey)
|
||||
}
|
||||
get {
|
||||
return NSUserDefaults.standardUserDefaults().URLForKey(UserDefaultsConstants.artworkTargetKey)!
|
||||
return NSUserDefaults.standardUserDefaults().URLForKey(UserDefaultsConstants.artworkTargetKey)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import Cocoa
|
||||
|
||||
/// Represents an `Album` returned fromm the
|
||||
/// [Search API](https://www.apple.com/itunes/affiliates/resources/documentation/itunes-store-web-service-search-api.html).
|
||||
public class SearchResult: Equatable {
|
||||
public class SearchResult {
|
||||
|
||||
public let id: iTunesId
|
||||
|
||||
@@ -44,6 +44,14 @@ public class SearchResult: Equatable {
|
||||
|
||||
}
|
||||
|
||||
extension SearchResult: Hashable {
|
||||
|
||||
public var hashValue: Int {
|
||||
return Int(id)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Album {
|
||||
|
||||
public convenience init(searchResult: SearchResult) {
|
||||
@@ -54,12 +62,4 @@ extension Album {
|
||||
|
||||
public func ==(lhs: SearchResult, rhs: SearchResult) -> Bool {
|
||||
return lhs.id == rhs.id
|
||||
}
|
||||
|
||||
public func ==(lhs: SearchResult, rhs: Album) -> Bool {
|
||||
return lhs.id == rhs.id
|
||||
}
|
||||
|
||||
public func ==(lhs: Album, rhs: SearchResult) -> Bool {
|
||||
return lhs.id == rhs.id
|
||||
}
|
||||
Reference in New Issue
Block a user