diff options
author | ch1p <me@ch1p.com> | 2015-08-14 01:04:22 +0300 |
---|---|---|
committer | ch1p <me@ch1p.com> | 2015-08-14 01:04:22 +0300 |
commit | 8c1a7423a0e526f2896d17be768abeccbeb77ad7 (patch) | |
tree | 67ad777e65ff6b0cca64a27ab5bb8455b575ffae |
initial
329 files changed, 14424 insertions, 0 deletions
diff --git a/README.md b/README.md new file mode 100644 index 0000000..3974f39 --- /dev/null +++ b/README.md @@ -0,0 +1,43 @@ +# VK Player Controller +VK Player Controller - приложение для OS X, позволяющее контролировать аудиоплеер vk.com медиа-клавишами (F7-F9) и иметь доступ к плейлисту из статусбара. + +#### Поддерживаемые браузеры: +* Chrome (+Opera, +Яндекс.Браузер) - *без расширения или с ним* +* Safari - *только без расширения* +* Firefox - *с расширением* + +#### Поддерживаемые системы +Должно работать без проблем на OS X 10.8 и выше. + +# Скриншоты +![](https://ch1p.com/vkpc/screenshots/dark_p.png) ![](https://ch1p.com/vkpc/screenshots/light_p.png) + +# Некоторые технические подробности +#### Внедрение JavaScript +Для того, чтобы получить доступ к плееру в браузере, нужно внедрить свой JavaScript-код во вкладку. Существует два способа сделать это: + +* через AppleScript *(работает в Chrome, Яндекс.Браузере и Safari)* +* через расширение *(работает в Firefox, Opera + опционально в Chrome и Яндекс.Браузере)* + +Поскольку у Firefox и Opera нет AppleScript API для выполнения JavaScript-кода в контексте вкладки, им нужно расширение. + +Судя по тому, что сообщали некоторые пользователи, метод AppleScript иногда не работает. Не очень понятно, от чего это зависит и как это воспроизвести, но на этот случай есть возможность включить режим расширения - для Chrome и Яндекс.Браузера. +Теоретически такой workaround возможен и в Safari, но под него расширение еще не портировано (предлагаю желающим заняться этим :), поэтому там альтернативы нет. + +#### Как приложение общается с брауером +Для того, чтобы внедренный во вкладку скрипт имел возможность отправлять данные приложению и принимать их от него, приложение поднимает локальный WebSocket-сервер (с поддержкой SSL) на 127.0.0.1:56130 (в данный момент для SSL используется сертификат, выданный для домена vkpc-local.ch1p.com и истекающий 16 августа 2017 года. О том, зачем это нужно – ниже). + +Когда в плеере на сайте происходит какое-то событие (например, вы переключаете трек), скрипт через WS отправляет его приложению. Когда происходит событие в самом приложении (например, вы нажали F7 или кликнули на трек в плейлисте из статусбара), сообщение отправляется скрипту, который уже переключает трек на сайте. + +Если vk.com открыт по https, браузер запрещает открывать WebSocket-соединение по http. Поэтому в качестве хака скрипт подключается не к 127.0.0.1, а к vkpc-local.ch1p.com, который указывает на 127.0.0.1. + +#### libwebsockets +Для реализации WebSocket используется библиотека libwebsockets (https://github.com/warmcat/libwebsockets). +Нужно собрать ее с поддержкой SSL, установить заголовочные файлы и подключить libwebsockets.dylib к проекту. + +# По вопросам +... можно писать сюда: https://vk.com/ez + + +P.S. Исторически сложилось, что однажды написанный говнокод в vkpc.js больше не менялся (нет желания туда лезть), его бы переписать... + diff --git a/Sparkle.framework/Headers b/Sparkle.framework/Headers new file mode 120000 index 0000000..a177d2a --- /dev/null +++ b/Sparkle.framework/Headers @@ -0,0 +1 @@ +Versions/Current/Headers
\ No newline at end of file diff --git a/Sparkle.framework/Resources b/Sparkle.framework/Resources new file mode 120000 index 0000000..953ee36 --- /dev/null +++ b/Sparkle.framework/Resources @@ -0,0 +1 @@ +Versions/Current/Resources
\ No newline at end of file diff --git a/Sparkle.framework/Sparkle b/Sparkle.framework/Sparkle new file mode 120000 index 0000000..b2c5273 --- /dev/null +++ b/Sparkle.framework/Sparkle @@ -0,0 +1 @@ +Versions/Current/Sparkle
\ No newline at end of file diff --git a/Sparkle.framework/Versions/A/Headers/SUAppcast.h b/Sparkle.framework/Versions/A/Headers/SUAppcast.h new file mode 100644 index 0000000..702f549 --- /dev/null +++ b/Sparkle.framework/Versions/A/Headers/SUAppcast.h @@ -0,0 +1,30 @@ +// +// SUAppcast.h +// Sparkle +// +// Created by Andy Matuschak on 3/12/06. +// Copyright 2006 Andy Matuschak. All rights reserved. +// + +#ifndef SUAPPCAST_H +#define SUAPPCAST_H + +@protocol SUAppcastDelegate; + +@class SUAppcastItem; +@interface SUAppcast : NSObject <NSURLDownloadDelegate> + +@property (weak) id<SUAppcastDelegate> delegate; +@property (copy) NSString *userAgentString; + +- (void)fetchAppcastFromURL:(NSURL *)url; + +@property (readonly, copy) NSArray *items; +@end + +@protocol SUAppcastDelegate <NSObject> +- (void)appcastDidFinishLoading:(SUAppcast *)appcast; +- (void)appcast:(SUAppcast *)appcast failedToLoadWithError:(NSError *)error; +@end + +#endif diff --git a/Sparkle.framework/Versions/A/Headers/SUAppcastItem.h b/Sparkle.framework/Versions/A/Headers/SUAppcastItem.h new file mode 100644 index 0000000..3334a1a --- /dev/null +++ b/Sparkle.framework/Versions/A/Headers/SUAppcastItem.h @@ -0,0 +1,40 @@ +// +// SUAppcastItem.h +// Sparkle +// +// Created by Andy Matuschak on 3/12/06. +// Copyright 2006 Andy Matuschak. All rights reserved. +// + +#ifndef SUAPPCASTITEM_H +#define SUAPPCASTITEM_H + +@interface SUAppcastItem : NSObject +@property (copy, readonly) NSString *title; +@property (copy, readonly) NSDate *date; +@property (copy, readonly) NSString *itemDescription; +@property (strong, readonly) NSURL *releaseNotesURL; +@property (copy, readonly) NSString *DSASignature; +@property (copy, readonly) NSString *minimumSystemVersion; +@property (copy, readonly) NSString *maximumSystemVersion; +@property (strong, readonly) NSURL *fileURL; +@property (copy, readonly) NSString *versionString; +@property (copy, readonly) NSString *displayVersionString; +@property (copy, readonly) NSDictionary *deltaUpdates; +@property (strong, readonly) NSURL *infoURL; + +// Initializes with data from a dictionary provided by the RSS class. +- (instancetype)initWithDictionary:(NSDictionary *)dict; +- (instancetype)initWithDictionary:(NSDictionary *)dict failureReason:(NSString **)error; + +@property (getter=isDeltaUpdate, readonly) BOOL deltaUpdate; +@property (getter=isCriticalUpdate, readonly) BOOL criticalUpdate; + +// Returns the dictionary provided in initWithDictionary; this might be useful later for extensions. +@property (readonly, copy) NSDictionary *propertiesDictionary; + +- (NSURL *)infoURL; + +@end + +#endif diff --git a/Sparkle.framework/Versions/A/Headers/SUStandardVersionComparator.h b/Sparkle.framework/Versions/A/Headers/SUStandardVersionComparator.h new file mode 100644 index 0000000..f40d571 --- /dev/null +++ b/Sparkle.framework/Versions/A/Headers/SUStandardVersionComparator.h @@ -0,0 +1,37 @@ +// +// SUStandardVersionComparator.h +// Sparkle +// +// Created by Andy Matuschak on 12/21/07. +// Copyright 2007 Andy Matuschak. All rights reserved. +// + +#ifndef SUSTANDARDVERSIONCOMPARATOR_H +#define SUSTANDARDVERSIONCOMPARATOR_H + + +#import "SUVersionComparisonProtocol.h" + +/*! + Sparkle's default version comparator. + + This comparator is adapted from MacPAD, by Kevin Ballard. + It's "dumb" in that it does essentially string comparison, + in components split by character type. +*/ +@interface SUStandardVersionComparator : NSObject <SUVersionComparison> + +/*! + Returns a singleton instance of the comparator. +*/ ++ (SUStandardVersionComparator *)defaultComparator; + +/*! + Compares version strings through textual analysis. + + See the implementation for more details. +*/ +- (NSComparisonResult)compareVersion:(NSString *)versionA toVersion:(NSString *)versionB; +@end + +#endif diff --git a/Sparkle.framework/Versions/A/Headers/SUUpdater.h b/Sparkle.framework/Versions/A/Headers/SUUpdater.h new file mode 100644 index 0000000..4c83857 --- /dev/null +++ b/Sparkle.framework/Versions/A/Headers/SUUpdater.h @@ -0,0 +1,334 @@ +// +// SUUpdater.h +// Sparkle +// +// Created by Andy Matuschak on 1/4/06. +// Copyright 2006 Andy Matuschak. All rights reserved. +// + +#ifndef SUUPDATER_H +#define SUUPDATER_H + +#import "SUVersionComparisonProtocol.h" +#import "SUVersionDisplayProtocol.h" + +@class SUUpdateDriver, SUAppcastItem, SUHost, SUAppcast; + +@protocol SUUpdaterDelegate; + +/*! + The main API in Sparkle for controlling the update mechanism. + + This class is used to configure the update paramters as well as manually + and automatically schedule and control checks for updates. + */ +@interface SUUpdater : NSObject + +@property (weak) IBOutlet id<SUUpdaterDelegate> delegate; + ++ (SUUpdater *)sharedUpdater; ++ (SUUpdater *)updaterForBundle:(NSBundle *)bundle; +- (instancetype)initForBundle:(NSBundle *)bundle; + +@property (readonly, strong) NSBundle *hostBundle; + +@property BOOL automaticallyChecksForUpdates; + +@property NSTimeInterval updateCheckInterval; + +/*! + * The URL of the appcast used to download update information. + * + * This property must be called on the main thread. + */ +@property (copy) NSURL *feedURL; + +@property (nonatomic, copy) NSString *userAgentString; + +@property BOOL sendsSystemProfile; + +@property BOOL automaticallyDownloadsUpdates; + +/*! + Explicitly checks for updates and displays a progress dialog while doing so. + + This method is meant for a main menu item. + Connect any menu item to this action in Interface Builder, + and Sparkle will check for updates and report back its findings verbosely + when it is invoked. + */ +- (IBAction)checkForUpdates:(id)sender; + +/*! + Checks for updates, but does not display any UI unless an update is found. + + This is meant for programmatically initating a check for updates. That is, + it will display no UI unless it actually finds an update, in which case it + proceeds as usual. + + If the fully automated updating is turned on, however, this will invoke that + behavior, and if an update is found, it will be downloaded and prepped for + installation. + */ +- (void)checkForUpdatesInBackground; + +/*! + Returns the date of last update check. + + \returns \c nil if no check has been performed. + */ +@property (readonly, copy) NSDate *lastUpdateCheckDate; + +/*! + Begins a "probing" check for updates which will not actually offer to + update to that version. + + However, the delegate methods + SUUpdaterDelegate::updater:didFindValidUpdate: and + SUUpdaterDelegate::updaterDidNotFindUpdate: will be called, + so you can use that information in your UI. + */ +- (void)checkForUpdateInformation; + +/*! + Appropriately schedules or cancels the update checking timer according to + the preferences for time interval and automatic checks. + + This call does not change the date of the next check, + but only the internal NSTimer. + */ +- (void)resetUpdateCycle; + +@property (readonly) BOOL updateInProgress; + +@end + +// ----------------------------------------------------------------------------- +// SUUpdater Notifications for events that might be interesting to more than just the delegate +// The updater will be the notification object +// ----------------------------------------------------------------------------- +extern NSString *const SUUpdaterDidFinishLoadingAppCastNotification; +extern NSString *const SUUpdaterDidFindValidUpdateNotification; +extern NSString *const SUUpdaterDidNotFindUpdateNotification; +extern NSString *const SUUpdaterWillRestartNotification; +#define SUUpdaterWillRelaunchApplicationNotification SUUpdaterWillRestartNotification; +#define SUUpdaterWillInstallUpdateNotification SUUpdaterWillRestartNotification; + +// Key for the SUAppcastItem object in the SUUpdaterDidFindValidUpdateNotification userInfo +extern NSString *const SUUpdaterAppcastItemNotificationKey; +// Key for the SUAppcast object in the SUUpdaterDidFinishLoadingAppCastNotification userInfo +extern NSString *const SUUpdaterAppcastNotificationKey; + +// ----------------------------------------------------------------------------- +// SUUpdater Delegate: +// ----------------------------------------------------------------------------- + +/*! + Provides methods to control the behavior of an SUUpdater object. + */ +@protocol SUUpdaterDelegate <NSObject> +@optional + +/*! + Returns whether to allow Sparkle to pop up. + + For example, this may be used to prevent Sparkle from interrupting a setup assistant. + + \param updater The SUUpdater instance. + */ +- (BOOL)updaterMayCheckForUpdates:(SUUpdater *)updater; + +/*! + Returns additional parameters to append to the appcast URL's query string. + + This is potentially based on whether or not Sparkle will also be sending along the system profile. + + \param updater The SUUpdater instance. + \param sendingProfile Whether the system profile will also be sent. + + \return An array of dictionaries with keys: "key", "value", "displayKey", "displayValue", the latter two being specifically for display to the user. + */ +- (NSArray *)feedParametersForUpdater:(SUUpdater *)updater sendingSystemProfile:(BOOL)sendingProfile; + +/*! + Returns a custom appcast URL. + + Override this to dynamically specify the entire URL. + + \param updater The SUUpdater instance. + */ +- (NSString *)feedURLStringForUpdater:(SUUpdater *)updater; + +/*! + Returns whether Sparkle should prompt the user about automatic update checks. + + Use this to override the default behavior. + + \param updater The SUUpdater instance. + */ +- (BOOL)updaterShouldPromptForPermissionToCheckForUpdates:(SUUpdater *)updater; + +/*! + Called after Sparkle has downloaded the appcast from the remote server. + + Implement this if you want to do some special handling with the appcast once it finishes loading. + + \param updater The SUUpdater instance. + \param appcast The appcast that was downloaded from the remote server. + */ +- (void)updater:(SUUpdater *)updater didFinishLoadingAppcast:(SUAppcast *)appcast; + +/*! + Returns the item in the appcast corresponding to the update that should be installed. + + If you're using special logic or extensions in your appcast, + implement this to use your own logic for finding a valid update, if any, + in the given appcast. + + \param appcast The appcast that was downloaded from the remote server. + \param updater The SUUpdater instance. + */ +- (SUAppcastItem *)bestValidUpdateInAppcast:(SUAppcast *)appcast forUpdater:(SUUpdater *)updater; + +/*! + Called when a valid update is found by the update driver. + + \param updater The SUUpdater instance. + \param item The appcast item corresponding to the update that is proposed to be installed. + */ +- (void)updater:(SUUpdater *)updater didFindValidUpdate:(SUAppcastItem *)item; + +/*! + Called when a valid update is not found. + + \param updater The SUUpdater instance. + */ +- (void)updaterDidNotFindUpdate:(SUUpdater *)updater; + +/*! + Called immediately before installing the specified update. + + \param updater The SUUpdater instance. + \param item The appcast item corresponding to the update that is proposed to be installed. + */ +- (void)updater:(SUUpdater *)updater willInstallUpdate:(SUAppcastItem *)item; + +/*! + Returns whether the relaunch should be delayed in order to perform other tasks. + + This is not called if the user didn't relaunch on the previous update, + in that case it will immediately restart. + + \param updater The SUUpdater instance. + \param item The appcast item corresponding to the update that is proposed to be installed. + \param invocation The invocation that must be completed before continuing with the relaunch. + + \return \c YES to delay the relaunch until \p invocation is invoked. + */ +- (BOOL)updater:(SUUpdater *)updater shouldPostponeRelaunchForUpdate:(SUAppcastItem *)item untilInvoking:(NSInvocation *)invocation; + +/*! + Returns whether the application should be relaunched at all. + + Some apps \b cannot be relaunched under certain circumstances. + This method can be used to explicitly prevent a relaunch. + + \param updater The SUUpdater instance. + */ +- (BOOL)updaterShouldRelaunchApplication:(SUUpdater *)updater; + +/*! + Called immediately before relaunching. + + \param updater The SUUpdater instance. + */ +- (void)updaterWillRelaunchApplication:(SUUpdater *)updater; + +/*! + Returns an object that compares version numbers to determine their arithmetic relation to each other. + + This method allows you to provide a custom version comparator. + If you don't implement this method or return \c nil, + the standard version comparator will be used. + + \sa SUStandardVersionComparator + + \param updater The SUUpdater instance. + */ +- (id<SUVersionComparison>)versionComparatorForUpdater:(SUUpdater *)updater; + +/*! + Returns an object that formats version numbers for display to the user. + + If you don't implement this method or return \c nil, + the standard version formatter will be used. + + \sa SUUpdateAlert + + \param updater The SUUpdater instance. + */ +- (id<SUVersionDisplay>)versionDisplayerForUpdater:(SUUpdater *)updater; + +/*! + Returns the path which is used to relaunch the client after the update is installed. + + The default is the path of the host bundle. + + \param updater The SUUpdater instance. + */ +- (NSString *)pathToRelaunchForUpdater:(SUUpdater *)updater; + +/*! + Called before an updater shows a modal alert window, + to give the host the opportunity to hide attached windows that may get in the way. + + \param updater The SUUpdater instance. + */ +- (void)updaterWillShowModalAlert:(SUUpdater *)updater; + +/*! + Called after an updater shows a modal alert window, + to give the host the opportunity to hide attached windows that may get in the way. + + \param updater The SUUpdater instance. + */ +- (void)updaterDidShowModalAlert:(SUUpdater *)updater; + +/*! + Called when an update is scheduled to be silently installed on quit. + + \param updater The SUUpdater instance. + \param item The appcast item corresponding to the update that is proposed to be installed. + \param invocation Can be used to trigger an immediate silent install and relaunch. + */ +- (void)updater:(SUUpdater *)updater willInstallUpdateOnQuit:(SUAppcastItem *)item immediateInstallationInvocation:(NSInvocation *)invocation; + +/*! + Calls after an update that was scheduled to be silently installed on quit has been canceled. + + \param updater The SUUpdater instance. + \param item The appcast item corresponding to the update that was proposed to be installed. + */ +- (void)updater:(SUUpdater *)updater didCancelInstallUpdateOnQuit:(SUAppcastItem *)item; + +@end + + +// ----------------------------------------------------------------------------- +// Constants: +// ----------------------------------------------------------------------------- + +// Define some minimum intervals to avoid DOS-like checking attacks. These are in seconds. +#if defined(DEBUG) && DEBUG && 0 +#define SU_MIN_CHECK_INTERVAL 60 +#else +#define SU_MIN_CHECK_INTERVAL 60 * 60 +#endif + +#if defined(DEBUG) && DEBUG && 0 +#define SU_DEFAULT_CHECK_INTERVAL 60 +#else +#define SU_DEFAULT_CHECK_INTERVAL 60 * 60 * 24 +#endif + +#endif diff --git a/Sparkle.framework/Versions/A/Headers/SUVersionComparisonProtocol.h b/Sparkle.framework/Versions/A/Headers/SUVersionComparisonProtocol.h new file mode 100644 index 0000000..d3fb3d2 --- /dev/null +++ b/Sparkle.framework/Versions/A/Headers/SUVersionComparisonProtocol.h @@ -0,0 +1,29 @@ +// +// SUVersionComparisonProtocol.h +// Sparkle +// +// Created by Andy Matuschak on 12/21/07. +// Copyright 2007 Andy Matuschak. All rights reserved. +// + +#ifndef SUVERSIONCOMPARISONPROTOCOL_H +#define SUVERSIONCOMPARISONPROTOCOL_H + +#import <Cocoa/Cocoa.h> + +/*! + Provides version comparison facilities for Sparkle. +*/ +@protocol SUVersionComparison + +/*! + An abstract method to compare two version strings. + + Should return NSOrderedAscending if b > a, NSOrderedDescending if b < a, + and NSOrderedSame if they are equivalent. +*/ +- (NSComparisonResult)compareVersion:(NSString *)versionA toVersion:(NSString *)versionB; // *** MAY BE CALLED ON NON-MAIN THREAD! + +@end + +#endif diff --git a/Sparkle.framework/Versions/A/Headers/SUVersionDisplayProtocol.h b/Sparkle.framework/Versions/A/Headers/SUVersionDisplayProtocol.h new file mode 100644 index 0000000..697f1a8 --- /dev/null +++ b/Sparkle.framework/Versions/A/Headers/SUVersionDisplayProtocol.h @@ -0,0 +1,25 @@ +// +// SUVersionDisplayProtocol.h +// EyeTV +// +// Created by Uli Kusterer on 08.12.09. +// Copyright 2009 Elgato Systems GmbH. All rights reserved. +// + +#import <Cocoa/Cocoa.h> + + +/*! + Applies special display formatting to version numbers. +*/ +@protocol SUVersionDisplay + +/*! + Formats two version strings. + + Both versions are provided so that important distinguishing information + can be displayed while also leaving out unnecessary/confusing parts. +*/ +- (void)formatVersion:(NSString **)inOutVersionA andVersion:(NSString **)inOutVersionB; + +@end diff --git a/Sparkle.framework/Versions/A/Headers/Sparkle.h b/Sparkle.framework/Versions/A/Headers/Sparkle.h new file mode 100644 index 0000000..954ca51 --- /dev/null +++ b/Sparkle.framework/Versions/A/Headers/Sparkle.h @@ -0,0 +1,22 @@ +// +// Sparkle.h +// Sparkle +// +// Created by Andy Matuschak on 3/16/06. (Modified by CDHW on 23/12/07) +// Copyright 2006 Andy Matuschak. All rights reserved. +// + +#ifndef SPARKLE_H +#define SPARKLE_H + +// This list should include the shared headers. It doesn't matter if some of them aren't shared (unless +// there are name-space collisions) so we can list all of them to start with: + +#import <Sparkle/SUUpdater.h> + +#import <Sparkle/SUAppcast.h> +#import <Sparkle/SUAppcastItem.h> +#import <Sparkle/SUVersionComparisonProtocol.h> +#import <Sparkle/SUStandardVersionComparator.h> + +#endif diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Info.plist b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Info.plist new file mode 100644 index 0000000..e557818 --- /dev/null +++ b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Info.plist @@ -0,0 +1,50 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>BuildMachineOSBuild</key> + <string>14A298i</string> + <key>CFBundleDevelopmentRegion</key> + <string>English</string> + <key>CFBundleExecutable</key> + <string>Autoupdate</string> + <key>CFBundleIconFile</key> + <string>AppIcon</string> + <key>CFBundleIdentifier</key> + <string>org.sparkle-project.Sparkle.Autoupdate</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundlePackageType</key> + <string>APPL</string> + <key>CFBundleShortVersionString</key> + <string>1.8.0</string> + <key>CFBundleSignature</key> + <string>????</string> + <key>CFBundleVersion</key> + <string>1.8.0</string> + <key>DTCompiler</key> + <string>com.apple.compilers.llvm.clang.1_0</string> + <key>DTPlatformBuild</key> + <string>6A254o</string> + <key>DTPlatformVersion</key> + <string>GM</string> + <key>DTSDKBuild</key> + <string>14A283h</string> + <key>DTSDKName</key> + <string>macosx10.10</string> + <key>DTXcode</key> + <string>0600</string> + <key>DTXcodeBuild</key> + <string>6A254o</string> + <key>LSBackgroundOnly</key> + <string>1</string> + <key>LSMinimumSystemVersion</key> + <string>10.7</string> + <key>LSUIElement</key> + <string>1</string> + <key>NSMainNibFile</key> + <string>MainMenu</string> + <key>NSPrincipalClass</key> + <string>NSApplication</string> +</dict> +</plist> diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/MacOS/Autoupdate b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/MacOS/Autoupdate Binary files differnew file mode 100755 index 0000000..a3740cd --- /dev/null +++ b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/MacOS/Autoupdate diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/PkgInfo b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/PkgInfo new file mode 100644 index 0000000..bd04210 --- /dev/null +++ b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/PkgInfo @@ -0,0 +1 @@ +APPL????
\ No newline at end of file diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/AppIcon.icns b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/AppIcon.icns Binary files differnew file mode 100644 index 0000000..b6fa734 --- /dev/null +++ b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/AppIcon.icns diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/SUStatus.nib b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/SUStatus.nib Binary files differnew file mode 100644 index 0000000..b16eb22 --- /dev/null +++ b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/SUStatus.nib diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/ar.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/ar.lproj/Sparkle.strings Binary files differnew file mode 100644 index 0000000..28c2c21 --- /dev/null +++ b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/ar.lproj/Sparkle.strings diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/cs.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/cs.lproj/Sparkle.strings Binary files differnew file mode 100644 index 0000000..a50d6bf --- /dev/null +++ b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/cs.lproj/Sparkle.strings diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/da.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/da.lproj/Sparkle.strings Binary files differnew file mode 100644 index 0000000..8c349bd --- /dev/null +++ b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/da.lproj/Sparkle.strings diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/de.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/de.lproj/Sparkle.strings Binary files differnew file mode 100644 index 0000000..fe50db5 --- /dev/null +++ b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/de.lproj/Sparkle.strings diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/el.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/el.lproj/Sparkle.strings Binary files differnew file mode 100644 index 0000000..015c213 --- /dev/null +++ b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/el.lproj/Sparkle.strings diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/en.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/en.lproj/Sparkle.strings Binary files differnew file mode 100644 index 0000000..d2e5e5c --- /dev/null +++ b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/en.lproj/Sparkle.strings diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/es.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/es.lproj/Sparkle.strings Binary files differnew file mode 100644 index 0000000..ee69407 --- /dev/null +++ b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/es.lproj/Sparkle.strings diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/fr.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/fr.lproj/Sparkle.strings Binary files differnew file mode 100644 index 0000000..c7b221b --- /dev/null +++ b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/fr.lproj/Sparkle.strings diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/is.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/is.lproj/Sparkle.strings Binary files differnew file mode 100644 index 0000000..68ae6e0 --- /dev/null +++ b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/is.lproj/Sparkle.strings diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/it.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/it.lproj/Sparkle.strings Binary files differnew file mode 100644 index 0000000..99955af --- /dev/null +++ b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/it.lproj/Sparkle.strings diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/ja.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/ja.lproj/Sparkle.strings Binary files differnew file mode 100644 index 0000000..faa546c --- /dev/null +++ b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/ja.lproj/Sparkle.strings diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/ko.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/ko.lproj/Sparkle.strings Binary files differnew file mode 100644 index 0000000..d5df79d --- /dev/null +++ b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/ko.lproj/Sparkle.strings diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/nb.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/nb.lproj/Sparkle.strings Binary files differnew file mode 100644 index 0000000..05a426d --- /dev/null +++ b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/nb.lproj/Sparkle.strings diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/nl.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/nl.lproj/Sparkle.strings Binary files differnew file mode 100644 index 0000000..95923ee --- /dev/null +++ b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/nl.lproj/Sparkle.strings diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/pl.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/pl.lproj/Sparkle.strings Binary files differnew file mode 100644 index 0000000..d10801f --- /dev/null +++ b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/pl.lproj/Sparkle.strings diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/pt_BR.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/pt_BR.lproj/Sparkle.strings Binary files differnew file mode 100644 index 0000000..0f2ef4d --- /dev/null +++ b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/pt_BR.lproj/Sparkle.strings diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/pt_PT.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/pt_PT.lproj/Sparkle.strings Binary files differnew file mode 100644 index 0000000..69853a3 --- /dev/null +++ b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/pt_PT.lproj/Sparkle.strings diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/ro.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/ro.lproj/Sparkle.strings Binary files differnew file mode 100644 index 0000000..3cd694d --- /dev/null +++ b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/ro.lproj/Sparkle.strings diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/ru.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/ru.lproj/Sparkle.strings Binary files differnew file mode 100644 index 0000000..349f17f --- /dev/null +++ b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/ru.lproj/Sparkle.strings diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/sk.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/sk.lproj/Sparkle.strings Binary files differnew file mode 100644 index 0000000..38bb70d --- /dev/null +++ b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/sk.lproj/Sparkle.strings diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/sl.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/sl.lproj/Sparkle.strings Binary files differnew file mode 100644 index 0000000..889a658 --- /dev/null +++ b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/sl.lproj/Sparkle.strings diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/sv.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/sv.lproj/Sparkle.strings Binary files differnew file mode 100644 index 0000000..fbebf96 --- /dev/null +++ b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/sv.lproj/Sparkle.strings diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/th.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/th.lproj/Sparkle.strings Binary files differnew file mode 100644 index 0000000..323bb70 --- /dev/null +++ b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/th.lproj/Sparkle.strings diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/tr.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/tr.lproj/Sparkle.strings Binary files differnew file mode 100644 index 0000000..ccc268f --- /dev/null +++ b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/tr.lproj/Sparkle.strings diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/uk.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/uk.lproj/Sparkle.strings Binary files differnew file mode 100644 index 0000000..c3f3d9e --- /dev/null +++ b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/uk.lproj/Sparkle.strings diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/zh_CN.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/zh_CN.lproj/Sparkle.strings Binary files differnew file mode 100644 index 0000000..49b68ce --- /dev/null +++ b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/zh_CN.lproj/Sparkle.strings diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/zh_TW.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/zh_TW.lproj/Sparkle.strings Binary files differnew file mode 100644 index 0000000..17fcca0 --- /dev/null +++ b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/zh_TW.lproj/Sparkle.strings diff --git a/Sparkle.framework/Versions/A/Resources/Info.plist b/Sparkle.framework/Versions/A/Resources/Info.plist new file mode 100644 index 0000000..fd7868b --- /dev/null +++ b/Sparkle.framework/Versions/A/Resources/Info.plist @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>BuildMachineOSBuild</key> + <string>14A298i</string> + <key>CFBundleDevelopmentRegion</key> + <string>en</string> + <key>CFBundleExecutable</key> + <string>Sparkle</string> + <key>CFBundleIdentifier</key> + <string>org.andymatuschak.Sparkle</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundleName</key> + <string>Sparkle</string> + <key>CFBundlePackageType</key> + <string>FMWK</string> + <key>CFBundleShortVersionString</key> + <string>1.8.0</string> + <key>CFBundleSignature</key> + <string>????</string> + <key>CFBundleVersion</key> + <string>0b186dc</string> + <key>DTCompiler</key> + <string>com.apple.compilers.llvm.clang.1_0</string> + <key>DTPlatformBuild</key> + <string>6A254o</string> + <key>DTPlatformVersion</key> + <string>GM</string> + <key>DTSDKBuild</key> + <string>14A283h</string> + <key>DTSDKName</key> + <string>macosx10.10</string> + <key>DTXcode</key> + <string>0600</string> + <key>DTXcodeBuild</key> + <string>6A254o</string> +</dict> +</plist> diff --git a/Sparkle.framework/Versions/A/Resources/SUModelTranslation.plist b/Sparkle.framework/Versions/A/Resources/SUModelTranslation.plist new file mode 100644 index 0000000..74db0ff --- /dev/null +++ b/Sparkle.framework/Versions/A/Resources/SUModelTranslation.plist @@ -0,0 +1,228 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>ADP2,1</key> + <string>Developer Transition Kit</string> + <key>iMac1,1</key> + <string>iMac G3 (Rev A-D)</string> + <key>iMac4,1</key> + <string>iMac (Core Duo)</string> + <key>iMac4,2</key> + <string>iMac for Education (17 inch, Core Duo)</string> + <key>iMac5,1</key> + <string>iMac (Core 2 Duo, 17 or 20 inch, SuperDrive)</string> + <key>iMac5,2</key> + <string>iMac (Core 2 Duo, 17 inch, Combo Drive)</string> + <key>iMac6,1</key> + <string>iMac (Core 2 Duo, 24 inch, SuperDrive)</string> + <key>iMac8,1</key> + <string>iMac (Core 2 Duo, 20 or 24 inch, Early 2008 )</string> + <key>iMac9,1</key> + <string>iMac (Core 2 Duo, 20 or 24 inch, Early or Mid 2009 )</string> + <key>iMac10,1</key> + <string>iMac (Core 2 Duo, 21.5 or 27 inch, Late 2009 )</string> + <key>iMac11,1</key> + <string>iMac (Core i5 or i7, 27 inch Late 2009)</string> + <key>iMac11,2</key> + <string>iMac (Core i3 or i5, 27 inch Mid 2010)</string> + <key>iMac11,3</key> + <string>iMac (Core i5 or i7, 27 inch Mid 2010)</string> + <key>iMac12,1</key> + <string>iMac (Core i3 or i5 or i7, 21.5 inch Mid 2010 or Late 2011)</string> + <key>iMac12,2</key> + <string>iMac (Core i5 or i7, 27 inch Mid 2011)</string> + <key>iMac13,1</key> + <string>iMac (Core i3 or i5 or i7, 21.5 inch Late 2012 or Early 2013)</string> + <key>iMac13,2</key> + <string>iMac (Core i5 or i7, 27 inch Late 2012)</string> + <key>iMac14,1</key> + <string>iMac (Core i5, 21.5 inch Late 2013)</string> + <key>iMac14,2</key> + <string>iMac (Core i5 or i7, 27 inch Late 2013)</string> + <key>iMac14,3</key> + <string>iMac (Core i5 or i7, 21.5 inch Late 2013)</string> + <key>MacBook1,1</key> + <string>MacBook (Core Duo)</string> + <key>MacBook2,1</key> + <string>MacBook (Core 2 Duo)</string> + <key>MacBook4,1</key> + <string>MacBook (Core 2 Duo Feb 2008)</string> + <key>MacBook5,1</key> + <string>MacBook (Core 2 Duo, Late 2008, Unibody)</string> + <key>MacBook5,2</key> + <string>MacBook (Core 2 Duo, Early 2009, White)</string> + <key>MacBook6,1</key> + <string>MacBook (Core 2 Duo, Late 2009, Unibody)</string> + <key>MacBook7,1</key> + <string>MacBook (Core 2 Duo, Mid 2010, White)</string> + <key>MacBookAir1,1</key> + <string>MacBook Air (Core 2 Duo, 13 inch, Early 2008)</string> + <key>MacBookAir2,1</key> + <string>MacBook Air (Core 2 Duo, 13 inch, Mid 2009)</string> + <key>MacBookAir3,1</key> + <string>MacBook Air (Core 2 Duo, 11 inch, Late 2010)</string> + <key>MacBookAir3,2</key> + <string>MacBook Air (Core 2 Duo, 13 inch, Late 2010)</string> + <key>MacBookAir4,1</key> + <string>MacBook Air (Core i5 or i7, 11 inch, Mid 2011)</string> + <key>MacBookAir4,2</key> + <string>MacBook Air (Core i5 or i7, 13 inch, Mid 2011)</string> + <key>MacBookAir5,1</key> + <string>MacBook Air (Core i5 or i7, 11 inch, Mid 2012)</string> + <key>MacBookAir5,2</key> + <string>MacBook Air (Core i5 or i7, 13 inch, Mid 2012)</string> + <key>MacBookAir6,1</key> + <string>MacBook Air (Core i5 or i7, 11 inch, Mid 2013 or Early 2014)</string> + <key>MacBookAir6,2</key> + <string>MacBook Air (Core i5 or i7, 13 inch, Mid 2013 or Early 2014)</string> + <key>MacBookPro1,1</key> + <string>MacBook Pro Core Duo (15-inch)</string> + <key>MacBookPro1,2</key> + <string>MacBook Pro Core Duo (17-inch)</string> + <key>MacBookPro2,1</key> + <string>MacBook Pro Core 2 Duo (17-inch)</string> + <key>MacBookPro2,2</key> + <string>MacBook Pro Core 2 Duo (15-inch)</string> + <key>MacBookPro3,1</key> + <string>MacBook Pro Core 2 Duo (15-inch LED, Core 2 Duo)</string> + <key>MacBookPro3,2</key> + <string>MacBook Pro Core 2 Duo (17-inch HD, Core 2 Duo)</string> + <key>MacBookPro4,1</key> + <string>MacBook Pro (Core 2 Duo Feb 2008)</string> + <key>Macmini1,1</key> + <string>Mac Mini (Core Solo/Duo)</string> + <key>MacPro1,1</key> + <string>Mac Pro (four-core)</string> + <key>MacPro2,1</key> + <string>Mac Pro (eight-core)</string> + <key>MacPro3,1</key> + <string>Mac Pro (January 2008 4- or 8- core "Harpertown")</string> + <key>MacPro4,1</key> + <string>Mac Pro (March 2009)</string> + <key>MacPro5,1</key> + <string>Mac Pro (August 2010)</string> + <key>PowerBook1,1</key> + <string>PowerBook G3</string> + <key>PowerBook2,1</key> + <string>iBook G3</string> + <key>PowerBook2,2</key> + <string>iBook G3 (FireWire)</string> + <key>PowerBook2,3</key> + <string>iBook G3</string> + <key>PowerBook2,4</key> + <string>iBook G3</string> + <key>PowerBook3,1</key> + <string>PowerBook G3 (FireWire)</string> + <key>PowerBook3,2</key> + <string>PowerBook G4</string> + <key>PowerBook3,3</key> + <string>PowerBook G4 (Gigabit Ethernet)</string> + <key>PowerBook3,4</key> + <string>PowerBook G4 (DVI)</string> + <key>PowerBook3,5</key> + <string>PowerBook G4 (1GHz / 867MHz)</string> + <key>PowerBook4,1</key> + <string>iBook G3 (Dual USB, Late 2001)</string> + <key>PowerBook4,2</key> + <string>iBook G3 (16MB VRAM)</string> + <key>PowerBook4,3</key> + <string>iBook G3 Opaque 16MB VRAM, 32MB VRAM, Early 2003)</string> + <key>PowerBook5,1</key> + <string>PowerBook G4 (17 inch)</string> + <key>PowerBook5,2</key> + <string>PowerBook G4 (15 inch FW 800)</string> + <key>PowerBook5,3</key> + <string>PowerBook G4 (17-inch 1.33GHz)</string> + <key>PowerBook5,4</key> + <string>PowerBook G4 (15 inch 1.5/1.33GHz)</string> + <key>PowerBook5,5</key> + <string>PowerBook G4 (17-inch 1.5GHz)</string> + <key>PowerBook5,6</key> + <string>PowerBook G4 (15 inch 1.67GHz/1.5GHz)</string> + <key>PowerBook5,7</key> + <string>PowerBook G4 (17-inch 1.67GHz)</string> + <key>PowerBook5,8</key> + <string>PowerBook G4 (Double layer SD, 15 inch)</string> + <key>PowerBook5,9</key> + <string>PowerBook G4 (Double layer SD, 17 inch)</string> + <key>PowerBook6,1</key> + <string>PowerBook G4 (12 inch)</string> + <key>PowerBook6,2</key> + <string>PowerBook G4 (12 inch, DVI)</string> + <key>PowerBook6,3</key> + <string>iBook G4</string> + <key>PowerBook6,4</key> + <string>PowerBook G4 (12 inch 1.33GHz)</string> + <key>PowerBook6,5</key> + <string>iBook G4 (Early-Late 2004)</string> + <key>PowerBook6,7</key> + <string>iBook G4 (Mid 2005)</string> + <key>PowerBook6,8</key> + <string>PowerBook G4 (12 inch 1.5GHz)</string> + <key>PowerMac1,1</key> + <string>Power Macintosh G3 (Blue & White)</string> + <key>PowerMac1,2</key> + <string>Power Macintosh G4 (PCI Graphics)</string> + <key>PowerMac10,1</key> + <string>Mac Mini G4</string> + <key>PowerMac10,2</key> + <string>Mac Mini (Late 2005)</string> + <key>PowerMac11,2</key> + <string>Power Macintosh G5 (Late 2005)</string> + <key>PowerMac12,1</key> + <string>iMac G5 (iSight)</string> + <key>PowerMac2,1</key> + <string>iMac G3 (Slot-loading CD-ROM)</string> + <key>PowerMac2,2</key> + <string>iMac G3 (Summer 2000)</string> + <key>PowerMac3,1</key> + <string>Power Macintosh G4 (AGP Graphics)</string> + <key>PowerMac3,2</key> + <string>Power Macintosh G4 (AGP Graphics)</string> + <key>PowerMac3,3</key> + <string>Power Macintosh G4 (Gigabit Ethernet)</string> + <key>PowerMac3,4</key> + <string>Power Macintosh G4 (Digital Audio)</string> + <key>PowerMac3,5</key> + <string>Power Macintosh G4 (Quick Silver)</string> + <key>PowerMac3,6</key> + <string>Power Macintosh G4 (Mirrored Drive Door)</string> + <key>PowerMac4,1</key> + <string>iMac G3 (Early/Summer 2001)</string> + <key>PowerMac4,2</key> + <string>iMac G4 (Flat Panel)</string> + <key>PowerMac4,4</key> + <string>eMac</string> + <key>PowerMac4,5</key> + <string>iMac G4 (17-inch Flat Panel)</string> + <key>PowerMac5,1</key> + <string>Power Macintosh G4 Cube</string> + <key>PowerMac6,1</key> + <string>iMac G4 (USB 2.0)</string> + <key>PowerMac6,3</key> + <string>iMac G4 (20-inch Flat Panel)</string> + <key>PowerMac6,4</key> + <string>eMac (USB 2.0, 2005)</string> + <key>PowerMac7,2</key> + <string>Power Macintosh G5</string> + <key>PowerMac7,3</key> + <string>Power Macintosh G5</string> + <key>PowerMac8,1</key> + <string>iMac G5</string> + <key>PowerMac8,2</key> + <string>iMac G5 (Ambient Light Sensor)</string> + <key>PowerMac9,1</key> + <string>Power Macintosh G5 (Late 2005)</string> + <key>RackMac1,1</key> + <string>Xserve G4</string> + <key>RackMac1,2</key> + <string>Xserve G4 (slot-loading, cluster node)</string> + <key>RackMac3,1</key> + <string>Xserve G5</string> + <key>Xserve1,1</key> + <string>Xserve (Intel Xeon)</string> + <key>Xserve2,1</key> + <string>Xserve (January 2008 quad-core)</string> +</dict> +</plist> diff --git a/Sparkle.framework/Versions/A/Resources/SUStatus.nib b/Sparkle.framework/Versions/A/Resources/SUStatus.nib Binary files differnew file mode 100644 index 0000000..b16eb22 --- /dev/null +++ b/Sparkle.framework/Versions/A/Resources/SUStatus.nib diff --git a/Sparkle.framework/Versions/A/Resources/en.lproj/SUAutomaticUpdateAlert.nib b/Sparkle.framework/Versions/A/Resources/en.lproj/SUAutomaticUpdateAlert.nib Binary files differnew file mode 100644 index 0000000..10b3948 --- /dev/null +++ b/Sparkle.framework/Versions/A/Resources/en.lproj/SUAutomaticUpdateAlert.nib diff --git a/Sparkle.framework/Versions/A/Resources/en.lproj/SUUpdateAlert.nib b/Sparkle.framework/Versions/A/Resources/en.lproj/SUUpdateAlert.nib Binary files differnew file mode 100644 index 0000000..2f73d6e --- /dev/null +++ b/Sparkle.framework/Versions/A/Resources/en.lproj/SUUpdateAlert.nib diff --git a/Sparkle.framework/Versions/A/Resources/en.lproj/SUUpdatePermissionPrompt.nib b/Sparkle.framework/Versions/A/Resources/en.lproj/SUUpdatePermissionPrompt.nib Binary files differnew file mode 100644 index 0000000..e962ae2 --- /dev/null +++ b/Sparkle.framework/Versions/A/Resources/en.lproj/SUUpdatePermissionPrompt.nib diff --git a/Sparkle.framework/Versions/A/Resources/en.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/en.lproj/Sparkle.strings Binary files differnew file mode 100644 index 0000000..d2e5e5c --- /dev/null +++ b/Sparkle.framework/Versions/A/Resources/en.lproj/Sparkle.strings diff --git a/Sparkle.framework/Versions/A/Sparkle b/Sparkle.framework/Versions/A/Sparkle Binary files differnew file mode 100755 index 0000000..5398212 --- /dev/null +++ b/Sparkle.framework/Versions/A/Sparkle diff --git a/Sparkle.framework/Versions/Current b/Sparkle.framework/Versions/Current new file mode 120000 index 0000000..8c7e5a6 --- /dev/null +++ b/Sparkle.framework/Versions/Current @@ -0,0 +1 @@ +A
\ No newline at end of file diff --git a/VKPC.xcodeproj/project.pbxproj b/VKPC.xcodeproj/project.pbxproj new file mode 100644 index 0000000..025b039 --- /dev/null +++ b/VKPC.xcodeproj/project.pbxproj @@ -0,0 +1,849 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 5C09B68E1A12B21600F970E8 /* Statistics.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C09B68D1A12B21600F970E8 /* Statistics.m */; }; + 5C0B60271A191C86009595C5 /* Application.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C0B60261A191C86009595C5 /* Application.m */; }; + 5C0B602A1A191CFD009595C5 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5C0B60281A191CFD009595C5 /* MainMenu.xib */; }; + 5C14D8051A07EC4F007E6D59 /* VKPC1.icns in Resources */ = {isa = PBXBuildFile; fileRef = 5C14D8041A07EC4F007E6D59 /* VKPC1.icns */; }; + 5C14D8081A07ED84007E6D59 /* PopoverClipView.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C14D8071A07ED84007E6D59 /* PopoverClipView.m */; }; + 5C14D80F1A07EECB007E6D59 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C14D80B1A07EE18007E6D59 /* QuartzCore.framework */; }; + 5C14D8121A07EF31007E6D59 /* PopoverScrollView.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C14D8111A07EF31007E6D59 /* PopoverScrollView.m */; }; + 5C2EF5421A0FCE80005442E0 /* Autostart.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C2EF5411A0FCE80005442E0 /* Autostart.m */; }; + 5C2F3A8C1A0FD58500C4ADB7 /* Sparkle.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C2F3A8B1A0FD58500C4ADB7 /* Sparkle.framework */; }; + 5C2F3A8E1A0FD5DE00C4ADB7 /* libwebsockets.dylib in CopyFiles */ = {isa = PBXBuildFile; fileRef = 5CA5468119F94ADE0038F869 /* libwebsockets.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + 5C2F3A8F1A0FD5E400C4ADB7 /* Sparkle.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 5C2F3A8B1A0FD58500C4ADB7 /* Sparkle.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 5C2FFD0D19FE6DC900CB8FA3 /* vkpc-local.ch1p.com.key in Resources */ = {isa = PBXBuildFile; fileRef = 5C2FFD0B19FE6DC900CB8FA3 /* vkpc-local.ch1p.com.key */; }; + 5C2FFD0E19FEA25400CB8FA3 /* libwebsockets.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CA5468119F94ADE0038F869 /* libwebsockets.dylib */; }; + 5C2FFD1219FEA61B00CB8FA3 /* ssl_bundle.crt in Resources */ = {isa = PBXBuildFile; fileRef = 5C2FFD1119FEA61B00CB8FA3 /* ssl_bundle.crt */; }; + 5C2FFD9219FFCF7D00CB8FA3 /* ImagesLegacy.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 5C2FFD8F19FFCF7D00CB8FA3 /* ImagesLegacy.bundle */; }; + 5C2FFD9319FFCF7D00CB8FA3 /* ImagesYosemite.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 5C2FFD9019FFCF7D00CB8FA3 /* ImagesYosemite.bundle */; }; + 5C2FFD9419FFCF7D00CB8FA3 /* ImagesYosemiteDark.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 5C2FFD9119FFCF7D00CB8FA3 /* ImagesYosemiteDark.bundle */; }; + 5C2FFD9719FFFC3500CB8FA3 /* VibrantTextField.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C2FFD9619FFFC3500CB8FA3 /* VibrantTextField.m */; }; + 5C2FFD9D1A00048100CB8FA3 /* VibrantButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C2FFD9C1A00048100CB8FA3 /* VibrantButton.m */; }; + 5C2FFDA01A001EF700CB8FA3 /* VibrantImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C2FFD9F1A001EF700CB8FA3 /* VibrantImageView.m */; }; + 5C2FFDA51A00FED500CB8FA3 /* PopoverImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C2FFDA41A00FED500CB8FA3 /* PopoverImageView.m */; }; + 5C4F7DD91844E65700394A5A /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5C4F7DD71844E65700394A5A /* InfoPlist.strings */; }; + 5C4F7DDB1844E65700394A5A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C4F7DDA1844E65700394A5A /* main.m */; }; + 5C4F7DE21844E65700394A5A /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C4F7DE11844E65700394A5A /* AppDelegate.m */; }; + 5C4F7DE71844E65700394A5A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5C4F7DE61844E65700394A5A /* Images.xcassets */; }; + 5C4F7E0B1844F8FF00394A5A /* SPMediaKeyTap.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C4F7E0A1844F8FF00394A5A /* SPMediaKeyTap.m */; }; + 5C4F7E151844F98900394A5A /* NSObject+SPInvocationGrabbing.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C4F7E111844F98900394A5A /* NSObject+SPInvocationGrabbing.m */; }; + 5C4FE5FA1848F07E0023CB77 /* inject.js in Resources */ = {isa = PBXBuildFile; fileRef = 5C67ADB01848E25F005B541C /* inject.js */; }; + 5C4FE6001849072A0023CB77 /* Server.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C4FE5FF1849072A0023CB77 /* Server.m */; }; + 5C5D418919F6D60500DEE14A /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4F7DD31844E65700394A5A /* Foundation.framework */; }; + 5C5D418A19F6D60D00DEE14A /* Carbon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4F7E161844FB2700394A5A /* Carbon.framework */; }; + 5C5D418D19F6D61700DEE14A /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4F7DCE1844E65700394A5A /* Cocoa.framework */; }; + 5C5D418E19F6D61D00DEE14A /* IOKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC25BD11861AC4E00A69C93 /* IOKit.framework */; }; + 5C5D419719F7BDAF00DEE14A /* CatchMediaButtons.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C5D419619F7BDAF00DEE14A /* CatchMediaButtons.m */; }; + 5C5F831B18718A3A00E67F59 /* AppleRemote.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C5F831418718A3A00E67F59 /* AppleRemote.m */; }; + 5C5F831C18718A3A00E67F59 /* HIDRemoteControlDevice.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C5F831618718A3A00E67F59 /* HIDRemoteControlDevice.m */; }; + 5C5F831D18718A3A00E67F59 /* MultiClickRemoteBehavior.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C5F831818718A3A00E67F59 /* MultiClickRemoteBehavior.m */; }; + 5C5F831E18718A3A00E67F59 /* RemoteControl.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C5F831A18718A3A00E67F59 /* RemoteControl.m */; }; + 5C67ADAC1848CB40005B541C /* Global.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C67ADAB1848CB40005B541C /* Global.m */; }; + 5C67ADB21848E25F005B541C /* inject.js in Sources */ = {isa = PBXBuildFile; fileRef = 5C67ADB01848E25F005B541C /* inject.js */; }; + 5C67ADB31848E25F005B541C /* inject.as in Resources */ = {isa = PBXBuildFile; fileRef = 5C67ADB11848E25F005B541C /* inject.as */; }; + 5C9D6D861A1BA5D100494738 /* NonVibrantButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C9D6D851A1BA5D100494738 /* NonVibrantButton.m */; }; + 5CA5463B19F8F10C0038F869 /* Controller.m in Sources */ = {isa = PBXBuildFile; fileRef = 5CA5463A19F8F10C0038F869 /* Controller.m */; }; + 5CBCAC6C1A025C6400C8E803 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C0E001A1847DACE00AA2D44 /* Security.framework */; }; + 5CD13449184B8919003295B0 /* FlippedView.m in Sources */ = {isa = PBXBuildFile; fileRef = 5CD13448184B8919003295B0 /* FlippedView.m */; }; + 5CD1345E184BAB48003295B0 /* PlaylistTableController.m in Sources */ = {isa = PBXBuildFile; fileRef = 5CD1345D184BAB48003295B0 /* PlaylistTableController.m */; }; + 5CD13471184BC185003295B0 /* PlaylistTableView.m in Sources */ = {isa = PBXBuildFile; fileRef = 5CD13470184BC185003295B0 /* PlaylistTableView.m */; }; + 5CD13474184BE8E2003295B0 /* PlaylistTableCellView.m in Sources */ = {isa = PBXBuildFile; fileRef = 5CD13473184BE8E2003295B0 /* PlaylistTableCellView.m */; }; + 5CD1347F184BF17E003295B0 /* PlaylistTableRowView.m in Sources */ = {isa = PBXBuildFile; fileRef = 5CD1347E184BF17E003295B0 /* PlaylistTableRowView.m */; }; + 5CD13482184CFD5C003295B0 /* ShadowTextFieldCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 5CD13481184CFD5C003295B0 /* ShadowTextFieldCell.m */; }; + 5CD13486184E4A59003295B0 /* Queue.m in Sources */ = {isa = PBXBuildFile; fileRef = 5CD13485184E4A59003295B0 /* Queue.m */; }; + 5CD1348C184E4C96003295B0 /* NSMutableArray+QueueAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 5CD1348B184E4C96003295B0 /* NSMutableArray+QueueAdditions.m */; }; + 5CEBA880184FC3C800EEB81E /* Playlist.m in Sources */ = {isa = PBXBuildFile; fileRef = 5CEBA87F184FC3C800EEB81E /* Playlist.m */; }; + 5CF166A6184A164800FB9495 /* Popover.m in Sources */ = {isa = PBXBuildFile; fileRef = 5CF166A5184A164800FB9495 /* Popover.m */; }; + 5CF166A9184A16EC00FB9495 /* PopoverController.m in Sources */ = {isa = PBXBuildFile; fileRef = 5CF166A8184A16EC00FB9495 /* PopoverController.m */; }; + 5CF166AB184A194D00FB9495 /* PopoverView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5CF166AA184A194D00FB9495 /* PopoverView.xib */; }; + 5CF166CB184A6B4D00FB9495 /* AboutWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5CF166CA184A6B4D00FB9495 /* AboutWindow.xib */; }; + 5CF166D0184AA01A00FB9495 /* AboutWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = 5CF166CF184AA01A00FB9495 /* AboutWindowController.m */; }; + 5CF166D3184AA94800FB9495 /* WindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = 5CF166D2184AA94800FB9495 /* WindowController.m */; }; + 5CFD57041A08228700891DA7 /* NSTimer+Blocks.m in Sources */ = {isa = PBXBuildFile; fileRef = 5CFD57031A08228700891DA7 /* NSTimer+Blocks.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 5C2F3A8D1A0FD5C300C4ADB7 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 5C2F3A8E1A0FD5DE00C4ADB7 /* libwebsockets.dylib in CopyFiles */, + 5C2F3A8F1A0FD5E400C4ADB7 /* Sparkle.framework in CopyFiles */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 5C09B68C1A12B21600F970E8 /* Statistics.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Statistics.h; sourceTree = "<group>"; }; + 5C09B68D1A12B21600F970E8 /* Statistics.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Statistics.m; sourceTree = "<group>"; }; + 5C0B60251A191C86009595C5 /* Application.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Application.h; sourceTree = "<group>"; }; + 5C0B60261A191C86009595C5 /* Application.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Application.m; sourceTree = "<group>"; }; + 5C0B60291A191CFD009595C5 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; }; + 5C0E001A1847DACE00AA2D44 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; + 5C14D8041A07EC4F007E6D59 /* VKPC1.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = VKPC1.icns; sourceTree = "<group>"; }; + 5C14D8061A07ED84007E6D59 /* PopoverClipView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PopoverClipView.h; sourceTree = "<group>"; }; + 5C14D8071A07ED84007E6D59 /* PopoverClipView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PopoverClipView.m; sourceTree = "<group>"; }; + 5C14D8091A07EDC3007E6D59 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; + 5C14D80B1A07EE18007E6D59 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; + 5C14D80D1A07EE67007E6D59 /* Quartz.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Quartz.framework; path = System/Library/Frameworks/Quartz.framework; sourceTree = SDKROOT; }; + 5C14D8101A07EF31007E6D59 /* PopoverScrollView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PopoverScrollView.h; sourceTree = "<group>"; }; + 5C14D8111A07EF31007E6D59 /* PopoverScrollView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PopoverScrollView.m; sourceTree = "<group>"; }; + 5C2EF5401A0FCE80005442E0 /* Autostart.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Autostart.h; sourceTree = "<group>"; }; + 5C2EF5411A0FCE80005442E0 /* Autostart.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Autostart.m; sourceTree = "<group>"; }; + 5C2F3A8B1A0FD58500C4ADB7 /* Sparkle.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Sparkle.framework; sourceTree = "<group>"; }; + 5C2FFD0B19FE6DC900CB8FA3 /* vkpc-local.ch1p.com.key */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "vkpc-local.ch1p.com.key"; sourceTree = "<group>"; }; + 5C2FFD1119FEA61B00CB8FA3 /* ssl_bundle.crt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = ssl_bundle.crt; sourceTree = "<group>"; }; + 5C2FFD8F19FFCF7D00CB8FA3 /* ImagesLegacy.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = ImagesLegacy.bundle; sourceTree = "<group>"; }; + 5C2FFD9019FFCF7D00CB8FA3 /* ImagesYosemite.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = ImagesYosemite.bundle; sourceTree = "<group>"; }; + 5C2FFD9119FFCF7D00CB8FA3 /* ImagesYosemiteDark.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = ImagesYosemiteDark.bundle; sourceTree = "<group>"; }; + 5C2FFD9519FFFC3500CB8FA3 /* VibrantTextField.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VibrantTextField.h; sourceTree = "<group>"; }; + 5C2FFD9619FFFC3500CB8FA3 /* VibrantTextField.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VibrantTextField.m; sourceTree = "<group>"; }; + 5C2FFD9B1A00048100CB8FA3 /* VibrantButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VibrantButton.h; sourceTree = "<group>"; }; + 5C2FFD9C1A00048100CB8FA3 /* VibrantButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VibrantButton.m; sourceTree = "<group>"; }; + 5C2FFD9E1A001EF700CB8FA3 /* VibrantImageView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VibrantImageView.h; sourceTree = "<group>"; }; + 5C2FFD9F1A001EF700CB8FA3 /* VibrantImageView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VibrantImageView.m; sourceTree = "<group>"; }; + 5C2FFDA31A00FED500CB8FA3 /* PopoverImageView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PopoverImageView.h; sourceTree = "<group>"; }; + 5C2FFDA41A00FED500CB8FA3 /* PopoverImageView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PopoverImageView.m; sourceTree = "<group>"; }; + 5C441BDE18479493004175A0 /* Global.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Global.h; sourceTree = "<group>"; }; + 5C4F7DCB1844E65700394A5A /* VKPC.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = VKPC.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 5C4F7DCE1844E65700394A5A /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; + 5C4F7DD11844E65700394A5A /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; }; + 5C4F7DD21844E65700394A5A /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = System/Library/Frameworks/CoreData.framework; sourceTree = SDKROOT; }; + 5C4F7DD31844E65700394A5A /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; + 5C4F7DD61844E65700394A5A /* VKPC-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "VKPC-Info.plist"; sourceTree = "<group>"; }; + 5C4F7DD81844E65700394A5A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; }; + 5C4F7DDA1844E65700394A5A /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; }; + 5C4F7DDC1844E65700394A5A /* VKPC-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "VKPC-Prefix.pch"; sourceTree = "<group>"; }; + 5C4F7DE01844E65700394A5A /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; }; + 5C4F7DE11844E65700394A5A /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; }; + 5C4F7DE61844E65700394A5A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = "<group>"; }; + 5C4F7DF41844E65700394A5A /* VKPCTests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "VKPCTests-Info.plist"; sourceTree = "<group>"; }; + 5C4F7DF61844E65700394A5A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; }; + 5C4F7DF81844E65700394A5A /* VKPCTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = VKPCTests.m; sourceTree = "<group>"; }; + 5C4F7E091844F8FF00394A5A /* SPMediaKeyTap.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPMediaKeyTap.h; sourceTree = "<group>"; }; + 5C4F7E0A1844F8FF00394A5A /* SPMediaKeyTap.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPMediaKeyTap.m; sourceTree = "<group>"; }; + 5C4F7E101844F98900394A5A /* NSObject+SPInvocationGrabbing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSObject+SPInvocationGrabbing.h"; sourceTree = "<group>"; }; + 5C4F7E111844F98900394A5A /* NSObject+SPInvocationGrabbing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSObject+SPInvocationGrabbing.m"; sourceTree = "<group>"; }; + 5C4F7E161844FB2700394A5A /* Carbon.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Carbon.framework; path = System/Library/Frameworks/Carbon.framework; sourceTree = SDKROOT; }; + 5C4FE5FE1849072A0023CB77 /* Server.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Server.h; sourceTree = "<group>"; }; + 5C4FE5FF1849072A0023CB77 /* Server.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Server.m; sourceTree = "<group>"; }; + 5C5D418B19F6D61000DEE14A /* AudioToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AudioToolbox.framework; path = System/Library/Frameworks/AudioToolbox.framework; sourceTree = SDKROOT; }; + 5C5D418F19F6F7BE00DEE14A /* Types.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Types.h; sourceTree = "<group>"; }; + 5C5D419519F7BDAF00DEE14A /* CatchMediaButtons.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CatchMediaButtons.h; sourceTree = "<group>"; }; + 5C5D419619F7BDAF00DEE14A /* CatchMediaButtons.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CatchMediaButtons.m; sourceTree = "<group>"; }; + 5C5F831318718A3A00E67F59 /* AppleRemote.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppleRemote.h; sourceTree = "<group>"; }; + 5C5F831418718A3A00E67F59 /* AppleRemote.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppleRemote.m; sourceTree = "<group>"; }; + 5C5F831518718A3A00E67F59 /* HIDRemoteControlDevice.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HIDRemoteControlDevice.h; sourceTree = "<group>"; }; + 5C5F831618718A3A00E67F59 /* HIDRemoteControlDevice.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HIDRemoteControlDevice.m; sourceTree = "<group>"; }; + 5C5F831718718A3A00E67F59 /* MultiClickRemoteBehavior.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MultiClickRemoteBehavior.h; sourceTree = "<group>"; }; + 5C5F831818718A3A00E67F59 /* MultiClickRemoteBehavior.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MultiClickRemoteBehavior.m; sourceTree = "<group>"; }; + 5C5F831918718A3A00E67F59 /* RemoteControl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RemoteControl.h; sourceTree = "<group>"; }; + 5C5F831A18718A3A00E67F59 /* RemoteControl.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RemoteControl.m; sourceTree = "<group>"; }; + 5C67ADAB1848CB40005B541C /* Global.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Global.m; sourceTree = "<group>"; }; + 5C67ADB01848E25F005B541C /* inject.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; name = inject.js; path = scripts/inject.js; sourceTree = "<group>"; }; + 5C67ADB11848E25F005B541C /* inject.as */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = inject.as; path = scripts/inject.as; sourceTree = "<group>"; }; + 5C9D6D841A1BA5D100494738 /* NonVibrantButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NonVibrantButton.h; sourceTree = "<group>"; }; + 5C9D6D851A1BA5D100494738 /* NonVibrantButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NonVibrantButton.m; sourceTree = "<group>"; }; + 5C9D6D871A1BAA6100494738 /* NSUserNotificationCenter+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSUserNotificationCenter+Private.h"; sourceTree = "<group>"; }; + 5CA5463919F8F10C0038F869 /* Controller.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Controller.h; sourceTree = "<group>"; }; + 5CA5463A19F8F10C0038F869 /* Controller.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Controller.m; sourceTree = "<group>"; }; + 5CA5468119F94ADE0038F869 /* libwebsockets.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; path = libwebsockets.dylib; sourceTree = "<group>"; }; + 5CBCAC6A1A025C3400C8E803 /* LocalAuthentication.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = LocalAuthentication.framework; path = System/Library/Frameworks/LocalAuthentication.framework; sourceTree = SDKROOT; }; + 5CC25BD11861AC4E00A69C93 /* IOKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IOKit.framework; path = System/Library/Frameworks/IOKit.framework; sourceTree = SDKROOT; }; + 5CD13447184B8919003295B0 /* FlippedView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FlippedView.h; sourceTree = "<group>"; }; + 5CD13448184B8919003295B0 /* FlippedView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FlippedView.m; sourceTree = "<group>"; }; + 5CD1345C184BAB48003295B0 /* PlaylistTableController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PlaylistTableController.h; sourceTree = "<group>"; }; + 5CD1345D184BAB48003295B0 /* PlaylistTableController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PlaylistTableController.m; sourceTree = "<group>"; }; + 5CD1346F184BC185003295B0 /* PlaylistTableView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PlaylistTableView.h; sourceTree = "<group>"; }; + 5CD13470184BC185003295B0 /* PlaylistTableView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PlaylistTableView.m; sourceTree = "<group>"; }; + 5CD13472184BE8E2003295B0 /* PlaylistTableCellView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PlaylistTableCellView.h; sourceTree = "<group>"; }; + 5CD13473184BE8E2003295B0 /* PlaylistTableCellView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PlaylistTableCellView.m; sourceTree = "<group>"; }; + 5CD1347D184BF17E003295B0 /* PlaylistTableRowView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PlaylistTableRowView.h; sourceTree = "<group>"; }; + 5CD1347E184BF17E003295B0 /* PlaylistTableRowView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PlaylistTableRowView.m; sourceTree = "<group>"; }; + 5CD13480184CFD5C003295B0 /* ShadowTextFieldCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ShadowTextFieldCell.h; sourceTree = "<group>"; }; + 5CD13481184CFD5C003295B0 /* ShadowTextFieldCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ShadowTextFieldCell.m; sourceTree = "<group>"; }; + 5CD13484184E4A59003295B0 /* Queue.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Queue.h; sourceTree = "<group>"; }; + 5CD13485184E4A59003295B0 /* Queue.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Queue.m; sourceTree = "<group>"; }; + 5CD13487184E4B95003295B0 /* QueueControllerProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QueueControllerProtocol.h; sourceTree = "<group>"; }; + 5CD1348A184E4C96003295B0 /* NSMutableArray+QueueAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSMutableArray+QueueAdditions.h"; sourceTree = "<group>"; }; + 5CD1348B184E4C96003295B0 /* NSMutableArray+QueueAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSMutableArray+QueueAdditions.m"; sourceTree = "<group>"; }; + 5CEBA87E184FC3C800EEB81E /* Playlist.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Playlist.h; sourceTree = "<group>"; }; + 5CEBA87F184FC3C800EEB81E /* Playlist.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Playlist.m; sourceTree = "<group>"; }; + 5CF166A4184A164800FB9495 /* Popover.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Popover.h; path = AXStatusItemPopup/Popover.h; sourceTree = "<group>"; }; + 5CF166A5184A164800FB9495 /* Popover.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Popover.m; path = AXStatusItemPopup/Popover.m; sourceTree = "<group>"; }; + 5CF166A7184A16EC00FB9495 /* PopoverController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PopoverController.h; sourceTree = "<group>"; }; + 5CF166A8184A16EC00FB9495 /* PopoverController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PopoverController.m; sourceTree = "<group>"; }; + 5CF166AA184A194D00FB9495 /* PopoverView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = PopoverView.xib; sourceTree = "<group>"; }; + 5CF166CA184A6B4D00FB9495 /* AboutWindow.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = AboutWindow.xib; sourceTree = "<group>"; }; + 5CF166CE184AA01A00FB9495 /* AboutWindowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AboutWindowController.h; sourceTree = "<group>"; }; + 5CF166CF184AA01A00FB9495 /* AboutWindowController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AboutWindowController.m; sourceTree = "<group>"; }; + 5CF166D1184AA94800FB9495 /* WindowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WindowController.h; sourceTree = "<group>"; }; + 5CF166D2184AA94800FB9495 /* WindowController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = WindowController.m; sourceTree = "<group>"; }; + 5CFD57021A08228700891DA7 /* NSTimer+Blocks.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSTimer+Blocks.h"; sourceTree = "<group>"; }; + 5CFD57031A08228700891DA7 /* NSTimer+Blocks.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSTimer+Blocks.m"; sourceTree = "<group>"; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 5C4F7DC81844E65700394A5A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 5C14D80F1A07EECB007E6D59 /* QuartzCore.framework in Frameworks */, + 5CBCAC6C1A025C6400C8E803 /* Security.framework in Frameworks */, + 5C2FFD0E19FEA25400CB8FA3 /* libwebsockets.dylib in Frameworks */, + 5C5D418E19F6D61D00DEE14A /* IOKit.framework in Frameworks */, + 5C5D418D19F6D61700DEE14A /* Cocoa.framework in Frameworks */, + 5C5D418A19F6D60D00DEE14A /* Carbon.framework in Frameworks */, + 5C5D418919F6D60500DEE14A /* Foundation.framework in Frameworks */, + 5C2F3A8C1A0FD58500C4ADB7 /* Sparkle.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 5C21778619F6A0CD0038495C /* Libs */ = { + isa = PBXGroup; + children = ( + 5C5F831218718A3A00E67F59 /* AppleRemote */, + ); + name = Libs; + sourceTree = "<group>"; + }; + 5C21778719F6A0DD0038495C /* Source */ = { + isa = PBXGroup; + children = ( + 5CBCAC701A02780D00C8E803 /* Catch Media Buttons */, + 5CBCAC6F1A0277AF00C8E803 /* About Window */, + 5CBCAC6D1A02776000C8E803 /* Popover */, + 5CBCAC711A02782900C8E803 /* Playlist */, + 5C21778619F6A0CD0038495C /* Libs */, + 5C2FFD1319FEC4F300CB8FA3 /* Views */, + 5C2FFDA21A004D3500CB8FA3 /* Main */, + 5CBCAC721A02786600C8E803 /* Player Controller */, + 5CF166D1184AA94800FB9495 /* WindowController.h */, + 5CF166D2184AA94800FB9495 /* WindowController.m */, + 5CD1348A184E4C96003295B0 /* NSMutableArray+QueueAdditions.h */, + 5C9D6D871A1BAA6100494738 /* NSUserNotificationCenter+Private.h */, + 5CD1348B184E4C96003295B0 /* NSMutableArray+QueueAdditions.m */, + 5CFD57031A08228700891DA7 /* NSTimer+Blocks.m */, + 5CFD57021A08228700891DA7 /* NSTimer+Blocks.h */, + ); + name = Source; + sourceTree = "<group>"; + }; + 5C2FFD0919FE6DC900CB8FA3 /* SSL */ = { + isa = PBXGroup; + children = ( + 5C2FFD1119FEA61B00CB8FA3 /* ssl_bundle.crt */, + 5C2FFD0B19FE6DC900CB8FA3 /* vkpc-local.ch1p.com.key */, + ); + path = SSL; + sourceTree = "<group>"; + }; + 5C2FFD1319FEC4F300CB8FA3 /* Views */ = { + isa = PBXGroup; + children = ( + 5CD13447184B8919003295B0 /* FlippedView.h */, + 5CD13448184B8919003295B0 /* FlippedView.m */, + 5C9D6D841A1BA5D100494738 /* NonVibrantButton.h */, + 5C9D6D851A1BA5D100494738 /* NonVibrantButton.m */, + 5C2FFD9B1A00048100CB8FA3 /* VibrantButton.h */, + 5C2FFD9C1A00048100CB8FA3 /* VibrantButton.m */, + 5C2FFD9519FFFC3500CB8FA3 /* VibrantTextField.h */, + 5C2FFD9619FFFC3500CB8FA3 /* VibrantTextField.m */, + 5C2FFD9E1A001EF700CB8FA3 /* VibrantImageView.h */, + 5C2FFD9F1A001EF700CB8FA3 /* VibrantImageView.m */, + 5CD13480184CFD5C003295B0 /* ShadowTextFieldCell.h */, + 5CD13481184CFD5C003295B0 /* ShadowTextFieldCell.m */, + ); + name = Views; + sourceTree = "<group>"; + }; + 5C2FFDA21A004D3500CB8FA3 /* Main */ = { + isa = PBXGroup; + children = ( + 5C0B60281A191CFD009595C5 /* MainMenu.xib */, + 5C0B60251A191C86009595C5 /* Application.h */, + 5C0B60261A191C86009595C5 /* Application.m */, + 5C4F7DDA1844E65700394A5A /* main.m */, + 5C441BDE18479493004175A0 /* Global.h */, + 5C67ADAB1848CB40005B541C /* Global.m */, + 5C5D418F19F6F7BE00DEE14A /* Types.h */, + 5C4F7DE01844E65700394A5A /* AppDelegate.h */, + 5C4F7DE11844E65700394A5A /* AppDelegate.m */, + 5C2EF5401A0FCE80005442E0 /* Autostart.h */, + 5C2EF5411A0FCE80005442E0 /* Autostart.m */, + 5C09B68C1A12B21600F970E8 /* Statistics.h */, + 5C09B68D1A12B21600F970E8 /* Statistics.m */, + ); + name = Main; + sourceTree = "<group>"; + }; + 5C4F7DC21844E65700394A5A = { + isa = PBXGroup; + children = ( + 5C4F7DD41844E65700394A5A /* VKPC */, + 5C4F7DF21844E65700394A5A /* VKPCTests */, + 5C4F7DCD1844E65700394A5A /* Frameworks */, + 5C4F7DCC1844E65700394A5A /* Products */, + ); + sourceTree = "<group>"; + }; + 5C4F7DCC1844E65700394A5A /* Products */ = { + isa = PBXGroup; + children = ( + 5C4F7DCB1844E65700394A5A /* VKPC.app */, + ); + name = Products; + sourceTree = "<group>"; + }; + 5C4F7DCD1844E65700394A5A /* Frameworks */ = { + isa = PBXGroup; + children = ( + 5C2F3A8B1A0FD58500C4ADB7 /* Sparkle.framework */, + 5C14D80D1A07EE67007E6D59 /* Quartz.framework */, + 5C14D80B1A07EE18007E6D59 /* QuartzCore.framework */, + 5C14D8091A07EDC3007E6D59 /* CoreGraphics.framework */, + 5CBCAC6A1A025C3400C8E803 /* LocalAuthentication.framework */, + 5CA5468119F94ADE0038F869 /* libwebsockets.dylib */, + 5C5D418B19F6D61000DEE14A /* AudioToolbox.framework */, + 5CC25BD11861AC4E00A69C93 /* IOKit.framework */, + 5C0E001A1847DACE00AA2D44 /* Security.framework */, + 5C4F7E161844FB2700394A5A /* Carbon.framework */, + 5C4F7DCE1844E65700394A5A /* Cocoa.framework */, + 5C4F7DD01844E65700394A5A /* Other Frameworks */, + ); + name = Frameworks; + sourceTree = "<group>"; + }; + 5C4F7DD01844E65700394A5A /* Other Frameworks */ = { + isa = PBXGroup; + children = ( + 5C4F7DD11844E65700394A5A /* AppKit.framework */, + 5C4F7DD21844E65700394A5A /* CoreData.framework */, + 5C4F7DD31844E65700394A5A /* Foundation.framework */, + ); + name = "Other Frameworks"; + sourceTree = "<group>"; + }; + 5C4F7DD41844E65700394A5A /* VKPC */ = { + isa = PBXGroup; + children = ( + 5C14D8041A07EC4F007E6D59 /* VKPC1.icns */, + 5C2FFD8F19FFCF7D00CB8FA3 /* ImagesLegacy.bundle */, + 5C2FFD9019FFCF7D00CB8FA3 /* ImagesYosemite.bundle */, + 5C2FFD9119FFCF7D00CB8FA3 /* ImagesYosemiteDark.bundle */, + 5C2FFD0919FE6DC900CB8FA3 /* SSL */, + 5C4F7E18184509CC00394A5A /* Scripts */, + 5C21778719F6A0DD0038495C /* Source */, + 5C4F7DE61844E65700394A5A /* Images.xcassets */, + 5C4F7DD51844E65700394A5A /* Supporting Files */, + ); + path = VKPC; + sourceTree = "<group>"; + }; + 5C4F7DD51844E65700394A5A /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 5C4F7DD61844E65700394A5A /* VKPC-Info.plist */, + 5C4F7DD71844E65700394A5A /* InfoPlist.strings */, + 5C4F7DDC1844E65700394A5A /* VKPC-Prefix.pch */, + ); + name = "Supporting Files"; + sourceTree = "<group>"; + }; + 5C4F7DF21844E65700394A5A /* VKPCTests */ = { + isa = PBXGroup; + children = ( + 5C4F7DF81844E65700394A5A /* VKPCTests.m */, + 5C4F7DF31844E65700394A5A /* Supporting Files */, + ); + path = VKPCTests; + sourceTree = "<group>"; + }; + 5C4F7DF31844E65700394A5A /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 5C4F7DF41844E65700394A5A /* VKPCTests-Info.plist */, + 5C4F7DF51844E65700394A5A /* InfoPlist.strings */, + ); + name = "Supporting Files"; + sourceTree = "<group>"; + }; + 5C4F7E0C1844F98900394A5A /* SPInvocationGrabbing */ = { + isa = PBXGroup; + children = ( + 5C4F7E101844F98900394A5A /* NSObject+SPInvocationGrabbing.h */, + 5C4F7E111844F98900394A5A /* NSObject+SPInvocationGrabbing.m */, + ); + path = SPInvocationGrabbing; + sourceTree = "<group>"; + }; + 5C4F7E18184509CC00394A5A /* Scripts */ = { + isa = PBXGroup; + children = ( + 5C67ADB01848E25F005B541C /* inject.js */, + 5C67ADB11848E25F005B541C /* inject.as */, + ); + name = Scripts; + sourceTree = "<group>"; + }; + 5C5F831218718A3A00E67F59 /* AppleRemote */ = { + isa = PBXGroup; + children = ( + 5C5F831318718A3A00E67F59 /* AppleRemote.h */, + 5C5F831418718A3A00E67F59 /* AppleRemote.m */, + 5C5F831518718A3A00E67F59 /* HIDRemoteControlDevice.h */, + 5C5F831618718A3A00E67F59 /* HIDRemoteControlDevice.m */, + 5C5F831718718A3A00E67F59 /* MultiClickRemoteBehavior.h */, + 5C5F831818718A3A00E67F59 /* MultiClickRemoteBehavior.m */, + 5C5F831918718A3A00E67F59 /* RemoteControl.h */, + 5C5F831A18718A3A00E67F59 /* RemoteControl.m */, + ); + path = AppleRemote; + sourceTree = "<group>"; + }; + 5CBCAC6D1A02776000C8E803 /* Popover */ = { + isa = PBXGroup; + children = ( + 5CF166AA184A194D00FB9495 /* PopoverView.xib */, + 5CF166A4184A164800FB9495 /* Popover.h */, + 5CF166A5184A164800FB9495 /* Popover.m */, + 5CF166A7184A16EC00FB9495 /* PopoverController.h */, + 5CF166A8184A16EC00FB9495 /* PopoverController.m */, + 5C2FFDA31A00FED500CB8FA3 /* PopoverImageView.h */, + 5C2FFDA41A00FED500CB8FA3 /* PopoverImageView.m */, + 5C14D8061A07ED84007E6D59 /* PopoverClipView.h */, + 5C14D8071A07ED84007E6D59 /* PopoverClipView.m */, + 5C14D8101A07EF31007E6D59 /* PopoverScrollView.h */, + 5C14D8111A07EF31007E6D59 /* PopoverScrollView.m */, + ); + name = Popover; + sourceTree = "<group>"; + }; + 5CBCAC6F1A0277AF00C8E803 /* About Window */ = { + isa = PBXGroup; + children = ( + 5CF166CA184A6B4D00FB9495 /* AboutWindow.xib */, + 5CF166CE184AA01A00FB9495 /* AboutWindowController.h */, + 5CF166CF184AA01A00FB9495 /* AboutWindowController.m */, + ); + name = "About Window"; + sourceTree = "<group>"; + }; + 5CBCAC701A02780D00C8E803 /* Catch Media Buttons */ = { + isa = PBXGroup; + children = ( + 5CF166D9184AABB300FB9495 /* SPMediaKeyTap */, + 5C5D419519F7BDAF00DEE14A /* CatchMediaButtons.h */, + 5C5D419619F7BDAF00DEE14A /* CatchMediaButtons.m */, + ); + name = "Catch Media Buttons"; + sourceTree = "<group>"; + }; + 5CBCAC711A02782900C8E803 /* Playlist */ = { + isa = PBXGroup; + children = ( + 5CEBA87E184FC3C800EEB81E /* Playlist.h */, + 5CEBA87F184FC3C800EEB81E /* Playlist.m */, + 5CD1345C184BAB48003295B0 /* PlaylistTableController.h */, + 5CD1345D184BAB48003295B0 /* PlaylistTableController.m */, + 5CD13472184BE8E2003295B0 /* PlaylistTableCellView.h */, + 5CD13473184BE8E2003295B0 /* PlaylistTableCellView.m */, + 5CD1347D184BF17E003295B0 /* PlaylistTableRowView.h */, + 5CD1347E184BF17E003295B0 /* PlaylistTableRowView.m */, + 5CD1346F184BC185003295B0 /* PlaylistTableView.h */, + 5CD13470184BC185003295B0 /* PlaylistTableView.m */, + 5CD13484184E4A59003295B0 /* Queue.h */, + 5CD13485184E4A59003295B0 /* Queue.m */, + 5CD13487184E4B95003295B0 /* QueueControllerProtocol.h */, + ); + name = Playlist; + sourceTree = "<group>"; + }; + 5CBCAC721A02786600C8E803 /* Player Controller */ = { + isa = PBXGroup; + children = ( + 5CA5463919F8F10C0038F869 /* Controller.h */, + 5CA5463A19F8F10C0038F869 /* Controller.m */, + 5C4FE5FE1849072A0023CB77 /* Server.h */, + 5C4FE5FF1849072A0023CB77 /* Server.m */, + ); + name = "Player Controller"; + sourceTree = "<group>"; + }; + 5CF166D9184AABB300FB9495 /* SPMediaKeyTap */ = { + isa = PBXGroup; + children = ( + 5C4F7E0C1844F98900394A5A /* SPInvocationGrabbing */, + 5C4F7E091844F8FF00394A5A /* SPMediaKeyTap.h */, + 5C4F7E0A1844F8FF00394A5A /* SPMediaKeyTap.m */, + ); + name = SPMediaKeyTap; + sourceTree = "<group>"; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 5C4F7DCA1844E65700394A5A /* VKPC */ = { + isa = PBXNativeTarget; + buildConfigurationList = 5C4F7DFC1844E65700394A5A /* Build configuration list for PBXNativeTarget "VKPC" */; + buildPhases = ( + 5C4F7DC71844E65700394A5A /* Sources */, + 5C4F7DC81844E65700394A5A /* Frameworks */, + 5C4F7DC91844E65700394A5A /* Resources */, + 5CEEDC641A07A0D400DC2114 /* ShellScript */, + 5C2F3A8D1A0FD5C300C4ADB7 /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = VKPC; + productName = VKPC; + productReference = 5C4F7DCB1844E65700394A5A /* VKPC.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 5C4F7DC31844E65700394A5A /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0600; + ORGANIZATIONNAME = "Eugene Z"; + }; + buildConfigurationList = 5C4F7DC61844E65700394A5A /* Build configuration list for PBXProject "VKPC" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 5C4F7DC21844E65700394A5A; + productRefGroup = 5C4F7DCC1844E65700394A5A /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 5C4F7DCA1844E65700394A5A /* VKPC */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 5C4F7DC91844E65700394A5A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 5C2FFD0D19FE6DC900CB8FA3 /* vkpc-local.ch1p.com.key in Resources */, + 5C2FFD9319FFCF7D00CB8FA3 /* ImagesYosemite.bundle in Resources */, + 5C2FFD9219FFCF7D00CB8FA3 /* ImagesLegacy.bundle in Resources */, + 5C4FE5FA1848F07E0023CB77 /* inject.js in Resources */, + 5C0B602A1A191CFD009595C5 /* MainMenu.xib in Resources */, + 5C4F7DD91844E65700394A5A /* InfoPlist.strings in Resources */, + 5CF166AB184A194D00FB9495 /* PopoverView.xib in Resources */, + 5C2FFD1219FEA61B00CB8FA3 /* ssl_bundle.crt in Resources */, + 5C2FFD9419FFCF7D00CB8FA3 /* ImagesYosemiteDark.bundle in Resources */, + 5C14D8051A07EC4F007E6D59 /* VKPC1.icns in Resources */, + 5CF166CB184A6B4D00FB9495 /* AboutWindow.xib in Resources */, + 5C4F7DE71844E65700394A5A /* Images.xcassets in Resources */, + 5C67ADB31848E25F005B541C /* inject.as in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 5CEEDC641A07A0D400DC2114 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 8; + files = ( + ); + inputPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 1; + shellPath = /bin/sh; + shellScript = "codesign --deep -f -s \"Self-signed Applications\" \"${BUILT_PRODUCTS_DIR}/${WRAPPER_NAME}/\"\ninstall_name_tool -id @loader_path/../Frameworks/libwebsockets.dylib libwebsockets.dylib"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 5C4F7DC71844E65700394A5A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 5CD1348C184E4C96003295B0 /* NSMutableArray+QueueAdditions.m in Sources */, + 5CF166D3184AA94800FB9495 /* WindowController.m in Sources */, + 5CF166D0184AA01A00FB9495 /* AboutWindowController.m in Sources */, + 5C67ADB21848E25F005B541C /* inject.js in Sources */, + 5CD13449184B8919003295B0 /* FlippedView.m in Sources */, + 5CF166A9184A16EC00FB9495 /* PopoverController.m in Sources */, + 5CD13486184E4A59003295B0 /* Queue.m in Sources */, + 5C4FE6001849072A0023CB77 /* Server.m in Sources */, + 5CA5463B19F8F10C0038F869 /* Controller.m in Sources */, + 5C5F831D18718A3A00E67F59 /* MultiClickRemoteBehavior.m in Sources */, + 5C5F831C18718A3A00E67F59 /* HIDRemoteControlDevice.m in Sources */, + 5C4F7DE21844E65700394A5A /* AppDelegate.m in Sources */, + 5CD1345E184BAB48003295B0 /* PlaylistTableController.m in Sources */, + 5CD13471184BC185003295B0 /* PlaylistTableView.m in Sources */, + 5C2FFD9D1A00048100CB8FA3 /* VibrantButton.m in Sources */, + 5CD1347F184BF17E003295B0 /* PlaylistTableRowView.m in Sources */, + 5CD13482184CFD5C003295B0 /* ShadowTextFieldCell.m in Sources */, + 5C2FFDA01A001EF700CB8FA3 /* VibrantImageView.m in Sources */, + 5C4F7DDB1844E65700394A5A /* main.m in Sources */, + 5C14D8121A07EF31007E6D59 /* PopoverScrollView.m in Sources */, + 5C5F831E18718A3A00E67F59 /* RemoteControl.m in Sources */, + 5C2FFDA51A00FED500CB8FA3 /* PopoverImageView.m in Sources */, + 5CEBA880184FC3C800EEB81E /* Playlist.m in Sources */, + 5C4F7E151844F98900394A5A /* NSObject+SPInvocationGrabbing.m in Sources */, + 5C14D8081A07ED84007E6D59 /* PopoverClipView.m in Sources */, + 5C09B68E1A12B21600F970E8 /* Statistics.m in Sources */, + 5C0B60271A191C86009595C5 /* Application.m in Sources */, + 5C9D6D861A1BA5D100494738 /* NonVibrantButton.m in Sources */, + 5C2FFD9719FFFC3500CB8FA3 /* VibrantTextField.m in Sources */, + 5C67ADAC1848CB40005B541C /* Global.m in Sources */, + 5CFD57041A08228700891DA7 /* NSTimer+Blocks.m in Sources */, + 5C5F831B18718A3A00E67F59 /* AppleRemote.m in Sources */, + 5CF166A6184A164800FB9495 /* Popover.m in Sources */, + 5C4F7E0B1844F8FF00394A5A /* SPMediaKeyTap.m in Sources */, + 5CD13474184BE8E2003295B0 /* PlaylistTableCellView.m in Sources */, + 5C5D419719F7BDAF00DEE14A /* CatchMediaButtons.m in Sources */, + 5C2EF5421A0FCE80005442E0 /* Autostart.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 5C0B60281A191CFD009595C5 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 5C0B60291A191CFD009595C5 /* Base */, + ); + name = MainMenu.xib; + sourceTree = "<group>"; + }; + 5C4F7DD71844E65700394A5A /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + 5C4F7DD81844E65700394A5A /* en */, + ); + name = InfoPlist.strings; + sourceTree = "<group>"; + }; + 5C4F7DF51844E65700394A5A /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + 5C4F7DF61844E65700394A5A /* en */, + ); + name = InfoPlist.strings; + sourceTree = "<group>"; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 5C4F7DFA1844E65700394A5A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_ENABLE_OBJC_EXCEPTIONS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.8; + ONLY_ACTIVE_ARCH = YES; + OTHER_CFLAGS = ""; + SDKROOT = macosx; + }; + name = Debug; + }; + 5C4F7DFB1844E65700394A5A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_ENABLE_OBJC_EXCEPTIONS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.8; + ONLY_ACTIVE_ARCH = NO; + OTHER_CFLAGS = ""; + SDKROOT = macosx; + }; + name = Release; + }; + 5C4F7DFD1844E65700394A5A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LIBRARY = "compiler-default"; + CLANG_ENABLE_OBJC_ARC = YES; + CODE_SIGN_IDENTITY = "Self-signed Applications"; + COMBINE_HIDPI_IMAGES = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(DEVELOPER_DIR)/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.10.sdk/System/Library/Frameworks", + "$(PROJECT_DIR)", + ); + GCC_LINK_WITH_DYNAMIC_LIBRARIES = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "VKPC/VKPC-Prefix.pch"; + GCC_PREPROCESSOR_DEFINITIONS = ( + "_DEBUG=1", + "DEBUG=1", + "$(inherited)", + ); + "GCC_PREPROCESSOR_DEFINITIONS[arch=*]" = ( + "DEBUG=1", + "_DEBUG=1", + "$(inherited)", + ); + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + /usr/local/include, + /Users/evgeny/Development/Mac/libs/CocoaHTTPServer/Core, + ); + INFOPLIST_FILE = "VKPC/VKPC-Info.plist"; + LD_DYLIB_INSTALL_NAME = ""; + LD_RUNPATH_SEARCH_PATHS = ""; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(DEVELOPER_DIR)/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.10.sdk/usr/lib", + /Users/evgeny/dev/Mac, + "$(PROJECT_DIR)", + ); + MACOSX_DEPLOYMENT_TARGET = 10.7; + OTHER_LDFLAGS = "-Wl,-rpath,@loader_path/../Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + USER_HEADER_SEARCH_PATHS = ""; + WRAPPER_EXTENSION = app; + }; + name = Debug; + }; + 5C4F7DFE1844E65700394A5A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LIBRARY = "compiler-default"; + CLANG_ENABLE_OBJC_ARC = YES; + CODE_SIGN_IDENTITY = "Self-signed Applications"; + COMBINE_HIDPI_IMAGES = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(DEVELOPER_DIR)/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.10.sdk/System/Library/Frameworks", + "$(PROJECT_DIR)", + ); + GCC_LINK_WITH_DYNAMIC_LIBRARIES = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "VKPC/VKPC-Prefix.pch"; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + /usr/local/include, + /Users/evgeny/Development/Mac/libs/CocoaHTTPServer/Core, + ); + INFOPLIST_FILE = "VKPC/VKPC-Info.plist"; + LD_DYLIB_INSTALL_NAME = ""; + LD_RUNPATH_SEARCH_PATHS = ""; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(DEVELOPER_DIR)/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.10.sdk/usr/lib", + /Users/evgeny/dev/Mac, + "$(PROJECT_DIR)", + ); + MACOSX_DEPLOYMENT_TARGET = 10.7; + OTHER_LDFLAGS = "-Wl,-rpath,@loader_path/../Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + USER_HEADER_SEARCH_PATHS = ""; + WRAPPER_EXTENSION = app; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 5C4F7DC61844E65700394A5A /* Build configuration list for PBXProject "VKPC" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 5C4F7DFA1844E65700394A5A /* Debug */, + 5C4F7DFB1844E65700394A5A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 5C4F7DFC1844E65700394A5A /* Build configuration list for PBXNativeTarget "VKPC" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 5C4F7DFD1844E65700394A5A /* Debug */, + 5C4F7DFE1844E65700394A5A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 5C4F7DC31844E65700394A5A /* Project object */; +} diff --git a/VKPC.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/VKPC.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..83d8df8 --- /dev/null +++ b/VKPC.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Workspace + version = "1.0"> + <FileRef + location = "self:VKPC.xcodeproj"> + </FileRef> +</Workspace> diff --git a/VKPC.xcodeproj/project.xcworkspace/xcuserdata/evgeny.xcuserdatad/UserInterfaceState (Evgenys-MacBook-Pro's conflicted copy 2013-12-04).xcuserstate b/VKPC.xcodeproj/project.xcworkspace/xcuserdata/evgeny.xcuserdatad/UserInterfaceState (Evgenys-MacBook-Pro's conflicted copy 2013-12-04).xcuserstate Binary files differnew file mode 100644 index 0000000..635bd1f --- /dev/null +++ b/VKPC.xcodeproj/project.xcworkspace/xcuserdata/evgeny.xcuserdatad/UserInterfaceState (Evgenys-MacBook-Pro's conflicted copy 2013-12-04).xcuserstate diff --git a/VKPC.xcodeproj/project.xcworkspace/xcuserdata/evgeny.xcuserdatad/UserInterfaceState.xcuserstate b/VKPC.xcodeproj/project.xcworkspace/xcuserdata/evgeny.xcuserdatad/UserInterfaceState.xcuserstate Binary files differnew file mode 100644 index 0000000..61f66fa --- /dev/null +++ b/VKPC.xcodeproj/project.xcworkspace/xcuserdata/evgeny.xcuserdatad/UserInterfaceState.xcuserstate diff --git a/VKPC.xcodeproj/project.xcworkspace/xcuserdata/evgeny.xcuserdatad/WorkspaceSettings.xcsettings b/VKPC.xcodeproj/project.xcworkspace/xcuserdata/evgeny.xcuserdatad/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..28f6741 --- /dev/null +++ b/VKPC.xcodeproj/project.xcworkspace/xcuserdata/evgeny.xcuserdatad/WorkspaceSettings.xcsettings @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>BuildLocationStyle</key> + <string>UseAppPreferences</string> + <key>CustomBuildLocationType</key> + <string>RelativeToDerivedData</string> + <key>DerivedDataLocationStyle</key> + <string>Default</string> + <key>HasAskedToTakeAutomaticSnapshotBeforeSignificantChanges</key> + <true/> + <key>IssueFilterStyle</key> + <string>ShowActiveSchemeOnly</string> + <key>LiveSourceIssuesEnabled</key> + <true/> + <key>SnapshotAutomaticallyBeforeSignificantChanges</key> + <true/> + <key>SnapshotLocationStyle</key> + <string>Default</string> +</dict> +</plist> diff --git a/VKPC.xcodeproj/xcshareddata/xcdebugger/Breakpoints_v2.xcbkptlist b/VKPC.xcodeproj/xcshareddata/xcdebugger/Breakpoints_v2.xcbkptlist new file mode 100644 index 0000000..ab8eb6e --- /dev/null +++ b/VKPC.xcodeproj/xcshareddata/xcdebugger/Breakpoints_v2.xcbkptlist @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Bucket + type = "4" + version = "2.0"> +</Bucket> diff --git a/VKPC.xcodeproj/xcuserdata/evgeny.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/VKPC.xcodeproj/xcuserdata/evgeny.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist new file mode 100644 index 0000000..6cc790b --- /dev/null +++ b/VKPC.xcodeproj/xcuserdata/evgeny.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -0,0 +1,65 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Bucket + type = "1" + version = "2.0"> + <Breakpoints> + <BreakpointProxy + BreakpointExtensionID = "Xcode.Breakpoint.SymbolicBreakpoint"> + <BreakpointContent + shouldBeEnabled = "Yes" + ignoreCount = "0" + continueAfterRunningActions = "No" + symbolName = "malloc_error_break" + moduleName = ""> + </BreakpointContent> + </BreakpointProxy> + <BreakpointProxy + BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint"> + <BreakpointContent + shouldBeEnabled = "No" + ignoreCount = "0" + continueAfterRunningActions = "No" + filePath = "VKPC/Statistics.m" + timestampString = "437673144.95233" + startingColumnNumber = "9223372036854775807" + endingColumnNumber = "9223372036854775807" + startingLineNumber = "134" + endingLineNumber = "134" + landmarkName = "getOSXVersion()" + landmarkType = "7"> + </BreakpointContent> + </BreakpointProxy> + <BreakpointProxy + BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint"> + <BreakpointContent + shouldBeEnabled = "No" + ignoreCount = "0" + continueAfterRunningActions = "No" + filePath = "VKPC/PlaylistTableController.m" + timestampString = "437590603.092032" + startingColumnNumber = "9223372036854775807" + endingColumnNumber = "9223372036854775807" + startingLineNumber = "37" + endingLineNumber = "37" + landmarkName = "-init" + landmarkType = "5"> + </BreakpointContent> + </BreakpointProxy> + <BreakpointProxy + BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint"> + <BreakpointContent + shouldBeEnabled = "No" + ignoreCount = "0" + continueAfterRunningActions = "No" + filePath = "VKPC/PopoverController.m" + timestampString = "446058171.654243" + startingColumnNumber = "9223372036854775807" + endingColumnNumber = "9223372036854775807" + startingLineNumber = "347" + endingLineNumber = "347" + landmarkName = "-menuItemBrowserAction:" + landmarkType = "5"> + </BreakpointContent> + </BreakpointProxy> + </Breakpoints> +</Bucket> diff --git a/VKPC.xcodeproj/xcuserdata/evgeny.xcuserdatad/xcschemes/VKPC.xcscheme b/VKPC.xcodeproj/xcuserdata/evgeny.xcuserdatad/xcschemes/VKPC.xcscheme new file mode 100644 index 0000000..6d665d4 --- /dev/null +++ b/VKPC.xcodeproj/xcuserdata/evgeny.xcuserdatad/xcschemes/VKPC.xcscheme @@ -0,0 +1,86 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Scheme + LastUpgradeVersion = "0600" + version = "1.3"> + <BuildAction + parallelizeBuildables = "YES" + buildImplicitDependencies = "YES"> + <BuildActionEntries> + <BuildActionEntry + buildForTesting = "YES" + buildForRunning = "YES" + buildForProfiling = "YES" + buildForArchiving = "YES" + buildForAnalyzing = "YES"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "5C4F7DCA1844E65700394A5A" + BuildableName = "VKPC.app" + BlueprintName = "VKPC" + ReferencedContainer = "container:VKPC.xcodeproj"> + </BuildableReference> + </BuildActionEntry> + </BuildActionEntries> + </BuildAction> + <TestAction + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + shouldUseLaunchSchemeArgsEnv = "YES" + buildConfiguration = "Debug"> + <Testables> + </Testables> + <MacroExpansion> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "5C4F7DCA1844E65700394A5A" + BuildableName = "VKPC.app" + BlueprintName = "VKPC" + ReferencedContainer = "container:VKPC.xcodeproj"> + </BuildableReference> + </MacroExpansion> + </TestAction> + <LaunchAction + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + launchStyle = "0" + useCustomWorkingDirectory = "NO" + buildConfiguration = "Release" + ignoresPersistentStateOnLaunch = "NO" + debugDocumentVersioning = "YES" + allowLocationSimulation = "YES"> + <BuildableProductRunnable> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "5C4F7DCA1844E65700394A5A" + BuildableName = "VKPC.app" + BlueprintName = "VKPC" + ReferencedContainer = "container:VKPC.xcodeproj"> + </BuildableReference> + </BuildableProductRunnable> + <AdditionalOptions> + </AdditionalOptions> + </LaunchAction> + <ProfileAction + shouldUseLaunchSchemeArgsEnv = "YES" + savedToolIdentifier = "" + useCustomWorkingDirectory = "NO" + buildConfiguration = "Release" + debugDocumentVersioning = "YES"> + <BuildableProductRunnable> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "5C4F7DCA1844E65700394A5A" + BuildableName = "VKPC.app" + BlueprintName = "VKPC" + ReferencedContainer = "container:VKPC.xcodeproj"> + </BuildableReference> + </BuildableProductRunnable> + </ProfileAction> + <AnalyzeAction + buildConfiguration = "Debug"> + </AnalyzeAction> + <ArchiveAction + buildConfiguration = "Release" + revealArchiveInOrganizer = "YES"> + </ArchiveAction> +</Scheme> diff --git a/VKPC.xcodeproj/xcuserdata/evgeny.xcuserdatad/xcschemes/xcschememanagement.plist b/VKPC.xcodeproj/xcuserdata/evgeny.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..222a9cf --- /dev/null +++ b/VKPC.xcodeproj/xcuserdata/evgeny.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>SchemeUserState</key> + <dict> + <key>VKPC.xcscheme</key> + <dict> + <key>orderHint</key> + <integer>0</integer> + </dict> + </dict> + <key>SuppressBuildableAutocreation</key> + <dict> + <key>5C4F7DCA1844E65700394A5A</key> + <dict> + <key>primary</key> + <true/> + </dict> + <key>5C4F7DEB1844E65700394A5A</key> + <dict> + <key>primary</key> + <true/> + </dict> + </dict> +</dict> +</plist> diff --git a/VKPC/AXStatusItemPopup/Popover.h b/VKPC/AXStatusItemPopup/Popover.h new file mode 100644 index 0000000..a3366fb --- /dev/null +++ b/VKPC/AXStatusItemPopup/Popover.h @@ -0,0 +1,30 @@ +// +// Created by Alexander Schuch on 06/03/13. +// Modified by Eugene Zinoviev on 12/03/13. +// Copyright (c) 2013 Alexander Schuch. All rights reserved. +// Copyright (c) 2013-2014 Eugene Zinoviev. All rights reserved. +// + +#import <Cocoa/Cocoa.h> +//#import "PopupControllerProtocol.h" +#import "PopoverController.h" + +@interface Popover : NSView + +@property(assign, nonatomic, getter=isActive) BOOL active; +@property(strong, nonatomic) NSImage *defaultImage; +@property(strong, nonatomic) NSImage *altImage; +@property(strong, nonatomic) NSImage *flatImage; +@property(strong, nonatomic) NSStatusItem *statusItem; +@property(strong) NSPopover *popover; + ++ (id)shared; +- (id)init; + +- (void)showPopover; +- (void)hidePopover; + +- (NSSize)getSize; +- (void)setSize:(NSSize)size animate:(BOOL)animate; + +@end diff --git a/VKPC/AXStatusItemPopup/Popover.m b/VKPC/AXStatusItemPopup/Popover.m new file mode 100644 index 0000000..3c62275 --- /dev/null +++ b/VKPC/AXStatusItemPopup/Popover.m @@ -0,0 +1,163 @@ +// +// Created by Alexander Schuch on 06/03/13. +// Modified by Eugene Zinoviev on 12/03/13. +// Copyright (c) 2013 Alexander Schuch. All rights reserved. +// Copyright (c) 2013-2014 Eugene Zinoviev. All rights reserved. +// + +#import "Popover.h" +#import "PopoverImageView.h" + +static const int kMinViewWidth = 28; + +@implementation Popover { + PopoverImageView *imageView; + id popoverTransiencyMonitor; + BOOL popoverTransiencyMonitorEnabled; +// BOOL firstDrawed; +// id eventMonitor; +// BOOL escMonitorSet; +} + ++ (id)shared { + static Popover *shared = nil; + @synchronized (self) { + if (shared == nil){ + shared = [[self alloc] init]; + } + } + return shared; +} + +- (id)init { + popoverTransiencyMonitorEnabled = NO; +// firstDrawed = NO; + CGFloat height = [NSStatusBar systemStatusBar].thickness; + _active = NO; +// escMonitorSet = NO; + + if (self = [super initWithFrame:NSMakeRect(0, 0, kMinViewWidth, height)]) { + imageView = [[PopoverImageView alloc] initWithFrame:NSMakeRect(0, 0, kMinViewWidth, height)]; + [self addSubview:imageView]; + + _statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSVariableStatusItemLength]; + _statusItem.view = self; + +// dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ +// [self showPopover]; +// }); + } + return self; +} + +- (BOOL)allowsVibrancy { + return YES; +} + +- (void)drawRect:(NSRect)dirtyRect { +// if (!firstDrawed) { +// firstDrawed = YES; +// [self showPopover]; +// } + + if (_active) { + [[NSColor selectedMenuItemColor] setFill]; + } else { + [[NSColor clearColor] setFill]; + } + NSRectFill(dirtyRect); + + NSDictionary *images = VKPCGetImagesDictionary(); + NSImage *imgDefault = images[VKPCImageStatus], *imgActive = images[VKPCImageStatusPressed]; + imageView.image = _active ? imgActive : imgDefault; +} + +- (void)mouseDown:(NSEvent *)theEvent { + if (_popover.isShown) { + [self hidePopover]; + } else { + [self showPopover]; + } +} + +- (void)setActive:(BOOL)active { + _active = active; + [self setNeedsDisplay:YES]; +} + +- (void)updateViewFrame { + CGFloat width = MAX(MAX(kMinViewWidth, _altImage.size.width), _defaultImage.size.width); + CGFloat height = [NSStatusBar systemStatusBar].thickness; + + NSRect frame = NSMakeRect(0, 0, width, height); + self.frame = frame; + imageView.frame = frame; + + [self setNeedsDisplay:YES]; +} + +- (void)showPopover { + self.active = YES; + + if (!_popover) { + _popover = [[NSPopover alloc] init]; + _popover.contentViewController = [PopoverController shared]; + +// NSEvent *(^handler)(NSEvent *) = ^NSEvent *(NSEvent *theEvent) { +// if (theEvent.keyCode == 53) { +// NSLog(@"[Popover] catch ESC"); +// return nil; +// } +// +// return theEvent; +// }; +// +// eventMonitor = [NSEvent addLocalMonitorForEventsMatchingMask:NSKeyDownMask handler:handler]; + + }; + + if (!_popover.isShown) { + _popover.animates = NO; + [_popover showRelativeToRect:self.frame ofView:self preferredEdge:NSMinYEdge]; + if (!popoverTransiencyMonitorEnabled) { + popoverTransiencyMonitor = [NSEvent addGlobalMonitorForEventsMatchingMask:NSLeftMouseDownMask|NSRightMouseDownMask handler:^(NSEvent* event) { + [self hidePopover]; + }]; + popoverTransiencyMonitorEnabled = YES; + } + } + + [[PopoverController shared] popoverDidShow]; +} + +- (void)hidePopover { + self.active = NO; + + if (_popover && _popover.isShown) { + [_popover close]; + [[PopoverController shared] popoverDidHide]; + if (popoverTransiencyMonitorEnabled) { + [NSEvent removeMonitor:popoverTransiencyMonitor]; + popoverTransiencyMonitorEnabled = NO; + } + } +} + +- (NSSize)getSize { + return [_popover contentSize]; +} + +- (void)setSize:(NSSize)size animate:(BOOL)animate { + BOOL bkAnimates = _popover.animates; + _popover.animates = animate; + [_popover setContentSize:size]; + _popover.animates = bkAnimates; +} + +@end + + + + + + diff --git a/VKPC/AboutWindow.xib b/VKPC/AboutWindow.xib new file mode 100644 index 0000000..1dacf46 --- /dev/null +++ b/VKPC/AboutWindow.xib @@ -0,0 +1,74 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="6254" systemVersion="14D72i" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES"> + <dependencies> + <deployment identifier="macosx"/> + <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="6254"/> + </dependencies> + <objects> + <customObject id="-2" userLabel="File's Owner" customClass="AboutWindowController"> + <connections> + <outlet property="ch1pTextField" destination="RKx-9V-CuI" id="wrk-64-JVE"/> + <outlet property="copyrightTextField" destination="ERc-8w-PEv" id="Oqv-42-sHP"/> + <outlet property="ezTextField" destination="ptd-Ue-h4k" id="Xqt-aa-z4S"/> + <outlet property="titleTextField" destination="9iH-bO-wdX" id="6Tm-zx-l1p"/> + <outlet property="window" destination="G5N-nr-30p" id="wNb-Cf-WGv"/> + </connections> + </customObject> + <customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/> + <customObject id="-3" userLabel="Application" customClass="NSObject"/> + <window title="About" allowsToolTipsWhenApplicationIsInactive="NO" oneShot="NO" releasedWhenClosed="NO" showsToolbarButton="NO" visibleAtLaunch="NO" animationBehavior="default" id="G5N-nr-30p"> + <windowStyleMask key="styleMask" titled="YES" closable="YES"/> + <rect key="contentRect" x="131" y="159" width="336" height="152"/> + <rect key="screenRect" x="0.0" y="0.0" width="1440" height="877"/> + <view key="contentView" id="NwM-Tg-Ru9"> + <rect key="frame" x="0.0" y="0.0" width="336" height="152"/> + <autoresizingMask key="autoresizingMask"/> + <subviews> + <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="ERc-8w-PEv"> + <rect key="frame" x="31" y="52" width="274" height="22"/> + <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" allowsUndo="NO" sendsActionOnEndEditing="YES" alignment="center" title="Eugene Z © 2013-2015" id="uv2-0i-619"> + <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> + <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="ptd-Ue-h4k"> + <rect key="frame" x="31" y="33" width="274" height="23"/> + <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" allowsUndo="NO" sendsActionOnEndEditing="YES" alignment="center" title="vk.com/ez" id="yTi-Tb-TPm"> + <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> + <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="9iH-bO-wdX"> + <rect key="frame" x="14" y="121" width="309" height="17"/> + <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="center" title="Title" id="sen-Gb-PyL"> + <font key="font" metaFont="systemBold"/> + <color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/> + </textFieldCell> + </textField> + <textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" setsMaxLayoutWidthAtFirstLayout="YES" translatesAutoresizingMaskIntoConstraints="NO" id="1Eu-bN-jqb"> + <rect key="frame" x="-9" y="84" width="354" height="34"/> + <textFieldCell key="cell" sendsActionOnEndEditing="YES" alignment="center" id="Ar7-Dg-5Ft"> + <font key="font" metaFont="system"/> + <string key="title">Use media buttons (F7-F9) to switch +between tracks</string> + <color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/> + </textFieldCell> + </textField> + <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="RKx-9V-CuI"> + <rect key="frame" x="31" y="14" width="274" height="23"/> + <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" allowsUndo="NO" sendsActionOnEndEditing="YES" alignment="center" title="ch1p.com/vkpc/" id="lw6-hX-ors"> + <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> + </view> + <point key="canvasLocation" x="280" y="254"/> + </window> + </objects> +</document> diff --git a/VKPC/AboutWindowController.h b/VKPC/AboutWindowController.h new file mode 100644 index 0000000..0335065 --- /dev/null +++ b/VKPC/AboutWindowController.h @@ -0,0 +1,22 @@ +// +// AboutWindowController.h +// VKPC +// +// Created by Eugene on 12/1/13. +// Copyright (c) 2013 Eugene Z. All rights reserved. +// + +#import <Cocoa/Cocoa.h> +#import "WindowController.h" + +@interface AboutWindowController : WindowController<NSWindowDelegate> + +@property (strong) IBOutlet NSWindow *window; +@property (weak) IBOutlet NSTextField *titleTextField; +@property (weak) IBOutlet NSTextField *copyrightTextField; +@property (weak) IBOutlet NSTextField *ezTextField; +@property (weak) IBOutlet NSTextField *ch1pTextField; + +//- (IBAction)sendEmailAction:(id)sender; + +@end diff --git a/VKPC/AboutWindowController.m b/VKPC/AboutWindowController.m new file mode 100644 index 0000000..e7de619 --- /dev/null +++ b/VKPC/AboutWindowController.m @@ -0,0 +1,90 @@ +// +// AboutWindowController.m +// VKPC +// +// Created by Eugene on 12/1/13. +// Copyright (c) 2013 Eugene Z. All rights reserved. +// + +#import "AboutWindowController.h" + +static NSString * const ezURL = @"<a href=\"http://vk.com/ez\">vk.com/ez</a>"; +static NSString * const ch1pURL = @"<a href=\"http://ch1p.com/vkpc/\">ch1p.com/vkpc/</a>"; + +@implementation AboutWindowController + +- (BOOL)allowsClosingWithShortcut { + return YES; +} + +static void setStyleForAttributedString(NSMutableAttributedString *string) { + NSRange range = NSMakeRange(0, string.length); + + NSFont *font = [NSFont fontWithName:GetSystemFontName() size:13.0]; + + NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init]; + [paragraphStyle setAlignment:NSCenterTextAlignment]; + [paragraphStyle setLineSpacing:3]; + + [string addAttributes:[NSDictionary dictionaryWithObject:font forKey:NSFontAttributeName] range:range]; + [string addAttributes:[NSDictionary dictionaryWithObject:paragraphStyle forKey:NSParagraphStyleAttributeName] range:range]; +} + +- (void)windowDidLoad { + [super windowDidLoad]; + + // Title + NSString *version = [[[NSBundle mainBundle] infoDictionary] objectForKey:kCFBundleShortVersionString]; + if (VKPCIsDebug) + version = [NSString stringWithFormat:@"%@ %@", version, @"dev"]; + + NSString *title = [NSString stringWithFormat:@"%@ %@ (build %@)", + [[[NSBundle mainBundle] infoDictionary] objectForKey:kCFBundleDisplayName], + version, + [[[NSBundle mainBundle] infoDictionary] objectForKey:kCFBundleVersion]]; + [_titleTextField setStringValue:title]; + + NSDictionary *stringOptions = @{NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType, + NSCharacterEncodingDocumentAttribute: @(NSUTF8StringEncoding)}; + + // Copyright +// NSMutableAttributedString *copyright = [[NSMutableAttributedString alloc] initWithHTML:[copyrightHTML dataUsingEncoding:NSUTF8StringEncoding] +// options:stringOptions +// documentAttributes:nil]; +// setStyleForAttributedString(copyright); + + + // EZ Link + NSMutableAttributedString *ez = [[NSMutableAttributedString alloc] initWithHTML:[ezURL dataUsingEncoding:NSUTF8StringEncoding] + options:stringOptions + documentAttributes:nil]; + setStyleForAttributedString(ez); + [_ezTextField setAllowsEditingTextAttributes:YES]; + [_ezTextField setSelectable:YES]; + [_ezTextField setAttributedStringValue:ez]; + + // CH1P Link + NSMutableAttributedString *ch1p = [[NSMutableAttributedString alloc] initWithHTML:[ch1pURL dataUsingEncoding:NSUTF8StringEncoding] + options:stringOptions + documentAttributes:nil]; + setStyleForAttributedString(ch1p); + [_ch1pTextField setAllowsEditingTextAttributes:YES]; + [_ch1pTextField setSelectable:YES]; + [_ch1pTextField setAttributedStringValue:ch1p]; + +// [_copyrightTextField setAllowsEditingTextAttributes:YES]; +// [_copyrightTextField setSelectable:YES]; +// [_copyrightTextFie/ld setAttributedStringValue:copyright]; +} + +//- (IBAction)sendEmailAction:(id)sender { +// NSString *encodedSubject = [NSString stringWithFormat:@"SUBJECT=%@", [@"VK Player Controller" stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]; +// NSString *encodedTo = [CH1PEmail stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; +// +// NSString *encodedURLString = [NSString stringWithFormat:@"mailto:%@?%@&%@", encodedTo, encodedSubject, @""]; +// NSURL *mailtoURL = [NSURL URLWithString:encodedURLString]; +// +// [[NSWorkspace sharedWorkspace] openURL:mailtoURL]; +//} + +@end diff --git a/VKPC/AppDelegate.h b/VKPC/AppDelegate.h new file mode 100644 index 0000000..8caab4c --- /dev/null +++ b/VKPC/AppDelegate.h @@ -0,0 +1,14 @@ +// +// AppDelegate.h +// VKPC +// +// Created by Eugene on 11/26/13. +// Copyright (c) 2013-2014 Eugene Z. All rights reserved. +// + +@interface AppDelegate : NSObject + ++ (AppDelegate *)shared; +- (void)continueRunning; + +@end diff --git a/VKPC/AppDelegate.m b/VKPC/AppDelegate.m new file mode 100644 index 0000000..1ec88ff --- /dev/null +++ b/VKPC/AppDelegate.m @@ -0,0 +1,76 @@ +// +// AppDelegate.m +// VKPC +// +// Created by Eugene on 11/26/13. +// Copyright (c) 2013-2014 Eugene Z. All rights reserved. +// + +#import "AppDelegate.h" +#import "Server.h" +#import "CatchMediaButtons.h" +#import "Controller.h" +//#import "HostsHack.h" +#import "Statistics.h" + +#import "PopoverController.h" +#import "Popover.h" + +static AppDelegate *shared; + +@implementation AppDelegate + ++ (AppDelegate *)shared { + return shared; +} + +- (void)awakeFromNib { +} + +- (void)applicationDidFinishLaunching:(NSNotification *)aNotification { + shared = self; + + if (IsAnotherProcessRunning()) { + NSAlert *alert = [[NSAlert alloc] init]; + [alert setMessageText:@"Error"]; + [alert setInformativeText:@"Another VKPC process is already running."]; + [alert setAlertStyle:NSWarningAlertStyle]; + [alert runModal]; + + [[NSApplication sharedApplication] terminate:nil]; + return; + } + + // Init UI stuff + [Popover shared]; + [PopoverController shared]; + + // Preferences + [[NSUserDefaults standardUserDefaults] registerDefaults:@{ + VKPCPreferencesShowNotifications: [NSNumber numberWithBool:YES], + VKPCPreferencesInvertPlaylistIcons: [NSNumber numberWithBool:YES], + VKPCPreferencesCatchMediaButtons: [NSNumber numberWithBool:YES], + VKPCPreferencesBrowser: [NSNumber numberWithInt:0], + VKPCPreferencesStatisticReportedTimestamp: [NSNumber numberWithInt:0], + VKPCPreferencesUUID: @"", + VKPCPreferencesUseExtensionMode: [NSNumber numberWithBool:NO], + }]; + + VKPCInitUUID(); + + // Start catching (or not catching) media buttons + [CatchMediaButtons initialize]; + + // Usage reporting + [Statistics initialize]; + + [[PopoverController shared] setState:PopoverStatePlaylistNotLoaded]; + + // Start server in a background thread + [Server start]; + + // Controller + [Controller initialize]; +} + +@end diff --git a/VKPC/AppleRemote/AppleRemote.h b/VKPC/AppleRemote/AppleRemote.h new file mode 100644 index 0000000..1b9fc26 --- /dev/null +++ b/VKPC/AppleRemote/AppleRemote.h @@ -0,0 +1,42 @@ +/***************************************************************************** + * RemoteControlWrapper.h + * RemoteControlWrapper + * + * Created by Martin Kahr on 11.03.06 under a MIT-style license. + * Copyright (c) 2006 martinkahr.com. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED ‚ÄúAS IS‚Äù, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + *****************************************************************************/ + +#import <Cocoa/Cocoa.h> +#import "HIDRemoteControlDevice.h" + +/* Interacts with the Apple Remote Control HID device + The class is not thread safe +*/ +@interface AppleRemote : HIDRemoteControlDevice { + BOOL lastSecureEventInputState; + io_object_t eventSecureInputNotification; + IONotificationPortRef notifyPort; +} + +- (BOOL) retrieveSecureEventInputState; + +@end diff --git a/VKPC/AppleRemote/AppleRemote.m b/VKPC/AppleRemote/AppleRemote.m new file mode 100644 index 0000000..2338b0c --- /dev/null +++ b/VKPC/AppleRemote/AppleRemote.m @@ -0,0 +1,319 @@ +/***************************************************************************** + * RemoteControlWrapper.m + * RemoteControlWrapper + * + * Created by Martin Kahr on 11.03.06 under a MIT-style license. + * Copyright (c) 2006 martinkahr.com. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + *****************************************************************************/ + +#import "AppleRemote.h" + +#import <IOKit/IOKitLib.h> +#import <IOKit/IOCFPlugIn.h> +#import <IOKit/hid/IOHIDKeys.h> +#import <IOKit/IOKitLib.h> + +static void IOREInterestCallback( + void * refcon, + io_service_t service, + uint32_t messageType, + void * messageArgument ); + + +#ifndef NSAppKitVersionNumber10_4 + #define NSAppKitVersionNumber10_4 824 +#endif + +#ifndef NSAppKitVersionNumber10_5 + #define NSAppKitVersionNumber10_5 949 +#endif + +const char* AppleRemoteDeviceName = "AppleIRController"; + +@implementation AppleRemote + ++ (const char*) remoteControlDeviceName { + return AppleRemoteDeviceName; +} + +- (id) initWithDelegate: (id) _remoteControlDelegate { + if ((self = [super initWithDelegate: _remoteControlDelegate])) { + // A security update in february of 2007 introduced an odd behavior. + // Whenever SecureEventInput is activated or deactivated the exclusive access + // to the apple remote control device is lost. This leads to very strange behavior where + // a press on the Menu button activates FrontRow while your app still gets the event. + // A great number of people have complained about this. + // + // Finally I found a way to get the state of the SecureEventInput + // With that information I regain access to the device each time when the SecureEventInput state + // is changing. + io_registry_entry_t root = IORegistryGetRootEntry( kIOMasterPortDefault ); + if (root != MACH_PORT_NULL) { + notifyPort = IONotificationPortCreate( kIOMasterPortDefault ); + if (notifyPort) { + CFRunLoopSourceRef runLoopSource = IONotificationPortGetRunLoopSource(notifyPort); + CFRunLoopRef gRunLoop = CFRunLoopGetCurrent(); + CFRunLoopAddSource(gRunLoop, runLoopSource, kCFRunLoopDefaultMode); + + io_registry_entry_t entry = IORegistryEntryFromPath( kIOMasterPortDefault, kIOServicePlane ":/"); + if (entry != MACH_PORT_NULL) { + kern_return_t kr; + kr = IOServiceAddInterestNotification(notifyPort, + entry, + kIOBusyInterest, + &IOREInterestCallback, (__bridge void *)self, &eventSecureInputNotification ); + if (kr != KERN_SUCCESS) { + NSLog(@"Error when installing EventSecureInput Notification"); + IONotificationPortDestroy(notifyPort); + notifyPort = NULL; + } + IOObjectRelease(entry); + } + } + IOObjectRelease(root); + } + + lastSecureEventInputState = [self retrieveSecureEventInputState]; + } + return self; +} + +- (void)dealloc +{ + if (notifyPort) { + IONotificationPortDestroy(notifyPort); + notifyPort = NULL; + } + IOObjectRelease (eventSecureInputNotification); + eventSecureInputNotification = MACH_PORT_NULL; + +// [super dealloc]; +} + +- (void)finalize +{ + IONotificationPortDestroy(notifyPort); + notifyPort = NULL; + // Although IOObjectRelease is not documented as thread safe, I was assured at WWDC09 that it is. + IOObjectRelease (eventSecureInputNotification); + eventSecureInputNotification = MACH_PORT_NULL; + + [super finalize]; +} + +- (void) setCookieMappingInDictionary: (NSMutableDictionary*) _cookieToButtonMapping { + + // check if we are using the rb device driver instead of the one from Apple + io_object_t foundRemoteDevice = [[self class] findRemoteDevice]; + BOOL leopardEmulation = NO; + if (foundRemoteDevice != 0) { + CFTypeRef leoEmuAttr = IORegistryEntryCreateCFProperty(foundRemoteDevice, CFSTR("RemoteBuddyEmulationV2"), kCFAllocatorDefault, 0); + if (leoEmuAttr) { + leopardEmulation = CFEqual(leoEmuAttr, kCFBooleanTrue); + CFRelease(leoEmuAttr); + } + IOObjectRelease(foundRemoteDevice); + } + + if (floor(NSAppKitVersionNumber) <= NSAppKitVersionNumber10_4) { + // 10.4.x Tiger + [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonPlus] forKey:@"14_12_11_6_"]; + [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonMinus] forKey:@"14_13_11_6_"]; + [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonMenu] forKey:@"14_7_6_14_7_6_"]; + [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonPlay] forKey:@"14_8_6_14_8_6_"]; + [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonRight] forKey:@"14_9_6_14_9_6_"]; + [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonLeft] forKey:@"14_10_6_14_10_6_"]; + [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonRight_Hold] forKey:@"14_6_4_2_"]; + [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonLeft_Hold] forKey:@"14_6_3_2_"]; + [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonMenu_Hold] forKey:@"14_6_14_6_"]; + [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonPlay_Hold] forKey:@"18_14_6_18_14_6_"]; + [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteControl_Switched] forKey:@"19_"]; + } else if ((floor(NSAppKitVersionNumber) <= NSAppKitVersionNumber10_5) || (leopardEmulation)) { + // 10.5.x Leopard + [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonPlus] forKey:@"31_29_28_19_18_"]; + [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonMinus] forKey:@"31_30_28_19_18_"]; + [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonMenu] forKey:@"31_20_19_18_31_20_19_18_"]; + [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonPlay] forKey:@"31_21_19_18_31_21_19_18_"]; + [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonRight] forKey:@"31_22_19_18_31_22_19_18_"]; + [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonLeft] forKey:@"31_23_19_18_31_23_19_18_"]; + [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonRight_Hold] forKey:@"31_19_18_4_2_"]; + [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonLeft_Hold] forKey:@"31_19_18_3_2_"]; + [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonMenu_Hold] forKey:@"31_19_18_31_19_18_"]; + [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonPlay_Hold] forKey:@"35_31_19_18_35_31_19_18_"]; + [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteControl_Switched] forKey:@"19_"]; + } else { + // 10.6.2 Snow Leopard + // Note: does not work on 10.6.0 and 10.6.1 + [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonPlus] forKey:@"33_31_30_21_20_2_"]; + [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonMinus] forKey:@"33_32_30_21_20_2_"]; + [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonMenu] forKey:@"33_22_21_20_2_33_22_21_20_2_"]; + [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonPlay] forKey:@"33_23_21_20_2_33_23_21_20_2_"]; + [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonRight] forKey:@"33_24_21_20_2_33_24_21_20_2_"]; + [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonLeft] forKey:@"33_25_21_20_2_33_25_21_20_2_"]; + [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonRight_Hold] forKey:@"33_21_20_14_12_2_"]; + [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonLeft_Hold] forKey:@"33_21_20_13_12_2_"]; + [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonMenu_Hold] forKey:@"33_21_20_2_33_21_20_2_"]; + [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonPlay_Hold] forKey:@"37_33_21_20_2_37_33_21_20_2_"]; + [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteControl_Switched] forKey:@"19_"]; + + // new Aluminum model + // Mappings changed due to addition of a 7th center button + // Treat the new center button and play/pause button the same + [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonPlay] forKey:@"33_21_20_8_2_33_21_20_8_2_"]; + [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonPlay] forKey:@"33_21_20_3_2_33_21_20_3_2_"]; + [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonPlay_Hold] forKey:@"33_21_20_11_2_33_21_20_11_2_"]; + } + +} + +- (void) sendRemoteButtonEvent: (RemoteControlEventIdentifier) event pressedDown: (BOOL) pressedDown { + if (pressedDown == NO && event == kRemoteButtonMenu_Hold) { + // There is no seperate event for pressed down on menu hold. We are simulating that event here + [super sendRemoteButtonEvent:event pressedDown:YES]; + } + + [super sendRemoteButtonEvent:event pressedDown:pressedDown]; + + if (pressedDown && (event == kRemoteButtonRight || event == kRemoteButtonLeft || event == kRemoteButtonPlay || event == kRemoteButtonMenu || event == kRemoteButtonPlay_Hold)) { + // There is no seperate event when the button is being released. We are simulating that event here + [super sendRemoteButtonEvent:event pressedDown:NO]; + } +} + +// overwritten to handle a special case with old versions of the rb driver ++ (io_object_t) findRemoteDevice +{ + CFMutableDictionaryRef hidMatchDictionary = NULL; + IOReturn ioReturnValue = kIOReturnSuccess; + io_iterator_t hidObjectIterator = 0; + io_object_t hidDevice = 0; + + // Set up a matching dictionary to search the I/O Registry by class + // name for all HID class devices + hidMatchDictionary = IOServiceMatching([self remoteControlDeviceName]); + + // Now search I/O Registry for matching devices. + ioReturnValue = IOServiceGetMatchingServices(kIOMasterPortDefault, hidMatchDictionary, &hidObjectIterator); + + if ((ioReturnValue == kIOReturnSuccess) && (hidObjectIterator != 0)) + { + io_object_t matchingService = 0, foundService = 0; + BOOL finalMatch = NO; + + while ((matchingService = IOIteratorNext(hidObjectIterator))) + { + if (!finalMatch) + { + CFTypeRef className; + + if (!foundService) + { + if (IOObjectRetain(matchingService) == kIOReturnSuccess) + { + foundService = matchingService; + } + } + + if ((className = IORegistryEntryCreateCFProperty((io_registry_entry_t)matchingService, CFSTR("IOClass"), kCFAllocatorDefault, 0))) + { + if ([(__bridge NSString *)className isEqual:[NSString stringWithUTF8String:[self remoteControlDeviceName]]]) + { + if (foundService) + { + IOObjectRelease(foundService); + foundService = 0; + } + + if (IOObjectRetain(matchingService) == kIOReturnSuccess) + { + foundService = matchingService; + finalMatch = YES; + } + } + + CFRelease(className); + } + } + + IOObjectRelease(matchingService); + } + + hidDevice = foundService; + + // release the iterator + IOObjectRelease(hidObjectIterator); + } + + return hidDevice; +} + +- (BOOL) retrieveSecureEventInputState { + BOOL returnValue = NO; + + io_registry_entry_t root = IORegistryGetRootEntry( kIOMasterPortDefault ); + if (root != MACH_PORT_NULL) { + CFArrayRef arrayRef = IORegistryEntrySearchCFProperty(root, kIOServicePlane, CFSTR("IOConsoleUsers"), NULL, kIORegistryIterateRecursively); + if (arrayRef != NULL) { + NSArray* array = (__bridge NSArray*)arrayRef; + unsigned int i; + for(i=0; i < [array count]; i++) { + NSDictionary* dict = [array objectAtIndex:i]; + if ([[dict objectForKey: @"kCGSSessionUserNameKey"] isEqual: NSUserName()]) { + returnValue = ([dict objectForKey:@"kCGSSessionSecureInputPID"] != nil); + } + } + CFRelease(arrayRef); + } + IOObjectRelease(root); + } + return returnValue; +} + +- (void) dealWithSecureEventInputChange { + if ([self isListeningToRemote] == NO || [self isOpenInExclusiveMode] == NO) return; + + BOOL newState = [self retrieveSecureEventInputState]; + if (lastSecureEventInputState == newState) return; + + // close and open the device again + [self closeRemoteControlDevice: NO]; + [self openRemoteControlDevice]; + + lastSecureEventInputState = newState; +} + +static void IOREInterestCallback(void * refcon, + io_service_t service, + uint32_t messageType, + void * messageArgument ) +{ + (void)service; + (void)messageType; + (void)messageArgument; + + // With garbage collection, such a cast is dangerous because the refcon parameter is not strong. That means that, unless someone has a strong reference somewhere, the AppleRemote may have already been finalized. But it should be pretty safe in this case, since if the AppleRemote is finalized, the callback is cancelled and should never be invoked. + AppleRemote* remote = (__bridge AppleRemote*)refcon; + + [remote dealWithSecureEventInputChange]; +} + +@end diff --git a/VKPC/AppleRemote/HIDRemoteControlDevice.h b/VKPC/AppleRemote/HIDRemoteControlDevice.h new file mode 100644 index 0000000..9297e69 --- /dev/null +++ b/VKPC/AppleRemote/HIDRemoteControlDevice.h @@ -0,0 +1,72 @@ +/***************************************************************************** + * HIDRemoteControlDevice.h + * RemoteControlWrapper + * + * Created by Martin Kahr on 11.03.06 under a MIT-style license. + * Copyright (c) 2006 martinkahr.com. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED ‚ÄúAS IS‚Äù, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + *****************************************************************************/ + +#import <Cocoa/Cocoa.h> +#import <IOKit/hid/IOHIDLib.h> + +#import "RemoteControl.h" + +/* + Base class for HID based remote control devices + */ +@interface HIDRemoteControlDevice : RemoteControl { + IOHIDDeviceInterface** hidDeviceInterface; + IOHIDQueueInterface** queue; + NSMutableArray* allCookies; + NSMutableDictionary* cookieToButtonMapping; + +// __strong CFRunLoopSourceRef eventSource; + CFRunLoopSourceRef eventSource; + + BOOL openInExclusiveMode; + BOOL processesBacklog; + + int supportedButtonEvents; +} + +// When your application needs to much time on the main thread when processing an event other events +// may already be received which are put on a backlog. As soon as your main thread +// has some spare time this backlog is processed and may flood your delegate with calls. +// Backlog processing is turned off by default. +- (BOOL) processesBacklog; +- (void) setProcessesBacklog: (BOOL) value; + +// methods that should be overridden by subclasses +- (void) setCookieMappingInDictionary: (NSMutableDictionary*) cookieToButtonMapping; + +- (void) sendRemoteButtonEvent: (RemoteControlEventIdentifier) event pressedDown: (BOOL) pressedDown; + ++ (const char*) remoteControlDeviceName; + +// protected methods +- (void) openRemoteControlDevice; +- (void) closeRemoteControlDevice: (BOOL) shallSendNotifications; + ++ (io_object_t) findRemoteDevice; ++ (BOOL) isRemoteAvailable; + +@end diff --git a/VKPC/AppleRemote/HIDRemoteControlDevice.m b/VKPC/AppleRemote/HIDRemoteControlDevice.m new file mode 100644 index 0000000..30e7758 --- /dev/null +++ b/VKPC/AppleRemote/HIDRemoteControlDevice.m @@ -0,0 +1,535 @@ +/***************************************************************************** + * HIDRemoteControlDevice.m + * RemoteControlWrapper + * + * Created by Martin Kahr on 11.03.06 under a MIT-style license. + * Copyright (c) 2006 martinkahr.com. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + *****************************************************************************/ + +#import "HIDRemoteControlDevice.h" + +#import <IOKit/IOKitLib.h> +#import <IOKit/IOCFPlugIn.h> +#import <IOKit/hid/IOHIDKeys.h> + +@interface HIDRemoteControlDevice (PrivateMethods) +- (NSDictionary*) cookieToButtonMapping; +- (IOHIDQueueInterface**) queue; +- (IOHIDDeviceInterface**) hidDeviceInterface; +- (void) handleEventWithCookieString: (NSString*) cookieString sumOfValues: (SInt32) sumOfValues; +- (void) removeNotifcationObserver; +- (void) remoteControlAvailable:(NSNotification *)notification; + +@end + +@interface HIDRemoteControlDevice (IOKitMethods) +- (IOHIDDeviceInterface**) createInterfaceForDevice: (io_object_t) hidDevice; +- (BOOL) initializeCookies; +- (BOOL) openDevice; +@end + +@implementation HIDRemoteControlDevice + +// This class acts as an abstract base class - therefore subclasses have to override this method ++ (const char*) remoteControlDeviceName { + return ""; +} + ++ (BOOL) isRemoteAvailable { + io_object_t hidDevice = [self findRemoteDevice]; + if (hidDevice != 0) { + IOObjectRelease(hidDevice); + return YES; + } else { + return NO; + } +} + ++ (io_object_t) findRemoteDevice { + CFMutableDictionaryRef hidMatchDictionary = NULL; + IOReturn ioReturnValue = kIOReturnSuccess; + io_iterator_t hidObjectIterator = 0; + io_object_t hidDevice = 0; + + // Set up a matching dictionary to search the I/O Registry by class + // name for all HID class devices + hidMatchDictionary = IOServiceMatching([self remoteControlDeviceName]); + + // Now search I/O Registry for matching devices. + ioReturnValue = IOServiceGetMatchingServices(kIOMasterPortDefault, hidMatchDictionary, &hidObjectIterator); + + if (hidObjectIterator != 0) { + if (ioReturnValue == kIOReturnSuccess) { + hidDevice = IOIteratorNext(hidObjectIterator); + } + // release the iterator + IOObjectRelease(hidObjectIterator); + } + + // Returned value must be released by the caller when it is finished + return hidDevice; +} + +- (id) initWithDelegate: (id) _remoteControlDelegate { + if ([[self class] isRemoteAvailable] == NO) { +// [super dealloc]; + self = nil; + } else if ( (self = [super initWithDelegate: _remoteControlDelegate]) ) { + openInExclusiveMode = YES; + queue = NULL; + hidDeviceInterface = NULL; + cookieToButtonMapping = [[NSMutableDictionary alloc] init]; + + [self setCookieMappingInDictionary: cookieToButtonMapping]; + + NSEnumerator* enumerator = [cookieToButtonMapping objectEnumerator]; + NSNumber* identifier; + supportedButtonEvents = 0; + while( (identifier = [enumerator nextObject]) ) { + supportedButtonEvents |= [identifier intValue]; + } + } + + return self; +} + +- (void) dealloc { + [self removeNotifcationObserver]; + [self stopListening:self]; +// [cookieToButtonMapping release]; + cookieToButtonMapping = nil; +// [super dealloc]; +} + +- (void) sendRemoteButtonEvent: (RemoteControlEventIdentifier) event pressedDown: (BOOL) pressedDown { + [delegate sendRemoteButtonEvent: event pressedDown: pressedDown remoteControl:self]; +} + +- (void) setCookieMappingInDictionary: (NSMutableDictionary*) aCookieToButtonMapping { + (void)aCookieToButtonMapping; +} +- (int) remoteIdSwitchCookie { + return 0; +} + +- (BOOL) sendsEventForButtonIdentifier: (RemoteControlEventIdentifier) identifier { + return (supportedButtonEvents & identifier) == identifier; +} + +- (BOOL) isListeningToRemote { + return (hidDeviceInterface != NULL && allCookies != NULL && queue != NULL); +} + +- (void) setListeningToRemote: (BOOL) value { + if (value == NO) { + [self stopListening:self]; + } else { + [self startListening:self]; + } +} + +- (BOOL) isOpenInExclusiveMode { + return openInExclusiveMode; +} +- (void) setOpenInExclusiveMode: (BOOL) value { + openInExclusiveMode = value; +} + +- (BOOL) processesBacklog { + return processesBacklog; +} +- (void) setProcessesBacklog: (BOOL) value { + processesBacklog = value; +} + +- (void) openRemoteControlDevice { + io_object_t hidDevice = [[self class] findRemoteDevice]; + if (hidDevice == 0) return; + + if ([self createInterfaceForDevice:hidDevice] == NULL) { + goto error; + } + + if ([self initializeCookies]==NO) { + goto error; + } + + if ([self openDevice]==NO) { + goto error; + } + goto cleanup; + +error: + [self stopListening:self]; + +cleanup: + IOObjectRelease(hidDevice); +} + +- (void) closeRemoteControlDevice: (BOOL) shallSendNotifications { + BOOL sendNotification = NO; + + if (eventSource != NULL) { + CFRunLoopRemoveSource(CFRunLoopGetCurrent(), eventSource, kCFRunLoopDefaultMode); + CFRelease(eventSource); + eventSource = NULL; + } + if (queue != NULL) { + (*queue)->stop(queue); + + //dispose of queue + (*queue)->dispose(queue); + + //release the queue we allocated + (*queue)->Release(queue); + + queue = NULL; + + sendNotification = YES; + } + + if (allCookies != nil) { +// [allCookies autorelease]; + allCookies = nil; + } + + if (hidDeviceInterface != NULL) { + //close the device + (*hidDeviceInterface)->close(hidDeviceInterface); + + //release the interface + (*hidDeviceInterface)->Release(hidDeviceInterface); + + hidDeviceInterface = NULL; + } + + if (shallSendNotifications && [self isOpenInExclusiveMode] && sendNotification) { + [[self class] sendFinishedNotifcationForAppIdentifier: nil]; + } +} + +- (IBAction) startListening: (id) sender { + (void)sender; + + if ([self isListeningToRemote]) return; + + [self willChangeValueForKey:@"listeningToRemote"]; + + [self openRemoteControlDevice]; + + [self didChangeValueForKey:@"listeningToRemote"]; +} + +- (IBAction) stopListening: (id) sender { + (void)sender; + + if ([self isListeningToRemote]==NO) return; + + [self willChangeValueForKey:@"listeningToRemote"]; + + [self closeRemoteControlDevice: YES]; + + [self didChangeValueForKey:@"listeningToRemote"]; +} + +@end + +@implementation HIDRemoteControlDevice (PrivateMethods) + +- (IOHIDQueueInterface**) queue { + return queue; +} + +- (IOHIDDeviceInterface**) hidDeviceInterface { + return hidDeviceInterface; +} + + +- (NSDictionary*) cookieToButtonMapping { + return cookieToButtonMapping; +} + +- (NSString*) validCookieSubstring: (NSString*) cookieString { + if (cookieString == nil || [cookieString length] == 0) return nil; + NSEnumerator* keyEnum = [[self cookieToButtonMapping] keyEnumerator]; + NSString* key; + + // find the best match + while( (key = [keyEnum nextObject]) ) { + NSRange range = [cookieString rangeOfString:key]; + if (range.location == 0) return key; + } + return nil; +} + +- (void) handleEventWithCookieString: (NSString*) cookieString sumOfValues: (SInt32) sumOfValues { + /* + if (previousRemainingCookieString) { + cookieString = [previousRemainingCookieString stringByAppendingString: cookieString]; + NSLog(@"New cookie string is %@", cookieString); + [previousRemainingCookieString release], previousRemainingCookieString=nil; + }*/ + if (cookieString == nil || [cookieString length] == 0) return; + + NSNumber* buttonId = [[self cookieToButtonMapping] objectForKey: cookieString]; + if (buttonId != nil) { + [self sendRemoteButtonEvent: [buttonId intValue] pressedDown: (sumOfValues>0)]; + } else { + // let's see if this is the first event after a restart of the OS. + // In this case the event has a prefix that we can ignore and we just get the down event but no up event + NSEnumerator* keyEnum = [[self cookieToButtonMapping] keyEnumerator]; + NSString* key; + while( (key = [keyEnum nextObject]) ) { + NSRange range = [cookieString rangeOfString:key]; + if (range.location != NSNotFound && range.location > 0) { + buttonId = [[self cookieToButtonMapping] objectForKey: key]; + if (buttonId != nil) { + [self sendRemoteButtonEvent: [buttonId intValue] pressedDown: YES]; + [self sendRemoteButtonEvent: [buttonId intValue] pressedDown: NO]; + return; + } + return; + } + } + + // let's see if a number of events are stored in the cookie string. this does + // happen when the main thread is too busy to handle all incoming events in time. + NSString* subCookieString; + NSString* lastSubCookieString=nil; + while( (subCookieString = [self validCookieSubstring: cookieString]) ) { + cookieString = [cookieString substringFromIndex: [subCookieString length]]; + lastSubCookieString = subCookieString; + if (processesBacklog) [self handleEventWithCookieString: subCookieString sumOfValues:sumOfValues]; + } + if (processesBacklog == NO && lastSubCookieString != nil) { + // process the last event of the backlog and assume that the button is not pressed down any longer. + // The events in the backlog do not seem to be in order and therefore (in rare cases) the last event might be + // a button pressed down event while in reality the user has released it. + // NSLog(@"processing last event of backlog"); + [self handleEventWithCookieString: lastSubCookieString sumOfValues:0]; + } + if ([cookieString length] > 0) { + NSLog(@"Unknown button for cookiestring %@", cookieString); + } + } +} + +- (void) removeNotifcationObserver { + NSDistributedNotificationCenter* defaultCenter = [NSDistributedNotificationCenter defaultCenter]; + [defaultCenter removeObserver:self name:FINISHED_USING_REMOTE_CONTROL_NOTIFICATION object:nil]; +} + +- (void) remoteControlAvailable:(NSNotification *)notification { + (void)notification; + [self removeNotifcationObserver]; + [self startListening: self]; +} + +@end + +/* Callback method for the device queue +Will be called for any event of any type (cookie) to which we subscribe +*/ +static void QueueCallbackFunction(void* target, IOReturn result, void* refcon, void* sender) { + (void)refcon; + (void)sender; + + if (target == NULL) { + NSLog(@"QueueCallbackFunction called with invalid target!"); + return; + } +// NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; + + HIDRemoteControlDevice* remote = (__bridge HIDRemoteControlDevice*)target; + IOHIDEventStruct event; + AbsoluteTime zeroTime = {0,0}; + NSMutableString* cookieString = [NSMutableString string]; + SInt32 sumOfValues = 0; + while (result == kIOReturnSuccess) + { + result = (*[remote queue])->getNextEvent([remote queue], &event, zeroTime, 0); + if ( result != kIOReturnSuccess ) + continue; + + //printf("%u %d %p\n", event.elementCookie, event.value, event.longValue); + + if (((int)event.elementCookie)!=5) { + sumOfValues+=event.value; + [cookieString appendString:[NSString stringWithFormat:@"%u_", event.elementCookie]]; + } + } + + [remote handleEventWithCookieString: cookieString sumOfValues: sumOfValues]; + +// [pool release]; +} + +@implementation HIDRemoteControlDevice (IOKitMethods) + +- (IOHIDDeviceInterface**) createInterfaceForDevice: (io_object_t) hidDevice { + io_name_t className; + IOCFPlugInInterface** plugInInterface = NULL; + HRESULT plugInResult = S_OK; + SInt32 score = 0; + IOReturn ioReturnValue = kIOReturnSuccess; + + hidDeviceInterface = NULL; + + ioReturnValue = IOObjectGetClass(hidDevice, className); + + if (ioReturnValue != kIOReturnSuccess) { + NSLog(@"Error: Failed to get class name."); + return NULL; + } + + ioReturnValue = IOCreatePlugInInterfaceForService(hidDevice, + kIOHIDDeviceUserClientTypeID, + kIOCFPlugInInterfaceID, + &plugInInterface, + &score); + if (ioReturnValue == kIOReturnSuccess) + { + //Call a method of the intermediate plug-in to create the device interface + plugInResult = (*plugInInterface)->QueryInterface(plugInInterface, CFUUIDGetUUIDBytes(kIOHIDDeviceInterfaceID), (LPVOID) &hidDeviceInterface); + + if (plugInResult != S_OK) { + NSLog(@"Error: Couldn't create HID class device interface"); + } + // Release + if (plugInInterface) (*plugInInterface)->Release(plugInInterface); + } + return hidDeviceInterface; +} + +- (BOOL) initializeCookies { + IOHIDDeviceInterface122** handle = (IOHIDDeviceInterface122**)hidDeviceInterface; + IOHIDElementCookie cookie; + //long usage; + //long usagePage; + id object; + CFArrayRef elements = nil; + NSDictionary* element; + IOReturn success; + + if (!handle || !(*handle)) return NO; + + // Copy all elements, since we're grabbing most of the elements + // for this device anyway, and thus, it's faster to iterate them + // ourselves. When grabbing only one or two elements, a matching + // dictionary should be passed in here instead of NULL. + + success = (*handle)->copyMatchingElements(handle, NULL, &elements); + + if ( (success == kIOReturnSuccess) && elements ) { + /* + cookies = calloc(NUMBER_OF_APPLE_REMOTE_ACTIONS, sizeof(IOHIDElementCookie)); + memset(cookies, 0, sizeof(IOHIDElementCookie) * NUMBER_OF_APPLE_REMOTE_ACTIONS); + */ + allCookies = [[NSMutableArray alloc] init]; + + NSEnumerator *elementsEnumerator = [(__bridge NSArray*)elements objectEnumerator]; + + while ( (element = [elementsEnumerator nextObject]) ) { + //Get cookie + object = [element valueForKey:@kIOHIDElementCookieKey ]; + if (object == nil || ![object isKindOfClass:[NSNumber class]]) continue; + if (object == 0 || CFGetTypeID((__bridge const void *)object) != CFNumberGetTypeID()) continue; + cookie = (IOHIDElementCookie) [object longValue]; + + //Get usage + object = [element valueForKey: @kIOHIDElementUsageKey ]; + if (object == nil || ![object isKindOfClass:[NSNumber class]]) continue; + //usage = [object longValue]; + + //Get usage page + object = [element valueForKey: @kIOHIDElementUsagePageKey ]; + if (object == nil || ![object isKindOfClass:[NSNumber class]]) continue; + //usagePage = [object longValue]; + + //It seems wrong to cast a cookie to a 32 bit integer since it is a void*, but in 64 bit it's actually a uint32_t! So in both 32 and 64 bit it is 32 bit in size. + [allCookies addObject: [NSNumber numberWithUnsignedInt:(uint32_t)cookie]]; + } + + CFRelease(elements); + } else { + return NO; + } + + return YES; +} + +- (BOOL)openDevice { + HRESULT result; + + IOHIDOptionsType openMode = kIOHIDOptionsTypeNone; + if ([self isOpenInExclusiveMode]) openMode = kIOHIDOptionsTypeSeizeDevice; + IOReturn ioReturnValue = (*hidDeviceInterface)->open(hidDeviceInterface, openMode); + + if (ioReturnValue == KERN_SUCCESS) { + queue = (*hidDeviceInterface)->allocQueue(hidDeviceInterface); + if (queue) { + result = (*queue)->create(queue, 0, 12); //depth: maximum number of elements in queue before oldest elements in queue begin to be lost. + if (result == kIOReturnSuccess) { + IOHIDElementCookie cookie; + NSEnumerator *allCookiesEnumerator = [allCookies objectEnumerator]; + + while ( (cookie = (IOHIDElementCookie)[[allCookiesEnumerator nextObject] unsignedIntValue]) ) { + (*queue)->addElement(queue, cookie, 0); + } + + // add callback for async events + ioReturnValue = (*queue)->createAsyncEventSource(queue, &eventSource); + if (ioReturnValue == KERN_SUCCESS) { + ioReturnValue = (*queue)->setEventCallout(queue,QueueCallbackFunction, (__bridge void *)self, NULL); + if (ioReturnValue == KERN_SUCCESS) { + CFRunLoopAddSource(CFRunLoopGetCurrent(), eventSource, kCFRunLoopDefaultMode); + + //start data delivery to queue + (*queue)->start(queue); + return YES; + } else { + NSLog(@"Error when setting event callback"); + } + } else { + NSLog(@"Error when creating async event source"); + } + } else { + NSLog(@"Error when creating queue"); + } + } else { + NSLog(@"Error when opening device"); + } + } else if (ioReturnValue == kIOReturnExclusiveAccess) { + // the device is used exclusive by another application + + // 1. we register for the FINISHED_USING_REMOTE_CONTROL_NOTIFICATION notification + NSDistributedNotificationCenter* defaultCenter = [NSDistributedNotificationCenter defaultCenter]; + [defaultCenter addObserver:self selector:@selector(remoteControlAvailable:) name:FINISHED_USING_REMOTE_CONTROL_NOTIFICATION object:nil]; + + // 2. send a distributed notification that we wanted to use the remote control + [[self class] sendRequestForRemoteControlNotification]; + } + return NO; +} + +@end + diff --git a/VKPC/AppleRemote/MultiClickRemoteBehavior.h b/VKPC/AppleRemote/MultiClickRemoteBehavior.h new file mode 100644 index 0000000..eea756f --- /dev/null +++ b/VKPC/AppleRemote/MultiClickRemoteBehavior.h @@ -0,0 +1,90 @@ +/***************************************************************************** + * MultiClickRemoteBehavior.h + * RemoteControlWrapper + * + * Created by Martin Kahr on 11.03.06 under a MIT-style license. + * Copyright (c) 2006 martinkahr.com. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + *****************************************************************************/ + + +#import <Cocoa/Cocoa.h> +#import "RemoteControl.h" + +/** + A behavior that adds multiclick and hold events on top of a device. + Events are generated and send to a delegate + */ +@interface MultiClickRemoteBehavior : NSObject { + id delegate; + + // state for simulating plus/minus hold + BOOL simulateHoldEvents; + BOOL lastEventSimulatedHold; + RemoteControlEventIdentifier lastHoldEvent; + NSTimeInterval lastHoldEventTime; + + // state for multi click + unsigned int clickCountEnabledButtons; + NSTimeInterval maxClickTimeDifference; + NSTimeInterval lastClickCountEventTime; + RemoteControlEventIdentifier lastClickCountEvent; + unsigned int eventClickCount; +} + +- (id) init; + +// Delegates are not retained +- (void) setDelegate: (id) delegate; +- (id) delegate; + +// Simulating hold events does deactivate sending of individual requests for pressed down/released. +// Instead special hold events are being triggered when the user is pressing and holding a button for a small period. +// Simulation is activated only for those buttons and remote control that do not have a seperate event already +- (BOOL) simulateHoldEvent; +- (void) setSimulateHoldEvent: (BOOL) value; + +// click counting makes it possible to recognize if the user has pressed a button repeatedly +// click counting does delay each event as it has to wait if there is another event (second click) +// therefore there is a slight time difference (maximumClickCountTimeDifference) between a single click +// of the user and the call of your delegate method +// click counting can be enabled individually for specific buttons. Use the property clickCountEnableButtons to +// set the buttons for which click counting shall be enabled +- (BOOL) clickCountingEnabled; +- (void) setClickCountingEnabled: (BOOL) value; + +- (unsigned int) clickCountEnabledButtons; +- (void) setClickCountEnabledButtons: (unsigned int)value; + +// the maximum time difference till which clicks are recognized as multi clicks +- (NSTimeInterval) maximumClickCountTimeDifference; +- (void) setMaximumClickCountTimeDifference: (NSTimeInterval) timeDiff; + +@end + +/* + * Method definitions for the delegate of the MultiClickRemoteBehavior class + */ +@interface NSObject(MultiClickRemoteBehaviorDelegate) + +- (void) remoteButton: (RemoteControlEventIdentifier)buttonIdentifier pressedDown: (BOOL) pressedDown clickCount: (unsigned int) count; + +@end diff --git a/VKPC/AppleRemote/MultiClickRemoteBehavior.m b/VKPC/AppleRemote/MultiClickRemoteBehavior.m new file mode 100644 index 0000000..d861969 --- /dev/null +++ b/VKPC/AppleRemote/MultiClickRemoteBehavior.m @@ -0,0 +1,214 @@ +/***************************************************************************** + * MultiClickRemoteBehavior.m + * RemoteControlWrapper + * + * Created by Martin Kahr on 11.03.06 under a MIT-style license. + * Copyright (c) 2006 martinkahr.com. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + *****************************************************************************/ + +#import "MultiClickRemoteBehavior.h" + +const NSTimeInterval DEFAULT_MAXIMUM_CLICK_TIME_DIFFERENCE=0.35; +const NSTimeInterval HOLD_RECOGNITION_TIME_INTERVAL=0.4; + +@implementation MultiClickRemoteBehavior + +- (id) init { + if (self = [super init]) { + maxClickTimeDifference = DEFAULT_MAXIMUM_CLICK_TIME_DIFFERENCE; + } + return self; +} + +// Delegates are not retained! +// http://developer.apple.com/documentation/Cocoa/Conceptual/CocoaFundamentals/CommunicatingWithObjects/chapter_6_section_4.html +// Delegating objects do not (and should not) retain their delegates. +// However, clients of delegating objects (applications, usually) are responsible for ensuring that their delegates are around +// to receive delegation messages. To do this, they may have to retain the delegate. +- (void) setDelegate: (id) _delegate { + if (_delegate && [_delegate respondsToSelector:@selector(remoteButton:pressedDown:clickCount:)]==NO) return; + + delegate = _delegate; +} +- (id) delegate { + return delegate; +} + +- (BOOL) simulateHoldEvent { + return simulateHoldEvents; +} +- (void) setSimulateHoldEvent: (BOOL) value { + simulateHoldEvents = value; +} + +- (BOOL) simulatesHoldForButtonIdentifier: (RemoteControlEventIdentifier) identifier remoteControl: (RemoteControl*) remoteControl { + // we do that check only for the normal button identifiers as we would check for hold support for hold events instead + if (identifier > (1 << EVENT_TO_HOLD_EVENT_OFFSET)) return NO; + + return [self simulateHoldEvent] && [remoteControl sendsEventForButtonIdentifier: (identifier << EVENT_TO_HOLD_EVENT_OFFSET)]==NO; +} + +- (BOOL) clickCountingEnabled { + return clickCountEnabledButtons != 0; +} +- (void) setClickCountingEnabled: (BOOL) value { + if (value) { + [self setClickCountEnabledButtons: kRemoteButtonPlus | kRemoteButtonMinus | kRemoteButtonPlay | kRemoteButtonLeft | kRemoteButtonRight | kRemoteButtonMenu]; + } else { + [self setClickCountEnabledButtons: 0]; + } +} + +- (unsigned int) clickCountEnabledButtons { + return clickCountEnabledButtons; +} +- (void) setClickCountEnabledButtons: (unsigned int)value { + clickCountEnabledButtons = value; +} + +- (NSTimeInterval) maximumClickCountTimeDifference { + return maxClickTimeDifference; +} +- (void) setMaximumClickCountTimeDifference: (NSTimeInterval) timeDiff { + maxClickTimeDifference = timeDiff; +} + +- (void) sendPressedDownEventToMainThread: (NSNumber*) event { + [delegate remoteButton:[event intValue] pressedDown:YES clickCount:1]; +} + +- (void) sendSimulatedHoldEvent: (id) time { + BOOL startSimulateHold = NO; + RemoteControlEventIdentifier event = lastHoldEvent; + @synchronized(self) { + startSimulateHold = (lastHoldEvent>0 && lastHoldEventTime == [time doubleValue]); + } + if (startSimulateHold) { + lastEventSimulatedHold = YES; + event = (event << EVENT_TO_HOLD_EVENT_OFFSET); + [self performSelectorOnMainThread:@selector(sendPressedDownEventToMainThread:) withObject:[NSNumber numberWithInt:event] waitUntilDone:NO]; + } +} + +- (void) executeClickCountEvent: (NSArray*) values { + RemoteControlEventIdentifier event = [[values objectAtIndex: 0] unsignedIntValue]; + NSTimeInterval eventTimePoint = [[values objectAtIndex: 1] doubleValue]; + + BOOL finishedClicking = NO; + int finalClickCount = eventClickCount; + + @synchronized(self) { + finishedClicking = (event != lastClickCountEvent || eventTimePoint == lastClickCountEventTime); + if (finishedClicking) { + eventClickCount = 0; + lastClickCountEvent = 0; + lastClickCountEventTime = 0; + } + } + + if (finishedClicking) { + [delegate remoteButton:event pressedDown: YES clickCount:finalClickCount]; + // trigger a button release event, too + [NSThread sleepUntilDate: [NSDate dateWithTimeIntervalSinceNow:0.1]]; + [delegate remoteButton:event pressedDown: NO clickCount:finalClickCount]; + } +} + +- (void) sendRemoteButtonEvent: (RemoteControlEventIdentifier) event pressedDown: (BOOL) pressedDown remoteControl: (RemoteControl*) remoteControl { + if (!delegate) return; + + BOOL clickCountingForEvent = ([self clickCountEnabledButtons] & event) == event; + + if ([self simulatesHoldForButtonIdentifier: event remoteControl: remoteControl] && lastClickCountEvent==0) { + if (pressedDown) { + // wait to see if it is a hold + lastHoldEvent = event; + lastHoldEventTime = [NSDate timeIntervalSinceReferenceDate]; + [self performSelector:@selector(sendSimulatedHoldEvent:) + withObject:[NSNumber numberWithDouble:lastHoldEventTime] + afterDelay:HOLD_RECOGNITION_TIME_INTERVAL]; + return; + } else { + if (lastEventSimulatedHold) { + // it was a hold + // send an event for "hold release" + event = (event << EVENT_TO_HOLD_EVENT_OFFSET); + lastHoldEvent = 0; + lastEventSimulatedHold = NO; + + [delegate remoteButton:event pressedDown: pressedDown clickCount:1]; + return; + } else { + RemoteControlEventIdentifier previousEvent = lastHoldEvent; + @synchronized(self) { + lastHoldEvent = 0; + } + + // in case click counting is enabled we have to setup the state for that, too + if (clickCountingForEvent) { + lastClickCountEvent = previousEvent; + lastClickCountEventTime = lastHoldEventTime; + NSNumber* eventNumber; + NSNumber* timeNumber; + eventClickCount = 1; + timeNumber = [NSNumber numberWithDouble:lastClickCountEventTime]; + eventNumber= [NSNumber numberWithUnsignedInt:previousEvent]; + NSTimeInterval diffTime = maxClickTimeDifference-([NSDate timeIntervalSinceReferenceDate]-lastHoldEventTime); + [self performSelector: @selector(executeClickCountEvent:) + withObject: [NSArray arrayWithObjects:eventNumber, timeNumber, nil] + afterDelay: diffTime]; + // we do not return here because we are still in the press-release event + // that will be consumed below + } else { + // trigger the pressed down event that we consumed first + [delegate remoteButton:event pressedDown: YES clickCount:1]; + } + } + } + } + + if (clickCountingForEvent) { + if (pressedDown == NO) return; + + NSNumber* eventNumber; + NSNumber* timeNumber; + @synchronized(self) { + lastClickCountEventTime = [NSDate timeIntervalSinceReferenceDate]; + if (lastClickCountEvent == event) { + eventClickCount = eventClickCount + 1; + } else { + eventClickCount = 1; + } + lastClickCountEvent = event; + timeNumber = [NSNumber numberWithDouble:lastClickCountEventTime]; + eventNumber= [NSNumber numberWithUnsignedInt:event]; + } + [self performSelector: @selector(executeClickCountEvent:) + withObject: [NSArray arrayWithObjects:eventNumber, timeNumber, nil] + afterDelay: maxClickTimeDifference]; + } else { + [delegate remoteButton:event pressedDown: pressedDown clickCount:1]; + } + +} + +@end diff --git a/VKPC/AppleRemote/RemoteControl.h b/VKPC/AppleRemote/RemoteControl.h new file mode 100644 index 0000000..c393ea5 --- /dev/null +++ b/VKPC/AppleRemote/RemoteControl.h @@ -0,0 +1,105 @@ +/***************************************************************************** + * RemoteControl.h + * RemoteControlWrapper + * + * Created by Martin Kahr on 11.03.06 under a MIT-style license. + * Copyright (c) 2006 martinkahr.com. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + *****************************************************************************/ + +#import <Cocoa/Cocoa.h> + +// notifaction names that are being used to signal that an application wants to +// have access to the remote control device or if the application has finished +// using the remote control device +extern NSString* const REQUEST_FOR_REMOTE_CONTROL_NOTIFCATION; +extern NSString* const FINISHED_USING_REMOTE_CONTROL_NOTIFICATION; + +// keys used in user objects for distributed notifications +extern NSString* const kRemoteControlDeviceName; +extern NSString* const kApplicationIdentifier; +extern NSString* const kTargetApplicationIdentifier; + +// we have a 6 bit offset to make a hold event out of a normal event +#define EVENT_TO_HOLD_EVENT_OFFSET 6 + +@class RemoteControl; + +typedef enum _RemoteControlEventIdentifier { + // normal events + kRemoteButtonPlus =1<<1, + kRemoteButtonMinus =1<<2, + kRemoteButtonMenu =1<<3, + kRemoteButtonPlay =1<<4, + kRemoteButtonRight =1<<5, + kRemoteButtonLeft =1<<6, + + // hold events + kRemoteButtonPlus_Hold =1<<7, + kRemoteButtonMinus_Hold =1<<8, + kRemoteButtonMenu_Hold =1<<9, + kRemoteButtonPlay_Hold =1<<10, + kRemoteButtonRight_Hold =1<<11, + kRemoteButtonLeft_Hold =1<<12, + + // special events (not supported by all devices) + kRemoteControl_Switched =1<<13, +} RemoteControlEventIdentifier; + +@interface NSObject(RemoteControlDelegate) + +- (void) sendRemoteButtonEvent: (RemoteControlEventIdentifier) event pressedDown: (BOOL) pressedDown remoteControl: (RemoteControl*) remoteControl; + +@end + +/* + Base Interface for Remote Control devices +*/ +@interface RemoteControl : NSObject { + id delegate; +} + +// returns nil if the remote control device is not available +- (id) initWithDelegate: (id) remoteControlDelegate; + +- (void) setDelegate: (id) value; +- (id) delegate; + +- (void) setListeningToRemote: (BOOL) value; +- (BOOL) isListeningToRemote; + +- (BOOL) isOpenInExclusiveMode; +- (void) setOpenInExclusiveMode: (BOOL) value; + +- (IBAction) startListening: (id) sender; +- (IBAction) stopListening: (id) sender; + +// is this remote control sending the given event? +- (BOOL) sendsEventForButtonIdentifier: (RemoteControlEventIdentifier) identifier; + +// sending of notifications between applications ++ (void) sendFinishedNotifcationForAppIdentifier: (NSString*) identifier; ++ (void) sendRequestForRemoteControlNotification; + +// name of the device ++ (const char*) remoteControlDeviceName; + +@end diff --git a/VKPC/AppleRemote/RemoteControl.m b/VKPC/AppleRemote/RemoteControl.m new file mode 100644 index 0000000..43d87a9 --- /dev/null +++ b/VKPC/AppleRemote/RemoteControl.m @@ -0,0 +1,120 @@ +/***************************************************************************** + * RemoteControl.m + * RemoteControlWrapper + * + * Created by Martin Kahr on 11.03.06 under a MIT-style license. + * Copyright (c) 2006 martinkahr.com. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + *****************************************************************************/ + +#import "RemoteControl.h" + +// notifaction names that are being used to signal that an application wants to +// have access to the remote control device or if the application has finished +// using the remote control device +NSString* const REQUEST_FOR_REMOTE_CONTROL_NOTIFCATION = @"mac.remotecontrols.RequestForRemoteControl"; +NSString* const FINISHED_USING_REMOTE_CONTROL_NOTIFICATION = @"mac.remotecontrols.FinishedUsingRemoteControl"; + +// keys used in user objects for distributed notifications +NSString* const kRemoteControlDeviceName = @"RemoteControlDeviceName"; +NSString* const kApplicationIdentifier = @"CFBundleIdentifier"; +// bundle identifier of the application that should get access to the remote control +// this key is being used in the FINISHED notification only +NSString* const kTargetApplicationIdentifier = @"TargetBundleIdentifier"; + + +@implementation RemoteControl + +// returns nil if the remote control device is not available +- (id) initWithDelegate: (id) _remoteControlDelegate { + if ( (self = [super init]) ) { +// delegate = [_remoteControlDelegate retain]; + } + return self; +} + +//- (void) dealloc { +// [delegate release]; +// delegate = nil; +// [super dealloc]; +//} + +- (void)setDelegate:(id)value { + delegate = value; +// if (delegate != value) { +// [delegate release]; +// delegate = [value retain]; +// } +} + +- (id) delegate { + return delegate; +} + +- (void) setListeningToRemote: (BOOL) value { + (void)value; +} +- (BOOL) isListeningToRemote { + return NO; +} + +- (IBAction) startListening: (id) sender { + (void)sender; +} +- (IBAction) stopListening: (id) sender { + (void)sender; +} + +- (BOOL) isOpenInExclusiveMode { + return YES; +} +- (void) setOpenInExclusiveMode: (BOOL) value { + (void)value; +} + +- (BOOL) sendsEventForButtonIdentifier: (RemoteControlEventIdentifier) identifier { + (void)identifier; + return YES; +} + ++ (void) sendDistributedNotification: (NSString*) notificationName targetBundleIdentifier: (NSString*) targetIdentifier { + NSDictionary* userInfo = [NSDictionary dictionaryWithObjectsAndKeys: [NSString stringWithCString:[self remoteControlDeviceName] encoding:NSASCIIStringEncoding], + kRemoteControlDeviceName, [[NSBundle mainBundle] bundleIdentifier], kApplicationIdentifier, + targetIdentifier, kTargetApplicationIdentifier, nil]; + + [[NSDistributedNotificationCenter defaultCenter] postNotificationName:notificationName + object:nil + userInfo:userInfo + deliverImmediately:YES]; +} + ++ (void) sendFinishedNotifcationForAppIdentifier: (NSString*) identifier { + [self sendDistributedNotification:FINISHED_USING_REMOTE_CONTROL_NOTIFICATION targetBundleIdentifier:identifier]; +} ++ (void) sendRequestForRemoteControlNotification { + [self sendDistributedNotification:REQUEST_FOR_REMOTE_CONTROL_NOTIFCATION targetBundleIdentifier:nil]; +} + ++ (const char*) remoteControlDeviceName { + return NULL; +} + +@end diff --git a/VKPC/Application.h b/VKPC/Application.h new file mode 100644 index 0000000..e23f425 --- /dev/null +++ b/VKPC/Application.h @@ -0,0 +1,12 @@ +// +// Application.h +// VKPC +// +// Created by Eugene on 11/29/13. +// Copyright (c) 2013-2014 Eugene Z. All rights reserved. +// + +#import <Cocoa/Cocoa.h> + +@interface Application : NSApplication +@end diff --git a/VKPC/Application.m b/VKPC/Application.m new file mode 100644 index 0000000..c43d60b --- /dev/null +++ b/VKPC/Application.m @@ -0,0 +1,27 @@ +// +// Application.m +// VKPC +// +// Created by Eugene on 11/29/13. +// Copyright (c) 2013-2014 Eugene Z. All rights reserved. +// + +//#import <IOKit/hidsystem/ev_keymap.h> +#import "Application.h" +//#import "SPMediaKeyTap.h" + +@implementation Application { +} + +//- (void)sendEvent:(NSEvent *)event { +// [super sendEvent:event]; + + // If event tap is not installed, handle events that reach the app instead +// BOOL shouldHandleMediaKeyEventLocally = ![SPMediaKeyTap usesGlobalMediaKeyTap]; + +// if (shouldHandleMediaKeyEventLocally && event.type == NSSystemDefined && event.subtype == SPSystemDefinedEventMediaKeys) { +// [(id)[self delegate] mediaKeyTap:nil receivedMediaKeyEvent:event]; +// } +//} + +@end
\ No newline at end of file diff --git a/VKPC/Autostart.h b/VKPC/Autostart.h new file mode 100644 index 0000000..5c307f5 --- /dev/null +++ b/VKPC/Autostart.h @@ -0,0 +1,16 @@ +// +// Autostart.h +// VKPC +// +// Created by Eugene on 11/9/14. +// Copyright (c) 2014 Eugene Z. All rights reserved. +// + +#import <Foundation/Foundation.h> + +@interface Autostart : NSObject + ++ (BOOL)isLaunchAtStartup; ++ (void)toggleLaunchAtStartup; + +@end diff --git a/VKPC/Autostart.m b/VKPC/Autostart.m new file mode 100644 index 0000000..b40735b --- /dev/null +++ b/VKPC/Autostart.m @@ -0,0 +1,77 @@ +// +// Autostart.m +// VKPC +// +// Created by Eugene on 11/9/14. +// Copyright (c) 2014 Eugene Z. All rights reserved. +// +// Based on https://stackoverflow.com/questions/608963/register-as-login-item-with-cocoa + +#import "Autostart.h" + +@implementation Autostart + +static LSSharedFileListItemRef itemRefInLoginItems() { + LSSharedFileListItemRef res = nil; + + // Get the app's URL. + NSURL *bundleURL = [NSURL fileURLWithPath:[[NSBundle mainBundle] bundlePath]]; + // Get the LoginItems list. + LSSharedFileListRef loginItemsRef = LSSharedFileListCreate(NULL, kLSSharedFileListSessionLoginItems, NULL); + if (loginItemsRef == nil) return nil; + // Iterate over the LoginItems. + NSArray *loginItems = (__bridge NSArray *)LSSharedFileListCopySnapshot(loginItemsRef, nil); + for (id item in loginItems) { + LSSharedFileListItemRef itemRef = (__bridge LSSharedFileListItemRef)(item); + CFURLRef itemURLRef; + if (LSSharedFileListItemResolve(itemRef, 0, &itemURLRef, NULL) == noErr) { + // Again, use toll-free bridging. + NSURL *itemURL = (__bridge NSURL *)itemURLRef; + if ([itemURL isEqual:bundleURL]) { + res = itemRef; + break; + } + } + } + // Retain the LoginItem reference. + if (res != nil) CFRetain(res); + CFRelease(loginItemsRef); + CFRelease((__bridge CFTypeRef)(loginItems)); + + return res; +} + ++ (BOOL)isLaunchAtStartup { + // See if the app is currently in LoginItems. + LSSharedFileListItemRef itemRef = itemRefInLoginItems(); + // Store away that boolean. + BOOL isInList = itemRef != nil; + // Release the reference if it exists. + if (itemRef != nil) { + CFRelease(itemRef); + } + + return isInList; +} + ++ (void)toggleLaunchAtStartup { + // Toggle the state. + BOOL shouldBeToggled = ![self isLaunchAtStartup]; + // Get the LoginItems list. + LSSharedFileListRef loginItemsRef = LSSharedFileListCreate(NULL, kLSSharedFileListSessionLoginItems, NULL); + if (loginItemsRef == nil) return; + if (shouldBeToggled) { + // Add the app to the LoginItems list. + CFURLRef appUrl = (__bridge CFURLRef)[NSURL fileURLWithPath:[[NSBundle mainBundle] bundlePath]]; + LSSharedFileListItemRef itemRef = LSSharedFileListInsertItemURL(loginItemsRef, kLSSharedFileListItemLast, NULL, NULL, appUrl, NULL, NULL); + if (itemRef) CFRelease(itemRef); + } else { + // Remove the app from the LoginItems list. + LSSharedFileListItemRef itemRef = itemRefInLoginItems(); + LSSharedFileListItemRemove(loginItemsRef,itemRef); + if (itemRef != nil) CFRelease(itemRef); + } + CFRelease(loginItemsRef); +} + +@end diff --git a/VKPC/Base.lproj/MainMenu.xib b/VKPC/Base.lproj/MainMenu.xib new file mode 100644 index 0000000..e7f2fd6 --- /dev/null +++ b/VKPC/Base.lproj/MainMenu.xib @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="6250" systemVersion="14A388a" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES"> + <dependencies> + <deployment version="1070" identifier="macosx"/> + <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="6250"/> + </dependencies> + <objects> + <customObject id="-2" userLabel="File's Owner" customClass="Application"> + <connections> + <outlet property="delegate" destination="494" id="495"/> + </connections> + </customObject> + <customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/> + <customObject id="-3" userLabel="Application" customClass="NSObject"/> + <customObject id="494" customClass="AppDelegate"/> + </objects> +</document> diff --git a/VKPC/CatchMediaButtons.h b/VKPC/CatchMediaButtons.h new file mode 100644 index 0000000..e1a1a30 --- /dev/null +++ b/VKPC/CatchMediaButtons.h @@ -0,0 +1,18 @@ +// +// CatchMediaButtons.h +// VKPC +// +// Created by Eugene on 10/22/14. +// Copyright (c) 2014 Eugene Z. All rights reserved. +// + +#import <Foundation/Foundation.h> +#import "AppleRemote.h" + +@interface CatchMediaButtons : NSObject + +//+ (id)shared; ++ (void)start; ++ (void)stop; + +@end diff --git a/VKPC/CatchMediaButtons.m b/VKPC/CatchMediaButtons.m new file mode 100644 index 0000000..34db688 --- /dev/null +++ b/VKPC/CatchMediaButtons.m @@ -0,0 +1,146 @@ +// +// CatchMediaButtons.m +// VKPC +// +// Created by Eugene on 10/22/14. +// Copyright (c) 2014 Eugene Z. All rights reserved. + +#import "CatchMediaButtons.h" +#import "MultiClickRemoteBehavior.h" +#import "SPMediaKeyTap.h" +#import "Controller.h" + +static SPMediaKeyTap *keyTap; +static MultiClickRemoteBehavior *remoteBehavior; +static RemoteControl *remoteControl; +static BOOL started = NO; +static BOOL initialized = NO; + +@implementation CatchMediaButtons + ++ (void)initialize { + if (initialized) { + return; + } + + keyTap = nil; + [[NSUserDefaults standardUserDefaults] addObserver:(id)[self class] + forKeyPath:VKPCPreferencesCatchMediaButtons + options:NSKeyValueObservingOptionNew + context:NULL]; + if ([[NSUserDefaults standardUserDefaults] boolForKey:VKPCPreferencesCatchMediaButtons]) { + [CatchMediaButtons start]; + } + + initialized = YES; +} + ++ (void)start { + NSLog(@"[CatchMediaButtons] start"); + + if (started) { + NSLog(@"[CatchMediaButtons] start: already started, calling stop first"); + [self stop]; + } + + if (keyTap == nil) { + NSLog(@"[CatchMediaButtons] start: keyTap == nil, creating instance"); + + keyTap = [[SPMediaKeyTap alloc] initWithDelegate:self]; + + remoteControl = [[AppleRemote alloc] initWithDelegate:self]; + [remoteControl setDelegate:self]; + + remoteBehavior = [MultiClickRemoteBehavior new]; + [remoteBehavior setDelegate:self]; + [remoteControl setDelegate:remoteBehavior]; + } + + [keyTap startWatchingMediaKeys]; + [remoteControl startListening:self]; + + NSLog(@"[CatchMediaButtons] started"); + started = YES; +} + ++ (void)stop { + NSLog(@"[CatchMediaButtons] stop"); + + if (!started) { + NSLog(@"[CatchMediaButtons] stop: not started"); + return; + } + + [keyTap stopWatchingMediaKeys]; + [remoteControl stopListening:self]; + + NSLog(@"[CatchMediaButtons] stopped"); + started = NO; +} + ++ (void)remoteButton:(RemoteControlEventIdentifier)buttonIdentifier pressedDown:(BOOL)pressedDown clickCount:(unsigned int)clickCount { + if (!pressedDown) { + return; + } + + switch(buttonIdentifier) { + case kRemoteButtonPlay: +// [self forAllPlay]; + break; + + case kRemoteButtonRight: +// [self forAllNext]; + break; + + case kRemoteButtonLeft: +// [self forAllPrev]; + break; + + default: + break; + } +} + ++ (void)mediaKeyTap:(SPMediaKeyTap *)keyTap receivedMediaKeyEvent:(NSEvent *)event; { + NSAssert(event.type == NSSystemDefined && [event subtype] == SPSystemDefinedEventMediaKeys, @"Unexpected NSEvent in mediaKeyTap:receivedMediaKeyEvent:"); + int keyCode = (([event data1] & 0xFFFF0000) >> 16); + int keyFlags = ([event data1] & 0x0000FFFF); + BOOL keyIsPressed = (((keyFlags & 0xFF00) >> 8)) == 0xA; + + if (keyIsPressed) { + switch (keyCode) { + case NX_KEYTYPE_PLAY: + [Controller playpause]; + break; + + case NX_KEYTYPE_FAST: + [Controller next]; + break; + + case NX_KEYTYPE_REWIND: + [Controller prev]; + break; + + default: + // More cases defined in hidsystem/ev_keymap.h + break; + } + } +} + +// KVO ++ (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { + if ([keyPath isEqualToString:VKPCPreferencesCatchMediaButtons]) { + NSNumber *new = change[NSKeyValueChangeKindKey]; + if ([new integerValue] == NSKeyValueChangeSetting) { + BOOL value = [(NSNumber *)change[NSKeyValueChangeNewKey] boolValue]; + if (!value) { + [CatchMediaButtons stop]; + } else { + [CatchMediaButtons start]; + } + } + } +} + +@end diff --git a/VKPC/Controller.h b/VKPC/Controller.h new file mode 100644 index 0000000..8de646f --- /dev/null +++ b/VKPC/Controller.h @@ -0,0 +1,33 @@ +// +// Controller.h +// VKPC +// +// Created by Eugene on 10/23/14. +// Copyright (c) 2014 Eugene Z. All rights reserved. +// + +#import <Foundation/Foundation.h> +#import "Playlist.h" + +@interface Controller : NSObject <PlaylistDelegate> + ++ (void)prev; ++ (void)next; ++ (void)playpause; ++ (void)operateTrack:(NSString *)trackID; ++ (NSString *)findRunningAppAndPrepareASForCommand:(NSString *)command; ++ (void)sendCommand:(NSString *)command; ++ (void)handleClient:(NSDictionary *)json; + ++ (BOOL)isASBrowser:(NSInteger)browser; ++ (NSString *)JSONForCommand:(NSString *)command data:(NSObject *)data; + + +#ifdef DEBUG ++ (void)debugInject; ++ (void)debugSendPlay; ++ (void)debugCopyJS; ++ (void)debugCopyAS; +#endif + +@end diff --git a/VKPC/Controller.m b/VKPC/Controller.m new file mode 100644 index 0000000..825aa6f --- /dev/null +++ b/VKPC/Controller.m @@ -0,0 +1,345 @@ +// +// Controller.m +// VKPC +// +// Created by Eugene on 10/23/14. +// Copyright (c) 2014 Eugene Z. All rights reserved. +// + +#import "Controller.h" +#import "PopoverController.h" +#import "Server.h" + +static NSString * const kASExecuteJSChrome = @"execute javascript"; +static NSString * const kASExecuteJSSafari = @"do JavaScript"; +static NSString * const kASCurrentTabChrome = @"active tab"; +static NSString * const kASCurrentTabSafari = @"current tab"; +static NSString * const kASTabTitleChrome = @"title of"; +static NSString * const kASTabTitleSafari = @"name of"; + +static NSString * const kCommandAfterInjection = @"afterInjection"; +static NSString * const kCommandPlayPause = @"playpause"; +static NSString * const kCommandPrev = @"prev"; +static NSString * const kCommandNext = @"next"; +static NSString * const kCommandOperateTrack = @"operateTrack:{id}"; + +static NSArray *browsers = nil; +static NSString *scriptJS; +#ifdef DEBUG +static NSString *scriptJSUnescaped; +#endif +static NSString *scriptAS; +static NSMutableDictionary *cache = nil; +static NSTimer *timer = nil; +static NSInteger browser; +static BOOL initialized = NO; + +@implementation Controller + ++ (void)initialize { + if (initialized) { + return; + } + + browsers = @[ + @[@{@"id": @"com.google.Chrome", @"name": @"Google Chrome", @"key": @"chrome"}, @{@"id": @"com.google.Chrome.canary", @"name": @"Google Chrome Canary", @"key": @"chromecanary"}], + @[@{@"id": @"org.mozilla.firefox", @"name": @"Firefox", @"key": @"firefox"}], + @[@{@"id": @"com.apple.Safari", @"name": @"Safari", @"key": @"safari"}], + @[@{@"id": @"com.operasoftware.Opera", @"name": @"Opera", @"key": @"opera"}, @{@"id": @"com.operasoftware.OperaNext", @"name": @"Opera Next", @"key": @"operanext"}], + @[@{@"id": @"ru.yandex.desktop.yandex-browser", @"name": @"Yandex", @"key": @"yandex"}] + ]; + NSError *error = nil; + + scriptJS = GetFileFromResourceAsString(@"inject.js", &error); + scriptAS = GetFileFromResourceAsString(@"inject.as", &error); + + if (error) { + NSLog(@"Error while reading from resources: %@", error); + // TODO something + return; + } + + scriptJS = [scriptJS stringByReplacingOccurrencesOfString:@"{sid}" withString:[NSString stringWithFormat:@"%d", VKPCSessionID]]; +// scriptJS = [scriptJS stringByReplacingOccurrencesOfString:@"{debug}" withString:(VKPCIsDebug ? @"true" : @"false")]; + +#ifdef DEBUG + scriptJSUnescaped = [NSString stringWithString:scriptJS]; +#endif + + scriptJS = [scriptJS stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"]; + scriptJS = [scriptJS stringByReplacingOccurrencesOfString:@"\"" withString:@"\\\""]; + + cache = [[NSMutableDictionary alloc] init]; + +// if (NO) + [self setupTimer]; + + browser = [[NSUserDefaults standardUserDefaults] integerForKey:VKPCPreferencesBrowser]; + + [[NSUserDefaults standardUserDefaults] addObserver:(id)[Controller class] + forKeyPath:VKPCPreferencesBrowser + options:NSKeyValueObservingOptionNew + context:NULL]; + + initialized = YES; +} + ++ (void)setupTimer { + if (timer != nil) { + [timer invalidate]; + timer = nil; + } + + [self timerCallback:nil]; + timer = [NSTimer scheduledTimerWithTimeInterval:2.0 + target:[Controller class] + selector:@selector(timerCallback:) + userInfo:nil + repeats:YES]; +} + +#ifdef DEBUG ++ (void)debugSendPlay { + [Controller playpause]; +} + ++ (void)debugInject { + [Controller sendCommand:kCommandAfterInjection]; +} + ++ (void)debugCopyJS { + NSPasteboard *pasteBoard = [NSPasteboard generalPasteboard]; + [pasteBoard declareTypes:[NSArray arrayWithObjects:NSStringPboardType, nil] owner:nil]; + [pasteBoard setString:scriptJSUnescaped forType:NSStringPboardType]; +} + ++ (void)debugCopyAS { + NSString *code = [self findRunningAppAndPrepareASForCommand:kCommandAfterInjection]; + if (code != nil) { + NSPasteboard *pasteBoard = [NSPasteboard generalPasteboard]; + [pasteBoard declareTypes:[NSArray arrayWithObjects:NSStringPboardType, nil] owner:nil]; + [pasteBoard setString:code forType:NSStringPboardType]; + } else { + NSLog(@"[Controller debugCopyAS] code == nil"); + } +} +#endif + +//static BOOL playlistDelegateSet = NO; ++ (void)timerCallback:(NSTimer *)timer { + if ([[PopoverController shared] playlistTableController] != nil && [[PopoverController shared] playlistTableController].playlist.delegate == nil) { + [[PopoverController shared].playlistTableController.playlist setDelegate:(id)[self class]]; + NSLog(@"[Controller timerCallback] playlist delegate set"); +// playlistDelegateSet = YES; + } + + if ([self isASBrowser:browser]) { + [Controller sendCommand:kCommandAfterInjection]; + } else if ([Server connectedCount:browser] <= 0) { + [[PopoverController shared].playlistTableController clearPlaylist]; + } +} + ++ (void)prev { + [Controller sendCommand:kCommandPrev]; +} + ++ (void)next { + [Controller sendCommand:kCommandNext]; +} + ++ (void)playpause { + [Controller sendCommand:kCommandPlayPause]; +} + ++ (void)operateTrack:(NSString *)trackID { + [Controller sendCommand:[kCommandOperateTrack stringByReplacingOccurrencesOfString:@"{id}" withString:trackID]]; +} + ++ (void)sendCommand:(NSString *)command { + if ([self isASBrowser:browser]) { + NSString *code = [self findRunningAppAndPrepareASForCommand:command]; + if (code == nil) { + // NSLog(@"[Controller sendCommand:] code == nil, returning"); + // Clear playlist? + [[PopoverController shared].playlistTableController clearPlaylist]; + return; + } + + NSAppleScript *as = [[NSAppleScript alloc] initWithSource:code]; + NSDictionary *error = nil; + NSAppleEventDescriptor *result = [as executeAndReturnError:&error]; + + if (error) { + NSLog(@"[Controller sendCommand:] error: %@", error); + } else if ([command isEqualToString:kCommandAfterInjection]) { + int returnValue = 0; + [result.data getBytes:&returnValue length:result.data.length]; + // NSLog(@"[Controller sendCommand:] returnValue = %d", returnValue); + if (returnValue == 1) { + [[PopoverController shared].playlistTableController clearPlaylist]; + } + } + } else { + if ([Server connectedCount:browser] <= 0) { + [[PopoverController shared].playlistTableController clearPlaylist]; + return; + } + + // Send to extensions + [Server send:[self JSONForCommand:@"vkpc" data:command] forBrowser:browser]; + } +} + ++ (NSString *)findRunningAppAndPrepareASForCommand:(NSString *)command { + NSArray *list = browsers[browser]; + NSArray *apps = [[NSWorkspace sharedWorkspace] runningApplications]; + NSDictionary *app; + + BOOL found = NO; + for (int i = 0; i < apps.count; i++) { + NSRunningApplication *currentApp = apps[i]; + + for (NSDictionary *dict in list) { + if ([currentApp.bundleIdentifier isEqualToString:dict[@"id"]]) { + app = dict; + found = YES; + break; + } + } + + if (found) + break; + } + + if (!found) { +// NSLog(@"[Controller findRunningAppAndPrepareASForCommand:] %@ not found in running applications, nil will returned", (NSString *)browsers[browser][0][@"name"]); + return nil; + } + + NSString *as = scriptAS; + NSInteger playlistID = [PopoverController shared].playlistTableController.playlist.playlistID; + + NSString *ASExecuteJS, *ASCurrentTab, *ASTabTitle; + if (browser == BrowserSafari) { + ASExecuteJS = kASExecuteJSSafari; + ASCurrentTab = kASCurrentTabSafari; + ASTabTitle = kASTabTitleSafari; + } else { + ASExecuteJS = kASExecuteJSChrome; + ASCurrentTab = kASCurrentTabChrome; + ASTabTitle = kASTabTitleChrome; + } + + as = [as stringByReplacingOccurrencesOfString:@"{appName}" withString:app[@"name"]]; + as = [as stringByReplacingOccurrencesOfString:@"{js}" withString:scriptJS]; + as = [as stringByReplacingOccurrencesOfString:@"{ASExecuteJS}" withString:ASExecuteJS]; + as = [as stringByReplacingOccurrencesOfString:@"{ASCurrentTab}" withString:ASCurrentTab]; + as = [as stringByReplacingOccurrencesOfString:@"{ASTabTitle}" withString:ASTabTitle]; + + as = [as stringByReplacingOccurrencesOfString:@"{playlistID}" withString:[NSString stringWithFormat:@"%ld", playlistID]]; + as = [as stringByReplacingOccurrencesOfString:@"{command}" withString:command]; + + return as; +} + ++ (void)handleClient:(NSDictionary *)json { +// NSLog(@"[Controller handleClient] json: %@", json); + + NSInteger fromBrowser = [(NSNumber *)json[@"_browser"] integerValue]; + if (fromBrowser != browser) { +// NSLog(@"[Controller handleClient] received message from browser=%zd, but current browser=%zd, skipping", fromBrowser, browser); + return; + } + + NSString *command = json[@"command"]; + NSDictionary *data = json[@"data"]; + if (!command || [command isEqual:[NSNull null]]) { + NSLog(@"[Controller handleCommand] !json"); + return; + } + + if ([command isEqualToString:@"updatePlaylist"]) { + NSArray *tracks = data[@"tracks"]; + NSInteger playlistId = [(NSNumber *)data[@"id"] intValue]; + NSString *title = data[@"title"]; + NSDictionary *active = data[@"active"]; + NSString *browser = data[@"browser"]; + + NSString *activeStatus = active[@"status"]; + NSString *activeId = active[@"id"]; + BOOL playingStatus = ( activeStatus && ![activeStatus isEqual:[NSNull null]] && [activeStatus isEqualToString:@"play"] ) ? YES : NO; + + NSLog(@"[server] got updatePlaylist; id=%ld, activeId=%@, activeStatus=%@, browser=%@, title=%@", + playlistId, (NSString *)active[@"id"], active[@"status"], browser, title); + + if ([[PopoverController shared].playlistTableController inited]) { + NSLog(@"[Controller handleClient] call setPlaylist.."); + [[PopoverController shared].playlistTableController setPlaylistDataWithTracks:tracks title:title id:playlistId activeId:activeId activePlaying:playingStatus browser:browser]; + } else { + NSLog(@"[Controller handleClient] call preSetPlaylist.."); + [PlaylistTableController preSetPlaylistDataWithTracks:tracks title:title id:playlistId activeId:activeId activePlaying:playingStatus browser:browser]; + } + } else if ([command isEqualToString:@"operateTrack"]) { + NSString *trackId = data[@"id"]; + NSString *status = data[@"status"]; + NSInteger playlistId = [(NSNumber *)data[@"playlistId"] intValue]; + + NSLog(@"[server] got operateTrack; trackId=%@, status=%@, plId=%ld", + trackId, status, playlistId); + + PlayingStatus playingStatus = (status && ![status isEqual:[NSNull null]] && [status isEqualToString:@"play"]) ? PlayingStatusPlaying : PlayingStatusPaused; + [[PopoverController shared].playlistTableController setPlayingTrackById:trackId withStatus:playingStatus forPlaylist:playlistId]; + } else if ([command isEqualToString:@"clearPlaylist"]) { + [[PopoverController shared].playlistTableController clearPlaylist]; + } +} + +// PlaylistDeletage ++ (void)playlistIDChanged:(NSInteger)playlistID { +// NSLog(@"playlist id changed! new id: %zd", playlistID); + if (initialized) { +// NSLog(@"now send new playlist id to clients"); + [Server send:[self JSONForCommand:@"set_playlist_id" data:[NSNumber numberWithInteger:playlistID]] forBrowser:-1]; + } +} + +// KVO ++ (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { + if ([keyPath isEqualToString:VKPCPreferencesBrowser]) { + NSNumber *new = change[NSKeyValueChangeKindKey]; + if ([new integerValue] == NSKeyValueChangeSetting) { + NSInteger value = [(NSNumber *)change[NSKeyValueChangeNewKey] integerValue]; + if (browser != value) { + NSLog(@"[Controller KVO] new browser is %@", browsers[value][0][@"name"]); + browser = value; + [cache removeAllObjects]; + [[PopoverController shared].playlistTableController clearPlaylist]; + [self setupTimer]; + } + } + } +} + +// Other ++ (BOOL)isASBrowser:(NSInteger)browser { + return ![[NSUserDefaults standardUserDefaults] boolForKey:VKPCPreferencesUseExtensionMode] && ( + browser == BrowserChrome + || browser == BrowserYandex + || browser == BrowserSafari ); +} + ++ (NSString *)JSONForCommand:(NSString *)command data:(NSObject *)data { + NSDictionary *dict = @{@"command": command, @"data": data}; + NSError *error; + NSData *json = [NSJSONSerialization dataWithJSONObject:dict options:(NSJSONWritingOptions)0 error:&error]; + + if (!json) { + NSLog(@"[Controller JSONForCommand] error: %@", error.localizedDescription); + return @"{}"; + } else { + return [[NSString alloc] initWithData:json encoding:NSUTF8StringEncoding]; + } +} + +@end diff --git a/VKPC/FlippedView.h b/VKPC/FlippedView.h new file mode 100644 index 0000000..77c871a --- /dev/null +++ b/VKPC/FlippedView.h @@ -0,0 +1,15 @@ +// +// PopoverView.h +// VKPC +// +// Created by Eugene on 12/1/13. +// Copyright (c) 2013 Eugene Z. All rights reserved. +// + +#import <Cocoa/Cocoa.h> + +@interface FlippedView : NSView + +- (BOOL)isFlipped; + +@end diff --git a/VKPC/FlippedView.m b/VKPC/FlippedView.m new file mode 100644 index 0000000..bdf5972 --- /dev/null +++ b/VKPC/FlippedView.m @@ -0,0 +1,31 @@ +// +// PopoverView.m +// VKPC +// +// Created by Eugene on 12/1/13. +// Copyright (c) 2013 Eugene Z. All rights reserved. +// + +#import "FlippedView.h" + +@implementation FlippedView + +- (id)initWithFrame:(NSRect)frame { + self = [super initWithFrame:frame]; + if (self) { + // Initialization code here. + } + return self; +} + +- (void)drawRect:(NSRect)dirtyRect { + [super drawRect:dirtyRect]; + + // Drawing code here. +} + +- (BOOL)isFlipped { + return YES; +} + +@end diff --git a/VKPC/Global.h b/VKPC/Global.h new file mode 100644 index 0000000..d1f4ce2 --- /dev/null +++ b/VKPC/Global.h @@ -0,0 +1,72 @@ +// +// global.h +// VKPC +// +// Created by Eugene on 11/28/13. +// Copyright (c) 2013-2014 Eugene Z. All rights reserved. +// + +#import "PlaylistTableController.h" + +// Variables +extern int const VKPCHTTPServerPort; +extern NSString * const VKPCHTTPServerHost; + +extern int const VKPCWSServerPort; +extern char * const VKPCWSServerHost; +extern char * const VKPCWSClientHost; +//extern char * const VKPCHostsFile; + +extern NSString * const VKPCAppHomeURL; +extern NSString * const CH1PEmail; + +extern BOOL const VKPCIsDebug; +extern BOOL const VKPCIsServerLogsEnabled; +extern BOOL VKPCIsYosemite; + +extern NSString * const VKPCEZCopyright; +extern NSString * const VKPCEZCopyrightYears; +extern NSString * const VKPCEZURL; + +extern NSString * const VKPCPreferencesShowNotifications; +extern NSString * const VKPCPreferencesInvertPlaylistIcons; +extern NSString * const VKPCPreferencesCatchMediaButtons; +extern NSString * const VKPCPreferencesBrowser; +extern NSString * const VKPCPreferencesStatisticReportedTimestamp; +extern NSString * const VKPCPreferencesUUID; +extern NSString * const VKPCPreferencesUseExtensionMode; + +extern int VKPCSessionID; +//extern PlaylistTableController *VKPCPlaylistTableController; +extern pid_t VKPCPID; + +extern NSString * const VKPCImageEmpty; +extern NSString * const VKPCImageCellBg; +extern NSString * const VKPCImageCellPressedBg; +extern NSString * const VKPCImagePause; +extern NSString * const VKPCImagePlay; +extern NSString * const VKPCImageTitleSeparator; +extern NSString * const VKPCImageSettings; +extern NSString * const VKPCImageSettingsPressed; +extern NSString * const VKPCImageStatus; +extern NSString * const VKPCImageStatusPressed; + +extern NSString * const kAppleInterfaceStyle; +extern NSString * const kAppleInterfaceStyleDark; +extern NSString * const kAppleInterfaceThemeChangedNotification; +extern NSString * const kCFBundleDisplayName; +extern NSString * const kCFBundleShortVersionString; +extern NSString * const kCFBundleVersion; + +// Functions +void VKPCInitGlobals(); +void VKPCInitUUID(); +void ShowNotification(); +NSString * GetFileFromResourceAsString(NSString *fileName, NSError * __autoreleasing *error); +NSString *GetSystemFontName(); +//BOOL IsDarkMode(); +InterfaceStyle GetInterfaceStyle(); +NSDictionary * VKPCGetImagesDictionary(); +void DebugLog(const char *str); +long GetTimestamp(); +BOOL IsAnotherProcessRunning();
\ No newline at end of file diff --git a/VKPC/Global.m b/VKPC/Global.m new file mode 100644 index 0000000..4f5f145 --- /dev/null +++ b/VKPC/Global.m @@ -0,0 +1,234 @@ +// +// global.m +// VKPC +// +// Created by Eugene on 11/28/13. +// Copyright (c) 2013-2014 Eugene Z. All rights reserved. +// + +#import "Global.h" +#import <CoreServices/CoreServices.h> +#import "NSTimer+Blocks.h" +#import "NSUserNotificationCenter+Private.h" + +#include <stdlib.h> +#include <math.h> + +int const VKPCWSServerPort = 56130; +char * const VKPCWSServerHost = "127.0.0.1"; +char * const VKPCWSClientHost = "vkpc-local.ch1p.com"; +//char * const VKPCHostsFile = "/private/etc/hosts"; + +NSString * const VKPCAppHomeURL = @"https://ch1p.com/vkpc/?v={version}"; +NSString * const CH1PEmail = @"ch1p@ch1p.com"; + +#ifdef DEBUG +BOOL const VKPCIsDebug = YES; +#else +BOOL const VKPCIsDebug = NO; +#endif + +BOOL const VKPCIsServerLogsEnabled = NO; +BOOL VKPCIsYosemite = NO; + +NSString * const VKPCEZCopyright = @"Eugene Z"; +NSString * const VKPCEZCopyrightYears = @" © 2013-2015"; +NSString * const VKPCEZURL = @"https://vk.com/ez"; + +NSString * const VKPCPreferencesShowNotifications = @"VKPCShowNotifications"; +NSString * const VKPCPreferencesInvertPlaylistIcons = @"VKPCInvertPlaylistIcons"; +NSString * const VKPCPreferencesCatchMediaButtons = @"VKPCCatchMediaButtons"; +NSString * const VKPCPreferencesBrowser = @"VKPCBrowser"; +NSString * const VKPCPreferencesStatisticReportedTimestamp = @"VKPCStatisticReportedTimestamp"; +NSString * const VKPCPreferencesUUID = @"VKPCUUID"; +NSString * const VKPCPreferencesUseExtensionMode = @"VKPCUseExtensionMode"; + +NSString * const kAppleInterfaceStyle = @"AppleInterfaceStyle"; +NSString * const kAppleInterfaceThemeChangedNotification = @"AppleInterfaceThemeChangedNotification"; +NSString * const kAppleInterfaceStyleDark = @"Dark"; +NSString * const kCFBundleDisplayName = @"CFBundleDisplayName"; +NSString * const kCFBundleShortVersionString = @"CFBundleShortVersionString"; +NSString * const kCFBundleVersion = @"CFBundleVersion"; + +int VKPCSessionID; +pid_t VKPCPID; + +void VKPCInitGlobals() { + SInt32 major, minor; + Gestalt(gestaltSystemVersionMajor, &major); + Gestalt(gestaltSystemVersionMinor, &minor); + + VKPCIsYosemite = major >= 10 && minor >= 10; + VKPCSessionID = arc4random() % 65536; + VKPCPID = [[NSProcessInfo processInfo] processIdentifier]; +} + +void VKPCInitUUID() { + NSString *currentUUID = [[NSUserDefaults standardUserDefaults] stringForKey:VKPCPreferencesUUID]; + if ([currentUUID isEqualToString:@""]) { + CFUUIDRef uuid = CFUUIDCreate(kCFAllocatorDefault); + NSString *uuidString = (__bridge_transfer NSString *)CFUUIDCreateString(kCFAllocatorDefault, uuid); + CFRelease(uuid); + + [[NSUserDefaults standardUserDefaults] setObject:uuidString forKey:VKPCPreferencesUUID]; + } +} + +static NSUserNotification *lastNotification; +static BOOL isLowerThan10_9() { + SInt32 major, minor; + Gestalt(gestaltSystemVersionMajor, &major); + Gestalt(gestaltSystemVersionMinor, &minor); + + return major == 10 && minor < 9; +} +static void removeNotification(NSUserNotification *notification) { +// if (isLowerThan10_9()) { + [[NSUserNotificationCenter defaultUserNotificationCenter] _removeDisplayedNotification:notification]; +// } else { +// [[NSUserNotificationCenter defaultUserNotificationCenter] removeDeliveredNotification:notification]; +// } +} +void ShowNotification(NSString *title, NSString *text) { + NSUserNotification *notification = [[NSUserNotification alloc] init]; + [notification setTitle:title]; + [notification setInformativeText:text]; + [notification setHasActionButton:NO]; + + if (lastNotification != nil) { + removeNotification(lastNotification); + } + + [[NSUserNotificationCenter defaultUserNotificationCenter] deliverNotification:notification]; + [NSTimer scheduledTimerWithTimeInterval:4.0 block:^{ + removeNotification(notification); + } repeats:NO]; + + lastNotification = notification; +} + +NSString * GetFileFromResourceAsString(NSString *fileName, NSError * __autoreleasing * error) { + NSString *path = [[fileName lastPathComponent] stringByDeletingPathExtension]; + NSString *type = [fileName pathExtension]; + NSError *localError = nil; + + NSString *resPath = [[NSBundle mainBundle] pathForResource:path ofType:type]; + NSURL *url = [NSURL fileURLWithPath:resPath]; + NSString *content = nil; + content = [[NSString alloc] + initWithContentsOfURL:url + encoding:NSUTF8StringEncoding + error:&localError]; + + if (localError || content == nil) { + *error = localError; + NSLog(@"Error reading file %@\n%@", url, [localError localizedFailureReason]); + } + + return content; +} + +NSString * GetSystemFontName() { + return VKPCIsYosemite ? @"Helvetica Neue" : @"Lucida Grande"; +} + +InterfaceStyle GetInterfaceStyle() { + if (VKPCIsYosemite) { + NSString *theme = [[NSUserDefaults standardUserDefaults] stringForKey:kAppleInterfaceStyle]; + if (theme != nil && [theme isKindOfClass:[NSString class]] && [theme isEqualToString:kAppleInterfaceStyleDark]) { + return InterfaceStyleYosemiteDark; + } + return InterfaceStyleYosemite; + } + return InterfaceStyleLegacy; +} + +long GetTimestamp() { + return (long)floor([[NSDate date] timeIntervalSince1970]); +} + +BOOL IsAnotherProcessRunning() { + NSArray *list = [[NSWorkspace sharedWorkspace] runningApplications]; + for (int i = 0; i < list.count; i++) { + NSRunningApplication *app = list[i]; + if ([[app bundleIdentifier] isEqualToString:[[NSBundle mainBundle] bundleIdentifier]] + && [app processIdentifier] != VKPCPID) { + return YES; + } + } + return NO; +} + +void DebugLog(const char *str) { +// printf("><> %s\n", str); +} + +//////////////////////////////////// Images //////////////////////////////////// + +NSString * const VKPCImageEmpty = @"empty"; +NSString * const VKPCImageCellBg = @"pl_cell_bg"; +NSString * const VKPCImageCellPressedBg = @"pl_cell_pressed_bg"; +NSString * const VKPCImagePause = @"pl_pause"; +NSString * const VKPCImagePlay = @"pl_play"; +NSString * const VKPCImageTitleSeparator = @"pl_title_separator"; +NSString * const VKPCImageSettings = @"settings"; +NSString * const VKPCImageSettingsPressed = @"settings_pressed"; +NSString * const VKPCImageStatus = @"status"; +NSString * const VKPCImageStatusPressed = @"status_pressed"; + +static NSString * const kImagesBundleLegacy = @"ImagesLegacy"; +static NSString * const kImagesBundleYosemite = @"ImagesYosemite"; +static NSString * const kImagesBundleYosemiteDark = @"ImagesYosemiteDark"; + +static BOOL imagesInited = NO; +static NSArray *imageNames; +static NSMutableDictionary *imageBundles; // @{<bundleName>: NSBundle} +static NSMutableDictionary *allImages; // @{<bundleName>: @{<imageName>: NSImage, ...}} + +NSDictionary * VKPCGetImagesDictionary() { + if (!imagesInited) { + allImages = [[NSMutableDictionary alloc] init]; + imageBundles = [[NSMutableDictionary alloc] init]; + + imageNames = @[VKPCImageEmpty, VKPCImageCellBg, VKPCImageCellPressedBg, + VKPCImagePause, VKPCImagePlay, VKPCImageTitleSeparator, VKPCImageSettings, + VKPCImageSettingsPressed, VKPCImageStatus, VKPCImageStatusPressed]; + + // Loading bundles + NSArray *bundlePaths = [[NSBundle mainBundle] pathsForResourcesOfType:@"bundle" inDirectory:@""]; + for (NSString *bundlePath in bundlePaths) { + NSString *bundleKey = [[bundlePath lastPathComponent] stringByDeletingPathExtension]; + if ([bundleKey hasPrefix:@"Images"]) { + imageBundles[bundleKey] = [NSBundle bundleWithPath:bundlePath]; + } + } + + imagesInited = YES; + } + + NSString *bundleKey; + switch (GetInterfaceStyle()) { + case InterfaceStyleYosemite: + bundleKey = kImagesBundleYosemite; + break; + case InterfaceStyleLegacy: + bundleKey = kImagesBundleLegacy; + break; + case InterfaceStyleYosemiteDark: + bundleKey = kImagesBundleYosemiteDark; + break; + } + + if (allImages[bundleKey] != nil) { +// NSLog(@"[VKPCGetImagesDictionary] returning from cache, bundleKey = %@", bundleKey); + return (NSDictionary *)allImages[bundleKey]; + } + + allImages[bundleKey] = [[NSMutableDictionary alloc] init]; + for (NSString *named in imageNames) { + NSImage *img = [(NSBundle *)imageBundles[bundleKey] imageForResource:named]; + allImages[bundleKey][named] = img; + } + + return (NSDictionary *)allImages[bundleKey]; +}
\ No newline at end of file diff --git a/VKPC/HostsHack.h b/VKPC/HostsHack.h new file mode 100644 index 0000000..4271aa0 --- /dev/null +++ b/VKPC/HostsHack.h @@ -0,0 +1,21 @@ +// +// HostsHack.h +// VKPC +// +// Created by Eugene on 10/30/14. +// Copyright (c) 2014 Eugene Z. All rights reserved. +// + +#import <Foundation/Foundation.h> + +extern NSString * const VKPCHostsHackTaskFinished; + +@interface HostsHack : NSObject + ++ (void)check; ++ (void)hack; ++ (BOOL)found; ++ (void)showWindow; ++ (int)doHack; + +@end diff --git a/VKPC/HostsHack.m b/VKPC/HostsHack.m new file mode 100644 index 0000000..89f0752 --- /dev/null +++ b/VKPC/HostsHack.m @@ -0,0 +1,163 @@ +// +// HostsHack.m +// VKPC +// +// Created by Eugene on 10/30/14. +// Copyright (c) 2014 Eugene Z. All rights reserved. +// + +#import "HostsHack.h" +#import "HostsHackWindowController.h" +#import "AppDelegate.h" + +#include <stdio.h> + +#ifdef DEBUG +#include <time.h> +#endif + +NSString * const VKPCHostsHackTaskFinished = @"VKPCHostsHackTaskFinished"; + +static BOOL hackFound = NO; +static HostsHackWindowController *windowController = nil; +#ifdef DEBUG +static char *testPath = "/tmp/vkpc_test"; +#endif + +@implementation HostsHack + +static NSString *readLineASNSString(FILE *file) { + char *line = NULL; + size_t len = 0; + getline(&line, &len, file); + return [NSString stringWithUTF8String:line]; +} + ++ (void)check { + hackFound = NO; + +#ifdef DEBUG + clock_t begin = clock(); +#endif + + FILE *file = fopen(VKPCHostsFile, "r"); + if (file == NULL) { + NSLog(@"[HostsHack check] !file, returning"); + return; + } + + while (!feof(file)) { + NSString *line = readLineASNSString(file); +// NSLog(@"[hostshack] line: %@", line); + NSRange rng = [line rangeOfString:[NSString stringWithUTF8String:VKPCWSClientHost]]; + if (rng.location != NSNotFound) { +// if ([line containsString:[NSString stringWithUTF8String:VKPCWSClientHost]]) { + line = [line stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + if ([line hasPrefix:@"127.0.0.1"]) { + hackFound = YES; + break; + } + } + } + fclose(file); + +#ifdef DEBUG + NSLog(@"[HostsHack check] file reading time: %lf", (double)(clock() - begin) / CLOCKS_PER_SEC); + NSLog(@"[HostsHack check] found: %s", hackFound ? "YES" : "NO"); +#endif +} + +static void showAlert(NSString *text, NSString *informativeText) { + NSAlert *alert = [[NSAlert alloc] init]; + [alert setMessageText:text]; + [alert setInformativeText:informativeText]; + [alert setAlertStyle:NSWarningAlertStyle]; + [alert runModal]; +} + ++ (void)hack { +// return; + + AuthorizationRef auth = NULL; + OSStatus err; + + err = AuthorizationCreate(NULL, kAuthorizationEmptyEnvironment, kAuthorizationFlagInteractionAllowed, &auth); + if (err != errAuthorizationSuccess) { + showAlert(@"VKPC Error", [NSString stringWithFormat:@"Failed to obtain authorization. Code = %d", err]); + [windowController setButtonRetry]; + return; + } + + const char *path = [[NSProcessInfo processInfo].arguments[0] UTF8String]; + char * const args[] = {"--hostshack", NULL}; + + [windowController setButtonWait]; + + err = AuthorizationExecuteWithPrivileges(auth, path, kAuthorizationFlagDefaults, args, NULL); + if (err != errAuthorizationSuccess) { + showAlert(@"VKPC Error", [NSString stringWithFormat:@"Failed to run command with adminstrative privileges. Code = %d", err]); + [windowController setButtonRetry]; + return; + } + + [[NSDistributedNotificationCenter defaultCenter] addObserver:(id)[self class] + selector:@selector(hackTaskFinished:) + name:VKPCHostsHackTaskFinished + object:nil]; +} + ++ (void)hackTaskFinished:(id)notification { + [[NSDistributedNotificationCenter defaultCenter] removeObserver:(id)[self class]]; + + [self check]; + + if (hackFound) { + [windowController close]; + [[AppDelegate shared] continueRunning]; + } else { + [self showUnableAlert]; + } +} + ++ (void)showUnableAlert { + showAlert(@"VKPC Error", [NSString stringWithFormat: + @"Unfortunately, VKPC failed to automatically edit the file %@. Now you have to make it manually.\n\n" + "Please open the file %@ with root privileges and add following line at the end:\n\n" + "127.0.0.1\t%@\n\n" + "Then save the file and relaunch the app.", + [NSString stringWithUTF8String:VKPCHostsFile], + [NSString stringWithUTF8String:VKPCHostsFile], + [NSString stringWithUTF8String:VKPCWSClientHost]]); +} + ++ (int)doHack { +// sleep(2); + + char *path = VKPCHostsFile; + FILE *file = fopen(path, "a"); + if (!file) { + NSLog(@"[HostsHack doHack] error opening file %s, returning error", path); + return -1; + } + + fputs("\n#VK Player Controller", file); + fputs([[NSString stringWithFormat:@"\n127.0.0.1\t%@", [NSString stringWithUTF8String:VKPCWSClientHost]] UTF8String], file); + + fclose(file); + return 0; +} + ++ (BOOL)found { + return hackFound; +} + ++ (void)showWindow { + if (!windowController) { + windowController = [[HostsHackWindowController alloc] initWithWindowNibName:@"HostsHackWindow"]; + } + [windowController showWindow:nil]; + [windowController.window makeKeyAndOrderFront:nil]; + [windowController.window setLevel:kCGFloatingWindowLevel]; +} + +@end diff --git a/VKPC/HostsHackWindow.xib b/VKPC/HostsHackWindow.xib new file mode 100644 index 0000000..d083fdb --- /dev/null +++ b/VKPC/HostsHackWindow.xib @@ -0,0 +1,50 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="6250" systemVersion="14A388a" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none"> + <dependencies> + <deployment identifier="macosx"/> + <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="6250"/> + </dependencies> + <objects> + <customObject id="-2" userLabel="File's Owner" customClass="HostsHackWindowController"> + <connections> + <outlet property="button" destination="DkI-Wh-sfI" id="nZQ-ER-OSv"/> + <outlet property="configurationRequiredTextField" destination="LPj-KT-VLz" id="697-Qc-LGh"/> + <outlet property="window" destination="QvC-M9-y7g" id="Ip1-1G-tLg"/> + </connections> + </customObject> + <customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/> + <customObject id="-3" userLabel="Application" customClass="NSObject"/> + <window allowsToolTipsWhenApplicationIsInactive="NO" oneShot="NO" releasedWhenClosed="NO" showsToolbarButton="NO" visibleAtLaunch="NO" animationBehavior="default" id="QvC-M9-y7g"> + <windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES"/> + <rect key="contentRect" x="480" y="296" width="480" height="270"/> + <rect key="screenRect" x="0.0" y="0.0" width="1440" height="877"/> + <view key="contentView" id="EiT-Mj-1SZ"> + <rect key="frame" x="0.0" y="0.0" width="480" height="270"/> + <autoresizingMask key="autoresizingMask"/> + <subviews> + <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" setsMaxLayoutWidthAtFirstLayout="YES" id="LPj-KT-VLz"> + <rect key="frame" x="18" y="175" width="444" height="80"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> + <textFieldCell key="cell" allowsUndo="NO" sendsActionOnEndEditing="YES" alignment="justified" title="bla bla" allowsEditingTextAttributes="YES" id="hi1-HP-gi7"> + <font key="font" metaFont="system"/> + <color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/> + </textFieldCell> + </textField> + <button verticalHuggingPriority="750" id="DkI-Wh-sfI"> + <rect key="frame" x="169" y="51" width="142" height="32"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> + <buttonCell key="cell" type="push" title="Continue" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="S5R-Td-Ee1"> + <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/> + <font key="font" metaFont="system"/> + </buttonCell> + <connections> + <action selector="buttonPressed:" target="-2" id="ynn-3H-jCh"/> + </connections> + </button> + </subviews> + </view> + <point key="canvasLocation" x="347" y="88"/> + </window> + </objects> +</document> diff --git a/VKPC/HostsHackWindowController.h b/VKPC/HostsHackWindowController.h new file mode 100644 index 0000000..e12d363 --- /dev/null +++ b/VKPC/HostsHackWindowController.h @@ -0,0 +1,26 @@ +// +// HostsHackWindowController.h +// VKPC +// +// Created by Eugene on 10/30/14. +// Copyright (c) 2014 Eugene Z. All rights reserved. +// + +#import <Cocoa/Cocoa.h> +#import "WindowController.h" +#import "FlippedView.h" + +//@class FlippedView; + +@interface HostsHackWindowController : WindowController<NSWindowDelegate> + +//@property (strong) IBOutlet NSWindow *window; +@property (weak) IBOutlet NSTextField *configurationRequiredTextField; +@property (weak) IBOutlet NSButton *button; + +- (IBAction)buttonPressed:(id)sender; +- (void)setButtonRetry; +- (void)setButtonContinue; +- (void)setButtonWait; + +@end diff --git a/VKPC/HostsHackWindowController.m b/VKPC/HostsHackWindowController.m new file mode 100644 index 0000000..17da248 --- /dev/null +++ b/VKPC/HostsHackWindowController.m @@ -0,0 +1,105 @@ +// +// HostsHackWindowController.m +// VKPC +// +// Created by Eugene on 10/30/14. +// Copyright (c) 2014 Eugene Z. All rights reserved. +// + +#import "HostsHackWindowController.h" +#import "HostsHack.h" + +// TODO rewrite using normal coords + +@implementation HostsHackWindowController { + NSMutableAttributedString *configurationRequired; + NSView *contentView; +} + +- (BOOL)allowsClosingWithShortcut { + return YES; +} + +- (void)windowDidLoad { + [super windowDidLoad]; + + contentView = self.window.contentView; + + NSTextFieldCell *cell = (NSTextFieldCell *)_configurationRequiredTextField.cell; + NSString *configurationTextHTML = [NSString stringWithFormat: + @"<html><span style=\"font-family: %@;\">" + "<span style=\"line-height: 10px; font-size: 14px\"><b>Welcome to VK Player Controller!</b></span>" + "<span style=\"font-size: 6px\"><br/><br/></span>" + "<span style=\"font-size: 13px\">" + "Let's make one magic trick with system DNS settings, it's necessary for a proper work of VK Player Controller. Don't worry, <b>it's absolutely safe</b>. Please press <b>Continue</b> button below." +// "For VK Player Controller to work it is necessary to do some hacking with DNS resolution configuration. Press <b>Continue</b> to continue." + "</span>" + "<span style=\"font-size: 7px\"><br/><br/></span>" + "<span style=\"font-size: 12px; color: #707070;\">" + "The app modifies the file <b>%@</b>. If you don't trust us, open that file manually with admin privileges, add this line: <b>127.0.0.1\t%@</b>, save it and relaunch the app." + "</span>" + "</span></html>", + //GetSystemFontName(), + @"Helvetica Neue", + [NSString stringWithUTF8String:VKPCHostsFile], + [NSString stringWithUTF8String:VKPCWSClientHost]]; + + configurationRequired = [[NSMutableAttributedString alloc] initWithHTML:[configurationTextHTML dataUsingEncoding:NSUTF8StringEncoding] + options:@{NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType, + NSCharacterEncodingDocumentAttribute: @(NSUTF8StringEncoding)} + documentAttributes:nil]; + + [_configurationRequiredTextField setAttributedStringValue:configurationRequired]; + [cell setWraps:YES]; + + // Position text + NSRect textFrame = _configurationRequiredTextField.frame; + float textHeight = [configurationRequired boundingRectWithSize:CGSizeMake(textFrame.size.width, FLT_MAX) options:NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading].size.height; + textFrame.origin.y += textFrame.size.height - textHeight; + textFrame.size.height = textHeight; + [_configurationRequiredTextField setFrame:textFrame]; + + // Position button + NSRect buttonFrame = _button.frame; + float padding = contentView.frame.size.height - (textFrame.origin.y + textFrame.size.height); + float buttonPadding = 20; + float buttonY = [self.window.contentView frame].size.height - textHeight - padding * 2 - buttonFrame.size.height; + buttonFrame.origin.y = buttonY; + [_button setFrame:buttonFrame]; + + float windowHeight = contentView.frame.size.height - buttonFrame.origin.y + padding + buttonPadding; + NSRect windowFrame = self.window.frame; + windowFrame.origin.y += windowFrame.size.height; + windowFrame.origin.y -= windowHeight; + windowFrame.size.height = windowHeight; + [self.window setFrame:windowFrame display:YES]; +} + +- (void)setButtonContinue { + [_button setTitle:@"Continue"]; + [_button setEnabled:YES]; +} + +- (void)setButtonRetry { + [_button setTitle:@"Retry"]; + [_button setEnabled:YES]; +} + +- (void)setButtonWait { + [_button setTitle:@"Please wait.."]; + [_button setEnabled:NO]; +} + +- (IBAction)buttonPressed:(id)sender { + [HostsHack hack]; +} + +- (void)showWindow:(id)sender { + [self setButtonContinue]; + + [super showWindow:sender]; + + [self.window setDefaultButtonCell:_button.cell]; +} + +@end diff --git a/VKPC/Images.xcassets/AppIcon-2.appiconset/Contents.json b/VKPC/Images.xcassets/AppIcon-2.appiconset/Contents.json new file mode 100644 index 0000000..50ab7bd --- /dev/null +++ b/VKPC/Images.xcassets/AppIcon-2.appiconset/Contents.json @@ -0,0 +1,58 @@ +{ + "images" : [ + { + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +}
\ No newline at end of file diff --git a/VKPC/ImagesLegacy.bundle/empty.png b/VKPC/ImagesLegacy.bundle/empty.png Binary files differnew file mode 100644 index 0000000..f38e9f9 --- /dev/null +++ b/VKPC/ImagesLegacy.bundle/empty.png diff --git a/VKPC/ImagesLegacy.bundle/empty@2x.png b/VKPC/ImagesLegacy.bundle/empty@2x.png Binary files differnew file mode 100644 index 0000000..f38e9f9 --- /dev/null +++ b/VKPC/ImagesLegacy.bundle/empty@2x.png diff --git a/VKPC/ImagesLegacy.bundle/pl_cell_bg.png b/VKPC/ImagesLegacy.bundle/pl_cell_bg.png Binary files differnew file mode 100644 index 0000000..8d3b407 --- /dev/null +++ b/VKPC/ImagesLegacy.bundle/pl_cell_bg.png diff --git a/VKPC/ImagesLegacy.bundle/pl_cell_bg@2x.png b/VKPC/ImagesLegacy.bundle/pl_cell_bg@2x.png Binary files differnew file mode 100644 index 0000000..6a14480 --- /dev/null +++ b/VKPC/ImagesLegacy.bundle/pl_cell_bg@2x.png diff --git a/VKPC/ImagesLegacy.bundle/pl_cell_pressed_bg.png b/VKPC/ImagesLegacy.bundle/pl_cell_pressed_bg.png Binary files differnew file mode 100644 index 0000000..9c8ef88 --- /dev/null +++ b/VKPC/ImagesLegacy.bundle/pl_cell_pressed_bg.png diff --git a/VKPC/ImagesLegacy.bundle/pl_cell_pressed_bg@2x.png b/VKPC/ImagesLegacy.bundle/pl_cell_pressed_bg@2x.png Binary files differnew file mode 100644 index 0000000..6c40513 --- /dev/null +++ b/VKPC/ImagesLegacy.bundle/pl_cell_pressed_bg@2x.png diff --git a/VKPC/ImagesLegacy.bundle/pl_pause.png b/VKPC/ImagesLegacy.bundle/pl_pause.png Binary files differnew file mode 100644 index 0000000..f195e4c --- /dev/null +++ b/VKPC/ImagesLegacy.bundle/pl_pause.png diff --git a/VKPC/ImagesLegacy.bundle/pl_pause@2x.png b/VKPC/ImagesLegacy.bundle/pl_pause@2x.png Binary files differnew file mode 100644 index 0000000..e21d55d --- /dev/null +++ b/VKPC/ImagesLegacy.bundle/pl_pause@2x.png diff --git a/VKPC/ImagesLegacy.bundle/pl_play.png b/VKPC/ImagesLegacy.bundle/pl_play.png Binary files differnew file mode 100644 index 0000000..6d280a6 --- /dev/null +++ b/VKPC/ImagesLegacy.bundle/pl_play.png diff --git a/VKPC/ImagesLegacy.bundle/pl_play@2x.png b/VKPC/ImagesLegacy.bundle/pl_play@2x.png Binary files differnew file mode 100644 index 0000000..9ba1986 --- /dev/null +++ b/VKPC/ImagesLegacy.bundle/pl_play@2x.png diff --git a/VKPC/ImagesLegacy.bundle/pl_title_separator.png b/VKPC/ImagesLegacy.bundle/pl_title_separator.png Binary files differnew file mode 100644 index 0000000..62cb211 --- /dev/null +++ b/VKPC/ImagesLegacy.bundle/pl_title_separator.png diff --git a/VKPC/ImagesLegacy.bundle/pl_title_separator@2x.png b/VKPC/ImagesLegacy.bundle/pl_title_separator@2x.png Binary files differnew file mode 100644 index 0000000..5f092a7 --- /dev/null +++ b/VKPC/ImagesLegacy.bundle/pl_title_separator@2x.png diff --git a/VKPC/ImagesLegacy.bundle/settings.png b/VKPC/ImagesLegacy.bundle/settings.png Binary files differnew file mode 100644 index 0000000..dfc3762 --- /dev/null +++ b/VKPC/ImagesLegacy.bundle/settings.png diff --git a/VKPC/ImagesLegacy.bundle/settings@2x.png b/VKPC/ImagesLegacy.bundle/settings@2x.png Binary files differnew file mode 100644 index 0000000..19e989b --- /dev/null +++ b/VKPC/ImagesLegacy.bundle/settings@2x.png diff --git a/VKPC/ImagesLegacy.bundle/settings_pressed.png b/VKPC/ImagesLegacy.bundle/settings_pressed.png Binary files differnew file mode 100644 index 0000000..02fa17a --- /dev/null +++ b/VKPC/ImagesLegacy.bundle/settings_pressed.png diff --git a/VKPC/ImagesLegacy.bundle/settings_pressed@2x.png b/VKPC/ImagesLegacy.bundle/settings_pressed@2x.png Binary files differnew file mode 100644 index 0000000..2180f6c --- /dev/null +++ b/VKPC/ImagesLegacy.bundle/settings_pressed@2x.png diff --git a/VKPC/ImagesLegacy.bundle/status.png b/VKPC/ImagesLegacy.bundle/status.png Binary files differnew file mode 100644 index 0000000..609b279 --- /dev/null +++ b/VKPC/ImagesLegacy.bundle/status.png diff --git a/VKPC/ImagesLegacy.bundle/status@2x.png b/VKPC/ImagesLegacy.bundle/status@2x.png Binary files differnew file mode 100644 index 0000000..5162663 --- /dev/null +++ b/VKPC/ImagesLegacy.bundle/status@2x.png diff --git a/VKPC/ImagesLegacy.bundle/status_pressed.png b/VKPC/ImagesLegacy.bundle/status_pressed.png Binary files differnew file mode 100644 index 0000000..4b02f10 --- /dev/null +++ b/VKPC/ImagesLegacy.bundle/status_pressed.png diff --git a/VKPC/ImagesLegacy.bundle/status_pressed@2x.png b/VKPC/ImagesLegacy.bundle/status_pressed@2x.png Binary files differnew file mode 100644 index 0000000..b6fcb71 --- /dev/null +++ b/VKPC/ImagesLegacy.bundle/status_pressed@2x.png diff --git a/VKPC/ImagesYosemite.bundle/empty.png b/VKPC/ImagesYosemite.bundle/empty.png Binary files differnew file mode 100644 index 0000000..f38e9f9 --- /dev/null +++ b/VKPC/ImagesYosemite.bundle/empty.png diff --git a/VKPC/ImagesYosemite.bundle/empty@2x.png b/VKPC/ImagesYosemite.bundle/empty@2x.png Binary files differnew file mode 100644 index 0000000..f38e9f9 --- /dev/null +++ b/VKPC/ImagesYosemite.bundle/empty@2x.png diff --git a/VKPC/ImagesYosemite.bundle/pl_cell_bg.png b/VKPC/ImagesYosemite.bundle/pl_cell_bg.png Binary files differnew file mode 100644 index 0000000..5cbd893 --- /dev/null +++ b/VKPC/ImagesYosemite.bundle/pl_cell_bg.png diff --git a/VKPC/ImagesYosemite.bundle/pl_cell_bg@2x.png b/VKPC/ImagesYosemite.bundle/pl_cell_bg@2x.png Binary files differnew file mode 100644 index 0000000..7417908 --- /dev/null +++ b/VKPC/ImagesYosemite.bundle/pl_cell_bg@2x.png diff --git a/VKPC/ImagesYosemite.bundle/pl_cell_pressed_bg.png b/VKPC/ImagesYosemite.bundle/pl_cell_pressed_bg.png Binary files differnew file mode 100644 index 0000000..9482124 --- /dev/null +++ b/VKPC/ImagesYosemite.bundle/pl_cell_pressed_bg.png diff --git a/VKPC/ImagesYosemite.bundle/pl_cell_pressed_bg@2x.png b/VKPC/ImagesYosemite.bundle/pl_cell_pressed_bg@2x.png Binary files differnew file mode 100644 index 0000000..502407e --- /dev/null +++ b/VKPC/ImagesYosemite.bundle/pl_cell_pressed_bg@2x.png diff --git a/VKPC/ImagesYosemite.bundle/pl_pause.png b/VKPC/ImagesYosemite.bundle/pl_pause.png Binary files differnew file mode 100644 index 0000000..73a00d9 --- /dev/null +++ b/VKPC/ImagesYosemite.bundle/pl_pause.png diff --git a/VKPC/ImagesYosemite.bundle/pl_pause@2x.png b/VKPC/ImagesYosemite.bundle/pl_pause@2x.png Binary files differnew file mode 100644 index 0000000..2cde740 --- /dev/null +++ b/VKPC/ImagesYosemite.bundle/pl_pause@2x.png diff --git a/VKPC/ImagesYosemite.bundle/pl_play.png b/VKPC/ImagesYosemite.bundle/pl_play.png Binary files differnew file mode 100644 index 0000000..422f589 --- /dev/null +++ b/VKPC/ImagesYosemite.bundle/pl_play.png diff --git a/VKPC/ImagesYosemite.bundle/pl_play@2x.png b/VKPC/ImagesYosemite.bundle/pl_play@2x.png Binary files differnew file mode 100644 index 0000000..2d45059 --- /dev/null +++ b/VKPC/ImagesYosemite.bundle/pl_play@2x.png diff --git a/VKPC/ImagesYosemite.bundle/pl_title_separator.png b/VKPC/ImagesYosemite.bundle/pl_title_separator.png Binary files differnew file mode 100644 index 0000000..2dc7720 --- /dev/null +++ b/VKPC/ImagesYosemite.bundle/pl_title_separator.png diff --git a/VKPC/ImagesYosemite.bundle/pl_title_separator@2x.png b/VKPC/ImagesYosemite.bundle/pl_title_separator@2x.png Binary files differnew file mode 100644 index 0000000..d9556a4 --- /dev/null +++ b/VKPC/ImagesYosemite.bundle/pl_title_separator@2x.png diff --git a/VKPC/ImagesYosemite.bundle/settings.png b/VKPC/ImagesYosemite.bundle/settings.png Binary files differnew file mode 100644 index 0000000..46cf933 --- /dev/null +++ b/VKPC/ImagesYosemite.bundle/settings.png diff --git a/VKPC/ImagesYosemite.bundle/settings@2x.png b/VKPC/ImagesYosemite.bundle/settings@2x.png Binary files differnew file mode 100644 index 0000000..38d1823 --- /dev/null +++ b/VKPC/ImagesYosemite.bundle/settings@2x.png diff --git a/VKPC/ImagesYosemite.bundle/settings_pressed.png b/VKPC/ImagesYosemite.bundle/settings_pressed.png Binary files differnew file mode 100644 index 0000000..8d5de59 --- /dev/null +++ b/VKPC/ImagesYosemite.bundle/settings_pressed.png diff --git a/VKPC/ImagesYosemite.bundle/settings_pressed@2x.png b/VKPC/ImagesYosemite.bundle/settings_pressed@2x.png Binary files differnew file mode 100644 index 0000000..7a83c8c --- /dev/null +++ b/VKPC/ImagesYosemite.bundle/settings_pressed@2x.png diff --git a/VKPC/ImagesYosemite.bundle/status.png b/VKPC/ImagesYosemite.bundle/status.png Binary files differnew file mode 100644 index 0000000..f33b1bd --- /dev/null +++ b/VKPC/ImagesYosemite.bundle/status.png diff --git a/VKPC/ImagesYosemite.bundle/status@2x.png b/VKPC/ImagesYosemite.bundle/status@2x.png Binary files differnew file mode 100644 index 0000000..4cf24ca --- /dev/null +++ b/VKPC/ImagesYosemite.bundle/status@2x.png diff --git a/VKPC/ImagesYosemite.bundle/status_pressed.png b/VKPC/ImagesYosemite.bundle/status_pressed.png Binary files differnew file mode 100644 index 0000000..450722b --- /dev/null +++ b/VKPC/ImagesYosemite.bundle/status_pressed.png diff --git a/VKPC/ImagesYosemite.bundle/status_pressed@2x.png b/VKPC/ImagesYosemite.bundle/status_pressed@2x.png Binary files differnew file mode 100644 index 0000000..f9b3b9c --- /dev/null +++ b/VKPC/ImagesYosemite.bundle/status_pressed@2x.png diff --git a/VKPC/ImagesYosemiteDark.bundle/empty.png b/VKPC/ImagesYosemiteDark.bundle/empty.png Binary files differnew file mode 100644 index 0000000..f38e9f9 --- /dev/null +++ b/VKPC/ImagesYosemiteDark.bundle/empty.png diff --git a/VKPC/ImagesYosemiteDark.bundle/empty@2x.png b/VKPC/ImagesYosemiteDark.bundle/empty@2x.png Binary files differnew file mode 100644 index 0000000..f38e9f9 --- /dev/null +++ b/VKPC/ImagesYosemiteDark.bundle/empty@2x.png diff --git a/VKPC/ImagesYosemiteDark.bundle/pl_cell_bg.png b/VKPC/ImagesYosemiteDark.bundle/pl_cell_bg.png Binary files differnew file mode 100644 index 0000000..66cd00c --- /dev/null +++ b/VKPC/ImagesYosemiteDark.bundle/pl_cell_bg.png diff --git a/VKPC/ImagesYosemiteDark.bundle/pl_cell_bg@2x.png b/VKPC/ImagesYosemiteDark.bundle/pl_cell_bg@2x.png Binary files differnew file mode 100644 index 0000000..acbb5af --- /dev/null +++ b/VKPC/ImagesYosemiteDark.bundle/pl_cell_bg@2x.png diff --git a/VKPC/ImagesYosemiteDark.bundle/pl_cell_pressed_bg.png b/VKPC/ImagesYosemiteDark.bundle/pl_cell_pressed_bg.png Binary files differnew file mode 100644 index 0000000..03fb40f --- /dev/null +++ b/VKPC/ImagesYosemiteDark.bundle/pl_cell_pressed_bg.png diff --git a/VKPC/ImagesYosemiteDark.bundle/pl_cell_pressed_bg@2x.png b/VKPC/ImagesYosemiteDark.bundle/pl_cell_pressed_bg@2x.png Binary files differnew file mode 100644 index 0000000..6cf09d1 --- /dev/null +++ b/VKPC/ImagesYosemiteDark.bundle/pl_cell_pressed_bg@2x.png diff --git a/VKPC/ImagesYosemiteDark.bundle/pl_pause.png b/VKPC/ImagesYosemiteDark.bundle/pl_pause.png Binary files differnew file mode 100644 index 0000000..75babd0 --- /dev/null +++ b/VKPC/ImagesYosemiteDark.bundle/pl_pause.png diff --git a/VKPC/ImagesYosemiteDark.bundle/pl_pause@2x.png b/VKPC/ImagesYosemiteDark.bundle/pl_pause@2x.png Binary files differnew file mode 100644 index 0000000..ae380cd --- /dev/null +++ b/VKPC/ImagesYosemiteDark.bundle/pl_pause@2x.png diff --git a/VKPC/ImagesYosemiteDark.bundle/pl_play.png b/VKPC/ImagesYosemiteDark.bundle/pl_play.png Binary files differnew file mode 100644 index 0000000..b90acfb --- /dev/null +++ b/VKPC/ImagesYosemiteDark.bundle/pl_play.png diff --git a/VKPC/ImagesYosemiteDark.bundle/pl_play@2x.png b/VKPC/ImagesYosemiteDark.bundle/pl_play@2x.png Binary files differnew file mode 100644 index 0000000..c963dd3 --- /dev/null +++ b/VKPC/ImagesYosemiteDark.bundle/pl_play@2x.png diff --git a/VKPC/ImagesYosemiteDark.bundle/pl_title_separator.png b/VKPC/ImagesYosemiteDark.bundle/pl_title_separator.png Binary files differnew file mode 100644 index 0000000..6f7ff91 --- /dev/null +++ b/VKPC/ImagesYosemiteDark.bundle/pl_title_separator.png diff --git a/VKPC/ImagesYosemiteDark.bundle/pl_title_separator@2x.png b/VKPC/ImagesYosemiteDark.bundle/pl_title_separator@2x.png Binary files differnew file mode 100644 index 0000000..2c13de1 --- /dev/null +++ b/VKPC/ImagesYosemiteDark.bundle/pl_title_separator@2x.png diff --git a/VKPC/ImagesYosemiteDark.bundle/settings.png b/VKPC/ImagesYosemiteDark.bundle/settings.png Binary files differnew file mode 100644 index 0000000..682dac4 --- /dev/null +++ b/VKPC/ImagesYosemiteDark.bundle/settings.png diff --git a/VKPC/ImagesYosemiteDark.bundle/settings@2x.png b/VKPC/ImagesYosemiteDark.bundle/settings@2x.png Binary files differnew file mode 100644 index 0000000..a2322d0 --- /dev/null +++ b/VKPC/ImagesYosemiteDark.bundle/settings@2x.png diff --git a/VKPC/ImagesYosemiteDark.bundle/settings_pressed.png b/VKPC/ImagesYosemiteDark.bundle/settings_pressed.png Binary files differnew file mode 100644 index 0000000..636405e --- /dev/null +++ b/VKPC/ImagesYosemiteDark.bundle/settings_pressed.png diff --git a/VKPC/ImagesYosemiteDark.bundle/settings_pressed@2x.png b/VKPC/ImagesYosemiteDark.bundle/settings_pressed@2x.png Binary files differnew file mode 100644 index 0000000..1c66b72 --- /dev/null +++ b/VKPC/ImagesYosemiteDark.bundle/settings_pressed@2x.png diff --git a/VKPC/ImagesYosemiteDark.bundle/status.png b/VKPC/ImagesYosemiteDark.bundle/status.png Binary files differnew file mode 100644 index 0000000..450722b --- /dev/null +++ b/VKPC/ImagesYosemiteDark.bundle/status.png diff --git a/VKPC/ImagesYosemiteDark.bundle/status@2x.png b/VKPC/ImagesYosemiteDark.bundle/status@2x.png Binary files differnew file mode 100644 index 0000000..f9b3b9c --- /dev/null +++ b/VKPC/ImagesYosemiteDark.bundle/status@2x.png diff --git a/VKPC/ImagesYosemiteDark.bundle/status_pressed.png b/VKPC/ImagesYosemiteDark.bundle/status_pressed.png Binary files differnew file mode 100644 index 0000000..450722b --- /dev/null +++ b/VKPC/ImagesYosemiteDark.bundle/status_pressed.png diff --git a/VKPC/ImagesYosemiteDark.bundle/status_pressed@2x.png b/VKPC/ImagesYosemiteDark.bundle/status_pressed@2x.png Binary files differnew file mode 100644 index 0000000..f9b3b9c --- /dev/null +++ b/VKPC/ImagesYosemiteDark.bundle/status_pressed@2x.png diff --git a/VKPC/NSMutableArray+QueueAdditions.h b/VKPC/NSMutableArray+QueueAdditions.h new file mode 100644 index 0000000..298cdee --- /dev/null +++ b/VKPC/NSMutableArray+QueueAdditions.h @@ -0,0 +1,12 @@ +// +// NSMutableArray+QueueAdditions.h +// VKPC +// +// Created by Eugene on 12/3/13. +// Copyright (c) 2013 Eugene Z. All rights reserved. +// + +@interface NSMutableArray (QueueAdditions) +- (id) dequeue; +- (void) enqueue:(id)obj; +@end
\ No newline at end of file diff --git a/VKPC/NSMutableArray+QueueAdditions.m b/VKPC/NSMutableArray+QueueAdditions.m new file mode 100644 index 0000000..2d5a35d --- /dev/null +++ b/VKPC/NSMutableArray+QueueAdditions.m @@ -0,0 +1,25 @@ +// +// NSMutableArray+QueueAdditions.m +// VKPC +// +// Created by Eugene on 12/3/13. +// Copyright (c) 2013 Eugene Z. All rights reserved. +// + +#import "NSMutableArray+QueueAdditions.h" + +@implementation NSMutableArray (QueueAdditions) + +- (id) dequeue { + id headObject = [self objectAtIndex:0]; + if (headObject != nil) { + [self removeObjectAtIndex:0]; + } + return headObject; +} + +- (void) enqueue:(id)anObject { + [self addObject:anObject]; +} + +@end
\ No newline at end of file diff --git a/VKPC/NSThread+Blocks.h b/VKPC/NSThread+Blocks.h new file mode 100644 index 0000000..41398ce --- /dev/null +++ b/VKPC/NSThread+Blocks.h @@ -0,0 +1,14 @@ +// +// NSThead+Blocks.h +// VKPC +// +// Created by Eugene on 11/7/14. +// Copyright (c) 2014 Eugene Z. All rights reserved. +// + +@interface NSThread (BlocksAdditions) + +- (void)performBlock:(void (^)())block; +- (void)performBlock:(void (^)())block waitUntilDone:(BOOL)wait; + +@end diff --git a/VKPC/NSThread+Blocks.m b/VKPC/NSThread+Blocks.m new file mode 100644 index 0000000..c7897dd --- /dev/null +++ b/VKPC/NSThread+Blocks.m @@ -0,0 +1,32 @@ +// +// NSThread+Blocks.m +// Shopify_Mobile +// +// Created by Matthew Newberry on 9/3/10. +// Copyright 2010 Shopify. All rights reserved. +// + +#import "NSThread+Blocks.h" + +@implementation NSThread (BlocksAdditions) + +- (void)performBlock:(void (^)())block { + if ([[NSThread currentThread] isEqual:self]) { + block(); + } else { + [self performBlock:block waitUntilDone:NO]; + } +} + +- (void)performBlock:(void (^)())block waitUntilDone:(BOOL)wait { + [NSThread performSelector:@selector(ng_runBlock:) + onThread:self + withObject:block + waitUntilDone:wait]; +} + ++ (void)ng_runBlock:(void (^)())block { + block(); +} + +@end diff --git a/VKPC/NSTimer+Blocks.h b/VKPC/NSTimer+Blocks.h new file mode 100644 index 0000000..3e481b3 --- /dev/null +++ b/VKPC/NSTimer+Blocks.h @@ -0,0 +1,13 @@ +// +// NSTimer+Blocks.h +// +// Created by Jiva DeVoe on 1/14/11. +// Copyright 2011 Random Ideas, LLC. All rights reserved. +// + +#import <Foundation/Foundation.h> + +@interface NSTimer (Blocks) ++(id)scheduledTimerWithTimeInterval:(NSTimeInterval)inTimeInterval block:(void (^)())inBlock repeats:(BOOL)inRepeats; ++(id)timerWithTimeInterval:(NSTimeInterval)inTimeInterval block:(void (^)())inBlock repeats:(BOOL)inRepeats; +@end diff --git a/VKPC/NSTimer+Blocks.m b/VKPC/NSTimer+Blocks.m new file mode 100644 index 0000000..eec2a59 --- /dev/null +++ b/VKPC/NSTimer+Blocks.m @@ -0,0 +1,37 @@ +// +// NSTimer+Blocks.m +// +// Created by Jiva DeVoe on 1/14/11. +// Copyright 2011 Random Ideas, LLC. All rights reserved. +// + +#import "NSTimer+Blocks.h" + +@implementation NSTimer (Blocks) + ++(id)scheduledTimerWithTimeInterval:(NSTimeInterval)inTimeInterval block:(void (^)())inBlock repeats:(BOOL)inRepeats +{ + void (^block)() = [inBlock copy]; + id ret = [self scheduledTimerWithTimeInterval:inTimeInterval target:self selector:@selector(jdExecuteSimpleBlock:) userInfo:block repeats:inRepeats]; +// [block release]; + return ret; +} + ++(id)timerWithTimeInterval:(NSTimeInterval)inTimeInterval block:(void (^)())inBlock repeats:(BOOL)inRepeats +{ + void (^block)() = [inBlock copy]; + id ret = [self timerWithTimeInterval:inTimeInterval target:self selector:@selector(jdExecuteSimpleBlock:) userInfo:block repeats:inRepeats]; +// [block release]; + return ret; +} + ++(void)jdExecuteSimpleBlock:(NSTimer *)inTimer; +{ + if([inTimer userInfo]) + { + void (^block)() = (void (^)())[inTimer userInfo]; + block(); + } +} + +@end diff --git a/VKPC/NSUserNotificationCenter+Private.h b/VKPC/NSUserNotificationCenter+Private.h new file mode 100644 index 0000000..afb5e8b --- /dev/null +++ b/VKPC/NSUserNotificationCenter+Private.h @@ -0,0 +1,17 @@ +// +// NSUserNotificationCenter+Private.h +// VKPC +// +// Created by Eugene on 11/18/14. +// Copyright (c) 2014 Eugene Z. All rights reserved. +// + +#import <Foundation/Foundation.h> + +@interface NSUserNotificationCenter (Private) + +//- (void)_removeAllDisplayedNotifications; +- (void)_removeDisplayedNotification:(NSUserNotification *)notification; + +@end + diff --git a/VKPC/NonVibrantButton.h b/VKPC/NonVibrantButton.h new file mode 100644 index 0000000..0864678 --- /dev/null +++ b/VKPC/NonVibrantButton.h @@ -0,0 +1,13 @@ +// +// NonVibrantButton.h +// VKPC +// +// Created by Eugene on 11/18/14. +// Copyright (c) 2014 Eugene Z. All rights reserved. +// + +#import <Cocoa/Cocoa.h> + +@interface NonVibrantButton : NSButton + +@end diff --git a/VKPC/NonVibrantButton.m b/VKPC/NonVibrantButton.m new file mode 100644 index 0000000..4092820 --- /dev/null +++ b/VKPC/NonVibrantButton.m @@ -0,0 +1,23 @@ +// +// NonVibrantButton.m +// VKPC +// +// Created by Eugene on 11/18/14. +// Copyright (c) 2014 Eugene Z. All rights reserved. +// + +#import "NonVibrantButton.h" + +@implementation NonVibrantButton + +- (void)drawRect:(NSRect)dirtyRect { + [super drawRect:dirtyRect]; + + // Drawing code here. +} + +- (BOOL)allowsVibrancy { + return NO; +} + +@end diff --git a/VKPC/Playlist.h b/VKPC/Playlist.h new file mode 100644 index 0000000..9a36bab --- /dev/null +++ b/VKPC/Playlist.h @@ -0,0 +1,42 @@ +// +// Playlist.h +// VKPC +// +// Created by Evgeny on 12/4/13. +// Copyright (c) 2013-2014 Eugene Z. All rights reserved. +// + +#import <Foundation/Foundation.h> + +@protocol PlaylistDelegate <NSObject> +- (void)playlistIDChanged:(NSInteger)playlistID; ++ (void)playlistIDChanged:(NSInteger)playlistID; +@end + +@interface Playlist : NSObject + +@property (strong, nonatomic) NSMutableArray *tracks; + +@property (strong, nonatomic) NSString *title; +@property (strong, nonatomic) NSString *lastTitle; + +@property (assign, nonatomic) NSInteger playlistID; +@property (assign) NSInteger lastPlaylistID; + +@property (assign) NSInteger lastTracksCount; + +@property (assign) PlayingTrackStatus playing; +@property (assign) PlayingTrackStatus lastPlaying; + +@property (strong) NSString *browser; // TODO delete +@property (strong) id<PlaylistDelegate> delegate; + +- (void)replaceWithDataFromPlaylist:(Playlist *)pl; +- (int)trackIndexById:(NSString *)_id; +- (void)setPlayingStatus:(PlayingStatus)status; +- (void)setPlayingIndex:(NSInteger)index; +- (void)clear; +- (BOOL)changed; +- (BOOL)empty; + +@end diff --git a/VKPC/Playlist.m b/VKPC/Playlist.m new file mode 100644 index 0000000..0b51cd4 --- /dev/null +++ b/VKPC/Playlist.m @@ -0,0 +1,139 @@ +// +// Playlist.m +// VKPC +// +// Created by Evgeny on 12/4/13. +// Copyright (c) 2013-2014 Eugene Z. All rights reserved. +// + +#import "Playlist.h" + +@implementation Playlist + +- (id)init { + if (self = [super init]) { + _tracks = [[NSMutableArray alloc] init]; + _title = [[NSString alloc] init]; + _playlistID = 0; + _lastPlaylistID = 0; + _lastTitle = @""; + _lastTracksCount = 0; + + _lastPlaying.index = -1; + _lastPlaying.status = PlayingStatusNotPlaying; + + _playing.index = -1; + _playing.status = PlayingStatusNotPlaying; + + _browser = @""; + _delegate = nil; + } + + return self; +} + +- (void)setTracks:(NSArray *)tracks { + _lastTracksCount = tracks.count; + + [_tracks removeAllObjects]; + [_tracks addObjectsFromArray:tracks]; +} + +- (void)setPlaylistID:(NSInteger)playlistID { + _lastPlaylistID = playlistID; + _playlistID = playlistID; + + if (_delegate != nil) { + [_delegate playlistIDChanged:playlistID]; + } +} + +- (void)setTitle:(NSString *)title { + _lastTitle = [NSString stringWithString:_title]; + _title = [NSString stringWithString:title]; +} + +// TODO fix in receiver +//- (NSString *)title { +// return [_title isEqualToString:@""] ? [[[NSBundle mainBundle] infoDictionary] objectForKey:kCFBundleDisplayName] : _title; +//} + +//- (NSString *)lastTitle { +// return [_lastTitle isEqualToString:@""] ? [[[NSBundle mainBundle] infoDictionary] objectForKey:kCFBundleDisplayName] : _lastTitle; +//} + +//- (int)lastTracksCount { +// return lastTracksCount; +//} + +//- (int)playlistId { +// return playlistId; +//} + +//- (int)lastPlaylistId { +// return lastPlaylistId; +//} + +//- (PlayingTrackStatus)playing { +// return playing; +//} + +//- (PlayingTrackStatus)lastPlaying { +// return lastPlaying; +//} + +- (void)setPlayingIndex:(NSInteger)index { + _lastPlaying.index = _playing.index; + _playing.index = index; +} + +- (void)setPlayingStatus:(PlayingStatus)status { + _lastPlaying.status = _playing.status; + _playing.status = status; +} + +- (void)replaceWithDataFromPlaylist:(Playlist *)pl { + self.tracks = pl.tracks; + self.title = pl.title; + self.playlistID = pl.playlistID; + self.browser = pl.browser; + + [self setPlayingIndex:pl.playing.index]; + [self setPlayingStatus:pl.playing.status]; +} + +// TODO можно хранить таблицу _id => index, обновлять ее при каждом обновлении tracks +- (int)trackIndexById:(NSString *)_id { + for (int i = 0; i < _tracks.count; i++) { + if ([(NSString *)((NSDictionary *)_tracks[i])[@"id"] isEqualToString:_id]) return i; + } + return -1; +} + +- (void)clear { + self.title = @""; + self.browser = @""; +// [self setTitle:@""]; +// [self setBrowser:@""]; + + _lastTracksCount = _tracks.count; + [_tracks removeAllObjects]; + + self.playlistID = 0; +// [self setId:0]; + _playing.status = PlayingStatusNotPlaying; + _playing.index = -1; + + _lastPlaying.status = PlayingStatusNotPlaying; + _lastPlaying.index = -1; +} + +- (BOOL)changed { + return _tracks.count != 0 || _lastTracksCount != _tracks.count || !([_title isEqualToString:_lastTitle]) || _playlistID != _lastPlaylistID; +} + +- (BOOL)empty { + return _tracks.count == 0 && _playlistID <= 0; +} + +@end diff --git a/VKPC/PlaylistTableCellView.h b/VKPC/PlaylistTableCellView.h new file mode 100644 index 0000000..d8502af --- /dev/null +++ b/VKPC/PlaylistTableCellView.h @@ -0,0 +1,27 @@ +// +// PlaylistTableCellView.h +// VKPC +// +// Created by Eugene on 12/2/13. +// Copyright (c) 2013-2014 Eugene Z. All rights reserved. +// + +#import <Cocoa/Cocoa.h> + +@interface PlaylistTableCellView : NSTableCellView + +@property (assign, nonatomic) PlayingStatus playingStatus; + +//- (void)setPlay; +//- (void)setPause; +//- (void)unsetPlay; +//- (void)moveTextFields; +- (void)updateStyle; +- (void)drawMode; +//- (NSImageView *)playIconImageView; +//- (NSTextField *)artistTextField; +//- (NSTextField *)titleTextField; +//- (NSTextField *)durationTextField; +//- (void)setMode:(PlayingStatus)mode; + +@end diff --git a/VKPC/PlaylistTableCellView.m b/VKPC/PlaylistTableCellView.m new file mode 100644 index 0000000..06cc3a1 --- /dev/null +++ b/VKPC/PlaylistTableCellView.m @@ -0,0 +1,158 @@ +// +// PlaylistTableCellView.m +// VKPC +// +// Created by Eugene on 12/2/13. +// Copyright (c) 2013-2014 Eugene Z. All rights reserved. +// +// TODO maybe remove lastDrawed + +#import "PlaylistTableCellView.h" +#import "VibrantTextField.h" + +static const int kTextFieldNormalX = 17; +static const int kTextFieldPlayingX = 46; +static const int kTitleNormalWidth = 315; +static const int kArtistNormalWidth = 283; +static const int kTitlePlayingWidth = 285; +static const int kArtistPlayingWidth = 253; + +@implementation PlaylistTableCellView { + PlayingStatus lastDrawed; +} + +- (id)initWithFrame:(NSRect)frame { + self = [super initWithFrame:frame]; + if (self) { + // Initialization code here. + _playingStatus = PlayingStatusNotPlaying; + lastDrawed = PlayingStatusUndefined; + [self updateStyle]; + } + + return self; +} + +- (id)initWithCoder:(NSCoder *)aDecoder { + if (self = [super initWithCoder:aDecoder]) { + _playingStatus = PlayingStatusNotPlaying; + lastDrawed = PlayingStatusUndefined; + [self updateStyle]; + } + return self; +} + +- (void)drawRect:(NSRect)dirtyRect { + [super drawRect:dirtyRect]; + [self drawMode]; +} + +- (void)setPlayingStatus:(PlayingStatus)playingStatus { +// NSLog(@"view setplayingstatus"); + lastDrawed = PlayingStatusUndefined; + _playingStatus = playingStatus; + [self drawMode]; +} + +- (NSImageView *)playIconImageView { + return [self viewWithTag:0]; +} + +- (NSTextField *)artistTextField { + return [self viewWithTag:1]; +} + +- (VibrantTextField *)titleTextField { + return [self viewWithTag:2]; +} + +- (VibrantTextField *)durationTextField { + return [self viewWithTag:3]; +} + +- (void)setPlay { + [self.playIconImageView setImage:VKPCGetImagesDictionary()[[[NSUserDefaults standardUserDefaults] boolForKey:VKPCPreferencesInvertPlaylistIcons] == YES ? VKPCImagePause : VKPCImagePlay]]; + [self moveTextFields]; +} + +- (void)setPause { + [self.playIconImageView setImage:VKPCGetImagesDictionary()[[[NSUserDefaults standardUserDefaults] boolForKey:VKPCPreferencesInvertPlaylistIcons] == YES ? VKPCImagePlay : VKPCImagePause]]; + [self moveTextFields]; +} + +- (void)unsetPlay { + [self.playIconImageView setImage:VKPCGetImagesDictionary()[VKPCImageEmpty]]; + [self moveTextFields]; +} + +- (void)moveTextFields { +// NSLog(@"[cellview movetextfields]"); + int x, artistWidth, titleWidth; + if (_playingStatus <= PlayingStatusNotPlaying) { + x = kTextFieldNormalX; + artistWidth = kArtistNormalWidth; + titleWidth = kTitleNormalWidth; + } else { + x = kTextFieldPlayingX; + artistWidth = kArtistPlayingWidth; + titleWidth = kTitlePlayingWidth; + } + + NSTextField *artist = [self artistTextField]; + NSTextField *title = [self titleTextField]; + + NSRect artistRect = artist.frame; + NSRect titleRect = title.frame; + + NSRect newArtistRect = NSMakeRect(x, artistRect.origin.y, artistWidth, artistRect.size.height); + NSRect newTitleRect = NSMakeRect(x, titleRect.origin.y, titleWidth, titleRect.size.height); + + [artist setFrame:newArtistRect]; + [title setFrame:newTitleRect]; + + [self setNeedsDisplay:YES]; +} + +- (void)updateStyle { + switch (GetInterfaceStyle()) { + case InterfaceStyleLegacy: + [self.titleTextField setTextColor:[NSColor colorWithSRGBRed:0.529 green:0.537 blue:0.549 alpha:1]]; + [self.durationTextField setTextColor:[NSColor colorWithSRGBRed:0.71 green:0.714 blue:0.718 alpha:1]]; + break; + + case InterfaceStyleYosemite: + [self.titleTextField setTextColor:[NSColor colorWithSRGBRed:0.0 green:0.0 blue:0.0 alpha:0.35]]; + [self.durationTextField setTextColor:[NSColor colorWithSRGBRed:0.0 green:0.0 blue:0.0 alpha:0.17]]; + break; + + case InterfaceStyleYosemiteDark: + [self.titleTextField setTextColor:[NSColor colorWithSRGBRed:1.0 green:1.0 blue:1.0 alpha:0.28]]; + [self.durationTextField setTextColor:[NSColor colorWithSRGBRed:1.0 green:1.0 blue:1.0 alpha:0.15]]; + break; + } +} + +- (void)drawMode { +// if (lastDrawed != _playingStatus) { + switch (_playingStatus) { + case PlayingStatusNotPlaying: + [self unsetPlay]; + break; + + case PlayingStatusPaused: + [self setPause]; + break; + + case PlayingStatusPlaying: + [self setPlay]; + break; + + default: + break; + } +// } + + lastDrawed = _playingStatus; +} + +@end diff --git a/VKPC/PlaylistTableCellViewHelper.h b/VKPC/PlaylistTableCellViewHelper.h new file mode 100644 index 0000000..e827862 --- /dev/null +++ b/VKPC/PlaylistTableCellViewHelper.h @@ -0,0 +1,23 @@ +// +// PlaylistTableCellViewHelper.h +// VKPC +// +// Created by Evgeny on 12/4/13. +// Copyright (c) 2013 Eugene Z. All rights reserved. +// + +#import <Foundation/Foundation.h> + +@interface PlaylistTableCellViewHelper : NSObject + ++ (NSTableCellView *)initialDrawingForView:(NSTableCellView *)view; ++ (NSImageView *)playIconImageViewForView:(NSTableCellView *)view; ++ (NSTextField *)artistTextFieldForView:(NSTableCellView *)view; ++ (NSTextField *)titleTextFieldForView:(NSTableCellView *)view; ++ (NSTextField *)durationTextFieldForView:(NSTableCellView *)view; ++ (void)setPlayForView:(NSTableCellView *)view; ++ (void)setPauseForView:(NSTableCellView *)view; ++ (void)unsetPlayForView:(NSTableCellView *)view; ++ (void)moveTextFieldsForView:(NSTableCellView *)view; + +@end diff --git a/VKPC/PlaylistTableCellViewHelper.m b/VKPC/PlaylistTableCellViewHelper.m new file mode 100644 index 0000000..760993c --- /dev/null +++ b/VKPC/PlaylistTableCellViewHelper.m @@ -0,0 +1,80 @@ +// +// PlaylistTableCellViewHelper.m +// VKPC +// +// Created by Evgeny on 12/4/13. +// Copyright (c) 2013 Eugene Z. All rights reserved. +// + +#import "PlaylistTableCellViewHelper.h" + +@implementation PlaylistTableCellViewHelper + ++ (NSTableCellView *)initialDrawingForView:(NSTableCellView *)view { + [[self titleTextFieldForView:view] setTextColor:[NSColor colorWithSRGBRed:0.529 green:0.537 blue:0.549 alpha:1]]; + [[self durationTextFieldForView:view] setTextColor:[NSColor colorWithSRGBRed:0.71 green:0.714 blue:0.718 alpha:1]]; + return view; +} + ++ (NSImageView *)playIconImageViewForView:(NSTableCellView *)view { + return [[view subviews] objectAtIndex:0]; +} + ++ (NSTextField *)artistTextFieldForView:(NSTableCellView *)view { + return [[view subviews] objectAtIndex:1]; +} + ++ (NSTextField *)titleTextFieldForView:(NSTableCellView *)view { + return [[view subviews] objectAtIndex:2]; +} + ++ (NSTextField *)durationTextFieldForView:(NSTableCellView *)view { + return [[view subviews] objectAtIndex:3]; +} + ++ (void)setPlayForView:(NSTableCellView *)view { + NSImageView *image = [self playIconImageViewForView:view]; + [image setImage:[NSImage imageNamed:@"pl_play"]]; + //self.playingStatus = PlayingStatusPlaying; + + [self moveTextFieldsForView:view]; +} + ++ (void)setPauseForView:(NSTableCellView *)view { + NSImageView *image = [self playIconImageViewForView:view]; + [image setImage:[NSImage imageNamed:@"pl_pause"]]; + //self.playingStatus = PlayingStatusPaused; + + [self moveTextFieldsForView:view]; +} + ++ (void)unsetPlayForView:(NSTableCellView *)view { + NSImageView *image = [self playIconImageViewForView:view]; + [image setImage:[NSImage imageNamed:@"empty"]]; + //self.playingStatus = PlayingStatusNotPlaying; + + [self moveTextFieldsForView:view]; +} + ++ (void)moveTextFieldsForView:(NSTableCellView *)view { + int x = 20; + //int x = playingStatus == PlayingStatusNotPlaying ? kTextFieldNormalX : kTextFieldPlayingX; + + NSTextField *artist = [self artistTextFieldForView:view]; + NSTextField *title = [self titleTextFieldForView:view]; + + NSRect artistRect = artist.frame; + NSRect titleRect = title.frame; + + NSRect newArtistRect = NSMakeRect(x, artistRect.origin.y, artistRect.size.width, artistRect.size.height); + NSRect newTitleRect = NSMakeRect(x, titleRect.origin.y, titleRect.size.width, titleRect.size.height); + + [artist setFrame:newArtistRect]; + [title setFrame:newTitleRect]; + + // Fucking shit + // TODO ? + // [view setNeedsDisplay:YES]; +} + +@end diff --git a/VKPC/PlaylistTableController.h b/VKPC/PlaylistTableController.h new file mode 100644 index 0000000..1a520e5 --- /dev/null +++ b/VKPC/PlaylistTableController.h @@ -0,0 +1,32 @@ +// +// PlaylistTableController.h +// VKPC +// +// Created by Eugene on 12/1/13. +// Copyright (c) 2013-2014 Eugene Z. All rights reserved. +// + +#import <Foundation/Foundation.h> +#import "QueueControllerProtocol.h" +#import "Types.h" + +@class PlaylistTableView, Queue, Playlist, PlaylistTableCellView; + +@interface PlaylistTableController : NSViewController <NSTableViewDataSource, NSTableViewDelegate, QueueControllerProtocol> + +@property (assign) BOOL inited; +@property (strong) Playlist *playlist; + +- (void)selectedRowAtIndex:(NSInteger)index; +- (void)setPlaylistDataWithTracks:(NSArray *)tracks title:(NSString *)title id:(NSInteger)_id activeId:(NSString *)activeId activePlaying:(BOOL)activePlaying browser:(NSString *)browser; ++ (void)preSetPlaylistDataWithTracks:(NSArray *)tracks title:(NSString *)title id:(NSInteger)_id activeId:(NSString *)activeId activePlaying:(BOOL)activePlaying browser:(NSString *)browser; +- (void)clearPlaylist; +- (void)showNotification:(NSInteger)trackIndex; +- (void)onQueueTask:(NSInteger)task forQueue:(Queue *)queue; +- (void)playlistUpdated; +- (void)setPlayingRow:(NSInteger)index withStatus:(PlayingStatus)status; +- (void)setPlayingTrackById:(NSString *)_id withStatus:(PlayingStatus)status forPlaylist:(NSInteger)playlistId; +- (int)numberOfRowsInTable; +- (PlaylistTableCellView *)getCellViewForIndex:(NSInteger)index; + +@end diff --git a/VKPC/PlaylistTableController.m b/VKPC/PlaylistTableController.m new file mode 100644 index 0000000..71a2cff --- /dev/null +++ b/VKPC/PlaylistTableController.m @@ -0,0 +1,351 @@ +// +// PlaylistTableController.m +// VKPC +// +// Created by Eugene on 12/1/13. +// Copyright (c) 2013-2014 Eugene Z. All rights reserved. +// + +#import "PopoverController.h" +#import "PlaylistTableController.h" +#import "PlaylistTableCellView.h" +#import "PlaylistTableView.h" +#import "Queue.h" +#import "QueueControllerProtocol.h" +#import "Playlist.h" +#import "Controller.h" +#import "Playlist.h" + +static NSString * const kTitleKey = @"title"; +static NSString * const kArtistKey = @"artist"; +static NSString * const kPlayImageKey = @"playImage"; +static NSString * const kDurationKey = @"duration"; +static NSString * const kIdKey = @"id"; + +static Playlist *prePlaylist = nil; +//static const int kRowHeight = 51; + +@implementation PlaylistTableController { + BOOL haveTrackForNextPlaylist; + PlaylistTableView *playlistTableView; + NSArrayController *playlistArrayController; + NSView *placeholderView; + Queue *setTracksQueue; + NSMutableDictionary *trackForNextPlaylist; +} + +- (id)init { + if (self = [super init]) { + _inited = NO; + haveTrackForNextPlaylist = NO; + + trackForNextPlaylist = [[NSMutableDictionary alloc] init]; + + setTracksQueue = [[Queue alloc] init]; + [setTracksQueue setHandler:self]; + + _playlist = [[Playlist alloc] init]; + + playlistTableView = [PopoverController shared].playlistTableView; + playlistArrayController = [PopoverController shared].playlistArrayController; + placeholderView = [PopoverController shared].customView; + // popoverController = [PopoverController shared]; + + // set some variables + // playlistTableView = _playlistTableView; + // playlistArrayController = controller; + // placeholderView = view; + // popoverController = _popoverController; + + // init some objects .. + NSScrollView *scrollView = [[PopoverController shared] scrollView]; + [[scrollView contentView] setPostsBoundsChangedNotifications: YES]; + [[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(viewDidScroll:) name:NSViewBoundsDidChangeNotification object:nil]; + + playlistTableView.controller = self; + // [playlistTableView setController:self]; + [playlistTableView setDataSource:self]; + [playlistTableView setDelegate:self]; + [playlistTableView numberOfRows]; + + [playlistArrayController addObject:[_playlist tracks]]; + + if (prePlaylist != nil) { + [setTracksQueue addTask:prePlaylist]; + } else { + [self playlistUpdated]; + } + _inited = YES; + + } + return self; +} + + +//- (void)initController:(PlaylistTableView *)_playlistTableView withArrayController:(NSArrayController *)controller placeholderView:(NSView *)view withPopoverController:(PopoverController *)_popoverController { +// } + +//- (BOOL)inited { +// return inited; +//} + +//- (void)dealloc { +// [super dealloc]; +//} + +/** Playlist related methods **/ + +//- (Playlist *)playlist { +// return playlist; +//} + +- (void)setPlaylistDataWithTracks:(NSArray *)tracks title:(NSString *)title id:(NSInteger)_id activeId:(NSString *)activeId activePlaying:(BOOL)activePlaying browser:(NSString *)browser { + for (int i = 0; i < [tracks count]; i++) { + [[tracks objectAtIndex:i] setObject:VKPCGetImagesDictionary()[VKPCImageEmpty] forKey:kPlayImageKey]; + } + Playlist *pl = [[Playlist alloc] init]; + pl.title = title; + pl.tracks = [NSMutableArray arrayWithArray:tracks]; + pl.playlistID = _id; + pl.browser = browser; + + if (![activeId isEqualToString:@""]) { + [pl setPlayingIndex:[pl trackIndexById:activeId]]; + [pl setPlayingStatus:(activePlaying ? PlayingStatusPlaying : PlayingStatusPaused)]; + } else { + [pl setPlayingIndex:-1]; + [pl setPlayingStatus:PlayingStatusNotPlaying]; + } + + [setTracksQueue addTask:pl]; +} + ++ (void)preSetPlaylistDataWithTracks:(NSArray *)tracks title:(NSString *)title id:(NSInteger)_id activeId:(NSString *)activeId activePlaying:(BOOL)activePlaying browser:(NSString *)browser { + if (prePlaylist == nil) { + prePlaylist = [[Playlist alloc] init]; + } + prePlaylist.tracks = [NSMutableArray arrayWithArray:tracks]; + prePlaylist.title = title; + prePlaylist.browser = browser; + prePlaylist.playlistID = _id; + + if (![activeId isEqualToString:@""]) { + int index = [prePlaylist trackIndexById:activeId]; + if (index != [prePlaylist playing].index) [self showNotification:index]; + [prePlaylist setPlayingIndex:index]; + [prePlaylist setPlayingStatus:(activePlaying ? PlayingStatusPlaying : PlayingStatusPaused)]; + } else { + [prePlaylist setPlayingIndex:-1]; + [prePlaylist setPlayingStatus:PlayingStatusNotPlaying]; + } +} + +- (void)clearPlaylist { + if (![_playlist empty]) { + NSLog(@"clearPlaylist(): is not empty"); + if (_inited) { + Playlist *pl = [[Playlist alloc] init]; + [pl clear]; + + [setTracksQueue addTask:pl]; + } else { + [_playlist clear]; + } + } +} + +- (void)onQueueTask:(id)task forQueue:(Queue *)queue { + if (setTracksQueue == queue) { + [_playlist replaceWithDataFromPlaylist:task]; + + if (haveTrackForNextPlaylist) { + NSInteger toPlaylistId = [(NSNumber *)[trackForNextPlaylist objectForKey:@"playlistId"] intValue]; + if (toPlaylistId == _playlist.playlistID) { + [_playlist setPlayingIndex:[_playlist trackIndexById:[trackForNextPlaylist objectForKey:@"id"]]]; + [_playlist setPlayingStatus:( [(NSString *)[trackForNextPlaylist objectForKey:@"status"] isEqualToString:@"play"] ? PlayingStatusPlaying : PlayingStatusPaused )]; + } + [trackForNextPlaylist removeAllObjects]; + haveTrackForNextPlaylist = false; + } + + [self playlistUpdated]; + } +} + +- (void)playlistUpdated { + if ([_playlist lastPlaying].index != -1 && [_playlist lastPlaying].index < [self numberOfRowsInTable]) { + [[self getCellViewForIndex:_playlist.lastPlaying.index] setPlayingStatus:PlayingStatusNotPlaying]; + } + + // Update title + [[PopoverController shared] updateTitle:_playlist.title]; + + // Update tracks + [playlistTableView beginUpdates]; + [playlistTableView performSelectorOnMainThread:@selector(reloadData) + withObject:nil + waitUntilDone:YES]; + + NSLog(@"in playlistUpdated: dispatch_async() now"); + dispatch_async(dispatch_get_main_queue(), ^{ + NSLog(@"<reloadData done>"); + [playlistTableView endUpdates]; + [placeholderView setHidden:(_playlist.tracks.count > 0)]; // TODO maybe just send message to popoverController? + + if ([_playlist playing].index != -1) { + [self setPlayingRow:_playlist.playing.index withStatus:[_playlist playing].status]; + } else if ([_playlist lastPlaying].index != -1) { + [self unselectRow:_playlist.lastPlaying.index]; + } + + [[PopoverController shared] resizeWithContentHeight:[playlistTableView getContentSize]]; + [setTracksQueue taskDone]; + }); +} + +- (void)setPlayingTrackById:(NSString *)_id withStatus:(PlayingStatus)status forPlaylist:(NSInteger)playlistId { + if (playlistId != _playlist.playlistID) { + [trackForNextPlaylist setValue:_id forKey:@"id"]; + [trackForNextPlaylist setValue:(status == PlayingStatusPlaying ? @"play" : @"pause") forKey:@"status"]; + [trackForNextPlaylist setValue:[NSNumber numberWithLong:playlistId] forKey:@"playlistId"]; + haveTrackForNextPlaylist = YES; + return; + } + + int index = [_playlist trackIndexById:_id]; + if (index != -1) { + if ([_playlist playing].index != index) [self showNotification:index]; + + if (_inited) { + if (index <= [playlistTableView numberOfRows]) { + //if ([playlist playing].index != index) [self showNotification:index]; + [self setPlayingRow:index withStatus:status]; + } + } else { + [_playlist setPlayingIndex:index]; + [_playlist setPlayingStatus:status]; + } + } +} + ++ (void)showNotification:(NSInteger)trackIndex { + if (trackIndex < prePlaylist.tracks.count && [[NSUserDefaults standardUserDefaults] boolForKey:VKPCPreferencesShowNotifications] == YES) { + NSDictionary *track = [[prePlaylist tracks] objectAtIndex:trackIndex]; + ShowNotification([track objectForKey:@"artist"], [track objectForKey:@"title"]); + } +} + +- (void)showNotification:(NSInteger)trackIndex { + if (trackIndex < _playlist.tracks.count && [[NSUserDefaults standardUserDefaults] boolForKey:VKPCPreferencesShowNotifications] == YES) { + NSDictionary *track = [[_playlist tracks] objectAtIndex:trackIndex]; + ShowNotification([track objectForKey:@"artist"], [track objectForKey:@"title"]); + } +} + +/** UI **/ + +- (void)viewDidScroll:(NSNotification *)notification { + NSScrollView *view = [[PopoverController shared] scrollView]; + + // Fix for retina + if ([view contentView].bounds.origin.y != (int)[view contentView].bounds.origin.y) { + NSPoint point = NSMakePoint([view contentView].bounds.origin.x, (int)([view contentView].bounds.origin.y + 0.5)); + [[view documentView] scrollPoint:point]; + } +} + +- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView { + return _playlist.tracks.count; +} + +- (id)tableView:(NSTableView *)tableView +objectValueForTableColumn:(NSTableColumn *)tableColumn + row:(NSInteger)row { + if (row < [[_playlist tracks] count]) + return [[_playlist tracks] objectAtIndex:row]; + return nil; +} + +- (NSView *)tableView:(NSTableView *)tableView + viewForTableColumn:(NSTableColumn *)tableColumn + row:(NSInteger)row { + PlaylistTableCellView *view = [playlistTableView makeViewWithIdentifier:@"VKPCCell" owner:self]; + if (view == nil) { + NSLog(@"VIEW IS NIL"); + } + PlayingStatus newPlayingStatus = _playlist.playing.index == row ? _playlist.playing.status : PlayingStatusNotPlaying; + [view setPlayingStatus:newPlayingStatus]; + return view; +} + +// User clicked on row +- (void)selectedRowAtIndex:(NSInteger)index { + NSDictionary *track = _playlist.tracks[index]; + + if (_playlist.playing.index != index) { + [self setPlayingRow:index withStatus:PlayingStatusPlaying]; + + [_playlist setPlayingIndex:index]; + [_playlist setPlayingStatus:PlayingStatusPlaying]; + } else if (_playlist.playing.index == index) { + if (_playlist.playing.status == PlayingStatusPlaying) { + [self setPlayingRow:index withStatus:PlayingStatusPaused]; + [_playlist setPlayingStatus:PlayingStatusPaused]; + } else { + [self setPlayingRow:index withStatus:PlayingStatusPlaying]; + [_playlist setPlayingStatus:PlayingStatusPlaying]; + } + } + + // TODO call script +// [Script executeForAll:@"common" withCommand:@"operateTrack" withData:[track objectForKey:kIdKey]]; + [Controller operateTrack:track[kIdKey]]; +} + +- (void)setPlayingRow:(NSInteger)index withStatus:(PlayingStatus)status { + if (index >= [self numberOfRowsInTable]) { + return; + } + + PlaylistTableCellView *cellView = [self getCellViewForIndex:index]; + + if (_playlist.lastPlaying.index != index) { + if (_playlist.lastPlaying.index >= 0 && _playlist.lastPlaying.index < [self numberOfRowsInTable]) { + [[self getCellViewForIndex:_playlist.lastPlaying.index] setPlayingStatus:PlayingStatusNotPlaying]; + } + } + + if (_playlist.playing.index != index) { + if (_playlist.playing.index != -1 && _playlist.playing.index < [self numberOfRowsInTable]) { + [[self getCellViewForIndex:_playlist.playing.index] setPlayingStatus:PlayingStatusNotPlaying]; + } + [_playlist setPlayingIndex:index]; + [_playlist setPlayingStatus:status]; + + [cellView setPlayingStatus:status]; + } else if (_playlist.playing.index == index) { + [_playlist setPlayingStatus:status]; + [cellView setPlayingStatus:status]; + } + + [playlistTableView scrollRowToVisible:index]; +} + +- (void)unselectRow:(NSInteger)index { + if (index >= [self numberOfRowsInTable]) { + return; + } + + PlaylistTableCellView *cellView = [self getCellViewForIndex:index]; + [cellView setPlayingStatus:PlayingStatusNotPlaying]; +} + +- (PlaylistTableCellView *)getCellViewForIndex:(NSInteger)index { + return [playlistTableView viewAtColumn:0 row:index makeIfNecessary:YES]; +} + +- (int)numberOfRowsInTable { + return (int)[playlistTableView numberOfRows]; +} + +@end diff --git a/VKPC/PlaylistTableRowView.h b/VKPC/PlaylistTableRowView.h new file mode 100644 index 0000000..ea1b77d --- /dev/null +++ b/VKPC/PlaylistTableRowView.h @@ -0,0 +1,17 @@ +// +// PlaylistTableRowView.h +// VKPC +// +// Created by Eugene on 12/2/13. +// Copyright (c) 2013-2014 Eugene Z. All rights reserved. +// + +#import <Cocoa/Cocoa.h> + +@interface PlaylistTableRowView : NSTableRowView + +@property (assign, nonatomic) BOOL mouseInside; + +- (void)setTrackSelected:(BOOL)is; + +@end diff --git a/VKPC/PlaylistTableRowView.m b/VKPC/PlaylistTableRowView.m new file mode 100644 index 0000000..5c92958 --- /dev/null +++ b/VKPC/PlaylistTableRowView.m @@ -0,0 +1,105 @@ +// +// PlaylistTableRowView.m +// VKPC +// +// Created by Eugene on 12/2/13. +// Copyright (c) 2013-2014 Eugene Z. All rights reserved. +// TODO иногда не реагирует на нажатия; найти и исправить +// TODO проблемы с ретиной! исправить +// TODO выяснить, актуальны ли предыдущие TODO + +#import "PlaylistTableRowView.h" + +@implementation PlaylistTableRowView { + BOOL _trackSelected; + BOOL _everSelected; + NSTrackingArea *trackingArea; +} + +//@dynamic _mouseInside; + +- (id)initWithFrame:(NSRect)frameRect { + if (self = [super initWithFrame:frameRect]) { + _everSelected = NO; + } + return self; +} + +- (void)setMouseInside:(BOOL)mouseInside { + if (_mouseInside != mouseInside) { + _mouseInside = mouseInside; + if (_trackSelected) { + [self setNeedsDisplay:YES]; + if (!_mouseInside) { + _trackSelected = NO; + } + } + } +} + +- (void)ensureTrackingArea { + if (trackingArea == nil) { + trackingArea = [[NSTrackingArea alloc] initWithRect:NSZeroRect options:NSTrackingInVisibleRect | NSTrackingActiveAlways | NSTrackingMouseEnteredAndExited owner:self userInfo:nil]; + } +} + +- (void)updateTrackingAreas { + [super updateTrackingAreas]; + [self ensureTrackingArea]; + if (![[self trackingAreas] containsObject:trackingArea]) { + [self addTrackingArea:trackingArea]; + } +} + +- (void)mouseEntered:(NSEvent *)theEvent { + self.mouseInside = YES; +} + +- (void)mouseExited:(NSEvent *)theEvent { + self.mouseInside = NO; + _trackSelected = NO; +} + +- (BOOL)isFlipped { + return NO; +} + +- (BOOL)allowsVibrancy { + return YES; +} + +//- (BOOL)wantsLayer { +// return YES; +//} + +// TODO what is it +- (void)setSelected:(BOOL)selected { + // Do nothing +} + +- (void)setTrackSelected:(BOOL)is { + if (_trackSelected != is) { + _everSelected = YES; + _trackSelected = is; + [self setNeedsDisplay:YES]; + } +} + +- (void)drawBackgroundInRect:(NSRect)dirtyRect { + [super drawBackgroundInRect:dirtyRect]; +// return; + NSImage *img = VKPCGetImagesDictionary()[_trackSelected && _everSelected && _mouseInside ? VKPCImageCellPressedBg : VKPCImageCellBg]; +// NSImage *img = VKPCGetImagesDictionary()[VKPCImageCellBg]; + [img drawInRect:dirtyRect fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1]; +// NSLog(@"rowview draw"); +} + +- (void)drawSelectionInRect:(NSRect)dirtyRect { + [super drawSelectionInRect:dirtyRect]; +} + +- (NSBackgroundStyle)interiorBackgroundStyle { + return NSBackgroundStyleLight; +} + +@end diff --git a/VKPC/PlaylistTableView.h b/VKPC/PlaylistTableView.h new file mode 100644 index 0000000..ac098a4 --- /dev/null +++ b/VKPC/PlaylistTableView.h @@ -0,0 +1,19 @@ +// +// PlaylistTableView.h +// VKPC +// +// Created by Eugene on 12/1/13. +// Copyright (c) 2013-2014 Eugene Z. All rights reserved. +// + +#import <Cocoa/Cocoa.h> + +@class PlaylistTableController; + +@interface PlaylistTableView : NSTableView + +@property (strong) PlaylistTableController *controller; + +- (int)getContentSize; + +@end diff --git a/VKPC/PlaylistTableView.m b/VKPC/PlaylistTableView.m new file mode 100644 index 0000000..ca41641 --- /dev/null +++ b/VKPC/PlaylistTableView.m @@ -0,0 +1,74 @@ +// +// PlaylistTableView.m +// VKPC +// +// Created by Eugene on 12/1/13. +// Copyright (c) 2013-2014 Eugene Z. All rights reserved. +// + +#import "PlaylistTableView.h" +#import "Global.h" +#import "PlaylistTableRowView.h" +#import "PlaylistTableController.h" + +static const int kRowHeight = 51; + +@implementation PlaylistTableView { + NSInteger pressedRow; +} + +- (id)initWithFrame:(NSRect)frame { + self = [super initWithFrame:frame]; + if (self) { + // Initialization code here. + [self.enclosingScrollView setDrawsBackground:NO]; + [self.enclosingScrollView setBorderType:NSNoBorder]; + [self setHeaderView:nil]; + [self setBackgroundColor:[NSColor clearColor]]; + + pressedRow = -1; + } + return self; +} + +- (BOOL)isOpaque { + return NO; +} + +- (void)drawRect:(NSRect)dirtyRect { + [super drawRect:dirtyRect]; +} + +//- (BOOL)wantsLayer { +// return YES; +//} + +- (void)mouseDown:(NSEvent *)theEvent { + NSPoint point = [self convertPoint:[theEvent locationInWindow] fromView:nil]; + NSInteger row = [self rowAtPoint:point]; + + if (row != -1) { + pressedRow = row; + PlaylistTableRowView *rowView = [self rowViewAtRow:row makeIfNecessary:NO]; + rowView.mouseInside = YES; + [rowView setTrackSelected:YES]; + } +} + +- (void)mouseUp:(NSEvent *)theEvent { + NSPoint point = [self convertPoint:[theEvent locationInWindow] fromView:nil]; + NSInteger row = [self rowAtPoint:point]; + + if (row != -1 && (int)row == pressedRow) { + PlaylistTableRowView *rowView = [self rowViewAtRow:row makeIfNecessary:NO]; + [rowView setTrackSelected:NO]; + + [_controller selectedRowAtIndex:(int)row]; + } +} + +- (int)getContentSize { + return kRowHeight * (int)[self numberOfRows]; +} + +@end diff --git a/VKPC/PopoverClipView.h b/VKPC/PopoverClipView.h new file mode 100644 index 0000000..145c7bd --- /dev/null +++ b/VKPC/PopoverClipView.h @@ -0,0 +1,13 @@ +// +// PopoverClipView.h +// VKPC +// +// Created by Eugene on 11/3/14. +// Copyright (c) 2014 Eugene Z. All rights reserved. +// + +#import <Cocoa/Cocoa.h> + +@interface PopoverClipView : NSClipView + +@end diff --git a/VKPC/PopoverClipView.m b/VKPC/PopoverClipView.m new file mode 100644 index 0000000..8471a55 --- /dev/null +++ b/VKPC/PopoverClipView.m @@ -0,0 +1,35 @@ +// +// PopoverClipView.m +// VKPC +// +// Created by Eugene on 11/3/14. +// Copyright (c) 2014 Eugene Z. All rights reserved. +// + +#import "PopoverClipView.h" +#import <QuartzCore/QuartzCore.h> + +@implementation PopoverClipView + +- (id)initWithFrame:(NSRect)frameRect { + self = [super initWithFrame:frameRect]; + if (!self) return nil; + + self.layer = [CAScrollLayer layer]; + self.wantsLayer = YES; + self.layerContentsRedrawPolicy = NSViewLayerContentsRedrawNever; + + return self; +} + +- (void)drawRect:(NSRect)dirtyRect { + [super drawRect:dirtyRect]; + + // Drawing code here. +} + +- (BOOL)wantsLayer { + return YES; +} + +@end diff --git a/VKPC/PopoverController.h b/VKPC/PopoverController.h new file mode 100644 index 0000000..498f806 --- /dev/null +++ b/VKPC/PopoverController.h @@ -0,0 +1,79 @@ +// +// PopoverController.h +// VKPC +// +// Created by Eugene on 11/30/13. +// Copyright (c) 2013-2014 Eugene Z. All rights reserved. +// + +#import <Cocoa/Cocoa.h> +#import <Sparkle/SUUpdater.h> + +@class PlaylistTableView, FlippedView, Popover, AboutWindowController, PreferencesWindowController, ShadowTextFieldCell, VibrantTextField, VibrantButton; + +@interface PopoverController : NSViewController + +@property (strong) IBOutlet NSArrayController *playlistArrayController; +@property (weak) IBOutlet VibrantTextField *titleTextField; +@property (weak) IBOutlet ShadowTextFieldCell *titleTextFieldCell; +@property (strong) IBOutlet NSMenu *browserMenu; +@property (strong) IBOutlet NSMenu *appMenu; +//@property (weak) IBOutlet FlippedView *_view; +@property (weak) IBOutlet PlaylistTableView *playlistTableView; +@property (weak) IBOutlet NSView *customView; +@property (weak) IBOutlet NSScrollView *scrollView; +@property (weak) IBOutlet NSImageCell *titleSeparatorImageCell; +@property (weak) IBOutlet NSButtonCell *settingsButtonCell; +@property (weak) IBOutlet VibrantButton *settingsButton; +@property (strong) IBOutlet FlippedView *view; + +// Placeholder and system configuration button +@property (weak) IBOutlet VibrantTextField *playlistNotLoadedTextField; +//@property (weak) IBOutlet NSButton *systemConfigurationRequiredButton; + +// Settings menu outlets +@property (weak) IBOutlet NSMenuItem *menuItemShowNotifications; +@property (weak) IBOutlet NSMenuItem *menuItemInvert; +@property (weak) IBOutlet NSMenuItem *menuItemCatch; +@property (weak) IBOutlet NSMenuItem *menuItemAutostart; + +@property (strong) PlaylistTableController *playlistTableController; +@property (assign, nonatomic) PopoverState state; + +/*@property (weak) IBOutlet NSMenuItem *menuItemBrowserChrome; +@property (weak) IBOutlet NSMenuItem *menuItemBrowserFirefox; +@property (weak) IBOutlet NSMenuItem *menuItemBrowserSafari; +@property (weak) IBOutlet NSMenuItem *menuItemBrowserOpera; +@property (weak) IBOutlet NSMenuItem *menuItemBrowserYandex;*/ +@property (strong) IBOutlet SUUpdater *sparkleUpdater; +@property (weak) IBOutlet NSMenuItem *useExtensionMode; +@property (weak) IBOutlet NSMenuItem *downloadExtensionsMenuItem; + ++ (PopoverController *)shared; + +// Actions +- (IBAction)menuButtonAction:(id)sender; +- (IBAction)systemConfigurationRequiredAction:(id)sender; + +// Settings actions +- (IBAction)menuItemShowNotificationsAction:(id)sender; +- (IBAction)menuItemInvertAction:(id)sender; +- (IBAction)menuItemCatchAction:(id)sender; +- (IBAction)menuItemAutostartAction:(id)sender; +- (IBAction)menuItemAboutAction:(id)sender; +- (IBAction)menuItemQuitAction:(id)sender; +- (IBAction)menuItemBrowserAction:(id)sender; +- (IBAction)menuItemDownloadExtensionsAction:(id)sender; +- (IBAction)menuItemCheckForUpdatesAction:(id)sender; +- (IBAction)useExtensionModeAction:(id)sender; + +//- (void)initPlaceholder; +//- (void)initPlaylist; +- (void)resizeWithContentHeight:(int)height; +- (void)doResizeWithContentHeight:(int)height animate:(BOOL)animate; +- (void)popoverDidShow; +- (void)popoverDidHide; +- (void)updateTitle:(NSString *)title; +- (void)setState:(PopoverState)state; + +@end diff --git a/VKPC/PopoverController.m b/VKPC/PopoverController.m new file mode 100644 index 0000000..146cf5e --- /dev/null +++ b/VKPC/PopoverController.m @@ -0,0 +1,449 @@ +// +// PopoverViewController.m +// VKPC +// +// Created by Eugene on 11/30/13. +// Copyright (c) 2013-2014 Eugene Z. All rights reserved. +// + +#import "Popover.h" +#import "PopoverController.h" +#import "FlippedView.h" +#import "AboutWindowController.h" +#import "PlaylistTableController.h" +#import "NSMutableArray+QueueAdditions.h" +#import "Controller.h" +//#import "HostsHack.h" +#import "Playlist.h" +#import "Server.h" +#import "Autostart.h" +#import "CatchMediaButtons.h" +#import "Statistics.h" + +#import "PlaylistTableView.h" +#import "PlaylistTableCellView.h" +#import "ShadowTextFieldCell.h" +#import "VibrantTextField.h" + +static const int kMinPopoverHeight = 240; +static const int kMaxPopoverHeight = 480; + +static NSInteger NSStateFromBool(BOOL v) { + return v ? NSOnState : NSOffState; +} +static BOOL BoolFromNSState(NSInteger state) { + return state == NSOffState ? NO : YES; +} +static NSInteger InvertNSState(NSInteger state) { + return state == NSOffState ? NSOnState : NSOffState; +} + +@implementation PopoverController { + int setHeightOnShow; + AboutWindowController *aboutWindowController; +} + ++ (PopoverController *)shared { + static PopoverController *shared = nil; + @synchronized (self) { + if (shared == nil){ + shared = [[self alloc] initWithNibName:@"PopoverView" bundle:nil]; + } + } + return shared; +} + +- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { + if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) { + _state = PopoverStatePlaylistNotLoaded; + } + return self; +} + +- (void)awakeFromNib { + NSLog(@"[PopoverController awakeFromNib]"); + + [super awakeFromNib]; + [self updateStyle]; + + // Load settings + BOOL catchMediaButtons = [[NSUserDefaults standardUserDefaults] boolForKey:VKPCPreferencesCatchMediaButtons]; + BOOL invertPlaylistIcons = [[NSUserDefaults standardUserDefaults] boolForKey:VKPCPreferencesInvertPlaylistIcons]; + BOOL showNotifications = [[NSUserDefaults standardUserDefaults] boolForKey:VKPCPreferencesShowNotifications]; + BOOL launchOnStartup = [Autostart isLaunchAtStartup]; + BOOL useExtensionMode = [[NSUserDefaults standardUserDefaults] boolForKey:VKPCPreferencesUseExtensionMode]; + NSInteger browser = [[NSUserDefaults standardUserDefaults] integerForKey:VKPCPreferencesBrowser]; + + [_menuItemCatch setState:NSStateFromBool(catchMediaButtons)]; + [_menuItemInvert setState:NSStateFromBool(invertPlaylistIcons)]; + [_menuItemShowNotifications setState:NSStateFromBool(showNotifications)]; + [_menuItemAutostart setState:NSStateFromBool(launchOnStartup)]; + [_useExtensionMode setState:NSStateFromBool(useExtensionMode)]; + [self useExtensionModeUpdated]; + + if (browser < [_browserMenu itemArray].count && browser >= 0) { + [(NSMenuItem *)[_browserMenu itemArray][browser] setState:NSOnState]; + } else { + [[NSUserDefaults standardUserDefaults] setInteger:0 forKey:VKPCPreferencesBrowser]; + [(NSMenuItem *)[_browserMenu itemArray][0] setState:NSOnState]; + } + + // + _playlistTableController = [[PlaylistTableController alloc] init]; + setHeightOnShow = -1; + + if (VKPCIsYosemite) { + [[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(darkModeChanged:) name:kAppleInterfaceThemeChangedNotification object:nil]; + } + + [[NSUserDefaults standardUserDefaults] addObserver:self + forKeyPath:VKPCPreferencesInvertPlaylistIcons + options:NSKeyValueObservingOptionNew + context:NULL]; + + [self updateState]; + +#ifdef DEBUG + NSArray *debugMenuItems = @[ + [[NSMenuItem alloc] initWithTitle:@"Inject script" action:@selector(menuInjectAction:) keyEquivalent:@"inject"], + [[NSMenuItem alloc] initWithTitle:@"Send 'play' command" action:@selector(menuSendPlayAction:) keyEquivalent:@"send_play"], + [[NSMenuItem alloc] initWithTitle:@"JS to clipboard" action:@selector(menuJSToClipboardAction:) keyEquivalent:@"copy_js"], + [[NSMenuItem alloc] initWithTitle:@"AS to clipboard" action:@selector(menuASToClipboardAction:) keyEquivalent:@"copy_as"], + [[NSMenuItem alloc] initWithTitle:@"Notification after 1 sec" action:@selector(menuNotificationAction:) keyEquivalent:@"notification"], + [[NSMenuItem alloc] initWithTitle:@"Add tracks" action:@selector(menuAddTracksAction:) keyEquivalent:@"add_tracks"], + [[NSMenuItem alloc] initWithTitle:@"Remove tracks" action:@selector(menuRemoveTracksAction:) keyEquivalent:@"remove_tracks"], +// [[NSMenuItem alloc] initWithTitle:@"Show HH window" action:@selector(menuShowHHWindowAction:) keyEquivalent:@"show_hh"], + [[NSMenuItem alloc] initWithTitle:@"Print debug info" action:@selector(menuPrintDebugInfoAction:) keyEquivalent:@"print_debug_info"], + [[NSMenuItem alloc] initWithTitle:@"Something" action:@selector(menuSomethingAction:) keyEquivalent:@"something"] + ]; + + for (NSMenuItem *item in debugMenuItems) { + [_appMenu insertItem:item atIndex:[_appMenu itemArray].count-3]; + } + [_appMenu insertItem:[NSMenuItem separatorItem] atIndex:[_appMenu itemArray].count-3]; +#endif +} + +// UI + +- (void)setState:(PopoverState)state { + _state = state; + [self updateState]; +} + +- (void)updateState { + switch (_state) { + case PopoverStatePlaylistNotLoaded: + [_customView setHidden:NO]; +// [_systemConfigurationRequiredButton setHidden:YES]; + [_playlistNotLoadedTextField setHidden:NO]; + break; + + case PopoverStatePlaylistLoaded: + [_customView setHidden:YES]; + break; + + case PopoverStateSystemConfigurationRequired: + [_customView setHidden:NO]; + [_playlistNotLoadedTextField setHidden:YES]; +// [_systemConfigurationRequiredButton setHidden:NO]; + break; + } +} + +- (void)resizeWithContentHeight:(int)height { + NSPopover *popover = [[Popover shared] popover]; + if (!popover.isShown) + setHeightOnShow = height; + //else + [self doResizeWithContentHeight:height animate:popover.isShown]; +} + +- (void)doResizeWithContentHeight:(int)height animate:(BOOL)animate { + NSRect scrollViewFrame = self.scrollView.frame; + int scrollViewYOffset = scrollViewFrame.origin.y; + + int popoverHeight = height + scrollViewYOffset; + if (popoverHeight > kMaxPopoverHeight) popoverHeight = kMaxPopoverHeight; + if (popoverHeight < kMinPopoverHeight) popoverHeight = kMinPopoverHeight; + + NSSize popoverSize = [[Popover shared] getSize]; + [[Popover shared] setSize:NSMakeSize(popoverSize.width, popoverHeight) animate:animate]; + [[self scrollView] setFrame:NSMakeRect(scrollViewFrame.origin.x, scrollViewFrame.origin.y, scrollViewFrame.size.width, popoverHeight-scrollViewYOffset)]; +} + +- (void)updateStyle { + NSDictionary *images = VKPCGetImagesDictionary(); + NSFontManager *fontManager = [NSFontManager sharedFontManager]; + NSFont *bold = [fontManager fontWithFamily:GetSystemFontName() traits:NSUnitalicFontMask weight:9 size:13.0]; + + VibrantTextField *ph = [[[self customView] subviews] objectAtIndex:0]; + + switch (GetInterfaceStyle()) { + case InterfaceStyleLegacy: + // title + [_titleTextField setTextColor:[NSColor colorWithSRGBRed:0.498 green:0.51 blue:0.522 alpha:1]]; + + // placeholder + [ph setTextColor:[NSColor colorWithSRGBRed:0.612 green:0.624 blue:0.631 alpha:1]]; + break; + + case InterfaceStyleYosemite: + // title + [_titleTextFieldCell setTextColor:[NSColor colorWithSRGBRed:0.0 green:0.0 blue:0.0 alpha:0.32]]; + + // placeholder + [ph setTextColor:[NSColor colorWithSRGBRed:0.0 green:0.0 blue:0.0 alpha:0.2]]; + break; + + case InterfaceStyleYosemiteDark: + // title + [_titleTextField setTextColor:[NSColor colorWithSRGBRed:1.0 green:1.0 blue:1.0 alpha:0.5]]; + + // placeholder + [ph setTextColor:[NSColor colorWithSRGBRed:1.0 green:1.0 blue:1.0 alpha:0.2]]; + break; + } + + [_titleTextField setFont:bold]; + + [_titleSeparatorImageCell setImage:images[VKPCImageTitleSeparator]]; + [_settingsButtonCell setImage:images[VKPCImageSettings]]; + [_settingsButtonCell setAlternateImage:images[VKPCImageSettingsPressed]]; +} + +- (void)updateTitle:(NSString *)title { + if ([title isEqualToString:@""]) { + title = [[[NSBundle mainBundle] infoDictionary] objectForKey:kCFBundleDisplayName]; + } + [_titleTextField setStringValue:title]; +} + +- (IBAction)menuButtonAction:(id)sender { + [NSMenu popUpContextMenu:_appMenu + withEvent:[NSApp currentEvent] + forView:sender]; +} + +//- (IBAction)systemConfigurationRequiredAction:(id)sender { +// [[Popover shared] hidePopover]; +// [HostsHack showWindow]; +//} + +- (IBAction)menuItemAboutAction:(id)sender { + if (!aboutWindowController) { + aboutWindowController = [[AboutWindowController alloc] initWithWindowNibName:@"AboutWindow"]; + } + [aboutWindowController showWindow:nil]; + [aboutWindowController.window makeKeyAndOrderFront:nil]; + [NSApp activateIgnoringOtherApps:YES]; + + [[Popover shared] hidePopover]; +} + +- (IBAction)menuItemQuitAction:(id)sender { + [[NSApplication sharedApplication] terminate:nil]; +} + +- (IBAction)menuItemShowNotificationsAction:(id)sender { + NSInteger newState = InvertNSState(_menuItemShowNotifications.state); + [_menuItemShowNotifications setState:newState]; + [[NSUserDefaults standardUserDefaults] setBool:BoolFromNSState(newState) forKey:VKPCPreferencesShowNotifications]; +} + +- (IBAction)menuItemInvertAction:(id)sender { + NSInteger newState = InvertNSState(_menuItemInvert.state); + [_menuItemInvert setState:newState]; + [[NSUserDefaults standardUserDefaults] setBool:BoolFromNSState(newState) forKey:VKPCPreferencesInvertPlaylistIcons]; +} + +- (IBAction)menuItemCatchAction:(id)sender { + NSInteger newState = InvertNSState(_menuItemCatch.state); + [_menuItemCatch setState:newState]; + [[NSUserDefaults standardUserDefaults] setBool:BoolFromNSState(newState) forKey:VKPCPreferencesCatchMediaButtons]; +} + +- (IBAction)menuItemAutostartAction:(id)sender { + BOOL status = [Autostart isLaunchAtStartup]; + [Autostart toggleLaunchAtStartup]; + [_menuItemAutostart setState:NSStateFromBool(!status)]; +} + +#ifdef DEBUG +- (IBAction)menuInjectAction:(id)sender { + [Controller debugInject]; +} + +- (IBAction)menuSendPlayAction:(id)sender { + [Controller debugSendPlay]; +} + +- (IBAction)menuJSToClipboardAction:(id)sender { + [Controller debugCopyJS]; +} + +- (IBAction)menuASToClipboardAction:(id)sender { + [Controller debugCopyAS]; +} + +- (IBAction)menuNotificationAction:(id)sender { + [NSTimer scheduledTimerWithTimeInterval:1.0 + target:self + selector:@selector(showTestNotification) + userInfo:nil + repeats:NO]; + [[Popover shared] hidePopover]; +} + +- (void)showTestNotification { + ShowNotification(@"Title", @"Text"); +} + +//- (IBAction)menuShowHHWindowAction:(id)sender { +// [[Popover shared] hidePopover]; +// [HostsHack showWindow]; +//} + +- (IBAction)menuAddTracksAction:(id)sender { + for (int i = 0; i < 1000; i++) + [_playlistTableController.playlist.tracks addObject:@{ + @"id": @"0_0", + @"artist": [@"Within Temptation " stringByAppendingString:[NSString stringWithFormat:@"%d", i]], + @"title": [@"Promise " stringByAppendingString:[NSString stringWithFormat:@"%d", i]], + @"duration": @"7:25", + @"playImage": VKPCGetImagesDictionary()[VKPCImageEmpty] + }]; + [_playlistTableController playlistUpdated]; +} + +- (IBAction)menuRemoveTracksAction:(id)sender { + [_playlistTableController clearPlaylist]; +// [_playlistTableController.playlist.tracks removeAllObjects]; +// [_playlistTableController playlistUpdated]; +} + +- (IBAction)menuPrintDebugInfoAction:(id)sender { + for (int i = 0; i < 5; i++) { + NSLog(@"[DEBUG INFO] browserid=%d, connected=%zd", i, [Server connectedCount:i]); + } + if (_playlistTableController) { + NSLog(@"[DEBUG_INFO] playlist id: %zu", _playlistTableController.playlist.playlistID); + } + NSLog(@"[DEBUG_INFO] sid: %d", VKPCSessionID); +} + +- (IBAction)menuSomethingAction:(id)sender { +// for (int i = 0; i < 20; i++) { +// [CatchMediaButtons stop]; +// [CatchMediaButtons start]; +// } +// NSLog(@"timestamp: %ld\n", GetTimestamp()); +// NSLog(@"another timestamp: %lf\n", [[NSDate date] timeIntervalSince1970]); +// / NSLog(@"UUID: %@", [[NSUserDefaults standardUserDefaults] stringForKey:VKPCPreferencesUUID]); + [Statistics initialize]; +} +#endif + +- (IBAction)menuItemBrowserAction:(id)sender { + NSMenuItem *item = (NSMenuItem *)sender; + NSInteger index = [[_browserMenu itemArray] indexOfObject:item]; + + for (int i = 0; i < BrowsersCount; i++) { + if (i != index) { + [(NSMenuItem *)[_browserMenu itemArray][i] setState:NSOffState]; + } else { + [item setState:NSOnState]; + } + } + + [[NSUserDefaults standardUserDefaults] setInteger:index forKey:VKPCPreferencesBrowser]; +} + +- (IBAction)menuItemDownloadExtensionsAction:(id)sender { + [[Popover shared] hidePopover]; + [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"https://ch1p.com/vkpc/#extensions"]]; +} + +- (IBAction)menuItemCheckForUpdatesAction:(id)sender { + [[Popover shared] hidePopover]; + [_sparkleUpdater checkForUpdates:sender]; +} + +- (IBAction)useExtensionModeAction:(id)sender { + BOOL use = BoolFromNSState(((NSMenuItem *)sender).state); + [_useExtensionMode setState:NSStateFromBool(!use)]; + [[NSUserDefaults standardUserDefaults] setBool:BoolFromNSState(!use) forKey:VKPCPreferencesUseExtensionMode]; + [self useExtensionModeUpdated]; +} + +- (void)useExtensionModeUpdated { + BOOL use = BoolFromNSState(_useExtensionMode.state); + [_downloadExtensionsMenuItem setTitle:( use ? @"Download extensions" : @"Extensions for Firefox and Opera" )]; +} + +//#if !IS_PRODUCTION +// +//- (IBAction)onSettingsItemTestAddTracksClick:(id)sender { +// [playlistTableController testAddTracks]; +//} +// +//- (IBAction)onSettingsItemTestRemoveAllTracksClick:(id)sender { +// [playlistTableController testRemoveAllTracks]; +//} +// +//- (IBAction)onSettingsItemTestResizePopoverClick:(id)sender { +// NSSize popoverSize = [[self statusItemPopup] getSize]; +// int add = 100; +// +// [[self statusItemPopup] setSize:NSMakeSize(popoverSize.width, popoverSize.height+add) animate:YES]; +//} +// +//- (IBAction)onSettingsItemTestPrintDebugInfoClick:(id)sender { +// NSRect rect = [[self _view] frame], scrollRect = [[self scrollView] frame]; +// NSLog(@"[view] x: %f, y: %f, width: %f, height: %f", rect.origin.x, rect.origin.y, rect.size.width, rect.size.height); +// NSLog(@"[scrollView] x: %f, y: %f, width: %f, height: %f", scrollRect.origin.x, scrollRect.origin.y, scrollRect.size.width, scrollRect.size.height); +//} +// +//# endif + +- (void)popoverDidShow { + [_scrollView setHidden:NO]; + if (setHeightOnShow != -1) { + [self doResizeWithContentHeight:setHeightOnShow animate:NO]; + setHeightOnShow = -1; + } +} + +- (void)popoverDidHide { + [_scrollView setHidden:YES]; +} + +- (void)darkModeChanged:(NSNotification *)notification { + [self updateStyle]; + + for (int i = 0; i < _playlistTableView.numberOfRows; i++) { + [[_playlistTableController getCellViewForIndex:i] updateStyle]; + } +} + +- (void)invertPrefChanged { + Playlist *playlist = _playlistTableController.playlist; + PlayingTrackStatus playing = playlist.playing; + NSInteger index = playing.index; + + if (index < _playlistTableView.numberOfRows) { + [[_playlistTableController getCellViewForIndex:index] drawMode]; + } +} + +// KVO +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { + if ([keyPath isEqualToString:VKPCPreferencesInvertPlaylistIcons]) { + NSNumber *new = change[NSKeyValueChangeKindKey]; + if ([new integerValue] == NSKeyValueChangeSetting) { + [self invertPrefChanged]; + } + } +} + +@end diff --git a/VKPC/PopoverImageView.h b/VKPC/PopoverImageView.h new file mode 100644 index 0000000..b1cd312 --- /dev/null +++ b/VKPC/PopoverImageView.h @@ -0,0 +1,13 @@ +// +// PopoverImageView.h +// VKPC +// +// Created by Eugene on 10/29/14. +// Copyright (c) 2014 Eugene Z. All rights reserved. +// + +#import <Cocoa/Cocoa.h> + +@interface PopoverImageView : NSImageView + +@end diff --git a/VKPC/PopoverImageView.m b/VKPC/PopoverImageView.m new file mode 100644 index 0000000..6ac07b3 --- /dev/null +++ b/VKPC/PopoverImageView.m @@ -0,0 +1,23 @@ +// +// PopoverImageView.m +// VKPC +// +// Created by Eugene on 10/29/14. +// Copyright (c) 2014 Eugene Z. All rights reserved. +// + +#import "PopoverImageView.h" + +@implementation PopoverImageView + +- (void)drawRect:(NSRect)dirtyRect { + [super drawRect:dirtyRect]; + + // Drawing code here. +} + +- (BOOL)allowsVibrancy { + return NO; +} + +@end diff --git a/VKPC/PopoverScrollView.h b/VKPC/PopoverScrollView.h new file mode 100644 index 0000000..9d73309 --- /dev/null +++ b/VKPC/PopoverScrollView.h @@ -0,0 +1,13 @@ +// +// PopoverScrollView.h +// VKPC +// +// Created by Eugene on 11/3/14. +// Copyright (c) 2014 Eugene Z. All rights reserved. +// + +#import <Cocoa/Cocoa.h> + +@interface PopoverScrollView : NSScrollView + +@end diff --git a/VKPC/PopoverScrollView.m b/VKPC/PopoverScrollView.m new file mode 100644 index 0000000..208942c --- /dev/null +++ b/VKPC/PopoverScrollView.m @@ -0,0 +1,49 @@ +// +// PopoverScrollView.m +// VKPC +// +// Created by Eugene on 11/3/14. +// Copyright (c) 2014 Eugene Z. All rights reserved. +// + +#import "PopoverScrollView.h" +#import "PopoverClipView.h" + +@implementation PopoverScrollView + +- (id)initWithFrame:(NSRect)frameRect { + self = [super initWithFrame:frameRect]; + if (self == nil) return nil; + + [self swapClipView]; + + return self; +} + +- (void)awakeFromNib { + [super awakeFromNib]; + + if (![self.contentView isKindOfClass:PopoverClipView.class] ) { + [self swapClipView]; + } +} + +- (void)swapClipView { + self.wantsLayer = YES; + id documentView = self.documentView; + PopoverClipView *clipView = [[PopoverClipView alloc] initWithFrame:self.contentView.frame]; + self.contentView = clipView; + self.documentView = documentView; +} + +- (void)drawRect:(NSRect)dirtyRect { + [super drawRect:dirtyRect]; + + // Drawing code here. +} + +- (BOOL)wantsLayer { + return YES; +} + +@end diff --git a/VKPC/PopoverView.xib b/VKPC/PopoverView.xib new file mode 100644 index 0000000..effc58a --- /dev/null +++ b/VKPC/PopoverView.xib @@ -0,0 +1,291 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="6254" systemVersion="14D72i" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES"> + <dependencies> + <deployment identifier="macosx"/> + <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="6254"/> + </dependencies> + <objects> + <customObject id="-2" userLabel="File's Owner" customClass="PopoverController"> + <connections> + <outlet property="appMenu" destination="eS5-q8-ejX" id="f02-rT-I3k"/> + <outlet property="browserMenu" destination="yTJ-KK-Izy" id="Q41-ju-gAf"/> + <outlet property="customView" destination="eYS-Lb-ED9" id="Ivc-uZ-Jc8"/> + <outlet property="downloadExtensionsMenuItem" destination="DWm-pV-zLp" id="M2e-uP-cZb"/> + <outlet property="menuItemAutostart" destination="Jcq-xy-zwE" id="39M-fE-mSq"/> + <outlet property="menuItemCatch" destination="8pN-Yq-4O3" id="vdX-8S-nSY"/> + <outlet property="menuItemInvert" destination="Z5x-tY-mfd" id="5qp-gG-PvK"/> + <outlet property="menuItemShowNotifications" destination="9B5-pC-iyk" id="gBp-0d-Pja"/> + <outlet property="playlistArrayController" destination="abY-q8-MAO" id="eaN-f3-AVS"/> + <outlet property="playlistNotLoadedTextField" destination="YaH-XW-SKI" id="rOH-6t-o3x"/> + <outlet property="playlistTableView" destination="ehC-Us-Pmq" id="uyR-ci-6sj"/> + <outlet property="scrollView" destination="aM8-4p-2Mt" id="1PM-xj-zPa"/> + <outlet property="settingsButton" destination="g7G-UQ-Bab" id="n4R-1U-dCU"/> + <outlet property="settingsButtonCell" destination="0GK-LT-UDr" id="3ZN-0u-v8O"/> + <outlet property="sparkleUpdater" destination="SMc-bZ-8kV" id="IyR-fQ-9wd"/> + <outlet property="titleSeparatorImageCell" destination="1uh-ov-4m0" id="ALk-Q0-8F6"/> + <outlet property="titleTextField" destination="e2k-OI-cLW" id="uma-uR-tFP"/> + <outlet property="titleTextFieldCell" destination="5xk-n7-53L" id="jfD-YQ-XY4"/> + <outlet property="useExtensionMode" destination="lzE-S6-HMP" id="xT2-mi-A60"/> + <outlet property="view" destination="Izk-4b-coz" id="esd-AF-JKU"/> + </connections> + </customObject> + <customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/> + <customObject id="-3" userLabel="Application" customClass="NSObject"/> + <customView id="Izk-4b-coz" customClass="FlippedView"> + <rect key="frame" x="0.0" y="0.0" width="350" height="240"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> + <subviews> + <imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="HpE-Mq-8z1" customClass="VibrantImageView"> + <rect key="frame" x="0.0" y="204" width="344" height="3"/> + <imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="pl_title_separator" id="1uh-ov-4m0"/> + </imageView> + <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="e2k-OI-cLW" customClass="VibrantTextField"> + <rect key="frame" x="29" y="210" width="287" height="23"/> + <textFieldCell key="cell" lineBreakMode="truncatingTail" allowsUndo="NO" sendsActionOnEndEditing="YES" alignment="center" title="VK Player Controller" allowsEditingTextAttributes="YES" usesSingleLineMode="YES" id="5xk-n7-53L" customClass="ShadowTextFieldCell"> + <font key="font" metaFont="systemBold"/> + <color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/> + </textFieldCell> + </textField> + <button fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="g7G-UQ-Bab" customClass="VibrantButton"> + <rect key="frame" x="315" y="215" width="29" height="18"/> + <buttonCell key="cell" type="bevel" bezelStyle="regularSquare" image="settings" imagePosition="only" alignment="center" alternateImage="settings_pressed" imageScaling="proportionallyDown" inset="2" id="0GK-LT-UDr"> + <behavior key="behavior" lightByContents="YES"/> + <font key="font" metaFont="system"/> + </buttonCell> + <connections> + <action selector="menuButtonAction:" target="-2" id="d0w-DT-5ia"/> + </connections> + </button> + <scrollView fixedFrame="YES" borderType="none" autohidesScrollers="YES" horizontalLineScroll="51" horizontalPageScroll="10" verticalLineScroll="51" verticalPageScroll="10" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="aM8-4p-2Mt"> + <rect key="frame" x="0.0" y="0.0" width="350" height="206"/> + <clipView key="contentView" misplaced="YES" drawsBackground="NO" copiesOnScroll="NO" id="hzO-9n-Gn0"> + <rect key="frame" x="0.0" y="0.0" width="350" height="206"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> + <subviews> + <tableView verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnAutoresizingStyle="lastColumnOnly" columnReordering="NO" columnResizing="NO" multipleSelection="NO" emptySelection="NO" autosaveColumns="NO" typeSelect="NO" rowHeight="51" viewBased="YES" id="ehC-Us-Pmq" customClass="PlaylistTableView"> + <autoresizingMask key="autoresizingMask"/> + <color key="backgroundColor" white="1" alpha="0.0" colorSpace="calibratedWhite"/> + <color key="gridColor" name="gridColor" catalog="System" colorSpace="catalog"/> + <tableColumns> + <tableColumn width="350" minWidth="40" maxWidth="1000" id="O1z-83-oDn"> + <tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="left"> + <font key="font" metaFont="smallSystem"/> + <color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" white="0.33333298560000002" alpha="1" colorSpace="calibratedWhite"/> + </tableHeaderCell> + <textFieldCell key="dataCell" lineBreakMode="truncatingTail" selectable="YES" editable="YES" alignment="left" title="Text Cell" id="Mtg-ie-nyr"> + <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="VKPCCell" id="nBY-nW-juI" customClass="PlaylistTableCellView"> + <rect key="frame" x="0.0" y="0.0" width="350" height="51"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> + <subviews> + <imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="wWX-LM-U3O"> + <rect key="frame" x="20" y="16" width="16" height="19"/> + <imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="empty" id="01E-Kg-jH1"/> + <connections> + <binding destination="nBY-nW-juI" name="value" keyPath="objectValue.playImage" id="Vja-ew-Xo3"/> + </connections> + </imageView> + <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" tag="1" translatesAutoresizingMaskIntoConstraints="NO" id="sdo-fm-n7o"> + <rect key="frame" x="17" y="26" width="276" height="21"/> + <textFieldCell key="cell" lineBreakMode="truncatingTail" refusesFirstResponder="YES" sendsActionOnEndEditing="YES" tag="-1" title="artist" id="tgE-0W-o7b"> + <font key="font" size="14" name="HelveticaNeue-Medium"/> + <color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/> + </textFieldCell> + <connections> + <binding destination="nBY-nW-juI" name="value" keyPath="objectValue.artist" id="IN0-Zm-NB4"> + <dictionary key="options"> + <bool key="NSConditionallySetsEnabled" value="YES"/> + </dictionary> + </binding> + </connections> + </textField> + <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" tag="2" translatesAutoresizingMaskIntoConstraints="NO" id="jtd-CX-nQ0" customClass="VibrantTextField"> + <rect key="frame" x="17" y="3" width="315" height="25"/> + <textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="title" id="Sdu-Im-Por"> + <font key="font" size="13" name="HelveticaNeue"/> + <color key="textColor" red="0.52900000000000003" green="0.53700000000000003" blue="0.54900000000000004" alpha="1" colorSpace="calibratedRGB"/> + <color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/> + </textFieldCell> + <connections> + <binding destination="nBY-nW-juI" name="value" keyPath="objectValue.title" id="QWQ-A6-RvE"/> + </connections> + </textField> + <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" tag="3" translatesAutoresizingMaskIntoConstraints="NO" id="9NZ-qw-N3t" customClass="VibrantTextField"> + <rect key="frame" x="277" y="27" width="57" height="19"/> + <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="00:00" id="16x-EY-U2I"> + <font key="font" size="13" name="HelveticaNeue"/> + <color key="textColor" red="0.70999999999999996" green="0.70999999999999996" blue="0.71399999999999997" alpha="0.71799999999999997" colorSpace="calibratedRGB"/> + <color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/> + </textFieldCell> + <connections> + <binding destination="nBY-nW-juI" name="value" keyPath="objectValue.duration" id="fE8-bt-LCP"/> + </connections> + </textField> + </subviews> + </tableCellView> + <customView identifier="NSTableViewRowViewKey" id="Cge-LI-ALw" userLabel="PlaylistTableRowView" customClass="PlaylistTableRowView"> + <rect key="frame" x="0.0" y="51" width="350" height="51"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> + </customView> + </prototypeCellViews> + </tableColumn> + </tableColumns> + </tableView> + </subviews> + <color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/> + </clipView> + <scroller key="horizontalScroller" hidden="YES" verticalHuggingPriority="750" horizontal="YES" id="Oyn-F3-qeL"> + <autoresizingMask key="autoresizingMask"/> + </scroller> + <scroller key="verticalScroller" hidden="YES" verticalHuggingPriority="750" horizontal="NO" id="WD8-tL-LUC"> + <autoresizingMask key="autoresizingMask"/> + </scroller> + </scrollView> + <customView fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="eYS-Lb-ED9"> + <rect key="frame" x="0.0" y="0.0" width="347" height="200"/> + <subviews> + <textField hidden="YES" horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="YaH-XW-SKI" customClass="VibrantTextField"> + <rect key="frame" x="49" y="75" width="249" height="59"/> + <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="center" title="Playlist is empty.
Start playing music on VK." id="3sV-et-4Ji"> + <font key="font" size="15" name="HelveticaNeue"/> + <color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/> + </textFieldCell> + </textField> + </subviews> + </customView> + </subviews> + <point key="canvasLocation" x="-274" y="76"/> + </customView> + <arrayController id="abY-q8-MAO" userLabel="PlaylistAray"> + <declaredKeys> + <string>title</string> + <string>artist</string> + <string>playImage</string> + <string>duration</string> + </declaredKeys> + </arrayController> + <menu id="eS5-q8-ejX"> + <items> + <menuItem title="Browser" id="ewQ-av-lb2"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Browser" id="yTJ-KK-Izy"> + <items> + <menuItem title="Google Chrome" id="OXY-mE-cET"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="menuItemBrowserAction:" target="-2" id="5WX-sE-kDq"/> + </connections> + </menuItem> + <menuItem title="Firefox" id="a3C-7R-G9o"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="menuItemBrowserAction:" target="-2" id="Mot-MU-uLy"/> + </connections> + </menuItem> + <menuItem title="Safari" id="khl-xy-QgX"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="menuItemBrowserAction:" target="-2" id="a42-fg-sWq"/> + </connections> + </menuItem> + <menuItem title="Opera" id="o3k-h5-G9d"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="menuItemBrowserAction:" target="-2" id="71e-sS-oSb"/> + </connections> + </menuItem> + <menuItem title="Yandex.Browser" id="Q9q-dO-gce"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="menuItemBrowserAction:" target="-2" id="xa7-B7-kNY"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="zN5-lZ-0TB"/> + <menuItem title="Use extension mode for all browsers" id="lzE-S6-HMP"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="useExtensionModeAction:" target="-2" id="yNf-MF-TyU"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="zxB-Hn-KsU"/> + <menuItem title="Extensions for Firefox and Opera" id="DWm-pV-zLp"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="menuItemDownloadExtensionsAction:" target="-2" id="cNj-iG-79j"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="Show notifications" id="9B5-pC-iyk"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="menuItemShowNotificationsAction:" target="-2" id="9Bk-O5-7bU"/> + </connections> + </menuItem> + <menuItem title="Invert play/pause icons" id="Z5x-tY-mfd"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="menuItemInvertAction:" target="-2" id="PpL-DV-ffS"/> + </connections> + </menuItem> + <menuItem title="Catch media buttons" id="8pN-Yq-4O3"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="menuItemCatchAction:" target="-2" id="4Ug-EZ-GZg"/> + </connections> + </menuItem> + <menuItem title="Launch at startup" id="Jcq-xy-zwE"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="menuItemAutostartAction:" target="-2" id="UKC-Ds-dBs"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="HJK-FC-SO3"/> + <menuItem title="About" id="9h3-wv-zTI"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="menuItemAboutAction:" target="-2" id="MTU-ht-ZSo"/> + </connections> + </menuItem> + <menuItem title="Check for updates..." id="cPw-rW-ds4"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="menuItemCheckForUpdatesAction:" target="-2" id="8bI-lr-pdb"/> + </connections> + </menuItem> + <menuItem title="Quit" id="dGF-lW-5MZ"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="menuItemQuitAction:" target="-2" id="61e-ml-dWE"/> + </connections> + </menuItem> + </items> + <point key="canvasLocation" x="-274" y="-180.5"/> + </menu> + <button verticalHuggingPriority="750" id="WKc-nW-BaT"> + <rect key="frame" x="0.0" y="0.0" width="82" height="32"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> + <buttonCell key="cell" type="push" title="Button" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="Phv-Dj-QEJ"> + <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/> + <font key="font" metaFont="system"/> + </buttonCell> + </button> + <customObject id="SMc-bZ-8kV" userLabel="SUUpdater" customClass="SUUpdater"/> + </objects> + <resources> + <image name="empty" width="1" height="1"/> + <image name="pl_title_separator" width="350" height="3"/> + <image name="settings" width="18" height="18"/> + <image name="settings_pressed" width="18" height="18"/> + </resources> +</document> diff --git a/VKPC/PopupControllerProtocol.h b/VKPC/PopupControllerProtocol.h new file mode 100644 index 0000000..5cf1757 --- /dev/null +++ b/VKPC/PopupControllerProtocol.h @@ -0,0 +1,14 @@ +// +// PopoverControllerProtocol.h +// VKPC +// +// Created by Evgeny on 12/4/13. +// Copyright (c) 2013-2014 Eugene Z. All rights reserved. +// + +@protocol PopupControllerProtocol <NSObject> + +- (void)popoverDidShow; +- (void)popoverDidHide; + +@end
\ No newline at end of file diff --git a/VKPC/Queue.h b/VKPC/Queue.h new file mode 100644 index 0000000..3274c08 --- /dev/null +++ b/VKPC/Queue.h @@ -0,0 +1,24 @@ +// +// Queue.h +// VKPC +// +// Created by Eugene on 12/3/13. +// Copyright (c) 2013 Eugene Z. All rights reserved. +// + +#import <Foundation/Foundation.h> +#import "QueueControllerProtocol.h" + +@interface Queue : NSObject { + __strong id<QueueControllerProtocol> handler; + NSMutableArray *tasks; + BOOL active; +} + +- (void)setHandler:(__strong id<QueueControllerProtocol>)val; +- (void)addTask:(id)task; +- (void)process; +- (void)passToHandler:(id)task; +- (void)taskDone; + +@end diff --git a/VKPC/Queue.m b/VKPC/Queue.m new file mode 100644 index 0000000..d5cdef6 --- /dev/null +++ b/VKPC/Queue.m @@ -0,0 +1,51 @@ +// +// Queue.m +// VKPC +// +// Created by Eugene on 12/3/13. +// Copyright (c) 2013 Eugene Z. All rights reserved. +// + +#import "Queue.h" +#import "NSMutableArray+QueueAdditions.h" + +@implementation Queue + +- (id)init { + if (self = [super init]) { + tasks = [[NSMutableArray alloc] init]; + active = false; + } + return self; +} + +- (void)setHandler:(__strong id<QueueControllerProtocol>)val { + handler = val; +} + +- (void)addTask:(id)task { + [tasks enqueue:task]; + + if (!active) [self process]; +} + +- (void)process { + if (active || ![tasks count]) return; + + active = true; + id task = [tasks dequeue]; + [self passToHandler:task]; +} + +- (void)passToHandler:(__strong id)task { + [handler onQueueTask:task forQueue:self]; +} + +- (void)taskDone { + if (active) { + active = false; + [self process]; + } +} + +@end diff --git a/VKPC/QueueControllerProtocol.h b/VKPC/QueueControllerProtocol.h new file mode 100644 index 0000000..34078c7 --- /dev/null +++ b/VKPC/QueueControllerProtocol.h @@ -0,0 +1,15 @@ +// +// QueueProtocol.h +// VKPC +// +// Created by Eugene on 12/3/13. +// Copyright (c) 2013 Eugene Z. All rights reserved. +// + +@class Queue; + +@protocol QueueControllerProtocol <NSObject> + +- (void)onQueueTask:(id)task forQueue:(Queue *)queue; + +@end diff --git a/VKPC/SPInvocationGrabbing/NSObject+SPInvocationGrabbing.h b/VKPC/SPInvocationGrabbing/NSObject+SPInvocationGrabbing.h new file mode 100644 index 0000000..d30233d --- /dev/null +++ b/VKPC/SPInvocationGrabbing/NSObject+SPInvocationGrabbing.h @@ -0,0 +1,30 @@ +#import <Foundation/Foundation.h> + +@interface SPInvocationGrabber : NSObject { + id _object; + NSInvocation *_invocation; + int frameCount; + char **frameStrings; + BOOL backgroundAfterForward; + BOOL onMainAfterForward; + BOOL waitUntilDone; +} +-(id)initWithObject:(id)obj; +-(id)initWithObject:(id)obj stacktraceSaving:(BOOL)saveStack; +@property (readonly, retain, nonatomic) id object; +@property (readonly, retain, nonatomic) NSInvocation *invocation; +@property BOOL backgroundAfterForward; +@property BOOL onMainAfterForward; +@property BOOL waitUntilDone; +-(void)invoke; // will release object and invocation +-(void)printBacktrace; +-(void)saveBacktrace; +@end + +@interface NSObject (SPInvocationGrabbing) +-(id)grab; +-(id)invokeAfter:(NSTimeInterval)delta; +-(id)nextRunloop; +-(id)inBackground; +-(id)onMainAsync:(BOOL)async; +@end diff --git a/VKPC/SPInvocationGrabbing/NSObject+SPInvocationGrabbing.m b/VKPC/SPInvocationGrabbing/NSObject+SPInvocationGrabbing.m new file mode 100644 index 0000000..c48ea08 --- /dev/null +++ b/VKPC/SPInvocationGrabbing/NSObject+SPInvocationGrabbing.m @@ -0,0 +1,122 @@ +#import "NSObject+SPInvocationGrabbing.h" +#import <execinfo.h> + +#pragma mark Invocation grabbing +@interface SPInvocationGrabber () + +@property (readwrite, retain, nonatomic) id object; +@property (readwrite, retain, nonatomic) NSInvocation *invocation; + +@end + +@implementation SPInvocationGrabber +- (id)initWithObject:(id)obj { + return [self initWithObject:obj stacktraceSaving:YES]; +} + +- (id)initWithObject:(id)obj stacktraceSaving:(BOOL)saveStack { + self.object = obj; + + if (saveStack) + [self saveBacktrace]; + + return self; +} + +- (void)dealloc { + free(frameStrings); + self.object = nil; + self.invocation = nil; +// [super dealloc]; +} + +@synthesize invocation = _invocation, object = _object; + +@synthesize backgroundAfterForward, onMainAfterForward, waitUntilDone; + +- (void)runInBackground { + @try { + [self invoke]; + } + @finally {} +} + + +- (void)forwardInvocation:(NSInvocation *)anInvocation { + [anInvocation retainArguments]; + anInvocation.target = _object; + self.invocation = anInvocation; + + if(backgroundAfterForward) + [NSThread detachNewThreadSelector:@selector(runInBackground) toTarget:self withObject:nil]; + else if(onMainAfterForward) + [self performSelectorOnMainThread:@selector(invoke) withObject:nil waitUntilDone:waitUntilDone]; +} + +- (NSMethodSignature *)methodSignatureForSelector:(SEL)inSelector { + NSMethodSignature *signature = [super methodSignatureForSelector:inSelector]; + if (signature == NULL) + signature = [_object methodSignatureForSelector:inSelector]; + + return signature; +} + +- (void)invoke { + @try { + [_invocation invoke]; + } + @catch (NSException * e) { + NSLog(@"SPInvocationGrabber's target raised %@:\n\t%@\nInvocation was originally scheduled at:", e.name, e); + [self printBacktrace]; + printf("\n"); + [e raise]; + } + + self.invocation = nil; + self.object = nil; +} + +- (void)saveBacktrace { + void *backtraceFrames[128]; + frameCount = backtrace(&backtraceFrames[0], 128); + frameStrings = backtrace_symbols(&backtraceFrames[0], frameCount); +} + +- (void)printBacktrace { + for(int x = 3; x < frameCount; x++) { + if(frameStrings[x] == NULL) { break; } + printf("%s\n", frameStrings[x]); + } +} +@end + +@implementation NSObject (SPInvocationGrabbing) + +-(id)grab { + return [[SPInvocationGrabber alloc] initWithObject:self]; +} + +- (id)invokeAfter:(NSTimeInterval)delta { + id grabber = [self grab]; + [NSTimer scheduledTimerWithTimeInterval:delta target:grabber selector:@selector(invoke) userInfo:nil repeats:NO]; + return grabber; +} + +- (id)nextRunloop { + return [self invokeAfter:0]; +} + +- (id)inBackground { + SPInvocationGrabber *grabber = [self grab]; + grabber.backgroundAfterForward = YES; + return grabber; +} + +- (id)onMainAsync:(BOOL)async { + SPInvocationGrabber *grabber = [self grab]; + grabber.onMainAfterForward = YES; + grabber.waitUntilDone = !async; + return grabber; +} + +@end diff --git a/VKPC/SPMediaKeyTap.h b/VKPC/SPMediaKeyTap.h new file mode 100644 index 0000000..8e7ac3d --- /dev/null +++ b/VKPC/SPMediaKeyTap.h @@ -0,0 +1,43 @@ +#include <Cocoa/Cocoa.h> +#import <IOKit/hidsystem/ev_keymap.h> +#import <Carbon/Carbon.h> + +// http://overooped.com/post/2593597587/mediakeys + +#define SPSystemDefinedEventMediaKeys 8 + +@interface SPMediaKeyTap : NSObject { + EventHandlerRef _app_switching_ref; + EventHandlerRef _app_terminating_ref; + CFMachPortRef _eventPort; + CFRunLoopSourceRef _eventPortSource; + CFRunLoopRef _tapThreadRL; + BOOL _shouldInterceptMediaKeyEvents; + id _delegate; + // The app that is frontmost in this list owns media keys + NSMutableArray *_mediaKeyAppList; +} ++ (NSArray*)defaultMediaKeyUserBundleIdentifiers; + +-(id)initWithDelegate:(id)delegate; + ++(BOOL)usesGlobalMediaKeyTap; +-(void)startWatchingMediaKeys; +-(void)stopWatchingMediaKeys; +-(void)handleAndReleaseMediaKeyEvent:(NSEvent *)event; +@end + +@interface NSObject (SPMediaKeyTapDelegate) +-(void)mediaKeyTap:(SPMediaKeyTap*)keyTap receivedMediaKeyEvent:(NSEvent*)event; +@end + +#ifdef __cplusplus +extern "C" { +#endif + +// extern NSString *kMediaKeyUsingBundleIdentifiersDefaultsKey; +// extern NSString *kIgnoreMediaKeysDefaultsKey; + +#ifdef __cplusplus +} +#endif
\ No newline at end of file diff --git a/VKPC/SPMediaKeyTap.m b/VKPC/SPMediaKeyTap.m new file mode 100644 index 0000000..4112b78 --- /dev/null +++ b/VKPC/SPMediaKeyTap.m @@ -0,0 +1,374 @@ +// Copyright (c) 2010 Spotify AB + +#import "SPMediaKeyTap.h" +#import "SPInvocationGrabbing/NSObject+SPInvocationGrabbing.h" // https://gist.github.com/511181, in submodule + +@interface SPMediaKeyTap () +-(BOOL)shouldInterceptMediaKeyEvents; +-(void)setShouldInterceptMediaKeyEvents:(BOOL)newSetting; +-(void)startWatchingAppSwitching; +-(void)stopWatchingAppSwitching; +-(void)eventTapThread; +@end + +static SPMediaKeyTap *singleton = nil; +static BOOL inited = NO; + +static pascal OSStatus appSwitched (EventHandlerCallRef nextHandler, EventRef evt, void* userData); +static pascal OSStatus appTerminated (EventHandlerCallRef nextHandler, EventRef evt, void* userData); +static CGEventRef tapEventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon); + +static NSArray *defaultMediaKeyUserBundleIdentifiers; + +// Inspired by http://gist.github.com/546311 + +@implementation SPMediaKeyTap + +#pragma mark - +#pragma mark Setup and teardown + +- (id)initWithDelegate:(id)delegate { + [SPMediaKeyTap initialize]; + _delegate = delegate; + [self startWatchingAppSwitching]; + singleton = self; + _mediaKeyAppList = [NSMutableArray new]; + _tapThreadRL = nil; + _eventPort = nil; + _eventPortSource = nil; + return self; +} + +- (void)dealloc { + [self stopWatchingMediaKeys]; + [self stopWatchingAppSwitching]; +// [_mediaKeyAppList release]; +// [super dealloc]; +} + +- (void)startWatchingAppSwitching { + // Listen to "app switched" event, so that we don't intercept media keys if we + // weren't the last "media key listening" app to be active + EventTypeSpec eventType = { kEventClassApplication, kEventAppFrontSwitched }; + OSStatus err = InstallApplicationEventHandler(NewEventHandlerUPP(appSwitched), 1, &eventType, (__bridge void *)self, &_app_switching_ref); + assert(err == noErr); + + eventType.eventKind = kEventAppTerminated; + err = InstallApplicationEventHandler(NewEventHandlerUPP(appTerminated), 1, &eventType, (__bridge void *)self, &_app_terminating_ref); + assert(err == noErr); +} +- (void)stopWatchingAppSwitching { + if (!_app_switching_ref) + return; + RemoveEventHandler(_app_switching_ref); + _app_switching_ref = NULL; +} + +- (void)startWatchingMediaKeys { + // Prevent having multiple mediaKeys threads + [self stopWatchingMediaKeys]; + + [self setShouldInterceptMediaKeyEvents:YES]; + + // Add an event tap to intercept the system defined media key events + _eventPort = CGEventTapCreate(kCGSessionEventTap, + kCGHeadInsertEventTap, + kCGEventTapOptionDefault, + CGEventMaskBit(NX_SYSDEFINED), + tapEventCallback, + (__bridge void *)self); + assert(_eventPort != NULL); + + _eventPortSource = CFMachPortCreateRunLoopSource(kCFAllocatorSystemDefault, _eventPort, 0); + assert(_eventPortSource != NULL); + + // Let's do this in a separate thread so that a slow app doesn't lag the event tap + [NSThread detachNewThreadSelector:@selector(eventTapThread) toTarget:self withObject:nil]; +} + +- (void)stopWatchingMediaKeys { + // TODO<nevyn>: Shut down thread, remove event tap port and source + + if (_tapThreadRL) { + CFRunLoopStop(_tapThreadRL); + _tapThreadRL = nil; + } + + if (_eventPort) { + CFMachPortInvalidate(_eventPort); + CFRelease(_eventPort); + _eventPort = nil; + } + + if (_eventPortSource) { + CFRelease(_eventPortSource); + _eventPortSource = nil; + } +} + +#pragma mark - +#pragma mark Accessors + ++ (BOOL)usesGlobalMediaKeyTap { + return [[NSUserDefaults standardUserDefaults] boolForKey:VKPCPreferencesCatchMediaButtons] + && floor(NSAppKitVersionNumber) >= NSAppKitVersionNumber10_5; +} + ++ (void)initialize { + if (inited) + return; + + defaultMediaKeyUserBundleIdentifiers = [NSArray arrayWithObjects: + [[NSBundle mainBundle] bundleIdentifier], // your app + @"com.apple.iTunes", + @"org.videolan.vlc", + // @"com.spotify.client", + @"com.apple.QuickTimePlayerX", + @"com.apple.quicktimeplayer", + // @"com.apple.iWork.Keynote", + // @"com.apple.iPhoto", + // @"com.apple.Aperture", + // @"com.plexsquared.Plex", + // @"com.soundcloud.desktop", + // @"org.niltsh.MPlayerX", + // @"com.ilabs.PandorasHelper", + // @"com.mahasoftware.pandabar", + // @"com.bitcartel.pandorajam", + // @"org.clementine-player.clementine", + // @"fm.last.Last.fm", + // @"fm.last.Scrobbler", + // @"com.beatport.BeatportPro", + // @"com.Timenut.SongKey", + // @"com.macromedia.fireworks", // the tap messes up their mouse input + // @"at.justp.Theremin", + // @"ru.ya.themblsha.YandexMusic", + // @"com.jriver.MediaCenter18", + // @"com.jriver.MediaCenter19", + // @"com.jriver.MediaCenter20", + // @"co.rackit.mate", + nil + ]; +// [defaultMediaKeyUserBundleIdentifiers retain]; + inited = YES; +} ++ (NSArray *)defaultMediaKeyUserBundleIdentifiers { + return defaultMediaKeyUserBundleIdentifiers; +} + + +- (BOOL)shouldInterceptMediaKeyEvents { + BOOL shouldIntercept = NO; + @synchronized(self) { + shouldIntercept = _shouldInterceptMediaKeyEvents; + } + return shouldIntercept; +} + +- (void)pauseTapOnTapThread:(BOOL)yeahno { + CGEventTapEnable(self->_eventPort, yeahno); +} +- (void)setShouldInterceptMediaKeyEvents:(BOOL)newSetting { + BOOL oldSetting; + @synchronized(self) { + oldSetting = _shouldInterceptMediaKeyEvents; + _shouldInterceptMediaKeyEvents = newSetting; + } + if (_tapThreadRL && oldSetting != newSetting) { + id grab = [self grab]; + [grab pauseTapOnTapThread:newSetting]; + NSTimer *timer = [NSTimer timerWithTimeInterval:0 invocation:[grab invocation] repeats:NO]; + CFRunLoopAddTimer(_tapThreadRL, (__bridge CFRunLoopTimerRef)timer, kCFRunLoopCommonModes); + } +} + +#pragma mark +#pragma mark - +#pragma mark Event tap callbacks + +// Note: method called on background thread + +static CGEventRef tapEventCallback2(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) { +// NSLog(@"tapEventCallback2() 1"); + SPMediaKeyTap *self = (__bridge id)refcon; + + if (type == kCGEventTapDisabledByTimeout) { + NSLog(@"Media key event tap was disabled by timeout"); + CGEventTapEnable(self->_eventPort, TRUE); + return event; + } else if (type == kCGEventTapDisabledByUserInput) { + // Was disabled manually by -[pauseTapOnTapThread] + return event; + } + + NSEvent *nsEvent = nil; + @try { + nsEvent = [NSEvent eventWithCGEvent:event]; + } + @catch (NSException * e) { + NSLog(@"Strange CGEventType: %d: %@", type, e); + assert(0); + return event; + } + + int keyCode = (([nsEvent data1] & 0xFFFF0000) >> 16); + +#ifdef DEBUG + int keyFlags = ([nsEvent data1] & 0x0000FFFF); + NSLog(@"Event: e.type=%lu, e.subtype=%d, e.keyCode=%d, e.keyFlags=%d, e.data1=%ld", + nsEvent.type, nsEvent.subtype, keyCode, keyFlags, nsEvent.data1); +#endif + + if (type != NX_SYSDEFINED || [nsEvent subtype] != SPSystemDefinedEventMediaKeys) + return event; + + if (keyCode != NX_KEYTYPE_PLAY && keyCode != NX_KEYTYPE_FAST && keyCode != NX_KEYTYPE_REWIND && keyCode != NX_KEYTYPE_PREVIOUS && keyCode != NX_KEYTYPE_NEXT) + return event; + + if (![self shouldInterceptMediaKeyEvents]) + return event; + +// [nsEvent retain]; // matched in handleAndReleaseMediaKeyEvent: + [self performSelectorOnMainThread:@selector(handleAndReleaseMediaKeyEvent:) withObject:nsEvent waitUntilDone:NO]; + + return NULL; +} + +static CGEventRef tapEventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) { + CGEventRef ret; + @autoreleasepool { + ret = tapEventCallback2(proxy, type, event, refcon); + } + return ret; +} + + +// event will have been retained in the other thread +- (void)handleAndReleaseMediaKeyEvent:(NSEvent *)event { + [_delegate mediaKeyTap:self receivedMediaKeyEvent:event]; +} + + +- (void)eventTapThread { + _tapThreadRL = CFRunLoopGetCurrent(); + CFRunLoopAddSource(_tapThreadRL, _eventPortSource, kCFRunLoopCommonModes); + CFRunLoopRun(); +} + +#pragma mark Task switching callbacks + +//NSString *kMediaKeyUsingBundleIdentifiersDefaultsKey = @"SPApplicationsNeedingMediaKeys"; +//NSString *kIgnoreMediaKeysDefaultsKey = @"SPIgnoreMediaKeys"; + +- (void)mediaKeyAppListChanged { + if ([_mediaKeyAppList count] == 0) return; + +// NSLog(@"--"); +// int i = 0; +// for (NSValue *psnv in _mediaKeyAppList) { +// ProcessSerialNumber psn; [psnv getValue:&psn]; +// NSDictionary *processInfo = [(id)ProcessInformationCopyDictionary( +// &psn, +// kProcessDictionaryIncludeAllInformationMask +// ) autorelease]; +// NSString *bundleIdentifier = [processInfo objectForKey:(id)kCFBundleIdentifierKey]; +// NSLog(@"%d: %@", i++, bundleIdentifier); +// } +// NSLog(@"--"); + + ProcessSerialNumber mySerial, topSerial; + GetCurrentProcess(&mySerial); + [[_mediaKeyAppList objectAtIndex:0] getValue:&topSerial]; + + Boolean same; + OSErr err = SameProcess(&mySerial, &topSerial, &same); + [self setShouldInterceptMediaKeyEvents:(err == noErr && same)]; +} + +- (void)appIsNowFrontmost:(ProcessSerialNumber)psn { +// NSLog(@"- appIsNowFrontmost: 1"); + NSValue *psnv = [NSValue valueWithBytes:&psn objCType:@encode(ProcessSerialNumber)]; + NSDictionary *processInfo = (__bridge id)ProcessInformationCopyDictionary( + &psn, + kProcessDictionaryIncludeAllInformationMask + ); +// NSLog(@"- appisnowfrontmost; processInfo: %@", processInfo); + NSString *bundleIdentifier = [processInfo objectForKey:(id)kCFBundleIdentifierKey]; + + // NSArray *whitelistIdentifiers = [[NSUserDefaults standardUserDefaults] arrayForKey:kMediaKeyUsingBundleIdentifiersDefaultsKey]; + if (![defaultMediaKeyUserBundleIdentifiers containsObject:bundleIdentifier]) { + if ([_mediaKeyAppList count] > 0) { + NSValue *tmpPsvn = [_mediaKeyAppList objectAtIndex:0]; + ProcessSerialNumber tmpPsn; + [tmpPsvn getValue:&tmpPsn]; + + NSDictionary *tmpProcessInfo = (__bridge id)ProcessInformationCopyDictionary( + &tmpPsn, + kProcessDictionaryIncludeAllInformationMask + ); + pid_t tmpPid = (pid_t)[(NSNumber *)[tmpProcessInfo objectForKey:(id)@"pid"] integerValue]; + if (tmpPid == VKPCPID) { + return; + } + } + + ProcessSerialNumber thisPsn; + GetProcessForPID(VKPCPID, &thisPsn); + + NSValue *thisPsnv = [NSValue valueWithBytes:&thisPsn objCType:@encode(ProcessSerialNumber)]; + + [_mediaKeyAppList removeObject:psnv]; + [_mediaKeyAppList removeObject:thisPsnv]; + [_mediaKeyAppList insertObject:thisPsnv atIndex:0]; + + [self mediaKeyAppListChanged]; + return; + } + + [_mediaKeyAppList removeObject:psnv]; + [_mediaKeyAppList insertObject:psnv atIndex:0]; + [self mediaKeyAppListChanged]; + +// NSLog(@"- appIsNowFrontmost: 2"); +} + +- (void)appTerminated:(ProcessSerialNumber)psn { +// NSLog(@"- appterminated"); + NSValue *psnv = [NSValue valueWithBytes:&psn objCType:@encode(ProcessSerialNumber)]; + [_mediaKeyAppList removeObject:psnv]; + [self mediaKeyAppListChanged]; +} + +static pascal OSStatus appSwitched (EventHandlerCallRef nextHandler, EventRef evt, void* userData) { +// NSLog(@"appswitched"); + SPMediaKeyTap *self = (__bridge id)userData; + + ProcessSerialNumber newSerial; + GetFrontProcess(&newSerial); + + [self appIsNowFrontmost:newSerial]; + + return CallNextEventHandler(nextHandler, evt); +} + +static pascal OSStatus appTerminated (EventHandlerCallRef nextHandler, EventRef evt, void* userData) { +// NSLog(@"appTermminated"); + SPMediaKeyTap *self = (__bridge id)userData; + + ProcessSerialNumber deadPSN; + + GetEventParameter( + evt, + kEventParamProcessID, + typeProcessSerialNumber, + NULL, + sizeof(deadPSN), + NULL, + &deadPSN + ); + + + [self appTerminated:deadPSN]; + return CallNextEventHandler(nextHandler, evt); +} + +@end
\ No newline at end of file diff --git a/VKPC/SSL/ssl_bundle.crt b/VKPC/SSL/ssl_bundle.crt new file mode 100644 index 0000000..89916c7 --- /dev/null +++ b/VKPC/SSL/ssl_bundle.crt @@ -0,0 +1,97 @@ +-----BEGIN CERTIFICATE----- +MIIGWTCCBUGgAwIBAgIRAN8G9vWS5CHcNT0GJDjy0GgwDQYJKoZIhvcNAQELBQAw +gZAxCzAJBgNVBAYTAkdCMRswGQYDVQQIExJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAO +BgNVBAcTB1NhbGZvcmQxGjAYBgNVBAoTEUNPTU9ETyBDQSBMaW1pdGVkMTYwNAYD +VQQDEy1DT01PRE8gUlNBIERvbWFpbiBWYWxpZGF0aW9uIFNlY3VyZSBTZXJ2ZXIg +Q0EwHhcNMTQxMDI3MDAwMDAwWhcNMTYxMDI2MjM1OTU5WjBXMSEwHwYDVQQLExhE +b21haW4gQ29udHJvbCBWYWxpZGF0ZWQxFDASBgNVBAsTC1Bvc2l0aXZlU1NMMRww +GgYDVQQDExN2a3BjLWxvY2FsLmNoMXAuY29tMIICHTANBgkqhkiG9w0BAQEFAAOC +AgoAMIICBQKCAfwA5qtpT7xe/Jv7e/e+eVKqoAxfrQKFH7A8AbVJbVy7nDuJ6JAu +A1FftWvu6yv7bUjyJZeF630t7Ensu4nVC515jEjmwhjYn5ffvIO4qP+bZfukSqG+ +tRz7XbExoxC43yWIN7rpqul0W6h7hu4gcF+Jnq6Khm33xNiAif7sqeSnYddEMAJb +jlsEcSnJQJ7KSfiegQyJHR7nkwYkoveLQOjyIOoog9/zOwxSi7kp7CAXlWwex8Ua +8A9gT2iT6obADD9YbbkQ9ucO+NYorcm8QiEtgWj8wUIQhoSgRFVtrbBkoScUnLAH +t4RLWEHDkZaQ4l8yrl/BMnMhd/YwwIu5mEILkPYUCNqg62TJlwFVnTEN4GgX0igI +iaBjt6DVq1S4AEFc/tPf9inUI5t8+9DSTAJM5A1bqsgxNpWaUgqLWq+pi+PW/egn +QR9nYhyE2D2LRo3NUukUxqJjW6bHQ1wMXIRWMYnDo4U3dUyT7ggPZ5IQxTS0+dWJ +xKChJnECmYtmXh9jOtXEe1k4EBarkVjrdF8R/0WWXY72hN1R6l34plOscyQ1A5As +9I/2UCPpdeXHUwdeCNsDLFvw6TVE7CfXzenBE/1WxYJr0rm6evnwQHb6ZpmiEIqg +BfB+4Sna3DtsAGK3zM/YUrGDa16s6WPwh1ur6J8PO5vRc0uqlX8PAgMBAAGjggHp +MIIB5TAfBgNVHSMEGDAWgBSQr2o6lFoL2JDqElZz30O0Oija5zAdBgNVHQ4EFgQU +7ilaEwa9NWC3sxDNJIDrlkfOgBowDgYDVR0PAQH/BAQDAgWgMAwGA1UdEwEB/wQC +MAAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCME8GA1UdIARIMEYwOgYL +KwYBBAGyMQECAgcwKzApBggrBgEFBQcCARYdaHR0cHM6Ly9zZWN1cmUuY29tb2Rv +LmNvbS9DUFMwCAYGZ4EMAQIBMFQGA1UdHwRNMEswSaBHoEWGQ2h0dHA6Ly9jcmwu +Y29tb2RvY2EuY29tL0NPTU9ET1JTQURvbWFpblZhbGlkYXRpb25TZWN1cmVTZXJ2 +ZXJDQS5jcmwwgYUGCCsGAQUFBwEBBHkwdzBPBggrBgEFBQcwAoZDaHR0cDovL2Ny +dC5jb21vZG9jYS5jb20vQ09NT0RPUlNBRG9tYWluVmFsaWRhdGlvblNlY3VyZVNl +cnZlckNBLmNydDAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuY29tb2RvY2EuY29t +MDcGA1UdEQQwMC6CE3ZrcGMtbG9jYWwuY2gxcC5jb22CF3d3dy52a3BjLWxvY2Fs +LmNoMXAuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQCFYP9kOKXKUZl7dP84mKoIqLyx +d/trxHpQA6h20opDmy1KLhr3c4sX4sTccFMvCWOHDWtI+AcogLUlZg+GqGlOG7kr +rm7hA5yJTOuF6IHm+LZmWEBbs8jfcJKWJsiRqQntQNQLLEOvdoVNhdtkAhykdsjW +nqpVDgPnh/L7KFYrphkHI5KDuEDnVKt3uNtYQhB3Hg5OOiEz0XGGg/8iYGxHliL9 +cvljwVBPUQReZgHt/IQE8QS3tE80TYV99Fp7LUFq/jesdy0EhuTGWiVmnekahK/W +ySd5JgKD2fzIk0w/8suEhsJh78SH7h9Ar+RFzHhaL8HaVlThpzjpmmH7tOH2 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIENjCCAx6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBvMQswCQYDVQQGEwJTRTEU +MBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFkZFRydXN0IEV4dGVybmFs +IFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBFeHRlcm5hbCBDQSBSb290 +MB4XDTAwMDUzMDEwNDgzOFoXDTIwMDUzMDEwNDgzOFowbzELMAkGA1UEBhMCU0Ux +FDASBgNVBAoTC0FkZFRydXN0IEFCMSYwJAYDVQQLEx1BZGRUcnVzdCBFeHRlcm5h +bCBUVFAgTmV0d29yazEiMCAGA1UEAxMZQWRkVHJ1c3QgRXh0ZXJuYWwgQ0EgUm9v +dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALf3GjPm8gAELTngTlvt +H7xsD821+iO2zt6bETOXpClMfZOfvUq8k+0DGuOPz+VtUFrWlymUWoCwSXrbLpX9 +uMq/NzgtHj6RQa1wVsfwTz/oMp50ysiQVOnGXw94nZpAPA6sYapeFI+eh6FqUNzX +mk6vBbOmcZSccbNQYArHE504B4YCqOmoaSYYkKtMsE8jqzpPhNjfzp/haW+710LX +a0Tkx63ubUFfclpxCDezeWWkWaCUN/cALw3CknLa0Dhy2xSoRcRdKn23tNbE7qzN +E0S3ySvdQwAl+mG5aWpYIxG3pzOPVnVZ9c0p10a3CitlttNCbxWyuHv77+ldU9U0 +WicCAwEAAaOB3DCB2TAdBgNVHQ4EFgQUrb2YejS0Jvf6xCZU7wO94CTLVBowCwYD +VR0PBAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wgZkGA1UdIwSBkTCBjoAUrb2YejS0 +Jvf6xCZU7wO94CTLVBqhc6RxMG8xCzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtBZGRU +cnVzdCBBQjEmMCQGA1UECxMdQWRkVHJ1c3QgRXh0ZXJuYWwgVFRQIE5ldHdvcmsx +IjAgBgNVBAMTGUFkZFRydXN0IEV4dGVybmFsIENBIFJvb3SCAQEwDQYJKoZIhvcN +AQEFBQADggEBALCb4IUlwtYj4g+WBpKdQZic2YR5gdkeWxQHIzZlj7DYd7usQWxH +YINRsPkyPef89iYTx4AWpb9a/IfPeHmJIZriTAcKhjW88t5RxNKWt9x+Tu5w/Rw5 +6wwCURQtjr0W4MHfRnXnJK3s9EK0hZNwEGe6nQY1ShjTK3rMUUKhemPR5ruhxSvC +Nr4TDea9Y355e6cJDUCrat2PisP29owaQgVR1EX1n6diIWgVIEM8med8vSTYqZEX +c4g/VhsxOBi0cQ+azcgOno4uG+GMmIPLHzHxREzGBHNJdmAPx/i9F4BrLunMTA5a +mnkPIAou1Z5jJh5VkpTYghdae9C8x49OhgQ= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGCDCCA/CgAwIBAgIQKy5u6tl1NmwUim7bo3yMBzANBgkqhkiG9w0BAQwFADCB +hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV +BAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTQwMjEy +MDAwMDAwWhcNMjkwMjExMjM1OTU5WjCBkDELMAkGA1UEBhMCR0IxGzAZBgNVBAgT +EkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR +Q09NT0RPIENBIExpbWl0ZWQxNjA0BgNVBAMTLUNPTU9ETyBSU0EgRG9tYWluIFZh +bGlkYXRpb24gU2VjdXJlIFNlcnZlciBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAI7CAhnhoFmk6zg1jSz9AdDTScBkxwtiBUUWOqigwAwCfx3M28Sh +bXcDow+G+eMGnD4LgYqbSRutA776S9uMIO3Vzl5ljj4Nr0zCsLdFXlIvNN5IJGS0 +Qa4Al/e+Z96e0HqnU4A7fK31llVvl0cKfIWLIpeNs4TgllfQcBhglo/uLQeTnaG6 +ytHNe+nEKpooIZFNb5JPJaXyejXdJtxGpdCsWTWM/06RQ1A/WZMebFEh7lgUq/51 +UHg+TLAchhP6a5i84DuUHoVS3AOTJBhuyydRReZw3iVDpA3hSqXttn7IzW3uLh0n +c13cRTCAquOyQQuvvUSH2rnlG51/ruWFgqUCAwEAAaOCAWUwggFhMB8GA1UdIwQY +MBaAFLuvfgI9+qbxPISOre44mOzZMjLUMB0GA1UdDgQWBBSQr2o6lFoL2JDqElZz +30O0Oija5zAOBgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNV +HSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwGwYDVR0gBBQwEjAGBgRVHSAAMAgG +BmeBDAECATBMBgNVHR8ERTBDMEGgP6A9hjtodHRwOi8vY3JsLmNvbW9kb2NhLmNv +bS9DT01PRE9SU0FDZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNybDBxBggrBgEFBQcB +AQRlMGMwOwYIKwYBBQUHMAKGL2h0dHA6Ly9jcnQuY29tb2RvY2EuY29tL0NPTU9E +T1JTQUFkZFRydXN0Q0EuY3J0MCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5jb21v +ZG9jYS5jb20wDQYJKoZIhvcNAQEMBQADggIBAE4rdk+SHGI2ibp3wScF9BzWRJ2p +mj6q1WZmAT7qSeaiNbz69t2Vjpk1mA42GHWx3d1Qcnyu3HeIzg/3kCDKo2cuH1Z/ +e+FE6kKVxF0NAVBGFfKBiVlsit2M8RKhjTpCipj4SzR7JzsItG8kO3KdY3RYPBps +P0/HEZrIqPW1N+8QRcZs2eBelSaz662jue5/DJpmNXMyYE7l3YphLG5SEXdoltMY +dVEVABt0iN3hxzgEQyjpFv3ZBdRdRydg1vs4O2xyopT4Qhrf7W8GjEXCBgCq5Ojc +2bXhc3js9iPc0d1sjhqPpepUfJa3w/5Vjo1JXvxku88+vZbrac2/4EjxYoIQ5QxG +V/Iz2tDIY+3GH5QFlkoakdH368+PUq4NCNk+qKBR6cGHdNXJ93SrLlP7u3r7l+L4 +HyaPs9Kg4DdbKDsx5Q5XLVq4rXmsXiBmGqW5prU5wfWYQ//u+aen/e7KJD2AFsQX +j4rBYKEMrltDR5FL1ZoXX/nUh8HCjLfn4g8wGTeGrODcQgPmlKidrv0PJFGUzpII +0fxQ8ANAe4hZ7Q7drNJ3gjTcBpUC2JD5Leo31Rpg0Gcg19hCC0Wvgmje3WYkN5Ap +lBlGGSW4gNfL1IYoakRwJiNiqZ+Gb7+6kHDSVneFeO/qJakXzlByjAA6quPbYzSf ++AZxAeKCINT+b72x +-----END CERTIFICATE----- + diff --git a/VKPC/SSL/vkpc-local.ch1p.com.key b/VKPC/SSL/vkpc-local.ch1p.com.key new file mode 100644 index 0000000..4bc23e4 --- /dev/null +++ b/VKPC/SSL/vkpc-local.ch1p.com.key @@ -0,0 +1,51 @@ +-----BEGIN PRIVATE KEY----- +MIIJJgIBADANBgkqhkiG9w0BAQEFAASCCRAwggkMAgEAAoIB/ADmq2lPvF78m/t7 +9755UqqgDF+tAoUfsDwBtUltXLucO4nokC4DUV+1a+7rK/ttSPIll4XrfS3sSey7 +idULnXmMSObCGNifl9+8g7io/5tl+6RKob61HPtdsTGjELjfJYg3uumq6XRbqHuG +7iBwX4meroqGbffE2ICJ/uyp5Kdh10QwAluOWwRxKclAnspJ+J6BDIkdHueTBiSi +94tA6PIg6iiD3/M7DFKLuSnsIBeVbB7HxRrwD2BPaJPqhsAMP1htuRD25w741iit +ybxCIS2BaPzBQhCGhKBEVW2tsGShJxScsAe3hEtYQcORlpDiXzKuX8EycyF39jDA +i7mYQguQ9hQI2qDrZMmXAVWdMQ3gaBfSKAiJoGO3oNWrVLgAQVz+09/2KdQjm3z7 +0NJMAkzkDVuqyDE2lZpSCotar6mL49b96CdBH2diHITYPYtGjc1S6RTGomNbpsdD +XAxchFYxicOjhTd1TJPuCA9nkhDFNLT51YnEoKEmcQKZi2ZeH2M61cR7WTgQFquR +WOt0XxH/RZZdjvaE3VHqXfimU6xzJDUDkCz0j/ZQI+l15cdTB14I2wMsW/DpNUTs +J9fN6cET/VbFgmvSubp6+fBAdvpmmaIQiqAF8H7hKdrcO2wAYrfMz9hSsYNrXqzp +Y/CHW6vonw87m9FzS6qVfw8CAwEAAQKCAftfm9S+s/k7Avwt2fYdG2KVjAtsDotX +Ixj8LEQPDEzG6Pa0am5n+aaN7/rLhyNjnMShSDi97tjGA64X3X1snwzuVJkxAlNn +cu/Nl1tG0Qt8Ld1fAcOmFikqIHPOfvPhqhDX6KVJrhIIm8D3oGPZr/++weGw7ouw +D3zXtc4qbRfjufINZCzGyHsC3KsKGccG+gHYw3VhmJmrmvQ8iBY0BVm2wtegMYcD +howAVfNGZ9benD3114De18C3qUUGBGw9p1Pjxr19WTAfe8TIo8FgN5hA918sS1HG +L9qt+CrJvz82fJhSibPpIPCJ2FhTU/EhowvCsvXFD2tj6aszY7a9lKr7dm5XQ+3T +Hhu78KVNJos262k3fDvmX5ZhUa7Dm7KAnx6bs73FHGEBMqIqO4fisZ3JSBmhSzH1 +Y6be0EOrNzzFbjAvxAzMIl6e91EiUI9CAvGJRhxb2AgsBpShWHRGw+eQ/9U3M+9K +okP+LoZz3AhuxtWEMSkui8NNwWDczRESEGTdRpcVV/bSkmSCrB03r9oPNO5AXWA7 +RTBdXP5aFhOLigcRvWZIBd2E+V9hGVMHi6lPqDmqVeQKNgD2LbcF3YvgjAfheHnG +gahLxsdInsuC+vK5JsYAQHoFkZwFzzrARKBfRTAYz5gp055R9RqOBjd+8bRX+jMQ +YH+UziECgf4PZojkT1Fa+OdkXhe+QcJd962otk7Jtf2wLWNTr9nDkG48JsvSZVOr +jn/XDchCDEsndY8VBfliUp2w8pTY4T1kth/4BAVu9r+T7KescerW7Md27vNBEogb +SEJfakFszVUbB1QbbI+3ZcSahKSXNNoF6UsSPGucVZQuQFzxivw8IrRj0rPxi+gJ +PXZzj/CyblT7RI4iOrw9l7L0KATA3yQ3MF7Gc0i9MYcDX5ncSErPs3g3uPaNsQot +LSoDSpyAQPkLZwpF0+/FTPw86aRZQf0mAeisYkv+LsifL2M8+9F98zyrgJ1D9XAS +IoWvZQPN+maQzpy+pJzIbjkiiFmjKwKB/g76YE39vH2vkGK+j9hYhY42ACvVcRJe +by4tzR6XWYRHr2igNtQnR1wF9GeOGtABxQ2m8H1qTNCPFr+FcwO56hKrYFun3VRR +CWmaARBI+CqNlX+OriUIv1WltbudUbqResMn1xDeV9YNHTXcd5iK6huWnnGYFFLw +HH8G3ZV+y0gfN9/Dg+GA4PokMZUElay1tSZXC3D2U4PNpJXupqWfMLgyBwtdRQRK +IDGS4Lys3mdtJo5/FFK8mnCsmZeUMDpVBD+ORCXiO1QkPPQGWsOieC9+mdZGjWJE +dvh5nijeq6NEXtgxwiKayjKmvHvmBUtatjdEPF8yszIhC3VR1TGtAoH+CRkNrbKd +4RxBti1P4Xl/lXuH/68ThF4a7EUj9IBiKQHd3XMd3cE2WJbqNuVRdJNGopz0Vfsd +hFRCDBKClE0bs69T23SksRTbwEuCEoGJtkZS35Px6HNGLLfXXf6rf9cde8Ysjbbo +f/OC3bWWdi0mz28uhitjdwunaltjjN4KfIwT/RSwcPWxsiO5IjPagc5kLbwmK1p/ +cfN4KHyrsjUgc+oDU0Evq3Tiyb9kaZnbeDARyPlfzfzUgjO4KhGduranaXEJxI6b +ivcb2/A6wvAU2KswBQZc1mBU7JVWDuTEsZ9MLBTi0w7fkhcK2r7WLIzMoqcsOgON +C2ryHdBnA9kCgf4LhO46LTIhK7wM5Wg1yGb253scOyyaJvy09E6z/0y+Wm+CNO3W +HltYX9AHMJhYb3QAuupsWiu3nsStCVJahEqMjzctZp3nMefeSq4Uuzn5aCiZEL/I +BcE5epeZMjXgGfDjVKmn9IhzPGTBvsxQCk5gZ2d2D7NZ0gbofvw99GUdE71iH7k2 +3mqoltt3My51xM4MLZfECf5S+P5ivUd3S0Yp83fNeq8QzH2UAtVK5lyqCJ1FrGCR +/TF+BJCfrj2lLyt0QdMRm7trS++A8b4uha6PQHvJ+zrTX7psAQTtg8tXZsbW2PXo +rUliyS9ezpo1yQ8o1BbmVFTS8e0hDo27hQKB/gFMyfRBAR4N+wTncrcVPbJLT7rP +dHUIVKG5lJlp//LCrCI9AVY2/N3J+r0vRvp+DUGfKrapFIttTBGEoz55N2yhJ9Zm +zgJLlhLh6UetB+PefwWzdatmFWe5fyhdYbcJDXIhjrb6Ui9OMkUFDMtAk5xR88VO +UIe4j8IhSV0VFgDTJhQxrSceLAnXxvQHm1VyPRVcIeVKGgLE/b36W0rJZv5hx4fQ +51HDa5Iga3rMbR80GZ3cXkUV5kBYGVvPrpclh6ccmnTIkw2RBtPJaC94afBSNXWL +kcX7plsoDiB/sJIFaae8kl0CLBgyrEf2RRjVncrCbEHoP7N8ev2LiISG +-----END PRIVATE KEY----- diff --git a/VKPC/Server.h b/VKPC/Server.h new file mode 100644 index 0000000..09157b5 --- /dev/null +++ b/VKPC/Server.h @@ -0,0 +1,27 @@ +// +// Server.h +// VKPC +// +// Created by Eugene on 11/29/13. +// Copyright (c) 2013 Eugene Z. All rights reserved. +// + +#import <Foundation/Foundation.h> + +typedef struct { + unsigned long receivedLength; + struct libwebsocket *wsi; + char *buffer; + NSInteger browser; + char *commandToSend; + unsigned long commandToSendLength; +} ServerSession; + +@interface Server : NSObject + ++ (void)start; ++ (BOOL)send:(NSString *)command forBrowser:(NSInteger)browser; ++ (NSThread *)thread; ++ (NSInteger)connectedCount:(NSInteger)browser; + +@end
\ No newline at end of file diff --git a/VKPC/Server.m b/VKPC/Server.m new file mode 100644 index 0000000..c00e481 --- /dev/null +++ b/VKPC/Server.m @@ -0,0 +1,455 @@ +// +// Server.m +// VKPC +// +// Created by Eugene on 11/29/13. +// Copyright (c) 2013-2014 Eugene Z. All rights reserved. +// + +#import "Server.h" +#import "Controller.h" + +#include <libwebsockets.h> +#include <pthread.h> + +#define CUSTOM_LOG 0 + +enum { + LWS_LOG_ERR = 1, + LWS_LOG_WARN = 2, + LWS_LOG_NOTICE = 4, + LWS_LOG_INFO = 8, + LWS_LOG_DEBUG = 16, + LWS_LOG_PARSER = 32, + LWS_LOG_HEADER = 64, + LWS_LOG_EXTENSION = 128, + LWS_LOG_CLIENT = 256, + LWS_LOG_LATENCY = 512 +}; + +#ifdef DEBUG +static const char *lws_callback_reasons[] = { + "LWS_CALLBACK_ESTABLISHED", + "LWS_CALLBACK_CLIENT_CONNECTION_ERROR", + "LWS_CALLBACK_CLIENT_FILTER_PRE_ESTABLISH", + "LWS_CALLBACK_CLIENT_ESTABLISHED", + "LWS_CALLBACK_CLOSED", + "LWS_CALLBACK_CLOSED_HTTP", + "LWS_CALLBACK_RECEIVE", + "LWS_CALLBACK_CLIENT_RECEIVE", + "LWS_CALLBACK_CLIENT_RECEIVE_PONG", + "LWS_CALLBACK_CLIENT_WRITEABLE", + "LWS_CALLBACK_SERVER_WRITEABLE", + "LWS_CALLBACK_HTTP", + "LWS_CALLBACK_HTTP_BODY", + "LWS_CALLBACK_HTTP_BODY_COMPLETION", + "LWS_CALLBACK_HTTP_FILE_COMPLETION", + "LWS_CALLBACK_HTTP_WRITEABLE", + "LWS_CALLBACK_FILTER_NETWORK_CONNECTION", + "LWS_CALLBACK_FILTER_HTTP_CONNECTION", + "LWS_CALLBACK_SERVER_NEW_CLIENT_INSTANTIATED", + "LWS_CALLBACK_FILTER_PROTOCOL_CONNECTION", + "LWS_CALLBACK_OPENSSL_LOAD_EXTRA_CLIENT_VERIFY_CERTS", + "LWS_CALLBACK_OPENSSL_LOAD_EXTRA_SERVER_VERIFY_CERTS", + "LWS_CALLBACK_OPENSSL_PERFORM_CLIENT_CERT_VERIFICATION", + "LWS_CALLBACK_CLIENT_APPEND_HANDSHAKE_HEADER", + "LWS_CALLBACK_CONFIRM_EXTENSION_OKAY", + "LWS_CALLBACK_CLIENT_CONFIRM_EXTENSION_SUPPORTED", + "LWS_CALLBACK_PROTOCOL_INIT", + "LWS_CALLBACK_PROTOCOL_DESTROY", + "LWS_CALLBACK_WSI_CREATE", + "LWS_CALLBACK_WSI_DESTROY", + "LWS_CALLBACK_GET_THREAD_ID", + "LWS_CALLBACK_ADD_POLL_FD", + "LWS_CALLBACK_DEL_POLL_FD", + "LWS_CALLBACK_CHANGE_MODE_POLL_FD", + "LWS_CALLBACK_LOCK_POLL", + "LWS_CALLBACK_UNLOCK_POLL", +}; +#endif + +static BOOL started = NO; +static NSMutableArray *sessions; +static NSMutableDictionary *connected; +static struct libwebsocket_context *context; + +static NSThread *thread; +static pthread_mutex_t mutex; +static struct { + char *command; + NSInteger browser; +} nextCommandToSend; + +static void ServerSession_Init(struct libwebsocket *wsi, ServerSession *s); +static void ServerSession_CreateString(ServerSession *session); +static void ServerSession_AppendString(ServerSession *session, const char *in); +static void ServerSession_DestroyString(ServerSession *session); +static void ServerSession_RecreateString(ServerSession *session); +static void AddSession(ServerSession *session); +static void DeleteSession(ServerSession *session); +static void incrConnected(NSInteger browser); +static void decrConnected(NSInteger browser); +static int SignalingCallback(struct libwebsocket_context *this, + struct libwebsocket *wsi, + enum libwebsocket_callback_reasons reason, + void *user, + void *in, + size_t len); +static void SendCommand(const char *command, NSInteger browser); +static void ServerStart(); + +// +// ServerSession +// +static void ServerSession_Init(struct libwebsocket *wsi, ServerSession *s) { + s->wsi = wsi; + s->browser = 0; + s->commandToSend = NULL; + s->commandToSendLength = 0; +} + +static void ServerSession_CreateString(ServerSession *session) { + session->buffer = NULL; + session->receivedLength = 0; +} + +static void ServerSession_AppendString(ServerSession *session, const char *in) { + unsigned long incLength = strlen(in); + unsigned long newLength = session->receivedLength + incLength; + + if (session->buffer == NULL) { + session->buffer = (char *)malloc(newLength + 1); + session->buffer[0] = '\0'; + } else { + session->buffer = realloc(session->buffer, newLength + 1); + } + + strcat(session->buffer, in); + session->receivedLength += incLength; +} + +static void ServerSession_DestroyString(ServerSession *session) { + if (session->buffer != NULL) + free(session->buffer); + session->receivedLength = 0; +} + +static void ServerSession_RecreateString(ServerSession *session) { + ServerSession_DestroyString(session); + ServerSession_CreateString(session); +} + +// +// Sessions +// +static void AddSession(ServerSession *session) { + [sessions addObject:[NSValue valueWithBytes:&session objCType:@encode(ServerSession*)]]; +// NSLog(@"[Server] AddSession, wsi points to: %p", session->wsi); +} + +static void DeleteSession(ServerSession *session) { + for (int i = 0; i < sessions.count; i++) { + ServerSession *s = (ServerSession *)[(NSValue *)sessions[i] pointerValue]; + if (s != NULL && s == session) { +#ifdef DEBUG +// NSLog(@"[DeleteSession] found, i=%d\n", i); +#endif + [sessions removeObjectAtIndex:i]; + decrConnected(s->browser); + break; + } + } +} + +static void incrConnected(NSInteger browser) { + NSNumber *key = [NSNumber numberWithInteger:browser]; + if (connected[key] == nil) { + connected[key] = @1; + } else { + NSNumber *count = (NSNumber *)connected[key]; + connected[key] = [NSNumber numberWithInteger:[count integerValue]+1]; + } +} + +static void decrConnected(NSInteger browser) { + NSNumber *key = [NSNumber numberWithInteger:browser]; + if (connected[key] != nil) { + NSNumber *count = (NSNumber *)connected[key]; + if ([count integerValue] > 0) { + connected[key] = [NSNumber numberWithInteger:[count integerValue]-1]; + } + } +} + +// server callbacks +static int SignalingCallback(struct libwebsocket_context *this, + struct libwebsocket *wsi, + enum libwebsocket_callback_reasons reason, + void *user, + void *in, + size_t len) { +#ifdef DEBUG + if (reason != LWS_CALLBACK_GET_THREAD_ID) { + printf("[SignalingCallback] >>> %s\n", lws_callback_reasons[reason]); + } +#endif + + ServerSession *session = (ServerSession *)user; + switch (reason) { + case LWS_CALLBACK_ESTABLISHED: { +#ifdef DEBUG +// lwsl_info("Connection established"); +#endif + +// session = ServerSession_Create(wsi); + ServerSession_Init(wsi, session); + ServerSession_CreateString(session); + AddSession(session); + + libwebsocket_callback_on_writable(context, wsi); + break; + } + + case LWS_CALLBACK_SERVER_WRITEABLE: { + if (session->commandToSend != NULL) { + unsigned char buf[LWS_SEND_BUFFER_PRE_PADDING + session->commandToSendLength + LWS_SEND_BUFFER_POST_PADDING]; + unsigned char *p = &buf[LWS_SEND_BUFFER_PRE_PADDING]; + strcpy((char *)p, session->commandToSend); + +// NSLog(@"[Server] LWS_CALLBACK_SERVER_WRITEABLE, commandToSend=%s", session->commandToSend); + + int m = libwebsocket_write(wsi, p, session->commandToSendLength, LWS_WRITE_TEXT); + if (m < session->commandToSendLength) { + lwsl_err("ERROR while writing %d bytes to socket\n", session->commandToSendLength); + return -1; + } + +// NSLog(@"before free() in writable callback"); + free(session->commandToSend); +// NSLog(@"after free() in writable callback"); + session->commandToSend = NULL; + session->commandToSendLength = 0; + } + break; + } + + case LWS_CALLBACK_RECEIVE: { +#ifdef DEBUG +// printf("[lws_callback_receive] \n"); +// printf("... received string: %s\n", (const char *)in); +// printf("... of length: %lu\n", strlen((const char *)in)); +// printf("... remaning packet: %lu\n", libwebsockets_remaining_packet_payload(wsi)); +#endif + + ServerSession_AppendString(session, (const char *)in); +#ifdef DEBUG +// printf("... after strcat: length: %lu\n", strlen(session->buffer)); +#endif + if (libwebsockets_remaining_packet_payload(wsi) == 0) { + if (session->receivedLength == 0) { + // + } else if (session->buffer != NULL && strcmp(session->buffer, "PING") == 0) { + ServerSession_RecreateString(session); + } else { + NSString *copy = [NSString stringWithUTF8String:session->buffer]; + + ServerSession_RecreateString(session); + + NSData *data = [copy dataUsingEncoding:NSUTF8StringEncoding]; + if (data != nil) { + NSError *error; + NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&error]; + if (error || !json || ![json isKindOfClass:[NSDictionary class]]) { + NSLog(@"[Server] Parse JSON error: %@; string was: %@", error, copy); + break; + } + + NSString *command = json[@"command"]; + if (command && ![command isEqual:[NSNull null]] && [command isEqualToString:@"setBrowser"]) { + NSInteger browserID = [(NSNumber *)json[@"_browser"] integerValue]; + session->browser = browserID; + + incrConnected(browserID); + + if (![Controller isASBrowser:browserID]) { + [Server send:[Controller JSONForCommand:@"set_sid" data:[NSNumber numberWithInt:VKPCSessionID]] forBrowser:browserID]; + } + } else { + #ifdef DEBUG + NSLog(@"in LWS_CALLBACK_RECEIVE: dispatch_async() now"); + #endif + + dispatch_async(dispatch_get_main_queue(), ^{ + [Controller handleClient:json]; + }); + } + } + } + } + break; + } + + case LWS_CALLBACK_CLOSED: { +#ifdef DEBUG +// lwsl_info("Connection closed\n"); +#endif + DeleteSession(session); + ServerSession_DestroyString(session); + break; + } + + default: + break; + } + + return 0; +} + +static void SendCommand(const char *command, NSInteger browser) { + unsigned long cstrlen = strlen(command); + for (int i = 0; i < sessions.count; i++) { + ServerSession *s = (ServerSession *)[(NSValue *)sessions[i] pointerValue]; + if (s != NULL && (s->browser == browser || browser == -1)) { + s->commandToSend = malloc(cstrlen + 1); + strcpy(s->commandToSend, command); + s->commandToSendLength = cstrlen; + +// NSLog(@"[Server] ready to send, wsi points to: %p", s->wsi); + libwebsocket_callback_on_writable(context, s->wsi); + } + } +} + +#ifdef DEBUG +#ifdef CUSTOM_LOG +static NSMutableString *syslog = nil; +static void emit_syslog(int level, const char *line) { + if (syslog == nil) { + syslog = [[NSMutableString alloc] init]; + } + + lwsl_emit_syslog(level, line); + [syslog appendString:[NSString stringWithFormat:@"[%d] %s", level, line]]; +// [syslog appendString:[NSString stringWithUTF8String:line]]; +// [syslog appendString:@"\n"]; +} +#endif +#endif + +static void ServerStart() { + sessions = [[NSMutableArray alloc] init]; + connected = [[NSMutableDictionary alloc] init]; + struct libwebsocket_protocols protocols[] = { + { "signaling-protocol", SignalingCallback, sizeof(ServerSession), 0 }, + { NULL, NULL, 0, 0 } + }; + + pthread_mutex_init(&mutex, NULL); + + nextCommandToSend.command = NULL; + nextCommandToSend.browser = 0; + + struct lws_context_creation_info info; + memset(&info, 0, sizeof(info)); + +#ifdef DEBUG +#ifdef CUSTOM_LOG + lws_set_log_level(LWS_LOG_ERR | LWS_LOG_WARN | LWS_LOG_NOTICE | LWS_LOG_INFO | LWS_LOG_DEBUG | LWS_LOG_HEADER, emit_syslog); +#else + lws_set_log_level(LWS_LOG_ERR | LWS_LOG_WARN | LWS_LOG_NOTICE | LWS_LOG_INFO | LWS_LOG_DEBUG | LWS_LOG_HEADER, NULL); +#endif +#else + lws_set_log_level(0, NULL); +#endif + + info.port = VKPCWSServerPort; + info.iface = VKPCWSServerHost; + info.protocols = protocols; + info.extensions = libwebsocket_get_internal_extensions(); +// info.ssl_cert_filepath = NULL; +// info.ssl_private_key_filepath = NULL; + info.ssl_cert_filepath = [[[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"ssl_bundle.crt"] UTF8String]; + info.ssl_private_key_filepath = [[[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"vkpc-local.ch1p.com.key"] UTF8String]; + info.gid = -1; + info.uid = -1; +#ifdef DEBUG + info.options = LWS_SERVER_OPTION_ALLOW_NON_SSL_ON_SSL_PORT; +#else + info.options = 0; +#endif + + context = libwebsocket_create_context(&info); + if (context == NULL) { + NSAlert *alert = [[NSAlert alloc] init]; + [alert setMessageText:@"VKPC Error"]; +#ifdef DEBUG +#ifdef CUSTOM_LOG + [alert setInformativeText:[NSString stringWithFormat:@"Local server failed to start on port %d\n\n%@", VKPCWSServerPort, syslog]]; +#else + [alert setInformativeText:[NSString stringWithFormat:@"Local server failed to start on port %d", VKPCWSServerPort]]; +#endif +#endif + [alert setAlertStyle:NSWarningAlertStyle]; + [alert runModal]; + + return; + } + + started = YES; + + while (1) { + pthread_mutex_lock(&mutex); + if (nextCommandToSend.command != NULL) { + SendCommand(nextCommandToSend.command, nextCommandToSend.browser); + free(nextCommandToSend.command); + nextCommandToSend.command = NULL; + nextCommandToSend.browser = 0; + } + pthread_mutex_unlock(&mutex); + libwebsocket_service(context, 50); + } + + libwebsocket_context_destroy(context); +} + +@implementation Server + ++ (void)start { + thread = [[NSThread alloc] initWithTarget:self selector:@selector(startThread) object:nil]; + [thread setName:@"server_thread"]; + [thread start]; +} + ++ (void)startThread { +// NSLog(@"[Server startThread] current thread: %@", [[NSThread currentThread] name]); + ServerStart(); +} + ++ (BOOL)send:(NSString *)command forBrowser:(NSInteger)browser { + if (!started) { + NSLog(@"[Server send:] server is not started yet, exising"); + return NO; + } + + pthread_mutex_lock(&mutex); + + const char *command_cstr = [command UTF8String]; + nextCommandToSend.command = malloc(strlen(command_cstr) + 1); + strcpy(nextCommandToSend.command, command_cstr); + nextCommandToSend.browser = browser; + + pthread_mutex_unlock(&mutex); + + return YES; +} + ++ (NSThread *)thread { + return thread; +} + ++ (NSInteger)connectedCount:(NSInteger)browser { + NSNumber *key = [NSNumber numberWithInteger:browser]; + return connected[key] == nil ? 0 : [(NSNumber *)connected[key] integerValue]; +} + +@end
\ No newline at end of file diff --git a/VKPC/ShadowTextFieldCell.h b/VKPC/ShadowTextFieldCell.h new file mode 100644 index 0000000..30e1451 --- /dev/null +++ b/VKPC/ShadowTextFieldCell.h @@ -0,0 +1,13 @@ +// +// ShadowTextFieldCell.h +// VKPC +// +// Created by Eugene on 12/2/13. +// Copyright (c) 2013 Eugene Z. All rights reserved. +// + +#import <Cocoa/Cocoa.h> + +@interface ShadowTextFieldCell : NSTextFieldCell + +@end diff --git a/VKPC/ShadowTextFieldCell.m b/VKPC/ShadowTextFieldCell.m new file mode 100644 index 0000000..8674048 --- /dev/null +++ b/VKPC/ShadowTextFieldCell.m @@ -0,0 +1,38 @@ +// +// ShadowTextFieldCell.m +// VKPC +// +// Created by Eugene on 12/2/13. +// Copyright (c) 2013-2014 Eugene Z. All rights reserved. +// + +#import "ShadowTextFieldCell.h" + +static NSShadow *kShadow = nil; + +@implementation ShadowTextFieldCell + ++ (void)initialize { + if (!VKPCIsYosemite) { + kShadow = [[NSShadow alloc] init]; + [kShadow setShadowColor:[NSColor colorWithCalibratedWhite:1.f alpha:0.85f]]; + [kShadow setShadowBlurRadius:0.f]; + [kShadow setShadowOffset:NSMakeSize(0.f, -1.f)]; + } +} + +- (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView *)controlView { + if (!VKPCIsYosemite) { + [kShadow set]; + } + [super drawInteriorWithFrame:cellFrame inView:controlView]; + +// [[NSColor colorWithCalibratedWhite:1.0 alpha:0.0] set]; +// NSRectFillUsingOperation(cellFrame, NSCompositeSourceOver); +} + +- (BOOL)isOpaque { + return NO; +} + +@end diff --git a/VKPC/Statistics.h b/VKPC/Statistics.h new file mode 100644 index 0000000..6802aba --- /dev/null +++ b/VKPC/Statistics.h @@ -0,0 +1,13 @@ +// +// Statistics.h +// VKPC +// +// Created by Eugene on 11/12/14. +// Copyright (c) 2014 Eugene Z. All rights reserved. +// + +#import <Foundation/Foundation.h> + +@interface Statistics : NSObject <NSURLConnectionDataDelegate> + +@end diff --git a/VKPC/Statistics.m b/VKPC/Statistics.m new file mode 100644 index 0000000..5b7e5b7 --- /dev/null +++ b/VKPC/Statistics.m @@ -0,0 +1,143 @@ +// +// Statistics.m +// VKPC +// +// Created by Eugene on 11/12/14. +// Copyright (c) 2014 Eugene Z. All rights reserved. +// + +#import "Statistics.h" + +@implementation Statistics { + NSMutableData *responseData; +} + +static BOOL initialized = NO; +static Statistics *instance; +static NSTimer *timer; + +static const float kNormalTimeout = 3600 * 6; +static const float kAfterFailureTimeout = 1200; + ++ (void)initialize { + if (initialized) { + return; + } + + instance = [[Statistics alloc] init]; + + long ts = GetTimestamp(); + long reported = [[NSUserDefaults standardUserDefaults] integerForKey:VKPCPreferencesStatisticReportedTimestamp]; + +// NSLog(@"[Statistics initialize] ts=%ld, reported=%ld", ts, reported); + + if (reported == 0 || ts - reported >= kNormalTimeout) { +// NSLog(@"[Statistics initalize] report now"); + [self report]; + } else { + [self initializeTimerWithTimeout:kNormalTimeout - (ts - reported)]; + } + + initialized = YES; +} + +//+ (void)timerCallback:(NSTimer *)timer { +// long ts = GetTimestamp(); +// long reported = [[NSUserDefaults standardUserDefaults] integerForKey:VKPCPreferencesStatisticReportedTimestamp]; +// +// NSLog(@"[Statistics timerCallback] ts=%ld, reported=%ld", ts, reported); +// +// if (ts - reported >= 3600 * 8) { +// [self report]; +// } else { +// [self initializeTimerWithTimeout:kNormalTimeout]; +// } +//} + ++ (void)initializeTimerWithTimeout:(float)timeout { + if (timer != nil) { + [timer invalidate]; + timer = nil; + } + + timer = [NSTimer scheduledTimerWithTimeInterval:timeout + target:[Statistics class] + selector:@selector(report) + userInfo:nil + repeats:NO]; +} + ++ (void)report { + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] + initWithURL:[NSURL URLWithString:@"https://ch1p.com/vkpc/usage.php"]]; + NSString *postData = [NSString stringWithFormat:@"app_v=%@&osx_v=%@&uuid=%@", getAppVersion(), getOSXVersion(), getUUID()]; + + [request setHTTPMethod:@"POST"]; + [request setHTTPBody:[postData dataUsingEncoding:NSUTF8StringEncoding]]; + + [instance mrProper]; + [[NSURLConnection alloc] initWithRequest:request delegate:instance]; +} + ++ (void)requestDone:(NSData *)data { + NSError *error; + NSArray *json = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&error]; + + if (error || !json) { + NSLog(@"[Statistics requestDone] error while parsing json: %@", error); + [self initializeTimerWithTimeout:kAfterFailureTimeout]; + return; + } + + NSString *result = json[0]; + NSLog(@"[Statistics requestDone] result: %@", result); + if ([result isEqualToString:@"ok"]) { + [[NSUserDefaults standardUserDefaults] setObject:[NSNumber numberWithInteger:GetTimestamp()] forKey:VKPCPreferencesStatisticReportedTimestamp]; + [self initializeTimerWithTimeout:kNormalTimeout]; + } else { + [self initializeTimerWithTimeout:kAfterFailureTimeout]; + } +} + ++ (void)requestFailed:(NSError *)error { + NSLog(@"[Statistics requestFailed] error: %@", [error description]); + [self initializeTimerWithTimeout:kAfterFailureTimeout]; +} + +- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { + responseData = [[NSMutableData alloc] init]; +} + +- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { + [responseData appendData:data]; +} + +- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error { + [Statistics requestFailed:error]; +} + +- (void)connectionDidFinishLoading:(NSURLConnection *)connection { + [Statistics requestDone:responseData]; +} + +- (void)mrProper { + responseData = nil; +} + +static NSString *getAppVersion() { + return [[[NSBundle mainBundle] infoDictionary] objectForKey:kCFBundleVersion]; +} + +static NSString *getOSXVersion() { + SInt32 major, minor, bugfix; + Gestalt(gestaltSystemVersionMajor, &major); + Gestalt(gestaltSystemVersionMinor, &minor); + Gestalt(gestaltSystemVersionBugFix, &bugfix); + return [NSString stringWithFormat:@"%d.%d.%d", major, minor, bugfix]; +} + +static NSString *getUUID() { + return [[NSUserDefaults standardUserDefaults] stringForKey:VKPCPreferencesUUID]; +} + +@end diff --git a/VKPC/TableView.h b/VKPC/TableView.h new file mode 100644 index 0000000..8f1e36b --- /dev/null +++ b/VKPC/TableView.h @@ -0,0 +1,13 @@ +// +// TableView.h +// VKPC +// +// Created by Eugene on 12/1/13. +// Copyright (c) 2013 Eugene Z. All rights reserved. +// + +#import <Cocoa/Cocoa.h> + +@interface TableView : NSTableView + +@end diff --git a/VKPC/TableView.m b/VKPC/TableView.m new file mode 100644 index 0000000..5a9370f --- /dev/null +++ b/VKPC/TableView.m @@ -0,0 +1,33 @@ +// +// TableView.m +// VKPC +// +// Created by Eugene on 12/1/13. +// Copyright (c) 2013 Eugene Z. All rights reserved. +// + +#import "TableView.h" + +@implementation TableView + +- (id)initWithFrame:(NSRect)frame +{ + self = [super initWithFrame:frame]; + if (self) { + // Initialization code here. + } + return self; +} + +- (BOOL)isOpaque { + return NO; +} + +- (void)drawRect:(NSRect)dirtyRect +{ + [super drawRect:dirtyRect]; + + // Drawing code here. +} + +@end diff --git a/VKPC/Types.h b/VKPC/Types.h new file mode 100644 index 0000000..6797d46 --- /dev/null +++ b/VKPC/Types.h @@ -0,0 +1,46 @@ +// +// Types.h +// VKPC +// +// Created by Eugene on 10/22/14. +// Copyright (c) 2014 Eugene Z. All rights reserved. +// + +#ifndef VKPC_Types_h +#define VKPC_Types_h + +typedef enum { + PlayingStatusUndefined = -1, + PlayingStatusNotPlaying = 0, + PlayingStatusPlaying = 1, + PlayingStatusPaused = 2 +} PlayingStatus; + +typedef struct { + NSInteger index; + PlayingStatus status; +} PlayingTrackStatus; + +typedef enum { + InterfaceStyleLegacy, + InterfaceStyleYosemite, + InterfaceStyleYosemiteDark +} InterfaceStyle; + +typedef enum { + PopoverStateSystemConfigurationRequired, + PopoverStatePlaylistNotLoaded, + PopoverStatePlaylistLoaded +} PopoverState; + +enum { + BrowserChrome = 0, + BrowserFirefox = 1, + BrowserSafari = 2, + BrowserOpera = 3, + BrowserYandex = 4, + + BrowsersCount = 5 +}; + +#endif diff --git a/VKPC/VKPC-Info.plist b/VKPC/VKPC-Info.plist new file mode 100644 index 0000000..f4f44f3 --- /dev/null +++ b/VKPC/VKPC-Info.plist @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleDevelopmentRegion</key> + <string>en</string> + <key>CFBundleDisplayName</key> + <string>VK Player Controller</string> + <key>CFBundleExecutable</key> + <string>${EXECUTABLE_NAME}</string> + <key>CFBundleGetInfoString</key> + <string></string> + <key>CFBundleIconFile</key> + <string>VKPC1.icns</string> + <key>CFBundleIdentifier</key> + <string>com.ch1p.$(PRODUCT_NAME:rfc1034identifier)</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundleName</key> + <string>${PRODUCT_NAME}</string> + <key>CFBundlePackageType</key> + <string>APPL</string> + <key>CFBundleShortVersionString</key> + <string>3.1.3</string> + <key>CFBundleSignature</key> + <string>????</string> + <key>CFBundleVersion</key> + <string>133</string> + <key>LSApplicationCategoryType</key> + <string>public.app-category.music</string> + <key>LSMinimumSystemVersion</key> + <string>${MACOSX_DEPLOYMENT_TARGET}</string> + <key>LSUIElement</key> + <true/> + <key>NSHumanReadableCopyright</key> + <string>Copyright © 2013-2015 Eugene Z</string> + <key>NSMainNibFile</key> + <string>MainMenu</string> + <key>NSPrincipalClass</key> + <string>Application</string> + <key>NSUserNotificationAlertStyle</key> + <string>alert</string> + <key>SUAllowsAutomaticUpdates</key> + <false/> + <key>SUEnableAutomaticChecks</key> + <false/> + <key>SUEnableSystemProfiling</key> + <false/> + <key>SUFeedURL</key> + <string>https://ch1p.com/vkpc/appcast.xml</string> + <key>SUShowReleaseNotes</key> + <false/> +</dict> +</plist> diff --git a/VKPC/VKPC-Prefix.pch b/VKPC/VKPC-Prefix.pch new file mode 100644 index 0000000..8894783 --- /dev/null +++ b/VKPC/VKPC-Prefix.pch @@ -0,0 +1,10 @@ +// +// Prefix header +// +// The contents of this file are implicitly included at the beginning of every source file. +// + +#ifdef __OBJC__ + #import <Cocoa/Cocoa.h> + #import "Global.h" +#endif diff --git a/VKPC/VKPC1.icns b/VKPC/VKPC1.icns Binary files differnew file mode 100644 index 0000000..ce19b9c --- /dev/null +++ b/VKPC/VKPC1.icns diff --git a/VKPC/VibrantButton.h b/VKPC/VibrantButton.h new file mode 100644 index 0000000..9babb22 --- /dev/null +++ b/VKPC/VibrantButton.h @@ -0,0 +1,13 @@ +// +// VKPCButton.h +// VKPC +// +// Created by Eugene on 10/28/14. +// Copyright (c) 2014 Eugene Z. All rights reserved. +// + +#import <Cocoa/Cocoa.h> + +@interface VibrantButton : NSButton + +@end diff --git a/VKPC/VibrantButton.m b/VKPC/VibrantButton.m new file mode 100644 index 0000000..64488cf --- /dev/null +++ b/VKPC/VibrantButton.m @@ -0,0 +1,23 @@ +// +// VKPCButton.m +// VKPC +// +// Created by Eugene on 10/28/14. +// Copyright (c) 2014 Eugene Z. All rights reserved. +// + +#import "VibrantButton.h" + +@implementation VibrantButton + +- (void)drawRect:(NSRect)dirtyRect { + [super drawRect:dirtyRect]; + + // Drawing code here. +} + +- (BOOL)allowsVibrancy { + return YES; +} + +@end diff --git a/VKPC/VibrantImageView.h b/VKPC/VibrantImageView.h new file mode 100644 index 0000000..39ff646 --- /dev/null +++ b/VKPC/VibrantImageView.h @@ -0,0 +1,13 @@ +// +// VibrantImageView.h +// VKPC +// +// Created by Eugene on 10/28/14. +// Copyright (c) 2014 Eugene Z. All rights reserved. +// + +#import <Cocoa/Cocoa.h> + +@interface VibrantImageView : NSImageView + +@end diff --git a/VKPC/VibrantImageView.m b/VKPC/VibrantImageView.m new file mode 100644 index 0000000..8b6381f --- /dev/null +++ b/VKPC/VibrantImageView.m @@ -0,0 +1,23 @@ +// +// VibrantImageView.m +// VKPC +// +// Created by Eugene on 10/28/14. +// Copyright (c) 2014 Eugene Z. All rights reserved. +// + +#import "VibrantImageView.h" + +@implementation VibrantImageView + +- (void)drawRect:(NSRect)dirtyRect { + [super drawRect:dirtyRect]; + + // Drawing code here. +} + +- (BOOL)allowsVibrancy { + return YES; +} + +@end diff --git a/VKPC/VibrantTextField.h b/VKPC/VibrantTextField.h new file mode 100644 index 0000000..711484f --- /dev/null +++ b/VKPC/VibrantTextField.h @@ -0,0 +1,13 @@ +// +// VKPCTextField.h +// VKPC +// +// Created by Eugene on 10/28/14. +// Copyright (c) 2014 Eugene Z. All rights reserved. +// + +#import <Cocoa/Cocoa.h> + +@interface VibrantTextField : NSTextField + +@end diff --git a/VKPC/VibrantTextField.m b/VKPC/VibrantTextField.m new file mode 100644 index 0000000..2fa3c2b --- /dev/null +++ b/VKPC/VibrantTextField.m @@ -0,0 +1,21 @@ +// +// VKPCTextField.m +// VKPC +// +// Created by Eugene on 10/28/14. +// Copyright (c) 2014 Eugene Z. All rights reserved. +// + +#import "VibrantTextField.h" + +@implementation VibrantTextField + +- (void)drawRect:(NSRect)dirtyRect { + [super drawRect:dirtyRect]; +} + +- (BOOL)allowsVibrancy { + return YES; +} + +@end diff --git a/VKPC/WindowController.h b/VKPC/WindowController.h new file mode 100644 index 0000000..435569e --- /dev/null +++ b/VKPC/WindowController.h @@ -0,0 +1,18 @@ +// +// WindowController.h +// VKPC +// +// Created by Eugene on 12/1/13. +// Copyright (c) 2013-2014 Eugene Z. All rights reserved. +// + +#import <Cocoa/Cocoa.h> + +@interface WindowController : NSWindowController<NSWindowDelegate> + +@property (strong) IBOutlet NSWindow *window; + +- (void)windowWillClose:(NSNotification *)notification; +- (BOOL)allowsClosingWithShortcut; + +@end diff --git a/VKPC/WindowController.m b/VKPC/WindowController.m new file mode 100644 index 0000000..247377a --- /dev/null +++ b/VKPC/WindowController.m @@ -0,0 +1,67 @@ +// +// WindowController.m +// VKPC +// +// Created by Eugene on 12/1/13. +// Copyright (c) 2013-2014 Eugene Z. All rights reserved. +// + +#import "WindowController.h" + +@implementation WindowController { + id eventMonitor; + BOOL eventMonitorSet; +} + +- (id)initWithWindow:(NSWindow *)window { + self = [super initWithWindow:window]; + if (self) { + // Initialization code here. + } + return self; +} + +- (BOOL)allowsClosingWithShortcut { + return NO; +} + +- (void)showWindow:(id)sender { + [super showWindow:sender]; + + if ([self allowsClosingWithShortcut] && !eventMonitorSet) { + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(windowWillClose:) + name:NSWindowWillCloseNotification + object:self.window]; + + // Close window on esc or cmd+w + NSEvent *(^handler)(NSEvent *) = ^(NSEvent *theEvent) { + NSWindow *targetWindow = theEvent.window; + if (targetWindow != self.window) { + return theEvent; + } + + NSEvent *result = theEvent; + if (theEvent.keyCode == 53 || ( theEvent.keyCode == 13 && [theEvent modifierFlags] & NSCommandKeyMask )) { + [self.window close]; + } + + return result; + }; + + eventMonitor = [NSEvent addLocalMonitorForEventsMatchingMask:NSKeyDownMask handler:handler]; + eventMonitorSet = YES; + } +} + +- (void)windowDidLoad { + eventMonitorSet = NO; + [super windowDidLoad]; +} + +- (void)windowWillClose:(NSNotification *)notification { + // [NSEvent removeMonitor:eventMonitor]; +} + + +@end diff --git a/VKPC/en.lproj/InfoPlist.strings b/VKPC/en.lproj/InfoPlist.strings new file mode 100644 index 0000000..477b28f --- /dev/null +++ b/VKPC/en.lproj/InfoPlist.strings @@ -0,0 +1,2 @@ +/* Localized versions of Info.plist keys */ + diff --git a/VKPC/main.m b/VKPC/main.m new file mode 100644 index 0000000..cb57fe2 --- /dev/null +++ b/VKPC/main.m @@ -0,0 +1,42 @@ +// +// main.m +// VKPC +// +// Created by Eugene on 11/26/13. +// Copyright (c) 2013-2014 Eugene Z. All rights reserved. +// + +#import <Cocoa/Cocoa.h> +#import "Global.h" +//#import "HostsHack.h" + +#include <string.h> +#include <unistd.h> +#include <signal.h> + +//int doHostsHack() { +// if (geteuid() != 0) { +// NSLog(@"Run as root to hack hosts"); +// return -2; +// } +// +// [HostsHack doHack]; +// [[NSDistributedNotificationCenter defaultCenter] postNotificationName: VKPCHostsHackTaskFinished object:nil userInfo:nil deliverImmediately:YES]; +// +// return 0; +//} + +int main(int argc, const char * argv[]) { + VKPCInitGlobals(); + signal(SIGPIPE, SIG_IGN); + +// if (argc > 1) { +// for (int i = 1; i < argc; i++) { +// if (strcmp(argv[i], "--hostshack") == 0) { +// return doHostsHack(); +// } +// } +// } + + return NSApplicationMain(argc, argv); +}
\ No newline at end of file diff --git a/VKPC/scripts/inject.as b/VKPC/scripts/inject.as new file mode 100644 index 0000000..34b07aa --- /dev/null +++ b/VKPC/scripts/inject.as @@ -0,0 +1,173 @@ +set js to "{js}" + +set allWindows to null +set allTabs to {} + +set okTab_nowPlaying to null +set okTab_playlistFound to null +set okTab_lsSource to null +set okTab_recentlyPlayed to null +set okTab_havePlaylist to null +set activeTab to null +set lastTab to null +set execTab to null +set outdatedTabs to {} +--set tabsWithPlayingMusic to {} + +set vkTabFound to 0 +set lsSourceId to null +set playlistID to {playlistID} +set returnValue to 0 +set command to "{command}" +set appPlaylistFound to 0 +set appName to "{appName}" + +if application "{appName}" is running then + tell application "{appName}" + set allWindows to every window + + repeat with currentWindow in allWindows + try + set allTabs to allTabs & every tab of currentWindow + onsuccess + if activeTab is null and class of ({ASCurrentTab} of currentWindow) is tab then set activeTab to ({ASCurrentTab} of currentWindow) + end try + end repeat + + repeat with currentTab in allTabs + try + set tabURL to (URL of currentTab) + set tabTitle to ({ASTabTitle} currentTab) + + if tabTitle is not equal to "" then + if (tabURL starts with "http://vk.com" or tabURL starts with "https://vk.com") and tabURL does not contain "view-source:" then + set vkTabFound to 1 + tell currentTab to {ASExecuteJS} js + + set results to result + + -- only for injection timer + if command is "afterInjection" then + -- set injectResult to item 1 of results + set _plid to item 6 of results + set _havePlaylist to item 2 of results + set _isPlaying to item 3 of results + + if _plid is not 0 and _plid is playlistID then + set appPlaylistFound to 1 + end if + + if _havePlaylist is 1 and _plid is not 0 and _plid is not playlistID then + set end of outdatedTabs to currentTab + end if + + if _havePlaylist is 1 then + set okTab_havePlaylist to currentTab + end if + + if _isPlaying is 1 then + set okTab_nowPlaying to currentTab + end if + else + -- get global info (for first time) + -- try + if lsSourceId is null then + -- tell currentTab to {ASExecuteJS} "VKPC.getLastInstanceId()" + set lsSourceId to item 7 of results + end if + -- end try + + -- get tab info + -- tell currentTab to {ASExecuteJS} "VKPC.getParams()" + -- set params to result + + try + set _havePlayer to item 1 of results + set _havePlaylist to item 2 of results + set _isPlaying to item 3 of results + set _tabId to item 4 of results + set _trackId to item 5 of results + set _playlistId to item 6 of results + + -- for safari: track all tabs with now playing music + --if appName is "Safari" and _isPlaying is true then + -- set end of tabsWithPlayingMusic to currentTab + --end if + + -- check playlist id + if playlistID is not 0 and _playlistId is playlistID then + set okTab_playlistFound to currentTab + end if + + -- set last VK tab + set lastTab to currentTab + + -- set recently played tab + if _havePlayer and ( _isPlaying or class of _trackId is text ) then + set okTab_recentlyPlayed to currentTab + end if + + -- set now playing tab + if _isPlaying = true then + set okTab_nowPlaying to currentTab + end if + + -- set 'found by ls source' tab + if lsSourceId is not null and lsSourceId is not missing value and lsSourceId is _tabId then + set okTab_lsSource to currentTab + end if + end try + end if + end if + end if + end try + end repeat + + set execCommand to "VKPC.executeCommand('{command}', {playlistID})" + + if command is not "afterInjection" then + set tabsToCheck to {} + if appName is "Safari" then + set end of tabsToCheck to okTab_playlistFound + set end of tabsToCheck to okTab_nowPlaying + else + set end of tabsToCheck to okTab_nowPlaying + set end of tabsToCheck to okTab_playlistFound + end if + + set end of tabsToCheck to okTab_lsSource + set end of tabsToCheck to okTab_recentlyPlayed + set end of tabsToCheck to okTab_havePlaylist + set end of tabsToCheck to activeTab + set end of tabsToCheck to lastTab + + set finExecTab to null + + repeat with execTab in tabsToCheck + if class of execTab is tab then + tell execTab to {ASExecuteJS} execCommand + set finExecTab to execTab + exit repeat + end if + end repeat + else + if appPlaylistFound is 0 then + if okTab_nowPlaying is not null then + tell okTab_nowPlaying to {ASExecuteJS} execCommand + else if okTab_havePlaylist is not null then + tell okTab_havePlaylist to {ASExecuteJS} execCommand + else + set returnValue to 1 + end if + end if + + repeat with outdatedTab in outdatedTabs + tell outdatedTab to {ASExecuteJS} "VKPC.clearPlaylist(true, 'as')" + end repeat + end if + + if vkTabFound is 0 then set returnValue to 1 + end tell +end if + +return returnValue diff --git a/VKPC/scripts/inject.js b/VKPC/scripts/inject.js new file mode 100644 index 0000000..aa0c68a --- /dev/null +++ b/VKPC/scripts/inject.js @@ -0,0 +1,847 @@ +// VKPC for Chrome and Safari + +(function(vkpc_sid) { +if (!window.VKPC) { + +var _debug = window.__vkpc_debug || true; + +if (!document.addEventListener) { + window.console && console.log("[VKPC] an outdated browser detected, very strange, plz update"); + return; +} + +var browser = (function() { + var ua = navigator.userAgent.toLowerCase(); + var browser = { + id: null, + chrome: false, + safari: false, + yandex: false, + firefox: false, + opera: false + }; + + if (/opr/i.test(ua) && /chrome/i.test(ua)) { + browser.opera = true; + browser.id = 3; + } else if (/yabrowser/i.test(ua) && /chrome/i.test(ua)) { + browser.yandex = true; + browser.id = 4; + } else if (/firefox|iceweasel/i.test(ua)) { + browser.firefox = true; + browser.id = 1; + } else if (!(/chrome/i.test(ua)) && /webkit|safari|khtml/i.test(ua)) { + browser.safari = true; + browser.id = 2; + } else if (/chrome/i.test(ua)) { + browser.chrome = true; + browser.id = 0; + } + + return browser; +})(); + +// functions +(function(window, document) { + var queue = [], done = false, _top = true, root = document.documentElement, eventsAdded = false; + + function init(e) { + if (e.type == 'readystatechange' && document.readyState != 'complete') return; + (e.type == 'load' ? window : document).removeEventListener(e.type, init); + if (!done) { + done = true; + while (queue.length) { + queue.shift().call(window); + } + } + } + function poll() { + try { + root.doScroll('left'); + } catch (e) { + setTimeout(poll, 50); + return; + } + init('poll'); + } + + window.DOMContentLoaded = function(fn) { + if (document.readyState == 'complete' || done) { + fn.call(window); + } else { + queue.push(fn); + + if (!eventsAdded) { + if (document.createEventObject && root.doScroll) { + try { + _top = !window.frameElement; + } catch (e) {} + if (_top) poll(); + } + + document.addEventListener('DOMContentLoaded', init); + document.addEventListener('readystatechange', init); + window.addEventListener('load', init); + } + } + } +})(window, document); + +function log() { + if (!_debug) + return; + var args = Array.prototype.slice.call(arguments); + args.unshift(window.VKPC ? '[VKPC '+window.VKPC.getSID()+']' : '[VKPC]'); + try { + window.console && console.log.apply(console, args); + } catch (e) {} +} +function trim(string) { + return string.replace(/(^\s+)|(\s+$)/g, ""); +} +function startsWith(str, needle) { + return str.indexOf(needle) == 0; +} +function endsWith(str, suffix) { + return str.indexOf(suffix, str.length - suffix.length) !== -1; +} +function random(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; +} +function shuffle(o) { + for (var j, x, i = o.length; i; j = Math.floor(Math.random() * i), x = o[--i], o[i] = o[j], o[j] = x); + return o; +} +function getStackTrace(split) { + split = split === undefined ? true : split; + try { + o.lo.lo += 0; + } catch(e) { + if (e.stack) { + return split ? e.stack.split('\n') : e.stack; + } + } + return null; +} +function buildQueryString(obj) { + var list = [], i; + for (i in obj) { + list.push(encodeURIComponent(i) + '=' + encodeURIComponent(obj[i])); + } + return list.join('&'); +} +function stripTags(html) { + var div = document.createElement("div"); + div.innerHTML = html; + return div.textContent || div.innerText || ""; +} +function decodeEntities(value) { + var textarea = document.createElement('textarea'); + textarea.innerHTML = value; + return textarea.value; +} +function extend(dest, source) { + for (var i in source) { + dest[i] = source[i]; + } + return dest; +} +function intval(value) { + if (value === true) return 1; + return parseInt(value) || 0; +} + +var WSC_STATE_NONE = 'NONE', + WSC_STATE_OK = 'OK', + WSC_STATE_CLOSED = 'CLOSED', + WSC_STATE_ERR = 'ERR'; +function WSClient(address, protocol, opts) { + this.state = WSC_STATE_NONE; + this._ws = null; + + this.address = address; + this.protocol = protocol; + + this._onmessage = opts.onmessage; + this._onclose = opts.onclose; + this._onerror = opts.onerror; + this._onopen = opts.onopen; + + this._pingTimer = null; + this._reconnectTimer = null; +} +extend(WSClient.prototype, { + connect: function() { + this.state = WSC_STATE_NONE; + var self = this; + + var _websocket = window.WebSocket || window.MozWebSocket; + if (!_websocket) { + log('[WSClient connect] websockets are not supported'); + return; + } + + this._ws = new _websocket(this.address, this.protocol); + this._ws.onopen = function() { + self.state = WSC_STATE_OK; + self._setTimers(); + self._onopen && self._onopen.apply(self); + }; + this._ws.onerror = function() { + self._unsetTimers(); + if (self.state != WSC_STATE_ERR) { + self.state = WSC_STATE_ERR; + } + self._onerror && self._onerror.apply(self); + }; + this._ws.onclose = function() { + self._unsetTimers(); + if (self.state != WSC_STATE_ERR) { + self.state = WSC_STATE_ERR; + } + self._onclose && self._onclose.apply(self); + }; + this._ws.onmessage = function(e) { + self._onmessage && self._onmessage.apply(self, [e.data]); + }; + }, + close: function() { + this._unsetTimers(); + this._ws.close(); + }, + reconnect: function() { + var self = this; + if (this.state == WSC_STATE_OK) { + log('[WSClient reconnect] state = '+this.state+', why reconnect?'); + return; + } + clearTimeout(this._reconnectTimer); + this._reconnectTimer = setTimeout(function() { + self.connect(); + }, 3000); + }, + send: function(obj) { + obj._browser = browser.id; + var self = this; + this._waitForConnection(function() { + self._ws.send(JSON.stringify(obj)); + }, 200); + }, + _setTimers: function() { + var self = this; + this._pingTimer = setInterval(function() { + try { + self._ws.send("PING"); + } catch (e) { + log('[WSClient _pingTimer]', e); + } + }, 30000); + }, + _unsetTimers: function() { + clearInterval(this._pingTimer); + }, + _waitForConnection: function(callback, interval) { + if (this._ws.readyState === 1) { + callback(); + } else { + var self = this; + setTimeout(function() { + self._waitForConnection(callback, interval); + }, interval); + } + } +}); + +var client = new WSClient("wss://vkpc-local.ch1p.com:56130", "signaling-protocol", { + onopen: function() { + this.send({command: 'setBrowser'}); + }, + onmessage: function(cmd) { + // pass + }, + onerror: function() { + this.reconnect(); + }, + onclose: function() { + this.reconnect(); + } +}); +client.connect(); + +window.VKPC = new function() { + var _sid = null; + var _currentTrackId = null; + var _lastPlaylistSummary = null; + var _lastPlaylistId = 0; + var _operateQueue = []; + var _setTrackIdTimeout = null; + var _watchGraphicsChange = false; + var _checkPlaylistTimer = null; + + function wrapAudioMethods() { + // var self = this; + if (window.audioPlayer) { + if (!audioPlayer.__operate) { + audioPlayer.__operate = audioPlayer.operate; + audioPlayer.operate = function(id, nextPlaylist, opts) { + var currentId = audioPlayer.id, _status = id != currentId ? 'play' : null; + audioPlayer.__operate.apply(audioPlayer, arguments); + //self.firstOperateAfterPlaylistUpdating = false; + log('operate(), arguments:', arguments); + + if (existsInCurrentPlaylist(id)) { + log('operate(), found in current pl, setTrackId() now'); + setTrackId(id, _status); + } else { + log('operate(), not found, setToOperateQueue() now'); + setToOperateQueue(id, _status); + } + }; + } + + // disable it + if (false && !audioPlayer.__setGraphics) { + audioPlayer.__setGraphics = audioPlayer.setGraphics; + audioPlayer.setGraphics = function(act) { + audioPlayer.__setGraphics.apply(audioPlayer, arguments); + return; + /*if (self.watchGraphicsChange) { + if (browser.safari) self.sendOperateTrack(audioPlayer.id, (act == 'play' || act == 'load') ? 'play' : 'pause'); + self.watchGraphicsChange = false; + }*/ + }; + } + } + + log('[wrapAudioMethods] wrapped DONE'); + } + + function clear() { + log('clear()'); + _currentTrackId = null; + _lastPlaylistSummary = null; + _lastPlaylistId = null; + _sid = null; + _watchGraphicsChange = false; + } + + /*function getBrowser() { + return browser.safari ? 'safari' : 'chrome'; + }*/ + + function executeCommand(command, plid) { + if (command == 'afterInjection') { + log('executeCommand: afterInjection'); + var pl = padAudioPlaylist(); + if (window.audioPlayer && pl) { + updatePlaylist(getPlaylist(pl)); + } else { + clearPlaylist(); + } + return; + } + + log('executeCommand:', command, plid); + // var self = this; + + if (!window.audioPlayer || !padAudioPlaylist()) { + log('[executeCommand] audioplayer or playlist not found'); + stManager.add(['audioplayer.js'], function() { + executeAfterPadLoading(function() { + log('[executeCommand] after execafterpadloading, window.audioPlayer:', window.audioPlayer); + wrapAudioMethods(); + + var plist = padAudioPlaylist(); + if (plist) { + log('[executeCommand] after exec...: send updatePlaylist() with plist'); + updatePlaylist(getPlaylist(plist)); + } + + if (command == 'playpause' || command == 'next' || command == 'prev') { + log('[executeCommand] after exec...: simple command'); + var id = getPlayFirstId(); + if (id) { + log('[executeCommand] after exec...: found id='+id+', playAudioNew() now'); + playAudioNew(id); + } else if (plist && plist.start) { + log('[executeCommand] after exec...: found plist.start, playAudioNew() now'); + playAudioNew(plist.start); + } + } else if (startsWith(command, 'operateTrack:')) { // TODO this is new fix + var id = parseInt(command.replace('operateTrack:')); + log('[executeCommand] after exec...: got operateTrack, id='+id); + if (!plist[id]) { + log('[executeCommand] after exec...: after got operateTrack: plist[id] not found, send new pl to app'); + //self.clearPlaylist(); + updatePlaylist(getPlaylist(plist)); + if (plist.start) { + log('[executeCommand] after exec...: got operateTrack, pl not found... ... play plist.start now'); + playAudioNew(plist.start); + } + } else { + log('[executeCommand] after exec...: got operateTrack, it is found, playAudioNew() now'); + playAudioNew(id); + } + } + }); + }); + return; + } + + function evaluateCommand(command) { + switch (command) { + case 'next': + case 'prev': + case 'playpause': + if (audioPlayer.id) { + if (command == 'next') next(); + else if (command == 'prev') prev(); + else if (command == 'playpause') playPause(); + } else { + var id = getPlayFirstId(); + if (id) playId(id); + } + break; + + default: + if (startsWith(command, 'operateTrack:')) { + log('[executeCommand] got operateTrack;'); + var id = command.replace('operateTrack:', ''), pl = padAudioPlaylist(); + if (pl[id] !== undefined) { + log('[executeCommand] got operateTrack; track is found, playAudioNew() now'); + //playAudioNew(id); + //audioPlayer.operate(id); + playId(id); + } else { + log('[executeCommand] got operateTrack; track not found, updatePlaylist with pl:', pl); + updatePlaylist(getPlaylist(pl)); + var id = getPlayFirstId(); + if (id) { + log('[executeCommand] got operateTrack; play id from getPlayFirstId() now'); + playId(id); + } + } + } + break; + } + } + + if (plid != _lastPlaylistId) { + log('[executeCommand] plid does not match'); + var pl = padAudioPlaylist(); + if (pl) { + updatePlaylist(getPlaylist(pl), true); + log('[executeCommand] plid does not match, sent updatePlaylist() with pl:', pl); + + if (plid == 0) { + evaluateCommand(command); + } else { + if (['next', 'prev', 'playpause'].indexOf(command) != -1) { + var id = audioPlayer.id || pl.start || getPlayFirstId(); + if (id) { + playId(id); + } + } + } + } + } else { + evaluateCommand(command); + } + } + + function setTrackId(id, _status) { + _status = _status || (audioPlayer.player.paused() ? 'pause' : 'play'); + clearTimeout(_setTrackIdTimeout); + + var check = function() { + if (audioPlayer.player) { + sendOperateTrack(id, _status); + } else { + _setTrackIdTimeout = setTimeout(check, 200); + } + }; + check(); + } + + function sendOperateTrack(id, _status) { + log('[sendOperateTrack]', id, _status); + client.send({ + command: 'operateTrack', + data: { + 'id': id, + 'status': _status, + 'playlistId': _lastPlaylistId + } + }); + } + + function setToOperateQueue(id, _status) { + var q = _operateQueue; + for (var i = 0; i < q.length; i++) { + var track = q[i]; + if (track[0] == id) { + track[1] = _status; + return; + } + } + q.push([id, _status]); + } + + function existsInCurrentPlaylist(id) { + return _lastPlaylistSummary && _lastPlaylistSummary.indexOf(id) != -1; + } + + function processOperateQueue(pl) { + log('[processOperateQueue]'); + var q = _operateQueue; + while (q.length) { + var track = q.shift(); + log('[processOperateQueue] track:', track[0]); + if (pl[track[0]] !== undefined) { + log('[processOperateQueue] track', track[0], 'found, send it now'); + sendOperateTrack(track[0], track[1]); + } + } + } + + function clearOperateQueue() { + _operateQueue = []; + } + + function printPlaylist() { + var pl = padAudioPlaylist(); + if (pl) { + for (var k in pl) { + log(pl[k][5] + ' - ' + pl[k][6]); + } + } + } + + function getPlaylist(_pl) { + _pl = _pl || padAudioPlaylist(); + var pl = null; + if (_pl) { + var start = _pl.start, pl = []; + var nextId = start; + do { + if (_pl[nextId]) { + _pl[nextId]._vkpcId = nextId; + pl.push(_pl[nextId]); + nextId = _pl[nextId]._next; + } + } while (nextId != '' && nextId !== undefined && nextId != start); + } + return pl; + } + + // force=true is used when plids not match + function updatePlaylist(pl, force) { + var tracks = [], summary = [], title; + if (pl) { + for (var k = 0; k < pl.length; k++) { + tracks.push({ + id: pl[k]._vkpcId, + artist: decodeEntities(pl[k][5]), + title: decodeEntities(pl[k][6]), + duration: pl[k][4] + }); + summary.push(pl[k]._vkpcId); + } + + summary = summary.join(';'); + + log("updatePlaylist: _lastPlaylistSummary:", _lastPlaylistSummary, 'summary:', summary); + if (force || _lastPlaylistSummary === null || _lastPlaylistSummary !== summary) { + log('[updatePlaylist] last summary not matched;', _lastPlaylistSummary, summary); + var activeId = '', activeStatus = ''; + var vkpl = padAudioPlaylist(); + var plTitle = (window.audioPlaylist && window.audioPlaylist.htitle) || vkpl.htitle; + if (audioPlayer.id && vkpl[audioPlayer.id] !== undefined) { + activeId = audioPlayer.id; + _watchGraphicsChange = true; + + activeStatus = getPlayerStatus(true) ? 'play' : 'pause'; + _watchGraphicsChange = true; + } + + _lastPlaylistSummary = summary; + _lastPlaylistId = random(100000, 1000000); + + log("[updatePlaylist] send pl with id="+_lastPlaylistId+', activeId='+activeId+', activeStatus='+activeStatus+' to app'); + try { + client.send({ + command: 'updatePlaylist', + data: { + tracks: tracks, + title: parsePlaylistTitle(plTitle) || "", + id: _lastPlaylistId, + active: { 'id': activeId, 'status': activeStatus }, + // browser: getBrowser() + } + }); + } catch(e) { + log('[updatePlaylist] exception:', e, e.stack); + } + + processOperateQueue(pl); + } + } + } + + function clearPlaylist(no_send, called_from) { + called_from = called_from || ""; + log('[clearPlaylist] (this may be called from: '+called_from+')'); + _lastPlaylistSummary = null; + _lastPlaylistId = 0; + if (!no_send) { + client.send({command: 'clearPlaylist', data: {}}); + } + } + + function checkPlaylist() { + var pl = padAudioPlaylist(); + if (!pl) { + clearPlaylist(true, 'checkPlaylist'); + } + } + + function parsePlaylistTitle(str) { + str = str || ""; + str = trim(str); + if (str == '') return str; + + var starts = { + 0: 'Сейчас играет — ', // ru + 100: '\u041d\u044b\u043d\u0447\u0435 \u0438\u0433\u0440\u0430\u0435\u0442\u044a\u2014 ', // re + 3: 'Now playing — ', // en + 1: 'Зараз звучить — ', // ua + 777: 'Проигрывается пластинка «' // su + }; + var ends = { + 0: ' \\| [0-9]+ аудиоза[^\\s]+$', + 3: ' \\| [0-9]+ audio [^\\s]+$', + 1: ' \\| [0-9]+ аудіоза[^\\s]+$', + 100: ' \\| [0-9]+ композ[^\\s]+$', + 777: ' \\| [0-9]+ грамза[^\\s]+»$' + }; + + if (window.vk && vk.lang !== undefined) { + if (starts[vk.lang] !== undefined && startsWith(str, starts[vk.lang])) { + str = str.substring(starts[vk.lang].length); + } + + if (ends[vk.lang] !== undefined) { + var regex = new RegExp(ends[vk.lang], 'i'); + if (str.match(regex)) str = str.replace(regex, ''); + } + } + + return stripTags(trim(str)); + } + + function afterInjection() { + log("after injection"); + var pl = getPlaylist(); + if (pl) updatePlaylist(pl); + } + + function next() { + audioPlayer.nextTrack(true, !window.audioPlaylist) + /*if (audioPlayer.controls && audioPlayer.controls.pd && audioPlayer.controls.pd.next) { + audioPlayer.controls.pd.next.click(); + } else { + audioPlayer.nextTrack(true, !window.audioPlaylist) + }*/ + } + + function prev() { + audioPlayer.prevTrack(true, !window.audioPlaylist); + /*if (audioPlayer.controls && audioPlayer.controls.pd && audioPlayer.controls.pd.prev) { + audioPlayer.controls.pd.prev.click(); + } else { + audioPlayer.prevTrack(true, !window.audioPlaylist); + }*/ + } + + function getPlayFirstId() { + var id = currentAudioId() || ls.get('audio_id') || (window.audioPlaylist && audioPlaylist.start); + return id || null; + } + + function playFirst() { + var id = getPlayFirstId(); + + if (id) playId(id); + else { + var plist = padAudioPlaylist(); + if (plist && plist.start) { + playId(plist.start); + } else { + executeAfterPadLoading(function() { + var plist = padAudioPlaylist(); + if (plist && plist.start) { + playId(plist.start); + } + }); + } + } + } + + function executeAfterPadLoading(f) { + Pads.show('mus'); + window.onPlaylistLoaded = function() { + if (f) { + try { + f(); + } catch(e) {} + } + setTimeout(function() { + Pads.show('mus'); + }, 10); + } + } + + function getPlayerStatus(justStarted) { + if (!audioPlayer.player) return false; + try { + var pl = audioPlayer.player; + if (pl && pl.music && pl.music.buffered && !pl.music.buffered.length && justStarted) return true; + } catch (e) { + return true; + } + + return audioPlayer.player && !audioPlayer.player.paused(); + } + + function pauseForSafari() { + if (window.audioPlayer && audioPlayer.player) audioPlayer.pauseTrack(); + } + + function playPause() { + if (window.audioPlayer && audioPlayer.player) { + if (audioPlayer.player.paused()) { + audioPlayer.playTrack(); + } else { + audioPlayer.pauseTrack(); + } + } + } + + function operateTrack(id) { + if (id == audioPlayer.id) { + playPause(); + } else { + audioPlayer.operate(id); + } + } + + function playId(id) { + if (window.audioPlayer) audioPlayer.operate(id); + else playAudioNew(id); + } + + function getLastInstanceId() { + var id = null, pp = ls.get('pad_playlist'); + if (pp && pp.source) id = pp.source; + //return [id, ls.get('vkpc_lastid')]; + return id; + } + + this.executeCommand = executeCommand; + + this.getParams = function() { + checkPlaylist(); + var havePlayer = window.audioPlayer !== undefined; + var havePlaylist = havePlayer && (window.padAudioPlaylist && !!padAudioPlaylist()); + + return [ + /* 1:havePlayer */ intval(havePlayer), + /* 2:havePlaylist */ intval(havePlaylist), + /* 3:isPlaying */ intval(window.audioPlayer && window.audioPlayer.player && !window.audioPlayer.player.paused()), + /* 4:tabId */ window.curNotifier && curNotifier.instance_id, + /* 5:trackId */ window.audioPlayer && audioPlayer.id, + /* 6:tabPlaylistId */ intval(_lastPlaylistId || 0), + /* 7:lsSourceId */ getLastInstanceId() + ]; + }; + + this.init = function(sid) { + if (_checkPlaylistTimer === null) { + _checkPlaylistTimer = setInterval(function() { + if ((_lastPlaylistId || _lastPlaylistSummary) && !padAudioPlaylist()) { + clearPlaylist(true, 'timer'); // TODO func + } + }, 1000); + } + + if (!window.__wrappedByVKPC && window.audioPlayer && window.ls && window.stManager) { + if (!stManager.__done) { + stManager.__done = stManager.done; + stManager.done = function(fn) { + if (fn == 'audioplayer.js') { + wrapAudioMethods(); // TODO func + } + stManager.__done.apply(stManager, arguments); + }; + } + + wrapAudioMethods(); + + if (!ls.__set) { + ls.__set = ls.set; + ls.set = function(k, v) { + ls.__set.apply(ls, arguments); + if (k == 'pad_playlist') { + log('pad_playlist updated:', v); + updatePlaylist(getPlaylist(v)); // TODO func + } + }; + } + if (!ls.__remove) { + ls.__remove = ls.remove; + ls.remove = function(k, v) { + ls.__remove.apply(ls, arguments); + if (k == 'pad_playlist') { + log('pad_playlist removed from ls'); + //self.clearPlaylist(true, 'ls.remove'); + // self.clearPlaylist(); + } + }; + } + + window.__wrappedByVKPC = true; + } + + if (sid === _sid) { + return; + } + if (_sid !== null) { + clear(); // TODO + } + _sid = sid; + + log('(re)inited OK'); + }; + + this.getSID = function() { + return _sid; + }; + + this.getLastInstanceId = getLastInstanceId; + this.clearPlaylist = clearPlaylist; +}; // window.VKPC = ... + +} // if (!window.VKPC) ... + +if (!window.DOMContentLoaded) { + window.console && console.log && console.log("[VKPC] !window.DOMContentLoaded, exising"); + return; +} + +window.DOMContentLoaded(function() { + VKPC.init(vkpc_sid); +}); + +return VKPC.getParams(); + +})({sid}); diff --git a/VKPCTests/VKPCTests-Info.plist b/VKPCTests/VKPCTests-Info.plist new file mode 100644 index 0000000..391c01a --- /dev/null +++ b/VKPCTests/VKPCTests-Info.plist @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleDevelopmentRegion</key> + <string>en</string> + <key>CFBundleExecutable</key> + <string>${EXECUTABLE_NAME}</string> + <key>CFBundleIdentifier</key> + <string>com.ch1p.${PRODUCT_NAME:rfc1034identifier}</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundlePackageType</key> + <string>BNDL</string> + <key>CFBundleShortVersionString</key> + <string>1.0</string> + <key>CFBundleSignature</key> + <string>????</string> + <key>CFBundleVersion</key> + <string>1</string> +</dict> +</plist> diff --git a/VKPCTests/VKPCTests.m b/VKPCTests/VKPCTests.m new file mode 100644 index 0000000..8cf4d86 --- /dev/null +++ b/VKPCTests/VKPCTests.m @@ -0,0 +1,34 @@ +// +// VKPCTests.m +// VKPCTests +// +// Created by Eugene on 11/26/13. +// Copyright (c) 2013 Eugene Z. All rights reserved. +// + +#import <XCTest/XCTest.h> + +@interface VKPCTests : XCTestCase + +@end + +@implementation VKPCTests + +- (void)setUp +{ + [super setUp]; + // Put setup code here. This method is called before the invocation of each test method in the class. +} + +- (void)tearDown +{ + // Put teardown code here. This method is called after the invocation of each test method in the class. + [super tearDown]; +} + +- (void)testExample +{ + XCTFail(@"No implementation for \"%s\"", __PRETTY_FUNCTION__); +} + +@end diff --git a/VKPCTests/en.lproj/InfoPlist.strings b/VKPCTests/en.lproj/InfoPlist.strings new file mode 100644 index 0000000..477b28f --- /dev/null +++ b/VKPCTests/en.lproj/InfoPlist.strings @@ -0,0 +1,2 @@ +/* Localized versions of Info.plist keys */ + diff --git a/chrome/bg.js b/chrome/bg.js new file mode 100644 index 0000000..91fea9a --- /dev/null +++ b/chrome/bg.js @@ -0,0 +1,192 @@ +var wsc, injectInterval; + +function init() { + // receive messages from webpage + chrome.runtime.onMessageExternal.addListener(function(msg, sender, sendResponse) { + if (msg.cmd == "injection_result") { + var obj = Injections.get(msg.id); + if (obj) { + obj.addResponse(sender.tab.id, msg.data); + } + } + if (msg.cmd == "to_app") { + // log('to_app received', msg.data); + wsc.send(msg.data); + } + }); + + // connect to the app + wsc = new WSClient("wss://vkpc-local.ch1p.com:56130", "signaling-protocol", { + onopen: function() { + Controller.clear(); + this.send({command: 'setBrowser'}); + }, + onmessage: function(cmd) { + var json = JSON.parse(cmd); + switch (json.command) { + case 'set_sid': + Controller.sid = json.data; + break; + + case 'set_playlist_id': + Controller.playlistId = json.data; + break; + + case 'vkpc': + inject(json.data); + break; + } + + // executeCommand(msg); + }, + onerror: function() { + this.reconnect(); + }, + onclose: function() { + this.reconnect(); + } + }); + wsc.connect(); + + injectInterval = setInterval(function() { + inject('afterInjection'); + }, 2000); +} + +function sendClear() { + wsc.send({command: 'clearPlaylist', data: null}); +} + +function inject(command, callback) { + var injId = Injections.getNextId(); + var data = { + extid: getExtensionId(), + injid: injId, + sid: Controller.sid, + command: command + }; + var code_inj = "var el = document.createElement('script');" + + "el.src = chrome.extension.getURL('vkpc.js');" + + "document.body.appendChild(el);" + + "var el1 = document.createElement('script');" + + "el1.textContent = 'window.__vkpc_data = "+JSON.stringify(data)+"';" + + "document.body.appendChild(el1)"; + + var okTab_nowPlaying, okTab_playlistFound, okTab_lsSource, okTab_recentlyPlayed, okTab_havePlaylist, + activeTab, lastTab, outdatedTabs = [], tabsWithPlayingMusic = []/*, tabPlaylistIds = {}*/; + var lsSourceId, appPlaylistFound = false; + + var injResponses, injResults; + + function getCode(code) { + return "var el = document.createElement('script');" + + "el.textContent = '"+code.replace(/'/g, "\\'")+"';" + + "document.body.appendChild(el)"; + } + function onDone(step) { + var results = injResponses.results; + var execCommand = getCode("VKPC.executeCommand('"+command+"', "+Controller.playlistId+")"); + + if (command == 'afterInjection') { + //log('[afterInjection onDone] results.length='+results.length); + + for (var i = 0; i < results.length; i++) { + var data = results[i].data, tab = results[i].tab; + + // tabPlaylistIds[tab] = data.playlistId; + if (data.playlistId != 0 && data.playlistId == Controller.playlistId) { + appPlaylistFound = true; + } + if (data.havePlaylist && data.playlistId != 0 && data.playlistId != Controller.playlistId) { + outdatedTabs.push(tab); + } + if (data.havePlaylist) { + okTab_havePlaylist = tab; + } + if (data.isPlaying) { + okTab_nowPlaying = tab; + } + } + + if (!appPlaylistFound) { + var okTab = okTab_nowPlaying || okTab_havePlaylist; + if (okTab !== undefined) { + chrome.tabs.executeScript(okTab, {code: execCommand}); + } else if (!appPlaylistFound) { + sendClear(); + } + } + + for (var i = 0; i < outdatedTabs.length; i++) { + chrome.tabs.executeScript(outdatedTabs[i], {code: getCode('VKPC.clearPlaylist(true, "as")')}); + } + } else { + for (var i = 0; i < results.length; i++) { + var data = results[i].data; + if (!lsSourceId && data.lsSourceId) { + lsSourceId = data.lsSourceId; + break; + } + } + + for (var i = 0; i < results.length; i++) { + var data = results[i].data, tab = results[i].tab; + + if (data.playlistId == Controller.playlistId) { + okTab_playlistFound = tab; + } + if (data.havePlayer && (data.isPlaying || typeof data.trackId == 'string')) { + okTab_recentlyPlayed = tab; + } + if (data.isPlaying) { + okTab_nowPlaying = tab; + } + if (lsSourceId == data.tabId) { + okTab_lsSource = tab; + } + + lastTab = tab; + } + + var check = [okTab_nowPlaying, okTab_lsSource, okTab_recentlyPlayed, okTab_recentlyPlayed, okTab_havePlaylist, activeTab, lastTab]; + //log('check[] =', check); + for (var i = 0; i < check.length; i++) { + if (check[i] !== undefined) { + chrome.tabs.executeScript(check[i], {code: execCommand}); + break; + } + } + } + + injResponses.unregister(); + callback && callback(); + } + + getVKTabs(function(tabs) { + if (!tabs.length) { + sendClear(); + return; + } + + injResponses = new InjectionResponses(injId, tabs.length, onDone); + for (var i = 0; i < tabs.length; i++) { + if (tabs[i].active) { + activeTab = tabs[i].id; + } + chrome.tabs.executeScript(tabs[i].id, { + code: code_inj + }); + } + }); +}; + +var Controller = { + sid: 0, + playlistId: 0, + clear: function() { + this.sid = 0; + this.playlistId = 0; + } +}; + +DOMContentLoaded(init); diff --git a/chrome/common.js b/chrome/common.js new file mode 100644 index 0000000..9acd7ec --- /dev/null +++ b/chrome/common.js @@ -0,0 +1,270 @@ +var browser = (function() { + var ua = navigator.userAgent.toLowerCase(); + var browser = { + id: null, + chrome: false, + safari: false, + yandex: false, + firefox: false, + opera: false + }; + + if (/opr/i.test(ua) && /chrome/i.test(ua)) { + browser.opera = true; + browser.id = 3; + } else if (/yabrowser/i.test(ua) && /chrome/i.test(ua)) { + browser.yandex = true; + browser.id = 4; + } else if (/firefox|iceweasel/i.test(ua)) { + browser.firefox = true; + browser.id = 1; + } else if (!(/chrome/i.test(ua)) && /webkit|safari|khtml/i.test(ua)) { + browser.safari = true; + browser.id = 2; + } else if (/chrome/i.test(ua)) { + browser.chrome = true; + browser.id = 0; + } + + return browser; +})(); + +function getExtensionId() { + return chrome.i18n.getMessage("@@extension_id"); +} + +function getVKTabs(callback) { + var vkTabs = []; + chrome.tabs.query({}, function(tabs) { + for (var i = 0; i < tabs.length; i++) { + var tab = tabs[i]; + if (tab.url.match(new RegExp('https?://vk.com/.*', 'gi'))) { + vkTabs.push(tab); + } + } + callback(vkTabs); + }); +} + +function extend(dest, source) { + for (var i in source) { + dest[i] = source[i]; + } + return dest; +} +function log() { + var msgs = [], i, tmp; + for (i = 0; i < arguments.length; i++) { + if (arguments[i] instanceof Error) tmp = [arguments[i], arguments[i].stack]; + else tmp = arguments[i]; + msgs.push(tmp); + } + + try { + console.log.apply(console, msgs); + } catch(e) {} +} +function intval(value) { + if (value === true) return 1; + return parseInt(value) || 0; +} +function str(v) { + var str; + if (v && v.toString) + str = v.toString(); + else + str = v + ''; + if (str == '[object Object]') { + str = JSON.stringify(v); + } + return str; +} + +var WSC_STATE_NONE = 'NONE', + WSC_STATE_OK = 'OK', + WSC_STATE_CLOSED = 'CLOSED', + WSC_STATE_ERR = 'ERR'; +function WSClient(address, protocol, opts) { + this.state = WSC_STATE_NONE; + this._ws = null; + + this.address = address; + this.protocol = protocol; + + this._onmessage = opts.onmessage; + this._onclose = opts.onclose; + this._onerror = opts.onerror; + this._onopen = opts.onopen; + + this._pingTimer = null; + this._reconnectTimer = null; +} +extend(WSClient.prototype, { + connect: function() { + this.state = WSC_STATE_NONE; + var self = this; + + var _websocket = window.WebSocket || window.MozWebSocket; + if (!_websocket) { + log('[WSClient connect] websockets are not supported'); + return; + } + + this._ws = new _websocket(this.address, this.protocol); + this._ws.onopen = function() { + self.state = WSC_STATE_OK; + self._setTimers(); + self._onopen && self._onopen.apply(self); + }; + this._ws.onerror = function() { + self._unsetTimers(); + if (self.state != WSC_STATE_ERR) { + self.state = WSC_STATE_ERR; + } + self._onerror && self._onerror.apply(self); + }; + this._ws.onclose = function() { + self._unsetTimers(); + if (self.state != WSC_STATE_ERR) { + self.state = WSC_STATE_ERR; + } + self._onclose && self._onclose.apply(self); + }; + this._ws.onmessage = function(e) { + self._onmessage && self._onmessage.apply(self, [e.data]); + }; + }, + close: function() { + this._unsetTimers(); + this._ws.close(); + }, + reconnect: function() { + var self = this; + if (this.state == WSC_STATE_OK) { + log('[WSClient reconnect] state = '+this.state+', why reconnect?'); + return; + } + clearTimeout(this._reconnectTimer); + this._reconnectTimer = setTimeout(function() { + self.connect(); + }, 3000); + }, + send: function(obj) { + obj._browser = browser.id; + var self = this; + this._waitForConnection(function() { + self._ws.send(JSON.stringify(obj)); + }, 200); + }, + _setTimers: function() { + var self = this; + this._pingTimer = setInterval(function() { + try { + self._ws.send("PING"); + } catch (e) { + log('[WSClient _pingTimer]', e); + } + }, 30000); + }, + _unsetTimers: function() { + clearInterval(this._pingTimer); + }, + _waitForConnection: function(callback, interval) { + if (this._ws.readyState === 1) { + callback(); + } else { + var self = this; + setTimeout(function() { + self._waitForConnection(callback, interval); + }, interval); + } + } +}); + +(function(window, document) { + var queue = [], done = false, _top = true, root = document.documentElement, eventsAdded = false; + + function init(e) { + if (e.type == 'readystatechange' && document.readyState != 'complete') return; + (e.type == 'load' ? window : document).removeEventListener(e.type, init); + if (!done) { + done = true; + while (queue.length) { + queue.shift().call(window); + } + } + } + function poll() { + try { + root.doScroll('left'); + } catch (e) { + setTimeout(poll, 50); + return; + } + init('poll'); + } + + window.DOMContentLoaded = function(fn) { + if (document.readyState == 'complete' || done) { + fn.call(window); + } else { + queue.push(fn); + + if (!eventsAdded) { + if (document.createEventObject && root.doScroll) { + try { + _top = !window.frameElement; + } catch (e) {} + if (_top) poll(); + } + + document.addEventListener('DOMContentLoaded', init); + document.addEventListener('readystatechange', init); + window.addEventListener('load', init); + eventsAdded = true; + } + } + } +})(window, document); + +var Injections = { + id: 0, + objs: {}, + getNextId: function() { + if (this.id == Number.MAX_VALUE) { + this.id = -1; + } + return ++this.id; + }, + get: function(id) { + return this.objs[id] || false; + }, + register: function(id, obj) { + this.objs[id] = obj; + }, + unregister: function(id) { + if (this.objs[id] !== undefined) delete this.objs[id]; + } +}; + +function InjectionResponses(id, count, callback) { + this.id = id; + this.results = []; + this.lsSource = null; + this.maxCount = count; + this.callback = callback || function() {}; + + Injections.register(this.id, this); +} +extend(InjectionResponses.prototype, { + addResponse: function(id, response) { + this.results.push({tab: id, data: response}); + if (!this.lsSource && response && response.lastInstanceId) this.lsSource = response.lastInstanceId; + if (this.results.length == this.maxCount) { + this.callback(); + } + }, + unregister: function() { + Injections.unregister(this.id); + } +}); diff --git a/chrome/icons/128.png b/chrome/icons/128.png Binary files differnew file mode 100644 index 0000000..6667635 --- /dev/null +++ b/chrome/icons/128.png diff --git a/chrome/icons/16.png b/chrome/icons/16.png Binary files differnew file mode 100644 index 0000000..18018ce --- /dev/null +++ b/chrome/icons/16.png diff --git a/chrome/icons/32.png b/chrome/icons/32.png Binary files differnew file mode 100644 index 0000000..80791ea --- /dev/null +++ b/chrome/icons/32.png diff --git a/chrome/manifest.json b/chrome/manifest.json new file mode 100644 index 0000000..4e29f6a --- /dev/null +++ b/chrome/manifest.json @@ -0,0 +1,30 @@ +{ + "manifest_version": 2, + "name": "VK Player Controller", + "description": "This is a part of VK Player Controller for OS X. For more information, please visit https://ch1p.com/vkpc/", + "version": "3.1", + "icons": { + "128": "icons/128.png", + "16": "icons/16.png", + "32": "icons/32.png" + }, + "content_security_policy": "script-src 'self' 'unsafe-eval' https://vk.com; object-src 'self' 'unsafe-eval'", + "permissions": [ + "tabs", + "background", + "https://vk.com/*", + "http://vk.com/*", + "https://*.vk.com/*", + "http://*.vk.com/*" + ], + "background": { + "scripts": [ + "common.js", + "bg.js" + ] + }, + "externally_connectable": { + "matches": ["https://vk.com/*", "http://vk.com/*", "https://*.vk.com/*", "http://*.vk.com/*"] + }, + "web_accessible_resources": ["inject_and_return.js", "inject_exec.js", "vkpc.js"] +} diff --git a/chrome/vkpc.js b/chrome/vkpc.js new file mode 100644 index 0000000..2070444 --- /dev/null +++ b/chrome/vkpc.js @@ -0,0 +1,709 @@ +// VKPC for Chrome + +(function(vkpc_sid) { +if (!window.VKPC) { + +if (!document.addEventListener) { + window.console && console.log("[VKPC] an outdated browser detected, very strange, plz update"); + return; +} + +// variables +var _debug = window.__vkpc_debug || true; +var _extid = window.__vkpc_data.extid; + +(function(window, document) { + var queue = [], done = false, _top = true, root = document.documentElement, eventsAdded = false; + + function init(e) { + if (e.type == 'readystatechange' && document.readyState != 'complete') return; + (e.type == 'load' ? window : document).removeEventListener(e.type, init); + if (!done) { + done = true; + while (queue.length) { + queue.shift().call(window); + } + } + } + function poll() { + try { + root.doScroll('left'); + } catch (e) { + setTimeout(poll, 50); + return; + } + init('poll'); + } + + window.DOMContentLoaded = function(fn) { + if (document.readyState == 'complete' || done) { + fn.call(window); + } else { + queue.push(fn); + + if (!eventsAdded) { + if (document.createEventObject && root.doScroll) { + try { + _top = !window.frameElement; + } catch (e) {} + if (_top) poll(); + } + + document.addEventListener('DOMContentLoaded', init); + document.addEventListener('readystatechange', init); + window.addEventListener('load', init); + eventsAdded = true; + } + } + } +})(window, document); + +function log() { + if (!_debug) + return; + var args = Array.prototype.slice.call(arguments); + args.unshift(window.VKPC ? '[VKPC '+window.VKPC.getSID()+']' : '[VKPC]'); + try { + window.console && console.log.apply(console, args); + } catch (e) {} +} +function trim(string) { + return string.replace(/(^\s+)|(\s+$)/g, ""); +} +function startsWith(str, needle) { + return str.indexOf(needle) == 0; +} +function endsWith(str, suffix) { + return str.indexOf(suffix, str.length - suffix.length) !== -1; +} +function random(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; +} +function shuffle(o) { + for (var j, x, i = o.length; i; j = Math.floor(Math.random() * i), x = o[--i], o[i] = o[j], o[j] = x); + return o; +} +function getStackTrace(split) { + split = split === undefined ? true : split; + try { + o.lo.lo += 0; + } catch(e) { + if (e.stack) { + return split ? e.stack.split('\n') : e.stack; + } + } + return null; +} +function buildQueryString(obj) { + var list = [], i; + for (i in obj) { + list.push(encodeURIComponent(i) + '=' + encodeURIComponent(obj[i])); + } + return list.join('&'); +} +function stripTags(html) { + var div = document.createElement("div"); + div.innerHTML = html; + return div.textContent || div.innerText || ""; +} +function decodeEntities(value) { + var textarea = document.createElement('textarea'); + textarea.innerHTML = value; + return textarea.value; +} + +function toApp(command, data) { + chrome.runtime.sendMessage(_extid, { + cmd: "to_app", + data: { + command: command, + data: data + } + }); +} + +window.VKPC = new function() { + var _sid = null; + var _currentTrackId = null; + var _lastPlaylistSummary = null; + var _lastPlaylistId = 0; + var _operateQueue = []; + var _setTrackIdTimeout = null; + var _watchGraphicsChange = false; + var _checkPlaylistTimer = null; + + function wrapAudioMethods() { + // var self = this; + if (window.audioPlayer) { + if (!audioPlayer.__operate) { + audioPlayer.__operate = audioPlayer.operate; + audioPlayer.operate = function(id, nextPlaylist, opts) { + var currentId = audioPlayer.id, _status = id != currentId ? 'play' : null; + audioPlayer.__operate.apply(audioPlayer, arguments); + //self.firstOperateAfterPlaylistUpdating = false; + log('operate(), arguments:', arguments); + + if (existsInCurrentPlaylist(id)) { + log('operate(), found in current pl, setTrackId() now'); + setTrackId(id, _status); + } else { + log('operate(), not found, setToOperateQueue() now'); + setToOperateQueue(id, _status); + } + }; + } + + // disable it + if (false && !audioPlayer.__setGraphics) { + audioPlayer.__setGraphics = audioPlayer.setGraphics; + audioPlayer.setGraphics = function(act) { + audioPlayer.__setGraphics.apply(audioPlayer, arguments); + return; + /*if (self.watchGraphicsChange) { + if (browser.safari) self.sendOperateTrack(audioPlayer.id, (act == 'play' || act == 'load') ? 'play' : 'pause'); + self.watchGraphicsChange = false; + }*/ + }; + } + } + + log('[wrapAudioMethods] wrapped DONE'); + } + + function clear() { + log('clear()'); + _currentTrackId = null; + _lastPlaylistSummary = null; + _lastPlaylistId = null; + _sid = null; + _watchGraphicsChange = false; + } + + function getBrowser() { + return browser.safari ? 'safari' : 'chrome'; + } + + function executeCommand(command, plid) { + if (command == 'afterInjection') { + log('executeCommand: afterInjection, plid='+plid); + var pl = padAudioPlaylist(); + if (window.audioPlayer && pl) { + updatePlaylist(getPlaylist(pl)); + } else { + clearPlaylist(); + } + return; + } + + log('executeCommand:', command, plid); + // var self = this; + + if (!window.audioPlayer || !padAudioPlaylist()) { + log('[executeCommand] audioplayer or playlist not found'); + stManager.add(['audioplayer.js'], function() { + executeAfterPadLoading(function() { + log('[executeCommand] after execafterpadloading, window.audioPlayer:', window.audioPlayer); + wrapAudioMethods(); + + var plist = padAudioPlaylist(); + if (plist) { + log('[executeCommand] after exec...: send updatePlaylist() with plist'); + updatePlaylist(getPlaylist(plist)); + } + + if (command == 'playpause' || command == 'next' || command == 'prev') { + log('[executeCommand] after exec...: simple command'); + var id = getPlayFirstId(); + if (id) { + log('[executeCommand] after exec...: found id='+id+', playAudioNew() now'); + playAudioNew(id); + } else if (plist && plist.start) { + log('[executeCommand] after exec...: found plist.start, playAudioNew() now'); + playAudioNew(plist.start); + } + } else if (startsWith(command, 'operateTrack:')) { // TODO this is new fix + var id = parseInt(command.replace('operateTrack:')); + log('[executeCommand] after exec...: got operateTrack, id='+id); + if (!plist[id]) { + log('[executeCommand] after exec...: after got operateTrack: plist[id] not found, send new pl to app'); + //self.clearPlaylist(); + updatePlaylist(getPlaylist(plist)); + if (plist.start) { + log('[executeCommand] after exec...: got operateTrack, pl not found... ... play plist.start now'); + playAudioNew(plist.start); + } + } else { + log('[executeCommand] after exec...: got operateTrack, it is found, playAudioNew() now'); + playAudioNew(id); + } + } + }); + }); + return; + } + + function evaluateCommand(command) { + switch (command) { + case 'next': + case 'prev': + case 'playpause': + if (audioPlayer.id) { + if (command == 'next') next(); + else if (command == 'prev') prev(); + else if (command == 'playpause') playPause(); + } else { + var id = getPlayFirstId(); + if (id) playId(id); + } + break; + + default: + if (startsWith(command, 'operateTrack:')) { + log('[executeCommand] got operateTrack;'); + var id = command.replace('operateTrack:', ''), pl = padAudioPlaylist(); + if (pl[id] !== undefined) { + log('[executeCommand] got operateTrack; track is found, playAudioNew() now'); + //playAudioNew(id); + //audioPlayer.operate(id); + playId(id); + } else { + log('[executeCommand] got operateTrack; track not found, updatePlaylist with pl:', pl); + updatePlaylist(getPlaylist(pl)); + var id = getPlayFirstId(); + if (id) { + log('[executeCommand] got operateTrack; play id from getPlayFirstId() now'); + playId(id); + } + } + } + break; + } + } + + if (plid != _lastPlaylistId) { + log('[executeCommand] plid does not match'); + var pl = padAudioPlaylist(); + if (pl) { + updatePlaylist(getPlaylist(pl), true); + log('[executeCommand] plid does not match, sent updatePlaylist() with pl:', pl); + + if (plid == 0) { + evaluateCommand(command); + } else { + if (['next', 'prev', 'playpause'].indexOf(command) != -1) { + var id = audioPlayer.id || pl.start || getPlayFirstId(); + if (id) { + playId(id); + } + } + } + } + } else { + evaluateCommand(command); + } + } + + function setTrackId(id, _status) { + _status = _status || (audioPlayer.player.paused() ? 'pause' : 'play'); + clearTimeout(_setTrackIdTimeout); + + var check = function() { + if (audioPlayer.player) { + sendOperateTrack(id, _status); + } else { + _setTrackIdTimeout = setTimeout(check, 200); + } + }; + check(); + } + + function sendOperateTrack(id, _status) { + log('[sendOperateTrack]', id, _status); + toApp('operateTrack', { + 'id': id, + 'status': _status, + 'playlistId': _lastPlaylistId + }); + } + + function setToOperateQueue(id, _status) { + var q = _operateQueue; + for (var i = 0; i < q.length; i++) { + var track = q[i]; + if (track[0] == id) { + track[1] = _status; + return; + } + } + q.push([id, _status]); + } + + function existsInCurrentPlaylist(id) { + return _lastPlaylistSummary && _lastPlaylistSummary.indexOf(id) != -1; + } + + function processOperateQueue(pl) { + log('[processOperateQueue]'); + var q = _operateQueue; + while (q.length) { + var track = q.shift(); + log('[processOperateQueue] track:', track[0]); + if (pl[track[0]] !== undefined) { + log('[processOperateQueue] track', track[0], 'found, send it now'); + sendOperateTrack(track[0], track[1]); + } + } + } + + function clearOperateQueue() { + _operateQueue = []; + } + + function printPlaylist() { + var pl = padAudioPlaylist(); + if (pl) { + for (var k in pl) { + log(pl[k][5] + ' - ' + pl[k][6]); + } + } + } + + function getPlaylist(_pl) { + _pl = _pl || padAudioPlaylist(); + var pl = null; + if (_pl) { + var start = _pl.start, pl = []; + var nextId = start; + do { + if (_pl[nextId]) { + _pl[nextId]._vkpcId = nextId; + pl.push(_pl[nextId]); + nextId = _pl[nextId]._next; + } + } while (nextId != '' && nextId !== undefined && nextId != start); + } + return pl; + } + + // force=true is used when plids not match + function updatePlaylist(pl, force) { + var tracks = [], summary = [], title; + if (pl) { + for (var k = 0; k < pl.length; k++) { + tracks.push({ + id: pl[k]._vkpcId, + artist: decodeEntities(pl[k][5]), + title: decodeEntities(pl[k][6]), + duration: pl[k][4] + }); + summary.push(pl[k]._vkpcId); + } + + summary = summary.join(';'); + + log("updatePlaylist: _lastPlaylistSummary:", _lastPlaylistSummary, 'summary:', summary); + if (force || _lastPlaylistSummary === null || _lastPlaylistSummary !== summary) { + log('[updatePlaylist] last summary not matched;', _lastPlaylistSummary, summary); + var activeId = '', activeStatus = ''; + var vkpl = padAudioPlaylist(); + var plTitle = (window.audioPlaylist && window.audioPlaylist.htitle) || vkpl.htitle; + if (audioPlayer.id && vkpl[audioPlayer.id] !== undefined) { + activeId = audioPlayer.id; + _watchGraphicsChange = true; + + activeStatus = getPlayerStatus(true) ? 'play' : 'pause'; + _watchGraphicsChange = true; + } + + _lastPlaylistSummary = summary; + _lastPlaylistId = random(100000, 1000000); + + log("[updatePlaylist] send pl with id="+_lastPlaylistId+', activeId='+activeId+', activeStatus='+activeStatus+' to app'); + try { + toApp('updatePlaylist', { + tracks: tracks, + title: parsePlaylistTitle(plTitle) || "", + id: _lastPlaylistId, + active: { 'id': activeId, 'status': activeStatus }, + browser: getBrowser() + }); + } catch(e) { + log('[updatePlaylist] exception:', e, e.stack); + } + + processOperateQueue(pl); + } + } + } + + function clearPlaylist(no_send, called_from) { + called_from = called_from || ""; + log('[clearPlaylist] (called from: '+called_from+')'); + + _lastPlaylistSummary = null; + _lastPlaylistId = 0; + if (!no_send) { + toApp('clearPlaylist', {}); + } + } + + function checkPlaylist() { + var pl = padAudioPlaylist(); + if (!pl) { + clearPlaylist(true, 'checkPlaylist'); + } + } + + function parsePlaylistTitle(str) { + str = str || ""; + str = trim(str); + if (str == '') return str; + + var starts = { + 0: 'Сейчас играет — ', // ru + 100: 'Нынче играетъ— ', // re + 3: 'Now playing — ', // en + 1: 'Зараз звучить — ', // ua + 777: 'Проигрывается пластинка «' // su + }; + var ends = { + 0: ' \\| [0-9]+ аудиоза[^\\s]+$', + 3: ' \\| [0-9]+ audio [^\\s]+$', + 1: ' \\| [0-9]+ аудіоза[^\\s]+$', + 100: ' \\| [0-9]+ композ[^\\s]+$', + 777: ' \\| [0-9]+ грамза[^\\s]+»$' + }; + + if (window.vk && vk.lang !== undefined) { + if (starts[vk.lang] !== undefined && startsWith(str, starts[vk.lang])) { + str = str.substring(starts[vk.lang].length); + } + + if (ends[vk.lang] !== undefined) { + var regex = new RegExp(ends[vk.lang], 'i'); + if (str.match(regex)) str = str.replace(regex, ''); + } + } + + return stripTags(trim(str)); + } + + function afterInjection() { + log("after injection"); + var pl = getPlaylist(); + if (pl) updatePlaylist(pl); + } + + function next() { + audioPlayer.nextTrack(true, !window.audioPlaylist) + /*if (audioPlayer.controls && audioPlayer.controls.pd && audioPlayer.controls.pd.next) { + audioPlayer.controls.pd.next.click(); + } else { + audioPlayer.nextTrack(true, !window.audioPlaylist) + }*/ + } + + function prev() { + audioPlayer.prevTrack(true, !window.audioPlaylist); + /*if (audioPlayer.controls && audioPlayer.controls.pd && audioPlayer.controls.pd.prev) { + audioPlayer.controls.pd.prev.click(); + } else { + audioPlayer.prevTrack(true, !window.audioPlaylist); + }*/ + } + + function getPlayFirstId() { + var id = currentAudioId() || ls.get('audio_id') || (window.audioPlaylist && audioPlaylist.start); + return id || null; + } + + function playFirst() { + var id = getPlayFirstId(); + + if (id) playId(id); + else { + var plist = padAudioPlaylist(); + if (plist && plist.start) { + playId(plist.start); + } else { + executeAfterPadLoading(function() { + var plist = padAudioPlaylist(); + if (plist && plist.start) { + playId(plist.start); + } + }); + } + } + } + + function executeAfterPadLoading(f) { + Pads.show('mus'); + window.onPlaylistLoaded = function() { + if (f) { + try { + f(); + } catch(e) {} + } + setTimeout(function() { + Pads.show('mus'); + }, 10); + } + } + + function getPlayerStatus(justStarted) { + if (!audioPlayer.player) return false; + try { + var pl = audioPlayer.player; + if (pl && pl.music && pl.music.buffered && !pl.music.buffered.length && justStarted) return true; + } catch (e) { + return true; + } + + return audioPlayer.player && !audioPlayer.player.paused(); + } + + function pauseForSafari() { + if (window.audioPlayer && audioPlayer.player) audioPlayer.pauseTrack(); + } + + function playPause() { + if (window.audioPlayer && audioPlayer.player) { + if (audioPlayer.player.paused()) { + audioPlayer.playTrack(); + } else { + audioPlayer.pauseTrack(); + } + } + } + + function operateTrack(id) { + if (id == audioPlayer.id) { + playPause(); + } else { + audioPlayer.operate(id); + } + } + + function playId(id) { + if (window.audioPlayer) audioPlayer.operate(id); + else playAudioNew(id); + } + + function getLastInstanceId() { + var id = null, pp = ls.get('pad_playlist'); + if (pp && pp.source) id = pp.source; + return id; + } + + this.executeCommand = executeCommand; + + this.getParams = function() { + if (window.__vkpc_data && window.__vkpc_data.command != 'afterInjection') { + checkPlaylist(); + } + var havePlayer = window.audioPlayer !== undefined; + var havePlaylist = havePlayer && (window.padAudioPlaylist && !!padAudioPlaylist()); + + return { + havePlayer: havePlayer, + havePlaylist: havePlaylist, + isPlaying: window.audioPlayer && window.audioPlayer.player && !window.audioPlayer.player.paused(), + tabId: window.curNotifier && curNotifier.instance_id, + trackId: window.audioPlayer && audioPlayer.id, + playlistId: havePlaylist ? _lastPlaylistId : 0, + lsSourceId: getLastInstanceId() + }; + }; + + this.init = function(sid) { + if (_checkPlaylistTimer === null) { + _checkPlaylistTimer = setInterval(function() { + if ((_lastPlaylistId || _lastPlaylistSummary) && !padAudioPlaylist()) { + clearPlaylist(true, 'timer'); // TODO func + } + }, 1000); + } + + if (!window.__wrappedByVKPC && window.audioPlayer && window.ls && window.stManager) { + if (!stManager.__done) { + stManager.__done = stManager.done; + stManager.done = function(fn) { + if (fn == 'audioplayer.js') { + wrapAudioMethods(); // TODO func + } + stManager.__done.apply(stManager, arguments); + }; + } + + wrapAudioMethods(); + + if (!ls.__set) { + ls.__set = ls.set; + ls.set = function(k, v) { + ls.__set.apply(ls, arguments); + if (k == 'pad_playlist') { + log('pad_playlist updated:', v); + updatePlaylist(getPlaylist(v)); // TODO func + } + }; + } + if (!ls.__remove) { + ls.__remove = ls.remove; + ls.remove = function(k, v) { + ls.__remove.apply(ls, arguments); + if (k == 'pad_playlist') { + log('pad_playlist removed from ls'); + //self.clearPlaylist(true, 'ls.remove'); + // self.clearPlaylist(); + } + }; + } + + window.__wrappedByVKPC = true; + } + + if (sid === _sid) { + return; + } + if (_sid !== null) { + clear(); // TODO + } + _sid = sid; + + log('(re)inited OK'); + }; + + this.getSID = function() { + return _sid; + }; + + this.getLastPlaylistID = function() { + return _lastPlaylistId; + }; + + this.getLastInstanceId = getLastInstanceId; + this.clearPlaylist = clearPlaylist; +}; // window.VKPC = ... + +} // if (!window.VKPC) ... + +if (!window.DOMContentLoaded) { + window.console && console.log && console.log("[VKPC] !window.DOMContentLoaded, exising"); + return; +} + +window.DOMContentLoaded(function() { + VKPC.init(vkpc_sid); +}); + +// afterInjection + +chrome.runtime.sendMessage(window.__vkpc_data.extid, { + cmd: "injection_result", + id: parseInt(window.__vkpc_data.injid, 10), + data: VKPC.getParams() +}); + +})(window.__vkpc_data.sid); + +delete window.__vkpc_data; diff --git a/firefox/bootstrap.js b/firefox/bootstrap.js new file mode 100644 index 0000000..4d65d68 --- /dev/null +++ b/firefox/bootstrap.js @@ -0,0 +1,73 @@ +Components.utils.import("resource://gre/modules/Services.jsm"); + +try { + var console = (Components.utils.import("resource://gre/modules/devtools/Console.jsm", {})).console; +} catch (e) {} + +function log() { + var msgs = [], i, tmp; + for (i = 0; i < arguments.length; i++) { + if (arguments[i] instanceof Error) tmp = [arguments[i], arguments[i].stack]; + else tmp = arguments[i]; + msgs.push(tmp); + } + + msgs.unshift('[VKPC bootstrap.js]'); + try { + console.log.apply(console, msgs); + } catch(e) {} +} + +function startup(data, reason) { + Components.utils.import("chrome://vkpc/content/module.jsm"); + VKPC.startup(); + + var windows = Services.wm.getEnumerator("navigator:browser"); + while (windows.hasMoreElements()) { + var win = windows.getNext().QueryInterface(Components.interfaces.nsIDOMWindow); + VKPC.addWindow(win, win.document && win.document.readyState == 'complete'); + } + + Services.wm.addListener(WindowListener); +} +function shutdown(data, reason) { + if (reason == APP_SHUTDOWN) + return; + + Services.wm.removeListener(WindowListener); + + VKPC.shutdown(); + + if (reason == ADDON_DISABLE) { + Services.obs.notifyObservers(null, "startupcache-invalidate", null); + Services.obs.notifyObservers(null, "chrome-flush-caches", null); + } +} +function install(data, reason) {} +function uninstall(data, reason) {} + +function forEachOpenWindow() { + var windows = Services.wm.getEnumerator("navigator:browser"); + while (windows.hasMoreElements()) { + VKPC.addWindow(windows.getNext().QueryInterface(Components.interfaces.nsIDOMWindow)); + } +} + +var WindowListener = { + onOpenWindow: function(xulWindow) { + var window = xulWindow.QueryInterface(Components.interfaces.nsIInterfaceRequestor) + .getInterface(Components.interfaces.nsIDOMWindow); + window.addEventListener("load", function onWindowLoad() { + window.removeEventListener("load", onWindowLoad); + + if (window.document.documentElement.getAttribute("windowtype") == "navigator:browser") { + VKPC.addWindow(window, true); + } + }); + }, + onCloseWindow: function(xulWindow) { + VKPC.removeWindow(xulWindow.QueryInterface(Components.interfaces.nsIInterfaceRequestor) + .getInterface(Components.interfaces.nsIDOMWindow)); + }, + onWindowTitleChange: function(xulWindow, newTitle) { } +}; diff --git a/firefox/chrome.manifest b/firefox/chrome.manifest new file mode 100644 index 0000000..883ec3d --- /dev/null +++ b/firefox/chrome.manifest @@ -0,0 +1,2 @@ +content vkpc content/ +overlay chrome://browser/content/browser.xul chrome://vkpc/content/overlay.xul diff --git a/firefox/content/icon.png b/firefox/content/icon.png Binary files differnew file mode 100644 index 0000000..6667635 --- /dev/null +++ b/firefox/content/icon.png diff --git a/firefox/content/module.jsm b/firefox/content/module.jsm new file mode 100644 index 0000000..dbadee1 --- /dev/null +++ b/firefox/content/module.jsm @@ -0,0 +1,569 @@ +var EXPORTED_SYMBOLS = ['VKPC']; + +try { + var console = (Components.utils.import("resource://gre/modules/devtools/Console.jsm", {})).console; +} catch (e) {} + +var browser = { + id: 1, + chrome: false, + safari: false, + yandex: false, + firefox: true, + opera: false +}; + +var started = false; + +function createTimeout(callback, interval) { + return new function() { + var timer = Components.classes["@mozilla.org/timer;1"].createInstance(Components.interfaces.nsITimer); + timer.initWithCallback({ + notify: function() { + callback(); + } + }, interval, Components.interfaces.nsITimer.TYPE_ONE_SHOT); + + this.cancel = function() { + timer.cancel(); + timer = null; + }; + } +} +function createInterval(callback, interval) { + return new function() { + var timer = Components.classes["@mozilla.org/timer;1"].createInstance(Components.interfaces.nsITimer); + timer.initWithCallback({ + notify: function() { + callback(); + } + }, interval, Components.interfaces.nsITimer.TYPE_REPEATING_SLACK); + + this.cancel = function() { + timer.cancel(); + timer = null; + }; + } +} +function log() { + return; // comment for debugging + + var msgs = [], i, tmp; + for (i = 0; i < arguments.length; i++) { + if (arguments[i] instanceof Error) tmp = [arguments[i], arguments[i].stack]; + else tmp = arguments[i]; + msgs.push(tmp); + } + + msgs.unshift('[VKPC module.jsm]'); + try { + console.log.apply(console, msgs); + } catch(e) {} +} +function extend(dest, source) { + for (var i in source) { + dest[i] = source[i]; + } + return dest; +} +function createCData(data) { + var parser = Components.classes["@mozilla.org/xmlextras/domparser;1"] + .createInstance(Components.interfaces.nsIDOMParser); + var doc = parser.parseFromString('<xml></xml>', "application/xml"); + var cdata = doc.createCDATASection(data); + doc.getElementsByTagName('xml')[0].appendChild(cdata); + return cdata; +} +function remove(element) { + element.parentNode.removeChild(element); +} + +function InjectionResponses(id, count, callback) { + this.id = id; + this.results = []; + this.lsSource = null; + this.maxCount = count; + this.callback = callback || function() {}; + + Injections.register(this.id, this); +} +extend(InjectionResponses.prototype, { + addResponse: function(id, response) { + this.results.push({tab: id, data: response}); + if (!this.lsSource && response && response.lastInstanceId) this.lsSource = response.lastInstanceId; + if (this.results.length == this.maxCount) { + this.callback(); + } + }, + unregister: function() { + Injections.unregister(this.id); + } +}); + +var Injections = { + id: 0, + objs: {}, + getNextId: function() { + if (this.id == Number.MAX_VALUE) { + this.id = -1; + } + return ++this.id; + }, + get: function(id) { + return this.objs[id] || false; + }, + register: function(id, obj) { + this.objs[id] = obj; + }, + unregister: function(id) { + if (this.objs[id] !== undefined) delete this.objs[id]; + } +}; + +var WSC_STATE_NONE = 'NONE', + WSC_STATE_OK = 'OK', + WSC_STATE_CLOSED = 'CLOSED', + WSC_STATE_ERR = 'ERR'; +function WSClient(address, protocol, opts) { + this.state = WSC_STATE_NONE; + this._ws = null; + + this.address = address; + this.protocol = protocol; + + this._onmessage = opts.onmessage; + this._onclose = opts.onclose; + this._onerror = opts.onerror; + this._onopen = opts.onopen; + + this._pingTimer = null; + this._reconnectTimer = null; +} +extend(WSClient.prototype, { + connect: function(callback) { + this.state = WSC_STATE_NONE; + var self = this; + + this._waitForWebSocketAvailable(function(_websocket) { + log('_waitForWebSocketAvailable DONE'); + self._ws = new _websocket(self.address, self.protocol); + + if (!self._ws) { + log('websockets are not supported'); + return; + } + + self._ws.onopen = function() { + self.state = WSC_STATE_OK; + self._setTimers(); + self._onopen && self._onopen.apply(self); + }; + self._ws.onerror = function() { + self._unsetTimers(); + if (self.state != WSC_STATE_ERR) { + self.state = WSC_STATE_ERR; + } + self._onerror && self._onerror.apply(self); + }; + self._ws.onclose = function() { + self._unsetTimers(); + if (self.state != WSC_STATE_ERR) { + self.state = WSC_STATE_ERR; + } + self._onclose && self._onclose.apply(self); + }; + self._ws.onmessage = function(e) { + self._onmessage && self._onmessage.apply(self, [e.data]); + }; + + callback && callback(); + }, 200); + }, + close: function() { + this._unsetTimers(); + if (this._ws) { + this.state = WSC_STATE_CLOSED; + this._ws.close(); + } + }, + reconnect: function() { + var self = this; + if (this.state == WSC_STATE_OK) { + try { + log('[WSClient reconnect] state = '+this.state+', why reconnect?'); + } catch (e) {} + return; + } + if (this._reconnectTimer) { + this._reconnectTimer.cancel(); + } + this._reconnectTimer = createTimeout(function() { + self.connect(); + }, 3000); + }, + send: function(obj) { + obj._browser = browser.id; + var self = this; + this._waitForConnection(function() { + self._ws.send(JSON.stringify(obj)); + }, 200); + }, + _setTimers: function() { + var self = this; + this._unsetTimers(); + this._pingTimer = createInterval(function() { + try { + self._ws.send("PING"); + } catch (e) { + log('[WSClient _pingTimer]', e); + } + }, 30000); + }, + _unsetTimers: function() { + if (this._pingTimer) + this._pingTimer.cancel(); + }, + _waitForConnection: function(callback, interval) { + if (this._ws.readyState === 1) { + callback(); + } else { + var self = this; + var timer = createTimeout(function() { + timer.cancel(); + self._waitForConnection(callback, interval); + }, interval); + } + }, + _waitForWebSocketAvailable: function(callback, interval) { + var win, self = this; + try { + win = Components.classes["@mozilla.org/appshell/appShellService;1"]. + getService(Components.interfaces.nsIAppShellService). + hiddenDOMWindow; + } catch (e) { + var timer = createTimeout(function() { + timer.cancel(); + self._waitForWebSocketAvailable(callback, interval); + }, interval); + } finally { + if (win) { + callback(win.WebSocket || win.MozWebSocket); + } + } + } +}); + +var Documents = { + list: [], + add: function(doc) { + this.cleanup(); + this.list.push(doc); + }, + cleanup: function() { + this.list = this.list.filter(function(t) { + return Object.prototype.toString.call(t) != '[object DeadObject]'; + }); + }, + send: function(json) { + var self = this; + this.cleanup(); + + this.list.forEach(function(doc) { + self.sendToDoc(doc, json); + }); + }, + sendToDoc: function(doc, json) { + var cdata = createCData(JSON.stringify(json)); + doc.getElementById('utils').appendChild(cdata); + + var evt = doc.createEvent("Events"); + evt.initEvent("VKPCBgMessage", true, false); + cdata.dispatchEvent(evt); + }, + getCount: function() { + this.cleanup(); + return this.list.length; + } +}; + +function sendClear() { + wsc.send({command: 'clearPlaylist', data: null}); +} + +function prepareWindow(win) { + function onPageLoaded(e) { + var doc = e.originalTarget, loc = doc.location; + if (!loc.href.match(/^https?:\/\/vk.com\/.*$/)) return; + + doc.addEventListener("VKPCInjectedMessage", function(e) { + var target = e.target, json = JSON.parse(target.data || "{}"), doc = target.ownerDocument; + receiveMessage(json, doc, target); + }, false); + + var loader = Components.classes["@mozilla.org/moz/jssubscript-loader;1"] + .getService(Components.interfaces.mozIJSSubScriptLoader); + loader.loadSubScript("chrome://vkpc/content/vkpc.js", doc); + } + + var appcontent = win.document.getElementById("appcontent"); + if (appcontent) { + appcontent.addEventListener("DOMContentLoaded", onPageLoaded, true); + } +} + +// receive message from tab +function receiveMessage(json, doc, target) { + switch (json.cmd) { + case "register": + Documents.add(doc); + break; + + case "afterInjection": + var id = json.id; + var obj = Injections.get(id); + if (obj) { + obj.addResponse(doc, json.data); + } + break; + + case "to_app": + wsc.send(json.data); + break; + } + + try { + remove(target); + } catch (e) {} +} + +// send message to tabs +function sendMessage(data, tab) { + if (tab) { + Documents.sendToDoc(tab, data); + } else { + Documents.send(data); + } +} + +function inject(command/*, callback*/) { + //log('inject', command); + var injId = Injections.getNextId(); + var data = { + sid: Controller.sid, + command: command + }; + + var okTab_nowPlaying, okTab_playlistFound, okTab_lsSource, okTab_recentlyPlayed, okTab_havePlaylist, + activeTab, lastTab, outdatedTabs = [], tabsWithPlayingMusic = []; + var lsSourceId, appPlaylistFound = false; + + var injResponses, injResults; + + function onDone(step) { + var results = injResponses.results; + // var execCommand = getCode("VKPC.executeCommand('"+command+"', "+Controller.playlistId+")"); + var vkpcCommand = {cmd: 'vkpc', command: command, playlistId: Controller.playlistId}; + + if (command == 'afterInjection') { + // log('[afterInjection onDone] results.length='+results.length); + + for (var i = 0; i < results.length; i++) { + var data = results[i].data, tab = results[i].tab; + + if (data.playlistId != 0 && data.playlistId == Controller.playlistId) { + appPlaylistFound = true; + } + if (data.havePlaylist && data.playlistId != 0 && data.playlistId != Controller.playlistId) { + outdatedTabs.push(tab); + } + if (data.havePlaylist) { + okTab_havePlaylist = tab; + } + if (data.isPlaying) { + okTab_nowPlaying = tab; + } + } + + if (!appPlaylistFound) { + var okTab = okTab_nowPlaying || okTab_havePlaylist; + if (okTab !== undefined) { + sendMessage(vkpcCommand, okTab); + } else { + sendClear(); + } + } + + for (var i = 0; i < outdatedTabs.length; i++) { + sendMessage({cmd: 'vkpc', command: 'clearPlaylist'}, outdatedTabs[i]); + } + } else { + for (var i = 0; i < results.length; i++) { + var data = results[i].data; + if (!lsSourceId && data.lsSourceId) { + lsSourceId = data.lsSourceId; + break; + } + } + + for (var i = 0; i < results.length; i++) { + var data = results[i].data, tab = results[i].tab; + + if (data.playlistId == Controller.playlistId) { + okTab_playlistFound = tab; + } + if (data.havePlayer && (data.isPlaying || typeof data.trackId == 'string')) { + okTab_recentlyPlayed = tab; + } + if (data.isPlaying) { + okTab_nowPlaying = tab; + } + if (lsSourceId == data.tabId) { + okTab_lsSource = tab; + } + + lastTab = tab; + } + + var check = [okTab_nowPlaying, okTab_lsSource, okTab_recentlyPlayed, okTab_recentlyPlayed, okTab_havePlaylist, activeTab, lastTab]; + for (var i = 0; i < check.length; i++) { + if (check[i] !== undefined) { + sendMessage(vkpcCommand, check[i]); + // chrome.tabs.executeScript(check[i], {code: execCommand}); + break; + } + } + } + + injResponses.unregister(); + } + + var count = Documents.getCount(); + //log('vk tabs count: ' + count); + + if (!count) { + log('vk tabs not found'); + sendClear(); + return; + } + + injResponses = new InjectionResponses(injId, Documents.getCount(), onDone); + sendMessage({ + cmd: "afterInjection", + id: injId, + data: data + }); +}; + +var Controller = { + sid: 0, + playlistId: 0, + clear: function() { + this.sid = 0; + this.playlistId = 0; + } +}; + +var VKPC = new function() { + var timer; + log('VKPC()'); + + var windows = []; + this.addWindow = function(win, notWait) { + if (windows.indexOf(win) == -1) { + log('window added', win); + windows.push(win); + + if (!notWait) { + win.addEventListener('load', function load(e) { + win.removeEventListener('load', load, false); + prepareWindow(win); + }, false); + } else { + prepareWindow(win); + } + } + }; + this.removeWindow = function(win) { + var index; + if ((index = windows.indexOf(win)) != -1) { + log('window removed', win); + windows.splice(index, 1); + } + }; + + var self = this; + this.startup = function() { + if (started) { + log('already started, ignoring'); + return; + } + + wsc = new WSClient("wss://vkpc-local.ch1p.com:56130", "signaling-protocol", { + onopen: function() { + Controller.clear(); + this.send({command: 'setBrowser'}); + + if (timer) { + timer.cancel(); + } + timer = createInterval(function() { + inject('afterInjection'); + }, 2000); + }, + onmessage: function(cmd) { + log('[wsc onmessage] cmd:', cmd); + + var json = JSON.parse(cmd); + switch (json.command) { + case 'set_sid': + Controller.sid = json.data; + break; + + case 'set_playlist_id': + Controller.playlistId = json.data; + break; + + case 'vkpc': + inject(json.data); + break; + } + }, + onerror: function() { + if (timer) { + timer.cancel(); + } + this.reconnect(); + }, + onclose: function() { + if (timer) { + timer.cancel(); + } + if (started) { + this.reconnect(); + } + } + }); + wsc.connect(); + + self.wsc = wsc; + started = true; + }; + this.shutdown = function() { + if (!started) { + return; + } + + started = false; + + if (wsc) { + wsc.close(); + wsc = undefined; + } + if (timer) { + timer.cancel(); + timer = undefined; + } + }; + + log('init finish'); +}; diff --git a/firefox/content/overlay.js b/firefox/content/overlay.js new file mode 100644 index 0000000..49bb695 --- /dev/null +++ b/firefox/content/overlay.js @@ -0,0 +1,8 @@ +Components.utils.import("chrome://vkpc/content/module.jsm"); + +VKPC.startup(); + +VKPC.addWindow(window); +window.addEventListener('close', function() { + VKPC.removeWindow(window); +}); diff --git a/firefox/content/overlay.xul b/firefox/content/overlay.xul new file mode 100644 index 0000000..d2ca855 --- /dev/null +++ b/firefox/content/overlay.xul @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<!DOCTYPE overlay > +<overlay id="vkpc-overlay" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script type="application/javascript" src="chrome://vkpc/content/overlay.js" /> +</overlay> diff --git a/firefox/content/vkpc.js b/firefox/content/vkpc.js new file mode 100644 index 0000000..2ce3129 --- /dev/null +++ b/firefox/content/vkpc.js @@ -0,0 +1,769 @@ +// VKPC for Firefox + +(function() { + +if (!document.addEventListener) { + window.console && console.log("[VKPC] an outdated browser detected, very strange, plz update"); + return; +} + +// variables +var _debug = window.__vkpc_debug || true; +var _isFocused = false; + +(function(window, document) { + var queue = [], done = false, _top = true, root = document.documentElement, eventsAdded = false; + + function init(e) { + if (e.type == 'readystatechange' && document.readyState != 'complete') return; + (e.type == 'load' ? window : document).removeEventListener(e.type, init); + if (!done) { + done = true; + while (queue.length) { + queue.shift().call(window); + } + } + } + function poll() { + try { + root.doScroll('left'); + } catch (e) { + setTimeout(poll, 50); + return; + } + init('poll'); + } + + window.DOMContentLoaded = function(fn) { + if (document.readyState == 'complete' || done) { + fn.call(window); + } else { + queue.push(fn); + + if (!eventsAdded) { + if (document.createEventObject && root.doScroll) { + try { + _top = !window.frameElement; + } catch (e) {} + if (_top) poll(); + } + + document.addEventListener('DOMContentLoaded', init); + document.addEventListener('readystatechange', init); + window.addEventListener('load', init); + eventsAdded = true; + } + } + } +})(window, document); + +function log() { + if (!_debug) + return; + var args = Array.prototype.slice.call(arguments); + args.unshift(window.VKPC ? '[VKPC '+window.VKPC.getSID()+']' : '[VKPC]'); + try { + window.console && console.log.apply(console, args); + } catch (e) {} +} +function trim(string) { + return string.replace(/(^\s+)|(\s+$)/g, ""); +} +function startsWith(str, needle) { + return str.indexOf(needle) == 0; +} +function endsWith(str, suffix) { + return str.indexOf(suffix, str.length - suffix.length) !== -1; +} +function random(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; +} +function shuffle(o) { + for (var j, x, i = o.length; i; j = Math.floor(Math.random() * i), x = o[--i], o[i] = o[j], o[j] = x); + return o; +} +function getStackTrace(split) { + split = split === undefined ? true : split; + try { + o.lo.lo += 0; + } catch(e) { + if (e.stack) { + return split ? e.stack.split('\n') : e.stack; + } + } + return null; +} +function buildQueryString(obj) { + var list = [], i; + for (i in obj) { + list.push(encodeURIComponent(i) + '=' + encodeURIComponent(obj[i])); + } + return list.join('&'); +} +function stripTags(html) { + var div = document.createElement("div"); + div.innerHTML = html; + return div.textContent || div.innerText || ""; +} +function decodeEntities(value) { + var textarea = document.createElement('textarea'); + textarea.innerHTML = value; + return textarea.value; +} +function createCData(data) { + var docu = new DOMParser().parseFromString('<xml></xml>', "application/xml"); + var cdata = docu.createCDATASection(data); + docu.getElementsByTagName('xml')[0].appendChild(cdata); + return cdata; +} +function remove() { + remove.parentNode.removeChild(remove); +} + +window.VKPC = new function() { + var _sid = null; + var _currentTrackId = null; + var _lastPlaylistSummary = null; + var _lastPlaylistId = 0; + var _operateQueue = []; + var _setTrackIdTimeout = null; + var _watchGraphicsChange = false; + var _checkPlaylistTimer = null; + + function wrapAudioMethods() { + // var self = this; + if (window.audioPlayer) { + if (!audioPlayer.__operate) { + audioPlayer.__operate = audioPlayer.operate; + audioPlayer.operate = function(id, nextPlaylist, opts) { + var currentId = audioPlayer.id, _status = id != currentId ? 'play' : null; + audioPlayer.__operate.apply(audioPlayer, arguments); + //self.firstOperateAfterPlaylistUpdating = false; + log('operate(), arguments:', arguments); + + if (existsInCurrentPlaylist(id)) { + log('operate(), found in current pl, setTrackId() now'); + setTrackId(id, _status); + } else { + log('operate(), not found, setToOperateQueue() now'); + setToOperateQueue(id, _status); + } + }; + } + + // disable it + if (false && !audioPlayer.__setGraphics) { + audioPlayer.__setGraphics = audioPlayer.setGraphics; + audioPlayer.setGraphics = function(act) { + audioPlayer.__setGraphics.apply(audioPlayer, arguments); + return; + /*if (self.watchGraphicsChange) { + if (browser.safari) self.sendOperateTrack(audioPlayer.id, (act == 'play' || act == 'load') ? 'play' : 'pause'); + self.watchGraphicsChange = false; + }*/ + }; + } + } + + log('[wrapAudioMethods] wrapped DONE'); + } + + function clear() { + log('clear()'); + _currentTrackId = null; + _lastPlaylistSummary = null; + _lastPlaylistId = null; + _sid = null; + _watchGraphicsChange = false; + } + + function getBrowser() { + return browser.safari ? 'safari' : 'chrome'; + } + + function executeCommand(command, plid) { + if (command == 'afterInjection') { + log('executeCommand: afterInjection, plid='+plid); + var pl = padAudioPlaylist(); + if (window.audioPlayer && pl) { + updatePlaylist(getPlaylist(pl)); + } else { + clearPlaylist(); + } + return; + } + + log('executeCommand:', command, plid); + // var self = this; + + if (!window.audioPlayer || !padAudioPlaylist()) { + log('[executeCommand] audioplayer or playlist not found'); + stManager.add(['audioplayer.js'], function() { + executeAfterPadLoading(function() { + log('[executeCommand] after execafterpadloading, window.audioPlayer:', window.audioPlayer); + wrapAudioMethods(); + + var plist = padAudioPlaylist(); + if (plist) { + log('[executeCommand] after exec...: send updatePlaylist() with plist'); + updatePlaylist(getPlaylist(plist)); + } + + if (command == 'playpause' || command == 'next' || command == 'prev') { + log('[executeCommand] after exec...: simple command'); + var id = getPlayFirstId(); + if (id) { + log('[executeCommand] after exec...: found id='+id+', playAudioNew() now'); + playAudioNew(id); + } else if (plist && plist.start) { + log('[executeCommand] after exec...: found plist.start, playAudioNew() now'); + playAudioNew(plist.start); + } + } else if (startsWith(command, 'operateTrack:')) { // TODO this is new fix + var id = parseInt(command.replace('operateTrack:')); + log('[executeCommand] after exec...: got operateTrack, id='+id); + if (!plist[id]) { + log('[executeCommand] after exec...: after got operateTrack: plist[id] not found, send new pl to app'); + //self.clearPlaylist(); + updatePlaylist(getPlaylist(plist)); + if (plist.start) { + log('[executeCommand] after exec...: got operateTrack, pl not found... ... play plist.start now'); + playAudioNew(plist.start); + } + } else { + log('[executeCommand] after exec...: got operateTrack, it is found, playAudioNew() now'); + playAudioNew(id); + } + } + }); + }); + return; + } + + function evaluateCommand(command) { + switch (command) { + case 'next': + case 'prev': + case 'playpause': + if (audioPlayer.id) { + if (command == 'next') next(); + else if (command == 'prev') prev(); + else if (command == 'playpause') playPause(); + } else { + var id = getPlayFirstId(); + if (id) playId(id); + } + break; + + default: + if (startsWith(command, 'operateTrack:')) { + log('[executeCommand] got operateTrack;'); + var id = command.replace('operateTrack:', ''), pl = padAudioPlaylist(); + if (pl[id] !== undefined) { + log('[executeCommand] got operateTrack; track is found, playAudioNew() now'); + //playAudioNew(id); + //audioPlayer.operate(id); + playId(id); + } else { + log('[executeCommand] got operateTrack; track not found, updatePlaylist with pl:', pl); + updatePlaylist(getPlaylist(pl)); + var id = getPlayFirstId(); + if (id) { + log('[executeCommand] got operateTrack; play id from getPlayFirstId() now'); + playId(id); + } + } + } + break; + } + } + + if (plid != _lastPlaylistId) { + log('[executeCommand] plid does not match; plid='+plid+', _lastPlaylistId='+_lastPlaylistId); + var pl = padAudioPlaylist(); + if (pl) { + updatePlaylist(getPlaylist(pl), true); + log('[executeCommand] plid does not match, sent updatePlaylist() with pl:', pl); + + if (plid == 0) { + evaluateCommand(command); + } else { + if (['next', 'prev', 'playpause'].indexOf(command) != -1) { + var id = audioPlayer.id || pl.start || getPlayFirstId(); + if (id) { + playId(id); + } + } + } + } + } else { + evaluateCommand(command); + } + } + + function setTrackId(id, _status) { + _status = _status || (audioPlayer.player.paused() ? 'pause' : 'play'); + clearTimeout(_setTrackIdTimeout); + + var check = function() { + if (audioPlayer.player) { + sendOperateTrack(id, _status); + } else { + _setTrackIdTimeout = setTimeout(check, 200); + } + }; + check(); + } + + function sendOperateTrack(id, _status) { + log('[sendOperateTrack]', id, _status); + toApp('operateTrack', { + 'id': id, + 'status': _status, + 'playlistId': _lastPlaylistId + }); + } + + function setToOperateQueue(id, _status) { + var q = _operateQueue; + for (var i = 0; i < q.length; i++) { + var track = q[i]; + if (track[0] == id) { + track[1] = _status; + return; + } + } + q.push([id, _status]); + } + + function existsInCurrentPlaylist(id) { + return _lastPlaylistSummary && _lastPlaylistSummary.indexOf(id) != -1; + } + + function processOperateQueue(pl) { + log('[processOperateQueue]'); + var q = _operateQueue; + while (q.length) { + var track = q.shift(); + log('[processOperateQueue] track:', track[0]); + if (pl[track[0]] !== undefined) { + log('[processOperateQueue] track', track[0], 'found, send it now'); + sendOperateTrack(track[0], track[1]); + } + } + } + + function clearOperateQueue() { + _operateQueue = []; + } + + function printPlaylist() { + var pl = padAudioPlaylist(); + if (pl) { + for (var k in pl) { + log(pl[k][5] + ' - ' + pl[k][6]); + } + } + } + + function getPlaylist(_pl) { + _pl = _pl || padAudioPlaylist(); + var pl = null; + if (_pl) { + var start = _pl.start, pl = []; + var nextId = start; + do { + if (_pl[nextId]) { + _pl[nextId]._vkpcId = nextId; + pl.push(_pl[nextId]); + nextId = _pl[nextId]._next; + } + } while (nextId != '' && nextId !== undefined && nextId != start); + } + return pl; + } + + // force=true is used when plids not match + function updatePlaylist(pl, force) { + var tracks = [], summary = [], title; + if (pl) { + for (var k = 0; k < pl.length; k++) { + tracks.push({ + id: pl[k]._vkpcId, + artist: decodeEntities(pl[k][5]), + title: decodeEntities(pl[k][6]), + duration: pl[k][4] + }); + summary.push(pl[k]._vkpcId); + } + + summary = summary.join(';'); + + log("updatePlaylist: _lastPlaylistSummary:", _lastPlaylistSummary, 'summary:', summary); + if (force || _lastPlaylistSummary === null || _lastPlaylistSummary !== summary) { + log('[updatePlaylist] last summary not matched;', _lastPlaylistSummary, summary); + var activeId = '', activeStatus = ''; + var vkpl = padAudioPlaylist(); + var plTitle = (window.audioPlaylist && window.audioPlaylist.htitle) || vkpl.htitle; + if (audioPlayer.id && vkpl[audioPlayer.id] !== undefined) { + activeId = audioPlayer.id; + _watchGraphicsChange = true; + + activeStatus = getPlayerStatus(true) ? 'play' : 'pause'; + _watchGraphicsChange = true; + } + + _lastPlaylistSummary = summary; + _lastPlaylistId = random(100000, 1000000); + + log("[updatePlaylist] send pl with id="+_lastPlaylistId+', activeId='+activeId+', activeStatus='+activeStatus+' to app'); + try { + toApp('updatePlaylist', { + tracks: tracks, + title: parsePlaylistTitle(plTitle) || "", + id: _lastPlaylistId, + active: { 'id': activeId, 'status': activeStatus }, + browser: getBrowser() + }); + } catch(e) { + log('[updatePlaylist] exception:', e, e.stack); + } + + processOperateQueue(pl); + } + } + } + + function clearPlaylist(no_send, called_from) { + called_from = called_from || ""; + log('[clearPlaylist] (called from: '+called_from+')'); + + _lastPlaylistSummary = null; + _lastPlaylistId = 0; + if (!no_send) { + toApp('clearPlaylist', {}); + } + } + + function checkPlaylist() { + var pl = padAudioPlaylist(); + if (!pl) { + clearPlaylist(true, 'checkPlaylist'); + } + } + + function parsePlaylistTitle(str) { + str = str || ""; + str = trim(str); + if (str == '') return str; + + var starts = { + 0: '\u0421\u0435\u0439\u0447\u0430\u0441 \u0438\u0433\u0440\u0430\u0435\u0442 \u2014 ', // Сейчас играет ru + 100: '\u041d\u044b\u043d\u0447\u0435 \u0438\u0433\u0440\u0430\u0435\u0442\u044a\u2014 ', // Нынче играетъ re + 3: 'Now playing \u2014 ', // en + 1: '\u0417\u0430\u0440\u0430\u0437 \u0437\u0432\u0443\u0447\u0438\u0442\u044c \u2014 ', // Зараз звучить ua + 777: '\u041f\u0440\u043e\u0438\u0433\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f \u043f\u043b\u0430\u0441\u0442\u0438\u043d\u043a\u0430 \u00ab' // Проигрывается пластинка su + }; + var ends = { + 0: ' \\| [0-9]+ \u0430\u0443\u0434\u0438\u043e\u0437\u0430[^\\s]+$', + 3: ' \\| [0-9]+ audio [^\\s]+$', + 1: ' \\| [0-9]+ \u0430\u0443\u0434\u0456\u043e\u0437\u0430[^\\s]+$', + 100: ' \\| [0-9]+ \u043a\u043e\u043c\u043f\u043e\u0437[^\\s]+$', + 777: ' \\| [0-9]+ \u0433\u0440\u0430\u043c\u0437\u0430[^\\s]+$' + }; + + if (window.vk && vk.lang !== undefined) { + log('1', starts[vk.lang] !== undefined, startsWith(str, starts[vk.lang]), str, starts[vk.lang], str.indexOf(starts[vk.lang])); + if (starts[vk.lang] !== undefined && startsWith(str, starts[vk.lang])) { + str = str.substring(starts[vk.lang].length); + } + + if (ends[vk.lang] !== undefined) { + var regex = new RegExp(ends[vk.lang], 'i'); + if (str.match(regex)) str = str.replace(regex, ''); + } + } + + return stripTags(trim(str)); + } + + function afterInjection() { + log("after injection"); + var pl = getPlaylist(); + if (pl) updatePlaylist(pl); + } + + function next() { + audioPlayer.nextTrack(true, !window.audioPlaylist) + /*if (audioPlayer.controls && audioPlayer.controls.pd && audioPlayer.controls.pd.next) { + audioPlayer.controls.pd.next.click(); + } else { + audioPlayer.nextTrack(true, !window.audioPlaylist) + }*/ + } + + function prev() { + audioPlayer.prevTrack(true, !window.audioPlaylist); + /*if (audioPlayer.controls && audioPlayer.controls.pd && audioPlayer.controls.pd.prev) { + audioPlayer.controls.pd.prev.click(); + } else { + audioPlayer.prevTrack(true, !window.audioPlaylist); + }*/ + } + + function getPlayFirstId() { + var id = currentAudioId() || ls.get('audio_id') || (window.audioPlaylist && audioPlaylist.start); + return id || null; + } + + function playFirst() { + var id = getPlayFirstId(); + + if (id) playId(id); + else { + var plist = padAudioPlaylist(); + if (plist && plist.start) { + playId(plist.start); + } else { + executeAfterPadLoading(function() { + var plist = padAudioPlaylist(); + if (plist && plist.start) { + playId(plist.start); + } + }); + } + } + } + + function executeAfterPadLoading(f) { + Pads.show('mus'); + window.onPlaylistLoaded = function() { + if (f) { + try { + f(); + } catch(e) {} + } + setTimeout(function() { + Pads.show('mus'); + }, 10); + } + } + + function getPlayerStatus(justStarted) { + if (!audioPlayer.player) return false; + try { + var pl = audioPlayer.player; + if (pl && pl.music && pl.music.buffered && !pl.music.buffered.length && justStarted) return true; + } catch (e) { + return true; + } + + return audioPlayer.player && !audioPlayer.player.paused(); + } + + function pauseForSafari() { + if (window.audioPlayer && audioPlayer.player) audioPlayer.pauseTrack(); + } + + function playPause() { + if (window.audioPlayer && audioPlayer.player) { + if (audioPlayer.player.paused()) { + audioPlayer.playTrack(); + } else { + audioPlayer.pauseTrack(); + } + } + } + + function operateTrack(id) { + if (id == audioPlayer.id) { + playPause(); + } else { + audioPlayer.operate(id); + } + } + + function playId(id) { + if (window.audioPlayer) audioPlayer.operate(id); + else playAudioNew(id); + } + + function getLastInstanceId() { + var id = null, pp = ls.get('pad_playlist'); + if (pp && pp.source) id = pp.source; + //return [id, ls.get('vkpc_lastid')]; + return id; + } + + this.executeCommand = executeCommand; + + this.getParams = function(command) { + if (command != 'afterInjection') { + checkPlaylist(); + } + var havePlayer = window.audioPlayer !== undefined; + var havePlaylist = havePlayer && (window.padAudioPlaylist && !!padAudioPlaylist()); + + return { + havePlayer: havePlayer, + havePlaylist: havePlaylist, + isPlaying: window.audioPlayer && window.audioPlayer.player && !window.audioPlayer.player.paused(), + tabId: window.curNotifier && curNotifier.instance_id, + trackId: window.audioPlayer && audioPlayer.id, + playlistId: havePlaylist ? _lastPlaylistId : 0, + lsSourceId: getLastInstanceId() + }; + }; + + this.init = function(sid) { + if (_checkPlaylistTimer === null) { + _checkPlaylistTimer = setInterval(function() { + if ((_lastPlaylistId || _lastPlaylistSummary) && !padAudioPlaylist()) { + clearPlaylist(true, 'timer'); // TODO func + } + }, 1000); + } + + if (!window.__wrappedByVKPC && window.audioPlayer && window.ls && window.stManager) { + if (!stManager.__done) { + stManager.__done = stManager.done; + stManager.done = function(fn) { + if (fn == 'audioplayer.js') { + wrapAudioMethods(); // TODO func + } + stManager.__done.apply(stManager, arguments); + }; + } + + wrapAudioMethods(); + + if (!ls.__set) { + ls.__set = ls.set; + ls.set = function(k, v) { + ls.__set.apply(ls, arguments); + if (k == 'pad_playlist') { + log('pad_playlist updated:', v); + updatePlaylist(getPlaylist(v)); // TODO func + } + }; + } + if (!ls.__remove) { + ls.__remove = ls.remove; + ls.remove = function(k, v) { + ls.__remove.apply(ls, arguments); + if (k == 'pad_playlist') { + log('pad_playlist removed from ls'); + //self.clearPlaylist(true, 'ls.remove'); + // self.clearPlaylist(); + } + }; + } + + window.__wrappedByVKPC = true; + } + + if (sid === _sid) { + return; + } + if (_sid !== null) { + clear(); // TODO + } + _sid = sid; + + log('(re)inited OK'); + }; + + this.getSID = function() { + return _sid; + }; + + this.getLastPlaylistID = function() { + return _lastPlaylistId; + }; + + this.getLastInstanceId = getLastInstanceId; + this.clearPlaylist = clearPlaylist; + this.parsePlaylistTitle = parsePlaylistTitle; +}; // window.VKPC = ... + +if (!window.DOMContentLoaded) { + window.console && console.log && console.log("[VKPC] !window.DOMContentLoaded, exising"); + return; +} + +window.DOMContentLoaded(function() { + if (window.vk) { + document.addEventListener("VKPCBgMessage", receiveMessage, false); + sendMessage({cmd: 'register'}); + } +}); + +window.addEventListener("focus", function(e) { + _isFocused = true; +}, false); +window.addEventListener("blur", function(e) { + _isFocused = false +}, false); + +// send message to background +function sendMessage(json) { + var cdata = createCData(JSON.stringify(json)); + document.getElementById('utils').appendChild(cdata); + + var evt = document.createEvent("Events"); + evt.initEvent("VKPCInjectedMessage", true, false); + cdata.dispatchEvent(evt); +} + +// received message from background +function receiveMessage(e) { + // log('receiveMessage', e); + var target = e.target, json = JSON.parse(target.data || "{}"); + + switch (json.cmd) { + case "afterInjection": + VKPC.init(json.data.sid); + + var params = VKPC.getParams(json.data.command); + params.isFocused = _isFocused; + + sendMessage({ + cmd: "afterInjection", + data: params, + id: json.id + }); + break; + + case "vkpc": + switch (json.command) { + case "clearPlaylist": + VKPC.clearPlaylist(true, "as"); + break; + + default: + VKPC.executeCommand(json.command, json.playlistId); + break; + } + + break; + } + + try { + remove(target); + } catch (e) {} +} + +// wrapper for sendMessage() to send message directly to VKPC.app +function toApp(command, data) { + var json = { + // bg: 1, + cmd: "to_app", + data: { + command: command, + data: data + } + }; + sendMessage(json); +} + +})(); diff --git a/firefox/install.rdf b/firefox/install.rdf new file mode 100644 index 0000000..e9ef2e9 --- /dev/null +++ b/firefox/install.rdf @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:em="http://www.mozilla.org/2004/em-rdf#"> + <Description about="urn:mozilla:install-manifest"> + <em:id>vkpc@ch1p.com</em:id> + <em:name>VK Player Controller</em:name> + <em:version>3.0.1</em:version> + <em:type>2</em:type> + <em:creator>ch1p</em:creator> + <em:description>This is a part of VK Player Controller for OSX. For more information, please visit https://ch1p.com/vkpc/</em:description> + <em:iconURL>chrome://vkpc/content/icon.png</em:iconURL> + <em:bootstrap>true</em:bootstrap> + + <em:targetApplication> + <Description> + <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id> + <em:minVersion>4.0</em:minVersion> + <em:maxVersion>99.0</em:maxVersion> + </Description> + </em:targetApplication> + </Description> +</RDF> diff --git a/graphics/Legacy/pl_cell_bg.psd b/graphics/Legacy/pl_cell_bg.psd Binary files differnew file mode 100644 index 0000000..a9287bc --- /dev/null +++ b/graphics/Legacy/pl_cell_bg.psd diff --git a/graphics/Legacy/pl_cell_bg@2x.psd b/graphics/Legacy/pl_cell_bg@2x.psd Binary files differnew file mode 100644 index 0000000..8e9a369 --- /dev/null +++ b/graphics/Legacy/pl_cell_bg@2x.psd diff --git a/graphics/Legacy/pl_cell_pressed_bg.psd b/graphics/Legacy/pl_cell_pressed_bg.psd Binary files differnew file mode 100644 index 0000000..9895eac --- /dev/null +++ b/graphics/Legacy/pl_cell_pressed_bg.psd diff --git a/graphics/Legacy/pl_cell_pressed_bg@2x.psd b/graphics/Legacy/pl_cell_pressed_bg@2x.psd Binary files differnew file mode 100644 index 0000000..f2388a2 --- /dev/null +++ b/graphics/Legacy/pl_cell_pressed_bg@2x.psd diff --git a/graphics/Legacy/pl_pause.psd b/graphics/Legacy/pl_pause.psd Binary files differnew file mode 100644 index 0000000..c00c8df --- /dev/null +++ b/graphics/Legacy/pl_pause.psd diff --git a/graphics/Legacy/pl_pause@2x.psd b/graphics/Legacy/pl_pause@2x.psd Binary files differnew file mode 100644 index 0000000..a089b6f --- /dev/null +++ b/graphics/Legacy/pl_pause@2x.psd diff --git a/graphics/Legacy/pl_play.psd b/graphics/Legacy/pl_play.psd Binary files differnew file mode 100644 index 0000000..64327de --- /dev/null +++ b/graphics/Legacy/pl_play.psd diff --git a/graphics/Legacy/pl_play@2x.psd b/graphics/Legacy/pl_play@2x.psd Binary files differnew file mode 100644 index 0000000..36cdc52 --- /dev/null +++ b/graphics/Legacy/pl_play@2x.psd diff --git a/graphics/Legacy/pl_title_separator.psd b/graphics/Legacy/pl_title_separator.psd Binary files differnew file mode 100644 index 0000000..ffac263 --- /dev/null +++ b/graphics/Legacy/pl_title_separator.psd diff --git a/graphics/Legacy/pl_title_separator@2x.psd b/graphics/Legacy/pl_title_separator@2x.psd Binary files differnew file mode 100644 index 0000000..609a809 --- /dev/null +++ b/graphics/Legacy/pl_title_separator@2x.psd diff --git a/graphics/Legacy/settings.psd b/graphics/Legacy/settings.psd Binary files differnew file mode 100644 index 0000000..858cc8e --- /dev/null +++ b/graphics/Legacy/settings.psd diff --git a/graphics/Legacy/settings@2x.psd b/graphics/Legacy/settings@2x.psd Binary files differnew file mode 100644 index 0000000..d40f31c --- /dev/null +++ b/graphics/Legacy/settings@2x.psd diff --git a/graphics/Legacy/settings_pressed.psd b/graphics/Legacy/settings_pressed.psd Binary files differnew file mode 100644 index 0000000..053ced1 --- /dev/null +++ b/graphics/Legacy/settings_pressed.psd diff --git a/graphics/Legacy/settings_pressed@2x.psd b/graphics/Legacy/settings_pressed@2x.psd Binary files differnew file mode 100644 index 0000000..3f86440 --- /dev/null +++ b/graphics/Legacy/settings_pressed@2x.psd diff --git a/graphics/Legacy/vkpc.psd b/graphics/Legacy/vkpc.psd Binary files differnew file mode 100644 index 0000000..b98f35f --- /dev/null +++ b/graphics/Legacy/vkpc.psd diff --git a/graphics/Legacy/vkpc@2x.psd b/graphics/Legacy/vkpc@2x.psd Binary files differnew file mode 100644 index 0000000..d90f77d --- /dev/null +++ b/graphics/Legacy/vkpc@2x.psd diff --git a/graphics/Legacy/vkpc_pressed.psd b/graphics/Legacy/vkpc_pressed.psd Binary files differnew file mode 100644 index 0000000..fd1629d --- /dev/null +++ b/graphics/Legacy/vkpc_pressed.psd diff --git a/graphics/Legacy/vkpc_pressed@2x.psd b/graphics/Legacy/vkpc_pressed@2x.psd Binary files differnew file mode 100644 index 0000000..0d8449e --- /dev/null +++ b/graphics/Legacy/vkpc_pressed@2x.psd diff --git a/graphics/Logo/Icon1024.png b/graphics/Logo/Icon1024.png Binary files differnew file mode 100644 index 0000000..9687a9b --- /dev/null +++ b/graphics/Logo/Icon1024.png diff --git a/graphics/Logo/Sizes/128.png b/graphics/Logo/Sizes/128.png Binary files differnew file mode 100644 index 0000000..6667635 --- /dev/null +++ b/graphics/Logo/Sizes/128.png diff --git a/graphics/Logo/Sizes/16.png b/graphics/Logo/Sizes/16.png Binary files differnew file mode 100644 index 0000000..18018ce --- /dev/null +++ b/graphics/Logo/Sizes/16.png diff --git a/graphics/Logo/Sizes/256.png b/graphics/Logo/Sizes/256.png Binary files differnew file mode 100644 index 0000000..73fc7b4 --- /dev/null +++ b/graphics/Logo/Sizes/256.png diff --git a/graphics/Logo/Sizes/32.png b/graphics/Logo/Sizes/32.png Binary files differnew file mode 100644 index 0000000..80791ea --- /dev/null +++ b/graphics/Logo/Sizes/32.png diff --git a/graphics/Logo/Sizes/512.png b/graphics/Logo/Sizes/512.png Binary files differnew file mode 100644 index 0000000..fcf8ee4 --- /dev/null +++ b/graphics/Logo/Sizes/512.png diff --git a/graphics/Logo/Sizes/64.png b/graphics/Logo/Sizes/64.png Binary files differnew file mode 100644 index 0000000..f36a1de --- /dev/null +++ b/graphics/Logo/Sizes/64.png diff --git a/graphics/Logo/VKPC.icns b/graphics/Logo/VKPC.icns Binary files differnew file mode 100644 index 0000000..ce19b9c --- /dev/null +++ b/graphics/Logo/VKPC.icns diff --git a/graphics/Logo/VKPC.iconset/icon_128x128.png b/graphics/Logo/VKPC.iconset/icon_128x128.png Binary files differnew file mode 100644 index 0000000..6667635 --- /dev/null +++ b/graphics/Logo/VKPC.iconset/icon_128x128.png diff --git a/graphics/Logo/VKPC.iconset/icon_128x128@2x.png b/graphics/Logo/VKPC.iconset/icon_128x128@2x.png Binary files differnew file mode 100644 index 0000000..73fc7b4 --- /dev/null +++ b/graphics/Logo/VKPC.iconset/icon_128x128@2x.png diff --git a/graphics/Logo/VKPC.iconset/icon_16x16.png b/graphics/Logo/VKPC.iconset/icon_16x16.png Binary files differnew file mode 100644 index 0000000..77ebb6d --- /dev/null +++ b/graphics/Logo/VKPC.iconset/icon_16x16.png diff --git a/graphics/Logo/VKPC.iconset/icon_16x16@2x.png b/graphics/Logo/VKPC.iconset/icon_16x16@2x.png Binary files differnew file mode 100644 index 0000000..2fb317e --- /dev/null +++ b/graphics/Logo/VKPC.iconset/icon_16x16@2x.png diff --git a/graphics/Logo/VKPC.iconset/icon_256x256.png b/graphics/Logo/VKPC.iconset/icon_256x256.png Binary files differnew file mode 100644 index 0000000..73fc7b4 --- /dev/null +++ b/graphics/Logo/VKPC.iconset/icon_256x256.png diff --git a/graphics/Logo/VKPC.iconset/icon_256x256@2x.png b/graphics/Logo/VKPC.iconset/icon_256x256@2x.png Binary files differnew file mode 100644 index 0000000..fcf8ee4 --- /dev/null +++ b/graphics/Logo/VKPC.iconset/icon_256x256@2x.png diff --git a/graphics/Logo/VKPC.iconset/icon_32x32.png b/graphics/Logo/VKPC.iconset/icon_32x32.png Binary files differnew file mode 100644 index 0000000..2fb317e --- /dev/null +++ b/graphics/Logo/VKPC.iconset/icon_32x32.png diff --git a/graphics/Logo/VKPC.iconset/icon_32x32@2x.png b/graphics/Logo/VKPC.iconset/icon_32x32@2x.png Binary files differnew file mode 100644 index 0000000..f36a1de --- /dev/null +++ b/graphics/Logo/VKPC.iconset/icon_32x32@2x.png diff --git a/graphics/Logo/VKPC.iconset/icon_512x512.png b/graphics/Logo/VKPC.iconset/icon_512x512.png Binary files differnew file mode 100644 index 0000000..fcf8ee4 --- /dev/null +++ b/graphics/Logo/VKPC.iconset/icon_512x512.png diff --git a/graphics/Logo/VKPC.iconset/icon_512x512@2X.png b/graphics/Logo/VKPC.iconset/icon_512x512@2X.png Binary files differnew file mode 100644 index 0000000..9614227 --- /dev/null +++ b/graphics/Logo/VKPC.iconset/icon_512x512@2X.png diff --git a/graphics/Popover.psd b/graphics/Popover.psd Binary files differnew file mode 100644 index 0000000..4d821a4 --- /dev/null +++ b/graphics/Popover.psd diff --git a/graphics/StatusIcon/black.png b/graphics/StatusIcon/black.png Binary files differnew file mode 100644 index 0000000..f33b1bd --- /dev/null +++ b/graphics/StatusIcon/black.png diff --git a/graphics/StatusIcon/black@2x.png b/graphics/StatusIcon/black@2x.png Binary files differnew file mode 100644 index 0000000..4cf24ca --- /dev/null +++ b/graphics/StatusIcon/black@2x.png diff --git a/graphics/StatusIcon/white.png b/graphics/StatusIcon/white.png Binary files differnew file mode 100644 index 0000000..450722b --- /dev/null +++ b/graphics/StatusIcon/white.png diff --git a/graphics/StatusIcon/white.psd b/graphics/StatusIcon/white.psd Binary files differnew file mode 100644 index 0000000..3e210de --- /dev/null +++ b/graphics/StatusIcon/white.psd diff --git a/graphics/StatusIcon/white@2x.png b/graphics/StatusIcon/white@2x.png Binary files differnew file mode 100644 index 0000000..f9b3b9c --- /dev/null +++ b/graphics/StatusIcon/white@2x.png diff --git a/graphics/Yosemite/pl_cell_bg.psd b/graphics/Yosemite/pl_cell_bg.psd Binary files differnew file mode 100644 index 0000000..2aad657 --- /dev/null +++ b/graphics/Yosemite/pl_cell_bg.psd diff --git a/graphics/Yosemite/pl_cell_bg@2x.psd b/graphics/Yosemite/pl_cell_bg@2x.psd Binary files differnew file mode 100644 index 0000000..e7294bd --- /dev/null +++ b/graphics/Yosemite/pl_cell_bg@2x.psd diff --git a/graphics/Yosemite/pl_cell_pressed_bg.psd b/graphics/Yosemite/pl_cell_pressed_bg.psd Binary files differnew file mode 100644 index 0000000..35c6265 --- /dev/null +++ b/graphics/Yosemite/pl_cell_pressed_bg.psd diff --git a/graphics/Yosemite/pl_cell_pressed_bg@2x.psd b/graphics/Yosemite/pl_cell_pressed_bg@2x.psd Binary files differnew file mode 100644 index 0000000..57ee7ab --- /dev/null +++ b/graphics/Yosemite/pl_cell_pressed_bg@2x.psd diff --git a/graphics/Yosemite/pl_pause.png b/graphics/Yosemite/pl_pause.png Binary files differnew file mode 100644 index 0000000..73a00d9 --- /dev/null +++ b/graphics/Yosemite/pl_pause.png diff --git a/graphics/Yosemite/pl_pause.psd b/graphics/Yosemite/pl_pause.psd Binary files differnew file mode 100644 index 0000000..e50d1f2 --- /dev/null +++ b/graphics/Yosemite/pl_pause.psd diff --git a/graphics/Yosemite/pl_pause@2x.png b/graphics/Yosemite/pl_pause@2x.png Binary files differnew file mode 100644 index 0000000..2cde740 --- /dev/null +++ b/graphics/Yosemite/pl_pause@2x.png diff --git a/graphics/Yosemite/pl_pause@2x.psd b/graphics/Yosemite/pl_pause@2x.psd Binary files differnew file mode 100644 index 0000000..022fe0b --- /dev/null +++ b/graphics/Yosemite/pl_pause@2x.psd diff --git a/graphics/Yosemite/pl_play.png b/graphics/Yosemite/pl_play.png Binary files differnew file mode 100644 index 0000000..422f589 --- /dev/null +++ b/graphics/Yosemite/pl_play.png diff --git a/graphics/Yosemite/pl_play.psd b/graphics/Yosemite/pl_play.psd Binary files differnew file mode 100644 index 0000000..bd36f84 --- /dev/null +++ b/graphics/Yosemite/pl_play.psd diff --git a/graphics/Yosemite/pl_play@2x.png b/graphics/Yosemite/pl_play@2x.png Binary files differnew file mode 100644 index 0000000..2d45059 --- /dev/null +++ b/graphics/Yosemite/pl_play@2x.png diff --git a/graphics/Yosemite/pl_play@2x.psd b/graphics/Yosemite/pl_play@2x.psd Binary files differnew file mode 100644 index 0000000..cc45a50 --- /dev/null +++ b/graphics/Yosemite/pl_play@2x.psd diff --git a/graphics/Yosemite/pl_title_separator.psd b/graphics/Yosemite/pl_title_separator.psd Binary files differnew file mode 100644 index 0000000..5b82e50 --- /dev/null +++ b/graphics/Yosemite/pl_title_separator.psd diff --git a/graphics/Yosemite/pl_title_separator@2x.psd b/graphics/Yosemite/pl_title_separator@2x.psd Binary files differnew file mode 100644 index 0000000..bc28fb7 --- /dev/null +++ b/graphics/Yosemite/pl_title_separator@2x.psd diff --git a/graphics/Yosemite/settings.psd b/graphics/Yosemite/settings.psd Binary files differnew file mode 100644 index 0000000..da68b1e --- /dev/null +++ b/graphics/Yosemite/settings.psd diff --git a/graphics/Yosemite/settings@2x.psd b/graphics/Yosemite/settings@2x.psd Binary files differnew file mode 100644 index 0000000..bf1adea --- /dev/null +++ b/graphics/Yosemite/settings@2x.psd diff --git a/graphics/Yosemite/settings_pressed.psd b/graphics/Yosemite/settings_pressed.psd Binary files differnew file mode 100644 index 0000000..246e351 --- /dev/null +++ b/graphics/Yosemite/settings_pressed.psd diff --git a/graphics/Yosemite/settings_pressed@2x.psd b/graphics/Yosemite/settings_pressed@2x.psd Binary files differnew file mode 100644 index 0000000..c372d6b --- /dev/null +++ b/graphics/Yosemite/settings_pressed@2x.psd diff --git a/graphics/YosemiteDark/pl_cell_bg.psd b/graphics/YosemiteDark/pl_cell_bg.psd Binary files differnew file mode 100644 index 0000000..281e2b2 --- /dev/null +++ b/graphics/YosemiteDark/pl_cell_bg.psd diff --git a/graphics/YosemiteDark/pl_cell_bg@2x.psd b/graphics/YosemiteDark/pl_cell_bg@2x.psd Binary files differnew file mode 100644 index 0000000..a1c8457 --- /dev/null +++ b/graphics/YosemiteDark/pl_cell_bg@2x.psd diff --git a/graphics/YosemiteDark/pl_cell_pressed_bg.psd b/graphics/YosemiteDark/pl_cell_pressed_bg.psd Binary files differnew file mode 100644 index 0000000..c435655 --- /dev/null +++ b/graphics/YosemiteDark/pl_cell_pressed_bg.psd diff --git a/graphics/YosemiteDark/pl_cell_pressed_bg@2x.psd b/graphics/YosemiteDark/pl_cell_pressed_bg@2x.psd Binary files differnew file mode 100644 index 0000000..2f59790 --- /dev/null +++ b/graphics/YosemiteDark/pl_cell_pressed_bg@2x.psd diff --git a/graphics/YosemiteDark/pl_pause.png b/graphics/YosemiteDark/pl_pause.png Binary files differnew file mode 100644 index 0000000..75babd0 --- /dev/null +++ b/graphics/YosemiteDark/pl_pause.png diff --git a/graphics/YosemiteDark/pl_pause.psd b/graphics/YosemiteDark/pl_pause.psd Binary files differnew file mode 100644 index 0000000..adce208 --- /dev/null +++ b/graphics/YosemiteDark/pl_pause.psd diff --git a/graphics/YosemiteDark/pl_pause@2x.png b/graphics/YosemiteDark/pl_pause@2x.png Binary files differnew file mode 100644 index 0000000..ae380cd --- /dev/null +++ b/graphics/YosemiteDark/pl_pause@2x.png diff --git a/graphics/YosemiteDark/pl_pause@2x.psd b/graphics/YosemiteDark/pl_pause@2x.psd Binary files differnew file mode 100644 index 0000000..60df095 --- /dev/null +++ b/graphics/YosemiteDark/pl_pause@2x.psd diff --git a/graphics/YosemiteDark/pl_play.png b/graphics/YosemiteDark/pl_play.png Binary files differnew file mode 100644 index 0000000..b90acfb --- /dev/null +++ b/graphics/YosemiteDark/pl_play.png diff --git a/graphics/YosemiteDark/pl_play.psd b/graphics/YosemiteDark/pl_play.psd Binary files differnew file mode 100644 index 0000000..4724785 --- /dev/null +++ b/graphics/YosemiteDark/pl_play.psd diff --git a/graphics/YosemiteDark/pl_play@2x.png b/graphics/YosemiteDark/pl_play@2x.png Binary files differnew file mode 100644 index 0000000..c963dd3 --- /dev/null +++ b/graphics/YosemiteDark/pl_play@2x.png diff --git a/graphics/YosemiteDark/pl_play@2x.psd b/graphics/YosemiteDark/pl_play@2x.psd Binary files differnew file mode 100644 index 0000000..6c8c435 --- /dev/null +++ b/graphics/YosemiteDark/pl_play@2x.psd diff --git a/graphics/YosemiteDark/pl_title_separator.psd b/graphics/YosemiteDark/pl_title_separator.psd Binary files differnew file mode 100644 index 0000000..f4cafbc --- /dev/null +++ b/graphics/YosemiteDark/pl_title_separator.psd diff --git a/graphics/YosemiteDark/pl_title_separator@2x.psd b/graphics/YosemiteDark/pl_title_separator@2x.psd Binary files differnew file mode 100644 index 0000000..633bbc0 --- /dev/null +++ b/graphics/YosemiteDark/pl_title_separator@2x.psd diff --git a/graphics/YosemiteDark/settings.psd b/graphics/YosemiteDark/settings.psd Binary files differnew file mode 100644 index 0000000..5d1a08b --- /dev/null +++ b/graphics/YosemiteDark/settings.psd diff --git a/graphics/YosemiteDark/settings@2x.psd b/graphics/YosemiteDark/settings@2x.psd Binary files differnew file mode 100644 index 0000000..b47c0d2 --- /dev/null +++ b/graphics/YosemiteDark/settings@2x.psd diff --git a/graphics/YosemiteDark/settings_pressed.psd b/graphics/YosemiteDark/settings_pressed.psd Binary files differnew file mode 100644 index 0000000..59e280d --- /dev/null +++ b/graphics/YosemiteDark/settings_pressed.psd diff --git a/graphics/YosemiteDark/settings_pressed@2x.psd b/graphics/YosemiteDark/settings_pressed@2x.psd Binary files differnew file mode 100644 index 0000000..3a6bc15 --- /dev/null +++ b/graphics/YosemiteDark/settings_pressed@2x.psd diff --git a/graphics/logo.psd b/graphics/logo.psd Binary files differnew file mode 100644 index 0000000..14b53b2 --- /dev/null +++ b/graphics/logo.psd diff --git a/opera/bg.js b/opera/bg.js new file mode 100644 index 0000000..91fea9a --- /dev/null +++ b/opera/bg.js @@ -0,0 +1,192 @@ +var wsc, injectInterval; + +function init() { + // receive messages from webpage + chrome.runtime.onMessageExternal.addListener(function(msg, sender, sendResponse) { + if (msg.cmd == "injection_result") { + var obj = Injections.get(msg.id); + if (obj) { + obj.addResponse(sender.tab.id, msg.data); + } + } + if (msg.cmd == "to_app") { + // log('to_app received', msg.data); + wsc.send(msg.data); + } + }); + + // connect to the app + wsc = new WSClient("wss://vkpc-local.ch1p.com:56130", "signaling-protocol", { + onopen: function() { + Controller.clear(); + this.send({command: 'setBrowser'}); + }, + onmessage: function(cmd) { + var json = JSON.parse(cmd); + switch (json.command) { + case 'set_sid': + Controller.sid = json.data; + break; + + case 'set_playlist_id': + Controller.playlistId = json.data; + break; + + case 'vkpc': + inject(json.data); + break; + } + + // executeCommand(msg); + }, + onerror: function() { + this.reconnect(); + }, + onclose: function() { + this.reconnect(); + } + }); + wsc.connect(); + + injectInterval = setInterval(function() { + inject('afterInjection'); + }, 2000); +} + +function sendClear() { + wsc.send({command: 'clearPlaylist', data: null}); +} + +function inject(command, callback) { + var injId = Injections.getNextId(); + var data = { + extid: getExtensionId(), + injid: injId, + sid: Controller.sid, + command: command + }; + var code_inj = "var el = document.createElement('script');" + + "el.src = chrome.extension.getURL('vkpc.js');" + + "document.body.appendChild(el);" + + "var el1 = document.createElement('script');" + + "el1.textContent = 'window.__vkpc_data = "+JSON.stringify(data)+"';" + + "document.body.appendChild(el1)"; + + var okTab_nowPlaying, okTab_playlistFound, okTab_lsSource, okTab_recentlyPlayed, okTab_havePlaylist, + activeTab, lastTab, outdatedTabs = [], tabsWithPlayingMusic = []/*, tabPlaylistIds = {}*/; + var lsSourceId, appPlaylistFound = false; + + var injResponses, injResults; + + function getCode(code) { + return "var el = document.createElement('script');" + + "el.textContent = '"+code.replace(/'/g, "\\'")+"';" + + "document.body.appendChild(el)"; + } + function onDone(step) { + var results = injResponses.results; + var execCommand = getCode("VKPC.executeCommand('"+command+"', "+Controller.playlistId+")"); + + if (command == 'afterInjection') { + //log('[afterInjection onDone] results.length='+results.length); + + for (var i = 0; i < results.length; i++) { + var data = results[i].data, tab = results[i].tab; + + // tabPlaylistIds[tab] = data.playlistId; + if (data.playlistId != 0 && data.playlistId == Controller.playlistId) { + appPlaylistFound = true; + } + if (data.havePlaylist && data.playlistId != 0 && data.playlistId != Controller.playlistId) { + outdatedTabs.push(tab); + } + if (data.havePlaylist) { + okTab_havePlaylist = tab; + } + if (data.isPlaying) { + okTab_nowPlaying = tab; + } + } + + if (!appPlaylistFound) { + var okTab = okTab_nowPlaying || okTab_havePlaylist; + if (okTab !== undefined) { + chrome.tabs.executeScript(okTab, {code: execCommand}); + } else if (!appPlaylistFound) { + sendClear(); + } + } + + for (var i = 0; i < outdatedTabs.length; i++) { + chrome.tabs.executeScript(outdatedTabs[i], {code: getCode('VKPC.clearPlaylist(true, "as")')}); + } + } else { + for (var i = 0; i < results.length; i++) { + var data = results[i].data; + if (!lsSourceId && data.lsSourceId) { + lsSourceId = data.lsSourceId; + break; + } + } + + for (var i = 0; i < results.length; i++) { + var data = results[i].data, tab = results[i].tab; + + if (data.playlistId == Controller.playlistId) { + okTab_playlistFound = tab; + } + if (data.havePlayer && (data.isPlaying || typeof data.trackId == 'string')) { + okTab_recentlyPlayed = tab; + } + if (data.isPlaying) { + okTab_nowPlaying = tab; + } + if (lsSourceId == data.tabId) { + okTab_lsSource = tab; + } + + lastTab = tab; + } + + var check = [okTab_nowPlaying, okTab_lsSource, okTab_recentlyPlayed, okTab_recentlyPlayed, okTab_havePlaylist, activeTab, lastTab]; + //log('check[] =', check); + for (var i = 0; i < check.length; i++) { + if (check[i] !== undefined) { + chrome.tabs.executeScript(check[i], {code: execCommand}); + break; + } + } + } + + injResponses.unregister(); + callback && callback(); + } + + getVKTabs(function(tabs) { + if (!tabs.length) { + sendClear(); + return; + } + + injResponses = new InjectionResponses(injId, tabs.length, onDone); + for (var i = 0; i < tabs.length; i++) { + if (tabs[i].active) { + activeTab = tabs[i].id; + } + chrome.tabs.executeScript(tabs[i].id, { + code: code_inj + }); + } + }); +}; + +var Controller = { + sid: 0, + playlistId: 0, + clear: function() { + this.sid = 0; + this.playlistId = 0; + } +}; + +DOMContentLoaded(init); diff --git a/opera/common.js b/opera/common.js new file mode 100644 index 0000000..9acd7ec --- /dev/null +++ b/opera/common.js @@ -0,0 +1,270 @@ +var browser = (function() { + var ua = navigator.userAgent.toLowerCase(); + var browser = { + id: null, + chrome: false, + safari: false, + yandex: false, + firefox: false, + opera: false + }; + + if (/opr/i.test(ua) && /chrome/i.test(ua)) { + browser.opera = true; + browser.id = 3; + } else if (/yabrowser/i.test(ua) && /chrome/i.test(ua)) { + browser.yandex = true; + browser.id = 4; + } else if (/firefox|iceweasel/i.test(ua)) { + browser.firefox = true; + browser.id = 1; + } else if (!(/chrome/i.test(ua)) && /webkit|safari|khtml/i.test(ua)) { + browser.safari = true; + browser.id = 2; + } else if (/chrome/i.test(ua)) { + browser.chrome = true; + browser.id = 0; + } + + return browser; +})(); + +function getExtensionId() { + return chrome.i18n.getMessage("@@extension_id"); +} + +function getVKTabs(callback) { + var vkTabs = []; + chrome.tabs.query({}, function(tabs) { + for (var i = 0; i < tabs.length; i++) { + var tab = tabs[i]; + if (tab.url.match(new RegExp('https?://vk.com/.*', 'gi'))) { + vkTabs.push(tab); + } + } + callback(vkTabs); + }); +} + +function extend(dest, source) { + for (var i in source) { + dest[i] = source[i]; + } + return dest; +} +function log() { + var msgs = [], i, tmp; + for (i = 0; i < arguments.length; i++) { + if (arguments[i] instanceof Error) tmp = [arguments[i], arguments[i].stack]; + else tmp = arguments[i]; + msgs.push(tmp); + } + + try { + console.log.apply(console, msgs); + } catch(e) {} +} +function intval(value) { + if (value === true) return 1; + return parseInt(value) || 0; +} +function str(v) { + var str; + if (v && v.toString) + str = v.toString(); + else + str = v + ''; + if (str == '[object Object]') { + str = JSON.stringify(v); + } + return str; +} + +var WSC_STATE_NONE = 'NONE', + WSC_STATE_OK = 'OK', + WSC_STATE_CLOSED = 'CLOSED', + WSC_STATE_ERR = 'ERR'; +function WSClient(address, protocol, opts) { + this.state = WSC_STATE_NONE; + this._ws = null; + + this.address = address; + this.protocol = protocol; + + this._onmessage = opts.onmessage; + this._onclose = opts.onclose; + this._onerror = opts.onerror; + this._onopen = opts.onopen; + + this._pingTimer = null; + this._reconnectTimer = null; +} +extend(WSClient.prototype, { + connect: function() { + this.state = WSC_STATE_NONE; + var self = this; + + var _websocket = window.WebSocket || window.MozWebSocket; + if (!_websocket) { + log('[WSClient connect] websockets are not supported'); + return; + } + + this._ws = new _websocket(this.address, this.protocol); + this._ws.onopen = function() { + self.state = WSC_STATE_OK; + self._setTimers(); + self._onopen && self._onopen.apply(self); + }; + this._ws.onerror = function() { + self._unsetTimers(); + if (self.state != WSC_STATE_ERR) { + self.state = WSC_STATE_ERR; + } + self._onerror && self._onerror.apply(self); + }; + this._ws.onclose = function() { + self._unsetTimers(); + if (self.state != WSC_STATE_ERR) { + self.state = WSC_STATE_ERR; + } + self._onclose && self._onclose.apply(self); + }; + this._ws.onmessage = function(e) { + self._onmessage && self._onmessage.apply(self, [e.data]); + }; + }, + close: function() { + this._unsetTimers(); + this._ws.close(); + }, + reconnect: function() { + var self = this; + if (this.state == WSC_STATE_OK) { + log('[WSClient reconnect] state = '+this.state+', why reconnect?'); + return; + } + clearTimeout(this._reconnectTimer); + this._reconnectTimer = setTimeout(function() { + self.connect(); + }, 3000); + }, + send: function(obj) { + obj._browser = browser.id; + var self = this; + this._waitForConnection(function() { + self._ws.send(JSON.stringify(obj)); + }, 200); + }, + _setTimers: function() { + var self = this; + this._pingTimer = setInterval(function() { + try { + self._ws.send("PING"); + } catch (e) { + log('[WSClient _pingTimer]', e); + } + }, 30000); + }, + _unsetTimers: function() { + clearInterval(this._pingTimer); + }, + _waitForConnection: function(callback, interval) { + if (this._ws.readyState === 1) { + callback(); + } else { + var self = this; + setTimeout(function() { + self._waitForConnection(callback, interval); + }, interval); + } + } +}); + +(function(window, document) { + var queue = [], done = false, _top = true, root = document.documentElement, eventsAdded = false; + + function init(e) { + if (e.type == 'readystatechange' && document.readyState != 'complete') return; + (e.type == 'load' ? window : document).removeEventListener(e.type, init); + if (!done) { + done = true; + while (queue.length) { + queue.shift().call(window); + } + } + } + function poll() { + try { + root.doScroll('left'); + } catch (e) { + setTimeout(poll, 50); + return; + } + init('poll'); + } + + window.DOMContentLoaded = function(fn) { + if (document.readyState == 'complete' || done) { + fn.call(window); + } else { + queue.push(fn); + + if (!eventsAdded) { + if (document.createEventObject && root.doScroll) { + try { + _top = !window.frameElement; + } catch (e) {} + if (_top) poll(); + } + + document.addEventListener('DOMContentLoaded', init); + document.addEventListener('readystatechange', init); + window.addEventListener('load', init); + eventsAdded = true; + } + } + } +})(window, document); + +var Injections = { + id: 0, + objs: {}, + getNextId: function() { + if (this.id == Number.MAX_VALUE) { + this.id = -1; + } + return ++this.id; + }, + get: function(id) { + return this.objs[id] || false; + }, + register: function(id, obj) { + this.objs[id] = obj; + }, + unregister: function(id) { + if (this.objs[id] !== undefined) delete this.objs[id]; + } +}; + +function InjectionResponses(id, count, callback) { + this.id = id; + this.results = []; + this.lsSource = null; + this.maxCount = count; + this.callback = callback || function() {}; + + Injections.register(this.id, this); +} +extend(InjectionResponses.prototype, { + addResponse: function(id, response) { + this.results.push({tab: id, data: response}); + if (!this.lsSource && response && response.lastInstanceId) this.lsSource = response.lastInstanceId; + if (this.results.length == this.maxCount) { + this.callback(); + } + }, + unregister: function() { + Injections.unregister(this.id); + } +}); diff --git a/opera/icons/128.png b/opera/icons/128.png Binary files differnew file mode 100644 index 0000000..6667635 --- /dev/null +++ b/opera/icons/128.png diff --git a/opera/icons/16.png b/opera/icons/16.png Binary files differnew file mode 100644 index 0000000..18018ce --- /dev/null +++ b/opera/icons/16.png diff --git a/opera/icons/32.png b/opera/icons/32.png Binary files differnew file mode 100644 index 0000000..80791ea --- /dev/null +++ b/opera/icons/32.png diff --git a/opera/manifest.json b/opera/manifest.json new file mode 100644 index 0000000..3a82a55 --- /dev/null +++ b/opera/manifest.json @@ -0,0 +1,30 @@ +{ + "manifest_version": 2, + "name": "VK Player Controller", + "description": "This is a part of VK Player Controller for OSX. For more information, please visit https://ch1p.com/vkpc/", + "version": "3.0", + "icons": { + "128": "icons/128.png", + "16": "icons/16.png", + "32": "icons/32.png" + }, + "content_security_policy": "script-src 'self' 'unsafe-eval' https://vk.com; object-src 'self' 'unsafe-eval'", + "permissions": [ + "tabs", + //"background", + "https://vk.com/*", + "http://vk.com/*", + "https://*.vk.com/*", + "http://*.vk.com/*" + ], + "background": { + "scripts": [ + "common.js", + "bg.js" + ] + }, + "externally_connectable": { + "matches": ["https://vk.com/*", "http://vk.com/*", "https://*.vk.com/*", "http://*.vk.com/*"] + }, + "web_accessible_resources": ["inject_and_return.js", "inject_exec.js", "vkpc.js"] +} diff --git a/opera/vkpc.js b/opera/vkpc.js new file mode 100644 index 0000000..b31d40a --- /dev/null +++ b/opera/vkpc.js @@ -0,0 +1,709 @@ +// VKPC for Opera + +(function(vkpc_sid) { +if (!window.VKPC) { + +if (!document.addEventListener) { + window.console && console.log("[VKPC] an outdated browser detected, very strange, plz update"); + return; +} + +// variables +var _debug = window.__vkpc_debug || true; +var _extid = window.__vkpc_data.extid; + +(function(window, document) { + var queue = [], done = false, _top = true, root = document.documentElement, eventsAdded = false; + + function init(e) { + if (e.type == 'readystatechange' && document.readyState != 'complete') return; + (e.type == 'load' ? window : document).removeEventListener(e.type, init); + if (!done) { + done = true; + while (queue.length) { + queue.shift().call(window); + } + } + } + function poll() { + try { + root.doScroll('left'); + } catch (e) { + setTimeout(poll, 50); + return; + } + init('poll'); + } + + window.DOMContentLoaded = function(fn) { + if (document.readyState == 'complete' || done) { + fn.call(window); + } else { + queue.push(fn); + + if (!eventsAdded) { + if (document.createEventObject && root.doScroll) { + try { + _top = !window.frameElement; + } catch (e) {} + if (_top) poll(); + } + + document.addEventListener('DOMContentLoaded', init); + document.addEventListener('readystatechange', init); + window.addEventListener('load', init); + eventsAdded = true; + } + } + } +})(window, document); + +function log() { + if (!_debug) + return; + var args = Array.prototype.slice.call(arguments); + args.unshift(window.VKPC ? '[VKPC '+window.VKPC.getSID()+']' : '[VKPC]'); + try { + window.console && console.log.apply(console, args); + } catch (e) {} +} +function trim(string) { + return string.replace(/(^\s+)|(\s+$)/g, ""); +} +function startsWith(str, needle) { + return str.indexOf(needle) == 0; +} +function endsWith(str, suffix) { + return str.indexOf(suffix, str.length - suffix.length) !== -1; +} +function random(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; +} +function shuffle(o) { + for (var j, x, i = o.length; i; j = Math.floor(Math.random() * i), x = o[--i], o[i] = o[j], o[j] = x); + return o; +} +function getStackTrace(split) { + split = split === undefined ? true : split; + try { + o.lo.lo += 0; + } catch(e) { + if (e.stack) { + return split ? e.stack.split('\n') : e.stack; + } + } + return null; +} +function buildQueryString(obj) { + var list = [], i; + for (i in obj) { + list.push(encodeURIComponent(i) + '=' + encodeURIComponent(obj[i])); + } + return list.join('&'); +} +function stripTags(html) { + var div = document.createElement("div"); + div.innerHTML = html; + return div.textContent || div.innerText || ""; +} +function decodeEntities(value) { + var textarea = document.createElement('textarea'); + textarea.innerHTML = value; + return textarea.value; +} + +function toApp(command, data) { + chrome.runtime.sendMessage(_extid, { + cmd: "to_app", + data: { + command: command, + data: data + } + }); +} + +window.VKPC = new function() { + var _sid = null; + var _currentTrackId = null; + var _lastPlaylistSummary = null; + var _lastPlaylistId = 0; + var _operateQueue = []; + var _setTrackIdTimeout = null; + var _watchGraphicsChange = false; + var _checkPlaylistTimer = null; + + function wrapAudioMethods() { + // var self = this; + if (window.audioPlayer) { + if (!audioPlayer.__operate) { + audioPlayer.__operate = audioPlayer.operate; + audioPlayer.operate = function(id, nextPlaylist, opts) { + var currentId = audioPlayer.id, _status = id != currentId ? 'play' : null; + audioPlayer.__operate.apply(audioPlayer, arguments); + //self.firstOperateAfterPlaylistUpdating = false; + log('operate(), arguments:', arguments); + + if (existsInCurrentPlaylist(id)) { + log('operate(), found in current pl, setTrackId() now'); + setTrackId(id, _status); + } else { + log('operate(), not found, setToOperateQueue() now'); + setToOperateQueue(id, _status); + } + }; + } + + // disable it + if (false && !audioPlayer.__setGraphics) { + audioPlayer.__setGraphics = audioPlayer.setGraphics; + audioPlayer.setGraphics = function(act) { + audioPlayer.__setGraphics.apply(audioPlayer, arguments); + return; + /*if (self.watchGraphicsChange) { + if (browser.safari) self.sendOperateTrack(audioPlayer.id, (act == 'play' || act == 'load') ? 'play' : 'pause'); + self.watchGraphicsChange = false; + }*/ + }; + } + } + + log('[wrapAudioMethods] wrapped DONE'); + } + + function clear() { + log('clear()'); + _currentTrackId = null; + _lastPlaylistSummary = null; + _lastPlaylistId = null; + _sid = null; + _watchGraphicsChange = false; + } + + function getBrowser() { + return browser.safari ? 'safari' : 'chrome'; + } + + function executeCommand(command, plid) { + if (command == 'afterInjection') { + log('executeCommand: afterInjection, plid='+plid); + var pl = padAudioPlaylist(); + if (window.audioPlayer && pl) { + updatePlaylist(getPlaylist(pl)); + } else { + clearPlaylist(); + } + return; + } + + log('executeCommand:', command, plid); + // var self = this; + + if (!window.audioPlayer || !padAudioPlaylist()) { + log('[executeCommand] audioplayer or playlist not found'); + stManager.add(['audioplayer.js'], function() { + executeAfterPadLoading(function() { + log('[executeCommand] after execafterpadloading, window.audioPlayer:', window.audioPlayer); + wrapAudioMethods(); + + var plist = padAudioPlaylist(); + if (plist) { + log('[executeCommand] after exec...: send updatePlaylist() with plist'); + updatePlaylist(getPlaylist(plist)); + } + + if (command == 'playpause' || command == 'next' || command == 'prev') { + log('[executeCommand] after exec...: simple command'); + var id = getPlayFirstId(); + if (id) { + log('[executeCommand] after exec...: found id='+id+', playAudioNew() now'); + playAudioNew(id); + } else if (plist && plist.start) { + log('[executeCommand] after exec...: found plist.start, playAudioNew() now'); + playAudioNew(plist.start); + } + } else if (startsWith(command, 'operateTrack:')) { // TODO this is new fix + var id = parseInt(command.replace('operateTrack:')); + log('[executeCommand] after exec...: got operateTrack, id='+id); + if (!plist[id]) { + log('[executeCommand] after exec...: after got operateTrack: plist[id] not found, send new pl to app'); + //self.clearPlaylist(); + updatePlaylist(getPlaylist(plist)); + if (plist.start) { + log('[executeCommand] after exec...: got operateTrack, pl not found... ... play plist.start now'); + playAudioNew(plist.start); + } + } else { + log('[executeCommand] after exec...: got operateTrack, it is found, playAudioNew() now'); + playAudioNew(id); + } + } + }); + }); + return; + } + + function evaluateCommand(command) { + switch (command) { + case 'next': + case 'prev': + case 'playpause': + if (audioPlayer.id) { + if (command == 'next') next(); + else if (command == 'prev') prev(); + else if (command == 'playpause') playPause(); + } else { + var id = getPlayFirstId(); + if (id) playId(id); + } + break; + + default: + if (startsWith(command, 'operateTrack:')) { + log('[executeCommand] got operateTrack;'); + var id = command.replace('operateTrack:', ''), pl = padAudioPlaylist(); + if (pl[id] !== undefined) { + log('[executeCommand] got operateTrack; track is found, playAudioNew() now'); + //playAudioNew(id); + //audioPlayer.operate(id); + playId(id); + } else { + log('[executeCommand] got operateTrack; track not found, updatePlaylist with pl:', pl); + updatePlaylist(getPlaylist(pl)); + var id = getPlayFirstId(); + if (id) { + log('[executeCommand] got operateTrack; play id from getPlayFirstId() now'); + playId(id); + } + } + } + break; + } + } + + if (plid != _lastPlaylistId) { + log('[executeCommand] plid does not match'); + var pl = padAudioPlaylist(); + if (pl) { + updatePlaylist(getPlaylist(pl), true); + log('[executeCommand] plid does not match, sent updatePlaylist() with pl:', pl); + + if (plid == 0) { + evaluateCommand(command); + } else { + if (['next', 'prev', 'playpause'].indexOf(command) != -1) { + var id = audioPlayer.id || pl.start || getPlayFirstId(); + if (id) { + playId(id); + } + } + } + } + } else { + evaluateCommand(command); + } + } + + function setTrackId(id, _status) { + _status = _status || (audioPlayer.player.paused() ? 'pause' : 'play'); + clearTimeout(_setTrackIdTimeout); + + var check = function() { + if (audioPlayer.player) { + sendOperateTrack(id, _status); + } else { + _setTrackIdTimeout = setTimeout(check, 200); + } + }; + check(); + } + + function sendOperateTrack(id, _status) { + log('[sendOperateTrack]', id, _status); + toApp('operateTrack', { + 'id': id, + 'status': _status, + 'playlistId': _lastPlaylistId + }); + } + + function setToOperateQueue(id, _status) { + var q = _operateQueue; + for (var i = 0; i < q.length; i++) { + var track = q[i]; + if (track[0] == id) { + track[1] = _status; + return; + } + } + q.push([id, _status]); + } + + function existsInCurrentPlaylist(id) { + return _lastPlaylistSummary && _lastPlaylistSummary.indexOf(id) != -1; + } + + function processOperateQueue(pl) { + log('[processOperateQueue]'); + var q = _operateQueue; + while (q.length) { + var track = q.shift(); + log('[processOperateQueue] track:', track[0]); + if (pl[track[0]] !== undefined) { + log('[processOperateQueue] track', track[0], 'found, send it now'); + sendOperateTrack(track[0], track[1]); + } + } + } + + function clearOperateQueue() { + _operateQueue = []; + } + + function printPlaylist() { + var pl = padAudioPlaylist(); + if (pl) { + for (var k in pl) { + log(pl[k][5] + ' - ' + pl[k][6]); + } + } + } + + function getPlaylist(_pl) { + _pl = _pl || padAudioPlaylist(); + var pl = null; + if (_pl) { + var start = _pl.start, pl = []; + var nextId = start; + do { + if (_pl[nextId]) { + _pl[nextId]._vkpcId = nextId; + pl.push(_pl[nextId]); + nextId = _pl[nextId]._next; + } + } while (nextId != '' && nextId !== undefined && nextId != start); + } + return pl; + } + + // force=true is used when plids not match + function updatePlaylist(pl, force) { + var tracks = [], summary = [], title; + if (pl) { + for (var k = 0; k < pl.length; k++) { + tracks.push({ + id: pl[k]._vkpcId, + artist: decodeEntities(pl[k][5]), + title: decodeEntities(pl[k][6]), + duration: pl[k][4] + }); + summary.push(pl[k]._vkpcId); + } + + summary = summary.join(';'); + + log("updatePlaylist: _lastPlaylistSummary:", _lastPlaylistSummary, 'summary:', summary); + if (force || _lastPlaylistSummary === null || _lastPlaylistSummary !== summary) { + log('[updatePlaylist] last summary not matched;', _lastPlaylistSummary, summary); + var activeId = '', activeStatus = ''; + var vkpl = padAudioPlaylist(); + var plTitle = (window.audioPlaylist && window.audioPlaylist.htitle) || vkpl.htitle; + if (audioPlayer.id && vkpl[audioPlayer.id] !== undefined) { + activeId = audioPlayer.id; + _watchGraphicsChange = true; + + activeStatus = getPlayerStatus(true) ? 'play' : 'pause'; + _watchGraphicsChange = true; + } + + _lastPlaylistSummary = summary; + _lastPlaylistId = random(100000, 1000000); + + log("[updatePlaylist] send pl with id="+_lastPlaylistId+', activeId='+activeId+', activeStatus='+activeStatus+' to app'); + try { + toApp('updatePlaylist', { + tracks: tracks, + title: parsePlaylistTitle(plTitle) || "", + id: _lastPlaylistId, + active: { 'id': activeId, 'status': activeStatus }, + browser: getBrowser() + }); + } catch(e) { + log('[updatePlaylist] exception:', e, e.stack); + } + + processOperateQueue(pl); + } + } + } + + function clearPlaylist(no_send, called_from) { + called_from = called_from || ""; + log('[clearPlaylist] (called from: '+called_from+')'); + + _lastPlaylistSummary = null; + _lastPlaylistId = 0; + if (!no_send) { + toApp('clearPlaylist', {}); + } + } + + function checkPlaylist() { + var pl = padAudioPlaylist(); + if (!pl) { + clearPlaylist(true, 'checkPlaylist'); + } + } + + function parsePlaylistTitle(str) { + str = str || ""; + str = trim(str); + if (str == '') return str; + + var starts = { + 0: 'Сейчас играет — ', // ru + 100: 'Нынче играетъ— ', // re + 3: 'Now playing — ', // en + 1: 'Зараз звучить — ', // ua + 777: 'Проигрывается пластинка «' // su + }; + var ends = { + 0: ' \\| [0-9]+ аудиоза[^\\s]+$', + 3: ' \\| [0-9]+ audio [^\\s]+$', + 1: ' \\| [0-9]+ аудіоза[^\\s]+$', + 100: ' \\| [0-9]+ композ[^\\s]+$', + 777: ' \\| [0-9]+ грамза[^\\s]+»$' + }; + + if (window.vk && vk.lang !== undefined) { + if (starts[vk.lang] !== undefined && startsWith(str, starts[vk.lang])) { + str = str.substring(starts[vk.lang].length); + } + + if (ends[vk.lang] !== undefined) { + var regex = new RegExp(ends[vk.lang], 'i'); + if (str.match(regex)) str = str.replace(regex, ''); + } + } + + return stripTags(trim(str)); + } + + function afterInjection() { + log("after injection"); + var pl = getPlaylist(); + if (pl) updatePlaylist(pl); + } + + function next() { + audioPlayer.nextTrack(true, !window.audioPlaylist) + /*if (audioPlayer.controls && audioPlayer.controls.pd && audioPlayer.controls.pd.next) { + audioPlayer.controls.pd.next.click(); + } else { + audioPlayer.nextTrack(true, !window.audioPlaylist) + }*/ + } + + function prev() { + audioPlayer.prevTrack(true, !window.audioPlaylist); + /*if (audioPlayer.controls && audioPlayer.controls.pd && audioPlayer.controls.pd.prev) { + audioPlayer.controls.pd.prev.click(); + } else { + audioPlayer.prevTrack(true, !window.audioPlaylist); + }*/ + } + + function getPlayFirstId() { + var id = currentAudioId() || ls.get('audio_id') || (window.audioPlaylist && audioPlaylist.start); + return id || null; + } + + function playFirst() { + var id = getPlayFirstId(); + + if (id) playId(id); + else { + var plist = padAudioPlaylist(); + if (plist && plist.start) { + playId(plist.start); + } else { + executeAfterPadLoading(function() { + var plist = padAudioPlaylist(); + if (plist && plist.start) { + playId(plist.start); + } + }); + } + } + } + + function executeAfterPadLoading(f) { + Pads.show('mus'); + window.onPlaylistLoaded = function() { + if (f) { + try { + f(); + } catch(e) {} + } + setTimeout(function() { + Pads.show('mus'); + }, 10); + } + } + + function getPlayerStatus(justStarted) { + if (!audioPlayer.player) return false; + try { + var pl = audioPlayer.player; + if (pl && pl.music && pl.music.buffered && !pl.music.buffered.length && justStarted) return true; + } catch (e) { + return true; + } + + return audioPlayer.player && !audioPlayer.player.paused(); + } + + function pauseForSafari() { + if (window.audioPlayer && audioPlayer.player) audioPlayer.pauseTrack(); + } + + function playPause() { + if (window.audioPlayer && audioPlayer.player) { + if (audioPlayer.player.paused()) { + audioPlayer.playTrack(); + } else { + audioPlayer.pauseTrack(); + } + } + } + + function operateTrack(id) { + if (id == audioPlayer.id) { + playPause(); + } else { + audioPlayer.operate(id); + } + } + + function playId(id) { + if (window.audioPlayer) audioPlayer.operate(id); + else playAudioNew(id); + } + + function getLastInstanceId() { + var id = null, pp = ls.get('pad_playlist'); + if (pp && pp.source) id = pp.source; + return id; + } + + this.executeCommand = executeCommand; + + this.getParams = function() { + if (window.__vkpc_data && window.__vkpc_data.command != 'afterInjection') { + checkPlaylist(); + } + var havePlayer = window.audioPlayer !== undefined; + var havePlaylist = havePlayer && (window.padAudioPlaylist && !!padAudioPlaylist()); + + return { + havePlayer: havePlayer, + havePlaylist: havePlaylist, + isPlaying: window.audioPlayer && window.audioPlayer.player && !window.audioPlayer.player.paused(), + tabId: window.curNotifier && curNotifier.instance_id, + trackId: window.audioPlayer && audioPlayer.id, + playlistId: havePlaylist ? _lastPlaylistId : 0, + lsSourceId: getLastInstanceId() + }; + }; + + this.init = function(sid) { + if (_checkPlaylistTimer === null) { + _checkPlaylistTimer = setInterval(function() { + if ((_lastPlaylistId || _lastPlaylistSummary) && !padAudioPlaylist()) { + clearPlaylist(true, 'timer'); // TODO func + } + }, 1000); + } + + if (!window.__wrappedByVKPC && window.audioPlayer && window.ls && window.stManager) { + if (!stManager.__done) { + stManager.__done = stManager.done; + stManager.done = function(fn) { + if (fn == 'audioplayer.js') { + wrapAudioMethods(); // TODO func + } + stManager.__done.apply(stManager, arguments); + }; + } + + wrapAudioMethods(); + + if (!ls.__set) { + ls.__set = ls.set; + ls.set = function(k, v) { + ls.__set.apply(ls, arguments); + if (k == 'pad_playlist') { + log('pad_playlist updated:', v); + updatePlaylist(getPlaylist(v)); // TODO func + } + }; + } + if (!ls.__remove) { + ls.__remove = ls.remove; + ls.remove = function(k, v) { + ls.__remove.apply(ls, arguments); + if (k == 'pad_playlist') { + log('pad_playlist removed from ls'); + //self.clearPlaylist(true, 'ls.remove'); + // self.clearPlaylist(); + } + }; + } + + window.__wrappedByVKPC = true; + } + + if (sid === _sid) { + return; + } + if (_sid !== null) { + clear(); // TODO + } + _sid = sid; + + log('(re)inited OK'); + }; + + this.getSID = function() { + return _sid; + }; + + this.getLastPlaylistID = function() { + return _lastPlaylistId; + }; + + this.getLastInstanceId = getLastInstanceId; + this.clearPlaylist = clearPlaylist; +}; // window.VKPC = ... + +} // if (!window.VKPC) ... + +if (!window.DOMContentLoaded) { + window.console && console.log && console.log("[VKPC] !window.DOMContentLoaded, exising"); + return; +} + +window.DOMContentLoaded(function() { + VKPC.init(vkpc_sid); +}); + +// afterInjection + +chrome.runtime.sendMessage(window.__vkpc_data.extid, { + cmd: "injection_result", + id: parseInt(window.__vkpc_data.injid, 10), + data: VKPC.getParams() +}); + +})(window.__vkpc_data.sid); + +delete window.__vkpc_data; diff --git a/screenshots/dark.png b/screenshots/dark.png Binary files differnew file mode 100644 index 0000000..84897b6 --- /dev/null +++ b/screenshots/dark.png diff --git a/screenshots/dark_p.png b/screenshots/dark_p.png Binary files differnew file mode 100644 index 0000000..ec54c03 --- /dev/null +++ b/screenshots/dark_p.png diff --git a/screenshots/light.png b/screenshots/light.png Binary files differnew file mode 100644 index 0000000..d6ec284 --- /dev/null +++ b/screenshots/light.png diff --git a/screenshots/light_p.png b/screenshots/light_p.png Binary files differnew file mode 100644 index 0000000..6d2e326 --- /dev/null +++ b/screenshots/light_p.png diff --git a/screenshots/notification b/screenshots/notification Binary files differnew file mode 100644 index 0000000..6760983 --- /dev/null +++ b/screenshots/notification diff --git a/screenshots/notification_p.png b/screenshots/notification_p.png Binary files differnew file mode 100644 index 0000000..d937da7 --- /dev/null +++ b/screenshots/notification_p.png |