Compare commits
40 Commits
version-1.
...
version-1.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3669932df | ||
|
|
cb231a9f0c | ||
|
|
ed5e1083d6 | ||
|
|
dfc3be5fd1 | ||
|
|
f75c16cb27 | ||
|
|
98d6d492d0 | ||
|
|
a228f91409 | ||
|
|
e152c3da44 | ||
|
|
1d55cb8c5e | ||
|
|
7f6ef6588c | ||
|
|
e0261f7353 | ||
|
|
8d4fba8d8a | ||
|
|
548214479a | ||
|
|
24bb68f7d8 | ||
|
|
2da74aa359 | ||
|
|
7dbdc047ae | ||
|
|
c0e7727785 | ||
|
|
7cd60ffed4 | ||
|
|
2b668967e2 | ||
|
|
e287a935b1 | ||
|
|
4548f43797 | ||
|
|
dcd7d2620d | ||
|
|
d49ad31b79 | ||
|
|
bfa0deae4f | ||
|
|
b694c18daf | ||
|
|
3a400037ff | ||
|
|
257a7811d6 | ||
|
|
e2bdcd21e5 | ||
|
|
5704d0c0e5 | ||
|
|
80f177807d | ||
|
|
45f664cb10 | ||
|
|
6bf18c1aae | ||
|
|
b851b70ddb | ||
|
|
2c2252f6af | ||
|
|
d1c5e3b98a | ||
|
|
7a6192d19a | ||
|
|
5196b6a577 | ||
|
|
05c7b121a5 | ||
|
|
b778d14ea6 | ||
|
|
0bfef01875 |
14
Base.lproj/Credits.rtf
Normal file
@@ -0,0 +1,14 @@
|
||||
{\rtf1\ansi\ansicpg1252\cocoartf1404
|
||||
{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
|
||||
{\colortbl;\red255\green255\blue255;}
|
||||
\paperw12240\paperh15840\margl1440\margr1440\vieww9000\viewh8400\viewkind0
|
||||
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural\partightenfactor0
|
||||
|
||||
\f0\b\fs24 \cf0 Design 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
@@ -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
@@ -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
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -5,6 +5,22 @@
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "3B76C76E1B909B280025D550"
|
||||
BuildableName = "TagTunes.app"
|
||||
BlueprintName = "TagTunes"
|
||||
ReferencedContainer = "container:TagTunes.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
@@ -12,7 +28,36 @@
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "3B76C77F1B909B280025D550"
|
||||
BuildableName = "TagTunesTests.xctest"
|
||||
BlueprintName = "TagTunesTests"
|
||||
ReferencedContainer = "container:TagTunes.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "3B76C78A1B909B280025D550"
|
||||
BuildableName = "TagTunesUITests.xctest"
|
||||
BlueprintName = "TagTunesUITests"
|
||||
ReferencedContainer = "container:TagTunes.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "3B76C76E1B909B280025D550"
|
||||
BuildableName = "TagTunes.app"
|
||||
BlueprintName = "TagTunes"
|
||||
ReferencedContainer = "container:TagTunes.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</TestAction>
|
||||
@@ -26,6 +71,16 @@
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "3B76C76E1B909B280025D550"
|
||||
BuildableName = "TagTunes.app"
|
||||
BlueprintName = "TagTunes"
|
||||
ReferencedContainer = "container:TagTunes.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
@@ -35,6 +90,16 @@
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "3B76C76E1B909B280025D550"
|
||||
BuildableName = "TagTunes.app"
|
||||
BlueprintName = "TagTunes"
|
||||
ReferencedContainer = "container:TagTunes.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
|
||||
@@ -109,8 +109,9 @@ public class Album: iTunesType {
|
||||
/// Saves the album's artwork to the directory specified in the user's
|
||||
/// preferences (See `Preferences` for details).
|
||||
public func saveArtwork() throws {
|
||||
let url = Preferences.sharedPreferences.artworkTarget
|
||||
try artwork.saveToURL(url, filename: name)
|
||||
if let url = Preferences.sharedPreferences.artworkTarget {
|
||||
try artwork.saveToURL(url, filename: name)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if all tracks of the album are saved.
|
||||
|
||||
209
TagTunes/AlbumCollection.swift
Normal file
@@ -0,0 +1,209 @@
|
||||
//
|
||||
// AlbumCollection.swift
|
||||
// TagTunes
|
||||
//
|
||||
// Created by Kim Wittenburg on 08.09.15.
|
||||
// Copyright © 2015 Kim Wittenburg. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Manages a collection of albums. Managing includes support for deferred
|
||||
/// loading of an album's tracks as well as error support.
|
||||
public class AlbumCollection: CollectionType {
|
||||
|
||||
// MARK: Types
|
||||
|
||||
private enum AlbumState {
|
||||
|
||||
case Normal
|
||||
|
||||
case Error(NSError)
|
||||
|
||||
case Loading(NSURLSessionTask)
|
||||
|
||||
}
|
||||
|
||||
/// Notifications posted by an album collection. The `userInfo` of these
|
||||
/// notifications contains all keys specified in `Keys`.
|
||||
public struct Notifications {
|
||||
|
||||
/// Posted when an album is added to a collection. This notification is
|
||||
/// only posted if the album collection actually changed.
|
||||
public static let albumAdded = "AlbumAddedNotificationName"
|
||||
|
||||
/// Posted when an album is removed from a collection. This notification
|
||||
/// is only posted if the album collection actually changed.
|
||||
public static let albumRemoved = "AlbumRemovedNotificationName"
|
||||
|
||||
/// Posted when the album collection started a network request for an
|
||||
/// album's tracks.
|
||||
///
|
||||
/// Note that the values for the keys `Album` and `AlbumIndex` for the
|
||||
/// corresponding `AlbumFinishedLoading` notification may both be
|
||||
/// different.
|
||||
public static let albumStartedLoading = "AlbumStartedLoadingNotificationName"
|
||||
|
||||
/// Posted when an album collection finished loading the tracks for an
|
||||
/// album. Receiving this notification does not mean that the tracks were
|
||||
/// actually loaded successfully. It just means that the networ
|
||||
/// connection terminated. Use `errorForAlbum` to determine if an error
|
||||
/// occured while the tracks have been loaded.
|
||||
///
|
||||
/// - note: Since the actual `Album` instance in the album collection may
|
||||
/// change during loading its tracks it is preferred that you use the
|
||||
/// `AlbumIndexKey` of the notification to determine which album finished
|
||||
/// loading its tracks. You can however use the `AlbumKey` as well to
|
||||
/// access the `Album` instance that is currently present in the
|
||||
/// collection at the respective index.
|
||||
public static let albumFinishedLoading = "AlbumFinishedLoadingNotificationName"
|
||||
|
||||
/// These constants are available as keys in the `userInfo` dictionary
|
||||
/// for any notification an album collection may post.
|
||||
public struct Keys {
|
||||
|
||||
/// The `Album` instance affected by the notification.
|
||||
public static let album = "AlbumKey"
|
||||
|
||||
/// The index of the album affected by the notification.
|
||||
public static let albumIndex = "AlbumIndexKey"
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// Access the userlying array of albums.
|
||||
public private(set) var albums = [Album]()
|
||||
|
||||
private var albumStates = [Album: AlbumState]()
|
||||
|
||||
/// The URL session used to load tracks for albums.
|
||||
private let urlSession = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration(), delegate: nil, delegateQueue: NSOperationQueue.mainQueue())
|
||||
|
||||
// MARK: Collection Type
|
||||
|
||||
public init() {}
|
||||
|
||||
public var startIndex: Int {
|
||||
return albums.startIndex
|
||||
}
|
||||
|
||||
public var endIndex: Int {
|
||||
return albums.endIndex
|
||||
}
|
||||
|
||||
public subscript (position: Int) -> Album {
|
||||
return albums[position]
|
||||
}
|
||||
|
||||
// MARK: Album Collection
|
||||
|
||||
/// Adds the specified album, if not already present, and begins to load its
|
||||
/// tracks.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - album: The album to be added.
|
||||
/// - flag: Specify `false` if the album collection should not begin to
|
||||
/// load the album's tracks immediately.
|
||||
public func addAlbum(album: Album, beginLoading flag: Bool = true) {
|
||||
if !albums.contains(album) {
|
||||
albums.append(album)
|
||||
let userInfo: [NSObject: AnyObject] = [AlbumCollection.Notifications.Keys.album: album, AlbumCollection.Notifications.Keys.albumIndex: albums.count-1]
|
||||
NSNotificationCenter.defaultCenter().postNotificationName(AlbumCollection.Notifications.albumAdded, object: self, userInfo: userInfo)
|
||||
if flag {
|
||||
beginLoadingTracksForAlbum(album)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes the specified album from the collection if it is present.
|
||||
public func removeAlbum(album: Album) {
|
||||
if let index = albums.indexOf(album) {
|
||||
removeAlbumAtIndex(index)
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes the album at the specified index from the collection.
|
||||
///
|
||||
/// - requires: The specified index must be in the collection's range.
|
||||
public func removeAlbumAtIndex(index: Int) {
|
||||
let album = self[index]
|
||||
setAlbumState(nil, forAlbum: album)
|
||||
albums.removeAtIndex(index)
|
||||
let userInfo: [NSObject: AnyObject] = [AlbumCollection.Notifications.Keys.album: album, AlbumCollection.Notifications.Keys.albumIndex: index]
|
||||
NSNotificationCenter.defaultCenter().postNotificationName(AlbumCollection.Notifications.albumRemoved, object: self, userInfo: userInfo)
|
||||
}
|
||||
|
||||
/// Begins to load the tracks for the specified album. If there is already a
|
||||
/// request for the specified album it is cancelled. When the tracks for the
|
||||
/// specified album have been loaded or an error occured, a
|
||||
/// `AlbumFinishedLoadingNotification` is posted.
|
||||
public func beginLoadingTracksForAlbum(album: Album) {
|
||||
guard let albumIndex = albums.indexOf(album) else {
|
||||
return
|
||||
}
|
||||
let url = iTunesAPI.createAlbumLookupURLForId(album.id)
|
||||
let task = urlSession.dataTaskWithURL(url) { (data, response, error) -> Void in
|
||||
var newAlbumIndex = self.albums.indexOf(album)!
|
||||
defer {
|
||||
let userInfo: [NSObject: AnyObject] = [AlbumCollection.Notifications.Keys.album: self.albums[albumIndex], AlbumCollection.Notifications.Keys.albumIndex: albumIndex]
|
||||
NSNotificationCenter.defaultCenter().postNotificationName(AlbumCollection.Notifications.albumFinishedLoading, object: self, userInfo: userInfo)
|
||||
}
|
||||
guard error == nil else {
|
||||
if error!.code != NSUserCancelledError {
|
||||
self.albumStates[album] = .Error(error!)
|
||||
}
|
||||
return
|
||||
}
|
||||
do {
|
||||
let newAlbum = try iTunesAPI.parseAPIData(data!)[0]
|
||||
self.albums.removeAtIndex(newAlbumIndex)
|
||||
self.albums.insert(newAlbum, atIndex: newAlbumIndex)
|
||||
self.setAlbumState(.Normal, forAlbum: album)
|
||||
} catch let error as NSError {
|
||||
self.setAlbumState(.Error(error), forAlbum: album)
|
||||
} catch _ {
|
||||
// Will never happen
|
||||
}
|
||||
}
|
||||
setAlbumState(.Loading(task), forAlbum: album)
|
||||
task.resume()
|
||||
let userInfo: [NSObject: AnyObject] = [AlbumCollection.Notifications.Keys.album: album, AlbumCollection.Notifications.Keys.albumIndex: albumIndex]
|
||||
NSNotificationCenter.defaultCenter().postNotificationName(AlbumCollection.Notifications.albumStartedLoading, object: self, userInfo: userInfo)
|
||||
|
||||
}
|
||||
|
||||
/// Cancels the request to load the tracks for the specified album and sets
|
||||
/// the error for the album to a `NSUserCancelledError` in the
|
||||
/// `NSCocoaErrorDomain`.
|
||||
public func cancelLoadingTracksForAlbum(album: Album) {
|
||||
let error = NSError(domain: NSCocoaErrorDomain, code: NSUserCancelledError, userInfo: nil)
|
||||
setAlbumState(.Error(error), forAlbum: album)
|
||||
}
|
||||
|
||||
/// Sets the state for the specified album. If the previous state was
|
||||
/// `Loading` the associated task is cancelled.
|
||||
private func setAlbumState(state: AlbumState?, forAlbum album: Album) {
|
||||
if case let .Some(.Loading(task)) = albumStates[album] {
|
||||
task.cancel()
|
||||
}
|
||||
albumStates[album] = state
|
||||
}
|
||||
|
||||
public func isAlbumLoading(album: Album) -> Bool {
|
||||
if case .Some(.Loading) = albumStates[album] {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public func errorForAlbum(album: Album) -> NSError? {
|
||||
if case let .Some(.Error(error)) = albumStates[album] {
|
||||
return error
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
}
|
||||
@@ -37,7 +37,7 @@ public class AlbumTableCellView: AdvancedTableCellView {
|
||||
/// Contains the *tick* image for saved albums.
|
||||
@IBOutlet public lazy var secondaryImageView: NSImageView! = {
|
||||
let secondaryImageView = NSImageView()
|
||||
secondaryImageView.image = NSImage(named: "Tick")
|
||||
secondaryImageView.image = NSImage(named: "Tick")?.imageByMaskingWithColor(NSColor.clearColor())
|
||||
secondaryImageView.imageScaling = .ScaleProportionallyDown
|
||||
secondaryImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
return secondaryImageView
|
||||
@@ -118,9 +118,9 @@ public class AlbumTableCellView: AdvancedTableCellView {
|
||||
/// - loading: `true` if a loading indicator should be displayed at the
|
||||
/// album view.
|
||||
public func setupForAlbum(album: Album, loading: Bool, error: NSError?) {
|
||||
textField?.stringValue = album.name
|
||||
textField?.stringValue = Preferences.sharedPreferences.useCensoredNames ? album.censoredName : album.name
|
||||
secondaryTextField?.stringValue = album.artistName
|
||||
asyncImageView.downloadImageFromURL(album.artwork.hiResURL)
|
||||
asyncImageView.downloadImageFromURL(album.artwork.displayImageURL)
|
||||
if loading {
|
||||
textField?.textColor = NSColor.disabledControlTextColor()
|
||||
rightAccessoryView = loadingIndicator
|
||||
@@ -160,10 +160,10 @@ public class AlbumTableCellView: AdvancedTableCellView {
|
||||
/// - selectable: `true` if the search result can be selected, `false`
|
||||
/// otherwise.
|
||||
public func setupForSearchResult(searchResult: SearchResult, selectable: Bool) {
|
||||
textField?.stringValue = searchResult.name
|
||||
textField?.stringValue = Preferences.sharedPreferences.useCensoredNames ? searchResult.censoredName : searchResult.name
|
||||
textField?.textColor = NSColor.controlTextColor()
|
||||
secondaryTextField?.stringValue = searchResult.artistName
|
||||
asyncImageView.downloadImageFromURL(searchResult.artwork.hiResURL)
|
||||
asyncImageView.downloadImageFromURL(searchResult.artwork.displayImageURL)
|
||||
if selectable {
|
||||
button.title = NSLocalizedString("Select", comment: "Button title for 'selecting a search result'")
|
||||
button.enabled = true
|
||||
|
||||
@@ -54,9 +54,13 @@ public class Artwork: iTunesType {
|
||||
public func saveToURL(url: NSURL, filename: String) throws {
|
||||
let directory = url.filePathURL!.path!
|
||||
let filePath = directory.stringByAppendingString("/\(filename).tiff")
|
||||
|
||||
if !Preferences.sharedPreferences.overwriteExistingFiles && NSFileManager.defaultManager().fileExistsAtPath(filePath) {
|
||||
return
|
||||
}
|
||||
|
||||
try NSFileManager.defaultManager().createDirectoryAtPath(directory, withIntermediateDirectories: true, attributes: nil)
|
||||
let _ = NSFileManager.defaultManager().createFileAtPath(filePath, contents: hiResImage?.TIFFRepresentation, attributes: nil)
|
||||
let _ = NSFileManager.defaultManager().createFileAtPath(filePath, contents: saveImage?.TIFFRepresentation, attributes: nil)
|
||||
}
|
||||
|
||||
// MARK: Calculated Properties
|
||||
@@ -103,4 +107,18 @@ public class Artwork: iTunesType {
|
||||
return cachedHiResImage
|
||||
}
|
||||
|
||||
/// Returns the url of an image that should be used to display this artwork
|
||||
/// with respect to the user's preferences.
|
||||
public var displayImageURL: NSURL {
|
||||
if !Preferences.sharedPreferences.useLowResolutionArtwork && hiResURL != nil {
|
||||
return hiResURL
|
||||
}
|
||||
return url100
|
||||
}
|
||||
|
||||
/// Returns the image that should be used to save this artwork.
|
||||
public var saveImage: NSImage? {
|
||||
return hiResImage != nil ? hiResImage : image100
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,22 +2,14 @@
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "Cross.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "Cross-1.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "Cross-2.png",
|
||||
"scale" : "3x"
|
||||
"filename" : "Cross.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
BIN
TagTunes/Assets.xcassets/Cross.imageset/Cross-1.png
vendored
|
Before Width: | Height: | Size: 55 KiB |
BIN
TagTunes/Assets.xcassets/Cross.imageset/Cross-2.png
vendored
|
Before Width: | Height: | Size: 55 KiB |
BIN
TagTunes/Assets.xcassets/Cross.imageset/Cross.pdf
vendored
Normal file
@@ -2,7 +2,7 @@
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "PauseProgressFreestandingTemplate.pdf"
|
||||
"filename" : "Note.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
30
TagTunes/Assets.xcassets/Note.imageset/Note.pdf
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
%PDF-1.4
|
||||
%âãÏÓ
|
||||
1 0 obj <</Filter/FlateDecode/Length 376>>stream
|
||||
xœÍ“;N1†ûœÂ'Å<><|$
|
||||
*
|
||||
D5Z<>–†ëó;™<>Ý¥ƒ
|
||||
<EFBFBD>~ÄqìÌ1S&ÎbôÈtHCÙa,‡ôšîÓGbúJB7|ƒ—nÓÃc¦§Ë—{öDy[¯rýiK|Ÿ/©!/¾tlïÖ–ºiÝ«—Ë%¯Ér
|
||||
›Ë"H§Í©wƒ_‹Q/!…º8"ƒ¤TD‰æk{¹ÙÆ,
|
||||
„ÏÌ5ƒ×´iÙƒø€$ˆ8%"†‚rTOE†J
|
||||
5‡Í}FC©¡iÏ;Àç>®§<C2AE>—hbŽ+õ¸$<07>v¬°GTe5Ø!9¨£É<ý–ã86!ãŠÂã˜A>Zª5ÆÒU¨i½ <C2BD>VmÈhàšó CªÅ¢\pÍ1å¨or´wM“Ùc„3<1A>¤È¹P÷´áDáÙhŒMcWÃàZØ$ǹgîðlr‘“×ãÎ"ñblR™'+óè¦UÞ=Îí<07>(r‹~÷6ìóîsÅ›}Nw¿ú½þSt|ßnÙÈ
|
||||
endstream
|
||||
endobj
|
||||
3 0 obj<</Contents 1 0 R/Type/Page/Resources<</ProcSet [/PDF /Text /ImageB /ImageC /ImageI]>>/Parent 2 0 R/MediaBox[0 0 1024 1024]>>
|
||||
endobj
|
||||
2 0 obj<</Kids[3 0 R]/Type/Pages/Count 1>>
|
||||
endobj
|
||||
4 0 obj<</Type/Catalog/Pages 2 0 R>>
|
||||
endobj
|
||||
5 0 obj<</ModDate(D:20150903235938Z)/Creator(http://www.fileformat.info/convert/image/svg2pdf.htm)/CreationDate(D:20150903235938Z)/Producer(iText1.2.3 by lowagie.com \(based on itext-paulo-152\))>>
|
||||
endobj
|
||||
xref
|
||||
0 6
|
||||
0000000000 65535 f
|
||||
0000000015 00000 n
|
||||
0000000598 00000 n
|
||||
0000000458 00000 n
|
||||
0000000648 00000 n
|
||||
0000000692 00000 n
|
||||
trailer
|
||||
<</Info 5 0 R/ID [<e234db29b05ecfd32d6259a19dede815><e234db29b05ecfd32d6259a19dede815>]/Root 4 0 R/Size 6>>
|
||||
@@ -1,29 +0,0 @@
|
||||
%PDF-1.4
|
||||
%âãÏÓ
|
||||
1 0 obj <</Filter/FlateDecode/Length 236>>stream
|
||||
xœÍ’1N1E{ŸÂ'ˆl''W@¢ ¢@Û`„f‘–b¹>ÎLⶃ
|
||||
EJüœïoGÊ®@È$ /.°‚ë¶ÀÏðŒ_ ø`Âw»ÅGx9ž<>Ç7¢¾ÿðúSI[Ÿod–@q<>Y%äTCXÔˆñR<>Ëf<C38B>™CÕ-¾<>J5oTÔâ´WYÒÑM;övû}$
|
||||
”G9g£i·n”S}úP#Ñgör;ˆØÝW¤C÷¤p3<70>45ÁRÔÖ¡ó2Xµ´ñ—¡¿g×Ï<C397>KuÚ5q{‚+:»C×ßóÁñž~õ«þ“ºo«P˜K
|
||||
endstream
|
||||
endobj
|
||||
3 0 obj<</Contents 1 0 R/Type/Page/Resources<</ProcSet [/PDF /Text /ImageB /ImageC /ImageI]>>/Parent 2 0 R/MediaBox[0 0 1024 1024]>>
|
||||
endobj
|
||||
2 0 obj<</Kids[3 0 R]/Type/Pages/Count 1>>
|
||||
endobj
|
||||
4 0 obj<</Type/Catalog/Pages 2 0 R>>
|
||||
endobj
|
||||
5 0 obj<</ModDate(D:20150902100931Z)/Creator(http://www.fileformat.info/convert/image/svg2pdf.htm)/CreationDate(D:20150902100931Z)/Producer(iText1.2.3 by lowagie.com \(based on itext-paulo-152\))>>
|
||||
endobj
|
||||
xref
|
||||
0 6
|
||||
0000000000 65535 f
|
||||
0000000015 00000 n
|
||||
0000000458 00000 n
|
||||
0000000318 00000 n
|
||||
0000000508 00000 n
|
||||
0000000552 00000 n
|
||||
trailer
|
||||
<</Info 5 0 R/ID [<ae74877cb59949374c065f7f1deb2d92><ae74877cb59949374c065f7f1deb2d92>]/Root 4 0 R/Size 6>>
|
||||
startxref
|
||||
757
|
||||
%%EOF
|
||||
@@ -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"
|
||||
}
|
||||
],
|
||||
BIN
TagTunes/Assets.xcassets/PreferenceStore.imageset/PreferenceStore@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "PreferencesTags.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "PreferencesTags-1.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "PreferencesTags-2.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 558 KiB |
|
Before Width: | Height: | Size: 558 KiB |
|
Before Width: | Height: | Size: 558 KiB |
15
TagTunes/Assets.xcassets/Save.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "Save.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
29
TagTunes/Assets.xcassets/Save.imageset/Save.pdf
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
%PDF-1.4
|
||||
%âãÏÓ
|
||||
1 0 obj <</Filter/FlateDecode/Length 321>>stream
|
||||
xœ“;N1†{ŸÂÀ²<C380>Ø“\‰b+
|
||||
Dµ<1A> áú8™É<¨(ÐJ£|~üòk?á…5ã{<gè°=úg†W¸‡üÅÛ|/ÞÁÃ#ãSHd%•!S‹ÐTGæJMp‰Ú_'í0y>J$?J‰ž¯“D«Ÿñë¬&*›X–<58>fH¹—…Ršˆ›‡+™¬Ù[ÅWXÙÝÈ[]f<>IwjÒî,žáu¯ÇàkžzðŠÂ1D·BcÎǺ¯Ñ”F|ÖmHaA<>aøÖÿ ɉ“QjúbJÙ±P6”I(+Þ01³(ª¦fÞø
|
||||
E¸Im–ŠKëm³”Ð0lj¼`‰2rÅ”É,’×ÝhX‹µTã>–ÁS±>âha>öÓvö—åvöÅïû=,ý|
|
||||
Ë®/¿¯n?®ÃÅ<C383>Oñ?2ÿþg¹ôß=K§=
|
||||
endstream
|
||||
endobj
|
||||
3 0 obj<</Contents 1 0 R/Type/Page/Resources<</ProcSet [/PDF /Text /ImageB /ImageC /ImageI]>>/Parent 2 0 R/MediaBox[0 0 1024 1024]>>
|
||||
endobj
|
||||
2 0 obj<</Kids[3 0 R]/Type/Pages/Count 1>>
|
||||
endobj
|
||||
4 0 obj<</Type/Catalog/Pages 2 0 R>>
|
||||
endobj
|
||||
5 0 obj<</ModDate(D:20150904000250Z)/Creator(http://www.fileformat.info/convert/image/svg2pdf.htm)/CreationDate(D:20150904000250Z)/Producer(iText1.2.3 by lowagie.com \(based on itext-paulo-152\))>>
|
||||
endobj
|
||||
xref
|
||||
0 6
|
||||
0000000000 65535 f
|
||||
0000000015 00000 n
|
||||
0000000543 00000 n
|
||||
0000000403 00000 n
|
||||
0000000593 00000 n
|
||||
0000000637 00000 n
|
||||
trailer
|
||||
<</Info 5 0 R/ID [<e10888a855614e5c858d394112bdd7fb><e10888a855614e5c858d394112bdd7fb>]/Root 4 0 R/Size 6>>
|
||||
startxref
|
||||
842
|
||||
15
TagTunes/Assets.xcassets/SaveArtwork.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "SaveArtwork.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
BIN
TagTunes/Assets.xcassets/SaveArtwork.imageset/SaveArtwork.pdf
vendored
Normal file
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "SaveToITunes.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "SaveToITunes-1.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "SaveToITunes-2.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 781 KiB |
|
Before Width: | Height: | Size: 781 KiB |
15
TagTunes/Assets.xcassets/Tick.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "Tick.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
4125
TagTunes/Assets.xcassets/Tick.imageset/Tick.pdf
vendored
Normal file
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "TickBW.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "TickBW-1.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "TickBW-2.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 41 KiB |
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
},
|
||||
"data" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
Before Width: | Height: | Size: 825 KiB |
|
Before Width: | Height: | Size: 825 KiB |
BIN
TagTunes/Assets.xcassets/iTunes.imageset/iTunes.png
vendored
|
Before Width: | Height: | Size: 825 KiB |
BIN
TagTunes/Base.lproj/Localizable.strings
Normal file
22
TagTunes/Base.lproj/Localizable.stringsdict
Normal file
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>%d artworks could not be saved.</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@artworks@ could not be saved</string>
|
||||
<key>artworks</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>d</string>
|
||||
<key>one</key>
|
||||
<string>One artwork</string>
|
||||
<key>other</key>
|
||||
<string>%d artworks</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
60
TagTunes/DescriptiveError.swift
Normal file
@@ -0,0 +1,60 @@
|
||||
//
|
||||
// DescriptiveError.swift
|
||||
// TagTunes
|
||||
//
|
||||
// Created by Kim Wittenburg on 06.09.15.
|
||||
// Copyright © 2015 Kim Wittenburg. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// A custom error class that wraps another error to display a more descriptive
|
||||
/// error description than for example "The operation couldn't be completed."
|
||||
public class DescriptiveError: NSError {
|
||||
|
||||
/// Initializes the receiver with the specified `underlyingError`.
|
||||
/// The underlying error is added to the receiver's `userInfo` dictionary as
|
||||
/// is the specified dictionary.
|
||||
public init(underlyingError: NSError, userInfo: [NSString: AnyObject]?) {
|
||||
var actualUserInfo = userInfo ?? [NSString: AnyObject]()
|
||||
actualUserInfo[NSUnderlyingErrorKey] = underlyingError
|
||||
super.init(domain: underlyingError.domain, code: underlyingError.code, userInfo: actualUserInfo)
|
||||
}
|
||||
|
||||
public required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("DescriptiveError instances should not be encoded.")
|
||||
}
|
||||
|
||||
/// Returns the value for the NSUnderlyingErrorKey in the error's `userInfo`
|
||||
/// dictionary as an `NSError` instance.
|
||||
public var underlyingError: NSError {
|
||||
return userInfo[NSUnderlyingErrorKey] as! NSError
|
||||
}
|
||||
|
||||
override public var localizedDescription: String {
|
||||
if domain == NSURLErrorDomain {
|
||||
switch code {
|
||||
case NSURLErrorNotConnectedToInternet:
|
||||
return NSLocalizedString("You are not connected to the internet.", comment: "Error message informing the user that he is not connected to the internet.")
|
||||
case NSURLErrorTimedOut:
|
||||
return NSLocalizedString("The network request timed out.", comment: "Error message informing the user that the network request timed out.")
|
||||
default: break
|
||||
}
|
||||
}
|
||||
return super.localizedDescription
|
||||
}
|
||||
|
||||
override public var localizedRecoverySuggestion: String? {
|
||||
if domain == NSURLErrorDomain {
|
||||
switch code {
|
||||
case NSURLErrorNotConnectedToInternet:
|
||||
return NSLocalizedString("Please check your network connection and try again.", comment: "Error recovery suggestion for 'not connected to the internet' error.")
|
||||
case NSURLErrorTimedOut:
|
||||
return NSLocalizedString("Please check your network connection and try again. If that does not help it may be possible that Apple's Search API service is currently offline. Please try again later.", comment: "Error recovery suggestion for 'time out' error.")
|
||||
default: break
|
||||
}
|
||||
}
|
||||
return super.localizedRecoverySuggestion
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
//
|
||||
// Error Handler.swift
|
||||
// TagTunes
|
||||
//
|
||||
// Created by Kim Wittenburg on 30.08.15.
|
||||
// Copyright © 2015 Kim Wittenburg. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
|
||||
class Error_Handler: T {
|
||||
|
||||
}
|
||||
@@ -17,15 +17,28 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<string>1.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>
|
||||
|
||||
@@ -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
|
||||
|
||||
162
TagTunes/Preference Controllers.swift
Normal 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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)!) }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
//
|
||||
// PreferencesTabViewController.swift
|
||||
// Harmony
|
||||
//
|
||||
// Created by Kim Wittenburg on 26.01.15.
|
||||
// Copyright (c) 2015 Das Code Kollektiv. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
|
||||
internal class GeneralPreferencesViewController: NSViewController {
|
||||
|
||||
// MARK: IBOutlets
|
||||
|
||||
@IBOutlet internal weak var artworkPathControl: NSPathControl!
|
||||
@IBOutlet internal weak var chooseArtworkButton: NSButton!
|
||||
|
||||
override internal func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
artworkPathControl.URL = Preferences.sharedPreferences.artworkTarget
|
||||
saveArtworkStateChanged(self)
|
||||
}
|
||||
|
||||
@IBAction internal func saveArtworkStateChanged(sender: AnyObject) {
|
||||
artworkPathControl.enabled = Preferences.sharedPreferences.saveArtwork
|
||||
chooseArtworkButton.enabled = Preferences.sharedPreferences.saveArtwork
|
||||
}
|
||||
|
||||
@IBAction internal func chooseArtworkPath(sender: AnyObject) {
|
||||
let openPanel = NSOpenPanel()
|
||||
openPanel.canChooseDirectories = true
|
||||
openPanel.canChooseFiles = false
|
||||
openPanel.canCreateDirectories = true
|
||||
openPanel.prompt = NSLocalizedString("Choose…", comment: "Button title prompting the user to choose a directory")
|
||||
openPanel.beginSheetModalForWindow(view.window!) {
|
||||
result in
|
||||
if result == NSModalResponseOK {
|
||||
Preferences.sharedPreferences.artworkTarget = openPanel.URL!.filePathURL!
|
||||
self.artworkPathControl.URL = openPanel.URL
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import Cocoa
|
||||
|
||||
/// Represents an `Album` returned fromm the
|
||||
/// [Search API](https://www.apple.com/itunes/affiliates/resources/documentation/itunes-store-web-service-search-api.html).
|
||||
public class SearchResult: Equatable {
|
||||
public class SearchResult {
|
||||
|
||||
public let id: iTunesId
|
||||
|
||||
@@ -44,6 +44,14 @@ public class SearchResult: Equatable {
|
||||
|
||||
}
|
||||
|
||||
extension SearchResult: Hashable {
|
||||
|
||||
public var hashValue: Int {
|
||||
return Int(id)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Album {
|
||||
|
||||
public convenience init(searchResult: SearchResult) {
|
||||
@@ -54,12 +62,4 @@ extension Album {
|
||||
|
||||
public func ==(lhs: SearchResult, rhs: SearchResult) -> Bool {
|
||||
return lhs.id == rhs.id
|
||||
}
|
||||
|
||||
public func ==(lhs: SearchResult, rhs: Album) -> Bool {
|
||||
return lhs.id == rhs.id
|
||||
}
|
||||
|
||||
public func ==(lhs: Album, rhs: SearchResult) -> Bool {
|
||||
return lhs.id == rhs.id
|
||||
}
|
||||
@@ -12,6 +12,45 @@ import Cocoa
|
||||
/// [Search API](https://www.apple.com/itunes/affiliates/resources/documentation/itunes-store-web-service-search-api.html).
|
||||
public class Track: iTunesType {
|
||||
|
||||
// MARK: Types
|
||||
|
||||
public enum Tag: String {
|
||||
case Name = "name", Artist = "artist", Year = "year", TrackNumber = "trackNumber", TrackCount = "trackCount", DiscNumber = "discNumber", DiscCount = "discCount", Genre = "genre", AlbumName = "album", AlbumArtist = "albumArtist"
|
||||
case SortName = "sortName", SortArtist = "sortArtist", SortAlbumName = "sortAlbum", SortAlbumArtist = "sortAlbumArtist", Composer = "composer", SortComposer = "sortComposer", Comment = "comment"
|
||||
|
||||
/// Returns `true` for tags that are returned from the
|
||||
/// [Search API](https://www.apple.com/itunes/affiliates/resources/documentation/itunes-store-web-service-search-api.html).
|
||||
public var isReturnedBySearchAPI: Bool {
|
||||
switch self {
|
||||
case .Name, .Artist, .Year, .TrackNumber, .TrackCount, .DiscNumber, .DiscCount, .Genre, .AlbumName, .AlbumArtist:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
public var clearable: Bool {
|
||||
switch self {
|
||||
case .Year, .TrackNumber, .TrackCount, .DiscNumber, .DiscCount:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a string identifying the respective tag that can be displayed
|
||||
/// to the user.
|
||||
public var localizedName: String {
|
||||
return NSLocalizedString("Tag: \(self.rawValue)", comment: "")
|
||||
}
|
||||
|
||||
/// Returns an array of all tags.
|
||||
public static var allTags: [Tag] {
|
||||
return [.Name, .Artist, .Year, .TrackNumber, .TrackCount, .DiscNumber, .DiscCount, .Genre, .AlbumName, .AlbumArtist, .SortName, .SortArtist, .SortAlbumName, .SortAlbumArtist, .Composer, .SortComposer, .Comment]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
public let id: iTunesId
|
||||
@@ -82,24 +121,40 @@ public class Track: iTunesType {
|
||||
/// This method respects the user's preferences (See `Preferences` class).
|
||||
public func saveToTrack(track: iTunesTrack) {
|
||||
let components = NSCalendar.currentCalendar().components(.Year, fromDate: releaseDate)
|
||||
track.name = name
|
||||
track.artist = artistName
|
||||
track.year = components.year
|
||||
track.trackNumber = trackNumber
|
||||
track.trackCount = trackCount
|
||||
track.discNumber = discNumber
|
||||
track.discCount = discCount
|
||||
track.genre = genre
|
||||
track.album = album?.name
|
||||
track.albumArtist = album?.artistName
|
||||
track.sortName = ""
|
||||
track.sortAlbum = ""
|
||||
track.sortAlbumArtist = ""
|
||||
track.sortArtist = ""
|
||||
track.composer = ""
|
||||
track.sortComposer = ""
|
||||
track.comment = ""
|
||||
track.artworks().removeAllObjects()
|
||||
let trackName = Preferences.sharedPreferences.useCensoredNames ? censoredName : name
|
||||
saveTag(.Name, toTrack: track, value: trackName)
|
||||
saveTag(.Artist, toTrack: track, value: artistName)
|
||||
saveTag(.Year, toTrack: track, value: components.year)
|
||||
saveTag(.TrackNumber, toTrack: track, value: trackNumber)
|
||||
saveTag(.TrackCount, toTrack: track, value: trackCount)
|
||||
saveTag(.DiscNumber, toTrack: track, value: discNumber)
|
||||
saveTag(.DiscCount, toTrack: track, value: discCount)
|
||||
saveTag(.Genre, toTrack: track, value: genre)
|
||||
let albumName = Preferences.sharedPreferences.useCensoredNames ? album.censoredName : album.name
|
||||
saveTag(.AlbumName, toTrack: track, value: albumName)
|
||||
saveTag(.AlbumArtist, toTrack: track, value: album.artistName)
|
||||
saveTag(.SortName, toTrack: track, value: nil)
|
||||
saveTag(.SortArtist, toTrack: track, value: nil)
|
||||
saveTag(.SortAlbumName, toTrack: track, value: nil)
|
||||
saveTag(.SortAlbumArtist, toTrack: track, value: nil)
|
||||
saveTag(.Composer, toTrack: track, value: nil)
|
||||
saveTag(.SortComposer, toTrack: track, value: nil)
|
||||
saveTag(.Comment, toTrack: track, value: nil)
|
||||
|
||||
if Preferences.sharedPreferences.clearArtworks {
|
||||
track.artworks().removeAllObjects()
|
||||
}
|
||||
}
|
||||
|
||||
private func saveTag(tag: Tag, toTrack track: iTunesTrack, value: AnyObject?) {
|
||||
switch Preferences.sharedPreferences.tagSavingBehaviors[tag]! {
|
||||
case .Save:
|
||||
track.setValue(value, forKey: tag.rawValue)
|
||||
case .Clear:
|
||||
track.setValue("", forKey: tag.rawValue)
|
||||
case .Ignore:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if all `associatedTrack`s contain the same values as the
|
||||
@@ -107,13 +162,22 @@ public class Track: iTunesType {
|
||||
public var saved: Bool {
|
||||
let components = NSCalendar.currentCalendar().components(.Year, fromDate: releaseDate)
|
||||
for track in associatedTracks {
|
||||
guard track.name == name && track.artist == artistName && track.year == components.year && track.trackNumber == trackNumber && track.trackCount == trackCount && track.discNumber == discNumber && track.discCount == discCount && track.genre == genre && track.album == album.name && track.albumArtist == album.artistName && track.composer == "" else {
|
||||
let trackName = Preferences.sharedPreferences.useCensoredNames ? censoredName : name
|
||||
let albumName = Preferences.sharedPreferences.useCensoredNames ? album.censoredName : album.name
|
||||
let options = Preferences.sharedPreferences.caseSensitive ? [] : NSStringCompareOptions.CaseInsensitiveSearch
|
||||
guard track.name.compare(trackName, options: options, range: nil, locale: nil) == .OrderedSame else {
|
||||
return false
|
||||
}
|
||||
guard track.album.compare(albumName, options: options, range: nil, locale: nil) == .OrderedSame else {
|
||||
return false
|
||||
}
|
||||
guard track.artist == artistName && track.year == components.year && track.trackNumber == trackNumber && track.trackCount == trackCount && track.discNumber == discNumber && track.discCount == discCount && track.genre == genre && track.albumArtist == album.artistName && track.composer == "" else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
extension Track: Hashable {
|
||||
|
||||
@@ -13,6 +13,20 @@ import AppKitPlus
|
||||
/// only be initialized from a nib or storyboard.
|
||||
public class TrackTableCellView: AdvancedTableCellView {
|
||||
|
||||
// MARK: Types
|
||||
|
||||
private struct Images {
|
||||
|
||||
/// Caches the tick image for track cells so that it does not need to be
|
||||
/// reloaded every time a cell is configured.
|
||||
static let tickImage = NSImage(named: "Tick")?.imageByMaskingWithColor(NSColor.clearColor())
|
||||
|
||||
/// Caches the gray tick image for track cells so that it does not need to be
|
||||
/// reloaded every time a cell is configured.
|
||||
static let grayTickImage = NSImage(named: "Tick")?.imageByMaskingWithColor(NSColor.lightGrayColor())
|
||||
|
||||
}
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// An outlet do display the track number. This acts as a secondary label.
|
||||
@@ -86,10 +100,10 @@ public class TrackTableCellView: AdvancedTableCellView {
|
||||
public func setupForTrack(track: Track) {
|
||||
style = track.album.hasSameArtistNameAsTracks ? .Simple : .CompactSubtitle
|
||||
|
||||
textField?.stringValue = track.name
|
||||
textField?.stringValue = Preferences.sharedPreferences.useCensoredNames ? track.censoredName : track.name
|
||||
if track.associatedTracks.isEmpty {
|
||||
textField?.textColor = NSColor.disabledControlTextColor()
|
||||
} else if track.associatedTracks.count > 1 || track.associatedTracks[0].name != track.name {
|
||||
} else if track.associatedTracks.count > 1 || track.associatedTracks[0].name.compare(track.name, options: Preferences.sharedPreferences.caseSensitive ? [] : .CaseInsensitiveSearch, range: nil, locale: nil) != .OrderedSame {
|
||||
textField?.textColor = NSColor.redColor()
|
||||
} else {
|
||||
textField?.textColor = NSColor.controlTextColor()
|
||||
@@ -97,9 +111,9 @@ public class TrackTableCellView: AdvancedTableCellView {
|
||||
secondaryTextField?.stringValue = track.artistName
|
||||
trackNumberTextField?.stringValue = "\(track.trackNumber)"
|
||||
if track.associatedTracks.isEmpty {
|
||||
imageView?.image = NSImage(named: "TickBW")
|
||||
imageView?.image = TrackTableCellView.Images.grayTickImage
|
||||
} else {
|
||||
imageView?.image = NSImage(named: "Tick")
|
||||
imageView?.image = TrackTableCellView.Images.tickImage
|
||||
}
|
||||
if track.saved {
|
||||
let aspectRatioConstraint = NSLayoutConstraint(
|
||||
|
||||
BIN
TagTunes/de.lproj/Localizable.strings
Normal file
22
TagTunes/de.lproj/Localizable.stringsdict
Normal file
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>%d artworks could not be saved.</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@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>
|
||||
915
TagTunes/de.lproj/Main.storyboard
Normal 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>
|
||||
@@ -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
@@ -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}}.}
|
||||