Archived
1

40 Commits

Author SHA1 Message Date
Kim Wittenburg
f3669932df Updated for Version 1.2.1 2015-10-21 11:48:08 +02:00
Kim Wittenburg
cb231a9f0c Updated Translations 2015-10-21 11:45:51 +02:00
Kim Wittenburg
ed5e1083d6 The Save Button is Now Disabled if Only an Album With No Associated Tracks Is Selected 2015-10-21 10:56:18 +02:00
Kim Wittenburg
dfc3be5fd1 Fixed an issue where TagTunes would crash when the user selected an outdated track 2015-10-21 10:40:34 +02:00
Kim Wittenburg
f75c16cb27 Added "Use English Tags" Preference 2015-10-21 10:27:58 +02:00
Kim Wittenburg
98d6d492d0 Added "iTunes Store" Image 2015-10-21 10:08:12 +02:00
Kim Wittenburg
a228f91409 Moved Some Preferences to a new "iTunes Store" Panel 2015-10-21 10:07:41 +02:00
Kim Wittenburg
e152c3da44 Updated project format to Xcode 6.3 compatibility 2015-09-20 14:29:13 +02:00
Kim Wittenburg
1d55cb8c5e Changed version number to 1.2 2015-09-16 21:10:29 +02:00
Kim Wittenburg
7f6ef6588c Added German localization 2015-09-16 21:02:19 +02:00
Kim Wittenburg
e0261f7353 Added Option to automatically remove saved items. 2015-09-16 20:06:45 +02:00
Kim Wittenburg
8d4fba8d8a Added preference for the used iTunes Store. 2015-09-16 19:41:44 +02:00
Kim Wittenburg
548214479a Added option for displayed number of search results 2015-09-15 16:54:22 +02:00
Kim Wittenburg
24bb68f7d8 Updated for version 1.1 2015-09-15 16:32:43 +02:00
Kim Wittenburg
2da74aa359 Added AppKitPlus to embedded binaries 2015-09-15 16:06:31 +02:00
Kim Wittenburg
7dbdc047ae Updated Version number 2015-09-15 15:45:08 +02:00
Kim Wittenburg
c0e7727785 Added pluralization 2015-09-15 15:37:05 +02:00
Kim Wittenburg
7cd60ffed4 Added English localization 2015-09-15 15:26:56 +02:00
Kim Wittenburg
2b668967e2 Added English localization 2015-09-15 15:26:35 +02:00
Kim Wittenburg
e287a935b1 Added option to overwrite existing files when saving artwork 2015-09-15 14:29:57 +02:00
Kim Wittenburg
4548f43797 Improved description for 'low resolution artwork' option 2015-09-15 14:19:24 +02:00
Kim Wittenburg
dcd7d2620d Added option to use lower resolution artworks in TagTunes 2015-09-15 14:17:40 +02:00
Kim Wittenburg
d49ad31b79 Added 'clear Artworks' option. 2015-09-15 14:03:17 +02:00
Kim Wittenburg
bfa0deae4f Removed unnecessary todo 2015-09-15 13:51:33 +02:00
Kim Wittenburg
b694c18daf Added 'Save Artwork' icon 2015-09-15 13:43:47 +02:00
Kim Wittenburg
3a400037ff Added 'Save Artwork' toolbar button 2015-09-15 13:43:18 +02:00
Kim Wittenburg
257a7811d6 Implemented censored names preference
Added case sensitivity preference
Integer tags are not clearable anymore
2015-09-14 00:43:01 +02:00
Kim Wittenburg
e2bdcd21e5 Merge remote-tracking branch 'origin/master' 2015-09-11 16:38:30 +02:00
Kim Wittenburg
5704d0c0e5 Reorganized model for 'Albums' section 2015-09-11 16:38:17 +02:00
Kim Wittenburg
80f177807d Updated for OS X 10.11 and Swift 2
Added more descriptive errors.
2015-09-11 14:45:48 +02:00
Kim Wittenburg
45f664cb10 Added 'Tags' preferences with saving behaviors for each tag 2015-09-04 17:23:45 +02:00
Kim Wittenburg
6bf18c1aae README.md edited online with Bitbucket 2015-09-04 00:36:02 +00:00
Kim Wittenburg
b851b70ddb Optimized image resources. 2015-09-04 02:06:52 +02:00
Kim Wittenburg
2c2252f6af Added NSImage extension for color masked images. 2015-09-04 01:39:22 +02:00
Kim Wittenburg
d1c5e3b98a README.md edited online with Bitbucket 2015-09-03 22:22:50 +00:00
Kim Wittenburg
7a6192d19a Updated README 2015-09-04 00:05:53 +02:00
Kim Wittenburg
5196b6a577 README.md edited online with Bitbucket 2015-09-03 06:18:21 +00:00
Kim Wittenburg
05c7b121a5 Merge remote-tracking branch 'origin/master' 2015-09-03 08:17:01 +02:00
Kim Wittenburg
b778d14ea6 Updated project files 2015-09-03 08:16:31 +02:00
Kim Wittenburg
0bfef01875 README.md edited online with Bitbucket 2015-09-03 06:14:52 +00:00
57 changed files with 7199 additions and 951 deletions

14
Base.lproj/Credits.rtf Normal file
View File

@@ -0,0 +1,14 @@
{\rtf1\ansi\ansicpg1252\cocoartf1404
{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
{\colortbl;\red255\green255\blue255;}
\paperw12240\paperh15840\margl1440\margr1440\vieww9000\viewh8400\viewkind0
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural\partightenfactor0
\f0\b\fs24 \cf0 Design and 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}}.}

32
Changelog.txt Normal file
View File

@@ -0,0 +1,32 @@
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
Version 1.2
Added:
+ Option for displayed number of search results
+ Option to change the iTunes Store used to get tags
+ Option to remove saved items
1.2.1
Added:
+ Option to use English tags when using a different iTunes Store
Fixed:
* Fixed an issue where TagTunes would sometimes crash when the user selected an album or track in the list

59
README.md Normal file
View File

@@ -0,0 +1,59 @@
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.2.1]
------------------------------------------------------------
## All Versions
### [Version 1.2.1][Download 1.2.1]
This version adds an option to use English tags in every iTunes Store and fixes some issues.
### [Version 1.2][Download 1.2]
This version adds a German translation and a setting to search in a specific iTunes Store.
### [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.
#### TagTunes does not find an album but iTunes does
> There is a different iTunes Store for each country. By default TagTunes uses a store based on your current system settings. You can change the store used by TagTunes in the preferences.
#### 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.
#### I have an iTunes account for a different country
> In the preferences of TagTunes you can set your preferred iTunes store. You may also choose to use English tags although you use metadata from a non-English store.
#### 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.2.1]: https://bitbucket.org/Codello/tagtunes/downloads/TagTunes%201.2.1.dmg
[Download 1.2]: https://bitbucket.org/Codello/tagtunes/downloads/TagTunes%201.2.dmg
[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

View File

@@ -3,11 +3,11 @@
archiveVersion = 1;
classes = {
};
objectVersion = 46;
objectVersion = 47;
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 */; };
@@ -18,12 +18,18 @@
3B489DD61B90E0D8002B7EB3 /* AlbumTableCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B489DD51B90E0D8002B7EB3 /* AlbumTableCellView.swift */; };
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 */; };
3B86A4021BA9E94F00B150AE /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 3B86A4041BA9E94F00B150AE /* Credits.rtf */; };
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; };
3B4A0A931BD790CE00EF1BA0 /* de */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = de; path = de.lproj/Main.storyboard; 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,20 @@
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>"; };
3B86A4001BA9E92F00B150AE /* de */ = {isa = PBXFileReference; fileEncoding = 10; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = "<group>"; };
3B86A4011BA9E92F00B150AE /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = de; path = de.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3B86A4031BA9E94F00B150AE /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; name = Base; path = Base.lproj/Credits.rtf; sourceTree = "<group>"; };
3B86A4051BA9E95100B150AE /* de */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; name = de; path = de.lproj/Credits.rtf; 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 +108,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
3BBF710B1B95E00F00BB1EDB /* AppKitPlus.framework in Frameworks */,
3B9752721BA85C2F00E26515 /* AppKitPlus.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -109,6 +141,8 @@
3B76C7661B909B280025D550 = {
isa = PBXGroup;
children = (
3BB8C5421BA2EEE800031021 /* Changelog.txt */,
3BBF71161B98FB4200BB1EDB /* README.md */,
3B76C7711B909B280025D550 /* TagTunes */,
3B76C7831B909B280025D550 /* TagTunesTests */,
3B76C78E1B909B280025D550 /* TagTunesUITests */,
@@ -134,7 +168,7 @@
3B76C79D1B909B8C0025D550 /* Model */,
3B76C79F1B909B960025D550 /* View */,
3B76C79E1B909B910025D550 /* Controller */,
3B76C77B1B909B280025D550 /* Info.plist */,
3BFDED631BA84ADB007E7F36 /* Resources */,
3B489DC81B90B3E2002B7EB3 /* TagTunes-Bridging-Header.h */,
);
path = TagTunes;
@@ -162,7 +196,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 +212,7 @@
children = (
3B76C7721B909B280025D550 /* AppDelegate.swift */,
3B76C7741B909B280025D550 /* MainViewController.swift */,
3B285DB71B9128C100F0A2F1 /* PreferencesTabViewController.swift */,
3B285DB71B9128C100F0A2F1 /* Preference Controllers.swift */,
);
name = Controller;
sourceTree = "<group>";
@@ -184,8 +220,6 @@
3B76C79F1B909B960025D550 /* View */ = {
isa = PBXGroup;
children = (
3B76C7781B909B280025D550 /* Main.storyboard */,
3B76C7761B909B280025D550 /* Assets.xcassets */,
3B489DD51B90E0D8002B7EB3 /* AlbumTableCellView.swift */,
3B489DBD1B90B055002B7EB3 /* TrackTableCellView.swift */,
);
@@ -195,11 +229,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 */,
3B86A4041BA9E94F00B150AE /* Credits.rtf */,
3BFDED761BA855B8007E7F36 /* Localizable.stringsdict */,
);
name = Resources;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -210,6 +258,7 @@
3B76C76B1B909B280025D550 /* Sources */,
3B76C76C1B909B280025D550 /* Frameworks */,
3B76C76D1B909B280025D550 /* Resources */,
3B9752741BA85C2F00E26515 /* Embed Frameworks */,
);
buildRules = (
);
@@ -280,12 +329,13 @@
};
};
buildConfigurationList = 3B76C76A1B909B280025D550 /* Build configuration list for PBXProject "TagTunes" */;
compatibilityVersion = "Xcode 3.2";
compatibilityVersion = "Xcode 6.3";
developmentRegion = English;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
de,
);
mainGroup = 3B76C7661B909B280025D550;
productRefGroup = 3B76C7701B909B280025D550 /* Products */;
@@ -304,8 +354,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 */,
3B86A4021BA9E94F00B150AE /* Credits.rtf in Resources */,
3BFDED741BA855B8007E7F36 /* Localizable.stringsdict in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -335,11 +388,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 */,
);
@@ -381,10 +436,39 @@
isa = PBXVariantGroup;
children = (
3B76C7791B909B280025D550 /* Base */,
3B4A0A931BD790CE00EF1BA0 /* de */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
3B86A4041BA9E94F00B150AE /* Credits.rtf */ = {
isa = PBXVariantGroup;
children = (
3B86A4031BA9E94F00B150AE /* Base */,
3B86A4051BA9E95100B150AE /* de */,
);
name = Credits.rtf;
path = ..;
sourceTree = "<group>";
};
3BFDED601BA84AD1007E7F36 /* Localizable.strings */ = {
isa = PBXVariantGroup;
children = (
3BFDED611BA84AD1007E7F36 /* Base */,
3B86A4001BA9E92F00B150AE /* de */,
);
name = Localizable.strings;
sourceTree = "<group>";
};
3BFDED761BA855B8007E7F36 /* Localizable.stringsdict */ = {
isa = PBXVariantGroup;
children = (
3BFDED751BA855B8007E7F36 /* Base */,
3B86A4011BA9E92F00B150AE /* de */,
);
name = Localizable.stringsdict;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
@@ -424,7 +508,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 +546,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 +558,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 +579,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;
};

View File

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

View File

@@ -109,8 +109,9 @@ public class Album: iTunesType {
/// Saves the album's artwork to the directory specified in the user's
/// preferences (See `Preferences` for details).
public func saveArtwork() throws {
let url = Preferences.sharedPreferences.artworkTarget
try artwork.saveToURL(url, filename: name)
if let url = Preferences.sharedPreferences.artworkTarget {
try artwork.saveToURL(url, filename: name)
}
}
/// Returns `true` if all tracks of the album are saved.

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

View File

@@ -2,7 +2,7 @@
"images" : [
{
"idiom" : "universal",
"filename" : "PauseProgressFreestandingTemplate.pdf"
"filename" : "Note.pdf"
}
],
"info" : {

View 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ìóî}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>>

View File

@@ -1,29 +0,0 @@
%PDF-1.4
%âãÏÓ
1 0 obj <</Filter/FlateDecode/Length 236>>stream
xœÍ1N1 E{ŸÂ'ˆl''W@¢ ¢@Û­`„fb¹>ÎLⶃ
EJüœïoGÊ®@È$ /.°‚ë¶À ÏðŒ_ ø`Âw»ÅGx9ž<>Ç7¢¾ÿðúSI[Ÿod@q<>Y%äTC­ˆñR<>Ëf<C38B>™CÕ-¾<>J 5oTÔâ´WYÒÑM;övû}$
”G9g£i·n”S}úP#Ñgör;ˆØÝW¤C÷¤p3<70>45ÁRÔÖ¡ó2Xµ´ñ—¡¿g×Ï<C397>K5q{+:»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

View File

@@ -2,17 +2,16 @@
"images" : [
{
"idiom" : "universal",
"filename" : "iTunes-2.png",
"filename" : "PreferenceStore.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "iTunes-1.png",
"filename" : "PreferenceStore@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "iTunes.png",
"scale" : "3x"
}
],

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 558 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 558 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 558 KiB

View File

@@ -0,0 +1,15 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "Save.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
},
"properties" : {
"template-rendering-intent" : "template"
}
}

View File

@@ -0,0 +1,29 @@
%PDF-1.4
%âãÏÓ
1 0 obj <</Filter/FlateDecode/Length 321>>stream
­“;N1 †{ŸÂÀ²<C380>Ø“\‰b+
<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

View File

@@ -0,0 +1,15 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "SaveArtwork.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

View 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"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 781 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 781 KiB

View File

@@ -0,0 +1,15 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "Tick.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
},
"properties" : {
"template-rendering-intent" : "template"
}
}

File diff suppressed because it is too large Load Diff

View 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"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

View File

@@ -1,11 +0,0 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
},
"data" : [
{
"idiom" : "universal"
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 825 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 825 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 825 KiB

Binary file not shown.

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

File diff suppressed because it is too large Load Diff

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

View File

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

View File

@@ -17,15 +17,28 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<string>1.2.1</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>4242</string>
<string>37</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>

View File

@@ -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,115 @@ 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
}
}
}
}
dispatch_sync(dispatch_get_main_queue()) {
if Preferences.sharedPreferences.removeSavedItems {
for item in items {
if let album = item as? Album {
for track in album.tracks {
track.associatedTracks.removeAll()
}
if !Preferences.sharedPreferences.keepSavedAlbums {
self.albumCollection.removeAlbum(album)
}
} else if let track = item as? Track {
track.associatedTracks.removeAll()
} else if let track = item as? iTunesTrack {
if let parentTrack = self.parentForTrack(track) {
parentTrack.associatedTracks.removeElement(track)
}
}
}
self.outlineView.reloadData()
}
}
}
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 +424,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 +433,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 +461,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 +486,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 +500,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 +612,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,23 +628,71 @@ 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 {
if sectionOfRow(row) == .Albums {
if let album = outlineView.itemAtRow(row) as? Album {
for track in album.tracks {
if !track.associatedTracks.isEmpty {
return true
}
}
} else {
return true
}
}
@@ -482,13 +700,35 @@ extension MainViewController: NSUserInterfaceValidations {
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
}
}
// MARK: - Outline View
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 +740,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 +752,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 +780,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 +846,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 +854,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 +893,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 +912,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 +932,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 +957,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 +968,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 +984,7 @@ extension MainViewController: NSOutlineViewDataSource, NSOutlineViewDelegate {
}
}
} else {
unsortedTracks.extend(draggedTracks)
unsortedTracks.appendContentsOf(draggedTracks)
}
outlineView.reloadData()
return true

View File

@@ -0,0 +1,162 @@
//
// 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 StorePreferencesViewController: NSViewController {
dynamic var iTunesStores: [String] {
return NSLocale.ISOCountryCodes().map { NSLocale.currentLocale().displayNameForKey(NSLocaleCountryCode, value: $0)! }
}
dynamic var currentITunesStoreIndex: Int {
set {
let countryCode = NSLocale.ISOCountryCodes()[newValue]
Preferences.sharedPreferences.iTunesStore = countryCode
}
get {
return NSLocale.ISOCountryCodes().indexOf(Preferences.sharedPreferences.iTunesStore)!
}
}
}
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
}
}

View File

@@ -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,55 @@ 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 numberOfSearchResultsKey = "Number of Search Results"
static let iTunesStoreKey = "iTunes Store"
static let useEnglishTagsKey = "Use English Tags"
static let useLowResolutionArtworkKey = "Use Low Resolution Artwork"
static let removeSavedItemsKey = "Remove Saved Items"
static let keepSavedAlbumsKey = "Keep Saved Albums"
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 +80,61 @@ 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.numberOfSearchResultsKey: 10,
UserDefaultsConstants.iTunesStoreKey: NSLocale.currentLocale().objectForKey(NSLocaleCountryCode)!,
UserDefaultsConstants.useEnglishTagsKey: false,
UserDefaultsConstants.useLowResolutionArtworkKey: false,
UserDefaultsConstants.removeSavedItemsKey: false,
UserDefaultsConstants.keepSavedAlbumsKey: 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 +142,120 @@ 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)
}
}
/// The number of search results that should be displayed.
public dynamic var numberOfSearchResults: Int {
set {
NSUserDefaults.standardUserDefaults().setInteger(newValue, forKey: UserDefaultsConstants.numberOfSearchResultsKey)
}
get {
return NSUserDefaults.standardUserDefaults().integerForKey(UserDefaultsConstants.numberOfSearchResultsKey)
}
}
/// The iTunes Store from which the metadata should be loaded.
///
/// The value of this property must be a valid two-letter ISO country code.
public dynamic var iTunesStore: String {
set {
NSUserDefaults.standardUserDefaults().setObject(newValue, forKey: UserDefaultsConstants.iTunesStoreKey)
}
get {
return NSUserDefaults.standardUserDefaults().stringForKey(UserDefaultsConstants.iTunesStoreKey)!
}
}
/// If `true` the Search API Request adds the "lang=en" option.
public dynamic var useEnglishTags: Bool {
set {
NSUserDefaults.standardUserDefaults().setBool(newValue, forKey: UserDefaultsConstants.useEnglishTagsKey)
}
get {
return NSUserDefaults.standardUserDefaults().boolForKey(UserDefaultsConstants.useEnglishTagsKey)
}
}
/// 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)
}
}
/// If `true` all saved items are removed from the list after saving.
public dynamic var removeSavedItems: Bool {
set {
NSUserDefaults.standardUserDefaults().setBool(newValue, forKey: UserDefaultsConstants.removeSavedItemsKey)
}
get {
return NSUserDefaults.standardUserDefaults().boolForKey(UserDefaultsConstants.removeSavedItemsKey)
}
}
/// If `true` and `removeSavedItems` is also `true` albums are not removed on
/// saving.
public dynamic var keepSavedAlbums: Bool {
set {
NSUserDefaults.standardUserDefaults().setBool(newValue, forKey: UserDefaultsConstants.keepSavedAlbumsKey)
}
get {
return NSUserDefaults.standardUserDefaults().boolForKey(UserDefaultsConstants.keepSavedAlbumsKey)
}
}
// 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)!) }
}
}
}

View File

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

View File

@@ -10,7 +10,7 @@ import Cocoa
/// Represents an `Album` returned fromm the
/// [Search API](https://www.apple.com/itunes/affiliates/resources/documentation/itunes-store-web-service-search-api.html).
public class SearchResult: Equatable {
public class SearchResult {
public let id: iTunesId
@@ -44,6 +44,14 @@ public class SearchResult: Equatable {
}
extension SearchResult: Hashable {
public var hashValue: Int {
return Int(id)
}
}
extension Album {
public convenience init(searchResult: SearchResult) {
@@ -54,12 +62,4 @@ extension Album {
public func ==(lhs: SearchResult, rhs: SearchResult) -> Bool {
return lhs.id == rhs.id
}
public func ==(lhs: SearchResult, rhs: Album) -> Bool {
return lhs.id == rhs.id
}
public func ==(lhs: Album, rhs: SearchResult) -> Bool {
return lhs.id == rhs.id
}

View File

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

View File

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

Binary file not shown.

View 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>%#@cover@ konnten nicht gespeichert werden.</string>
<key>artworks</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Ein Cover</string>
<key>other</key>
<string>%d Cover</string>
</dict>
</dict>
</dict>
</plist>

View File

@@ -0,0 +1,915 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="8191" systemVersion="15B38b" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="8191"/>
</dependencies>
<scenes>
<!--Application-->
<scene sceneID="JPo-4y-FX3">
<objects>
<application id="hnw-xV-0zn" sceneMemberID="viewController">
<menu key="mainMenu" systemMenu="main" id="AYu-sK-qS6">
<items>
<menuItem title="TagTunes" id="1Xt-HY-uBw">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="TagTunes" systemMenu="apple" id="uQy-DD-JDr">
<items>
<menuItem title="Über TagTunes" id="5kV-Vb-QxS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="orderFrontStandardAboutPanel:" target="Ady-hI-5gd" id="Exp-CZ-Vem"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/>
<menuItem title="Einstellungen…" keyEquivalent="," id="BOF-NM-1cW">
<connections>
<segue destination="d4o-tN-8wc" kind="show" id="Sol-gW-DTe"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="wFC-TO-SCJ"/>
<menuItem title="Dienste" id="NMo-om-nkz">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Dienste" systemMenu="services" id="hz9-B4-Xy5"/>
</menuItem>
<menuItem isSeparatorItem="YES" id="4je-JR-u6R"/>
<menuItem title="TagTunes ausblenden" keyEquivalent="h" id="Olw-nP-bQN">
<connections>
<action selector="hide:" target="Ady-hI-5gd" id="PnN-Uc-m68"/>
</connections>
</menuItem>
<menuItem title="Andere ausblenden" keyEquivalent="h" id="Vdr-fp-XzO">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="hideOtherApplications:" target="Ady-hI-5gd" id="VT4-aY-XCT"/>
</connections>
</menuItem>
<menuItem title="Alle einblenden" id="Kd2-mp-pUS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="unhideAllApplications:" target="Ady-hI-5gd" id="Dhg-Le-xox"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="kCx-OE-vgT"/>
<menuItem title="TagTunes beenden" keyEquivalent="q" id="4sb-4s-VLi">
<connections>
<action selector="terminate:" target="Ady-hI-5gd" id="Te7-pn-YzF"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Ablage" id="dMs-cI-mzQ">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Ablage" id="bib-Uj-vzu">
<items>
<menuItem title="Close" keyEquivalent="w" id="DVo-aG-piG">
<connections>
<action selector="performClose:" target="Ady-hI-5gd" id="HmO-Ls-i7Q"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="aJh-i4-bef"/>
<menuItem title="Page Setup…" keyEquivalent="P" id="qIS-W8-SiK">
<modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/>
<connections>
<action selector="runPageLayout:" target="Ady-hI-5gd" id="Din-rz-gC5"/>
</connections>
</menuItem>
<menuItem title="Print…" keyEquivalent="p" id="aTl-1u-JFS">
<connections>
<action selector="print:" target="Ady-hI-5gd" id="qaZ-4w-aoO"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Bearbeiten" id="5QF-Oa-p0T">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Bearbeiten" id="W48-6f-4Dl">
<items>
<menuItem title="Widerrufen" keyEquivalent="z" id="dRJ-4n-Yzg">
<connections>
<action selector="undo:" target="Ady-hI-5gd" id="M6e-cu-g7V"/>
</connections>
</menuItem>
<menuItem title="Wiederholen" keyEquivalent="Z" id="6dh-zS-Vam">
<connections>
<action selector="redo:" target="Ady-hI-5gd" id="oIA-Rs-6OD"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="WRV-NI-Exz"/>
<menuItem title="Ausschneiden" keyEquivalent="x" id="uRl-iY-unG">
<connections>
<action selector="cut:" target="Ady-hI-5gd" id="YJe-68-I9s"/>
</connections>
</menuItem>
<menuItem title="Kopieren" keyEquivalent="c" id="x3v-GG-iWU">
<connections>
<action selector="copy:" target="Ady-hI-5gd" id="G1f-GL-Joy"/>
</connections>
</menuItem>
<menuItem title="Einsetzen" keyEquivalent="v" id="gVA-U4-sdL">
<connections>
<action selector="paste:" target="Ady-hI-5gd" id="UvS-8e-Qdg"/>
</connections>
</menuItem>
<menuItem title="Löschen" id="pa3-QI-u2k">
<string key="keyEquivalent" base64-UTF8="YES">
CA
</string>
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="removeSelectedItems:" target="Ady-hI-5gd" id="Q55-Ch-yfL"/>
</connections>
</menuItem>
<menuItem title="Alles auswählen" keyEquivalent="a" id="Ruw-6m-B2m">
<connections>
<action selector="selectAll:" target="Ady-hI-5gd" id="VNm-Mi-diN"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Ansicht" id="H8h-7b-M4v">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Ansicht" id="HyV-fh-RgO">
<items>
<menuItem title="Symbolleiste einblenden" keyEquivalent="t" id="snW-S8-Cw5">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="toggleToolbarShown:" target="Ady-hI-5gd" id="BXY-wc-z0C"/>
</connections>
</menuItem>
<menuItem title="Symbolleiste anpassen…" id="1UK-8n-QPP">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="runToolbarCustomizationPalette:" target="Ady-hI-5gd" id="pQI-g3-MTW"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Fenster" id="aUF-d1-5bR">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Fenster" systemMenu="window" id="Td7-aD-5lo">
<items>
<menuItem title="Im Dock ablegen" keyEquivalent="m" id="OY7-WF-poV">
<connections>
<action selector="performMiniaturize:" target="Ady-hI-5gd" id="VwT-WD-YPe"/>
</connections>
</menuItem>
<menuItem title="Zoomen" id="R4o-n2-Eq4">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="performZoom:" target="Ady-hI-5gd" id="DIl-cC-cCs"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="eu3-7i-yIM"/>
<menuItem title="Alle nach vorne bringen" id="LE2-aR-0XJ">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="arrangeInFront:" target="Ady-hI-5gd" id="DRN-fu-gQh"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Hilfe" id="wpr-3q-Mcd">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Hilfe" systemMenu="help" id="F2S-fz-NVQ">
<items>
<menuItem title="TagTunes Hilfe" keyEquivalent="?" id="FKE-Sm-Kum">
<connections>
<action selector="showHelp:" target="Ady-hI-5gd" id="y7X-2Q-9no"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
</items>
</menu>
<connections>
<outlet property="delegate" destination="Voe-Tx-rLC" id="PrD-fu-P6m"/>
</connections>
</application>
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="TagTunes" customModuleProvider="target"/>
<customObject id="Ady-hI-5gd" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="75" y="-81"/>
</scene>
<!--Window Controller-->
<scene sceneID="R2V-B0-nI4">
<objects>
<windowController id="B8D-0N-5wS" sceneMemberID="viewController">
<window key="window" title="TagTunes" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" oneShot="NO" releasedWhenClosed="NO" showsToolbarButton="NO" visibleAtLaunch="NO" animationBehavior="default" id="IQv-IB-iLA">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="196" y="240" width="480" height="270"/>
<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="Auswahl einfügen" paletteLabel="Auswahl aus iTunes einfügen" 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="Speichern" paletteLabel="In iTunes speichern" tag="-1" image="Save" id="ax1-DR-t4v">
<connections>
<action selector="performSave:" target="Oky-zY-oP4" id="9BZ-UY-ffJ"/>
</connections>
</toolbarItem>
<toolbarItem implicitItemIdentifier="0BAA5124-A8B4-49FB-8522-C426773E90C7" label="Cover sichern" paletteLabel="Cover sichern" tag="-1" image="SaveArtwork" id="PKp-tG-6Fu">
<connections>
<action selector="saveArtworks:" target="Oky-zY-oP4" id="Sdk-EP-Qhk"/>
</connections>
</toolbarItem>
<toolbarItem implicitItemIdentifier="E49F1F29-A83B-4580-B02C-D394E2FE36E6" label="Entfernen" paletteLabel="Entfernen" 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="Wck-Nn-UcZ"/>
<toolbarItem reference="ax1-DR-t4v"/>
<toolbarItem reference="PKp-tG-6Fu"/>
<toolbarItem reference="Wck-Nn-UcZ"/>
<toolbarItem reference="Fst-WO-GlQ"/>
</defaultToolbarItems>
</toolbar>
</window>
<connections>
<segue destination="XfG-lQ-9wD" kind="relationship" relationship="window.shadowedContentViewController" id="cq2-FE-JQM"/>
</connections>
</windowController>
<customObject id="Oky-zY-oP4" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="75" y="250"/>
</scene>
<!--Window Controller-->
<scene sceneID="dU3-ef-E5X">
<objects>
<windowController showSeguePresentationStyle="single" id="d4o-tN-8wc" sceneMemberID="viewController">
<window key="window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" oneShot="NO" releasedWhenClosed="NO" showsToolbarButton="NO" visibleAtLaunch="NO" animationBehavior="default" id="7rU-ut-IAI">
<windowStyleMask key="styleMask" titled="YES" closable="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="294" y="362" width="480" height="270"/>
<rect key="screenRect" x="0.0" y="0.0" width="1920" height="1177"/>
<connections>
<binding destination="d4o-tN-8wc" name="title" keyPath="window.contentViewController.title" id="Iko-NW-ghs"/>
</connections>
</window>
<connections>
<segue destination="qfM-ma-lyn" kind="relationship" relationship="window.shadowedContentViewController" id="gBt-Dq-ctT"/>
</connections>
</windowController>
<customObject id="qtI-W6-JfI" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-767" y="250"/>
</scene>
<!--Preferences View Controller-->
<scene sceneID="9ZM-fo-zrI">
<objects>
<tabViewController tabStyle="toolbar" id="qfM-ma-lyn" customClass="PreferencesViewController" customModule="AppKitPlus" sceneMemberID="viewController">
<tabViewItems>
<tabViewItem image="NSPreferencesGeneral" id="CfO-ir-iZG"/>
<tabViewItem image="PreferenceStore" id="bd1-kG-znW"/>
<tabViewItem image="PreferenceTags" id="b4Q-Io-OCl"/>
</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>
<outlet property="delegate" destination="qfM-ma-lyn" id="IHd-U1-spO"/>
</connections>
</tabView>
<connections>
<segue destination="tzd-4a-CRb" kind="relationship" relationship="tabItems" id="nK1-6z-0uq"/>
<segue destination="eFs-7W-C1H" kind="relationship" relationship="tabItems" id="INP-Yo-Kge"/>
<segue destination="qlM-h3-Tfw" kind="relationship" relationship="tabItems" id="sfW-cu-zoE"/>
</connections>
</tabViewController>
<customObject id="5zh-wl-nwU" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-767" y="687"/>
</scene>
<!--Allgemein-->
<scene sceneID="RQU-Wx-xKw">
<objects>
<viewController title="Allgemein" 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="204"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<button translatesAutoresizingMaskIntoConstraints="NO" id="kl3-n8-T9b">
<rect key="frame" x="18" y="168" width="184" height="18"/>
<animations/>
<buttonCell key="cell" type="check" title="Cover automatisch sichern" 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="sharedPreferences.saveArtwork" id="Pif-70-Zto"/>
</connections>
</button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="1fj-p7-sMs">
<rect key="frame" x="320" y="134" width="116" height="32"/>
<animations/>
<buttonCell key="cell" type="push" title="Auswählen…" 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"/>
</buttonCell>
<connections>
<action selector="chooseArtworkPath:" target="tzd-4a-CRb" id="v67-Ay-ckG"/>
</connections>
</button>
<pathControl horizontalHuggingPriority="249" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" allowsExpansionToolTips="YES" mirrorLayoutDirectionWhenInternationalizing="always" translatesAutoresizingMaskIntoConstraints="NO" id="pT8-NA-x3W">
<rect key="frame" x="20" y="140" width="298" 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="sharedPreferences.artworkTarget" id="IIq-Ja-OZl">
<dictionary key="options">
<string key="NSNullPlaceholder">Wählen Sie einen Ordner…</string>
</dictionary>
</binding>
</connections>
</pathControl>
<button translatesAutoresizingMaskIntoConstraints="NO" id="iE4-HP-hS8">
<rect key="frame" x="18" y="116" width="258" height="18"/>
<animations/>
<buttonCell key="cell" type="check" title="Existierende Bilddateien überschreiben" 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="96" width="244" height="18"/>
<animations/>
<buttonCell key="cell" type="check" title="Suchergebnisse eingeblendet lassen" 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="62" width="338" height="28"/>
<animations/>
<textFieldCell key="cell" controlSize="small" sendsActionOnEndEditing="YES" title="Wenn ausgewählt, werden Suchergebnisse nicht ausgeblendet, wenn eins ausgewählt wird." 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="R6h-LJ-31t">
<rect key="frame" x="18" y="38" width="298" height="18"/>
<animations/>
<buttonCell key="cell" type="check" title="Gespeicherte Objekte aus der Liste entfernen" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="heo-fa-eoL">
<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.removeSavedItems" id="4jF-Yd-im9"/>
</connections>
</button>
<button translatesAutoresizingMaskIntoConstraints="NO" id="3Fb-Bt-hfa">
<rect key="frame" x="30" y="18" width="238" height="18"/>
<animations/>
<buttonCell key="cell" type="check" title="Gespeicherte Alben nicht entfernen" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="yih-Me-SPj">
<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.keepSavedAlbums" id="F8U-8C-mw6"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstItem="3Fb-Bt-hfa" firstAttribute="top" secondItem="R6h-LJ-31t" secondAttribute="bottom" constant="6" symbolic="YES" id="6Wr-xS-UbL"/>
<constraint firstItem="iE4-HP-hS8" firstAttribute="top" secondItem="pT8-NA-x3W" secondAttribute="bottom" constant="8" symbolic="YES" id="6bw-if-uzQ"/>
<constraint firstItem="QNf-Uy-bMT" firstAttribute="leading" secondItem="MSr-08-ucR" secondAttribute="leading" constant="12" id="CMf-fg-xwn"/>
<constraint firstItem="1fj-p7-sMs" firstAttribute="baseline" secondItem="pT8-NA-x3W" secondAttribute="baseline" id="Cbb-Md-hLt"/>
<constraint firstItem="1fj-p7-sMs" firstAttribute="leading" secondItem="pT8-NA-x3W" secondAttribute="trailing" constant="8" symbolic="YES" id="EBN-ov-eyz"/>
<constraint firstItem="MSr-08-ucR" firstAttribute="top" secondItem="iE4-HP-hS8" secondAttribute="bottom" constant="6" symbolic="YES" id="JSO-qU-fYO"/>
<constraint firstItem="pT8-NA-x3W" firstAttribute="leading" secondItem="kl3-n8-T9b" secondAttribute="leading" id="RTj-aH-O8R"/>
<constraint firstItem="R6h-LJ-31t" firstAttribute="top" secondItem="QNf-Uy-bMT" secondAttribute="bottom" constant="8" symbolic="YES" id="Tec-W6-UjU"/>
<constraint firstItem="QNf-Uy-bMT" firstAttribute="top" secondItem="MSr-08-ucR" secondAttribute="bottom" constant="8" symbolic="YES" id="VWX-7I-Z3L"/>
<constraint firstItem="R6h-LJ-31t" firstAttribute="leading" secondItem="kl3-n8-T9b" secondAttribute="leading" id="a4y-1P-siP"/>
<constraint firstAttribute="trailing" secondItem="1fj-p7-sMs" secondAttribute="trailing" constant="20" symbolic="YES" id="ePv-QH-p9T"/>
<constraint firstItem="pT8-NA-x3W" firstAttribute="top" secondItem="kl3-n8-T9b" secondAttribute="bottom" constant="8" symbolic="YES" id="hUh-aB-iwg"/>
<constraint firstItem="kl3-n8-T9b" firstAttribute="leading" secondItem="pOc-Vr-IET" secondAttribute="leading" constant="20" symbolic="YES" id="kt7-6h-DFt"/>
<constraint firstItem="3Fb-Bt-hfa" firstAttribute="leading" secondItem="R6h-LJ-31t" secondAttribute="leading" constant="12" id="ld4-dc-YQ1"/>
<constraint firstItem="iE4-HP-hS8" firstAttribute="leading" secondItem="kl3-n8-T9b" secondAttribute="leading" id="mFQ-g8-Z2O"/>
<constraint firstItem="kl3-n8-T9b" firstAttribute="top" secondItem="pOc-Vr-IET" secondAttribute="top" constant="20" symbolic="YES" id="mW3-jS-00k"/>
<constraint firstItem="MSr-08-ucR" firstAttribute="leading" secondItem="kl3-n8-T9b" secondAttribute="leading" id="pIO-CH-97G"/>
</constraints>
<animations/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="width">
<real key="value" value="450"/>
</userDefinedRuntimeAttribute>
<userDefinedRuntimeAttribute type="number" keyPath="height">
<real key="value" value="204"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</view>
<connections>
<outlet property="artworkPathControl" destination="pT8-NA-x3W" id="qQf-u8-VJb"/>
<outlet property="chooseArtworkButton" destination="1fj-p7-sMs" id="pA6-5i-JZI"/>
</connections>
</viewController>
<customObject id="RtZ-4O-Pgk" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
<customObject id="aSx-iH-PLA" userLabel="Preferences" customClass="PreferencesSingleton" customModule="TagTunes" customModuleProvider="target"/>
</objects>
<point key="canvasLocation" x="-1285" y="1088"/>
</scene>
<!--iTunes Store-->
<scene sceneID="PjF-If-Ni1">
<objects>
<viewController title="iTunes Store" id="eFs-7W-C1H" customClass="StorePreferencesViewController" customModule="TagTunes" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" id="Xpd-Fo-HYN" customClass="PreferenceView" customModule="AppKitPlus">
<rect key="frame" x="0.0" y="0.0" width="450" height="212"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<button translatesAutoresizingMaskIntoConstraints="NO" id="HT2-V6-Vrs">
<rect key="frame" x="18" y="54" width="273" height="18"/>
<animations/>
<buttonCell key="cell" type="check" title="Keine hochauflösenden Cover verwenden" 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="AI4-XW-81o" name="value" keyPath="sharedPreferences.useLowResolutionArtwork" id="gDP-XP-49T"/>
</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="28"/>
<animations/>
<textFieldCell key="cell" controlSize="small" sendsActionOnEndEditing="YES" id="NMm-Jw-bon">
<font key="font" metaFont="smallSystem"/>
<string key="title">Diese Option betrifft nur Cover, die in TagTunes angezeigt werden. Beim Speichern wird immer die höchste Auflösung für Cover verwendet.</string>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<stepper horizontalHuggingPriority="750" verticalHuggingPriority="750" misplaced="YES" translatesAutoresizingMaskIntoConstraints="NO" id="dA4-F6-y0G">
<rect key="frame" x="227" y="169" width="19" height="27"/>
<animations/>
<stepperCell key="cell" continuous="YES" alignment="left" minValue="1" maxValue="200" doubleValue="1" id="qF2-IP-GuD"/>
<connections>
<binding destination="AI4-XW-81o" name="value" keyPath="sharedPreferences.numberOfSearchResults" id="P28-QL-9dO"/>
</connections>
</stepper>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="tZ6-50-jBC">
<rect key="frame" x="18" y="175" width="105" height="17"/>
<animations/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Suchergebnisse:" id="aTC-x8-iNw">
<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>
<textField verticalHuggingPriority="750" misplaced="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Fxp-Bf-M0J">
<rect key="frame" x="122" y="172" width="100" height="22"/>
<constraints>
<constraint firstAttribute="width" constant="100" id="Slg-G8-db4"/>
</constraints>
<animations/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" state="on" borderStyle="bezel" drawsBackground="YES" id="0F2-h7-a02">
<numberFormatter key="formatter" formatterBehavior="default10_4" usesGroupingSeparator="NO" groupingSize="0" minimumIntegerDigits="0" maximumIntegerDigits="42" id="pJw-0u-LVa">
<real key="minimum" value="1"/>
<real key="maximum" value="200"/>
</numberFormatter>
<font key="font" metaFont="system"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<connections>
<binding destination="AI4-XW-81o" name="value" keyPath="sharedPreferences.numberOfSearchResults" id="XnR-2a-hZe"/>
</connections>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="QpH-uA-EUO">
<rect key="frame" x="18" y="147" width="84" height="17"/>
<animations/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="iTunes Store:" id="M6p-MI-JS7">
<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>
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="k1x-7X-WP5">
<rect key="frame" x="106" y="140" width="105" height="26"/>
<constraints>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="100" id="FjM-26-Vzy"/>
</constraints>
<animations/>
<popUpButtonCell key="cell" type="push" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" id="0ry-cV-J7h">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="menu"/>
<menu key="menu" id="Snk-Ps-hdB"/>
</popUpButtonCell>
<connections>
<binding destination="eFs-7W-C1H" name="selectedIndex" keyPath="currentITunesStoreIndex" previousBinding="tGj-cU-CGC" id="Hdo-09-3JW"/>
<binding destination="eFs-7W-C1H" name="content" keyPath="iTunesStores" id="tGj-cU-CGC"/>
</connections>
</popUpButton>
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" setsMaxLayoutWidthAtFirstLayout="YES" translatesAutoresizingMaskIntoConstraints="NO" id="gzG-qy-YYr">
<rect key="frame" x="30" y="107" width="402" height="28"/>
<animations/>
<textFieldCell key="cell" controlSize="small" sendsActionOnEndEditing="YES" title="Manche Songs sind nicht in allen Ländern verfügbar. TagTunes verwendet den iTunes Store des ausgewählten Landes." id="xMj-9f-8N6">
<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>
<popUpButton verticalHuggingPriority="750" misplaced="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Mtw-lO-h9I">
<rect key="frame" x="138" y="75" width="168" height="26"/>
<animations/>
<popUpButtonCell key="cell" type="push" title="Englisch" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" tag="1" imageScaling="proportionallyDown" inset="2" selectedItem="1wo-cb-tWj" id="wUi-qM-3kU">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="menu"/>
<menu key="menu" id="hrO-S2-gum">
<items>
<menuItem title="Wie iTunes Store" id="Y63-oI-goT"/>
<menuItem title="Englisch" state="on" tag="1" id="1wo-cb-tWj"/>
</items>
</menu>
</popUpButtonCell>
<connections>
<binding destination="AI4-XW-81o" name="selectedTag" keyPath="sharedPreferences.useEnglishTags" id="J89-Uj-yog"/>
</connections>
</popUpButton>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="LdU-fV-yc9">
<rect key="frame" x="18" y="81" width="115" height="17"/>
<animations/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Sprache der Tags:" id="dxA-BL-XqG">
<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>
</subviews>
<constraints>
<constraint firstItem="Mtw-lO-h9I" firstAttribute="baseline" secondItem="LdU-fV-yc9" secondAttribute="baseline" id="5ft-Rx-T6s"/>
<constraint firstItem="k1x-7X-WP5" firstAttribute="top" secondItem="Fxp-Bf-M0J" secondAttribute="bottom" constant="8" id="7qz-7n-M4x"/>
<constraint firstItem="Fxp-Bf-M0J" firstAttribute="leading" secondItem="tZ6-50-jBC" secondAttribute="trailing" constant="8" symbolic="YES" id="8FR-UJ-8XC"/>
<constraint firstItem="Mtw-lO-h9I" firstAttribute="leading" secondItem="LdU-fV-yc9" secondAttribute="trailing" constant="8" symbolic="YES" id="8YH-x8-DSd"/>
<constraint firstItem="tZ6-50-jBC" firstAttribute="top" secondItem="Xpd-Fo-HYN" secondAttribute="top" constant="20" symbolic="YES" id="8oj-aa-nCm"/>
<constraint firstItem="LdU-fV-yc9" firstAttribute="leading" secondItem="tZ6-50-jBC" secondAttribute="leading" id="CUz-yq-0Fm"/>
<constraint firstItem="Fxp-Bf-M0J" firstAttribute="baseline" secondItem="tZ6-50-jBC" secondAttribute="baseline" id="FX1-xH-vOa"/>
<constraint firstItem="k1x-7X-WP5" firstAttribute="baseline" secondItem="QpH-uA-EUO" secondAttribute="baseline" id="GfC-VO-PQS"/>
<constraint firstItem="Mtw-lO-h9I" firstAttribute="top" secondItem="gzG-qy-YYr" secondAttribute="bottom" constant="8" id="HOh-z8-KD3"/>
<constraint firstAttribute="trailing" secondItem="gzG-qy-YYr" secondAttribute="trailing" constant="20" symbolic="YES" id="HrQ-7O-26V"/>
<constraint firstItem="tZ6-50-jBC" firstAttribute="leading" secondItem="Xpd-Fo-HYN" secondAttribute="leading" constant="20" symbolic="YES" id="L67-ta-ag1"/>
<constraint firstItem="HT2-V6-Vrs" firstAttribute="top" secondItem="Mtw-lO-h9I" secondAttribute="bottom" constant="8" symbolic="YES" id="NAR-TU-FBm"/>
<constraint firstItem="dA4-F6-y0G" firstAttribute="centerY" secondItem="Fxp-Bf-M0J" secondAttribute="centerY" id="QP4-nn-4oz"/>
<constraint firstItem="dA4-F6-y0G" firstAttribute="leading" secondItem="Fxp-Bf-M0J" secondAttribute="trailing" constant="8" symbolic="YES" id="QmR-Np-Qqg"/>
<constraint firstItem="gzG-qy-YYr" firstAttribute="leading" secondItem="QpH-uA-EUO" secondAttribute="leading" constant="12" id="Uym-vB-f2X"/>
<constraint firstItem="ODz-LZ-Acs" firstAttribute="leading" secondItem="HT2-V6-Vrs" secondAttribute="leading" constant="12" id="WuH-9K-Rdb"/>
<constraint firstItem="gzG-qy-YYr" firstAttribute="top" secondItem="k1x-7X-WP5" secondAttribute="bottom" constant="8" symbolic="YES" id="amB-Je-R7A"/>
<constraint firstAttribute="trailing" secondItem="ODz-LZ-Acs" secondAttribute="trailing" constant="20" symbolic="YES" id="cdY-Lu-hG4"/>
<constraint firstItem="QpH-uA-EUO" firstAttribute="leading" secondItem="tZ6-50-jBC" secondAttribute="leading" id="dyb-6z-MWP"/>
<constraint firstItem="HT2-V6-Vrs" firstAttribute="leading" secondItem="tZ6-50-jBC" secondAttribute="leading" id="gtz-u4-bBe"/>
<constraint firstItem="ODz-LZ-Acs" firstAttribute="top" secondItem="HT2-V6-Vrs" secondAttribute="bottom" constant="8" symbolic="YES" id="tlc-yV-nZb"/>
<constraint firstItem="k1x-7X-WP5" firstAttribute="leading" secondItem="QpH-uA-EUO" secondAttribute="trailing" constant="8" symbolic="YES" id="uG3-te-Y6Y"/>
</constraints>
<animations/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="width">
<real key="value" value="450"/>
</userDefinedRuntimeAttribute>
<userDefinedRuntimeAttribute type="number" keyPath="height">
<real key="value" value="212"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</view>
</viewController>
<customObject id="8Lt-AO-VZl" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
<customObject id="AI4-XW-81o" customClass="PreferencesSingleton" customModule="TagTunes" customModuleProvider="target"/>
</objects>
<point key="canvasLocation" x="-767" y="1092"/>
</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="365"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<button translatesAutoresizingMaskIntoConstraints="NO" id="0Za-Q2-FET">
<rect key="frame" x="18" y="329" width="196" height="18"/>
<animations/>
<buttonCell key="cell" type="check" title="Zensierte Namen verwenden" 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="309" width="286" height="18"/>
<animations/>
<buttonCell key="cell" type="check" title="Groß- und Kleinschreibung berücksichtigen" 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="213" width="414" height="34"/>
<animations/>
<textFieldCell key="cell" sendsActionOnEndEditing="YES" title="Manche Tags werden nicht von der iTunes API zur Verfügung gestellt. Diese Tags können nicht gespeichert werden." 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="185"/>
<clipView key="contentView" id="awx-Qa-0a0">
<rect key="frame" x="1" y="1" width="408" height="183"/>
<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="183"/>
<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" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" borderStyle="bezel" imageScaling="proportionallyDown" inset="2" arrowPosition="arrowAtCenter" preferredEdge="maxY" id="1Sz-1n-thR">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="menu"/>
<menu key="menu" id="3zQ-Pc-Wgf">
<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" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" imageScaling="proportionallyDown" inset="2" id="Och-nm-1Kl">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="menu"/>
<menu key="menu" id="E1q-CP-zfN"/>
</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="157" width="408" 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="289" width="109" height="18"/>
<animations/>
<buttonCell key="cell" type="check" title="Cover löschen" 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="255" width="402" height="28"/>
<animations/>
<textFieldCell key="cell" controlSize="small" sendsActionOnEndEditing="YES" title="Wenn aktiviert, löscht TagTunes beim Speichern das aktuelle Cover aus iTunes." 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="365"/>
</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="-248" y="1168.5"/>
</scene>
<!--Main View Controller-->
<scene sceneID="hIz-AP-VOD">
<objects>
<viewController id="XfG-lQ-9wD" customClass="MainViewController" customModule="TagTunes" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" id="m2S-Jp-Qdl">
<rect key="frame" x="0.0" y="0.0" width="692" height="390"/>
<autoresizingMask key="autoresizingMask"/>
<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"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</searchFieldCell>
<connections>
<action selector="performSearch:" target="XfG-lQ-9wD" id="zNC-V3-DNP"/>
</connections>
</searchField>
<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" 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"/>
<tableColumns>
<tableColumn width="647" minWidth="40" maxWidth="10000" id="p3C-E5-sdk">
<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="D7Q-Pc-p9W">
<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"/>
</tableColumn>
</tableColumns>
<connections>
<outlet property="dataSource" destination="XfG-lQ-9wD" id="z99-eh-ktB"/>
<outlet property="delegate" destination="XfG-lQ-9wD" id="a5p-EA-EBG"/>
</connections>
</outlineView>
</subviews>
<animations/>
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
</clipView>
<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" 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>
<constraints>
<constraint firstItem="zXR-px-hjg" firstAttribute="leading" secondItem="JF0-Xb-DqY" secondAttribute="leading" id="7Ue-RX-gnl"/>
<constraint firstAttribute="trailing" secondItem="JF0-Xb-DqY" secondAttribute="trailing" constant="20" symbolic="YES" id="KHs-FT-bYo"/>
<constraint firstAttribute="bottom" secondItem="zXR-px-hjg" secondAttribute="bottom" constant="20" symbolic="YES" id="QjD-YK-8tD"/>
<constraint firstItem="JF0-Xb-DqY" firstAttribute="leading" secondItem="m2S-Jp-Qdl" secondAttribute="leading" constant="20" symbolic="YES" id="aNE-yT-Pj9"/>
<constraint firstItem="JF0-Xb-DqY" firstAttribute="top" secondItem="m2S-Jp-Qdl" secondAttribute="top" constant="20" symbolic="YES" id="l3D-mn-F0D"/>
<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"/>
</connections>
</viewController>
<customObject id="rPt-NT-nkU" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="75" y="715"/>
</scene>
</scenes>
<resources>
<image name="Cross" width="1024" height="1024"/>
<image name="NSPreferencesGeneral" width="32" height="32"/>
<image name="Note" width="1024" height="1024"/>
<image name="PreferenceStore" width="32" height="32"/>
<image name="PreferenceTags" width="730" height="730"/>
<image name="Save" width="1024" height="1024"/>
<image name="SaveArtwork" width="1024" height="1024"/>
</resources>
</document>

View File

@@ -125,11 +125,11 @@ 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
}
return NSURL(string: "https://itunes.apple.com/search?term=\(searchTerm)&media=music&entity=album&limit=10&country=de&lang=de_DE")
return NSURL(string: "https://itunes.apple.com/search?term=\(searchTerm)&media=music&entity=album&limit=\(Preferences.sharedPreferences.numberOfSearchResults)&country=\(Preferences.sharedPreferences.iTunesStore)" + (Preferences.sharedPreferences.useEnglishTags ? "&lang=en" : ""))
}
/// Creates an URL that looks up all tracks that belong to the album with the
@@ -138,7 +138,7 @@ public struct iTunesAPI {
///
/// This function respects the user's preferences (See `Preferences` class).
public static func createAlbumLookupURLForId(id: iTunesId) -> NSURL {
return NSURL(string: "http://itunes.apple.com/lookup?id=\(id)&entity=song&country=de&lang=de_DE&limit=200")!
return NSURL(string: "http://itunes.apple.com/lookup?id=\(id)&entity=song&country=\(Preferences.sharedPreferences.iTunesStore)&limit=200" + (Preferences.sharedPreferences.useEnglishTags ? "&lang=en" : ""))!
}
}

14
de.lproj/Credits.rtf Normal file
View File

@@ -0,0 +1,14 @@
{\rtf1\ansi\ansicpg1252\cocoartf1404
{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
{\colortbl;\red255\green255\blue255;}
\paperw12240\paperh15840\margl1440\margr1440\vieww9000\viewh8400\viewkind0
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural\partightenfactor0
\f0\b\fs24 \cf0 Design und Entwicklung
\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}}.}