Compare commits
27 Commits
version-1.
...
version-1.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24bb68f7d8 | ||
|
|
2da74aa359 | ||
|
|
7dbdc047ae | ||
|
|
c0e7727785 | ||
|
|
7cd60ffed4 | ||
|
|
2b668967e2 | ||
|
|
e287a935b1 | ||
|
|
4548f43797 | ||
|
|
dcd7d2620d | ||
|
|
d49ad31b79 | ||
|
|
bfa0deae4f | ||
|
|
b694c18daf | ||
|
|
3a400037ff | ||
|
|
257a7811d6 | ||
|
|
e2bdcd21e5 | ||
|
|
5704d0c0e5 | ||
|
|
80f177807d | ||
|
|
45f664cb10 | ||
|
|
6bf18c1aae | ||
|
|
b851b70ddb | ||
|
|
2c2252f6af | ||
|
|
d1c5e3b98a | ||
|
|
7a6192d19a | ||
|
|
5196b6a577 | ||
|
|
05c7b121a5 | ||
|
|
b778d14ea6 | ||
|
|
0bfef01875 |
17
Changelog.txt
Normal file
@@ -0,0 +1,17 @@
|
||||
Version 1.0
|
||||
|
||||
- First Release
|
||||
|
||||
Version 1.1
|
||||
|
||||
Added:
|
||||
+ 'Try again' option for errors.
|
||||
+ 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
|
||||
* Cancelling the saving process of tracks does not crash the application on slow network connections anymore
|
||||
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}}.}
|
||||
45
README.md
Normal file
@@ -0,0 +1,45 @@
|
||||
TagTunes is an application that uses Apple's [Search API][Search API] to get tags for your music files direcly from the iTunes store. With TagTunes you can easily clean your music library using exactly the tags that Apple itself uses.
|
||||
|
||||
#[Download TagTunes][Download 1.1]
|
||||
|
||||
------------------------------------------------------------
|
||||
|
||||
## All Versions
|
||||
|
||||
### [Version 1.1][Download 1.1]
|
||||
This version adds several new settings as well as support for OS X 10.11 El Capitan. It also improves the overall stability of the app.
|
||||
|
||||
### [Version 1.0][Download 1.0]
|
||||
The first release of TagTunes includes all functions needed to clean your iTunes library.
|
||||
|
||||
*For more details for each version, have a look at the changelog.*
|
||||
|
||||
------------------------------------------------------------
|
||||
|
||||
## Some random questions and answers
|
||||
|
||||
#### How do I install TagTunes?
|
||||
> To install TagTunes [download][Download] the disk image and mount it by double-clicking it in the Finder. Then drag the *TagTunes* icon to any folder you like and run it.
|
||||
|
||||
#### How does TagTunes work?
|
||||
> TagTunes uses Apple's [Search API][Search API] to get its tags.
|
||||
|
||||
#### The tags I got from TagTunes differ from the one's in the iTunes Store.
|
||||
> Unfortunately Apple's Search API does not always return the name results as the iTunes Store does. This mainly affects composers (which aren't returned by the Search API at all) and track titles.
|
||||
|
||||
#### Does TagTunes support other sources than the Search API?
|
||||
> No, currently TagTunes does not support other sources.
|
||||
|
||||
#### Can I contribute to TagTunes?
|
||||
> Please E-Mail me at [dev.kwittenburg@icloud.com][E-Mail]
|
||||
|
||||
------------------------------------------------------------
|
||||
|
||||
## A note to developers
|
||||
Currently TagTunes is not released under a open source license. This will most likely change in the future, though. Until then you can freely browse the source code. If you want to incorporate this project in your own, please write me at [dev.kwittenburg@icloud.com][E-Mail].
|
||||
|
||||
|
||||
[Search API]: https://www.apple.com/itunes/affiliates/resources/documentation/itunes-store-web-service-search-api.html
|
||||
[Download 1.1]: https://bitbucket.org/Codello/tagtunes/downloads/TagTunes%201.1.dmg
|
||||
[Download 1.0]: https://bitbucket.org/Codello/tagtunes/downloads/TagTunes%201.0.dmg
|
||||
[E-Mail]: mailto:dev.kwittenburg@icloud.com
|
||||
@@ -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 */; };
|
||||
@@ -16,14 +16,20 @@
|
||||
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 */; };
|
||||
3B9752721BA85C2F00E26515 /* AppKitPlus.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3BBF710A1B95E00F00BB1EDB /* AppKitPlus.framework */; };
|
||||
3B9752731BA85C2F00E26515 /* AppKitPlus.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3BBF710A1B95E00F00BB1EDB /* AppKitPlus.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
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 = (); }; };
|
||||
3BBF6FA01B946B7000BB1EDB /* SearchResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BBF6F9F1B946B7000BB1EDB /* SearchResult.swift */; };
|
||||
3BBF710B1B95E00F00BB1EDB /* AppKitPlus.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3BBF710A1B95E00F00BB1EDB /* AppKitPlus.framework */; };
|
||||
3BFDED621BA84AD1007E7F36 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3BFDED601BA84AD1007E7F36 /* Localizable.strings */; settings = {ASSET_TAGS = (); }; };
|
||||
3BFDED741BA855B8007E7F36 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 3BFDED761BA855B8007E7F36 /* Localizable.stringsdict */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -43,8 +49,22 @@
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
3B9752741BA85C2F00E26515 /* Embed Frameworks */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
3B9752731BA85C2F00E26515 /* AppKitPlus.framework in Embed Frameworks */,
|
||||
);
|
||||
name = "Embed Frameworks";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
3B285DB71B9128C100F0A2F1 /* PreferencesTabViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencesTabViewController.swift; sourceTree = "<group>"; };
|
||||
3B285DB71B9128C100F0A2F1 /* Preference Controllers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Preference Controllers.swift"; sourceTree = "<group>"; };
|
||||
3B285DBE1B912AB700F0A2F1 /* Preferences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = "<group>"; };
|
||||
3B489DBD1B90B055002B7EB3 /* TrackTableCellView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = TrackTableCellView.swift; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
|
||||
3B489DC01B90B116002B7EB3 /* Artwork.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Artwork.swift; sourceTree = "<group>"; };
|
||||
@@ -55,10 +75,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; name = Credits.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; };
|
||||
@@ -67,8 +87,16 @@
|
||||
3B76C78B1B909B280025D550 /* TagTunesUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TagTunesUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
3B76C78F1B909B280025D550 /* TagTunesUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagTunesUITests.swift; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
3B97526A1BA85B5A00E26515 /* AppKitPlus.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKitPlus.framework; path = ../AppKitPlus/build/Release/AppKitPlus.framework; 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>"; };
|
||||
3BBF71161B98FB4200BB1EDB /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
|
||||
3BFDED611BA84AD1007E7F36 /* Base */ = {isa = PBXFileReference; fileEncoding = 10; lastKnownFileType = text.plist.strings; name = Base; path = Base.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
3BFDED751BA855B8007E7F36 /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = Base; path = Base.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -76,7 +104,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
3BBF710B1B95E00F00BB1EDB /* AppKitPlus.framework in Frameworks */,
|
||||
3B9752721BA85C2F00E26515 /* AppKitPlus.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -109,6 +137,8 @@
|
||||
3B76C7661B909B280025D550 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3BB8C5421BA2EEE800031021 /* Changelog.txt */,
|
||||
3BBF71161B98FB4200BB1EDB /* README.md */,
|
||||
3B76C7711B909B280025D550 /* TagTunes */,
|
||||
3B76C7831B909B280025D550 /* TagTunesTests */,
|
||||
3B76C78E1B909B280025D550 /* TagTunesUITests */,
|
||||
@@ -134,7 +164,7 @@
|
||||
3B76C79D1B909B8C0025D550 /* Model */,
|
||||
3B76C79F1B909B960025D550 /* View */,
|
||||
3B76C79E1B909B910025D550 /* Controller */,
|
||||
3B76C77B1B909B280025D550 /* Info.plist */,
|
||||
3BFDED631BA84ADB007E7F36 /* Resources */,
|
||||
3B489DC81B90B3E2002B7EB3 /* TagTunes-Bridging-Header.h */,
|
||||
);
|
||||
path = TagTunes;
|
||||
@@ -162,7 +192,9 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3B489DC11B90B116002B7EB3 /* Album.swift */,
|
||||
3BAD17CC1B9F0F6800FEF908 /* AlbumCollection.swift */,
|
||||
3B489DC01B90B116002B7EB3 /* Artwork.swift */,
|
||||
3B96BD651B9CA24100CC4101 /* DescriptiveError.swift */,
|
||||
3B489DC61B90B38C002B7EB3 /* iTunes.swift */,
|
||||
3B285DBE1B912AB700F0A2F1 /* Preferences.swift */,
|
||||
3BBF6F9F1B946B7000BB1EDB /* SearchResult.swift */,
|
||||
@@ -176,7 +208,7 @@
|
||||
children = (
|
||||
3B76C7721B909B280025D550 /* AppDelegate.swift */,
|
||||
3B76C7741B909B280025D550 /* MainViewController.swift */,
|
||||
3B285DB71B9128C100F0A2F1 /* PreferencesTabViewController.swift */,
|
||||
3B285DB71B9128C100F0A2F1 /* Preference Controllers.swift */,
|
||||
);
|
||||
name = Controller;
|
||||
sourceTree = "<group>";
|
||||
@@ -184,8 +216,6 @@
|
||||
3B76C79F1B909B960025D550 /* View */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3B76C7781B909B280025D550 /* Main.storyboard */,
|
||||
3B76C7761B909B280025D550 /* Assets.xcassets */,
|
||||
3B489DD51B90E0D8002B7EB3 /* AlbumTableCellView.swift */,
|
||||
3B489DBD1B90B055002B7EB3 /* TrackTableCellView.swift */,
|
||||
);
|
||||
@@ -195,11 +225,25 @@
|
||||
3BBF710C1B95E02E00BB1EDB /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3B97526A1BA85B5A00E26515 /* AppKitPlus.framework */,
|
||||
3BBF710A1B95E00F00BB1EDB /* AppKitPlus.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
3BFDED631BA84ADB007E7F36 /* Resources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3B76C7781B909B280025D550 /* Main.storyboard */,
|
||||
3BB1C97C1BA76F5F0083301F /* Assets.xcassets */,
|
||||
3BFDED601BA84AD1007E7F36 /* Localizable.strings */,
|
||||
3B76C77B1B909B280025D550 /* Info.plist */,
|
||||
3B66275E1BA767C500483219 /* Credits.rtf */,
|
||||
3BFDED761BA855B8007E7F36 /* Localizable.stringsdict */,
|
||||
);
|
||||
name = Resources;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
@@ -210,6 +254,7 @@
|
||||
3B76C76B1B909B280025D550 /* Sources */,
|
||||
3B76C76C1B909B280025D550 /* Frameworks */,
|
||||
3B76C76D1B909B280025D550 /* Resources */,
|
||||
3B9752741BA85C2F00E26515 /* Embed Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@@ -304,8 +349,11 @@
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
3B76C7771B909B280025D550 /* Assets.xcassets in Resources */,
|
||||
3BFDED621BA84AD1007E7F36 /* Localizable.strings in Resources */,
|
||||
3B76C77A1B909B280025D550 /* Main.storyboard in Resources */,
|
||||
3BB1C97D1BA76F5F0083301F /* Assets.xcassets in Resources */,
|
||||
3B66275F1BA767C500483219 /* Credits.rtf in Resources */,
|
||||
3BFDED741BA855B8007E7F36 /* Localizable.stringsdict in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -335,11 +383,13 @@
|
||||
3B76C7751B909B280025D550 /* MainViewController.swift in Sources */,
|
||||
3B76C7731B909B280025D550 /* AppDelegate.swift in Sources */,
|
||||
3B489DBF1B90B055002B7EB3 /* TrackTableCellView.swift in Sources */,
|
||||
3BAD17CD1B9F0F6800FEF908 /* AlbumCollection.swift in Sources */,
|
||||
3B285DBF1B912AB700F0A2F1 /* Preferences.swift in Sources */,
|
||||
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 */,
|
||||
3B96BD661B9CA24100CC4101 /* DescriptiveError.swift in Sources */,
|
||||
3B489DCB1B90B3E3002B7EB3 /* iTunes.m in Sources */,
|
||||
3B489DD61B90E0D8002B7EB3 /* AlbumTableCellView.swift in Sources */,
|
||||
);
|
||||
@@ -385,6 +435,22 @@
|
||||
name = Main.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
3BFDED601BA84AD1007E7F36 /* Localizable.strings */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
3BFDED611BA84AD1007E7F36 /* Base */,
|
||||
);
|
||||
name = Localizable.strings;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
3BFDED761BA855B8007E7F36 /* Localizable.stringsdict */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
3BFDED751BA855B8007E7F36 /* Base */,
|
||||
);
|
||||
name = Localizable.stringsdict;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
@@ -424,7 +490,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.10;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.11;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = macosx;
|
||||
@@ -462,7 +528,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.10;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.11;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = macosx;
|
||||
};
|
||||
@@ -474,12 +540,18 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
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;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "TagTunes/TagTunes-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
@@ -489,11 +561,17 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
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;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "TagTunes/TagTunes-Bridging-Header.h";
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
|
||||
@@ -5,6 +5,22 @@
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "3B76C76E1B909B280025D550"
|
||||
BuildableName = "TagTunes.app"
|
||||
BlueprintName = "TagTunes"
|
||||
ReferencedContainer = "container:TagTunes.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
@@ -12,7 +28,36 @@
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "3B76C77F1B909B280025D550"
|
||||
BuildableName = "TagTunesTests.xctest"
|
||||
BlueprintName = "TagTunesTests"
|
||||
ReferencedContainer = "container:TagTunes.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "3B76C78A1B909B280025D550"
|
||||
BuildableName = "TagTunesUITests.xctest"
|
||||
BlueprintName = "TagTunesUITests"
|
||||
ReferencedContainer = "container:TagTunes.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "3B76C76E1B909B280025D550"
|
||||
BuildableName = "TagTunes.app"
|
||||
BlueprintName = "TagTunes"
|
||||
ReferencedContainer = "container:TagTunes.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</TestAction>
|
||||
@@ -26,6 +71,16 @@
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "3B76C76E1B909B280025D550"
|
||||
BuildableName = "TagTunes.app"
|
||||
BlueprintName = "TagTunes"
|
||||
ReferencedContainer = "container:TagTunes.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
@@ -35,6 +90,16 @@
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "3B76C76E1B909B280025D550"
|
||||
BuildableName = "TagTunes.app"
|
||||
BlueprintName = "TagTunes"
|
||||
ReferencedContainer = "container:TagTunes.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
|
||||
@@ -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.
|
||||
|
||||
209
TagTunes/AlbumCollection.swift
Normal file
@@ -0,0 +1,209 @@
|
||||
//
|
||||
// AlbumCollection.swift
|
||||
// TagTunes
|
||||
//
|
||||
// Created by Kim Wittenburg on 08.09.15.
|
||||
// Copyright © 2015 Kim Wittenburg. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Manages a collection of albums. Managing includes support for deferred
|
||||
/// loading of an album's tracks as well as error support.
|
||||
public class AlbumCollection: CollectionType {
|
||||
|
||||
// MARK: Types
|
||||
|
||||
private enum AlbumState {
|
||||
|
||||
case Normal
|
||||
|
||||
case Error(NSError)
|
||||
|
||||
case Loading(NSURLSessionTask)
|
||||
|
||||
}
|
||||
|
||||
/// Notifications posted by an album collection. The `userInfo` of these
|
||||
/// notifications contains all keys specified in `Keys`.
|
||||
public struct Notifications {
|
||||
|
||||
/// Posted when an album is added to a collection. This notification is
|
||||
/// only posted if the album collection actually changed.
|
||||
public static let albumAdded = "AlbumAddedNotificationName"
|
||||
|
||||
/// Posted when an album is removed from a collection. This notification
|
||||
/// is only posted if the album collection actually changed.
|
||||
public static let albumRemoved = "AlbumRemovedNotificationName"
|
||||
|
||||
/// Posted when the album collection started a network request for an
|
||||
/// album's tracks.
|
||||
///
|
||||
/// Note that the values for the keys `Album` and `AlbumIndex` for the
|
||||
/// corresponding `AlbumFinishedLoading` notification may both be
|
||||
/// different.
|
||||
public static let albumStartedLoading = "AlbumStartedLoadingNotificationName"
|
||||
|
||||
/// Posted when an album collection finished loading the tracks for an
|
||||
/// album. Receiving this notification does not mean that the tracks were
|
||||
/// actually loaded successfully. It just means that the networ
|
||||
/// connection terminated. Use `errorForAlbum` to determine if an error
|
||||
/// occured while the tracks have been loaded.
|
||||
///
|
||||
/// - note: Since the actual `Album` instance in the album collection may
|
||||
/// change during loading its tracks it is preferred that you use the
|
||||
/// `AlbumIndexKey` of the notification to determine which album finished
|
||||
/// loading its tracks. You can however use the `AlbumKey` as well to
|
||||
/// access the `Album` instance that is currently present in the
|
||||
/// collection at the respective index.
|
||||
public static let albumFinishedLoading = "AlbumFinishedLoadingNotificationName"
|
||||
|
||||
/// These constants are available as keys in the `userInfo` dictionary
|
||||
/// for any notification an album collection may post.
|
||||
public struct Keys {
|
||||
|
||||
/// The `Album` instance affected by the notification.
|
||||
public static let album = "AlbumKey"
|
||||
|
||||
/// The index of the album affected by the notification.
|
||||
public static let albumIndex = "AlbumIndexKey"
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// Access the userlying array of albums.
|
||||
public private(set) var albums = [Album]()
|
||||
|
||||
private var albumStates = [Album: AlbumState]()
|
||||
|
||||
/// The URL session used to load tracks for albums.
|
||||
private let urlSession = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration(), delegate: nil, delegateQueue: NSOperationQueue.mainQueue())
|
||||
|
||||
// MARK: Collection Type
|
||||
|
||||
public init() {}
|
||||
|
||||
public var startIndex: Int {
|
||||
return albums.startIndex
|
||||
}
|
||||
|
||||
public var endIndex: Int {
|
||||
return albums.endIndex
|
||||
}
|
||||
|
||||
public subscript (position: Int) -> Album {
|
||||
return albums[position]
|
||||
}
|
||||
|
||||
// MARK: Album Collection
|
||||
|
||||
/// Adds the specified album, if not already present, and begins to load its
|
||||
/// tracks.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - album: The album to be added.
|
||||
/// - flag: Specify `false` if the album collection should not begin to
|
||||
/// load the album's tracks immediately.
|
||||
public func addAlbum(album: Album, beginLoading flag: Bool = true) {
|
||||
if !albums.contains(album) {
|
||||
albums.append(album)
|
||||
let userInfo: [NSObject: AnyObject] = [AlbumCollection.Notifications.Keys.album: album, AlbumCollection.Notifications.Keys.albumIndex: albums.count-1]
|
||||
NSNotificationCenter.defaultCenter().postNotificationName(AlbumCollection.Notifications.albumAdded, object: self, userInfo: userInfo)
|
||||
if flag {
|
||||
beginLoadingTracksForAlbum(album)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes the specified album from the collection if it is present.
|
||||
public func removeAlbum(album: Album) {
|
||||
if let index = albums.indexOf(album) {
|
||||
removeAlbumAtIndex(index)
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes the album at the specified index from the collection.
|
||||
///
|
||||
/// - requires: The specified index must be in the collection's range.
|
||||
public func removeAlbumAtIndex(index: Int) {
|
||||
let album = self[index]
|
||||
setAlbumState(nil, forAlbum: album)
|
||||
albums.removeAtIndex(index)
|
||||
let userInfo: [NSObject: AnyObject] = [AlbumCollection.Notifications.Keys.album: album, AlbumCollection.Notifications.Keys.albumIndex: index]
|
||||
NSNotificationCenter.defaultCenter().postNotificationName(AlbumCollection.Notifications.albumRemoved, object: self, userInfo: userInfo)
|
||||
}
|
||||
|
||||
/// Begins to load the tracks for the specified album. If there is already a
|
||||
/// request for the specified album it is cancelled. When the tracks for the
|
||||
/// specified album have been loaded or an error occured, a
|
||||
/// `AlbumFinishedLoadingNotification` is posted.
|
||||
public func beginLoadingTracksForAlbum(album: Album) {
|
||||
guard let albumIndex = albums.indexOf(album) else {
|
||||
return
|
||||
}
|
||||
let url = iTunesAPI.createAlbumLookupURLForId(album.id)
|
||||
let task = urlSession.dataTaskWithURL(url) { (data, response, error) -> Void in
|
||||
var newAlbumIndex = self.albums.indexOf(album)!
|
||||
defer {
|
||||
let userInfo: [NSObject: AnyObject] = [AlbumCollection.Notifications.Keys.album: self.albums[albumIndex], AlbumCollection.Notifications.Keys.albumIndex: albumIndex]
|
||||
NSNotificationCenter.defaultCenter().postNotificationName(AlbumCollection.Notifications.albumFinishedLoading, object: self, userInfo: userInfo)
|
||||
}
|
||||
guard error == nil else {
|
||||
if error!.code != NSUserCancelledError {
|
||||
self.albumStates[album] = .Error(error!)
|
||||
}
|
||||
return
|
||||
}
|
||||
do {
|
||||
let newAlbum = try iTunesAPI.parseAPIData(data!)[0]
|
||||
self.albums.removeAtIndex(newAlbumIndex)
|
||||
self.albums.insert(newAlbum, atIndex: newAlbumIndex)
|
||||
self.setAlbumState(.Normal, forAlbum: album)
|
||||
} catch let error as NSError {
|
||||
self.setAlbumState(.Error(error), forAlbum: album)
|
||||
} catch _ {
|
||||
// Will never happen
|
||||
}
|
||||
}
|
||||
setAlbumState(.Loading(task), forAlbum: album)
|
||||
task.resume()
|
||||
let userInfo: [NSObject: AnyObject] = [AlbumCollection.Notifications.Keys.album: album, AlbumCollection.Notifications.Keys.albumIndex: albumIndex]
|
||||
NSNotificationCenter.defaultCenter().postNotificationName(AlbumCollection.Notifications.albumStartedLoading, object: self, userInfo: userInfo)
|
||||
|
||||
}
|
||||
|
||||
/// Cancels the request to load the tracks for the specified album and sets
|
||||
/// the error for the album to a `NSUserCancelledError` in the
|
||||
/// `NSCocoaErrorDomain`.
|
||||
public func cancelLoadingTracksForAlbum(album: Album) {
|
||||
let error = NSError(domain: NSCocoaErrorDomain, code: NSUserCancelledError, userInfo: nil)
|
||||
setAlbumState(.Error(error), forAlbum: album)
|
||||
}
|
||||
|
||||
/// Sets the state for the specified album. If the previous state was
|
||||
/// `Loading` the associated task is cancelled.
|
||||
private func setAlbumState(state: AlbumState?, forAlbum album: Album) {
|
||||
if case let .Some(.Loading(task)) = albumStates[album] {
|
||||
task.cancel()
|
||||
}
|
||||
albumStates[album] = state
|
||||
}
|
||||
|
||||
public func isAlbumLoading(album: Album) -> Bool {
|
||||
if case .Some(.Loading) = albumStates[album] {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public func errorForAlbum(album: Album) -> NSError? {
|
||||
if case let .Some(.Error(error)) = albumStates[album] {
|
||||
return error
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
}
|
||||
@@ -37,7 +37,7 @@ public class AlbumTableCellView: AdvancedTableCellView {
|
||||
/// Contains the *tick* image for saved albums.
|
||||
@IBOutlet public lazy var secondaryImageView: NSImageView! = {
|
||||
let secondaryImageView = NSImageView()
|
||||
secondaryImageView.image = NSImage(named: "Tick")
|
||||
secondaryImageView.image = NSImage(named: "Tick")?.imageByMaskingWithColor(NSColor.clearColor())
|
||||
secondaryImageView.imageScaling = .ScaleProportionallyDown
|
||||
secondaryImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
return secondaryImageView
|
||||
@@ -118,9 +118,9 @@ public class AlbumTableCellView: AdvancedTableCellView {
|
||||
/// - loading: `true` if a loading indicator should be displayed at the
|
||||
/// album view.
|
||||
public func setupForAlbum(album: Album, loading: Bool, error: NSError?) {
|
||||
textField?.stringValue = album.name
|
||||
textField?.stringValue = Preferences.sharedPreferences.useCensoredNames ? album.censoredName : album.name
|
||||
secondaryTextField?.stringValue = album.artistName
|
||||
asyncImageView.downloadImageFromURL(album.artwork.hiResURL)
|
||||
asyncImageView.downloadImageFromURL(album.artwork.displayImageURL)
|
||||
if loading {
|
||||
textField?.textColor = NSColor.disabledControlTextColor()
|
||||
rightAccessoryView = loadingIndicator
|
||||
@@ -160,10 +160,10 @@ public class AlbumTableCellView: AdvancedTableCellView {
|
||||
/// - selectable: `true` if the search result can be selected, `false`
|
||||
/// otherwise.
|
||||
public func setupForSearchResult(searchResult: SearchResult, selectable: Bool) {
|
||||
textField?.stringValue = searchResult.name
|
||||
textField?.stringValue = Preferences.sharedPreferences.useCensoredNames ? searchResult.censoredName : searchResult.name
|
||||
textField?.textColor = NSColor.controlTextColor()
|
||||
secondaryTextField?.stringValue = searchResult.artistName
|
||||
asyncImageView.downloadImageFromURL(searchResult.artwork.hiResURL)
|
||||
asyncImageView.downloadImageFromURL(searchResult.artwork.displayImageURL)
|
||||
if selectable {
|
||||
button.title = NSLocalizedString("Select", comment: "Button title for 'selecting a search result'")
|
||||
button.enabled = true
|
||||
|
||||
@@ -54,9 +54,13 @@ public class Artwork: iTunesType {
|
||||
public func saveToURL(url: NSURL, filename: String) throws {
|
||||
let directory = url.filePathURL!.path!
|
||||
let filePath = directory.stringByAppendingString("/\(filename).tiff")
|
||||
|
||||
if !Preferences.sharedPreferences.overwriteExistingFiles && NSFileManager.defaultManager().fileExistsAtPath(filePath) {
|
||||
return
|
||||
}
|
||||
|
||||
try NSFileManager.defaultManager().createDirectoryAtPath(directory, withIntermediateDirectories: true, attributes: nil)
|
||||
let _ = NSFileManager.defaultManager().createFileAtPath(filePath, contents: hiResImage?.TIFFRepresentation, attributes: nil)
|
||||
let _ = NSFileManager.defaultManager().createFileAtPath(filePath, contents: saveImage?.TIFFRepresentation, attributes: nil)
|
||||
}
|
||||
|
||||
// MARK: Calculated Properties
|
||||
@@ -103,4 +107,18 @@ public class Artwork: iTunesType {
|
||||
return cachedHiResImage
|
||||
}
|
||||
|
||||
/// Returns the url of an image that should be used to display this artwork
|
||||
/// with respect to the user's preferences.
|
||||
public var displayImageURL: NSURL {
|
||||
if !Preferences.sharedPreferences.useLowResolutionArtwork && hiResURL != nil {
|
||||
return hiResURL
|
||||
}
|
||||
return url100
|
||||
}
|
||||
|
||||
/// Returns the image that should be used to save this artwork.
|
||||
public var saveImage: NSImage? {
|
||||
return hiResImage != nil ? hiResImage : image100
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,22 +2,14 @@
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "Cross.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "Cross-1.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "Cross-2.png",
|
||||
"scale" : "3x"
|
||||
"filename" : "Cross.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
BIN
TagTunes/Assets.xcassets/Cross.imageset/Cross-1.png
vendored
|
Before Width: | Height: | Size: 55 KiB |
BIN
TagTunes/Assets.xcassets/Cross.imageset/Cross-2.png
vendored
|
Before Width: | Height: | Size: 55 KiB |
BIN
TagTunes/Assets.xcassets/Cross.imageset/Cross.pdf
vendored
Normal file
@@ -2,7 +2,7 @@
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "PauseProgressFreestandingTemplate.pdf"
|
||||
"filename" : "Note.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
30
TagTunes/Assets.xcassets/Note.imageset/Note.pdf
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
%PDF-1.4
|
||||
%âãÏÓ
|
||||
1 0 obj <</Filter/FlateDecode/Length 376>>stream
|
||||
xœÍ“;N1†ûœÂ'Å<><|$
|
||||
*
|
||||
D5Z<>–†ëó;™<>Ý¥ƒ
|
||||
<EFBFBD>~ÄqìÌ1S&ÎbôÈtHCÙa,‡ôšîÓGbúJB7|ƒ—nÓÃc¦§Ë—{öDy[¯rýiK|Ÿ/©!/¾tlïÖ–ºiÝ«—Ë%¯Ér
|
||||
›Ë"H§Í©wƒ_‹Q/!…º8"ƒ¤TD‰æk{¹ÙÆ,
|
||||
„ÏÌ5ƒ×´iÙƒø€$ˆ8%"†‚rTOE†J
|
||||
5‡Í}FC©¡iÏ;Àç>®§<C2AE>—hbŽ+õ¸$<07>v¬°GTe5Ø!9¨£É<ý–ã86!ãŠÂã˜A>Zª5ÆÒU¨i½ <C2BD>VmÈhàšó CªÅ¢\pÍ1å¨or´wM“Ùc„3<1A>¤È¹P÷´áDáÙhŒMcWÃàZØ$ǹgîðlr‘“×ãÎ"ñblR™'+óè¦UÞ=Îí<07>(r‹~÷6ìóîsÅ›}Nw¿ú½þSt|ßnÙÈ
|
||||
endstream
|
||||
endobj
|
||||
3 0 obj<</Contents 1 0 R/Type/Page/Resources<</ProcSet [/PDF /Text /ImageB /ImageC /ImageI]>>/Parent 2 0 R/MediaBox[0 0 1024 1024]>>
|
||||
endobj
|
||||
2 0 obj<</Kids[3 0 R]/Type/Pages/Count 1>>
|
||||
endobj
|
||||
4 0 obj<</Type/Catalog/Pages 2 0 R>>
|
||||
endobj
|
||||
5 0 obj<</ModDate(D:20150903235938Z)/Creator(http://www.fileformat.info/convert/image/svg2pdf.htm)/CreationDate(D:20150903235938Z)/Producer(iText1.2.3 by lowagie.com \(based on itext-paulo-152\))>>
|
||||
endobj
|
||||
xref
|
||||
0 6
|
||||
0000000000 65535 f
|
||||
0000000015 00000 n
|
||||
0000000598 00000 n
|
||||
0000000458 00000 n
|
||||
0000000648 00000 n
|
||||
0000000692 00000 n
|
||||
trailer
|
||||
<</Info 5 0 R/ID [<e234db29b05ecfd32d6259a19dede815><e234db29b05ecfd32d6259a19dede815>]/Root 4 0 R/Size 6>>
|
||||
@@ -1,29 +0,0 @@
|
||||
%PDF-1.4
|
||||
%âãÏÓ
|
||||
1 0 obj <</Filter/FlateDecode/Length 236>>stream
|
||||
xœÍ’1N1E{ŸÂ'ˆl''W@¢ ¢@Û`„f‘–b¹>ÎLⶃ
|
||||
EJüœïoGÊ®@È$ /.°‚ë¶ÀÏðŒ_ ø`Âw»ÅGx9ž<>Ç7¢¾ÿðúSI[Ÿod–@q<>Y%äTCXÔˆñR<>Ëf<C38B>™CÕ-¾<>J5oTÔâ´WYÒÑM;övû}$
|
||||
”G9g£i·n”S}úP#Ñgör;ˆØÝW¤C÷¤p3<70>45ÁRÔÖ¡ó2Xµ´ñ—¡¿g×Ï<C397>KuÚ5q{‚+:»C×ßóÁñž~õ«þ“ºo«P˜K
|
||||
endstream
|
||||
endobj
|
||||
3 0 obj<</Contents 1 0 R/Type/Page/Resources<</ProcSet [/PDF /Text /ImageB /ImageC /ImageI]>>/Parent 2 0 R/MediaBox[0 0 1024 1024]>>
|
||||
endobj
|
||||
2 0 obj<</Kids[3 0 R]/Type/Pages/Count 1>>
|
||||
endobj
|
||||
4 0 obj<</Type/Catalog/Pages 2 0 R>>
|
||||
endobj
|
||||
5 0 obj<</ModDate(D:20150902100931Z)/Creator(http://www.fileformat.info/convert/image/svg2pdf.htm)/CreationDate(D:20150902100931Z)/Producer(iText1.2.3 by lowagie.com \(based on itext-paulo-152\))>>
|
||||
endobj
|
||||
xref
|
||||
0 6
|
||||
0000000000 65535 f
|
||||
0000000015 00000 n
|
||||
0000000458 00000 n
|
||||
0000000318 00000 n
|
||||
0000000508 00000 n
|
||||
0000000552 00000 n
|
||||
trailer
|
||||
<</Info 5 0 R/ID [<ae74877cb59949374c065f7f1deb2d92><ae74877cb59949374c065f7f1deb2d92>]/Root 4 0 R/Size 6>>
|
||||
startxref
|
||||
757
|
||||
%%EOF
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "PreferencesTags.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "PreferencesTags-1.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "PreferencesTags-2.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 558 KiB |
|
Before Width: | Height: | Size: 558 KiB |
|
Before Width: | Height: | Size: 558 KiB |
15
TagTunes/Assets.xcassets/Save.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "Save.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
29
TagTunes/Assets.xcassets/Save.imageset/Save.pdf
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
%PDF-1.4
|
||||
%âãÏÓ
|
||||
1 0 obj <</Filter/FlateDecode/Length 321>>stream
|
||||
xœ“;N1†{ŸÂÀ²<C380>Ø“\‰b+
|
||||
Dµ<1A> áú8™É<¨(ÐJ£|~üòk?á…5ã{<gè°=úg†W¸‡üÅÛ|/ÞÁÃ#ãSHd%•!S‹ÐTGæJMp‰Ú_'í0y>J$?J‰ž¯“D«Ÿñë¬&*›X–<58>fH¹—…Ršˆ›‡+™¬Ù[ÅWXÙÝÈ[]f<>IwjÒî,žáu¯ÇàkžzðŠÂ1D·BcÎǺ¯Ñ”F|ÖmHaA<>aøÖÿ ɉ“QjúbJÙ±P6”I(+Þ01³(ª¦fÞø
|
||||
E¸Im–ŠKëm³”Ð0lj¼`‰2rÅ”É,’×ÝhX‹µTã>–ÁS±>âha>öÓvö—åvöÅïû=,ý|
|
||||
Ë®/¿¯n?®ÃÅ<C383>Oñ?2ÿþg¹ôß=K§=
|
||||
endstream
|
||||
endobj
|
||||
3 0 obj<</Contents 1 0 R/Type/Page/Resources<</ProcSet [/PDF /Text /ImageB /ImageC /ImageI]>>/Parent 2 0 R/MediaBox[0 0 1024 1024]>>
|
||||
endobj
|
||||
2 0 obj<</Kids[3 0 R]/Type/Pages/Count 1>>
|
||||
endobj
|
||||
4 0 obj<</Type/Catalog/Pages 2 0 R>>
|
||||
endobj
|
||||
5 0 obj<</ModDate(D:20150904000250Z)/Creator(http://www.fileformat.info/convert/image/svg2pdf.htm)/CreationDate(D:20150904000250Z)/Producer(iText1.2.3 by lowagie.com \(based on itext-paulo-152\))>>
|
||||
endobj
|
||||
xref
|
||||
0 6
|
||||
0000000000 65535 f
|
||||
0000000015 00000 n
|
||||
0000000543 00000 n
|
||||
0000000403 00000 n
|
||||
0000000593 00000 n
|
||||
0000000637 00000 n
|
||||
trailer
|
||||
<</Info 5 0 R/ID [<e10888a855614e5c858d394112bdd7fb><e10888a855614e5c858d394112bdd7fb>]/Root 4 0 R/Size 6>>
|
||||
startxref
|
||||
842
|
||||
15
TagTunes/Assets.xcassets/SaveArtwork.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "SaveArtwork.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
BIN
TagTunes/Assets.xcassets/SaveArtwork.imageset/SaveArtwork.pdf
vendored
Normal file
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "SaveToITunes.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "SaveToITunes-1.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "SaveToITunes-2.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 781 KiB |
|
Before Width: | Height: | Size: 781 KiB |
15
TagTunes/Assets.xcassets/Tick.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "Tick.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
4125
TagTunes/Assets.xcassets/Tick.imageset/Tick.pdf
vendored
Normal file
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "TickBW.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "TickBW-1.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "TickBW-2.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 41 KiB |
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
},
|
||||
"data" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "iTunes-2.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "iTunes-1.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "iTunes.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 825 KiB |
|
Before Width: | Height: | Size: 825 KiB |
BIN
TagTunes/Assets.xcassets/iTunes.imageset/iTunes.png
vendored
|
Before Width: | Height: | Size: 825 KiB |
BIN
TagTunes/Base.lproj/Localizable.strings
Normal file
22
TagTunes/Base.lproj/Localizable.stringsdict
Normal file
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>%d artworks could not be saved.</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@artworks@ could not be saved</string>
|
||||
<key>artworks</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>d</string>
|
||||
<key>one</key>
|
||||
<string>One artwork</string>
|
||||
<key>other</key>
|
||||
<string>%d artworks</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,7 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="8173.3" systemVersion="14F27" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="8191" systemVersion="15A282a" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="8173.3"/>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="8191"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Application-->
|
||||
@@ -664,29 +665,36 @@ CA
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="1680" height="1027"/>
|
||||
<toolbar key="toolbar" implicitIdentifier="77DCE6FB-61CD-40EE-B6F9-02081D2DE8BE" autosavesConfiguration="NO" displayMode="iconAndLabel" sizeMode="regular" id="jCk-5k-5x7">
|
||||
<allowedToolbarItems>
|
||||
<toolbarItem implicitItemIdentifier="694E800B-64CF-416A-A82D-2E13BB23AEC4" label="Add Selection" paletteLabel="Add iTunes Selection" tag="-1" image="iTunes" id="vfK-go-3oR">
|
||||
<toolbarItem implicitItemIdentifier="694E800B-64CF-416A-A82D-2E13BB23AEC4" label="Add Selection" paletteLabel="Add iTunes Selection" tag="-1" image="Note" id="vfK-go-3oR">
|
||||
<connections>
|
||||
<action selector="addITunesSelection:" target="Oky-zY-oP4" id="G9h-yp-J9e"/>
|
||||
</connections>
|
||||
</toolbarItem>
|
||||
<toolbarItem implicitItemIdentifier="573468FC-979E-4370-A98C-49CB9C00BE09" label="Save" paletteLabel="Save to iTunes" tag="-1" image="SaveToITunes" id="ax1-DR-t4v">
|
||||
<toolbarItem implicitItemIdentifier="573468FC-979E-4370-A98C-49CB9C00BE09" label="Save" paletteLabel="Save to iTunes" tag="-1" image="Save" id="ax1-DR-t4v">
|
||||
<connections>
|
||||
<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>
|
||||
@@ -725,11 +733,13 @@ CA
|
||||
<tabViewController tabStyle="toolbar" id="qfM-ma-lyn" customClass="PreferencesViewController" customModule="AppKitPlus" sceneMemberID="viewController">
|
||||
<tabViewItems>
|
||||
<tabViewItem image="NSPreferencesGeneral" id="YSn-2h-2B5"/>
|
||||
<tabViewItem image="PreferenceTags" id="6Mu-Ps-mDj"/>
|
||||
</tabViewItems>
|
||||
<viewControllerTransitionOptions key="transitionOptions" allowUserInteraction="YES"/>
|
||||
<tabView key="tabView" type="noTabsNoBorder" id="UTE-Sb-ieY">
|
||||
<rect key="frame" x="0.0" y="0.0" width="450" height="300"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<animations/>
|
||||
<font key="font" metaFont="message"/>
|
||||
<tabViewItems/>
|
||||
<connections>
|
||||
@@ -738,6 +748,7 @@ CA
|
||||
</tabView>
|
||||
<connections>
|
||||
<segue destination="tzd-4a-CRb" kind="relationship" relationship="tabItems" id="8pq-ZH-tzf"/>
|
||||
<segue destination="qlM-h3-Tfw" kind="relationship" relationship="tabItems" id="Qgp-Md-Wfq"/>
|
||||
</connections>
|
||||
</tabViewController>
|
||||
<customObject id="5zh-wl-nwU" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
@@ -749,25 +760,27 @@ CA
|
||||
<objects>
|
||||
<viewController title="General" id="tzd-4a-CRb" customClass="GeneralPreferencesViewController" customModule="TagTunes" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" id="pOc-Vr-IET" customClass="PreferenceView" customModule="AppKitPlus">
|
||||
<rect key="frame" x="0.0" y="0.0" width="450" height="128"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="450" height="222"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<button translatesAutoresizingMaskIntoConstraints="NO" id="kl3-n8-T9b">
|
||||
<rect key="frame" x="18" y="92" width="105" height="18"/>
|
||||
<buttonCell key="cell" type="check" title="Save Artwork" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="q2l-4t-mL0">
|
||||
<rect key="frame" x="18" y="186" width="188" height="18"/>
|
||||
<animations/>
|
||||
<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>
|
||||
<connections>
|
||||
<action selector="saveArtworkStateChanged:" target="tzd-4a-CRb" id="qQu-K2-wWk"/>
|
||||
<binding destination="aSx-iH-PLA" name="value" keyPath="saveArtwork" id="1An-Cl-B1c"/>
|
||||
<binding destination="aSx-iH-PLA" name="value" keyPath="sharedPreferences.saveArtwork" id="Pif-70-Zto"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="1fj-p7-sMs">
|
||||
<rect key="frame" x="335" y="58" width="101" height="32"/>
|
||||
<rect key="frame" x="335" y="152" width="101" height="32"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="89" id="WSP-Sc-NcB"/>
|
||||
<constraint firstAttribute="width" constant="89" id="EKB-5X-IoR"/>
|
||||
</constraints>
|
||||
<animations/>
|
||||
<buttonCell key="cell" type="push" title="Choose…" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="A5S-ps-EYW">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
@@ -776,55 +789,101 @@ CA
|
||||
<action selector="chooseArtworkPath:" target="tzd-4a-CRb" id="v67-Ay-ckG"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button translatesAutoresizingMaskIntoConstraints="NO" id="MSr-08-ucR">
|
||||
<rect key="frame" x="18" y="40" width="151" height="18"/>
|
||||
<buttonCell key="cell" type="check" title="Keep Search Results" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="uED-ee-Oc7">
|
||||
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<binding destination="aSx-iH-PLA" name="value" keyPath="keepSearchResults" id="Nwn-Ik-Zdy"/>
|
||||
</connections>
|
||||
</button>
|
||||
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" setsMaxLayoutWidthAtFirstLayout="YES" translatesAutoresizingMaskIntoConstraints="NO" id="QNf-Uy-bMT">
|
||||
<rect key="frame" x="30" y="20" width="402" height="14"/>
|
||||
<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">
|
||||
<font key="font" metaFont="smallSystem"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<pathControl verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" allowsExpansionToolTips="YES" translatesAutoresizingMaskIntoConstraints="NO" id="pT8-NA-x3W">
|
||||
<rect key="frame" x="20" y="64" width="313" height="22"/>
|
||||
<rect key="frame" x="20" y="158" width="313" height="22"/>
|
||||
<animations/>
|
||||
<pathCell key="cell" selectable="YES" alignment="left" id="Sgq-Mk-WnH">
|
||||
<font key="font" metaFont="system"/>
|
||||
<url key="url" string="file:///Applications/"/>
|
||||
</pathCell>
|
||||
<connections>
|
||||
<binding destination="aSx-iH-PLA" name="value" keyPath="artworkTarget" id="wEo-ga-KAB"/>
|
||||
<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>
|
||||
<button translatesAutoresizingMaskIntoConstraints="NO" id="iE4-HP-hS8">
|
||||
<rect key="frame" x="18" y="134" width="160" height="18"/>
|
||||
<animations/>
|
||||
<buttonCell key="cell" type="check" title="Overwrite existing files" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="WQN-Bj-me1">
|
||||
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<binding destination="aSx-iH-PLA" name="value" keyPath="sharedPreferences.overwriteExistingFiles" id="X5P-ch-dPk"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button translatesAutoresizingMaskIntoConstraints="NO" id="MSr-08-ucR">
|
||||
<rect key="frame" x="18" y="112" width="147" height="18"/>
|
||||
<animations/>
|
||||
<buttonCell key="cell" type="check" title="Keep Search Results" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="uED-ee-Oc7">
|
||||
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<binding destination="aSx-iH-PLA" name="value" keyPath="sharedPreferences.keepSearchResults" id="52h-XX-KDr"/>
|
||||
</connections>
|
||||
</button>
|
||||
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" setsMaxLayoutWidthAtFirstLayout="YES" translatesAutoresizingMaskIntoConstraints="NO" id="QNf-Uy-bMT">
|
||||
<rect key="frame" x="30" y="92" width="402" height="14"/>
|
||||
<animations/>
|
||||
<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"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<button translatesAutoresizingMaskIntoConstraints="NO" id="HT2-V6-Vrs">
|
||||
<rect key="frame" x="18" y="68" width="184" height="18"/>
|
||||
<animations/>
|
||||
<buttonCell key="cell" type="check" title="Use low resolution artwork" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="pp5-kZ-w5f">
|
||||
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<binding destination="aSx-iH-PLA" name="value" keyPath="sharedPreferences.useLowResolutionArtwork" id="pFX-Hd-IVH"/>
|
||||
</connections>
|
||||
</button>
|
||||
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" setsMaxLayoutWidthAtFirstLayout="YES" translatesAutoresizingMaskIntoConstraints="NO" id="ODz-LZ-Acs">
|
||||
<rect key="frame" x="30" y="20" width="402" height="42"/>
|
||||
<animations/>
|
||||
<textFieldCell key="cell" controlSize="small" sendsActionOnEndEditing="YES" id="NMm-Jw-bon">
|
||||
<font key="font" metaFont="smallSystem"/>
|
||||
<string key="title">This option one affects artwork displayed in TagTunes. Saved artwork will always have full resolution. Using this option may make searching faster but will also slow down saving.</string>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="QNf-Uy-bMT" firstAttribute="top" secondItem="MSr-08-ucR" secondAttribute="bottom" constant="8" symbolic="YES" id="3Vk-P7-72n"/>
|
||||
<constraint firstItem="kl3-n8-T9b" firstAttribute="leading" secondItem="pOc-Vr-IET" secondAttribute="leading" constant="20" symbolic="YES" id="5VS-q6-Nyw"/>
|
||||
<constraint firstAttribute="trailing" secondItem="1fj-p7-sMs" secondAttribute="trailing" constant="20" symbolic="YES" id="Np8-sr-aF0"/>
|
||||
<constraint firstItem="1fj-p7-sMs" firstAttribute="baseline" secondItem="pT8-NA-x3W" secondAttribute="baseline" id="c2l-d6-00D"/>
|
||||
<constraint firstItem="MSr-08-ucR" firstAttribute="leading" secondItem="pT8-NA-x3W" secondAttribute="leading" id="dAH-m2-853"/>
|
||||
<constraint firstItem="pT8-NA-x3W" firstAttribute="leading" secondItem="kl3-n8-T9b" secondAttribute="leading" id="dCN-mx-bY1"/>
|
||||
<constraint firstItem="1fj-p7-sMs" firstAttribute="leading" secondItem="pT8-NA-x3W" secondAttribute="trailing" constant="8" symbolic="YES" id="ejc-iL-QON"/>
|
||||
<constraint firstItem="kl3-n8-T9b" firstAttribute="top" secondItem="pOc-Vr-IET" secondAttribute="top" constant="20" symbolic="YES" id="h13-nP-neh"/>
|
||||
<constraint firstItem="pT8-NA-x3W" firstAttribute="top" secondItem="kl3-n8-T9b" secondAttribute="bottom" constant="8" symbolic="YES" id="qdu-oL-zyh"/>
|
||||
<constraint firstItem="MSr-08-ucR" firstAttribute="top" secondItem="pT8-NA-x3W" secondAttribute="bottom" constant="8" symbolic="YES" id="sSO-jr-zo1"/>
|
||||
<constraint firstItem="QNf-Uy-bMT" firstAttribute="trailing" secondItem="1fj-p7-sMs" secondAttribute="trailing" id="tri-hX-smF"/>
|
||||
<constraint firstItem="QNf-Uy-bMT" firstAttribute="leading" secondItem="MSr-08-ucR" secondAttribute="leading" constant="12" id="tyS-Y5-FOc"/>
|
||||
<constraint firstItem="kl3-n8-T9b" firstAttribute="leading" secondItem="pT8-NA-x3W" secondAttribute="leading" id="14U-Qy-cIb"/>
|
||||
<constraint firstItem="1fj-p7-sMs" firstAttribute="trailing" secondItem="QNf-Uy-bMT" secondAttribute="trailing" id="1dO-X0-RCl"/>
|
||||
<constraint firstAttribute="bottom" secondItem="ODz-LZ-Acs" secondAttribute="bottom" constant="20" symbolic="YES" id="3xM-bh-eY2"/>
|
||||
<constraint firstItem="ODz-LZ-Acs" firstAttribute="top" secondItem="HT2-V6-Vrs" secondAttribute="bottom" constant="8" symbolic="YES" id="DkF-kz-drm"/>
|
||||
<constraint firstItem="HT2-V6-Vrs" firstAttribute="top" secondItem="QNf-Uy-bMT" secondAttribute="bottom" constant="8" symbolic="YES" id="HMN-Go-Bye"/>
|
||||
<constraint firstItem="QNf-Uy-bMT" firstAttribute="leading" secondItem="ODz-LZ-Acs" secondAttribute="leading" id="JqT-1C-LFg"/>
|
||||
<constraint firstItem="pT8-NA-x3W" firstAttribute="baseline" secondItem="1fj-p7-sMs" secondAttribute="baseline" id="LqN-ud-IWM"/>
|
||||
<constraint firstItem="kl3-n8-T9b" firstAttribute="leading" secondItem="pOc-Vr-IET" secondAttribute="leading" constant="20" symbolic="YES" id="NSX-jk-ITr"/>
|
||||
<constraint firstItem="MSr-08-ucR" firstAttribute="leading" secondItem="iE4-HP-hS8" secondAttribute="leading" id="Ott-bf-Jd3"/>
|
||||
<constraint firstAttribute="trailing" secondItem="1fj-p7-sMs" secondAttribute="trailing" constant="20" symbolic="YES" id="Ucr-v6-obP"/>
|
||||
<constraint firstItem="iE4-HP-hS8" firstAttribute="leading" secondItem="pT8-NA-x3W" secondAttribute="leading" id="cZq-iz-phO"/>
|
||||
<constraint firstItem="iE4-HP-hS8" firstAttribute="top" secondItem="pT8-NA-x3W" secondAttribute="bottom" constant="8" symbolic="YES" id="g9V-s8-6a8"/>
|
||||
<constraint firstItem="QNf-Uy-bMT" firstAttribute="top" secondItem="MSr-08-ucR" secondAttribute="bottom" constant="8" symbolic="YES" id="l11-R3-ejh"/>
|
||||
<constraint firstItem="QNf-Uy-bMT" firstAttribute="leading" secondItem="pOc-Vr-IET" secondAttribute="leading" constant="32" id="mZA-pG-v6r"/>
|
||||
<constraint firstItem="kl3-n8-T9b" firstAttribute="top" secondItem="pOc-Vr-IET" secondAttribute="top" constant="20" symbolic="YES" id="pRK-sJ-oMw"/>
|
||||
<constraint firstItem="1fj-p7-sMs" firstAttribute="leading" secondItem="pT8-NA-x3W" secondAttribute="trailing" constant="8" symbolic="YES" id="qbN-IH-9co"/>
|
||||
<constraint firstItem="pT8-NA-x3W" firstAttribute="top" secondItem="kl3-n8-T9b" secondAttribute="bottom" constant="8" symbolic="YES" id="xAH-yb-aOW"/>
|
||||
<constraint firstItem="MSr-08-ucR" firstAttribute="leading" secondItem="HT2-V6-Vrs" secondAttribute="leading" id="xmI-IP-qsj"/>
|
||||
</constraints>
|
||||
<animations/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="number" keyPath="width">
|
||||
<real key="value" value="450"/>
|
||||
</userDefinedRuntimeAttribute>
|
||||
<userDefinedRuntimeAttribute type="number" keyPath="height">
|
||||
<real key="value" value="128"/>
|
||||
<real key="value" value="222"/>
|
||||
</userDefinedRuntimeAttribute>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</view>
|
||||
@@ -834,9 +893,228 @@ CA
|
||||
</connections>
|
||||
</viewController>
|
||||
<customObject id="RtZ-4O-Pgk" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
<customObject id="aSx-iH-PLA" customClass="Preferences" customModule="TagTunes" customModuleProvider="target"/>
|
||||
<customObject id="aSx-iH-PLA" userLabel="Preferences" customClass="PreferencesSingleton" customModule="TagTunes" customModuleProvider="target"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-1031" y="1051"/>
|
||||
<point key="canvasLocation" x="-1031" y="1098"/>
|
||||
</scene>
|
||||
<!--Tags-->
|
||||
<scene sceneID="oNy-iK-NsG">
|
||||
<objects>
|
||||
<viewController title="Tags" id="qlM-h3-Tfw" customClass="TagsPreferencesViewController" customModule="TagTunes" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" id="caz-ze-bnI" customClass="PreferenceView" customModule="AppKitPlus">
|
||||
<rect key="frame" x="0.0" y="0.0" width="450" height="340"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<button translatesAutoresizingMaskIntoConstraints="NO" id="0Za-Q2-FET">
|
||||
<rect key="frame" x="18" y="304" width="154" height="18"/>
|
||||
<animations/>
|
||||
<buttonCell key="cell" type="check" title="Use Censored Names" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="UOJ-Ol-UjW">
|
||||
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<binding destination="7Ts-a9-Cpv" name="value" keyPath="sharedPreferences.useCensoredNames" id="oYs-AD-vbO">
|
||||
<dictionary key="options">
|
||||
<bool key="NSConditionallySetsEnabled" value="NO"/>
|
||||
</dictionary>
|
||||
</binding>
|
||||
</connections>
|
||||
</button>
|
||||
<button translatesAutoresizingMaskIntoConstraints="NO" id="Rru-2X-J52">
|
||||
<rect key="frame" x="18" y="284" width="111" height="18"/>
|
||||
<animations/>
|
||||
<buttonCell key="cell" type="check" title="Case Sensitive" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="Ttb-GR-JAy">
|
||||
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<binding destination="7Ts-a9-Cpv" name="value" keyPath="sharedPreferences.caseSensitive" id="lFI-W5-ve4"/>
|
||||
</connections>
|
||||
</button>
|
||||
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" setsMaxLayoutWidthAtFirstLayout="YES" translatesAutoresizingMaskIntoConstraints="NO" id="wkM-Sb-z0R">
|
||||
<rect key="frame" x="18" y="202" width="414" height="34"/>
|
||||
<animations/>
|
||||
<textFieldCell key="cell" sendsActionOnEndEditing="YES" title="Some tags are not provided by the iTunes Search API. TagTunes can only clear these tags." id="8L8-Nr-zgu">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<scrollView autohidesScrollers="YES" horizontalLineScroll="23" horizontalPageScroll="10" verticalLineScroll="23" verticalPageScroll="10" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="cTv-EP-MQX">
|
||||
<rect key="frame" x="20" y="20" width="410" height="174"/>
|
||||
<clipView key="contentView" id="awx-Qa-0a0">
|
||||
<rect key="frame" x="1" y="1" width="408" height="172"/>
|
||||
<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="0.0"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<animations/>
|
||||
<size key="intercellSpacing" width="3" height="2"/>
|
||||
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="gridColor" name="gridColor" catalog="System" colorSpace="catalog"/>
|
||||
<tableColumns>
|
||||
<tableColumn identifier="tagColumn" editable="NO" width="292" minWidth="40" maxWidth="1000" id="GgR-1E-8ur">
|
||||
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border">
|
||||
<font key="font" metaFont="smallSystem"/>
|
||||
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="headerColor" catalog="System" colorSpace="catalog"/>
|
||||
</tableHeaderCell>
|
||||
<textFieldCell key="dataCell" lineBreakMode="truncatingTail" selectable="YES" editable="YES" title="Text Cell" id="QH5-6N-kJ8">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
|
||||
<prototypeCellViews>
|
||||
<tableCellView identifier="textCell" id="Zbr-i2-c1G">
|
||||
<rect key="frame" x="1" y="1" width="292" height="17"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="vww-dF-uIe">
|
||||
<rect key="frame" x="0.0" y="0.0" width="211" height="17"/>
|
||||
<animations/>
|
||||
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="Table View Cell" id="hxe-jr-7rI">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="vww-dF-uIe" firstAttribute="centerY" secondItem="Zbr-i2-c1G" secondAttribute="centerY" id="GmY-MF-H9J"/>
|
||||
<constraint firstAttribute="trailing" secondItem="vww-dF-uIe" secondAttribute="trailing" constant="83" id="Zem-p3-9DR"/>
|
||||
<constraint firstItem="vww-dF-uIe" firstAttribute="leading" secondItem="Zbr-i2-c1G" secondAttribute="leading" constant="2" id="ib0-Lg-5Ts"/>
|
||||
</constraints>
|
||||
<animations/>
|
||||
<connections>
|
||||
<outlet property="textField" destination="vww-dF-uIe" id="a8W-tw-hGo"/>
|
||||
</connections>
|
||||
</tableCellView>
|
||||
</prototypeCellViews>
|
||||
</tableColumn>
|
||||
<tableColumn identifier="savingBehaviorColumn" width="110" minWidth="40" maxWidth="1000" id="IPU-Kp-l3A">
|
||||
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border">
|
||||
<font key="font" metaFont="smallSystem"/>
|
||||
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="headerColor" catalog="System" colorSpace="catalog"/>
|
||||
</tableHeaderCell>
|
||||
<popUpButtonCell key="dataCell" type="bevel" title="Ignore" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="bezel" imageScaling="proportionallyDown" inset="2" arrowPosition="arrowAtCenter" preferredEdge="maxY" selectedItem="idt-SA-nnz" id="1Sz-1n-thR">
|
||||
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="menu"/>
|
||||
<menu key="menu" id="3zQ-Pc-Wgf">
|
||||
<items>
|
||||
<menuItem title="Save" id="dA3-hx-uqV"/>
|
||||
<menuItem title="Clear" id="ZNC-Us-FDj"/>
|
||||
<menuItem title="Ignore" state="on" id="idt-SA-nnz"/>
|
||||
</items>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="qlM-h3-Tfw" id="dAa-aM-qIx"/>
|
||||
</connections>
|
||||
</menu>
|
||||
</popUpButtonCell>
|
||||
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
|
||||
<prototypeCellViews>
|
||||
<popUpButton identifier="popupCell" id="Cxj-hO-zFk">
|
||||
<rect key="frame" x="296" y="1" width="110" height="21"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<animations/>
|
||||
<popUpButtonCell key="cell" type="bevel" title="Ignore" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" imageScaling="proportionallyDown" inset="2" selectedItem="Zxi-Bn-dST" id="Och-nm-1Kl">
|
||||
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="menu"/>
|
||||
<menu key="menu" id="E1q-CP-zfN">
|
||||
<items>
|
||||
<menuItem title="Save" id="zVz-3W-UA1"/>
|
||||
<menuItem title="Clear" id="5fq-yr-okX"/>
|
||||
<menuItem title="Ignore" state="on" id="Zxi-Bn-dST"/>
|
||||
</items>
|
||||
</menu>
|
||||
</popUpButtonCell>
|
||||
<connections>
|
||||
<action selector="savingBehaviorChanged:" target="qlM-h3-Tfw" id="B4Y-w0-fay"/>
|
||||
</connections>
|
||||
</popUpButton>
|
||||
</prototypeCellViews>
|
||||
</tableColumn>
|
||||
</tableColumns>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="qlM-h3-Tfw" id="QYi-UE-Crv"/>
|
||||
<outlet property="delegate" destination="qlM-h3-Tfw" id="yBy-gW-UJu"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
</subviews>
|
||||
<animations/>
|
||||
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</clipView>
|
||||
<animations/>
|
||||
<scroller key="horizontalScroller" hidden="YES" verticalHuggingPriority="750" horizontal="YES" id="Q38-Np-JBF">
|
||||
<rect key="frame" x="1" y="-15" width="0.0" height="16"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<animations/>
|
||||
</scroller>
|
||||
<scroller key="verticalScroller" hidden="YES" verticalHuggingPriority="750" horizontal="NO" id="255-EG-UwP">
|
||||
<rect key="frame" x="-15" y="17" width="16" height="0.0"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<animations/>
|
||||
</scroller>
|
||||
</scrollView>
|
||||
<button translatesAutoresizingMaskIntoConstraints="NO" id="TQ1-zY-V5i">
|
||||
<rect key="frame" x="18" y="264" width="112" height="18"/>
|
||||
<animations/>
|
||||
<buttonCell key="cell" type="check" title="Clear Artworks" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="Upq-Aq-AbA">
|
||||
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<binding destination="7Ts-a9-Cpv" name="value" keyPath="sharedPreferences.clearArtworks" id="bmf-Jz-MbE"/>
|
||||
</connections>
|
||||
</button>
|
||||
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" setsMaxLayoutWidthAtFirstLayout="YES" translatesAutoresizingMaskIntoConstraints="NO" id="SC1-rF-IBT">
|
||||
<rect key="frame" x="30" y="244" width="402" height="14"/>
|
||||
<animations/>
|
||||
<textFieldCell key="cell" controlSize="small" sendsActionOnEndEditing="YES" title="If selected, TagTunes will remove all artworks from saved tracks." id="Wg2-d3-xkK">
|
||||
<font key="font" metaFont="smallSystem"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="Rru-2X-J52" firstAttribute="leading" secondItem="0Za-Q2-FET" secondAttribute="leading" id="3LQ-xG-Kah"/>
|
||||
<constraint firstItem="0Za-Q2-FET" firstAttribute="top" secondItem="caz-ze-bnI" secondAttribute="top" constant="20" symbolic="YES" id="4hi-D6-aIL"/>
|
||||
<constraint firstItem="cTv-EP-MQX" firstAttribute="leading" secondItem="caz-ze-bnI" secondAttribute="leading" constant="20" symbolic="YES" id="7Ck-cS-n92"/>
|
||||
<constraint firstAttribute="bottom" secondItem="cTv-EP-MQX" secondAttribute="bottom" constant="20" symbolic="YES" id="Lvm-eR-ruw"/>
|
||||
<constraint firstAttribute="trailing" secondItem="wkM-Sb-z0R" secondAttribute="trailing" constant="20" symbolic="YES" id="PzL-bB-J8O"/>
|
||||
<constraint firstItem="SC1-rF-IBT" firstAttribute="top" secondItem="TQ1-zY-V5i" secondAttribute="bottom" constant="8" symbolic="YES" id="TJe-Ve-c8M"/>
|
||||
<constraint firstItem="SC1-rF-IBT" firstAttribute="leading" secondItem="TQ1-zY-V5i" secondAttribute="leading" constant="12" id="XLc-jZ-y2v"/>
|
||||
<constraint firstAttribute="trailing" secondItem="SC1-rF-IBT" secondAttribute="trailing" constant="20" symbolic="YES" id="YB7-zz-FUJ"/>
|
||||
<constraint firstItem="0Za-Q2-FET" firstAttribute="leading" secondItem="wkM-Sb-z0R" secondAttribute="leading" id="bGY-79-Gc5"/>
|
||||
<constraint firstItem="cTv-EP-MQX" firstAttribute="top" secondItem="wkM-Sb-z0R" secondAttribute="bottom" constant="8" symbolic="YES" id="cWo-Xz-1Al"/>
|
||||
<constraint firstAttribute="trailing" secondItem="cTv-EP-MQX" secondAttribute="trailing" constant="20" symbolic="YES" id="dMs-jm-ikl"/>
|
||||
<constraint firstItem="TQ1-zY-V5i" firstAttribute="top" secondItem="Rru-2X-J52" secondAttribute="bottom" constant="6" symbolic="YES" id="i7K-Hi-0SC"/>
|
||||
<constraint firstItem="wkM-Sb-z0R" firstAttribute="top" secondItem="SC1-rF-IBT" secondAttribute="bottom" constant="8" symbolic="YES" id="nio-Qc-p1E"/>
|
||||
<constraint firstItem="Rru-2X-J52" firstAttribute="top" secondItem="0Za-Q2-FET" secondAttribute="bottom" constant="6" symbolic="YES" id="w50-BO-5gG"/>
|
||||
<constraint firstItem="TQ1-zY-V5i" firstAttribute="leading" secondItem="caz-ze-bnI" secondAttribute="leading" constant="20" symbolic="YES" id="yFD-W0-CuJ"/>
|
||||
<constraint firstItem="0Za-Q2-FET" firstAttribute="leading" secondItem="caz-ze-bnI" secondAttribute="leading" constant="20" symbolic="YES" id="zoK-yx-jUT"/>
|
||||
</constraints>
|
||||
<animations/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="number" keyPath="width">
|
||||
<real key="value" value="450"/>
|
||||
</userDefinedRuntimeAttribute>
|
||||
<userDefinedRuntimeAttribute type="number" keyPath="height">
|
||||
<real key="value" value="340"/>
|
||||
</userDefinedRuntimeAttribute>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="tableView" destination="WCb-HH-YPh" id="Jje-KL-XOf"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<customObject id="w7s-ZN-87L" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
<customObject id="7Ts-a9-Cpv" userLabel="Preferences" customClass="PreferencesSingleton" customModule="TagTunes" customModuleProvider="target"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-470" y="1157"/>
|
||||
</scene>
|
||||
<!--Main View Controller-->
|
||||
<scene sceneID="hIz-AP-VOD">
|
||||
@@ -848,6 +1126,7 @@ CA
|
||||
<subviews>
|
||||
<searchField wantsLayer="YES" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="JF0-Xb-DqY">
|
||||
<rect key="frame" x="20" y="348" width="652" height="22"/>
|
||||
<animations/>
|
||||
<searchFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" borderStyle="bezel" usesSingleLineMode="YES" bezelStyle="round" sendsWholeSearchString="YES" id="iQi-K0-yFr">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
@@ -857,15 +1136,16 @@ CA
|
||||
<action selector="performSearch:" target="XfG-lQ-9wD" id="zNC-V3-DNP"/>
|
||||
</connections>
|
||||
</searchField>
|
||||
<scrollView autohidesScrollers="YES" horizontalLineScroll="33" horizontalPageScroll="10" verticalLineScroll="33" verticalPageScroll="10" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="zXR-px-hjg">
|
||||
<scrollView autohidesScrollers="YES" horizontalLineScroll="19" horizontalPageScroll="10" verticalLineScroll="19" verticalPageScroll="10" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="zXR-px-hjg">
|
||||
<rect key="frame" x="20" y="20" width="652" height="320"/>
|
||||
<clipView key="contentView" id="CyK-XI-Ggy">
|
||||
<rect key="frame" x="1" y="1" width="650" height="318"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<outlineView verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnAutoresizingStyle="lastColumnOnly" columnReordering="NO" autosaveColumns="NO" rowHeight="31" rowSizeStyle="automatic" viewBased="YES" indentationPerLevel="16" outlineTableColumn="p3C-E5-sdk" id="1Vy-Gq-TWU">
|
||||
<outlineView verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnAutoresizingStyle="lastColumnOnly" columnReordering="NO" autosaveColumns="NO" rowSizeStyle="automatic" viewBased="YES" indentationPerLevel="16" outlineTableColumn="p3C-E5-sdk" id="1Vy-Gq-TWU">
|
||||
<rect key="frame" x="0.0" y="0.0" width="650" height="0.0"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<animations/>
|
||||
<size key="intercellSpacing" width="3" height="2"/>
|
||||
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="gridColor" name="gridColor" catalog="System" colorSpace="catalog"/>
|
||||
@@ -890,15 +1170,19 @@ CA
|
||||
</connections>
|
||||
</outlineView>
|
||||
</subviews>
|
||||
<animations/>
|
||||
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</clipView>
|
||||
<scroller key="horizontalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" horizontal="YES" id="vEh-oN-xXy">
|
||||
<animations/>
|
||||
<scroller key="horizontalScroller" hidden="YES" verticalHuggingPriority="750" horizontal="YES" id="vEh-oN-xXy">
|
||||
<rect key="frame" x="1" y="303" width="650" height="16"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<animations/>
|
||||
</scroller>
|
||||
<scroller key="verticalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" horizontal="NO" id="iqM-dh-v73">
|
||||
<scroller key="verticalScroller" hidden="YES" verticalHuggingPriority="750" horizontal="NO" id="iqM-dh-v73">
|
||||
<rect key="frame" x="224" y="17" width="15" height="102"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<animations/>
|
||||
</scroller>
|
||||
</scrollView>
|
||||
</subviews>
|
||||
@@ -911,6 +1195,7 @@ CA
|
||||
<constraint firstItem="zXR-px-hjg" firstAttribute="top" secondItem="JF0-Xb-DqY" secondAttribute="bottom" constant="8" symbolic="YES" id="pKp-Ad-Sr5"/>
|
||||
<constraint firstItem="zXR-px-hjg" firstAttribute="trailing" secondItem="JF0-Xb-DqY" secondAttribute="trailing" id="r36-CQ-K22"/>
|
||||
</constraints>
|
||||
<animations/>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="outlineView" destination="1Vy-Gq-TWU" id="vRG-b2-ocW"/>
|
||||
@@ -922,9 +1207,11 @@ CA
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="Cross" width="245.75999450683594" height="245.75999450683594"/>
|
||||
<image name="Cross" width="1024" height="1024"/>
|
||||
<image name="NSPreferencesGeneral" width="32" height="32"/>
|
||||
<image name="SaveToITunes" width="512" height="512"/>
|
||||
<image name="iTunes" width="512" height="512"/>
|
||||
<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>
|
||||
|
||||
60
TagTunes/DescriptiveError.swift
Normal file
@@ -0,0 +1,60 @@
|
||||
//
|
||||
// DescriptiveError.swift
|
||||
// TagTunes
|
||||
//
|
||||
// Created by Kim Wittenburg on 06.09.15.
|
||||
// Copyright © 2015 Kim Wittenburg. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// A custom error class that wraps another error to display a more descriptive
|
||||
/// error description than for example "The operation couldn't be completed."
|
||||
public class DescriptiveError: NSError {
|
||||
|
||||
/// Initializes the receiver with the specified `underlyingError`.
|
||||
/// The underlying error is added to the receiver's `userInfo` dictionary as
|
||||
/// is the specified dictionary.
|
||||
public init(underlyingError: NSError, userInfo: [NSString: AnyObject]?) {
|
||||
var actualUserInfo = userInfo ?? [NSString: AnyObject]()
|
||||
actualUserInfo[NSUnderlyingErrorKey] = underlyingError
|
||||
super.init(domain: underlyingError.domain, code: underlyingError.code, userInfo: actualUserInfo)
|
||||
}
|
||||
|
||||
public required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("DescriptiveError instances should not be encoded.")
|
||||
}
|
||||
|
||||
/// Returns the value for the NSUnderlyingErrorKey in the error's `userInfo`
|
||||
/// dictionary as an `NSError` instance.
|
||||
public var underlyingError: NSError {
|
||||
return userInfo[NSUnderlyingErrorKey] as! NSError
|
||||
}
|
||||
|
||||
override public var localizedDescription: String {
|
||||
if domain == NSURLErrorDomain {
|
||||
switch code {
|
||||
case NSURLErrorNotConnectedToInternet:
|
||||
return NSLocalizedString("You are not connected to the internet.", comment: "Error message informing the user that he is not connected to the internet.")
|
||||
case NSURLErrorTimedOut:
|
||||
return NSLocalizedString("The network request timed out.", comment: "Error message informing the user that the network request timed out.")
|
||||
default: break
|
||||
}
|
||||
}
|
||||
return super.localizedDescription
|
||||
}
|
||||
|
||||
override public var localizedRecoverySuggestion: String? {
|
||||
if domain == NSURLErrorDomain {
|
||||
switch code {
|
||||
case NSURLErrorNotConnectedToInternet:
|
||||
return NSLocalizedString("Please check your network connection and try again.", comment: "Error recovery suggestion for 'not connected to the internet' error.")
|
||||
case NSURLErrorTimedOut:
|
||||
return NSLocalizedString("Please check your network connection and try again. If that does not help it may be possible that Apple's Search API service is currently offline. Please try again later.", comment: "Error recovery suggestion for 'time out' error.")
|
||||
default: break
|
||||
}
|
||||
}
|
||||
return super.localizedRecoverySuggestion
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
//
|
||||
// Error Handler.swift
|
||||
// TagTunes
|
||||
//
|
||||
// Created by Kim Wittenburg on 30.08.15.
|
||||
// Copyright © 2015 Kim Wittenburg. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
|
||||
class Error_Handler: T {
|
||||
|
||||
}
|
||||
@@ -17,15 +17,28 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<string>1.1</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>4242</string>
|
||||
<string>A5B4</string>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.music</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSExceptionDomains</key>
|
||||
<dict>
|
||||
<key>mzstatic.com</key>
|
||||
<dict>
|
||||
<key>NSIncludesSubdomains</key>
|
||||
<true/>
|
||||
<key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2015 Kim Wittenburg. All rights reserved.</string>
|
||||
<key>NSMainStoryboardFile</key>
|
||||
|
||||
@@ -33,6 +33,10 @@ internal class MainViewController: NSViewController {
|
||||
static let pasteboardType = "public.item.tagtunes"
|
||||
}
|
||||
|
||||
private struct KVOContexts {
|
||||
static var preferencesContext = "KVOPreferencesContext"
|
||||
}
|
||||
|
||||
internal enum Section: String {
|
||||
case SearchResults = "SearchResults"
|
||||
case Albums = "Albums"
|
||||
@@ -57,7 +61,9 @@ internal class MainViewController: NSViewController {
|
||||
private let urlSession = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration(), delegate: nil, delegateQueue: NSOperationQueue.mainQueue())
|
||||
|
||||
/// The URL task currently loading the search results
|
||||
private var searchTask: NSURLSessionDataTask?
|
||||
private var searchTask: NSURLSessionTask?
|
||||
|
||||
private var searchTerm: String?
|
||||
|
||||
/// If `true` the search section is displayed at the top of the
|
||||
/// `outlineView`.
|
||||
@@ -71,32 +77,45 @@ internal class MainViewController: NSViewController {
|
||||
/// The error that occured during searching, if any.
|
||||
internal private(set) var searchError: NSError?
|
||||
|
||||
/// The URL tasks currently loading the tracks for the respective albums.
|
||||
private var trackTasks = [Album: NSURLSessionDataTask]()
|
||||
// MARK: View Life Cycle
|
||||
|
||||
/// Errors that occured during loading the tracks for the respective album.
|
||||
private var trackErrors = [Album: NSError]()
|
||||
|
||||
// MARK: Overrides
|
||||
// Proxy objects that act as `NSNotificationCenter` observers.
|
||||
private var observerProxies = [NSObjectProtocol]()
|
||||
|
||||
override internal func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
let startedLoadingTracksObserver = NSNotificationCenter.defaultCenter().addObserverForName(AlbumCollection.Notifications.albumStartedLoading, object: albumCollection, queue: NSOperationQueue.mainQueue(), usingBlock: albumCollectionDidBeginLoadingTracks)
|
||||
let finishedLoadingTracksObserver = NSNotificationCenter.defaultCenter().addObserverForName(AlbumCollection.Notifications.albumFinishedLoading, object: albumCollection, queue: NSOperationQueue.mainQueue(), usingBlock: albumCollectionDidFinishLoadingTracks)
|
||||
|
||||
observerProxies.append(startedLoadingTracksObserver)
|
||||
observerProxies.append(finishedLoadingTracksObserver)
|
||||
|
||||
Preferences.sharedPreferences.addObserver(self, forKeyPath: "useCensoredNames", options: [], context: &KVOContexts.preferencesContext)
|
||||
Preferences.sharedPreferences.addObserver(self, forKeyPath: "caseSensitive", options: [], context: &KVOContexts.preferencesContext)
|
||||
|
||||
outlineView.setDraggingSourceOperationMask(.Move, forLocal: true)
|
||||
outlineView.registerForDraggedTypes([OutlineViewConstants.pasteboardType])
|
||||
}
|
||||
|
||||
deinit {
|
||||
for observer in observerProxies {
|
||||
NSNotificationCenter.defaultCenter().removeObserver(observer)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Outline View Content
|
||||
|
||||
internal private(set) var searchResults = [SearchResult]()
|
||||
|
||||
internal private(set) var albums = [Album]()
|
||||
internal let albumCollection = AlbumCollection()
|
||||
|
||||
internal private(set) var unsortedTracks = [iTunesTrack]()
|
||||
|
||||
/// Returns all `iTunesTrack` objects that are somewhere down the outline
|
||||
/// view.
|
||||
private var allITunesTracks: Set<iTunesTrack> {
|
||||
return Set(unsortedTracks).union(albums.flatMap({ $0.tracks.flatMap { $0.associatedTracks } }))
|
||||
return Set(unsortedTracks).union(albumCollection.flatMap({ $0.tracks.flatMap { $0.associatedTracks } }))
|
||||
}
|
||||
|
||||
/// Returns all contents of the outline view.
|
||||
@@ -109,7 +128,7 @@ internal class MainViewController: NSViewController {
|
||||
if showsSearch {
|
||||
contents.append(OutlineViewConstants.Items.searchResultsHeaderItem)
|
||||
if !searchResults.isEmpty {
|
||||
contents.extend(searchResults as [AnyObject])
|
||||
contents.appendContentsOf(searchResults as [AnyObject])
|
||||
} else if searching {
|
||||
contents.append(OutlineViewConstants.Items.loadingItem)
|
||||
} else if let error = searchError {
|
||||
@@ -117,18 +136,83 @@ internal class MainViewController: NSViewController {
|
||||
} else {
|
||||
contents.append(OutlineViewConstants.Items.noResultsItem)
|
||||
}
|
||||
if !albums.isEmpty {
|
||||
if !albumCollection.isEmpty {
|
||||
contents.append(OutlineViewConstants.Items.albumsHeaderItem)
|
||||
}
|
||||
}
|
||||
contents.extend(albums as [AnyObject])
|
||||
contents.appendContentsOf(albumCollection.albums as [AnyObject])
|
||||
if !unsortedTracks.isEmpty {
|
||||
contents.append(OutlineViewConstants.Items.unsortedTracksHeaderItem)
|
||||
contents.extend(unsortedTracks as [AnyObject])
|
||||
contents.appendContentsOf(unsortedTracks as [AnyObject])
|
||||
}
|
||||
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? {
|
||||
@@ -159,41 +243,14 @@ internal class MainViewController: NSViewController {
|
||||
return nil
|
||||
}
|
||||
|
||||
/// 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 albums.contains(album) {
|
||||
return .Albums
|
||||
} else if let track = item as? Track where albums.contains(track.album) {
|
||||
return .Albums
|
||||
} else if let track = item as? iTunesTrack {
|
||||
if let parentTrack = outlineView.parentForItem(track) as? Track where albums.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
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the specified `album` is currently loading its tracks.
|
||||
internal func isAlbumLoading(album: Album) -> Bool {
|
||||
return trackTasks[album] != nil
|
||||
}
|
||||
|
||||
// MARK: Searching
|
||||
|
||||
/// Starts a search for the specified search term. Calling this method
|
||||
internal func beginSearchForTerm(term: String) {
|
||||
searchTask?.cancel()
|
||||
searchResults.removeAll()
|
||||
cancelSearch()
|
||||
if let url = iTunesAPI.createAlbumSearchURLForTerm(term) {
|
||||
showsSearch = true
|
||||
searchTerm = term
|
||||
searchTask = urlSession.dataTaskWithURL(url, completionHandler: processSearchResults)
|
||||
searchTask?.resume()
|
||||
} else {
|
||||
@@ -202,24 +259,40 @@ internal class MainViewController: NSViewController {
|
||||
outlineView.reloadData()
|
||||
}
|
||||
|
||||
/// Cancels the current search (if there is one). This also hides the search
|
||||
/// results.
|
||||
internal func cancelSearch() {
|
||||
searchTask?.cancel()
|
||||
searchResults.removeAll()
|
||||
showsSearch = false
|
||||
outlineView.reloadData()
|
||||
}
|
||||
|
||||
/// Processes the data returned from a network request into the
|
||||
/// `searchResults`.
|
||||
private func processSearchResults(data: NSData?, response: NSURLResponse?, error: NSError?) {
|
||||
private func processSearchResults(data: NSData?, response: NSURLResponse?, var error: NSError?) {
|
||||
searchTask = nil
|
||||
if let theError = error {
|
||||
searchError = theError
|
||||
} else if let theData = data {
|
||||
if let theData = data where error == nil {
|
||||
do {
|
||||
let searchResults = try iTunesAPI.parseAPIData(theData).map { SearchResult(representedAlbum: $0) }
|
||||
searchTerm = nil
|
||||
self.searchResults = searchResults
|
||||
} catch let error as NSError {
|
||||
searchError = error
|
||||
} catch let theError as NSError {
|
||||
error = theError
|
||||
}
|
||||
}
|
||||
if let theError = error {
|
||||
searchErrorOccured(theError)
|
||||
}
|
||||
showsSearch = true
|
||||
outlineView.reloadData()
|
||||
}
|
||||
|
||||
/// Called when an error occurs during searching.
|
||||
private func searchErrorOccured(error: NSError) {
|
||||
searchError = error
|
||||
}
|
||||
|
||||
/// Adds the search result at the specified `row` to the albums section and
|
||||
/// begins loading its tracks.
|
||||
internal func selectSearchResultAtRow(row: Int) {
|
||||
@@ -231,77 +304,93 @@ internal class MainViewController: NSViewController {
|
||||
searchResults.removeAll()
|
||||
showsSearch = false
|
||||
}
|
||||
var albumAlreadyPresent = false
|
||||
for album in albums {
|
||||
if album == searchResult {
|
||||
albumAlreadyPresent = true
|
||||
}
|
||||
}
|
||||
if !albumAlreadyPresent {
|
||||
albums.append(beginLoadingTracksForSearchResult(searchResult))
|
||||
if !containsAlbumForSearchResult(searchResult) {
|
||||
let album = Album(searchResult: searchResult)
|
||||
albumCollection.addAlbum(album, beginLoading: true)
|
||||
}
|
||||
outlineView.reloadData()
|
||||
}
|
||||
|
||||
// MARK: Albums
|
||||
// MARK: Saving
|
||||
|
||||
private func beginLoadingTracksForSearchResult(searchResult: SearchResult) -> Album {
|
||||
let album = Album(searchResult: searchResult)
|
||||
let url = iTunesAPI.createAlbumLookupURLForId(album.id)
|
||||
let task = urlSession.dataTaskWithURL(url) { (data, response, var error) -> Void in
|
||||
self.trackTasks[album] = nil
|
||||
do {
|
||||
if let theData = data {
|
||||
let newAlbum = try iTunesAPI.parseAPIData(theData)[0]
|
||||
let index = self.albums.indexOf(album)!
|
||||
self.albums.removeAtIndex(index)
|
||||
self.albums.insert(newAlbum, atIndex: index)
|
||||
}
|
||||
} catch let theError as NSError {
|
||||
error = theError
|
||||
} catch _ {
|
||||
// Will never happen
|
||||
}
|
||||
self.trackErrors[album] = error
|
||||
self.outlineView.reloadData()
|
||||
}
|
||||
trackTasks[album] = task
|
||||
task.resume()
|
||||
return album
|
||||
}
|
||||
|
||||
func cancelLoadingTracksForAlbum(album: Album) {
|
||||
trackTasks[album]?.cancel()
|
||||
trackTasks[album] = nil
|
||||
}
|
||||
|
||||
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 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
|
||||
}
|
||||
do {
|
||||
try album.saveArtwork()
|
||||
} catch _ {
|
||||
@@ -313,11 +402,7 @@ internal class MainViewController: NSViewController {
|
||||
if errorCount > 0 {
|
||||
dispatch_sync(dispatch_get_main_queue()) {
|
||||
let alert = NSAlert()
|
||||
if errorCount == 1 {
|
||||
alert.messageText = NSLocalizedString("1 artwork could not be saved.", comment: "Error message indicating that one of the artworks could not be saved.")
|
||||
} else {
|
||||
alert.messageText = String(format: NSLocalizedString("%d artworks could not be saved.", comment: "Error message indicating that n artworks could not be saved."), errorCount)
|
||||
}
|
||||
alert.messageText = String(format: NSLocalizedString("%d artworks could not be saved.", comment: "Error message indicating that n artworks could not be saved."), errorCount)
|
||||
alert.informativeText = NSLocalizedString("Please check your privileges for the folder you set in the preferences and try again.", comment: "Informative text for 'artwork(s) could not be saved' errors")
|
||||
alert.alertStyle = .WarningAlertStyle
|
||||
alert.addButtonWithTitle("OK")
|
||||
@@ -326,6 +411,22 @@ internal class MainViewController: NSViewController {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Notifications
|
||||
|
||||
private func albumCollectionDidBeginLoadingTracks(notification: NSNotification) {
|
||||
outlineView.reloadData()
|
||||
}
|
||||
|
||||
private func albumCollectionDidFinishLoadingTracks(notification: NSNotification) {
|
||||
outlineView.reloadData()
|
||||
}
|
||||
|
||||
override internal func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
|
||||
if context == &KVOContexts.preferencesContext {
|
||||
outlineView.reloadData()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Actions
|
||||
|
||||
/// Adds the current iTunes selection
|
||||
@@ -338,7 +439,7 @@ internal class MainViewController: NSViewController {
|
||||
alert.beginSheetModalForWindow(view.window!, completionHandler: nil)
|
||||
} else if let selection = iTunes.selection.get() as? [iTunesTrack] {
|
||||
let newTracks = Set(selection).subtract(allITunesTracks)
|
||||
unsortedTracks.extend(newTracks)
|
||||
unsortedTracks.appendContentsOf(newTracks)
|
||||
outlineView.reloadData()
|
||||
}
|
||||
}
|
||||
@@ -363,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)) {
|
||||
@@ -396,28 +478,94 @@ 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 {
|
||||
if let album = item as? Album {
|
||||
cancelLoadingTracksForAlbum(album)
|
||||
albums.removeElement(album)
|
||||
albumCollection.removeAlbum(album)
|
||||
} 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)
|
||||
@@ -442,7 +590,7 @@ internal class MainViewController: NSViewController {
|
||||
if let theError = item as? NSError {
|
||||
error = theError
|
||||
} else if let album = item as? Album {
|
||||
if let theError = trackErrors[album] {
|
||||
if let theError = albumCollection.errorForAlbum(album) {
|
||||
error = theError
|
||||
} else {
|
||||
return
|
||||
@@ -458,25 +606,98 @@ internal class MainViewController: NSViewController {
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Error Handling
|
||||
|
||||
extension MainViewController {
|
||||
|
||||
override internal func willPresentError(error: NSError) -> NSError {
|
||||
let recoveryOptions = [
|
||||
NSLocalizedString("OK", comment: "Button title"),
|
||||
NSLocalizedString("Try Again", comment: "Button title for error alerts offering the user to try again.")
|
||||
]
|
||||
return DescriptiveError(underlyingError: error, userInfo: [NSRecoveryAttempterErrorKey: self, NSLocalizedRecoveryOptionsErrorKey: recoveryOptions])
|
||||
}
|
||||
|
||||
override internal func attemptRecoveryFromError(error: NSError, optionIndex recoveryOptionIndex: Int, delegate: AnyObject?, didRecoverSelector: Selector, contextInfo: UnsafeMutablePointer<Void>) {
|
||||
let didRecover = attemptRecoveryFromError(error, optionIndex: recoveryOptionIndex)
|
||||
delegate?.performSelector(didRecoverSelector, withObject: didRecover, withObject: contextInfo as! AnyObject)
|
||||
}
|
||||
|
||||
override internal func attemptRecoveryFromError(error: NSError, optionIndex recoveryOptionIndex: Int) -> Bool {
|
||||
if recoveryOptionIndex == 0 {
|
||||
return true
|
||||
}
|
||||
if let term = searchTerm where error == searchError || error.userInfo[NSUnderlyingErrorKey] === searchError {
|
||||
beginSearchForTerm(term)
|
||||
return true
|
||||
} else {
|
||||
for album in albumCollection {
|
||||
let albumError = albumCollection.errorForAlbum(album)
|
||||
if error == albumError || error.userInfo[NSUnderlyingErrorKey] === albumError {
|
||||
albumCollection.beginLoadingTracksForAlbum(album)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - User Interface Validations
|
||||
|
||||
extension MainViewController: NSUserInterfaceValidations {
|
||||
|
||||
func validateUserInterfaceItem(anItem: NSValidatedUserInterfaceItem) -> Bool {
|
||||
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
|
||||
@@ -488,7 +709,7 @@ extension MainViewController: NSUserInterfaceValidations {
|
||||
|
||||
extension MainViewController: NSOutlineViewDataSource, NSOutlineViewDelegate {
|
||||
|
||||
func outlineView(outlineView: NSOutlineView, numberOfChildrenOfItem item: AnyObject?) -> Int {
|
||||
internal func outlineView(outlineView: NSOutlineView, numberOfChildrenOfItem item: AnyObject?) -> Int {
|
||||
if item == nil {
|
||||
return outlineViewContents.count
|
||||
} else if let album = item as? Album {
|
||||
@@ -500,7 +721,7 @@ extension MainViewController: NSOutlineViewDataSource, NSOutlineViewDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func outlineView(outlineView: NSOutlineView, child index: Int, ofItem item: AnyObject?) -> AnyObject {
|
||||
internal func outlineView(outlineView: NSOutlineView, child index: Int, ofItem item: AnyObject?) -> AnyObject {
|
||||
if item == nil {
|
||||
return outlineViewContents[index]
|
||||
} else if let album = item as? Album {
|
||||
@@ -512,19 +733,19 @@ extension MainViewController: NSOutlineViewDataSource, NSOutlineViewDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func outlineView(outlineView: NSOutlineView, isItemExpandable item: AnyObject) -> Bool {
|
||||
internal func outlineView(outlineView: NSOutlineView, isItemExpandable item: AnyObject) -> Bool {
|
||||
return self.outlineView(outlineView, numberOfChildrenOfItem: item) > 0
|
||||
}
|
||||
|
||||
func outlineView(outlineView: NSOutlineView, isGroupItem item: AnyObject) -> Bool {
|
||||
internal func outlineView(outlineView: NSOutlineView, isGroupItem item: AnyObject) -> Bool {
|
||||
return Section.isHeaderItem(item)
|
||||
}
|
||||
|
||||
func outlineView(outlineView: NSOutlineView, shouldSelectItem item: AnyObject) -> Bool {
|
||||
internal func outlineView(outlineView: NSOutlineView, shouldSelectItem item: AnyObject) -> Bool {
|
||||
return !(self.outlineView(outlineView, isGroupItem: item) || item === OutlineViewConstants.Items.loadingItem || item === OutlineViewConstants.Items.noResultsItem || item is NSError)
|
||||
}
|
||||
|
||||
func outlineView(outlineView: NSOutlineView, heightOfRowByItem item: AnyObject) -> CGFloat {
|
||||
internal func outlineView(outlineView: NSOutlineView, heightOfRowByItem item: AnyObject) -> CGFloat {
|
||||
if item is Album || item is SearchResult {
|
||||
return 39
|
||||
} else if let track = item as? Track {
|
||||
@@ -540,7 +761,7 @@ extension MainViewController: NSOutlineViewDataSource, NSOutlineViewDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func outlineView(outlineView: NSOutlineView, viewForTableColumn tableColumn: NSTableColumn?, item: AnyObject) -> NSView? {
|
||||
internal func outlineView(outlineView: NSOutlineView, viewForTableColumn tableColumn: NSTableColumn?, item: AnyObject) -> NSView? {
|
||||
if item === OutlineViewConstants.Items.searchResultsHeaderItem {
|
||||
var view = outlineView.makeViewWithIdentifier(OutlineViewConstants.ViewIdentifiers.simpleTableCellViewIdentifier, owner: nil) as? AdvancedTableCellView
|
||||
if view == nil {
|
||||
@@ -606,17 +827,6 @@ 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 album = item as? Album {
|
||||
var view = outlineView.makeViewWithIdentifier(OutlineViewConstants.ViewIdentifiers.albumTableCellViewIdentifier, owner: nil) as? AlbumTableCellView
|
||||
if view == nil {
|
||||
view = AlbumTableCellView()
|
||||
view?.identifier = OutlineViewConstants.ViewIdentifiers.albumTableCellViewIdentifier
|
||||
}
|
||||
view?.setupForAlbum(album, loading: isAlbumLoading(album), error: trackErrors[album])
|
||||
view?.errorButton?.target = self
|
||||
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 {
|
||||
@@ -625,10 +835,21 @@ extension MainViewController: NSOutlineViewDataSource, NSOutlineViewDelegate {
|
||||
}
|
||||
view?.button.target = self
|
||||
view?.button.action = "selectSearchResult:"
|
||||
let selectable = albums.filter { $0.id == searchResult.id }.isEmpty
|
||||
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 {
|
||||
view = AlbumTableCellView()
|
||||
view?.identifier = OutlineViewConstants.ViewIdentifiers.albumTableCellViewIdentifier
|
||||
}
|
||||
view?.setupForAlbum(album, loading: albumCollection.isAlbumLoading(album), error: albumCollection.errorForAlbum(album))
|
||||
view?.errorButton?.target = self
|
||||
view?.errorButton?.action = "showErrorDetails:"
|
||||
return view
|
||||
}
|
||||
if let track = item as? Track {
|
||||
var view = outlineView.makeViewWithIdentifier(OutlineViewConstants.ViewIdentifiers.trackTableCellViewIdentifier, owner: nil) as? TrackTableCellView
|
||||
if view == nil {
|
||||
@@ -653,7 +874,7 @@ extension MainViewController: NSOutlineViewDataSource, NSOutlineViewDelegate {
|
||||
return nil
|
||||
}
|
||||
|
||||
func outlineView(outlineView: NSOutlineView, writeItems items: [AnyObject], toPasteboard pasteboard: NSPasteboard) -> Bool {
|
||||
internal func outlineView(outlineView: NSOutlineView, writeItems items: [AnyObject], toPasteboard pasteboard: NSPasteboard) -> Bool {
|
||||
var rows = [Int]()
|
||||
var containsValidItems = false
|
||||
for item in items {
|
||||
@@ -672,7 +893,7 @@ extension MainViewController: NSOutlineViewDataSource, NSOutlineViewDelegate {
|
||||
return true
|
||||
}
|
||||
|
||||
func outlineView(outlineView: NSOutlineView, validateDrop info: NSDraggingInfo, proposedItem item: AnyObject?, proposedChildIndex index: Int) -> NSDragOperation {
|
||||
internal func outlineView(outlineView: NSOutlineView, validateDrop info: NSDraggingInfo, proposedItem item: AnyObject?, proposedChildIndex index: Int) -> NSDragOperation {
|
||||
let firstUnsortedRow = outlineViewContents.count - (unsortedTracks.isEmpty ? 0 : unsortedTracks.count+1)
|
||||
// Drop in the 'unsorted' section
|
||||
if item == nil && index >= firstUnsortedRow || item === OutlineViewConstants.Items.unsortedTracksHeaderItem {
|
||||
@@ -692,13 +913,13 @@ extension MainViewController: NSOutlineViewDataSource, NSOutlineViewDelegate {
|
||||
if sectionOfRow(row) == .SearchResults {
|
||||
return .None
|
||||
}
|
||||
if let album = item as? Album where isAlbumLoading(album) || trackErrors[album] != nil {
|
||||
if let album = item as? Album where albumCollection.isAlbumLoading(album) || albumCollection.errorForAlbum(album) != nil {
|
||||
return .None
|
||||
}
|
||||
return .Every
|
||||
}
|
||||
|
||||
func outlineView(outlineView: NSOutlineView, acceptDrop info: NSDraggingInfo, item: AnyObject?, childIndex index: Int) -> Bool {
|
||||
internal func outlineView(outlineView: NSOutlineView, acceptDrop info: NSDraggingInfo, item: AnyObject?, childIndex index: Int) -> Bool {
|
||||
guard let data = info.draggingPasteboard().dataForType(OutlineViewConstants.pasteboardType), draggedRows = NSKeyedUnarchiver.unarchiveObjectWithData(data) as? [Int] else {
|
||||
return false
|
||||
}
|
||||
@@ -717,7 +938,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)
|
||||
@@ -728,7 +949,7 @@ extension MainViewController: NSOutlineViewDataSource, NSOutlineViewDelegate {
|
||||
|
||||
// Add the dragged tracks to the new target
|
||||
if let targetTrack = item as? Track {
|
||||
targetTrack.associatedTracks.extend(draggedTracks)
|
||||
targetTrack.associatedTracks.appendContentsOf(draggedTracks)
|
||||
} else if let targetAlbum = item as? Album {
|
||||
for draggedTrack in draggedTracks {
|
||||
var inserted = false
|
||||
@@ -744,7 +965,7 @@ extension MainViewController: NSOutlineViewDataSource, NSOutlineViewDelegate {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
unsortedTracks.extend(draggedTracks)
|
||||
unsortedTracks.appendContentsOf(draggedTracks)
|
||||
}
|
||||
outlineView.reloadData()
|
||||
return true
|
||||
|
||||
144
TagTunes/Preference Controllers.swift
Normal file
@@ -0,0 +1,144 @@
|
||||
//
|
||||
// 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
|
||||
}
|
||||
|
||||
@IBAction internal func saveArtworkStateChanged(sender: AnyObject) {
|
||||
if Preferences.sharedPreferences.saveArtwork && Preferences.sharedPreferences.artworkTarget == nil {
|
||||
chooseArtworkPath(sender)
|
||||
}
|
||||
}
|
||||
|
||||
@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 in an open dialog prompting the user to choose a directory")
|
||||
openPanel.beginSheetModalForWindow(view.window!) {
|
||||
result in
|
||||
if result == NSModalResponseOK {
|
||||
Preferences.sharedPreferences.artworkTarget = openPanel.URL!.filePathURL!
|
||||
} else if Preferences.sharedPreferences.artworkTarget == nil {
|
||||
Preferences.sharedPreferences.saveArtwork = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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"))
|
||||
}
|
||||
if tag.clearable {
|
||||
popupButton?.addItemWithTitle(NSLocalizedString("Clear", comment: "Menu item title for a tag that is going to be cleared"))
|
||||
}
|
||||
popupButton?.addItemWithTitle(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 = 1
|
||||
if !tag.isReturnedBySearchAPI {
|
||||
--selectedIndex
|
||||
}
|
||||
case .Ignore:
|
||||
selectedIndex = 2
|
||||
if !tag.isReturnedBySearchAPI {
|
||||
--selectedIndex
|
||||
}
|
||||
if !tag.clearable {
|
||||
--selectedIndex
|
||||
}
|
||||
}
|
||||
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:
|
||||
if tag.isReturnedBySearchAPI {
|
||||
savingBehavior = .Save
|
||||
} else if tag.clearable {
|
||||
savingBehavior = .Clear
|
||||
} else {
|
||||
savingBehavior = .Ignore
|
||||
}
|
||||
case 1:
|
||||
if tag.isReturnedBySearchAPI {
|
||||
if tag.clearable {
|
||||
savingBehavior = .Clear
|
||||
} else {
|
||||
savingBehavior = .Ignore
|
||||
}
|
||||
}
|
||||
savingBehavior = .Ignore
|
||||
case 2:
|
||||
savingBehavior = .Ignore
|
||||
default:
|
||||
break
|
||||
}
|
||||
Preferences.sharedPreferences.tagSavingBehaviors[tag] = savingBehavior
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,6 +8,15 @@
|
||||
|
||||
import Cocoa
|
||||
|
||||
/// Internal class to be used in IB to bind to the shared preferences.
|
||||
@objc internal class PreferencesSingleton: NSObject {
|
||||
|
||||
internal dynamic var sharedPreferences: Preferences {
|
||||
return Preferences.sharedPreferences
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// A custom interface for the `NSUserDefaults`. It is recommended to use this
|
||||
/// class insted of accessing the user defaults directly to prevent errors due
|
||||
/// to misspelled strings.
|
||||
@@ -15,6 +24,47 @@ import Cocoa
|
||||
/// All properties in this class are KCO compliant.
|
||||
@objc public class Preferences: NSObject {
|
||||
|
||||
// MARK: Types
|
||||
|
||||
internal struct UserDefaultsConstants {
|
||||
|
||||
static let saveArtworkKey = "Save Artwork"
|
||||
|
||||
static let artworkTargetKey = "Artwork Target"
|
||||
|
||||
static let overwriteExistingFilesKey = "Overwrite Existing Files"
|
||||
|
||||
static let keepSearchResultsKey = "Keep Search Results"
|
||||
|
||||
static let removeSavedAlbumsKey = "Remove Saved Albums"
|
||||
|
||||
static let useLowResolutionArtworkKey = "Use Low Resolution Artwork"
|
||||
|
||||
static let useCensoredNamesKey = "Use Censored Names"
|
||||
|
||||
static let caseSensitiveKey = "Case Sensitive"
|
||||
|
||||
static let clearArtworksKey = "Clear Artworks"
|
||||
|
||||
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.
|
||||
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,36 +72,57 @@ 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.overwriteExistingFilesKey: false,
|
||||
UserDefaultsConstants.keepSearchResultsKey: false,
|
||||
UserDefaultsConstants.removeSavedAlbumsKey: false,
|
||||
UserDefaultsConstants.useLowResolutionArtworkKey: false,
|
||||
UserDefaultsConstants.useCensoredNamesKey: false,
|
||||
UserDefaultsConstants.caseSensitiveKey: true,
|
||||
UserDefaultsConstants.clearArtworksKey: 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
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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: "Artwork Target")
|
||||
NSUserDefaults.standardUserDefaults().setURL(newValue, forKey: UserDefaultsConstants.artworkTargetKey)
|
||||
}
|
||||
get {
|
||||
return NSUserDefaults.standardUserDefaults().URLForKey("Artwork Target")!
|
||||
return NSUserDefaults.standardUserDefaults().URLForKey(UserDefaultsConstants.artworkTargetKey)
|
||||
}
|
||||
}
|
||||
|
||||
/// If `true` any existing files will be overwritten when artworks are saved.
|
||||
public dynamic var overwriteExistingFiles: Bool {
|
||||
set {
|
||||
NSUserDefaults.standardUserDefaults().setBool(newValue, forKey: UserDefaultsConstants.overwriteExistingFilesKey)
|
||||
}
|
||||
get {
|
||||
return NSUserDefaults.standardUserDefaults().boolForKey(UserDefaultsConstants.overwriteExistingFilesKey)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,11 +130,67 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// If `true` the main table view will use 100x100 artworks instead of full
|
||||
/// sized images. This option does not affect saving.
|
||||
public dynamic var useLowResolutionArtwork: Bool {
|
||||
set {
|
||||
NSUserDefaults.standardUserDefaults().setBool(newValue, forKey: UserDefaultsConstants.useLowResolutionArtworkKey)
|
||||
}
|
||||
get {
|
||||
return NSUserDefaults.standardUserDefaults().boolForKey(UserDefaultsConstants.useLowResolutionArtworkKey)
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
/// If `true` TagTunes ignores cases when comparing track titles and albums.
|
||||
public dynamic var caseSensitive: Bool {
|
||||
set {
|
||||
NSUserDefaults.standardUserDefaults().setBool(newValue, forKey: UserDefaultsConstants.caseSensitiveKey)
|
||||
}
|
||||
get {
|
||||
return NSUserDefaults.standardUserDefaults().boolForKey(UserDefaultsConstants.caseSensitiveKey)
|
||||
}
|
||||
}
|
||||
|
||||
/// If `true` TagTunes clears the artworsk of saved tracks.
|
||||
public dynamic var clearArtworks: Bool {
|
||||
set {
|
||||
NSUserDefaults.standardUserDefaults().setBool(newValue, forKey: UserDefaultsConstants.clearArtworksKey)
|
||||
}
|
||||
get {
|
||||
return NSUserDefaults.standardUserDefaults().boolForKey(UserDefaultsConstants.clearArtworksKey)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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)!) }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -12,6 +12,45 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
public var clearable: Bool {
|
||||
switch self {
|
||||
case .Year, .TrackNumber, .TrackCount, .DiscNumber, .DiscCount:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a string identifying the respective tag that can be displayed
|
||||
/// to the user.
|
||||
public var localizedName: String {
|
||||
return NSLocalizedString("Tag: \(self.rawValue)", comment: "")
|
||||
}
|
||||
|
||||
/// 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 +121,40 @@ 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)
|
||||
|
||||
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("", forKey: tag.rawValue)
|
||||
case .Ignore:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if all `associatedTrack`s contain the same values as the
|
||||
@@ -107,13 +162,22 @@ public class Track: iTunesType {
|
||||
public var saved: Bool {
|
||||
let components = NSCalendar.currentCalendar().components(.Year, fromDate: releaseDate)
|
||||
for track in associatedTracks {
|
||||
guard 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.composer == "" else {
|
||||
let trackName = Preferences.sharedPreferences.useCensoredNames ? censoredName : name
|
||||
let albumName = Preferences.sharedPreferences.useCensoredNames ? album.censoredName : album.name
|
||||
let options = Preferences.sharedPreferences.caseSensitive ? [] : NSStringCompareOptions.CaseInsensitiveSearch
|
||||
guard track.name.compare(trackName, options: options, range: nil, locale: nil) == .OrderedSame else {
|
||||
return false
|
||||
}
|
||||
guard track.album.compare(albumName, options: options, range: nil, locale: nil) == .OrderedSame else {
|
||||
return false
|
||||
}
|
||||
guard track.artist == artistName && track.year == components.year && track.trackNumber == trackNumber && track.trackCount == trackCount && track.discNumber == discNumber && track.discCount == discCount && track.genre == genre && track.albumArtist == album.artistName && track.composer == "" else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
extension Track: Hashable {
|
||||
|
||||
@@ -13,6 +13,20 @@ import AppKitPlus
|
||||
/// only be initialized from a nib or storyboard.
|
||||
public class TrackTableCellView: AdvancedTableCellView {
|
||||
|
||||
// MARK: Types
|
||||
|
||||
private struct Images {
|
||||
|
||||
/// Caches the tick image for track cells so that it does not need to be
|
||||
/// reloaded every time a cell is configured.
|
||||
static let tickImage = NSImage(named: "Tick")?.imageByMaskingWithColor(NSColor.clearColor())
|
||||
|
||||
/// Caches the gray tick image for track cells so that it does not need to be
|
||||
/// reloaded every time a cell is configured.
|
||||
static let grayTickImage = NSImage(named: "Tick")?.imageByMaskingWithColor(NSColor.lightGrayColor())
|
||||
|
||||
}
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// An outlet do display the track number. This acts as a secondary label.
|
||||
@@ -86,10 +100,10 @@ public class TrackTableCellView: AdvancedTableCellView {
|
||||
public func setupForTrack(track: Track) {
|
||||
style = track.album.hasSameArtistNameAsTracks ? .Simple : .CompactSubtitle
|
||||
|
||||
textField?.stringValue = track.name
|
||||
textField?.stringValue = Preferences.sharedPreferences.useCensoredNames ? track.censoredName : track.name
|
||||
if track.associatedTracks.isEmpty {
|
||||
textField?.textColor = NSColor.disabledControlTextColor()
|
||||
} else if track.associatedTracks.count > 1 || track.associatedTracks[0].name != track.name {
|
||||
} else if track.associatedTracks.count > 1 || track.associatedTracks[0].name.compare(track.name, options: Preferences.sharedPreferences.caseSensitive ? [] : .CaseInsensitiveSearch, range: nil, locale: nil) != .OrderedSame {
|
||||
textField?.textColor = NSColor.redColor()
|
||||
} else {
|
||||
textField?.textColor = NSColor.controlTextColor()
|
||||
@@ -97,9 +111,9 @@ public class TrackTableCellView: AdvancedTableCellView {
|
||||
secondaryTextField?.stringValue = track.artistName
|
||||
trackNumberTextField?.stringValue = "\(track.trackNumber)"
|
||||
if track.associatedTracks.isEmpty {
|
||||
imageView?.image = NSImage(named: "TickBW")
|
||||
imageView?.image = TrackTableCellView.Images.grayTickImage
|
||||
} else {
|
||||
imageView?.image = NSImage(named: "Tick")
|
||||
imageView?.image = TrackTableCellView.Images.tickImage
|
||||
}
|
||||
if track.saved {
|
||||
let aspectRatioConstraint = NSLayoutConstraint(
|
||||
|
||||
@@ -125,7 +125,7 @@ public struct iTunesAPI {
|
||||
/// - returns: The query URL or `nil` if `term` is invalid.
|
||||
public static func createAlbumSearchURLForTerm(term: String) -> NSURL? {
|
||||
var searchTerm = term.stringByReplacingOccurrencesOfString(" ", withString: "+")
|
||||
searchTerm = CFURLCreateStringByAddingPercentEscapes(nil, searchTerm, nil, nil, CFStringBuiltInEncodings.UTF8.rawValue) as String
|
||||
searchTerm = searchTerm.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet())!
|
||||
if searchTerm.isEmpty {
|
||||
return nil
|
||||
}
|
||||
|
||||