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

@@ -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
View 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}}.}

View File

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

View File

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

View File

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

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)

View File

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

View File

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

View File

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