Archive Project
15
Notifications for YouTube/ItemCellView.h
Executable file
@@ -0,0 +1,15 @@
|
||||
//
|
||||
// ItemCellView.h
|
||||
// LionTableViewTesting
|
||||
//
|
||||
// Created by Tomaž Kragelj on 8/29/11.
|
||||
// Copyright 2011 __MyCompanyName__. All rights reserved.
|
||||
//
|
||||
|
||||
/* A table cell view that supports a detail text field. This is useful because by default no label but the default text field gets highlighted when the cell gets selected. The item cell view also supports a second text field that toggles between [NSColor windowBackgroundColor] and [NSColor controlShadowColor] for the default and selected state respectively.
|
||||
*/
|
||||
@interface ItemCellView : NSTableCellView
|
||||
|
||||
@property (nonatomic) IBOutlet NSTextField *detailTextField;
|
||||
|
||||
@end
|
||||
21
Notifications for YouTube/ItemCellView.m
Executable file
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// ItemCellView.m
|
||||
// LionTableViewTesting
|
||||
//
|
||||
// Created by Tomaž Kragelj on 8/29/11.
|
||||
// Copyright 2011 __MyCompanyName__. All rights reserved.
|
||||
//
|
||||
|
||||
#import "ItemCellView.h"
|
||||
|
||||
@implementation ItemCellView
|
||||
|
||||
@synthesize detailTextField = _detailTextField;
|
||||
|
||||
- (void)setBackgroundStyle:(NSBackgroundStyle)backgroundStyle {
|
||||
NSColor *textColor = (backgroundStyle == NSBackgroundStyleDark) ? [NSColor windowBackgroundColor] : [NSColor controlShadowColor];
|
||||
self.detailTextField.textColor = textColor;
|
||||
[super setBackgroundStyle:backgroundStyle];
|
||||
}
|
||||
|
||||
@end
|
||||
54
Notifications for YouTube/NYTAppDelegate.h
Normal file → Executable file
@@ -6,10 +6,58 @@
|
||||
// Copyright (c) 2013 Kim Wittenburg. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
@interface NYTAppDelegate : NSObject <NSApplicationDelegate, NSUserNotificationCenterDelegate>
|
||||
|
||||
@interface NYTAppDelegate : NSObject <NSApplicationDelegate>
|
||||
@property (unsafe_unretained) IBOutlet NSWindow *window;
|
||||
|
||||
@property (assign) IBOutlet NSWindow *window;
|
||||
#pragma mark *** Handling the Status Item ***
|
||||
|
||||
/* The menu attached to the app's status item. There should be very few reason to change the menu's contents.
|
||||
*/
|
||||
@property (weak) IBOutlet NSMenu *statusMenu;
|
||||
|
||||
/* This property holds the app's status item if any. Following this definition this property will be nil if there is currently no status item.
|
||||
*/
|
||||
@property (readonly, strong) NSStatusItem *statusItem;
|
||||
|
||||
/* Use these methods to set and query the state of the app's status item in a secure and safe way.
|
||||
*/
|
||||
- (void)setStatusItemVisible:(BOOL)visible;
|
||||
- (BOOL)isStatusItemVisible;
|
||||
|
||||
#pragma mark *** Handling Login ***
|
||||
|
||||
/* This property returns YES as long as there is currently a login process running. You may want to observe this property to get notified when the login process finished. This is important because there should be no notifications delivered when the user is about to change.
|
||||
This property gives no information wether the login process sucessfully finished or was canceled by the user or if an error occured. If you want further information than wether the logging in process is running you must query NYTUpdateManager.
|
||||
*/
|
||||
@property (readonly) BOOL loggingIn;
|
||||
|
||||
/* This method is triggered from the user interface. It begins a modal sheet that gives the user the possibility to login and thus permit Notifications for YouTube to access the account data. This method does not try to access the user's keychain to query any existing login data. This is handled during applicationDidFinishLaunching:
|
||||
*/
|
||||
- (IBAction)login:(id)sender;
|
||||
|
||||
#pragma mark *** Handling Refreshing ***
|
||||
|
||||
/* Triggers a refresh from the user interface.
|
||||
ATTENTION: If you attempt to invoke this method on your own note that the sender's window will be used to present errors occured during the update (such as network unavailability). If the sender's window is nil or the sender does not respond to a window property the alerts are displayed application modal. If you do not want any errors to be displayed automatically use NYTUpdateManager.
|
||||
*/
|
||||
- (IBAction)performRefresh:(id)sender;
|
||||
|
||||
#pragma mark *** Configure Notifications ***
|
||||
|
||||
/* Begins a sheet that allows the user to edit the rules which notifications should be shown. This method is triggered from the user interface.
|
||||
*/
|
||||
- (IBAction)editRules:(id)sender;
|
||||
|
||||
#pragma mark *** Other Actions ***
|
||||
|
||||
/* Show application windows. These methods also make Notifications for YouTube the foremost application.
|
||||
*/
|
||||
- (IBAction)showPreferenceWindow:(id)sender;
|
||||
- (IBAction)showAboutPanel:(id)sender;
|
||||
|
||||
/* Opens the YouTube website.
|
||||
*/
|
||||
- (IBAction)browseYouTube:(id)sender;
|
||||
|
||||
@end
|
||||
|
||||
256
Notifications for YouTube/NYTAppDelegate.m
Normal file → Executable file
@@ -8,11 +8,263 @@
|
||||
|
||||
#import "NYTAppDelegate.h"
|
||||
|
||||
@implementation NYTAppDelegate
|
||||
// Core Imports
|
||||
#import "NYTAuthentication.h"
|
||||
#import "NYTUpdateManager.h"
|
||||
|
||||
// Other Imports
|
||||
#import "NumberValueTransformer.h"
|
||||
#import "NYTRulesWindowController.h"
|
||||
|
||||
@interface NYTAppDelegate ()
|
||||
|
||||
// Outlets etc.
|
||||
@property (weak) IBOutlet NSButton *loginButton;
|
||||
@property (readwrite, strong) NSStatusItem *statusItem;
|
||||
|
||||
// Login
|
||||
@property (readwrite) BOOL loggingIn;
|
||||
|
||||
@end
|
||||
|
||||
// Notifications used for KVO
|
||||
static void* const NYTLoginChangedNotification = @"NYTLoginChangedNotification";
|
||||
static void* const NYTUpdateManagerPropertyDidChangeNotification = @"NYTUpdateManagerPropertyDidChangeNotification";
|
||||
|
||||
@implementation NYTAppDelegate {
|
||||
NYTRulesWindowController *_rulesController;
|
||||
}
|
||||
|
||||
+ (void)initialize
|
||||
{
|
||||
[super initialize];
|
||||
// Register our value transformers
|
||||
[NSValueTransformer setValueTransformer:[NumberValueTransformer new] forName:@"NumberValueTransformer"];
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
// Make sure this object gets removed from any observed object
|
||||
NYTUpdateManager *manager = [NYTUpdateManager sharedManager];
|
||||
[manager removeObserver:self forKeyPath:@"autoRefreshEnabled"];
|
||||
[manager removeObserver:self forKeyPath:@"autoRefreshInterval"];
|
||||
[manager removeObserver:self forKeyPath:@"coalescesNotifications"];
|
||||
[manager removeObserver:self forKeyPath:@"maximumVideoAge"];
|
||||
[[NYTAuthentication sharedAuthentication] removeObserver:self forKeyPath:@"isLoggedIn"];
|
||||
}
|
||||
|
||||
#pragma mark - Application Delegate
|
||||
|
||||
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
|
||||
{
|
||||
// Insert code here to initialize your application
|
||||
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
||||
NYTUpdateManager *manager = [NYTUpdateManager sharedManager];
|
||||
NYTAuthentication *auth = [NYTAuthentication sharedAuthentication];
|
||||
|
||||
// Configure the User Defaults
|
||||
NSDictionary *userDefaults = @{@"autoRefreshEnabled": @YES, @"autoRefreshInterval": @600, @"coalescesNotifications": @YES, @"maximumVideoAge": @3600};
|
||||
[defaults registerDefaults:userDefaults];
|
||||
|
||||
// Configure Notification Centers
|
||||
[NSUserNotificationCenter defaultUserNotificationCenter].delegate = self;
|
||||
// Notification Startup
|
||||
NSUserNotification *startNotification = aNotification.userInfo[NSApplicationLaunchUserNotificationKey];
|
||||
if (startNotification) {
|
||||
[self userNotificationCenter:[NSUserNotificationCenter defaultUserNotificationCenter] didActivateNotification:startNotification];
|
||||
}
|
||||
|
||||
// Configure the status item
|
||||
self.statusItemVisible = YES;
|
||||
|
||||
// Configure the login (and log in). Do so before configuring notifications or auto refresh because this can take a while
|
||||
[auth addObserver:self forKeyPath:@"isLoggedIn" options:0 context:NYTLoginChangedNotification];
|
||||
self.loggingIn = YES;
|
||||
[auth tryLoginFromKeychainWithCompletionHandler:^{
|
||||
self.loggingIn = NO;
|
||||
// If there is no login in the keychain show the preferences
|
||||
if (!auth.isLoggedIn) {
|
||||
[[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
|
||||
[self.window makeKeyAndOrderFront:self];
|
||||
}
|
||||
}];
|
||||
|
||||
// Configure the update manager
|
||||
manager.maximumVideoAge = [defaults integerForKey:@"maximumVideoAge"];
|
||||
manager.coalescesNotifications = [defaults boolForKey:@"coalescesNotifications"];
|
||||
manager.autoRefreshInterval = [defaults integerForKey:@"autoRefreshInterval"];
|
||||
[manager addObserver:self forKeyPath:@"maximumVideoAge" options:NSKeyValueObservingOptionNew context:NYTUpdateManagerPropertyDidChangeNotification];
|
||||
[manager addObserver:self forKeyPath:@"coalescesNotifications" options:NSKeyValueObservingOptionNew context:NYTUpdateManagerPropertyDidChangeNotification];
|
||||
[manager addObserver:self forKeyPath:@"autoRefreshInterval" options:NSKeyValueObservingOptionNew context:NYTUpdateManagerPropertyDidChangeNotification];
|
||||
|
||||
// Configure (Auto) Refresh
|
||||
manager.autoRefreshEnabled = [defaults boolForKey:@"autoRefreshEnabled"];
|
||||
[manager addObserver:self forKeyPath:@"autoRefreshEnabled" options:NSKeyValueObservingOptionNew context:NYTUpdateManagerPropertyDidChangeNotification];
|
||||
}
|
||||
|
||||
- (BOOL)applicationShouldHandleReopen:(NSApplication *)sender hasVisibleWindows:(BOOL)flag
|
||||
{
|
||||
// Open the preferences on reopen (if no other window is visible)
|
||||
if (!flag) {
|
||||
[self.window makeKeyAndOrderFront:nil];
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)applicationWillTerminate:(NSNotification *)notification
|
||||
{
|
||||
[[NSUserDefaults standardUserDefaults] synchronize];
|
||||
}
|
||||
|
||||
#pragma mark - User Notification Center Delegate
|
||||
|
||||
- (void)userNotificationCenter:(NSUserNotificationCenter *)center didActivateNotification:(NSUserNotification *)notification
|
||||
{
|
||||
// Show the URL
|
||||
NSString *videoURL = notification.userInfo[NYTUserNotificationURLKey];
|
||||
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:videoURL]];
|
||||
// Remove the notification
|
||||
[center removeDeliveredNotification:notification];
|
||||
}
|
||||
|
||||
- (BOOL)userNotificationCenter:(NSUserNotificationCenter *)center shouldPresentNotification:(NSUserNotification *)notification
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark *** Handling the Status Item ***
|
||||
|
||||
- (void)setStatusItemVisible:(BOOL)visible
|
||||
{
|
||||
if (visible) {
|
||||
self.statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSVariableStatusItemLength];
|
||||
//NSMutableAttributedString *title = [[NSMutableAttributedString alloc] initWithString:@"(4)"];
|
||||
//[title addAttribute:NSStrokeWidthAttributeName value:@-5.0 range:NSMakeRange/*(3, 4)*/(1, 1)];
|
||||
//self.statusItem.attributedTitle = title;
|
||||
self.statusItem.image = [NSImage imageNamed:@"play-icon-bw"];
|
||||
self.statusItem.alternateImage = [NSImage imageNamed:@"play-icon-s"];
|
||||
self.statusItem.menu = self.statusMenu;
|
||||
self.statusItem.highlightMode = YES;
|
||||
} else {
|
||||
[[NSStatusBar systemStatusBar] removeStatusItem:self.statusItem];
|
||||
self.statusItem = nil;
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)isStatusItemVisible
|
||||
{
|
||||
return self.statusItem != nil;
|
||||
}
|
||||
|
||||
#pragma mark *** Handling Login ***
|
||||
|
||||
- (IBAction)login:(id)sender {
|
||||
if ([NYTAuthentication sharedAuthentication].isLoggedIn) {
|
||||
// Logout: Present a sheet giving the following options: Logout, Cancel, Switch user
|
||||
NSString *title = NSLocalizedString(@"Do you want to log out or switch to another user?", nil);
|
||||
NSString *switchUser = NSLocalizedString(@"Switch User…", nil);
|
||||
NSString *logout = NSLocalizedString(@"Logout", nil);
|
||||
NSString *cancel = NSLocalizedString(@"Cancel", nil);
|
||||
NSString *msg = NSLocalizedString(@"If you logout the login stored in your keychain will be removed. You would have to log in agin the next time you want to use Notifications for YouTube.", nil);
|
||||
NSBeginAlertSheet(title, switchUser, logout, cancel, self.window, self, NULL, @selector(sheetDidDismiss:returnCode:contextInfo:), NULL, @"%@", msg);
|
||||
} else {
|
||||
[self doLogin];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)sheetDidDismiss:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo
|
||||
{
|
||||
if (returnCode == NSAlertDefaultReturn) {
|
||||
// Switch User
|
||||
[self doLogin];
|
||||
} else if (returnCode == NSAlertAlternateReturn) {
|
||||
// Logout: Redirect to Update Manager
|
||||
[[NYTAuthentication sharedAuthentication] discardLogin];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)doLogin
|
||||
{
|
||||
// Login: Redirect to Update Manager
|
||||
self.loggingIn = YES;
|
||||
[[NYTAuthentication sharedAuthentication] beginLoginSheetForWindow:self.window completionHandler:^(NSError *error) {
|
||||
if (error) {
|
||||
// Show the error as sheet if possible
|
||||
if (self.window.isVisible) {
|
||||
[NSApp presentError:error modalForWindow:self.window delegate:nil didPresentSelector:NULL contextInfo:NULL];
|
||||
} else {
|
||||
[NSApp presentError:error];
|
||||
}
|
||||
}
|
||||
self.loggingIn = NO;
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma mark *** Handling Refreshing ***
|
||||
|
||||
- (IBAction)performRefresh:(id)sender {
|
||||
// Refresh with notifications
|
||||
[[NYTUpdateManager sharedManager] refreshFeedsWithErrorHandler:^(NSError *error) {
|
||||
// Show the error if the refresh was triggered manually
|
||||
// Possibly show it in a modal sheet otherwise a modal window
|
||||
if (error) {
|
||||
if ([sender respondsToSelector:@selector(window)] && [sender window].isVisible) {
|
||||
NSAlert *alert = [NSAlert alertWithError:error];
|
||||
[alert beginSheetModalForWindow:[sender window] modalDelegate:nil didEndSelector:NULL contextInfo:NULL];
|
||||
} else {
|
||||
[NSApp presentError:error];
|
||||
}
|
||||
}
|
||||
} notify:YES];
|
||||
}
|
||||
|
||||
#pragma mark *** Configure Notifications ***
|
||||
|
||||
- (IBAction)editRules:(id)sender {
|
||||
_rulesController = [[NYTRulesWindowController alloc] init];
|
||||
[NSApp beginSheet:_rulesController.window modalForWindow:self.window modalDelegate:self didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:) contextInfo:NULL];
|
||||
}
|
||||
|
||||
- (void)sheetDidEnd:(NSWindow *)sheet returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo
|
||||
{
|
||||
// For efficiency reasons
|
||||
_rulesController = nil;
|
||||
}
|
||||
|
||||
#pragma mark *** Other Actions ***
|
||||
|
||||
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
|
||||
{
|
||||
if (context == NYTUpdateManagerPropertyDidChangeNotification) {
|
||||
id value = change[NSKeyValueChangeNewKey];
|
||||
[[NSUserDefaults standardUserDefaults] setObject:value forKey:keyPath];
|
||||
}else if (context == NYTLoginChangedNotification) {
|
||||
// Manage the title of the login/logout button based on wether login information is present (using KVO)
|
||||
if ([NYTAuthentication sharedAuthentication].isLoggedIn) {
|
||||
self.loginButton.title = NSLocalizedString(@"Logout…", nil);
|
||||
} else {
|
||||
self.loginButton.title = NSLocalizedString(@"Login…", nil);
|
||||
}
|
||||
} else {
|
||||
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
|
||||
}
|
||||
}
|
||||
|
||||
- (IBAction)showPreferenceWindow:(id)sender
|
||||
{
|
||||
// Activate the app first
|
||||
[[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
|
||||
[self.window makeKeyAndOrderFront:sender];
|
||||
[self.window makeMainWindow];
|
||||
}
|
||||
|
||||
- (IBAction)showAboutPanel:(id)sender {
|
||||
[NSApp activateIgnoringOtherApps:YES];
|
||||
[NSApp orderFrontStandardAboutPanel:sender];
|
||||
}
|
||||
|
||||
- (IBAction)browseYouTube:(id)sender {
|
||||
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://www.youtube.com/"]];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
60
Notifications for YouTube/NYTAuthentication.h
Executable file
@@ -0,0 +1,60 @@
|
||||
//
|
||||
// NYTAuthentication.h
|
||||
// Notifications for YouTube
|
||||
//
|
||||
// Created by Kim Wittenburg on 21.07.13.
|
||||
// Copyright (c) 2013 Kim Wittenburg. All rights reserved.
|
||||
//
|
||||
|
||||
@class GTMOAuth2Authentication, NYTUser;
|
||||
|
||||
extern NSString * const DeveloperKey;
|
||||
extern NSString * const DeveloperKeyHTTPHeaderField;
|
||||
extern NSString * const ClientID;
|
||||
extern NSString * const ClientSecret;
|
||||
extern NSString * const KeychainItemName;
|
||||
|
||||
/* Authentication manages (as the name suggests) authentication. For any operation (login, logout, ...) you should prompt this class. There are also KVO compilant properties that allow you to observe the login and the logged in user.
|
||||
*/
|
||||
@interface NYTAuthentication : NSObject
|
||||
|
||||
#pragma mark *** Initialization ***
|
||||
|
||||
/* There is only one instance of this class at runtime. After the first instance was created with any of the initializing methods any further calls to any of them will just return the already created instance.
|
||||
*/
|
||||
+ (NYTAuthentication *)sharedAuthentication;
|
||||
- (id)init;
|
||||
|
||||
#pragma mark *** Login ***
|
||||
|
||||
/* Trys to get all information possible from the keychain item. If any error occurs or there is no keychain item this method does nothing. This method returns immediately.
|
||||
*/
|
||||
- (void)tryLoginFromKeychainWithCompletionHandler:(void(^)(void))handler;
|
||||
|
||||
/* Presents a modal sheet for the given window prompting the user to log in to permit Notifications for YouTube the access to his subscriptions. If the login process fails the completion handler will contain an appropriate error object.
|
||||
*/
|
||||
- (void)beginLoginSheetForWindow:(NSWindow *)window completionHandler:(void (^)(NSError *))handler;
|
||||
|
||||
#pragma mark *** Logout ***
|
||||
|
||||
/* Discards the current login information and thus logs the user out. This method makes sure that any information of the currently logged in user is discarded (as the keychain entry). Use this as a log out method. You do not have to log out before you log in with a new user.
|
||||
*/
|
||||
- (void)discardLogin;
|
||||
|
||||
#pragma mark *** Authentication and Login Information ***
|
||||
|
||||
/* The actual authentication that backs the NYTAuthentication. May be nil if no user is currently logged in.
|
||||
*/
|
||||
@property (readonly) GTMOAuth2Authentication *authentication;
|
||||
|
||||
/* Returns YES if there is currently a user logged in. Always prefer this property over a nil check to loggedInUser because the loggedInUser property may not be updated properly at the time you query this property.
|
||||
If this property is YES the loggedInUser property is set to the user that is currently logged in.
|
||||
You may want to observe this property to get informed when the user logs in and out.
|
||||
*/
|
||||
@property (readonly) BOOL isLoggedIn;
|
||||
|
||||
/* Returns the currently logged in user or nil if there is none. This property can be observed to get notified when the user changes.
|
||||
*/
|
||||
@property (readonly) NYTUser *loggedInUser;
|
||||
|
||||
@end
|
||||
172
Notifications for YouTube/NYTAuthentication.m
Executable file
@@ -0,0 +1,172 @@
|
||||
//
|
||||
// NYTAuthentication.m
|
||||
// Notifications for YouTube
|
||||
//
|
||||
// Created by Kim Wittenburg on 21.07.13.
|
||||
// Copyright (c) 2013 Kim Wittenburg. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NYTAuthentication.h"
|
||||
|
||||
// API Imports
|
||||
#import "OAuth2.h"
|
||||
|
||||
// Core Imports
|
||||
#import "NYTUser.h"
|
||||
#import "NYTUtil.h"
|
||||
#import "NYTUpdateManager.h"
|
||||
|
||||
// Google Access Constants
|
||||
NSString * const DeveloperKey = @"key=AI39si6ubNmTeFzFTFz27hFn6r-51knz0_CDWvqgyL-nVKBcFWu5C_c46TWQcHTwwK9bOBlrhdlKatyEqjEnJhZd2dMB7UgFhQ";
|
||||
NSString * const DeveloperKeyHTTPHeaderField = @"X-GData-Key";
|
||||
NSString * const ClientID = @"908431532405.apps.googleusercontent.com";
|
||||
NSString * const ClientSecret = @"hKcyoNAzEIuHZaz0pdTUBMBv";
|
||||
NSString * const KeychainItemName = @"Notifications for YouTube";
|
||||
|
||||
@interface NYTAuthentication ()
|
||||
|
||||
@property (readwrite) GTMOAuth2Authentication *authentication;
|
||||
@property (readwrite) NYTUser *loggedInUser;
|
||||
|
||||
@end
|
||||
|
||||
@implementation NYTAuthentication
|
||||
|
||||
#pragma mark *** Initialization ***
|
||||
|
||||
static NYTAuthentication *sharedAuthentication = nil;
|
||||
|
||||
+ (NYTAuthentication *)sharedAuthentication
|
||||
{
|
||||
if (!sharedAuthentication) {
|
||||
sharedAuthentication = [[self alloc] init];
|
||||
}
|
||||
return sharedAuthentication;
|
||||
}
|
||||
|
||||
- (id)init
|
||||
{
|
||||
// Allow exactly one instance at all (for any way of initialization)
|
||||
if (sharedAuthentication) {
|
||||
return sharedAuthentication;
|
||||
}
|
||||
self = [super init];
|
||||
if (self) {
|
||||
sharedAuthentication = self;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark *** KVO/KVC Additions to Properties ***
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingIsLoggedIn
|
||||
{
|
||||
return [NSSet setWithObjects:@"authentication", nil];
|
||||
}
|
||||
|
||||
#pragma mark *** Login ***
|
||||
|
||||
- (void)tryLoginFromKeychainWithCompletionHandler:(void(^)(void))handler
|
||||
{
|
||||
// Get keychain data
|
||||
GTMOAuth2Authentication *auth = [GTMOAuth2WindowController authForGoogleFromKeychainForName:KeychainItemName clientID:ClientID clientSecret:ClientSecret];
|
||||
if (auth.canAuthorize) {
|
||||
// Possible success (keychain)
|
||||
// Login (to refresh tokens/validate values) in background
|
||||
dispatch_queue_t prevQueue = dispatch_get_current_queue();
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
// Try logging in
|
||||
NSError *error = [self completeLoginWithAuthentication:auth];
|
||||
dispatch_async(prevQueue, ^{
|
||||
if (!error) {
|
||||
// We suceeded so update the authentication object properly
|
||||
self.authentication = auth;
|
||||
// Refresh the feeds without notifications to set the 'known' state
|
||||
[[NYTUpdateManager sharedManager] refreshFeedsWithErrorHandler:NULL notify:NO];
|
||||
}
|
||||
if (handler) {
|
||||
handler();
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Failed (keychain)
|
||||
if (handler) {
|
||||
handler();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)beginLoginSheetForWindow:(NSWindow *)window completionHandler:(void (^)(NSError *))handler
|
||||
{
|
||||
[[NYTUpdateManager sharedManager] pauseAutoRefreshing];
|
||||
|
||||
// Show the login window
|
||||
GTMOAuth2WindowController *windowController = [[GTMOAuth2WindowController alloc] initWithScope:@"https://gdata.youtube.com" clientID:ClientID clientSecret:ClientSecret keychainItemName:KeychainItemName resourceBundle:nil];
|
||||
[windowController signInSheetModalForWindow:window completionHandler:^(GTMOAuth2Authentication *auth, NSError *error) {
|
||||
if (error) {
|
||||
// If an error occured just return
|
||||
if (handler) {
|
||||
handler(error);
|
||||
}
|
||||
[[NYTUpdateManager sharedManager] resumeAutoRefreshing];
|
||||
} else {
|
||||
// Complete the login asynchronously (because we are very likely currently operating on the main thread)
|
||||
dispatch_queue_t prevQueue = dispatch_get_current_queue();
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
// Complete the login
|
||||
NSError *error = [self completeLoginWithAuthentication:auth];
|
||||
|
||||
// Handle the error on the caller's queue
|
||||
dispatch_async(prevQueue, ^{
|
||||
if (!error) {
|
||||
self.authentication = auth;
|
||||
[[NYTUpdateManager sharedManager] refreshFeedsWithErrorHandler:NULL notify:NO];
|
||||
}
|
||||
if (handler) {
|
||||
handler(error);
|
||||
}
|
||||
[[NYTUpdateManager sharedManager] resumeAutoRefreshing];
|
||||
});
|
||||
});
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (NSError *)completeLoginWithAuthentication:(GTMOAuth2Authentication *)auth
|
||||
{
|
||||
// Get the user's data
|
||||
NSError *error;
|
||||
static NSString *userPageURL = @"https://gdata.youtube.com/feeds/api/users/default";
|
||||
NSXMLDocument *document = LoadXMLDocumentSynchronous(userPageURL, auth, &error);
|
||||
if (!document) {
|
||||
return error;
|
||||
}
|
||||
|
||||
// Parse the data and store them app-friendly
|
||||
NYTUser *user = [[NYTUser alloc] initWithUserProfilePage:document];
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
// Set on the main queue due to bindings relations
|
||||
self.loggedInUser = user;
|
||||
});
|
||||
return nil;
|
||||
}
|
||||
|
||||
#pragma mark *** Logout ***
|
||||
|
||||
- (void)discardLogin
|
||||
{
|
||||
// Also remove from keychain
|
||||
[GTMOAuth2WindowController removeAuthFromKeychainForName:KeychainItemName];
|
||||
self.authentication = nil;
|
||||
self.loggedInUser = nil;
|
||||
}
|
||||
|
||||
#pragma mark *** Login Information ***
|
||||
|
||||
- (BOOL)isLoggedIn
|
||||
{
|
||||
return self.authentication != nil && self.authentication.canAuthorize;
|
||||
}
|
||||
|
||||
@end
|
||||
44
Notifications for YouTube/NYTChannelRestriction.h
Executable file
@@ -0,0 +1,44 @@
|
||||
//
|
||||
// NYTChannelRestrictions.h
|
||||
// Notifications for YouTube
|
||||
//
|
||||
// Created by Kim Wittenburg on 21.07.13.
|
||||
// Copyright (c) 2013 Kim Wittenburg. All rights reserved.
|
||||
//
|
||||
|
||||
@class NYTUser;
|
||||
|
||||
/* A channel restriction contains information about the rules that apply to a specific channel. Like a predicate a restriction eventually evaluates to YES or NO giving information wether a specific video should be included in the notifications or not.
|
||||
You should be aware that encoding a restriction will NOT encode the associated user. Since every channel restriction belongs to one user it is recommended that you store any restrictions as the value for a key that identifies the user (preferably the user id or the channel id).
|
||||
*/
|
||||
@interface NYTChannelRestriction : NSObject <NSCoding>
|
||||
|
||||
/* Initializes a restriction. By default a restriction always evaluates to YES.
|
||||
*/
|
||||
- (id)init;
|
||||
|
||||
/* The user this restriction belongs to. This property is not necessary but it may make things easier if you, say, want to bind restrictions to a user interface.
|
||||
*/
|
||||
@property NYTUser *user;
|
||||
|
||||
/* If YES any other properties are ignored and this restriction just evaluates to NO.
|
||||
*/
|
||||
@property BOOL disableAllNotifications;
|
||||
|
||||
/* Specifies the way of interpretation of the predicate. If this is YES a positive predicate evaluation means that the video should be included in the notifications, if this is NO a positive predicate evaluation means that the video should NOT be included. The default value is YES.
|
||||
*/
|
||||
@property BOOL positivePredicate;
|
||||
|
||||
/* The predicate that is used to evaluate this restriction. The object this predicate eventually should be evaluated with should be an instance of NYTVideo.
|
||||
*/
|
||||
@property NSPredicate *predicate;
|
||||
|
||||
/* A localized string that summarizes this restriction. It can be "Show all", "Restricted" or "Show none".
|
||||
*/
|
||||
- (NSString *)localizedRestrictionSummary;
|
||||
|
||||
/* Returns YES if this predicate evaluates to the same value as a newly created restriction would. You can use this method to efficiently store only those restrictions that are NOT the default.
|
||||
*/
|
||||
- (BOOL)differsFromDefaultValues;
|
||||
|
||||
@end
|
||||
69
Notifications for YouTube/NYTChannelRestriction.m
Executable file
@@ -0,0 +1,69 @@
|
||||
//
|
||||
// NYTChannelRestrictions.m
|
||||
// Notifications for YouTube
|
||||
//
|
||||
// Created by Kim Wittenburg on 21.07.13.
|
||||
// Copyright (c) 2013 Kim Wittenburg. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NYTChannelRestriction.h"
|
||||
|
||||
// Core Imports
|
||||
#import "NYTUtil.h"
|
||||
|
||||
@implementation NYTChannelRestriction
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingLocalizedRestrictionSummary
|
||||
{
|
||||
return [NSSet setWithObjects:@"disableAllNotifications", @"predicate", nil];
|
||||
}
|
||||
|
||||
- (id)init
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
// Default values
|
||||
_positivePredicate = YES;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (id)initWithCoder:(NSCoder *)aDecoder
|
||||
{
|
||||
self = [self init];
|
||||
if (self) {
|
||||
// Do not decode the user
|
||||
_disableAllNotifications = [aDecoder decodeBoolForKey:@"Disable All Notifications"];
|
||||
_predicate = [aDecoder decodeObjectForKey:@"Predicate"];
|
||||
_positivePredicate = [aDecoder decodeBoolForKey:@"Positive Predicate"];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)aCoder
|
||||
{
|
||||
// Do not encode the user
|
||||
[aCoder encodeBool:self.disableAllNotifications forKey:@"Disable All Notifications"];
|
||||
[aCoder encodeObject:self.predicate forKey:@"Predicate"];
|
||||
[aCoder encodeBool:self.positivePredicate forKey:@"Positive Predicate"];
|
||||
}
|
||||
|
||||
- (NSString *)localizedRestrictionSummary
|
||||
{
|
||||
if (self.disableAllNotifications) {
|
||||
return NSLocalizedString(@"No notifications", nil);
|
||||
} else {
|
||||
if (!self.predicate) {
|
||||
return NSLocalizedString(@"Show all notifications", nil);
|
||||
} else {
|
||||
return NSLocalizedString(@"Restrict notifications", nil);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)differsFromDefaultValues
|
||||
{
|
||||
return self.disableAllNotifications == YES || self.predicate != nil;
|
||||
}
|
||||
|
||||
@end
|
||||
32
Notifications for YouTube/NYTRulesWindowController.h
Executable file
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// NYTRulesWindowController.h
|
||||
// Notifications for YouTube
|
||||
//
|
||||
// Created by Kim Wittenburg on 21.07.13.
|
||||
// Copyright (c) 2013 Kim Wittenburg. All rights reserved.
|
||||
//
|
||||
|
||||
/* The rules window controller manages a window that lets the user change rules for notifications.
|
||||
*/
|
||||
@interface NYTRulesWindowController : NSWindowController
|
||||
|
||||
/* This property is YES as long as this controller loads the user's subscriptions. The loading process starts immediately after the window has been loaded.
|
||||
*/
|
||||
@property (readonly) BOOL loading;
|
||||
|
||||
/* Contains the user's subscriptions (after they have been loaded, before that this property is nil). An entry in this array is an instance of NYTChannelRestriction.
|
||||
Both of these arrays are fully KVO and KVC compilant.
|
||||
*/
|
||||
@property (readonly) NSArray *subscriptions;
|
||||
|
||||
/* Convenience actions for the user triggered from the interface: Enable/Disable all and search.
|
||||
*/
|
||||
- (IBAction)enableAllNotifications:(id)sender;
|
||||
- (IBAction)disableAllNotifications:(id)sender;
|
||||
|
||||
/* Finish the editing of rules in either way.
|
||||
*/
|
||||
- (IBAction)save:(id)sender;
|
||||
- (IBAction)cancel:(id)sender;
|
||||
|
||||
@end
|
||||
188
Notifications for YouTube/NYTRulesWindowController.m
Executable file
@@ -0,0 +1,188 @@
|
||||
//
|
||||
// NYTRulesWindowController.m
|
||||
// Notifications for YouTube
|
||||
//
|
||||
// Created by Kim Wittenburg on 21.07.13.
|
||||
// Copyright (c) 2013 Kim Wittenburg. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NYTRulesWindowController.h"
|
||||
|
||||
// Core Imports
|
||||
#import "NYTUser.h"
|
||||
#import "NYTUtil.h"
|
||||
#import "NYTAuthentication.h"
|
||||
|
||||
// Other Imports
|
||||
#import "NYTChannelRestriction.h"
|
||||
|
||||
@interface NYTRulesWindowController ()
|
||||
|
||||
@property (readwrite) BOOL loading;
|
||||
@property (readwrite) NSArray *subscriptions;
|
||||
|
||||
/* The restrictions previously stored in the user defaults
|
||||
*/
|
||||
@property NSDictionary *savedRestrictions;
|
||||
|
||||
@end
|
||||
|
||||
@implementation NYTRulesWindowController
|
||||
|
||||
/* Convenient init
|
||||
*/
|
||||
- (id)init
|
||||
{
|
||||
return [self initWithWindowNibName:@"NYTRulesWindowController" owner:self];
|
||||
}
|
||||
|
||||
- (void)windowDidLoad
|
||||
{
|
||||
[super windowDidLoad];
|
||||
|
||||
// Load the subscriptions
|
||||
[self loadSubscriptions];
|
||||
}
|
||||
|
||||
#pragma mark *** Loading ***
|
||||
|
||||
- (void)loadSubscriptions
|
||||
{
|
||||
self.loading = YES;
|
||||
|
||||
// Load in background to not block the main thread
|
||||
dispatch_queue_t prevQueue = dispatch_get_current_queue();
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
|
||||
// Prepare for multiple pages
|
||||
NSError *error;
|
||||
NSMutableArray *loadedSubscriptions;
|
||||
NSUInteger processedResults = 0;
|
||||
NSUInteger totalResults = 0;
|
||||
|
||||
do {
|
||||
// We are creating a couple of objects so setup a separate autoreleasepool
|
||||
@autoreleasepool {
|
||||
// Load the document
|
||||
NSString *url = [NSString stringWithFormat:@"https://gdata.youtube.com/feeds/api/users/default/subscriptions?max-results=50&start-index=%li&v=2", processedResults+1];
|
||||
NSXMLDocument *document = LoadXMLDocumentSynchronous(url, [NYTAuthentication sharedAuthentication].authentication, &error);
|
||||
|
||||
// Store the entries as app-friendly video objects
|
||||
if (document) {
|
||||
|
||||
// Set up the collection for collecting the videos
|
||||
if (processedResults == 0) {
|
||||
static NSString *const totalResultsPath = @"./feed/openSearch:totalResults";
|
||||
NSXMLElement *totalResultsElement = [document nodesForXPath:totalResultsPath error:nil][0];
|
||||
totalResults = (NSUInteger) totalResultsElement.stringValue.integerValue;
|
||||
loadedSubscriptions = [NSMutableArray arrayWithCapacity:totalResults];
|
||||
}
|
||||
|
||||
// For each page parse its results and add them to the collection
|
||||
static NSString *const resultsPerPagePath = @"./feed/openSearch:itemsPerPage";
|
||||
NSXMLElement *resultsPerPageElement = [document nodesForXPath:resultsPerPagePath error:nil][0];
|
||||
int resultsPerPage = resultsPerPageElement.stringValue.intValue;
|
||||
|
||||
processedResults += resultsPerPage;
|
||||
NSArray *pageSubscriptions = [self subscriptionsOnPage:document error:&error];
|
||||
if (pageSubscriptions) {
|
||||
[loadedSubscriptions addObjectsFromArray:pageSubscriptions];
|
||||
} else {
|
||||
// Abort on error
|
||||
[self abortLoadingOnQueue:prevQueue withError:error];
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Abort on error
|
||||
[self abortLoadingOnQueue:prevQueue withError:error];
|
||||
return;
|
||||
}
|
||||
}
|
||||
} while (processedResults < totalResults);
|
||||
|
||||
// Finish the loading process on the main thread
|
||||
dispatch_async(prevQueue, ^{
|
||||
self.subscriptions = loadedSubscriptions;
|
||||
self.loading = NO;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
- (NSArray *)subscriptionsOnPage:(NSXMLDocument *)document error:(NSError **)error
|
||||
{
|
||||
// Set up the stored restrictions (but just once)
|
||||
if (!self.savedRestrictions) {
|
||||
self.savedRestrictions = [[[NSUserDefaults standardUserDefaults] objectForKey:@"Rules"] mutableCopy];
|
||||
if (!self.savedRestrictions) {
|
||||
self.savedRestrictions = [NSMutableDictionary new];
|
||||
}
|
||||
}
|
||||
|
||||
// For every entry parse a subscription
|
||||
NSArray *entries = [document nodesForXPath:@"./feed/entry" error:nil];
|
||||
NSMutableArray *subscriptions = [NSMutableArray arrayWithCapacity:entries.count];
|
||||
for (NSXMLNode *entry in entries) {
|
||||
NYTUser *user = [[NYTUser alloc] initWithSubscriptionEntry:entry];
|
||||
|
||||
// Have a restriction object available for each subscription. If there is none stored create a default one.
|
||||
NSData *restrictionData = self.savedRestrictions[user.channelID];
|
||||
NYTChannelRestriction *restriction;
|
||||
if (restrictionData) {
|
||||
restriction = [NSKeyedUnarchiver unarchiveObjectWithData:restrictionData];
|
||||
} else {
|
||||
restriction = [[NYTChannelRestriction alloc] init];
|
||||
}
|
||||
restriction.user = user;
|
||||
[subscriptions addObject:restriction];
|
||||
}
|
||||
return subscriptions;
|
||||
}
|
||||
|
||||
- (void)abortLoadingOnQueue:(dispatch_queue_t)queue withError:(NSError *)error
|
||||
{
|
||||
dispatch_async(queue, ^{
|
||||
[NSApp presentError:error modalForWindow:self.window delegate:self didPresentSelector:@selector(didPresentErrorWithRecovery:contextInfo:) contextInfo:NULL];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)didPresentErrorWithRecovery:(BOOL)didRecover contextInfo:(void *)contextInfo
|
||||
{
|
||||
if (!didRecover) {
|
||||
[self cancel:nil];
|
||||
}
|
||||
}
|
||||
|
||||
- (IBAction)enableAllNotifications:(id)sender {
|
||||
// Really enable all (also clearing the predicate)
|
||||
for (NYTChannelRestriction *restriction in self.subscriptions) {
|
||||
restriction.disableAllNotifications = NO;
|
||||
restriction.predicate = nil;
|
||||
}
|
||||
}
|
||||
|
||||
- (IBAction)disableAllNotifications:(id)sender {
|
||||
for (NYTChannelRestriction *restriction in self.subscriptions) {
|
||||
restriction.disableAllNotifications = YES;
|
||||
}
|
||||
}
|
||||
|
||||
- (IBAction)save:(id)sender {
|
||||
// Encode and save the restrictions that are not default values.
|
||||
NSMutableDictionary *restrictsToSave = [NSMutableDictionary new];
|
||||
for (NYTChannelRestriction *restrictions in self.subscriptions) {
|
||||
if (restrictions.differsFromDefaultValues) {
|
||||
restrictsToSave[restrictions.user.channelID] = [NSKeyedArchiver archivedDataWithRootObject:restrictions];
|
||||
}
|
||||
}
|
||||
[[NSUserDefaults standardUserDefaults] setObject:restrictsToSave forKey:@"Rules"];
|
||||
[[NSUserDefaults standardUserDefaults] synchronize];
|
||||
// Hide this window
|
||||
[self cancel:sender];
|
||||
}
|
||||
|
||||
- (IBAction)cancel:(id)sender {
|
||||
// Just hide the window without saving
|
||||
[self.window orderOut:sender];
|
||||
[NSApp endSheet:self.window];
|
||||
}
|
||||
@end
|
||||
84
Notifications for YouTube/NYTUpdateManager.h
Executable file
@@ -0,0 +1,84 @@
|
||||
//
|
||||
// NYTUpdater.h
|
||||
// Notifications for YouTube
|
||||
//
|
||||
// Created by Kim Wittenburg on 09.07.13.
|
||||
// Copyright (c) 2013 Kim Wittenburg. All rights reserved.
|
||||
//
|
||||
|
||||
/* Defines a key in the userInfo dictionary of an NSUserNotification that can be used to receive an URL (in the form of a string) that represents the video the notification was sent for.
|
||||
*/
|
||||
extern NSString * const NYTUserNotificationURLKey;
|
||||
|
||||
@class NYTUser;
|
||||
|
||||
/* The NYTUpdater class is the core class to refresh the upload feeds of the channels defined in the NYTSubscriptionManager singletone class. This class is also responsible for generating appropriate user notifications and presenting them to the user.
|
||||
If you want to create you own subclass of update manager you must make sure that it's instance is created before the default one gets created.
|
||||
*/
|
||||
@interface NYTUpdateManager : NSObject
|
||||
|
||||
#pragma mark *** Obtaining the Shared Instance ***
|
||||
|
||||
/* Use this method to obtain the shared instance. The init message will only be sent once. Any further sharedUpdater or init messages will just return the shared instance.
|
||||
*/
|
||||
+ (NYTUpdateManager *)sharedManager;
|
||||
- (id)init;
|
||||
|
||||
#pragma mark *** Refreshing Feeds ***
|
||||
|
||||
/* A KVO compilant property telling you wether there is currently an update in progress or not.
|
||||
*/
|
||||
@property (readonly) BOOL refreshing;
|
||||
|
||||
/* Returns wether a refresh is currently possible. Always query this status before performing a refresh because otherwise your app might crash if there is no user currently logged in.
|
||||
*/
|
||||
- (BOOL)canRefresh;
|
||||
|
||||
/* Refreshes the feeds in the background and notifies the handler about any errors occured. If no error occured the handler is NOT invoked. If you want to know when the refresh process finished you can observe on the refreshing property for changes using KVO.
|
||||
If the update manager is already refreshing this method does nothing and returns immediately. The error handler is NOT invoked.
|
||||
You can specify a notify flag. If this flag is set to YES the update manger delivers a user notification for each new video. You may want to specify NO to set the current videos as the 'root' position, that is that these videos are 'known'. This is automatically done when the login changes (otherwise there could be a notification for each of the 98 videos in the feed).
|
||||
*/
|
||||
- (void)refreshFeedsWithErrorHandler:(void (^)(NSError *))handler notify:(BOOL)flag;
|
||||
|
||||
#pragma mark *** Notifications ***
|
||||
|
||||
/* Note to Notification restrictions: Notifications may also be restricted by the user-defined rules. The rules are stored in the user defaults for the key "Rules". For information on the format of rules see NYTRulesWindowController.
|
||||
*/
|
||||
|
||||
/* When refreshing feeds notifications will only be sent for feeds that are in the specified date range from now to a date that is offset to the past by this value.
|
||||
Specify 0 to not prevent notifications by time. Negative values will be evaluated as 0.
|
||||
*/
|
||||
@property NSTimeInterval maximumVideoAge;
|
||||
|
||||
/* If set to YES multiple video uploads will be coalesced into a single notification in the form of 'xy new videos' - 'abc, def, ghi and 42 more uploaded new videos'. The NYTUserNotificationURLKey value will not be a video URL but to an URL to the user's subscription video list on YouTube.
|
||||
*/
|
||||
@property BOOL coalescesNotifications;
|
||||
|
||||
#pragma mark *** Auto Refresh ***
|
||||
|
||||
/* Enabling and disabling auto refresh: You can turn on and off auto refresh permanently. This means that the auto refresh timer will be stopped and not be started again until auto refresh is re-enabled. While auto refresh is disabled pausing and resuming is still usable. Enabling auto refresh while it is paused will not start auto refresh. Auto refresh will be actually started when the same number of resume messages as pause messages were sent. You can still modify and query the refresh interval while auto refresh is disabled.
|
||||
While auto refresh is running the timer is also running and firing events. But if canRefresh returns NO auto refresh will not perform a refresh but just do nothing. (It will not stop the timer.)
|
||||
*/
|
||||
@property (getter = isAutoRefreshEnabled) BOOL autoRefreshEnabled;
|
||||
|
||||
/* Restarts auto refresh. If auto refresh is currently disabled this method has no effect. Use this method if you want the next auto refresh update to be fired in the amount of seconds specified by the auto refresh interval.
|
||||
*/
|
||||
- (void)restartAutoRefresh;
|
||||
|
||||
/* These methods pause and resume the auto refresh timer. The autoRefreshRunning property tells you wether the auto refresh timer is currently running (which means is will be set to NO if auto refresh is paused).
|
||||
Pausing and resuming auto refresh can be useful if you program enters a critical section during whose execution there should be no refresh (for example during modifying the update settings). Calls of pauseAutoRefreshing and resumeAutoRefreshing can be nested. Every pause message must have a corresponding resume message and vice versa. Auto refreshing will not be resumed until the same amount of resume messages were sent as pause messages were sent before.
|
||||
Pausing and resuming auto refresh is used by the update manager at several times (for example while the login/logout process is running). Do not suggest wether auto refreshing is currently running or paused based on your code. Always query the current staus by sending a isAutoRefreshRunning message before.
|
||||
*/
|
||||
@property (readonly, getter = isAutoRefreshRunning) BOOL autoRefreshRunning;
|
||||
- (void)pauseAutoRefreshing;
|
||||
- (void)resumeAutoRefreshing;
|
||||
|
||||
/* Performs an auto refresh. Although this method returns immediately it may not immediately refresh. If this method is invoked while auto refresh is "sleeping" the next time auto refresh will get actually started (as determined by isAutoRefreshRunning) it will trigger a refresh immediately. If auto refresh gets disabled after this method gets invoked or already is disabled by now nothing will happen.
|
||||
*/
|
||||
- (void)performAutoRefresh:(id)sender;
|
||||
|
||||
/* The time interval used by auto refresh to refresh the feeds. Specified in seconds. If you change this property the elapsed time is transfered to the new interval. If the new interval is smaller than the elapsed time auto refresh refreshes immediately. Specify 0 to refresh as often as possible (may be very inefficient since Auto Refresh is constructed to refresh over time.
|
||||
*/
|
||||
@property (nonatomic) NSTimeInterval autoRefreshInterval;
|
||||
|
||||
@end
|
||||
436
Notifications for YouTube/NYTUpdateManager.m
Executable file
@@ -0,0 +1,436 @@
|
||||
//
|
||||
// NYTUpdater.m
|
||||
// Notifications for YouTube
|
||||
//
|
||||
// Created by Kim Wittenburg on 09.07.13.
|
||||
// Copyright (c) 2013 Kim Wittenburg. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NYTUpdateManager.h"
|
||||
|
||||
// Core Imports
|
||||
#import "NYTVideo.h"
|
||||
#import "NYTUtil.h"
|
||||
#import "NYTAuthentication.h"
|
||||
|
||||
// Other Imports
|
||||
#import "NYTChannelRestriction.h"
|
||||
|
||||
/* User notifications sent by this class will contain this key in their userInfo dictionary. The value will be an NSString object representing an URL that can be opened to show the complete contents of the notification. These contents are either a video page or (for coalesced notifications) the user's "my subscriptions" page.
|
||||
*/
|
||||
NSString * const NYTUserNotificationURLKey = @"NYTUserNotificationURLKey";
|
||||
|
||||
@interface NYTUpdateManager ()
|
||||
|
||||
// Refreshing
|
||||
@property (readwrite) BOOL refreshing;
|
||||
|
||||
// Caching Refresh Data
|
||||
@property NSMutableSet *knownVideos;
|
||||
@property NSDate *lastRefreshDate; // Only for Auto Refresh
|
||||
|
||||
// Auto Refreshing
|
||||
@property NSTimer *autoRefreshTimer;
|
||||
@property BOOL autoRefreshWasDisabled;
|
||||
@property NSInteger autoRefreshPauseCount;
|
||||
@property BOOL refreshOnResume;
|
||||
|
||||
@end
|
||||
|
||||
@implementation NYTUpdateManager
|
||||
|
||||
@synthesize autoRefreshEnabled = _autoRefreshEnabled;
|
||||
|
||||
#pragma mark *** Obtaining the Shared Instance ***
|
||||
|
||||
static NYTUpdateManager *sharedManager = nil;
|
||||
|
||||
+ (NYTUpdateManager *)sharedManager
|
||||
{
|
||||
if (!sharedManager) {
|
||||
sharedManager = [[self alloc] init];
|
||||
}
|
||||
return sharedManager;
|
||||
}
|
||||
|
||||
- (id)init
|
||||
{
|
||||
// Allow exactly one instance at all (for any way of initialization)
|
||||
if (sharedManager) {
|
||||
return sharedManager;
|
||||
}
|
||||
self = [super init];
|
||||
if (self) {
|
||||
sharedManager = self;
|
||||
[[NSWorkspace sharedWorkspace].notificationCenter addObserver:self selector:@selector(systemWillGoToSleep:) name:NSWorkspaceWillSleepNotification object:NULL];
|
||||
[[NSWorkspace sharedWorkspace].notificationCenter addObserver:self selector:@selector(systemDidAwakeFromSleep:) name:NSWorkspaceDidWakeNotification object:NULL];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[self.autoRefreshTimer invalidate];
|
||||
[[NSWorkspace sharedWorkspace].notificationCenter removeObserver:self];
|
||||
}
|
||||
|
||||
#pragma mark *** Refreshing Feeds ***
|
||||
|
||||
- (BOOL)canRefresh
|
||||
{
|
||||
// If logged in refreshing is possible
|
||||
return [NYTAuthentication sharedAuthentication].isLoggedIn;
|
||||
}
|
||||
|
||||
- (void)refreshFeedsWithErrorHandler:(void (^)(NSError *))handler notify:(BOOL)flag
|
||||
{
|
||||
self.refreshOnResume = NO; // Just for sure
|
||||
if (self.refreshing) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare for refreshing
|
||||
self.refreshing = YES;
|
||||
[self pauseAutoRefreshing];
|
||||
self.lastRefreshDate = [NSDate date]; // Since the refresh date is just for auto refresh it should be changed here to let auto refresh continue even if there was an error
|
||||
|
||||
// Refresh on a background queue
|
||||
dispatch_queue_t prevQueue = dispatch_get_current_queue();
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
|
||||
// Prepare for paged feeds
|
||||
NSError *error;
|
||||
NSMutableSet *feedVideos;
|
||||
NSUInteger processedResults = 0;
|
||||
NSUInteger totalResults = 0;
|
||||
|
||||
do {
|
||||
// We are creating a couple of objects so setup a separate autoreleasepool
|
||||
@autoreleasepool {
|
||||
// Load the document
|
||||
NSString *url = [NSString stringWithFormat:@"https://gdata.youtube.com/feeds/api/users/default/newsubscriptionvideos?max-results=50&start-index=%li&v=2", processedResults+1];
|
||||
NSXMLDocument *document = LoadXMLDocumentSynchronous(url, [NYTAuthentication sharedAuthentication].authentication, &error);
|
||||
|
||||
// Store the entries as app-friendly video objects
|
||||
if (document) {
|
||||
|
||||
// Set up the collection for collecting the videos
|
||||
if (processedResults == 0) {
|
||||
static NSString *totalResultsPath = @"./feed/openSearch:totalResults";
|
||||
NSXMLElement *totalResultsElement = [document nodesForXPath:totalResultsPath error:nil][0];
|
||||
totalResults = (NSUInteger) totalResultsElement.stringValue.integerValue;
|
||||
feedVideos = [NSMutableSet setWithCapacity:totalResults];
|
||||
}
|
||||
|
||||
// For each page parse its results and add them to the collection
|
||||
static NSString *resultsPerPagePath = @"./feed/openSearch:itemsPerPage";
|
||||
NSXMLElement *resultsPerPageElement = [document nodesForXPath:resultsPerPagePath error:nil][0];
|
||||
int resultsPerPage = resultsPerPageElement.stringValue.intValue;
|
||||
|
||||
processedResults += resultsPerPage;
|
||||
[feedVideos unionSet:[self videosOnPage:document]];
|
||||
} else {
|
||||
// Abort on error
|
||||
[self abortRefreshOnQueue:prevQueue withErrorHandler:handler usingError:error];
|
||||
return;
|
||||
}
|
||||
}
|
||||
} while (processedResults < totalResults);
|
||||
|
||||
// Deliver user notifications if wanted
|
||||
if (flag) {
|
||||
[self notifyForVideos:[self videosForNotifications:feedVideos]];
|
||||
}
|
||||
|
||||
// Clean up
|
||||
self.knownVideos = [NSMutableSet setWithCapacity:feedVideos.count];
|
||||
for (NYTVideo *video in feedVideos) {
|
||||
// For memory efficiency just store the video IDs
|
||||
[self.knownVideos addObject:video.videoID];
|
||||
}
|
||||
// Resume on the caller's queue
|
||||
dispatch_async(prevQueue, ^{
|
||||
self.refreshing = NO;
|
||||
[self resumeAutoRefreshing];
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
- (void)abortRefreshOnQueue:(dispatch_queue_t)queue withErrorHandler:(void(^)(NSError *))handler usingError:(NSError *)error
|
||||
{
|
||||
dispatch_async(queue, ^{
|
||||
if (handler) {
|
||||
handler(error);
|
||||
}
|
||||
self.refreshing = NO;
|
||||
[self resumeAutoRefreshing];
|
||||
});
|
||||
}
|
||||
|
||||
- (NSSet *)videosOnPage:(NSXMLDocument *)document
|
||||
{
|
||||
// Get the entries
|
||||
static NSString *entryPath = @"./feed/entry";
|
||||
NSArray *entries = [document nodesForXPath:entryPath error:nil];
|
||||
NSMutableSet *videos = [NSMutableSet setWithCapacity:entries.count];
|
||||
|
||||
// Parse the entries
|
||||
for (NSXMLNode *entry in entries) {
|
||||
static NSString *mediaGroupPath = @"./media:group";
|
||||
NSXMLNode *mediaGroupNode = [entry nodesForXPath:mediaGroupPath error:nil][0];
|
||||
[videos addObject:[[NYTVideo alloc] initWithMediaGroupNode:mediaGroupNode]];
|
||||
}
|
||||
return videos;
|
||||
}
|
||||
|
||||
#pragma mark - System Notifications
|
||||
|
||||
- (void)systemWillGoToSleep:(NSNotification *)notification
|
||||
{
|
||||
[self pauseAutoRefreshing];
|
||||
}
|
||||
|
||||
- (void)systemDidAwakeFromSleep:(NSNotification *)notification
|
||||
{
|
||||
[self resumeAutoRefreshing];
|
||||
[self performAutoRefresh:nil];
|
||||
}
|
||||
|
||||
#pragma mark *** Notifications ***
|
||||
|
||||
- (NSSet *)videosForNotifications:(NSSet *)allVideos
|
||||
{
|
||||
// Prepare objects that are not dependant on videos
|
||||
NSMutableSet *acceptedVideos = [[NSMutableSet alloc] init];
|
||||
NSDictionary *restrictions = [[NSUserDefaults standardUserDefaults] objectForKey:@"Rules"];
|
||||
NSTimeInterval maximumVideoAgeOffset = self.maximumVideoAge <= 0 ? 0 : -self.maximumVideoAge;
|
||||
NSDate *minimumVideoUploadedDate = maximumVideoAgeOffset == 0 ? [NSDate distantPast] : [NSDate dateWithTimeIntervalSinceNow:maximumVideoAgeOffset];
|
||||
|
||||
// Enumerate all videos
|
||||
for (NYTVideo *video in allVideos) {
|
||||
// Get the video dependant objects
|
||||
NSTimeInterval timeSinceMinimumUploadedDate = [video.uploadedDate timeIntervalSinceDate:minimumVideoUploadedDate];
|
||||
NSData *restrictionData = restrictions[video.uploaderID];
|
||||
NYTChannelRestriction *restriction = nil;
|
||||
if (restrictionData) {
|
||||
restriction = [NSKeyedUnarchiver unarchiveObjectWithData:restrictionData];
|
||||
}
|
||||
|
||||
// Validate the video and accept it, if it passed the validation
|
||||
if (timeSinceMinimumUploadedDate >= 0 &&
|
||||
![self.knownVideos containsObject:video.videoID] &&
|
||||
[self acceptVideo:video withRestriction:restriction]) {
|
||||
[acceptedVideos addObject:video];
|
||||
} else {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return acceptedVideos;
|
||||
}
|
||||
|
||||
- (BOOL)acceptVideo:(NYTVideo *)video withRestriction:(NYTChannelRestriction *)restriction
|
||||
{
|
||||
// For efficiency restrictions are not stored if they only contain default values. The default values accept all videos.
|
||||
if (!restriction) {
|
||||
return YES;
|
||||
}
|
||||
if (restriction.disableAllNotifications) {
|
||||
return NO;
|
||||
}
|
||||
BOOL predicateResult = [restriction.predicate evaluateWithObject:video];
|
||||
return restriction.positivePredicate == predicateResult;
|
||||
}
|
||||
|
||||
// Convenience macros
|
||||
#define NYTCoalescedNotificationNumberOfIncludedChannels 3
|
||||
#define NYTCoalescedNotificationShouldIncludeAll(varName) (varName.count <= NYTCoalescedNotificationNumberOfIncludedChannels + 1)
|
||||
|
||||
- (void)notifyForVideos:(NSSet *)videos
|
||||
{
|
||||
// Should coalesce
|
||||
if (videos.count > 1 && self.coalescesNotifications) {
|
||||
|
||||
// Create the notification
|
||||
NSUserNotification *notification = [[NSUserNotification alloc] init];
|
||||
notification.title = [NSString stringWithFormat:NSLocalizedString(@"%li new videos", nil), videos.count];
|
||||
|
||||
// Create the textual enumeration for the uploaders
|
||||
NSMutableString *uploadersEnum = @"".mutableCopy;
|
||||
NSSet *uploaders = [videos valueForKey:@"uploaderDisplayName"];
|
||||
NSInteger processed = 0;
|
||||
for (NSString *uploader in uploaders) {
|
||||
if (processed == 0) {
|
||||
// The first uploader is just added as is
|
||||
[uploadersEnum appendString:uploader];
|
||||
} else if (processed < NYTCoalescedNotificationNumberOfIncludedChannels
|
||||
|| NYTCoalescedNotificationShouldIncludeAll(uploaders)) {
|
||||
if (processed == uploaders.count - 1) {
|
||||
// This is the last one so offer a special separation text
|
||||
[uploadersEnum appendFormat:NSLocalizedString(@" and %@", nil), uploader];
|
||||
} else {
|
||||
[uploadersEnum appendFormat:NSLocalizedString(@", %@", nil), uploader];
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
processed++;
|
||||
}
|
||||
// Set the enum for the notification
|
||||
NSString *informativeText;
|
||||
if (NYTCoalescedNotificationShouldIncludeAll(uploaders)) {
|
||||
// There are no "more" uploaders
|
||||
informativeText = [NSString stringWithFormat:NSLocalizedString(@"New videos from %@", nil), uploadersEnum];
|
||||
} else {
|
||||
// There are 2 or more "more" uploaders
|
||||
informativeText = [NSString stringWithFormat:NSLocalizedString(@"New videos from %@ and %li more", nil), uploadersEnum, uploaders.count - NYTCoalescedNotificationNumberOfIncludedChannels];
|
||||
}
|
||||
|
||||
// Finish the notification and deliver it
|
||||
notification.informativeText = informativeText;
|
||||
notification.userInfo = @{NYTUserNotificationURLKey: @"http://www.youtube.com/feed/subscriptions"};
|
||||
notification.soundName = NSUserNotificationDefaultSoundName;
|
||||
dispatch_async(dispatch_get_current_queue(), ^{
|
||||
[[NSUserNotificationCenter defaultUserNotificationCenter] deliverNotification:notification];
|
||||
});
|
||||
|
||||
} else {
|
||||
for (NYTVideo *video in videos) {
|
||||
// Configure the notification for every video
|
||||
NSUserNotification *notification = [[NSUserNotification alloc] init];
|
||||
notification.title = video.uploaderDisplayName;
|
||||
notification.informativeText = video.title;
|
||||
notification.userInfo = @{NYTUserNotificationURLKey: video.URL};
|
||||
notification.deliveryDate = video.uploadedDate;
|
||||
notification.soundName = NSUserNotificationDefaultSoundName;
|
||||
// Just to make sure notifications get delivered on the main thread
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[[NSUserNotificationCenter defaultUserNotificationCenter] deliverNotification:notification];
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark *** Auto Refresh ***
|
||||
|
||||
- (void)performAutoRefresh:(id)sender
|
||||
{
|
||||
if (self.canRefresh && self.isAutoRefreshRunning) {
|
||||
[self refreshFeedsWithErrorHandler:NULL notify:YES];
|
||||
} else {
|
||||
self.refreshOnResume = YES; // Will be of no matter if auto refresh is disabled
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setAutoRefreshEnabled:(BOOL)autoRefreshEnabled
|
||||
{
|
||||
if (autoRefreshEnabled) {
|
||||
[self enableAutoRefresh];
|
||||
} else {
|
||||
[self disableAutoRefresh];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)enableAutoRefresh
|
||||
{
|
||||
if (!self.autoRefreshEnabled) {
|
||||
_autoRefreshEnabled = YES;
|
||||
self.autoRefreshWasDisabled = YES;
|
||||
[self startAutoRefreshTimerIfAppropriate:NO];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)disableAutoRefresh
|
||||
{
|
||||
if (self.autoRefreshEnabled) {
|
||||
[self stopAutoRefreshTimer];
|
||||
_autoRefreshEnabled = NO;
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)isAutoRefreshEnabled
|
||||
{
|
||||
return _autoRefreshEnabled;
|
||||
}
|
||||
|
||||
- (void)restartAutoRefresh
|
||||
{
|
||||
[self stopAutoRefreshTimer];
|
||||
[self startAutoRefreshTimerIfAppropriate:YES];
|
||||
}
|
||||
|
||||
- (BOOL)isAutoRefreshRunning
|
||||
{
|
||||
return self.autoRefreshTimer != nil;
|
||||
}
|
||||
|
||||
- (void)pauseAutoRefreshing
|
||||
{
|
||||
self.autoRefreshPauseCount++;
|
||||
if (self.autoRefreshPauseCount == 1) {
|
||||
[self stopAutoRefreshTimer];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)resumeAutoRefreshing
|
||||
{
|
||||
self.autoRefreshPauseCount--;
|
||||
if (self.autoRefreshPauseCount == 0) {
|
||||
[self startAutoRefreshTimerIfAppropriate:YES];
|
||||
} else if (self.autoRefreshPauseCount < 0) {
|
||||
[NSException raise:@"AutoRefreshUnbalancedPauseException" format:@"Auto Refresh pause-resume messages are not balanced. Each pauseAutoRefreshing must be balanced with exactly one resumeAutoRefreshin message. The last resumeAutoRefreshing message did not have a corresponding pauseAutoRefreshing message."];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)startAutoRefreshTimerIfAppropriate:(BOOL)resume
|
||||
{
|
||||
// If auto refresh should be running right now
|
||||
if (self.autoRefreshEnabled && self.autoRefreshPauseCount == 0) {
|
||||
[self.autoRefreshTimer invalidate]; // Just to be sure
|
||||
// Should we really resume or restart?
|
||||
BOOL actualResume = resume && !self.autoRefreshWasDisabled;
|
||||
self.autoRefreshWasDisabled = NO;
|
||||
|
||||
// Auto refresh was paused by sending a pauseAutoRefreshing message and should now be resumed
|
||||
if (actualResume) {
|
||||
|
||||
// Get the time since the last update
|
||||
NSTimeInterval elapsed = [[NSDate date] timeIntervalSinceDate:self.lastRefreshDate];
|
||||
|
||||
// If the current time interval requires an update based on the elapsed time refresh immediately and schedule the timer with the complete interval
|
||||
if (self.refreshOnResume || elapsed >= self.autoRefreshInterval) {
|
||||
self.autoRefreshTimer = [NSTimer scheduledTimerWithTimeInterval:self.autoRefreshInterval target:self selector:@selector(performAutoRefresh:) userInfo:nil repeats:YES];
|
||||
[self.autoRefreshTimer fire]; // Fire immediately because the elapsed time since pausing the timer exceeds the refresh interval or we are forced to do so
|
||||
} else {
|
||||
|
||||
// Otherwise set the first fire date to the time point that is constitued by the current time interval and the last refresh date
|
||||
NSDate *firstFireDate = [NSDate dateWithTimeInterval:self.autoRefreshInterval sinceDate:self.lastRefreshDate];
|
||||
self.autoRefreshTimer = [[NSTimer alloc] initWithFireDate:firstFireDate interval:self.autoRefreshInterval target:self selector:@selector(performAutoRefresh:) userInfo:nil repeats:YES];
|
||||
[[NSRunLoop currentRunLoop] addTimer:self.autoRefreshTimer forMode:NSDefaultRunLoopMode];
|
||||
}
|
||||
} else {
|
||||
|
||||
// Auto refresh was disabled since the timer was stopped so just start the timer. Refresh on resume does not have an impact here.
|
||||
self.lastRefreshDate = [NSDate date];
|
||||
self.autoRefreshTimer = [NSTimer scheduledTimerWithTimeInterval:self.autoRefreshInterval target:self selector:@selector(performAutoRefresh:) userInfo:nil repeats:YES];
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
self.refreshOnResume = NO; // We already have passed the point where it's important
|
||||
}
|
||||
}
|
||||
|
||||
- (void)stopAutoRefreshTimer
|
||||
{
|
||||
[self.autoRefreshTimer invalidate];
|
||||
self.autoRefreshTimer = nil;
|
||||
}
|
||||
|
||||
- (void)setAutoRefreshInterval:(NSTimeInterval)autoRefreshInterval
|
||||
{
|
||||
[self pauseAutoRefreshing];
|
||||
_autoRefreshInterval = autoRefreshInterval;
|
||||
[self resumeAutoRefreshing];
|
||||
}
|
||||
|
||||
@end
|
||||
48
Notifications for YouTube/NYTUser.h
Executable file
@@ -0,0 +1,48 @@
|
||||
//
|
||||
// NYTUser.h
|
||||
// Notifications for YouTube
|
||||
//
|
||||
// Created by Kim Wittenburg on 18.06.13.
|
||||
// Copyright (c) 2013 Kim Wittenburg. All rights reserved.
|
||||
//
|
||||
|
||||
/* A NYTUser represents a user or a channel on YouTube.
|
||||
*/
|
||||
@interface NYTUser : NSObject
|
||||
|
||||
#pragma mark *** Properties ***
|
||||
|
||||
/* The display name of the user. Always use this property if you need to display a name to the user. This name is NOT unique. Use the userID or the channelID to uniquely identify a user or a channel respectively.
|
||||
*/
|
||||
@property (readonly) NSString *displayName;
|
||||
|
||||
/* Uniquely identifies a user. Every user on youtube has a unique user id.
|
||||
*/
|
||||
@property (readonly) NSString *userID;
|
||||
|
||||
/* Uniquely identifies a channel. Not every user has a channel. The channel id is always exactly the user id prefixed by @"UC".
|
||||
*/
|
||||
@property (readonly) NSString *channelID;
|
||||
|
||||
/* The textual summary of the channel. This is sometimes referred to as the channel's description. You should display this in combination with the display name to the user to help him identify the channel he wants.
|
||||
*/
|
||||
@property (readonly) NSString *summary;
|
||||
|
||||
/* The image URL is a network URL pointing to an image resource that is the user's image.
|
||||
*/
|
||||
@property (readonly) NSURL *imageURL;
|
||||
|
||||
#pragma mark *** Initialization ***
|
||||
|
||||
/* Parses a user from its profile page ("https://gdata.youtube.com/feeds/api/users/default"). Initializes displayName, userID, channelID and summary.
|
||||
*/
|
||||
- (id)initWithUserProfilePage:(NSXMLNode *)node;
|
||||
|
||||
/* Parses a user from its subscription entry (e.g. "https://gdata.youtube.com/feeds/api/users/default/subscriptions"). Initializes displayNamem, userID, channelID and imageURL
|
||||
*/
|
||||
- (id)initWithSubscriptionEntry:(NSXMLNode *)entry;
|
||||
|
||||
- (BOOL)isEqual:(id)object;
|
||||
- (BOOL)isEqualToUser:(NYTUser *)user;
|
||||
|
||||
@end
|
||||
86
Notifications for YouTube/NYTUser.m
Executable file
@@ -0,0 +1,86 @@
|
||||
//
|
||||
// NYTUser.m
|
||||
// Notifications for YouTube
|
||||
//
|
||||
// Created by Kim Wittenburg on 18.06.13.
|
||||
// Copyright (c) 2013 Kim Wittenburg. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NYTUser.h"
|
||||
|
||||
@implementation NYTUser
|
||||
|
||||
- (id)initWithUserProfilePage:(NSXMLNode *)node
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
static NSString *const displayNamePath = @"./entry/author/name";
|
||||
static NSString *const userIDPath = @"./entry/yt:username";
|
||||
static NSString *const summaryPath = @"./entry/content";
|
||||
NSXMLElement *displayNameElement = [node nodesForXPath:displayNamePath error:nil][0];
|
||||
NSXMLElement *userIDElement = [node nodesForXPath:userIDPath error:nil][0];
|
||||
NSXMLElement *summaryElement = [node nodesForXPath:summaryPath error:nil][0];
|
||||
_displayName = displayNameElement.stringValue;
|
||||
_userID = userIDElement.stringValue;
|
||||
_channelID = [NSString stringWithFormat:@"UC%@", _userID];
|
||||
_summary = summaryElement.stringValue;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (id)initWithSubscriptionEntry:(NSXMLNode *)entry
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
static NSString *const displayNamePath = @"./yt:username/@display";
|
||||
static NSString *const imagePath = @"./media:thumbnail/@url";
|
||||
static NSString *const channelIDPath = @"./yt:channelId";
|
||||
NSXMLElement *displayNameElement = [entry nodesForXPath:displayNamePath error:nil][0];
|
||||
NSXMLElement *imageElement = [entry nodesForXPath:imagePath error:nil][0];
|
||||
NSXMLElement *channelIDElement = [entry nodesForXPath:channelIDPath error:nil][0];
|
||||
_displayName = displayNameElement.stringValue;
|
||||
_imageURL = [NSURL URLWithString:imageElement.stringValue];
|
||||
_channelID = channelIDElement.stringValue;
|
||||
_userID = [_channelID substringFromIndex:2];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(id)object
|
||||
{
|
||||
if (self == object) {
|
||||
return YES;
|
||||
}
|
||||
if (object == nil) {
|
||||
return NO;
|
||||
}
|
||||
if (![object isKindOfClass:[NYTUser class]]) {
|
||||
return NO;
|
||||
}
|
||||
return [self isEqualToUser:object];
|
||||
}
|
||||
|
||||
- (BOOL)isEqualToUser:(NYTUser *)user
|
||||
{
|
||||
if (self == user) {
|
||||
return YES;
|
||||
}
|
||||
if (self.userID) {
|
||||
if (user.userID) {
|
||||
return [self.userID isEqualToString:user.userID];
|
||||
} else {
|
||||
NSString *otherUserID = [user.channelID substringFromIndex:2]; // Remove the UC from channel ID
|
||||
return [self.userID isEqualToString:otherUserID];
|
||||
}
|
||||
} else if (self.channelID) {
|
||||
if (user.channelID) {
|
||||
return [self.channelID isEqualToString:user.channelID];
|
||||
} else {
|
||||
NSString *userID = [self.channelID substringFromIndex:2]; // Remove the UC from channel ID
|
||||
return [userID isEqualToString:user.userID];
|
||||
}
|
||||
}
|
||||
return NO; // Probably won't get here
|
||||
}
|
||||
|
||||
@end
|
||||
17
Notifications for YouTube/NYTUtil.h
Executable file
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// NYTUtil.h
|
||||
// Notifications for YouTube
|
||||
//
|
||||
// Created by Kim Wittenburg on 21.07.13.
|
||||
// Copyright (c) 2013 Kim Wittenburg. All rights reserved.
|
||||
//
|
||||
|
||||
@class GTMOAuth2Authentication;
|
||||
|
||||
/* Convenience macro
|
||||
*/
|
||||
#define NYTNull [NSNull null]
|
||||
|
||||
/* Loads the contents of an URL synchronously. The URL request is authorized and authenticated for YouTube requests previously. If an error occurs nil is returned and outError is set to an appropriate error pointer. Otherwise outError is left unmodified.
|
||||
*/
|
||||
extern NSXMLDocument * LoadXMLDocumentSynchronous(NSString *url, GTMOAuth2Authentication *auth, NSError **outError);
|
||||
52
Notifications for YouTube/NYTUtil.m
Executable file
@@ -0,0 +1,52 @@
|
||||
//
|
||||
// NYTUtil.m
|
||||
// Notifications for YouTube
|
||||
//
|
||||
// Created by Kim Wittenburg on 21.07.13.
|
||||
// Copyright (c) 2013 Kim Wittenburg. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NYTUtil.h"
|
||||
|
||||
// API Imports
|
||||
#import "GTMOAuth2Authentication.h"
|
||||
|
||||
// Core Imports
|
||||
#import "NYTAuthentication.h"
|
||||
|
||||
extern NSXMLDocument * LoadXMLDocumentSynchronous(NSString *url, GTMOAuth2Authentication *auth, NSError **outError)
|
||||
{
|
||||
// Group to wait for refresh of token (if neccessary)
|
||||
dispatch_group_t group = dispatch_group_create();
|
||||
dispatch_group_enter(group);
|
||||
|
||||
// Create the request; authenticate and authorize it.
|
||||
NSURL *actualURL = [NSURL URLWithString:url];
|
||||
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:actualURL];
|
||||
[request setValue:DeveloperKey forHTTPHeaderField:DeveloperKeyHTTPHeaderField];
|
||||
dispatch_sync(dispatch_get_main_queue(), ^{
|
||||
// Seems that authorization must be performed on the main queue
|
||||
[auth authorizeRequest:request completionHandler:^(NSError *error) {
|
||||
if (outError != NULL) {
|
||||
*outError = error;
|
||||
}
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
});
|
||||
|
||||
// Wait for authorization
|
||||
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
|
||||
if (*outError) {
|
||||
// The authorization failed
|
||||
return nil;
|
||||
}
|
||||
|
||||
// Create the document
|
||||
NSData *data = [NSURLConnection sendSynchronousRequest:request returningResponse:NULL error:outError];
|
||||
if (!data) {
|
||||
return nil;
|
||||
}
|
||||
NSXMLDocument *document = [[NSXMLDocument alloc] initWithData:data options:NSXMLDocumentTidyXML error:outError];
|
||||
return document;
|
||||
|
||||
}
|
||||
48
Notifications for YouTube/NYTVideo.h
Executable file
@@ -0,0 +1,48 @@
|
||||
//
|
||||
// NYTVideo.h
|
||||
// Notifications for YouTube
|
||||
//
|
||||
// Created by Kim Wittenburg on 14.07.13.
|
||||
// Copyright (c) 2013 Kim Wittenburg. All rights reserved.
|
||||
//
|
||||
|
||||
/* A NYTVideo represents a YouTube video's metadata.
|
||||
*/
|
||||
@interface NYTVideo : NSObject
|
||||
|
||||
/* A unique id that identifies the video.
|
||||
*/
|
||||
@property (readonly) NSString *videoID;
|
||||
|
||||
/* The channel id of the channel that uploaded the video. See channelID in NYTUser for more details on channel ids.
|
||||
*/
|
||||
@property (readonly) NSString *uploaderID;
|
||||
|
||||
/* The date this video was uploaded to YouTube. Precise on 1 second.
|
||||
*/
|
||||
@property (readonly) NSDate *uploadedDate;
|
||||
|
||||
/* The title of the video as displayed on the website. Use this to display a video title to a user. Do not use this to uniquely identify a video. use the videoID instead for that purpose.
|
||||
*/
|
||||
@property (readonly) NSString *title;
|
||||
|
||||
/* The URL on YouTube that runs the video. Open this URL to display this video on YouTube.
|
||||
*/
|
||||
@property (readonly) NSString *URL;
|
||||
|
||||
/* The description of the video as displayed on the website. Display this with the video's title to help the user identify a video.
|
||||
*/
|
||||
@property (readonly) NSString *videoDescription;
|
||||
|
||||
/* The display name of the channel that uploaded the video. See displayName in NYTUser for more information.
|
||||
*/
|
||||
@property (readonly) NSString *uploaderDisplayName;
|
||||
|
||||
/* Initializes this video by parsing the given node. The node should be a media:group node of a video feed entry.
|
||||
*/
|
||||
- (id)initWithMediaGroupNode:(NSXMLNode *)node;
|
||||
|
||||
- (BOOL)isEqual:(id)object;
|
||||
- (BOOL)isEqualToVideo:(NYTVideo *)video;
|
||||
|
||||
@end
|
||||
81
Notifications for YouTube/NYTVideo.m
Executable file
@@ -0,0 +1,81 @@
|
||||
//
|
||||
// NYTVideo.m
|
||||
// Notifications for YouTube
|
||||
//
|
||||
// Created by Kim Wittenburg on 14.07.13.
|
||||
// Copyright (c) 2013 Kim Wittenburg. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NYTVideo.h"
|
||||
|
||||
// API Imports
|
||||
#import "ISO8601DateFormatter.h"
|
||||
|
||||
@implementation NYTVideo
|
||||
|
||||
static ISO8601DateFormatter *formatter = nil;
|
||||
|
||||
+ (ISO8601DateFormatter *)dateFormatter
|
||||
{
|
||||
// For efficiency use one instace for the entire app
|
||||
if (!formatter) {
|
||||
formatter = [[ISO8601DateFormatter alloc] init];
|
||||
formatter.includeTime = YES;
|
||||
}
|
||||
return formatter;
|
||||
}
|
||||
|
||||
- (id)initWithMediaGroupNode:(NSXMLNode *)node
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
static NSString *const videoIDPath = @"./yt:videoid";
|
||||
static NSString *const uploaderIDPath = @"./yt:uploaderId";
|
||||
static NSString *const uploadedDatePath = @"./yt:uploaded";
|
||||
static NSString *const titlePath = @"./media:title";
|
||||
static NSString *const urlPath = @"./media:player/@url";
|
||||
static NSString *const descriptionPath = @"./media:description";
|
||||
static NSString *const uploaderDisplayNamePath = @"./media:credit[@role='uploader']/@yt:display";
|
||||
NSXMLElement *videoIDElement = [node nodesForXPath:videoIDPath error:nil][0];
|
||||
NSXMLElement *uploaderIDElement = [node nodesForXPath:uploaderIDPath error:nil][0];
|
||||
NSXMLElement *uploadedDateElement = [node nodesForXPath:uploadedDatePath error:nil][0];
|
||||
NSXMLElement *titleElement = [node nodesForXPath:titlePath error:nil][0];
|
||||
NSXMLElement *urlElement = [node nodesForXPath:urlPath error:nil][0];
|
||||
NSXMLElement *descriptionElement = [node nodesForXPath:descriptionPath error:nil][0];
|
||||
NSXMLElement *uploaderDisplayNameElement = [node nodesForXPath:uploaderDisplayNamePath error:nil][0];
|
||||
_videoID = videoIDElement.stringValue;
|
||||
_uploaderID = uploaderIDElement.stringValue;
|
||||
_uploadedDate = [[NYTVideo dateFormatter] dateFromString:uploadedDateElement.stringValue];
|
||||
_title = titleElement.stringValue;
|
||||
_URL = urlElement.stringValue;
|
||||
_videoDescription = descriptionElement.stringValue;
|
||||
_uploaderDisplayName = uploaderDisplayNameElement.stringValue;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSString *)description
|
||||
{
|
||||
return self.videoID;
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(id)object
|
||||
{
|
||||
if (self == object) {
|
||||
return YES;
|
||||
}
|
||||
if (object == nil) {
|
||||
return NO;
|
||||
}
|
||||
if (![object isKindOfClass:[NYTVideo class]]) {
|
||||
return NO;
|
||||
}
|
||||
return [self isEqualToVideo:object];
|
||||
}
|
||||
|
||||
- (BOOL)isEqualToVideo:(NYTVideo *)video
|
||||
{
|
||||
return [self.videoID isEqualToString:video.videoID];
|
||||
}
|
||||
|
||||
@end
|
||||
12
Notifications for YouTube/Notifications for YouTube-Info.plist
Normal file → Executable file
@@ -4,10 +4,12 @@
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Notifications for YouTube</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>${EXECUTABLE_NAME}</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string></string>
|
||||
<string>Notifications for YouTube</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>wittenburg.kim.${PRODUCT_NAME:rfc1034identifier}</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
@@ -17,15 +19,19 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<string>1.1.3 Beta</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<string>Build 42</string>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.entertainment</string>
|
||||
<key>LSHasLocalizedDisplayName</key>
|
||||
<true/>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>${MACOSX_DEPLOYMENT_TARGET}</string>
|
||||
<key>LSUIElement</key>
|
||||
<true/>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2013 Kim Wittenburg. All rights reserved.</string>
|
||||
<key>NSMainNibFile</key>
|
||||
|
||||
0
Notifications for YouTube/Notifications for YouTube-Prefix.pch
Normal file → Executable file
16
Notifications for YouTube/NumberValueTransformer.h
Executable file
@@ -0,0 +1,16 @@
|
||||
//
|
||||
// TimeIntervalValueTransformer.h
|
||||
// Notifications for YouTube
|
||||
//
|
||||
// Created by Kim Wittenburg on 20.07.13.
|
||||
// Copyright (c) 2013 Kim Wittenburg. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
/* The number value transformer transforms a number into... a number. Into exactly the same number. You can use this transformer if you get an type error binding a property of the right type. For example if you bind the selected tag of a popup button to an NSTimeInterval property.
|
||||
This transformer does allow reverse transformation.
|
||||
*/
|
||||
@interface NumberValueTransformer : NSValueTransformer
|
||||
|
||||
@end
|
||||
33
Notifications for YouTube/NumberValueTransformer.m
Executable file
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// TimeIntervalValueTransformer.m
|
||||
// Notifications for YouTube
|
||||
//
|
||||
// Created by Kim Wittenburg on 20.07.13.
|
||||
// Copyright (c) 2013 Kim Wittenburg. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NumberValueTransformer.h"
|
||||
|
||||
@implementation NumberValueTransformer
|
||||
|
||||
+ (Class)transformedValueClass
|
||||
{
|
||||
return [NSNumber class];
|
||||
}
|
||||
|
||||
+ (BOOL)allowsReverseTransformation
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (id)transformedValue:(id)value
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
- (id)reverseTransformedValue:(id)value
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
@end
|
||||
19
Notifications for YouTube/de.lproj/Credits.rtf
Executable file
@@ -0,0 +1,19 @@
|
||||
{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390
|
||||
{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
|
||||
{\colortbl;\red255\green255\blue255;}
|
||||
\paperw11900\paperh16840\vieww9600\viewh8400\viewkind0
|
||||
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720
|
||||
|
||||
\f0\b\fs24 \cf0 Entwicklung:
|
||||
\b0 \
|
||||
Kim Wittenburg\
|
||||
\
|
||||
|
||||
\b Design:
|
||||
\b0 \
|
||||
Kim Wittenburg\
|
||||
\
|
||||
|
||||
\b Tests:
|
||||
\b0 \
|
||||
Kim Wittenburg}
|
||||
4
Notifications for YouTube/de.lproj/InfoPlist.strings
Executable file
@@ -0,0 +1,4 @@
|
||||
/* Localized versions of Info.plist keys */
|
||||
|
||||
"CFBundleDisplayName" = "Benachrichtigungen für YouTube";
|
||||
"Notifications for YouTube" = "Benachrichtigungen für YouTube";
|
||||
BIN
Notifications for YouTube/de.lproj/Localizable.strings
Executable file
993
Notifications for YouTube/de.lproj/MainMenu.xib
Executable file
@@ -0,0 +1,993 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="5053" systemVersion="13C64" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
|
||||
<dependencies>
|
||||
<deployment defaultVersion="1080" identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="5053"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
|
||||
<connections>
|
||||
<outlet property="delegate" destination="494" id="495"/>
|
||||
</connections>
|
||||
</customObject>
|
||||
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
|
||||
<customObject id="-3" userLabel="Application"/>
|
||||
<menu title="AMainMenu" systemMenu="main" id="29">
|
||||
<items>
|
||||
<menuItem title="Notifications for YouTube" id="56">
|
||||
<menu key="submenu" title="Notifications for YouTube" systemMenu="apple" id="57">
|
||||
<items>
|
||||
<menuItem title="About Notifications for YouTube" id="58">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="orderFrontStandardAboutPanel:" target="-2" id="142"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="236">
|
||||
<modifierMask key="keyEquivalentModifierMask" command="YES"/>
|
||||
</menuItem>
|
||||
<menuItem title="Preferences…" keyEquivalent="," id="129"/>
|
||||
<menuItem isSeparatorItem="YES" id="143">
|
||||
<modifierMask key="keyEquivalentModifierMask" command="YES"/>
|
||||
</menuItem>
|
||||
<menuItem title="Services" id="131">
|
||||
<menu key="submenu" title="Services" systemMenu="services" id="130"/>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="144">
|
||||
<modifierMask key="keyEquivalentModifierMask" command="YES"/>
|
||||
</menuItem>
|
||||
<menuItem title="Hide Notifications for YouTube" keyEquivalent="h" id="134">
|
||||
<connections>
|
||||
<action selector="hide:" target="-1" id="367"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Hide Others" keyEquivalent="h" id="145">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="hideOtherApplications:" target="-1" id="368"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Show All" id="150">
|
||||
<connections>
|
||||
<action selector="unhideAllApplications:" target="-1" id="370"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="149">
|
||||
<modifierMask key="keyEquivalentModifierMask" command="YES"/>
|
||||
</menuItem>
|
||||
<menuItem title="Quit Notifications for YouTube" keyEquivalent="q" id="136">
|
||||
<connections>
|
||||
<action selector="terminate:" target="-3" id="449"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="File" id="83">
|
||||
<menu key="submenu" title="File" id="81">
|
||||
<items>
|
||||
<menuItem title="New" keyEquivalent="n" id="82">
|
||||
<connections>
|
||||
<action selector="newDocument:" target="-1" id="373"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Open…" keyEquivalent="o" id="72">
|
||||
<connections>
|
||||
<action selector="openDocument:" target="-1" id="374"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Open Recent" id="124">
|
||||
<menu key="submenu" title="Open Recent" systemMenu="recentDocuments" id="125">
|
||||
<items>
|
||||
<menuItem title="Clear Menu" id="126">
|
||||
<connections>
|
||||
<action selector="clearRecentDocuments:" target="-1" id="127"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="79">
|
||||
<modifierMask key="keyEquivalentModifierMask" command="YES"/>
|
||||
</menuItem>
|
||||
<menuItem title="Close" keyEquivalent="w" id="73">
|
||||
<connections>
|
||||
<action selector="performClose:" target="-1" id="193"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Save…" keyEquivalent="s" id="75">
|
||||
<connections>
|
||||
<action selector="saveDocument:" target="-1" id="362"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Revert to Saved" id="112">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="revertDocumentToSaved:" target="-1" id="364"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="74">
|
||||
<modifierMask key="keyEquivalentModifierMask" command="YES"/>
|
||||
</menuItem>
|
||||
<menuItem title="Page Setup..." keyEquivalent="P" id="77">
|
||||
<modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="runPageLayout:" target="-1" id="87"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Print…" keyEquivalent="p" id="78">
|
||||
<connections>
|
||||
<action selector="print:" target="-1" id="86"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Edit" id="217">
|
||||
<menu key="submenu" title="Edit" id="205">
|
||||
<items>
|
||||
<menuItem title="Undo" keyEquivalent="z" id="207">
|
||||
<connections>
|
||||
<action selector="undo:" target="-1" id="223"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Redo" keyEquivalent="Z" id="215">
|
||||
<modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="redo:" target="-1" id="231"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="206">
|
||||
<modifierMask key="keyEquivalentModifierMask" command="YES"/>
|
||||
</menuItem>
|
||||
<menuItem title="Cut" keyEquivalent="x" id="199">
|
||||
<connections>
|
||||
<action selector="cut:" target="-1" id="228"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Copy" keyEquivalent="c" id="197">
|
||||
<connections>
|
||||
<action selector="copy:" target="-1" id="224"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Paste" keyEquivalent="v" id="203">
|
||||
<connections>
|
||||
<action selector="paste:" target="-1" id="226"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Paste and Match Style" keyEquivalent="V" id="485">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="pasteAsPlainText:" target="-1" id="486"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Delete" id="202">
|
||||
<connections>
|
||||
<action selector="delete:" target="-1" id="235"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Select All" keyEquivalent="a" id="198">
|
||||
<connections>
|
||||
<action selector="selectAll:" target="-1" id="232"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="214">
|
||||
<modifierMask key="keyEquivalentModifierMask" command="YES"/>
|
||||
</menuItem>
|
||||
<menuItem title="Find" id="218">
|
||||
<menu key="submenu" title="Find" id="220">
|
||||
<items>
|
||||
<menuItem title="Find…" tag="1" keyEquivalent="f" id="209">
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="-1" id="241"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Find and Replace…" tag="12" keyEquivalent="f" id="534">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="-1" id="535"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Find Next" tag="2" keyEquivalent="g" id="208">
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="-1" id="487"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Find Previous" tag="3" keyEquivalent="G" id="213">
|
||||
<modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="-1" id="488"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Use Selection for Find" tag="7" keyEquivalent="e" id="221">
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="-1" id="489"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Jump to Selection" keyEquivalent="j" id="210">
|
||||
<connections>
|
||||
<action selector="centerSelectionInVisibleArea:" target="-1" id="245"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Spelling and Grammar" id="216">
|
||||
<menu key="submenu" title="Spelling and Grammar" id="200">
|
||||
<items>
|
||||
<menuItem title="Show Spelling and Grammar" keyEquivalent=":" id="204">
|
||||
<connections>
|
||||
<action selector="showGuessPanel:" target="-1" id="230"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Check Document Now" keyEquivalent=";" id="201">
|
||||
<connections>
|
||||
<action selector="checkSpelling:" target="-1" id="225"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="453"/>
|
||||
<menuItem title="Check Spelling While Typing" id="219">
|
||||
<connections>
|
||||
<action selector="toggleContinuousSpellChecking:" target="-1" id="222"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Check Grammar With Spelling" id="346">
|
||||
<connections>
|
||||
<action selector="toggleGrammarChecking:" target="-1" id="347"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Correct Spelling Automatically" id="454">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticSpellingCorrection:" target="-1" id="456"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Substitutions" id="348">
|
||||
<menu key="submenu" title="Substitutions" id="349">
|
||||
<items>
|
||||
<menuItem title="Show Substitutions" id="457">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="orderFrontSubstitutionsPanel:" target="-1" id="458"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="459"/>
|
||||
<menuItem title="Smart Copy/Paste" tag="1" keyEquivalent="f" id="350">
|
||||
<connections>
|
||||
<action selector="toggleSmartInsertDelete:" target="-1" id="355"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Smart Quotes" tag="2" keyEquivalent="g" id="351">
|
||||
<connections>
|
||||
<action selector="toggleAutomaticQuoteSubstitution:" target="-1" id="356"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Smart Dashes" id="460">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticDashSubstitution:" target="-1" id="461"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Smart Links" tag="3" keyEquivalent="G" id="354">
|
||||
<modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticLinkDetection:" target="-1" id="357"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Text Replacement" id="462">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticTextReplacement:" target="-1" id="463"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Transformations" id="450">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Transformations" id="451">
|
||||
<items>
|
||||
<menuItem title="Make Upper Case" id="452">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="uppercaseWord:" target="-1" id="464"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Make Lower Case" id="465">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="lowercaseWord:" target="-1" id="468"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Capitalize" id="466">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="capitalizeWord:" target="-1" id="467"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Speech" id="211">
|
||||
<menu key="submenu" title="Speech" id="212">
|
||||
<items>
|
||||
<menuItem title="Start Speaking" id="196">
|
||||
<connections>
|
||||
<action selector="startSpeaking:" target="-1" id="233"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Stop Speaking" id="195">
|
||||
<connections>
|
||||
<action selector="stopSpeaking:" target="-1" id="227"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Format" id="375">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Format" id="376">
|
||||
<items>
|
||||
<menuItem title="Font" id="377">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Font" systemMenu="font" id="388">
|
||||
<items>
|
||||
<menuItem title="Show Fonts" keyEquivalent="t" id="389">
|
||||
<connections>
|
||||
<action selector="orderFrontFontPanel:" target="420" id="424"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Bold" tag="2" keyEquivalent="b" id="390">
|
||||
<connections>
|
||||
<action selector="addFontTrait:" target="420" id="421"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Italic" tag="1" keyEquivalent="i" id="391">
|
||||
<connections>
|
||||
<action selector="addFontTrait:" target="420" id="422"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Underline" keyEquivalent="u" id="392">
|
||||
<connections>
|
||||
<action selector="underline:" target="-1" id="432"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="393"/>
|
||||
<menuItem title="Bigger" tag="3" keyEquivalent="+" id="394">
|
||||
<connections>
|
||||
<action selector="modifyFont:" target="420" id="425"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Smaller" tag="4" keyEquivalent="-" id="395">
|
||||
<connections>
|
||||
<action selector="modifyFont:" target="420" id="423"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="396"/>
|
||||
<menuItem title="Kern" id="397">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Kern" id="415">
|
||||
<items>
|
||||
<menuItem title="Use Default" id="416">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="useStandardKerning:" target="-1" id="438"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Use None" id="417">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="turnOffKerning:" target="-1" id="441"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Tighten" id="418">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="tightenKerning:" target="-1" id="431"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Loosen" id="419">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="loosenKerning:" target="-1" id="435"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Ligatures" id="398">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Ligatures" id="411">
|
||||
<items>
|
||||
<menuItem title="Use Default" id="412">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="useStandardLigatures:" target="-1" id="439"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Use None" id="413">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="turnOffLigatures:" target="-1" id="440"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Use All" id="414">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="useAllLigatures:" target="-1" id="434"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Baseline" id="399">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Baseline" id="405">
|
||||
<items>
|
||||
<menuItem title="Use Default" id="406">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="unscript:" target="-1" id="437"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Superscript" id="407">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="superscript:" target="-1" id="430"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Subscript" id="408">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="subscript:" target="-1" id="429"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Raise" id="409">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="raiseBaseline:" target="-1" id="426"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Lower" id="410">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="lowerBaseline:" target="-1" id="427"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="400"/>
|
||||
<menuItem title="Show Colors" keyEquivalent="C" id="401">
|
||||
<connections>
|
||||
<action selector="orderFrontColorPanel:" target="-1" id="433"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="402"/>
|
||||
<menuItem title="Copy Style" keyEquivalent="c" id="403">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="copyFont:" target="-1" id="428"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Paste Style" keyEquivalent="v" id="404">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="pasteFont:" target="-1" id="436"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Text" id="496">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Text" id="497">
|
||||
<items>
|
||||
<menuItem title="Align Left" keyEquivalent="{" id="498">
|
||||
<connections>
|
||||
<action selector="alignLeft:" target="-1" id="524"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Center" keyEquivalent="|" id="499">
|
||||
<connections>
|
||||
<action selector="alignCenter:" target="-1" id="518"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Justify" id="500">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="alignJustified:" target="-1" id="523"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Align Right" keyEquivalent="}" id="501">
|
||||
<connections>
|
||||
<action selector="alignRight:" target="-1" id="521"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="502"/>
|
||||
<menuItem title="Writing Direction" id="503">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Writing Direction" id="508">
|
||||
<items>
|
||||
<menuItem title="Paragraph" enabled="NO" id="509">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
</menuItem>
|
||||
<menuItem id="510">
|
||||
<string key="title"> Default</string>
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="makeBaseWritingDirectionNatural:" target="-1" id="525"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem id="511">
|
||||
<string key="title"> Left to Right</string>
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="makeBaseWritingDirectionLeftToRight:" target="-1" id="526"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem id="512">
|
||||
<string key="title"> Right to Left</string>
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="makeBaseWritingDirectionRightToLeft:" target="-1" id="527"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="513"/>
|
||||
<menuItem title="Selection" enabled="NO" id="514">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
</menuItem>
|
||||
<menuItem id="515">
|
||||
<string key="title"> Default</string>
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="makeTextWritingDirectionNatural:" target="-1" id="528"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem id="516">
|
||||
<string key="title"> Left to Right</string>
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="makeTextWritingDirectionLeftToRight:" target="-1" id="529"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem id="517">
|
||||
<string key="title"> Right to Left</string>
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="makeTextWritingDirectionRightToLeft:" target="-1" id="530"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="504"/>
|
||||
<menuItem title="Show Ruler" id="505">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleRuler:" target="-1" id="520"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Copy Ruler" keyEquivalent="c" id="506">
|
||||
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="copyRuler:" target="-1" id="522"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Paste Ruler" keyEquivalent="v" id="507">
|
||||
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="pasteRuler:" target="-1" id="519"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="View" id="295">
|
||||
<menu key="submenu" title="View" id="296">
|
||||
<items>
|
||||
<menuItem title="Show Toolbar" keyEquivalent="t" id="297">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="toggleToolbarShown:" target="-1" id="366"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Customize Toolbar…" id="298">
|
||||
<connections>
|
||||
<action selector="runToolbarCustomizationPalette:" target="-1" id="365"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Window" id="19">
|
||||
<menu key="submenu" title="Window" systemMenu="window" id="24">
|
||||
<items>
|
||||
<menuItem title="Minimize" keyEquivalent="m" id="23">
|
||||
<connections>
|
||||
<action selector="performMiniaturize:" target="-1" id="37"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Zoom" id="239">
|
||||
<connections>
|
||||
<action selector="performZoom:" target="-1" id="240"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="92">
|
||||
<modifierMask key="keyEquivalentModifierMask" command="YES"/>
|
||||
</menuItem>
|
||||
<menuItem title="Bring All to Front" id="5">
|
||||
<connections>
|
||||
<action selector="arrangeInFront:" target="-1" id="39"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Help" id="490">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Help" systemMenu="help" id="491">
|
||||
<items>
|
||||
<menuItem title="Notifications for YouTube Help" keyEquivalent="?" id="492">
|
||||
<connections>
|
||||
<action selector="showHelp:" target="-1" id="493"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
<menu title="Status Menu" id="650">
|
||||
<items>
|
||||
<menuItem title="Aktualisieren" keyEquivalent="r" id="651">
|
||||
<connections>
|
||||
<action selector="performRefresh:" target="494" id="748"/>
|
||||
<binding destination="2373" name="enabled2" keyPath="isLoggedIn" previousBinding="762" id="2375">
|
||||
<dictionary key="options">
|
||||
<integer key="NSMultipleValuesPlaceholder" value="-1"/>
|
||||
<integer key="NSNoSelectionPlaceholder" value="-1"/>
|
||||
<integer key="NSNotApplicablePlaceholder" value="-1"/>
|
||||
<integer key="NSNullPlaceholder" value="-1"/>
|
||||
</dictionary>
|
||||
</binding>
|
||||
<binding destination="742" name="enabled" keyPath="refreshing" id="762">
|
||||
<dictionary key="options">
|
||||
<string key="NSValueTransformerName">NSNegateBoolean</string>
|
||||
</dictionary>
|
||||
</binding>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Einstellungen…" id="652">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="showPreferenceWindow:" target="494" id="826"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="YouTube öffnen" id="653">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="browseYouTube:" target="-1" id="735"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="654"/>
|
||||
<menuItem title="Über Benachrichtigungen für YouTube" id="2365">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="showAboutPanel:" target="494" id="2371"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Beenden" keyEquivalent="q" id="655">
|
||||
<connections>
|
||||
<action selector="terminate:" target="-3" id="734"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
<window title="Benachrichtigungen für YouTube" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" showsToolbarButton="NO" visibleAtLaunch="NO" animationBehavior="default" id="1126">
|
||||
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES"/>
|
||||
<windowCollectionBehavior key="collectionBehavior" fullScreenAuxiliary="YES"/>
|
||||
<rect key="contentRect" x="682" y="518" width="567" height="261"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="1680" height="1028"/>
|
||||
<view key="contentView" id="1127">
|
||||
<rect key="frame" x="0.0" y="0.0" width="567" height="261"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="1146">
|
||||
<rect key="frame" x="127" y="92" width="81" height="17"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Anmeldung:" id="1147">
|
||||
<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>
|
||||
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="1157">
|
||||
<rect key="frame" x="212" y="217" width="183" height="26"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="178" id="2337"/>
|
||||
</constraints>
|
||||
<popUpButtonCell key="cell" type="push" title="Alle 10 Minuten" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" tag="600" imageScaling="proportionallyDown" inset="2" selectedItem="1162" id="1158">
|
||||
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
<menu key="menu" title="OtherViews" id="1159">
|
||||
<items>
|
||||
<menuItem title="Jede Minute" tag="60" id="1160"/>
|
||||
<menuItem title="Alle 5 Minuten" tag="300" id="1161"/>
|
||||
<menuItem title="Alle 10 Minuten" state="on" tag="600" id="1162"/>
|
||||
<menuItem title="Alle 20 Minuten" tag="1200" id="1166">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
</menuItem>
|
||||
<menuItem title="Alle 30 Minuten" tag="1800" id="1167">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
</menuItem>
|
||||
<menuItem title="Jede Stunde" tag="3600" id="1168">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
</menuItem>
|
||||
<menuItem title="Alle 2 Stunden" tag="7200" id="1169">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</popUpButtonCell>
|
||||
<connections>
|
||||
<binding destination="2373" name="enabled2" keyPath="isLoggedIn" previousBinding="1913" id="2379">
|
||||
<dictionary key="options">
|
||||
<integer key="NSMultipleValuesPlaceholder" value="-1"/>
|
||||
<integer key="NSNoSelectionPlaceholder" value="-1"/>
|
||||
<integer key="NSNotApplicablePlaceholder" value="-1"/>
|
||||
<integer key="NSNullPlaceholder" value="-1"/>
|
||||
</dictionary>
|
||||
</binding>
|
||||
<binding destination="742" name="enabled" keyPath="autoRefreshEnabled" id="1913"/>
|
||||
<binding destination="742" name="selectedTag" keyPath="autoRefreshInterval" id="1923">
|
||||
<dictionary key="options">
|
||||
<string key="NSValueTransformerName">NumberValueTransformer</string>
|
||||
</dictionary>
|
||||
</binding>
|
||||
</connections>
|
||||
</popUpButton>
|
||||
<button translatesAutoresizingMaskIntoConstraints="NO" id="1184">
|
||||
<rect key="frame" x="18" y="222" width="190" height="18"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="186" id="2367"/>
|
||||
</constraints>
|
||||
<buttonCell key="cell" type="check" title="Automatisch aktualisieren:" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="1185">
|
||||
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<binding destination="2373" name="enabled" keyPath="isLoggedIn" id="2377"/>
|
||||
<binding destination="742" name="value" keyPath="autoRefreshEnabled" id="1910"/>
|
||||
</connections>
|
||||
</button>
|
||||
<box autoresizesSubviews="NO" verticalHuggingPriority="750" title="Box" boxType="separator" titlePosition="noTitle" translatesAutoresizingMaskIntoConstraints="NO" id="1189">
|
||||
<rect key="frame" x="0.0" y="197" width="567" height="5"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<color key="borderColor" white="0.0" alpha="0.41999999999999998" colorSpace="calibratedWhite"/>
|
||||
<color key="fillColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
|
||||
<font key="titleFont" metaFont="system"/>
|
||||
</box>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="1195">
|
||||
<rect key="frame" x="431" y="213" width="122" height="32"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<buttonCell key="cell" type="push" title="Aktualisieren" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="1196">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<action selector="performRefresh:" target="494" id="1852"/>
|
||||
<binding destination="2373" name="enabled2" keyPath="isLoggedIn" previousBinding="1866" id="2381">
|
||||
<dictionary key="options">
|
||||
<integer key="NSMultipleValuesPlaceholder" value="-1"/>
|
||||
<integer key="NSNoSelectionPlaceholder" value="-1"/>
|
||||
<integer key="NSNotApplicablePlaceholder" value="-1"/>
|
||||
<integer key="NSNullPlaceholder" value="-1"/>
|
||||
</dictionary>
|
||||
</binding>
|
||||
<binding destination="742" name="enabled" keyPath="refreshing" id="1866">
|
||||
<dictionary key="options">
|
||||
<string key="NSValueTransformerName">NSNegateBoolean</string>
|
||||
</dictionary>
|
||||
</binding>
|
||||
</connections>
|
||||
</button>
|
||||
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="1220">
|
||||
<rect key="frame" x="125" y="174" width="83" height="17"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Mitteilungen" id="1221">
|
||||
<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>
|
||||
<popUpButton toolTip="Wenn aktiviert, werden für Videos, deren Upload-Datum länger als diese Zeit zurückliegt, keine Mitteilungen gezeigt." verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="1224">
|
||||
<rect key="frame" x="212" y="143" width="214" height="26"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<popUpButtonCell key="cell" type="push" title="Video ist älter als 1 Stunde" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" tag="3600" imageScaling="proportionallyDown" inset="2" selectedItem="1228" id="1225">
|
||||
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
<menu key="menu" title="OtherViews" id="1226">
|
||||
<items>
|
||||
<menuItem title="Alle Mitteilungen anzeigen" id="1227"/>
|
||||
<menuItem title="Video ist älter als 1 Stunde" state="on" tag="3600" id="1228"/>
|
||||
<menuItem title="Video ist älter als 1 Tag" tag="86400" id="1229"/>
|
||||
</items>
|
||||
</menu>
|
||||
</popUpButtonCell>
|
||||
<connections>
|
||||
<binding destination="742" name="selectedTag" keyPath="maximumVideoAge" id="1924">
|
||||
<dictionary key="options">
|
||||
<string key="NSValueTransformerName">NumberValueTransformer</string>
|
||||
</dictionary>
|
||||
</binding>
|
||||
</connections>
|
||||
</popUpButton>
|
||||
<button translatesAutoresizingMaskIntoConstraints="NO" id="1245">
|
||||
<rect key="frame" x="212" y="173" width="213" height="18"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<string key="toolTip">Wenn seit der letzten Aktualisierung mehrere Videos hochgeladen wurden, werden die Mitteilungen zu einer einzigen zusammengefasst.</string>
|
||||
<buttonCell key="cell" type="check" title="Mitteilungen zusammenfassen" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="1246">
|
||||
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<binding destination="742" name="value" keyPath="coalescesNotifications" id="1908"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="1292">
|
||||
<rect key="frame" x="208" y="56" width="117" height="32"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<buttonCell key="cell" type="push" title="Anmelden…" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="1293">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<action selector="login:" target="494" id="1855"/>
|
||||
<binding destination="494" name="enabled" keyPath="loggingIn" id="1870">
|
||||
<dictionary key="options">
|
||||
<string key="NSValueTransformerName">NSNegateBoolean</string>
|
||||
</dictionary>
|
||||
</binding>
|
||||
</connections>
|
||||
</button>
|
||||
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="1319">
|
||||
<rect key="frame" x="212" y="92" width="337" height="17"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" sendsActionOnEndEditing="YES" placeholderString="(nicht angemeldet)" id="1320">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
<connections>
|
||||
<binding destination="2373" name="value" keyPath="loggedInUser.displayName" id="2387">
|
||||
<dictionary key="options">
|
||||
<string key="NSNullPlaceholder">(nicht angemeldet)</string>
|
||||
</dictionary>
|
||||
</binding>
|
||||
</connections>
|
||||
</textField>
|
||||
<progressIndicator horizontalHuggingPriority="750" verticalHuggingPriority="750" maxValue="100" displayedWhenStopped="NO" bezeled="NO" indeterminate="YES" controlSize="small" style="spinning" translatesAutoresizingMaskIntoConstraints="NO" id="1334">
|
||||
<rect key="frame" x="327" y="65" width="16" height="16"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<connections>
|
||||
<binding destination="494" name="animate" keyPath="loggingIn" id="1871"/>
|
||||
</connections>
|
||||
</progressIndicator>
|
||||
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="1337">
|
||||
<rect key="frame" x="343" y="65" width="81" height="17"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Anmelden…" id="1338">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
<connections>
|
||||
<binding destination="494" name="hidden" keyPath="loggingIn" id="1873">
|
||||
<dictionary key="options">
|
||||
<string key="NSValueTransformerName">NSNegateBoolean</string>
|
||||
</dictionary>
|
||||
</binding>
|
||||
</connections>
|
||||
</textField>
|
||||
<button toolTip="Definieren Sie Regeln, für welche Videos Sie Mitteilungen erhalten möchten." verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="1379">
|
||||
<rect key="frame" x="208" y="110" width="168" height="32"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<buttonCell key="cell" type="push" title="Regeln bearbeiten…" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="1380">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<action selector="editRules:" target="494" id="1895"/>
|
||||
<binding destination="2373" name="enabled" keyPath="isLoggedIn" id="2383"/>
|
||||
</connections>
|
||||
</button>
|
||||
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="1542">
|
||||
<rect key="frame" x="39" y="149" width="169" height="17"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Mitteilungen nicht zeigen:" id="1543">
|
||||
<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>
|
||||
<button translatesAutoresizingMaskIntoConstraints="NO" id="1650">
|
||||
<rect key="frame" x="120" y="20" width="171" height="23"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="171" id="1842"/>
|
||||
<constraint firstAttribute="height" constant="23" id="2389"/>
|
||||
</constraints>
|
||||
<buttonCell key="cell" type="square" bezelStyle="shadowlessSquare" image="poweredByYT" imagePosition="only" alignment="center" state="on" imageScaling="proportionallyUpOrDown" inset="2" id="1652">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<action selector="browseYouTube:" target="494" id="1849"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="1184" firstAttribute="leading" secondItem="1127" secondAttribute="leading" constant="20" symbolic="YES" id="1186"/>
|
||||
<constraint firstItem="1184" firstAttribute="baseline" secondItem="1157" secondAttribute="baseline" id="1187"/>
|
||||
<constraint firstItem="1189" firstAttribute="leading" secondItem="1127" secondAttribute="leading" id="1194"/>
|
||||
<constraint firstAttribute="trailing" secondItem="1195" secondAttribute="trailing" constant="20" symbolic="YES" id="1197"/>
|
||||
<constraint firstItem="1220" firstAttribute="baseline" secondItem="1245" secondAttribute="baseline" id="1421"/>
|
||||
<constraint firstItem="1224" firstAttribute="leading" secondItem="1245" secondAttribute="leading" id="1445"/>
|
||||
<constraint firstItem="1379" firstAttribute="top" secondItem="1224" secondAttribute="bottom" constant="8" id="1452"/>
|
||||
<constraint firstItem="1379" firstAttribute="leading" secondItem="1224" secondAttribute="leading" id="1458"/>
|
||||
<constraint firstItem="1319" firstAttribute="leading" secondItem="1379" secondAttribute="leading" id="1475"/>
|
||||
<constraint firstItem="1319" firstAttribute="top" secondItem="1379" secondAttribute="bottom" constant="8" symbolic="YES" id="1476"/>
|
||||
<constraint firstItem="1146" firstAttribute="baseline" secondItem="1319" secondAttribute="baseline" id="1481"/>
|
||||
<constraint firstItem="1292" firstAttribute="top" secondItem="1319" secondAttribute="bottom" constant="8" symbolic="YES" id="1484"/>
|
||||
<constraint firstItem="1292" firstAttribute="leading" secondItem="1319" secondAttribute="leading" id="1485"/>
|
||||
<constraint firstItem="1189" firstAttribute="trailing" secondItem="1127" secondAttribute="trailing" id="1499"/>
|
||||
<constraint firstItem="1337" firstAttribute="leading" secondItem="1334" secondAttribute="trailing" constant="2" id="1519"/>
|
||||
<constraint firstItem="1220" firstAttribute="top" secondItem="1189" secondAttribute="bottom" constant="8" symbolic="YES" id="1534"/>
|
||||
<constraint firstItem="1157" firstAttribute="top" secondItem="1127" secondAttribute="top" constant="20" symbolic="YES" id="1539"/>
|
||||
<constraint firstItem="1195" firstAttribute="top" secondItem="1127" secondAttribute="top" constant="20" symbolic="YES" id="1540"/>
|
||||
<constraint firstItem="1542" firstAttribute="baseline" secondItem="1224" secondAttribute="baseline" id="1545"/>
|
||||
<constraint firstItem="1542" firstAttribute="top" secondItem="1220" secondAttribute="bottom" constant="8" symbolic="YES" id="1546"/>
|
||||
<constraint firstItem="1245" firstAttribute="leading" secondItem="1220" secondAttribute="trailing" constant="8" symbolic="YES" id="1640"/>
|
||||
<constraint firstItem="1224" firstAttribute="leading" secondItem="1542" secondAttribute="trailing" constant="8" symbolic="YES" id="1641"/>
|
||||
<constraint firstItem="1245" firstAttribute="leading" secondItem="1157" secondAttribute="leading" id="1642"/>
|
||||
<constraint firstItem="1319" firstAttribute="leading" secondItem="1146" secondAttribute="trailing" constant="8" symbolic="YES" id="1643"/>
|
||||
<constraint firstItem="1189" firstAttribute="top" secondItem="1157" secondAttribute="bottom" constant="20" id="1649"/>
|
||||
<constraint firstItem="1334" firstAttribute="bottom" secondItem="1337" secondAttribute="bottom" id="1757"/>
|
||||
<constraint firstAttribute="bottom" secondItem="1650" secondAttribute="bottom" constant="20" symbolic="YES" id="1847"/>
|
||||
<constraint firstAttribute="trailing" secondItem="1319" secondAttribute="trailing" constant="20" symbolic="YES" id="1848"/>
|
||||
<constraint firstItem="1650" firstAttribute="leading" secondItem="1127" secondAttribute="leading" constant="120" id="2339"/>
|
||||
<constraint firstItem="1292" firstAttribute="centerY" secondItem="1334" secondAttribute="centerY" id="2354"/>
|
||||
<constraint firstItem="1157" firstAttribute="leading" secondItem="1184" secondAttribute="trailing" constant="8" symbolic="YES" id="2368"/>
|
||||
<constraint firstItem="1334" firstAttribute="leading" secondItem="1292" secondAttribute="trailing" constant="8" symbolic="YES" id="2369"/>
|
||||
<constraint firstItem="1245" firstAttribute="trailing" secondItem="1224" secondAttribute="trailing" id="2413"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</window>
|
||||
<customObject id="494" customClass="NYTAppDelegate">
|
||||
<connections>
|
||||
<outlet property="loginButton" destination="1292" id="1856"/>
|
||||
<outlet property="statusMenu" destination="650" id="656"/>
|
||||
<outlet property="window" destination="1126" id="1857"/>
|
||||
</connections>
|
||||
</customObject>
|
||||
<customObject id="2373" customClass="NYTAuthentication"/>
|
||||
<customObject id="742" customClass="NYTUpdateManager"/>
|
||||
<customObject id="420" customClass="NSFontManager"/>
|
||||
<userDefaultsController representsSharedInstance="YES" id="728"/>
|
||||
</objects>
|
||||
<resources>
|
||||
<image name="poweredByYT" width="578" height="76"/>
|
||||
</resources>
|
||||
</document>
|
||||
434
Notifications for YouTube/de.lproj/NYTRulesWindowController.xib
Executable file
@@ -0,0 +1,434 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="6250" systemVersion="14B25" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="6250"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<customObject id="-2" userLabel="File's Owner" customClass="NYTRulesWindowController">
|
||||
<connections>
|
||||
<outlet property="window" destination="1" id="3"/>
|
||||
</connections>
|
||||
</customObject>
|
||||
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
|
||||
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
|
||||
<window title="Rules" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" oneShot="NO" visibleAtLaunch="NO" animationBehavior="default" id="1">
|
||||
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
|
||||
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
|
||||
<rect key="contentRect" x="196" y="240" width="681" height="375"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="1920" height="1177"/>
|
||||
<value key="minSize" type="size" width="600" height="120"/>
|
||||
<view key="contentView" id="2">
|
||||
<rect key="frame" x="0.0" y="0.0" width="681" height="375"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<splitView dividerStyle="thin" vertical="YES" translatesAutoresizingMaskIntoConstraints="NO" id="364">
|
||||
<rect key="frame" x="0.0" y="0.0" width="681" height="375"/>
|
||||
<subviews>
|
||||
<customView id="365">
|
||||
<rect key="frame" x="0.0" y="0.0" width="217" height="375"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<scrollView autohidesScrollers="YES" horizontalLineScroll="44" horizontalPageScroll="10" verticalLineScroll="44" verticalPageScroll="10" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="191">
|
||||
<rect key="frame" x="0.0" y="50" width="217" height="325"/>
|
||||
<clipView key="contentView" id="AzN-zL-Ezn">
|
||||
<rect key="frame" x="1" y="1" width="215" height="323"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<tableView verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnAutoresizingStyle="lastColumnOnly" selectionHighlightStyle="sourceList" columnReordering="NO" columnSelection="YES" columnResizing="NO" emptySelection="NO" autosaveColumns="NO" rowHeight="42" rowSizeStyle="automatic" viewBased="YES" id="192">
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<size key="intercellSpacing" width="3" height="2"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<tableViewGridLines key="gridStyleMask" horizontal="YES"/>
|
||||
<color key="gridColor" name="gridColor" catalog="System" colorSpace="catalog"/>
|
||||
<tableColumns>
|
||||
<tableColumn editable="NO" width="212" minWidth="40" maxWidth="1000" id="195">
|
||||
<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>
|
||||
<customCell key="dataCell" alignment="left" id="196"/>
|
||||
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
|
||||
<prototypeCellViews>
|
||||
<tableCellView id="721" customClass="ItemCellView">
|
||||
<rect key="frame" x="1" y="1" width="212" height="42"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="751">
|
||||
<rect key="frame" x="47" y="19" width="133" height="17"/>
|
||||
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="RayWilliamJohnson" id="758">
|
||||
<font key="font" metaFont="systemBold"/>
|
||||
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
<connections>
|
||||
<binding destination="721" name="value" keyPath="objectValue.user.displayName" id="770"/>
|
||||
</connections>
|
||||
</textField>
|
||||
<imageView translatesAutoresizingMaskIntoConstraints="NO" id="752">
|
||||
<rect key="frame" x="6" y="3" width="35" height="35"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="35" id="756"/>
|
||||
<constraint firstAttribute="width" constant="35" id="757"/>
|
||||
</constraints>
|
||||
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" id="755"/>
|
||||
<connections>
|
||||
<binding destination="721" name="valueURL" keyPath="objectValue.user.imageURL" id="773"/>
|
||||
</connections>
|
||||
</imageView>
|
||||
<textField verticalHuggingPriority="750" misplaced="YES" translatesAutoresizingMaskIntoConstraints="NO" id="753">
|
||||
<rect key="frame" x="47" y="7" width="104" height="14"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Keine Mitteilungen" id="754">
|
||||
<font key="font" metaFont="smallSystem"/>
|
||||
<color key="textColor" red="0.49364849589999998" green="0.49858498089999997" blue="0.49858498089999997" alpha="1" colorSpace="calibratedRGB"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
<connections>
|
||||
<binding destination="721" name="value" keyPath="objectValue.localizedRestrictionSummary" id="833"/>
|
||||
</connections>
|
||||
</textField>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="753" firstAttribute="leading" secondItem="752" secondAttribute="trailing" constant="8" symbolic="YES" id="759"/>
|
||||
<constraint firstAttribute="bottom" secondItem="752" secondAttribute="bottom" constant="3" id="761"/>
|
||||
<constraint firstItem="751" firstAttribute="leading" secondItem="752" secondAttribute="trailing" constant="8" symbolic="YES" id="762"/>
|
||||
<constraint firstAttribute="bottom" secondItem="753" secondAttribute="bottom" constant="7" id="763"/>
|
||||
<constraint firstItem="751" firstAttribute="top" secondItem="721" secondAttribute="top" constant="6" id="764"/>
|
||||
<constraint firstItem="752" firstAttribute="leading" secondItem="721" secondAttribute="leading" constant="6" id="769"/>
|
||||
</constraints>
|
||||
<connections>
|
||||
<outlet property="detailTextField" destination="753" id="867"/>
|
||||
</connections>
|
||||
</tableCellView>
|
||||
</prototypeCellViews>
|
||||
</tableColumn>
|
||||
</tableColumns>
|
||||
<connections>
|
||||
<binding destination="675" name="content" keyPath="arrangedObjects" id="677"/>
|
||||
<binding destination="675" name="selectionIndexes" keyPath="selectionIndexes" previousBinding="677" id="863"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</clipView>
|
||||
<scroller key="horizontalScroller" hidden="YES" verticalHuggingPriority="750" horizontal="YES" id="193">
|
||||
<rect key="frame" x="1" y="119" width="223" height="15"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</scroller>
|
||||
<scroller key="verticalScroller" hidden="YES" verticalHuggingPriority="750" horizontal="NO" id="194">
|
||||
<rect key="frame" x="224" y="17" width="15" height="102"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</scroller>
|
||||
</scrollView>
|
||||
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="798">
|
||||
<rect key="frame" x="8" y="18" width="48" height="25"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="48" id="815"/>
|
||||
</constraints>
|
||||
<popUpButtonCell key="cell" type="roundTextured" bezelStyle="texturedRounded" alignment="center" lineBreakMode="truncatingTail" state="on" borderStyle="border" imageScaling="proportionallyDown" inset="2" pullsDown="YES" selectedItem="801" id="799">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="menu"/>
|
||||
<menu key="menu" title="OtherViews" id="800">
|
||||
<items>
|
||||
<menuItem state="on" image="NSActionTemplate" hidden="YES" id="801"/>
|
||||
<menuItem title="Alle Mitteilungen einschalten" id="803">
|
||||
<connections>
|
||||
<action selector="enableAllNotifications:" target="-2" id="869"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Alle Mitteilungen ausschalten" id="802">
|
||||
<connections>
|
||||
<action selector="disableAllNotifications:" target="-2" id="868"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</popUpButtonCell>
|
||||
</popUpButton>
|
||||
<searchField wantsLayer="YES" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="816">
|
||||
<rect key="frame" x="64" y="20" width="145" height="22"/>
|
||||
<searchFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" borderStyle="bezel" usesSingleLineMode="YES" bezelStyle="round" sendsSearchStringImmediately="YES" recentsAutosaveName="" id="817">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</searchFieldCell>
|
||||
<connections>
|
||||
<binding destination="675" name="predicate" keyPath="filterPredicate" id="925">
|
||||
<dictionary key="options">
|
||||
<string key="NSDisplayName">predicate</string>
|
||||
<string key="NSPredicateFormat">user.displayName contains[cd] $value</string>
|
||||
</dictionary>
|
||||
</binding>
|
||||
</connections>
|
||||
</searchField>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="191" firstAttribute="leading" secondItem="365" secondAttribute="leading" id="392"/>
|
||||
<constraint firstItem="191" firstAttribute="top" secondItem="365" secondAttribute="top" id="444"/>
|
||||
<constraint firstItem="191" firstAttribute="trailing" secondItem="365" secondAttribute="trailing" id="653"/>
|
||||
<constraint firstAttribute="bottom" secondItem="798" secondAttribute="bottom" constant="20" symbolic="YES" id="809"/>
|
||||
<constraint firstItem="798" firstAttribute="top" secondItem="191" secondAttribute="bottom" constant="8" symbolic="YES" id="814"/>
|
||||
<constraint firstAttribute="bottom" secondItem="816" secondAttribute="bottom" constant="20" symbolic="YES" id="820"/>
|
||||
<constraint firstAttribute="trailing" secondItem="816" secondAttribute="trailing" constant="8" id="909"/>
|
||||
<constraint firstItem="798" firstAttribute="leading" secondItem="365" secondAttribute="leading" constant="8" id="912"/>
|
||||
<constraint firstItem="816" firstAttribute="leading" secondItem="798" secondAttribute="trailing" constant="8" symbolic="YES" id="914"/>
|
||||
</constraints>
|
||||
</customView>
|
||||
<customView id="366">
|
||||
<rect key="frame" x="218" y="0.0" width="463" height="375"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="426">
|
||||
<rect key="frame" x="362" y="13" width="87" height="32"/>
|
||||
<buttonCell key="cell" type="push" title="Sichern" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="427">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<action selector="save:" target="-2" id="829"/>
|
||||
</connections>
|
||||
</button>
|
||||
<scrollView autohidesScrollers="YES" horizontalLineScroll="10" horizontalPageScroll="10" verticalLineScroll="10" verticalPageScroll="10" hasHorizontalScroller="NO" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="449">
|
||||
<rect key="frame" x="20" y="49" width="423" height="258"/>
|
||||
<clipView key="contentView" id="I2z-5M-Jxf">
|
||||
<rect key="frame" x="1" y="1" width="421" height="256"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<predicateEditor verticalHuggingPriority="750" nestingMode="compound" formattingStringsFilename="RulesPredicates" canRemoveAllRows="YES" rowHeight="25" id="450">
|
||||
<rect key="frame" x="0.0" y="0.0" width="421" height="50"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
|
||||
<rowTemplates>
|
||||
<predicateEditorRowTemplate rowType="compound" id="453">
|
||||
<popUpMenus>
|
||||
<menu id="465">
|
||||
<items>
|
||||
<menuItem title="Any" state="on" id="468">
|
||||
<integer key="representedObject" value="2"/>
|
||||
</menuItem>
|
||||
<menuItem title="All" id="469">
|
||||
<integer key="representedObject" value="1"/>
|
||||
</menuItem>
|
||||
<menuItem title="None" id="923">
|
||||
<integer key="representedObject" value="0"/>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
<menu id="466">
|
||||
<items>
|
||||
<menuItem title="of the following are true" state="on" id="467"/>
|
||||
</items>
|
||||
</menu>
|
||||
</popUpMenus>
|
||||
</predicateEditorRowTemplate>
|
||||
<predicateEditorRowTemplate rowType="simple" id="776">
|
||||
<array key="leftExpressionObject">
|
||||
<expression type="keyPath">
|
||||
<string key="keyPath">title</string>
|
||||
</expression>
|
||||
</array>
|
||||
<integer key="rightExpressionObject" value="700"/>
|
||||
<comparisonPredicateOptions key="options" caseInsensitive="YES" diacriticInsensitive="YES"/>
|
||||
<popUpMenus>
|
||||
<menu id="777">
|
||||
<items>
|
||||
<menuItem title="Video Title" state="on" id="778">
|
||||
<expression key="representedObject" type="keyPath">
|
||||
<string key="keyPath">title</string>
|
||||
</expression>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
<menu id="781">
|
||||
<items>
|
||||
<menuItem title="contains" state="on" id="782">
|
||||
<integer key="representedObject" value="99"/>
|
||||
</menuItem>
|
||||
<menuItem title="begins with" id="783">
|
||||
<integer key="representedObject" value="8"/>
|
||||
</menuItem>
|
||||
<menuItem title="ends with" id="784">
|
||||
<integer key="representedObject" value="9"/>
|
||||
</menuItem>
|
||||
<menuItem title="is" id="785">
|
||||
<integer key="representedObject" value="4"/>
|
||||
</menuItem>
|
||||
<menuItem title="is not" id="786">
|
||||
<integer key="representedObject" value="5"/>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</popUpMenus>
|
||||
</predicateEditorRowTemplate>
|
||||
</rowTemplates>
|
||||
<connections>
|
||||
<binding destination="675" name="enabled" keyPath="selection.disableAllNotifications" id="917">
|
||||
<dictionary key="options">
|
||||
<string key="NSValueTransformerName">NSNegateBoolean</string>
|
||||
</dictionary>
|
||||
</binding>
|
||||
<binding destination="675" name="value" keyPath="selection.predicate" id="866"/>
|
||||
</connections>
|
||||
</predicateEditor>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="0.91000002619999998" alpha="1" colorSpace="calibratedWhite"/>
|
||||
</clipView>
|
||||
<scroller key="horizontalScroller" hidden="YES" verticalHuggingPriority="750" horizontal="YES" id="451">
|
||||
<rect key="frame" x="-100" y="-100" width="360" height="15"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</scroller>
|
||||
<scroller key="verticalScroller" hidden="YES" verticalHuggingPriority="750" horizontal="NO" id="452">
|
||||
<rect key="frame" x="406" y="1" width="16" height="173"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</scroller>
|
||||
</scrollView>
|
||||
<button verticalHuggingPriority="750" misplaced="YES" translatesAutoresizingMaskIntoConstraints="NO" id="582">
|
||||
<rect key="frame" x="14" y="13" width="170" height="32"/>
|
||||
<buttonCell key="cell" type="push" title="Kriterien hinzufügen" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="583">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<action selector="addRow:" target="450" id="775"/>
|
||||
<binding destination="675" name="enabled" keyPath="canRemove" id="877"/>
|
||||
<binding destination="675" name="enabled2" keyPath="selection.disableAllNotifications" previousBinding="877" id="921">
|
||||
<dictionary key="options">
|
||||
<integer key="NSMultipleValuesPlaceholder" value="-1"/>
|
||||
<integer key="NSNoSelectionPlaceholder" value="-1"/>
|
||||
<integer key="NSNotApplicablePlaceholder" value="-1"/>
|
||||
<integer key="NSNullPlaceholder" value="-1"/>
|
||||
<string key="NSValueTransformerName">NSNegateBoolean</string>
|
||||
</dictionary>
|
||||
</binding>
|
||||
</connections>
|
||||
</button>
|
||||
<button misplaced="YES" translatesAutoresizingMaskIntoConstraints="NO" id="657">
|
||||
<rect key="frame" x="18" y="339" width="292" height="18"/>
|
||||
<buttonCell key="cell" type="check" title="Keine Mitteilungen für diesen Kanal zeigen" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="658">
|
||||
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<binding destination="675" name="value" keyPath="selection.disableAllNotifications" id="865"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button verticalHuggingPriority="750" misplaced="YES" translatesAutoresizingMaskIntoConstraints="NO" id="825">
|
||||
<rect key="frame" x="254" y="13" width="108" height="32"/>
|
||||
<buttonCell key="cell" type="push" title="Abbrechen" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="826">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
<string key="keyEquivalent" base64-UTF8="YES">
|
||||
Gw
|
||||
</string>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<action selector="cancel:" target="-2" id="831"/>
|
||||
</connections>
|
||||
</button>
|
||||
<matrix verticalHuggingPriority="750" allowsEmptySelection="NO" autorecalculatesCellSize="YES" translatesAutoresizingMaskIntoConstraints="NO" id="879">
|
||||
<rect key="frame" x="20" y="315" width="378" height="18"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="378" id="922"/>
|
||||
</constraints>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
<size key="cellSize" width="176" height="18"/>
|
||||
<size key="intercellSpacing" width="4" height="2"/>
|
||||
<buttonCell key="prototype" type="radio" title="Radio" imagePosition="left" alignment="left" inset="2" id="880">
|
||||
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
<cells>
|
||||
<column>
|
||||
<buttonCell type="radio" title="Mitteilungen zeigen" imagePosition="left" alignment="left" state="on" toolTip="Wenn ausgewählt, werden Mitteilungen nur für Videos gezeigt, auf die folgende Kriterien zutreffen" tag="1" inset="2" id="881">
|
||||
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
</column>
|
||||
<column>
|
||||
<buttonCell type="radio" title="Mitteilungen nicht zeigen" imagePosition="left" alignment="left" toolTip="Wenn ausgewählt, werden keine Mitteilungen für Videos gezeigt, auf die folgende Kriterien zutreffen" inset="2" id="887">
|
||||
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
</column>
|
||||
</cells>
|
||||
<connections>
|
||||
<binding destination="675" name="enabled" keyPath="selection.disableAllNotifications" id="919">
|
||||
<dictionary key="options">
|
||||
<string key="NSValueTransformerName">NSNegateBoolean</string>
|
||||
</dictionary>
|
||||
</binding>
|
||||
<binding destination="675" name="selectedTag" keyPath="selection.positivePredicate" id="907"/>
|
||||
</connections>
|
||||
</matrix>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="bottom" secondItem="426" secondAttribute="bottom" constant="20" symbolic="YES" id="447"/>
|
||||
<constraint firstItem="449" firstAttribute="leading" secondItem="366" secondAttribute="leading" constant="20" symbolic="YES" id="471"/>
|
||||
<constraint firstAttribute="trailing" secondItem="449" secondAttribute="trailing" constant="20" symbolic="YES" id="474"/>
|
||||
<constraint firstItem="426" firstAttribute="top" secondItem="449" secondAttribute="bottom" constant="8" id="475"/>
|
||||
<constraint firstItem="582" firstAttribute="leading" secondItem="366" secondAttribute="leading" constant="20" symbolic="YES" id="585"/>
|
||||
<constraint firstAttribute="bottom" secondItem="582" secondAttribute="bottom" constant="20" symbolic="YES" id="586"/>
|
||||
<constraint firstAttribute="trailing" secondItem="426" secondAttribute="trailing" constant="20" symbolic="YES" id="655"/>
|
||||
<constraint firstItem="657" firstAttribute="top" secondItem="366" secondAttribute="top" constant="20" symbolic="YES" id="659"/>
|
||||
<constraint firstItem="657" firstAttribute="leading" secondItem="366" secondAttribute="leading" constant="20" symbolic="YES" id="660"/>
|
||||
<constraint firstItem="426" firstAttribute="leading" secondItem="825" secondAttribute="trailing" constant="12" symbolic="YES" id="827"/>
|
||||
<constraint firstAttribute="bottom" secondItem="825" secondAttribute="bottom" constant="20" symbolic="YES" id="828"/>
|
||||
<constraint firstItem="879" firstAttribute="top" secondItem="657" secondAttribute="bottom" constant="8" symbolic="YES" id="890"/>
|
||||
<constraint firstItem="879" firstAttribute="leading" secondItem="366" secondAttribute="leading" constant="20" symbolic="YES" id="891"/>
|
||||
<constraint firstItem="449" firstAttribute="top" secondItem="879" secondAttribute="bottom" constant="8" symbolic="YES" id="904"/>
|
||||
</constraints>
|
||||
</customView>
|
||||
</subviews>
|
||||
<holdingPriorities>
|
||||
<real value="250"/>
|
||||
<real value="250"/>
|
||||
</holdingPriorities>
|
||||
</splitView>
|
||||
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="482">
|
||||
<rect key="frame" x="304" y="167" width="74" height="17"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Abos laden" id="483">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
<connections>
|
||||
<binding destination="-2" name="hidden" keyPath="loading" id="575">
|
||||
<dictionary key="options">
|
||||
<string key="NSValueTransformerName">NSNegateBoolean</string>
|
||||
</dictionary>
|
||||
</binding>
|
||||
</connections>
|
||||
</textField>
|
||||
<progressIndicator horizontalHuggingPriority="750" verticalHuggingPriority="750" maxValue="100" displayedWhenStopped="NO" bezeled="NO" indeterminate="YES" style="spinning" translatesAutoresizingMaskIntoConstraints="NO" id="477">
|
||||
<rect key="frame" x="325" y="183" width="32" height="32"/>
|
||||
<connections>
|
||||
<binding destination="-2" name="animate" keyPath="loading" id="573"/>
|
||||
</connections>
|
||||
</progressIndicator>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="364" firstAttribute="trailing" secondItem="2" secondAttribute="trailing" id="371"/>
|
||||
<constraint firstItem="364" firstAttribute="top" secondItem="2" secondAttribute="top" id="372"/>
|
||||
<constraint firstItem="364" firstAttribute="leading" secondItem="2" secondAttribute="leading" id="374"/>
|
||||
<constraint firstItem="364" firstAttribute="bottom" secondItem="2" secondAttribute="bottom" id="448"/>
|
||||
<constraint firstItem="477" firstAttribute="centerX" secondItem="482" secondAttribute="centerX" id="954"/>
|
||||
<constraint firstItem="477" firstAttribute="centerX" secondItem="364" secondAttribute="centerX" id="957"/>
|
||||
<constraint firstItem="477" firstAttribute="top" secondItem="2" secondAttribute="top" constant="160" id="983"/>
|
||||
<constraint firstAttribute="bottom" secondItem="482" secondAttribute="bottom" constant="167" id="984"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="-2" id="4"/>
|
||||
</connections>
|
||||
</window>
|
||||
<arrayController objectClassName="NYTChannelRestriction" editable="NO" selectsInsertedObjects="NO" clearsFilterPredicateOnInsertion="NO" id="675">
|
||||
<connections>
|
||||
<binding destination="-2" name="contentArrayForMultipleSelection" keyPath="selectedSubscriptions" previousBinding="676" id="930"/>
|
||||
<binding destination="-2" name="contentArray" keyPath="subscriptions" id="676"/>
|
||||
</connections>
|
||||
</arrayController>
|
||||
</objects>
|
||||
<resources>
|
||||
<image name="NSActionTemplate" width="14" height="14"/>
|
||||
</resources>
|
||||
</document>
|
||||
16
Notifications for YouTube/de.lproj/RulesPredicates.strings
Executable file
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
RulesPredicates.strings
|
||||
Notifications for YouTube
|
||||
|
||||
Created by Kim Wittenburg on 22.07.13.
|
||||
Copyright (c) 2013 Kim Wittenburg. All rights reserved.
|
||||
*/
|
||||
|
||||
"%[Any]@ of the following are true" = "Entspricht %[einem]@ der folgenden Kriterien";
|
||||
"%[All]@ of the following are true" = "Entspricht %[allen]@ der folgenden Kriterien";
|
||||
"%[None]@ of the following are true" = "Entspricht %[keinem]@ der folgenden Kriterien";
|
||||
"%[Video Title]@ %[contains]@ %@" = "%1$[Videotitel]@ %2$[enthält]@ %3$@";
|
||||
"%[Video Title]@ %[begins with]@ %@" = "%1$[Videotitel]@ %2$[beginnt mit]@ %3$@";
|
||||
"%[Video Title]@ %[ends with]@ %@" = "%1$[Videotitel]@ %2$[endet mit]@ %3$@";
|
||||
"%[Video Title]@ %[is]@ %@" = "%1$[Videotitel]@ %2$[ist]@ %3$@";
|
||||
"%[Video Title]@ %[is not]@ %@" = "%1$[Videotitel]@ %2$[ist nicht]@ %3$@";
|
||||
24
Notifications for YouTube/en.lproj/Credits.rtf
Normal file → Executable file
@@ -1,29 +1,19 @@
|
||||
{\rtf0\ansi{\fonttbl\f0\fswiss Helvetica;}
|
||||
{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390
|
||||
{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
|
||||
{\colortbl;\red255\green255\blue255;}
|
||||
\paperw9840\paperh8400
|
||||
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural
|
||||
\paperw11900\paperh16840\vieww9600\viewh8400\viewkind0
|
||||
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720
|
||||
|
||||
\f0\b\fs24 \cf0 Engineering:
|
||||
\b0 \
|
||||
Some people\
|
||||
Kim Wittenburg\
|
||||
\
|
||||
|
||||
\b Human Interface Design:
|
||||
\b0 \
|
||||
Some other people\
|
||||
Kim Wittenburg\
|
||||
\
|
||||
|
||||
\b Testing:
|
||||
\b0 \
|
||||
Hopefully not nobody\
|
||||
\
|
||||
|
||||
\b Documentation:
|
||||
\b0 \
|
||||
Whoever\
|
||||
\
|
||||
|
||||
\b With special thanks to:
|
||||
\b0 \
|
||||
Mom\
|
||||
}
|
||||
Kim Wittenburg}
|
||||
2
Notifications for YouTube/en.lproj/InfoPlist.strings
Normal file → Executable file
@@ -1,2 +1,4 @@
|
||||
/* Localized versions of Info.plist keys */
|
||||
|
||||
"CFBundleDisplayName" = "Notifications for YouTube";
|
||||
"Notifications for YouTube" = "Notifications for YouTube";
|
||||
BIN
Notifications for YouTube/en.lproj/Localizable.strings
Executable file
5657
Notifications for YouTube/en.lproj/MainMenu.xib
Normal file → Executable file
450
Notifications for YouTube/en.lproj/NYTRulesWindowController.xib
Executable file
@@ -0,0 +1,450 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="5053" systemVersion="13C64" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
|
||||
<dependencies>
|
||||
<deployment defaultVersion="1080" identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="5053"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<customObject id="-2" userLabel="File's Owner" customClass="NYTRulesWindowController">
|
||||
<connections>
|
||||
<outlet property="window" destination="1" id="3"/>
|
||||
</connections>
|
||||
</customObject>
|
||||
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
|
||||
<customObject id="-3" userLabel="Application"/>
|
||||
<window title="Rules" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" oneShot="NO" visibleAtLaunch="NO" animationBehavior="default" id="1">
|
||||
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
|
||||
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
|
||||
<rect key="contentRect" x="196" y="240" width="681" height="322"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="1680" height="1028"/>
|
||||
<value key="minSize" type="size" width="500" height="120"/>
|
||||
<view key="contentView" id="2">
|
||||
<rect key="frame" x="0.0" y="0.0" width="681" height="322"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<splitView dividerStyle="thin" vertical="YES" translatesAutoresizingMaskIntoConstraints="NO" id="364">
|
||||
<rect key="frame" x="0.0" y="0.0" width="681" height="322"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<subviews>
|
||||
<customView id="365">
|
||||
<rect key="frame" x="0.0" y="0.0" width="217" height="322"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<scrollView autohidesScrollers="YES" horizontalLineScroll="44" horizontalPageScroll="10" verticalLineScroll="44" verticalPageScroll="10" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="191">
|
||||
<rect key="frame" x="0.0" y="50" width="217" height="272"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<clipView key="contentView" id="1do-rc-3AA">
|
||||
<rect key="frame" x="1" y="1" width="215" height="270"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<tableView verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnAutoresizingStyle="lastColumnOnly" selectionHighlightStyle="sourceList" columnReordering="NO" columnSelection="YES" columnResizing="NO" emptySelection="NO" autosaveColumns="NO" rowHeight="42" rowSizeStyle="automatic" viewBased="YES" id="192">
|
||||
<rect key="frame" x="0.0" y="0.0" width="215" height="270"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<size key="intercellSpacing" width="3" height="2"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<tableViewGridLines key="gridStyleMask" horizontal="YES"/>
|
||||
<color key="gridColor" name="gridColor" catalog="System" colorSpace="catalog"/>
|
||||
<tableColumns>
|
||||
<tableColumn editable="NO" width="212" minWidth="40" maxWidth="1000" id="195">
|
||||
<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>
|
||||
<customCell key="dataCell" alignment="left" id="196"/>
|
||||
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
|
||||
<prototypeCellViews>
|
||||
<tableCellView id="721" customClass="ItemCellView">
|
||||
<rect key="frame" x="1" y="1" width="212" height="42"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="751">
|
||||
<rect key="frame" x="47" y="19" width="133" height="17"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="RayWilliamJohnson" id="758">
|
||||
<font key="font" metaFont="systemBold"/>
|
||||
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
<connections>
|
||||
<binding destination="721" name="value" keyPath="objectValue.user.displayName" id="770"/>
|
||||
</connections>
|
||||
</textField>
|
||||
<imageView translatesAutoresizingMaskIntoConstraints="NO" id="752">
|
||||
<rect key="frame" x="6" y="3" width="35" height="35"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="35" id="756"/>
|
||||
<constraint firstAttribute="width" constant="35" id="757"/>
|
||||
</constraints>
|
||||
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" id="755"/>
|
||||
<connections>
|
||||
<binding destination="721" name="valueURL" keyPath="objectValue.user.imageURL" id="773"/>
|
||||
</connections>
|
||||
</imageView>
|
||||
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="753">
|
||||
<rect key="frame" x="47" y="7" width="89" height="14"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="No notifications" id="754">
|
||||
<font key="font" metaFont="smallSystem"/>
|
||||
<color key="textColor" red="0.49364849589999998" green="0.49858498089999997" blue="0.49858498089999997" alpha="1" colorSpace="calibratedRGB"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
<connections>
|
||||
<binding destination="721" name="value" keyPath="objectValue.localizedRestrictionSummary" id="833"/>
|
||||
</connections>
|
||||
</textField>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="753" firstAttribute="leading" secondItem="752" secondAttribute="trailing" constant="8" symbolic="YES" id="759"/>
|
||||
<constraint firstAttribute="bottom" secondItem="752" secondAttribute="bottom" constant="3" id="761"/>
|
||||
<constraint firstItem="751" firstAttribute="leading" secondItem="752" secondAttribute="trailing" constant="8" symbolic="YES" id="762"/>
|
||||
<constraint firstAttribute="bottom" secondItem="753" secondAttribute="bottom" constant="7" id="763"/>
|
||||
<constraint firstItem="751" firstAttribute="top" secondItem="721" secondAttribute="top" constant="6" id="764"/>
|
||||
<constraint firstItem="752" firstAttribute="leading" secondItem="721" secondAttribute="leading" constant="6" id="769"/>
|
||||
</constraints>
|
||||
<connections>
|
||||
<outlet property="detailTextField" destination="753" id="867"/>
|
||||
</connections>
|
||||
</tableCellView>
|
||||
</prototypeCellViews>
|
||||
</tableColumn>
|
||||
</tableColumns>
|
||||
<connections>
|
||||
<binding destination="675" name="content" keyPath="arrangedObjects" id="677"/>
|
||||
<binding destination="675" name="selectionIndexes" keyPath="selectionIndexes" previousBinding="677" id="863"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</clipView>
|
||||
<scroller key="horizontalScroller" hidden="YES" verticalHuggingPriority="750" horizontal="YES" id="193">
|
||||
<rect key="frame" x="1" y="119" width="223" height="15"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</scroller>
|
||||
<scroller key="verticalScroller" hidden="YES" verticalHuggingPriority="750" horizontal="NO" id="194">
|
||||
<rect key="frame" x="224" y="17" width="15" height="102"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</scroller>
|
||||
</scrollView>
|
||||
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="798">
|
||||
<rect key="frame" x="8" y="18" width="48" height="25"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="48" id="815"/>
|
||||
</constraints>
|
||||
<popUpButtonCell key="cell" type="roundTextured" bezelStyle="texturedRounded" alignment="center" lineBreakMode="truncatingTail" state="on" borderStyle="border" imageScaling="proportionallyDown" inset="2" pullsDown="YES" id="799">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
<menu key="menu" title="OtherViews" id="800">
|
||||
<items>
|
||||
<menuItem state="on" image="NSActionTemplate" hidden="YES" id="801"/>
|
||||
<menuItem title="Enable all notifications" id="803">
|
||||
<connections>
|
||||
<action selector="enableAllNotifications:" target="-2" id="869"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Disable all notifications" id="802">
|
||||
<connections>
|
||||
<action selector="disableAllNotifications:" target="-2" id="868"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</popUpButtonCell>
|
||||
</popUpButton>
|
||||
<searchField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="816">
|
||||
<rect key="frame" x="64" y="20" width="145" height="22"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<searchFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" borderStyle="bezel" usesSingleLineMode="YES" bezelStyle="round" sendsSearchStringImmediately="YES" recentsAutosaveName="" id="817">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</searchFieldCell>
|
||||
<connections>
|
||||
<binding destination="675" name="predicate" keyPath="filterPredicate" id="930">
|
||||
<dictionary key="options">
|
||||
<string key="NSDisplayName">predicate</string>
|
||||
<string key="NSPredicateFormat">user.displayName contains[cd] $value</string>
|
||||
</dictionary>
|
||||
</binding>
|
||||
</connections>
|
||||
</searchField>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="191" firstAttribute="leading" secondItem="365" secondAttribute="leading" id="392"/>
|
||||
<constraint firstItem="191" firstAttribute="top" secondItem="365" secondAttribute="top" id="444"/>
|
||||
<constraint firstItem="191" firstAttribute="trailing" secondItem="365" secondAttribute="trailing" id="653"/>
|
||||
<constraint firstAttribute="bottom" secondItem="798" secondAttribute="bottom" constant="20" symbolic="YES" id="809"/>
|
||||
<constraint firstItem="798" firstAttribute="top" secondItem="191" secondAttribute="bottom" constant="8" symbolic="YES" id="814"/>
|
||||
<constraint firstAttribute="bottom" secondItem="816" secondAttribute="bottom" constant="20" symbolic="YES" id="820"/>
|
||||
<constraint firstAttribute="trailing" secondItem="816" secondAttribute="trailing" constant="8" id="909"/>
|
||||
<constraint firstItem="798" firstAttribute="leading" secondItem="365" secondAttribute="leading" constant="8" id="912"/>
|
||||
<constraint firstItem="816" firstAttribute="leading" secondItem="798" secondAttribute="trailing" constant="8" symbolic="YES" id="914"/>
|
||||
</constraints>
|
||||
</customView>
|
||||
<customView id="366">
|
||||
<rect key="frame" x="218" y="0.0" width="463" height="322"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="426">
|
||||
<rect key="frame" x="380" y="13" width="69" height="32"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<buttonCell key="cell" type="push" title="Save" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="427">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<action selector="save:" target="-2" id="829"/>
|
||||
</connections>
|
||||
</button>
|
||||
<scrollView autohidesScrollers="YES" horizontalLineScroll="10" horizontalPageScroll="10" verticalLineScroll="10" verticalPageScroll="10" hasHorizontalScroller="NO" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="449">
|
||||
<rect key="frame" x="20" y="49" width="423" height="205"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<clipView key="contentView" id="RNp-br-7fs">
|
||||
<rect key="frame" x="1" y="1" width="421" height="203"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<predicateEditor verticalHuggingPriority="750" nestingMode="compound" formattingStringsFilename="RulesPredicates" canRemoveAllRows="YES" rowHeight="25" id="450">
|
||||
<rect key="frame" x="0.0" y="0.0" width="421" height="203"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
|
||||
<rowTemplates>
|
||||
<predicateEditorRowTemplate rowType="compound" id="453">
|
||||
<popUpMenus>
|
||||
<menu id="465">
|
||||
<items>
|
||||
<menuItem title="Any" state="on" id="468">
|
||||
<integer key="representedObject" value="2"/>
|
||||
</menuItem>
|
||||
<menuItem title="All" id="469">
|
||||
<integer key="representedObject" value="1"/>
|
||||
</menuItem>
|
||||
<menuItem title="None" id="922">
|
||||
<integer key="representedObject" value="0"/>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
<menu id="466">
|
||||
<items>
|
||||
<menuItem title="of the following are true" state="on" id="467"/>
|
||||
</items>
|
||||
</menu>
|
||||
</popUpMenus>
|
||||
</predicateEditorRowTemplate>
|
||||
<predicateEditorRowTemplate rowType="simple" id="776">
|
||||
<array key="leftExpressionObject">
|
||||
<expression type="keyPath">
|
||||
<string key="keyPath">title</string>
|
||||
</expression>
|
||||
</array>
|
||||
<integer key="rightExpressionObject" value="700"/>
|
||||
<comparisonPredicateOptions key="options" caseInsensitive="YES" diacriticInsensitive="YES"/>
|
||||
<popUpMenus>
|
||||
<menu id="777">
|
||||
<items>
|
||||
<menuItem title="Video Title" state="on" id="778">
|
||||
<expression key="representedObject" type="keyPath">
|
||||
<string key="keyPath">title</string>
|
||||
</expression>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
<menu id="781">
|
||||
<items>
|
||||
<menuItem title="contains" state="on" id="782">
|
||||
<integer key="representedObject" value="99"/>
|
||||
</menuItem>
|
||||
<menuItem title="begins with" id="783">
|
||||
<integer key="representedObject" value="8"/>
|
||||
</menuItem>
|
||||
<menuItem title="ends with" id="784">
|
||||
<integer key="representedObject" value="9"/>
|
||||
</menuItem>
|
||||
<menuItem title="is" id="785">
|
||||
<integer key="representedObject" value="4"/>
|
||||
</menuItem>
|
||||
<menuItem title="is not" id="786">
|
||||
<integer key="representedObject" value="5"/>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</popUpMenus>
|
||||
</predicateEditorRowTemplate>
|
||||
</rowTemplates>
|
||||
<connections>
|
||||
<binding destination="675" name="enabled" keyPath="selection.disableAllNotifications" id="917">
|
||||
<dictionary key="options">
|
||||
<string key="NSValueTransformerName">NSNegateBoolean</string>
|
||||
</dictionary>
|
||||
</binding>
|
||||
<binding destination="675" name="value" keyPath="selection.predicate" id="866"/>
|
||||
</connections>
|
||||
</predicateEditor>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="0.91000002619999998" alpha="1" colorSpace="calibratedWhite"/>
|
||||
</clipView>
|
||||
<scroller key="horizontalScroller" hidden="YES" verticalHuggingPriority="750" horizontal="YES" id="451">
|
||||
<rect key="frame" x="-100" y="-100" width="360" height="15"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</scroller>
|
||||
<scroller key="verticalScroller" hidden="YES" verticalHuggingPriority="750" horizontal="NO" id="452">
|
||||
<rect key="frame" x="331" y="1" width="16" height="203"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</scroller>
|
||||
</scrollView>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="582">
|
||||
<rect key="frame" x="14" y="13" width="143" height="32"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<buttonCell key="cell" type="push" title="Add Restrictions" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="583">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<action selector="addRow:" target="450" id="775"/>
|
||||
<binding destination="675" name="enabled2" keyPath="selection.disableAllNotifications" previousBinding="877" id="921">
|
||||
<dictionary key="options">
|
||||
<integer key="NSMultipleValuesPlaceholder" value="-1"/>
|
||||
<integer key="NSNoSelectionPlaceholder" value="-1"/>
|
||||
<integer key="NSNotApplicablePlaceholder" value="-1"/>
|
||||
<integer key="NSNullPlaceholder" value="-1"/>
|
||||
<string key="NSValueTransformerName">NSNegateBoolean</string>
|
||||
</dictionary>
|
||||
</binding>
|
||||
<binding destination="675" name="enabled" keyPath="canRemove" id="877"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button translatesAutoresizingMaskIntoConstraints="NO" id="657">
|
||||
<rect key="frame" x="18" y="286" width="271" height="18"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<buttonCell key="cell" type="check" title="Disable all notifications for this channel" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="658">
|
||||
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<binding destination="675" name="value" keyPath="selection.disableAllNotifications" id="865"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="825">
|
||||
<rect key="frame" x="298" y="13" width="82" height="32"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<buttonCell key="cell" type="push" title="Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="826">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
<string key="keyEquivalent" base64-UTF8="YES">
|
||||
Gw
|
||||
</string>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<action selector="cancel:" target="-2" id="831"/>
|
||||
</connections>
|
||||
</button>
|
||||
<matrix verticalHuggingPriority="750" allowsEmptySelection="NO" autorecalculatesCellSize="YES" translatesAutoresizingMaskIntoConstraints="NO" id="879">
|
||||
<rect key="frame" x="20" y="262" width="320" height="18"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="320" id="905"/>
|
||||
</constraints>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
<size key="cellSize" width="151" height="18"/>
|
||||
<size key="intercellSpacing" width="4" height="2"/>
|
||||
<buttonCell key="prototype" type="radio" title="Radio" imagePosition="left" alignment="left" inset="2" id="880">
|
||||
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
<cells>
|
||||
<column>
|
||||
<buttonCell type="radio" title="Show notifications" imagePosition="left" alignment="left" state="on" toolTip="If selected notifications will only be shown for videos that conform to the following rules" tag="1" inset="2" id="881">
|
||||
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
</column>
|
||||
<column>
|
||||
<buttonCell type="radio" title="Restrict notifications" imagePosition="left" alignment="left" toolTip="If selected there will be no notifications for videos that conform to the following rules" inset="2" id="887">
|
||||
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
</column>
|
||||
</cells>
|
||||
<connections>
|
||||
<binding destination="675" name="enabled" keyPath="selection.disableAllNotifications" id="919">
|
||||
<dictionary key="options">
|
||||
<string key="NSValueTransformerName">NSNegateBoolean</string>
|
||||
</dictionary>
|
||||
</binding>
|
||||
<binding destination="675" name="selectedTag" keyPath="selection.positivePredicate" id="907"/>
|
||||
</connections>
|
||||
</matrix>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="bottom" secondItem="426" secondAttribute="bottom" constant="20" symbolic="YES" id="447"/>
|
||||
<constraint firstItem="449" firstAttribute="leading" secondItem="366" secondAttribute="leading" constant="20" symbolic="YES" id="471"/>
|
||||
<constraint firstAttribute="trailing" secondItem="449" secondAttribute="trailing" constant="20" symbolic="YES" id="474"/>
|
||||
<constraint firstItem="426" firstAttribute="top" secondItem="449" secondAttribute="bottom" constant="8" id="475"/>
|
||||
<constraint firstItem="582" firstAttribute="leading" secondItem="366" secondAttribute="leading" constant="20" symbolic="YES" id="585"/>
|
||||
<constraint firstAttribute="bottom" secondItem="582" secondAttribute="bottom" constant="20" symbolic="YES" id="586"/>
|
||||
<constraint firstAttribute="trailing" secondItem="426" secondAttribute="trailing" constant="20" symbolic="YES" id="655"/>
|
||||
<constraint firstItem="657" firstAttribute="top" secondItem="366" secondAttribute="top" constant="20" symbolic="YES" id="659"/>
|
||||
<constraint firstItem="657" firstAttribute="leading" secondItem="366" secondAttribute="leading" constant="20" symbolic="YES" id="660"/>
|
||||
<constraint firstItem="426" firstAttribute="leading" secondItem="825" secondAttribute="trailing" constant="12" symbolic="YES" id="827"/>
|
||||
<constraint firstAttribute="bottom" secondItem="825" secondAttribute="bottom" constant="20" symbolic="YES" id="828"/>
|
||||
<constraint firstItem="879" firstAttribute="top" secondItem="657" secondAttribute="bottom" constant="8" symbolic="YES" id="890"/>
|
||||
<constraint firstItem="879" firstAttribute="leading" secondItem="366" secondAttribute="leading" constant="20" symbolic="YES" id="891"/>
|
||||
<constraint firstItem="449" firstAttribute="top" secondItem="879" secondAttribute="bottom" constant="8" symbolic="YES" id="904"/>
|
||||
</constraints>
|
||||
</customView>
|
||||
</subviews>
|
||||
<holdingPriorities>
|
||||
<real value="250"/>
|
||||
<real value="250"/>
|
||||
</holdingPriorities>
|
||||
</splitView>
|
||||
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="482">
|
||||
<rect key="frame" x="269" y="139" width="143" height="17"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Loading Subscriptions" id="483">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
<connections>
|
||||
<binding destination="-2" name="hidden" keyPath="loading" id="575">
|
||||
<dictionary key="options">
|
||||
<string key="NSValueTransformerName">NSNegateBoolean</string>
|
||||
</dictionary>
|
||||
</binding>
|
||||
</connections>
|
||||
</textField>
|
||||
<progressIndicator horizontalHuggingPriority="750" verticalHuggingPriority="750" maxValue="100" displayedWhenStopped="NO" bezeled="NO" indeterminate="YES" style="spinning" translatesAutoresizingMaskIntoConstraints="NO" id="477">
|
||||
<rect key="frame" x="325" y="158" width="32" height="32"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<connections>
|
||||
<binding destination="-2" name="animate" keyPath="loading" id="573"/>
|
||||
</connections>
|
||||
</progressIndicator>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="364" firstAttribute="trailing" secondItem="2" secondAttribute="trailing" id="371"/>
|
||||
<constraint firstItem="364" firstAttribute="top" secondItem="2" secondAttribute="top" id="372"/>
|
||||
<constraint firstItem="364" firstAttribute="leading" secondItem="2" secondAttribute="leading" id="374"/>
|
||||
<constraint firstItem="364" firstAttribute="bottom" secondItem="2" secondAttribute="bottom" id="448"/>
|
||||
<constraint firstItem="482" firstAttribute="centerX" secondItem="477" secondAttribute="centerX" id="498"/>
|
||||
<constraint firstItem="477" firstAttribute="top" secondItem="2" secondAttribute="top" constant="132" id="570"/>
|
||||
<constraint firstAttribute="bottom" secondItem="482" secondAttribute="bottom" constant="139" id="572"/>
|
||||
<constraint firstItem="482" firstAttribute="centerX" secondItem="364" secondAttribute="centerX" id="584"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="-2" id="4"/>
|
||||
</connections>
|
||||
</window>
|
||||
<arrayController objectClassName="NYTChannelRestriction" editable="NO" selectsInsertedObjects="NO" clearsFilterPredicateOnInsertion="NO" id="675">
|
||||
<connections>
|
||||
<binding destination="-2" name="contentArray" keyPath="subscriptions" id="676"/>
|
||||
<binding destination="-2" name="contentArrayForMultipleSelection" keyPath="selectedSubscriptions" previousBinding="676" id="935"/>
|
||||
</connections>
|
||||
</arrayController>
|
||||
</objects>
|
||||
<resources>
|
||||
<image name="NSActionTemplate" width="14" height="14"/>
|
||||
</resources>
|
||||
</document>
|
||||
7
Notifications for YouTube/en.lproj/RulesPredicates.strings
Executable file
@@ -0,0 +1,7 @@
|
||||
/*
|
||||
RulesPredicates.strings
|
||||
Notifications for YouTube
|
||||
|
||||
Created by Kim Wittenburg on 22.07.13.
|
||||
Copyright (c) 2013 Kim Wittenburg. All rights reserved.
|
||||
*/
|
||||
2
Notifications for YouTube/main.m
Normal file → Executable file
@@ -8,6 +8,8 @@
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
#import "ISO8601DateFormatter.h"
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
return NSApplicationMain(argc, (const char **)argv);
|
||||
|
||||
BIN
Notifications for YouTube/play-icon-bw.png
Executable file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
Notifications for YouTube/play-icon-bw@2x.png
Executable file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
Notifications for YouTube/play-icon-s.png
Executable file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
Notifications for YouTube/play-icon-s@2x.png
Executable file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
Notifications for YouTube/play-icon.png
Executable file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
Notifications for YouTube/play-icon@2x.png
Executable file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
Notifications for YouTube/poweredByYT.png
Executable file
|
After Width: | Height: | Size: 12 KiB |