1

Archive Project

This commit is contained in:
Kim Wittenburg
2017-07-25 17:21:13 +02:00
parent 7d4f001502
commit 15062dfb27
66 changed files with 14172 additions and 4692 deletions

52
Changelog.rtf Executable file
View File

@@ -0,0 +1,52 @@
{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390
{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
{\colortbl;\red255\green255\blue255;}
\paperw12240\paperh15840\margl1440\margr1440\vieww9000\viewh8400\viewkind0
\pard\tx560\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural
\f0\b\fs24 \cf0 Version 1.0 Beta\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural
\b0 \cf0 - First Release\
\
\pard\tx560\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural
\b \cf0 Version 1.0.1 Beta\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural
\b0 \cf0 - Re-arranged preference window contents\
- Added "Coalesce Notifications" option\
- Added about panel\
\
\pard\tx560\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural
\b \cf0 Version 1.1 Beta
\b0 \
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural
\cf0 - Implemented Rules\
- Added tool tips\
\
\pard\tx560\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural
\b \cf0 Version 1.1.1 Beta
\b0 \
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural
\cf0 - Errors are now displayed when manual refreshing\
- The rules window shows its errors in a sheet now and dismisses itself after displaying those\
\
\pard\tx560\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural
\b \cf0 Version 1.1.2 Beta\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural
\b0 \cf0 - Changed some translations for clarification\
- Added system sleep / wake events to refresh just after the mac wakes up (if automatic refreshing is enabled)\
- Some changes to the code\
- Included "Beta" and "Build" identifier in the about panel\
\
\pard\tx560\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural
\b \cf0 Version 1.1.3 Beta
\b0 \
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural
\cf0 - Notifications now don't display "... uploaded a video" anymore but only the name of the uploader}

View File

@@ -0,0 +1,79 @@
/*ISO8601DateFormatter.h
*
*Created by Peter Hosey on 2009-04-11.
*Copyright 2009 Peter Hosey. All rights reserved.
*/
#import <Foundation/Foundation.h>
/*This class converts dates to and from ISO 8601 strings. A good introduction to ISO 8601: <http://www.cl.cam.ac.uk/~mgk25/iso-time.html>
*
*Parsing can be done strictly, or not. When you parse loosely, leading whitespace is ignored, as is anything after the date.
*The loose parser will return an NSDate for this string: @" \t\r\n\f\t 2006-03-02!!!"
*Leading non-whitespace will not be ignored; the string will be rejected, and nil returned. See the README that came with this addition.
*
*The strict parser will only accept a string if the date is the entire string. The above string would be rejected immediately, solely on these grounds.
*Also, the loose parser provides some extensions that the strict parser doesn't.
*For example, the standard says for "-DDD" (an ordinal date in the implied year) that the logical representation (meaning, hierarchically) would be "--DDD", but because that extra hyphen is "superfluous", it was omitted.
*The loose parser will accept the extra hyphen; the strict parser will not.
*A full list of these extensions is in the README file.
*/
/*The format to either expect or produce.
*Calendar format is YYYY-MM-DD.
*Ordinal format is YYYY-DDD, where DDD ranges from 1 to 366; for example, 2009-32 is 2009-02-01.
*Week format is YYYY-Www-D, where ww ranges from 1 to 53 (the 'W' is literal) and D ranges from 1 to 7; for example, 2009-W05-07.
*/
enum {
ISO8601DateFormatCalendar,
ISO8601DateFormatOrdinal,
ISO8601DateFormatWeek,
};
typedef NSUInteger ISO8601DateFormat;
//The default separator for time values. Currently, this is ':'.
extern unichar ISO8601DefaultTimeSeparatorCharacter;
@interface ISO8601DateFormatter: NSFormatter
{
NSString *lastUsedFormatString;
NSDateFormatter *unparsingFormatter;
NSCalendar *parsingCalendar, *unparsingCalendar;
NSTimeZone *defaultTimeZone;
ISO8601DateFormat format;
unichar timeSeparator;
BOOL includeTime;
BOOL parsesStrictly;
}
//Call this if you get a memory warning.
+ (void) purgeGlobalCaches;
@property(nonatomic, retain) NSTimeZone *defaultTimeZone;
#pragma mark Parsing
//As a formatter, this object converts strings to dates.
@property BOOL parsesStrictly;
- (NSDateComponents *) dateComponentsFromString:(NSString *)string;
- (NSDateComponents *) dateComponentsFromString:(NSString *)string timeZone:(out NSTimeZone **)outTimeZone;
- (NSDateComponents *) dateComponentsFromString:(NSString *)string timeZone:(out NSTimeZone **)outTimeZone range:(out NSRange *)outRange;
- (NSDate *) dateFromString:(NSString *)string;
- (NSDate *) dateFromString:(NSString *)string timeZone:(out NSTimeZone **)outTimeZone;
- (NSDate *) dateFromString:(NSString *)string timeZone:(out NSTimeZone **)outTimeZone range:(out NSRange *)outRange;
#pragma mark Unparsing
@property ISO8601DateFormat format;
@property BOOL includeTime;
@property unichar timeSeparator;
- (NSString *) stringFromDate:(NSDate *)date;
- (NSString *) stringFromDate:(NSDate *)date timeZone:(NSTimeZone *)timeZone;
@end

View File

@@ -0,0 +1,895 @@
/*ISO8601DateFormatter.m
*
*Created by Peter Hosey on 2009-04-11.
*Copyright 2009 Peter Hosey. All rights reserved.
*/
#import <Foundation/Foundation.h>
#import "ISO8601DateFormatter.h"
#ifndef DEFAULT_TIME_SEPARATOR
# define DEFAULT_TIME_SEPARATOR ':'
#endif
unichar ISO8601DefaultTimeSeparatorCharacter = DEFAULT_TIME_SEPARATOR;
//Unicode date formats.
#define ISO_CALENDAR_DATE_FORMAT @"yyyy-MM-dd"
//#define ISO_WEEK_DATE_FORMAT @"YYYY-'W'ww-ee" //Doesn't actually work because NSDateComponents counts the weekday starting at 1.
#define ISO_ORDINAL_DATE_FORMAT @"yyyy-DDD"
#define ISO_TIME_FORMAT @"HH:mm:ss"
#define ISO_TIME_WITH_TIMEZONE_FORMAT ISO_TIME_FORMAT @"Z"
//printf formats.
#define ISO_TIMEZONE_UTC_FORMAT @"Z"
#define ISO_TIMEZONE_OFFSET_FORMAT @"%+.2d%.2d"
@interface ISO8601DateFormatter(UnparsingPrivate)
- (NSString *) replaceColonsInString:(NSString *)timeFormat withTimeSeparator:(unichar)timeSep;
- (NSString *) stringFromDate:(NSDate *)date formatString:(NSString *)dateFormat timeZone:(NSTimeZone *)timeZone;
- (NSString *) weekDateStringForDate:(NSDate *)date timeZone:(NSTimeZone *)timeZone;
@end
static NSMutableDictionary *timeZonesByOffset;
@implementation ISO8601DateFormatter
+ (void) initialize {
if (!timeZonesByOffset) {
timeZonesByOffset = [[NSMutableDictionary alloc] init];
}
}
+ (void) purgeGlobalCaches {
NSMutableDictionary *oldCache = timeZonesByOffset;
timeZonesByOffset = nil;
[oldCache release];
}
- (NSCalendar *) makeCalendarWithDesiredConfiguration {
NSCalendar *calendar = [[[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar] autorelease];
calendar.firstWeekday = 2; //Monday
calendar.timeZone = [NSTimeZone defaultTimeZone];
return calendar;
}
- (id) init {
if ((self = [super init])) {
parsingCalendar = [[self makeCalendarWithDesiredConfiguration] retain];
unparsingCalendar = [[self makeCalendarWithDesiredConfiguration] retain];
format = ISO8601DateFormatCalendar;
timeSeparator = ISO8601DefaultTimeSeparatorCharacter;
includeTime = NO;
parsesStrictly = NO;
}
return self;
}
- (void) dealloc {
[defaultTimeZone release];
[unparsingFormatter release];
[lastUsedFormatString release];
[parsingCalendar release];
[unparsingCalendar release];
[super dealloc];
}
@synthesize defaultTimeZone;
- (void) setDefaultTimeZone:(NSTimeZone *)tz {
if (defaultTimeZone != tz) {
[defaultTimeZone release];
defaultTimeZone = [tz retain];
unparsingCalendar.timeZone = defaultTimeZone;
}
}
//The following properties are only here because GCC doesn't like @synthesize in category implementations.
#pragma mark Parsing
@synthesize parsesStrictly;
static NSUInteger read_segment(const unsigned char *str, const unsigned char **next, NSUInteger *out_num_digits);
static NSUInteger read_segment_4digits(const unsigned char *str, const unsigned char **next, NSUInteger *out_num_digits);
static NSUInteger read_segment_2digits(const unsigned char *str, const unsigned char **next);
static double read_double(const unsigned char *str, const unsigned char **next);
static BOOL is_leap_year(NSUInteger year);
/*Valid ISO 8601 date formats:
*
*YYYYMMDD
*YYYY-MM-DD
*YYYY-MM
*YYYY
*YY //century
* //Implied century: YY is 00-99
* YYMMDD
* YY-MM-DD
* -YYMM
* -YY-MM
* -YY
* //Implied year
* --MMDD
* --MM-DD
* --MM
* //Implied year and month
* ---DD
* //Ordinal dates: DDD is the number of the day in the year (1-366)
*YYYYDDD
*YYYY-DDD
* YYDDD
* YY-DDD
* -DDD
* //Week-based dates: ww is the number of the week, and d is the number (1-7) of the day in the week
*yyyyWwwd
*yyyy-Www-d
*yyyyWww
*yyyy-Www
*yyWwwd
*yy-Www-d
*yyWww
*yy-Www
* //Year of the implied decade
*-yWwwd
*-y-Www-d
*-yWww
*-y-Www
* //Week and day of implied year
* -Wwwd
* -Www-d
* //Week only of implied year
* -Www
* //Day only of implied week
* -W-d
*/
- (NSDateComponents *) dateComponentsFromString:(NSString *)string {
return [self dateComponentsFromString:string timeZone:NULL];
}
- (NSDateComponents *) dateComponentsFromString:(NSString *)string timeZone:(out NSTimeZone **)outTimeZone {
return [self dateComponentsFromString:string timeZone:outTimeZone range:NULL];
}
- (NSDateComponents *) dateComponentsFromString:(NSString *)string timeZone:(out NSTimeZone **)outTimeZone range:(out NSRange *)outRange {
NSDate *now = [NSDate date];
NSDateComponents *components = [[[NSDateComponents alloc] init] autorelease];
NSDateComponents *nowComponents = [parsingCalendar components:(NSYearCalendarUnit | NSMonthCalendarUnit | NSDayCalendarUnit) fromDate:now];
NSUInteger
//Date
year,
month_or_week = 0U,
day = 0U,
//Time
hour = 0U;
NSTimeInterval
minute = 0.0,
second = 0.0;
//Time zone
NSInteger tz_hour = 0;
NSInteger tz_minute = 0;
enum {
monthAndDate,
week,
dateOnly
} dateSpecification = monthAndDate;
BOOL strict = self.parsesStrictly;
unichar timeSep = self.timeSeparator;
if (strict) timeSep = ISO8601DefaultTimeSeparatorCharacter;
NSAssert(timeSep != '\0', @"Time separator must not be NUL.");
BOOL isValidDate = ([string length] > 0U);
NSTimeZone *timeZone = nil;
const unsigned char *ch = (const unsigned char *)[string UTF8String];
NSRange range = { 0U, 0U };
const unsigned char *start_of_date = NULL;
if (strict && isspace(*ch)) {
range.location = NSNotFound;
isValidDate = NO;
} else {
//Skip leading whitespace.
NSUInteger i = 0U;
for(NSUInteger len = strlen((const char *)ch); i < len; ++i) {
if (!isspace(ch[i]))
break;
}
range.location = i;
ch += i;
start_of_date = ch;
NSUInteger segment;
NSUInteger num_leading_hyphens = 0U, num_digits = 0U;
if (*ch == 'T') {
//There is no date here, only a time. Set the date to now; then we'll parse the time.
isValidDate = isdigit(*++ch);
year = nowComponents.year;
month_or_week = nowComponents.month;
day = nowComponents.day;
} else {
while(*ch == '-') {
++num_leading_hyphens;
++ch;
}
segment = read_segment(ch, &ch, &num_digits);
switch(num_digits) {
case 0:
if (*ch == 'W') {
if ((ch[1] == '-') && isdigit(ch[2]) && ((num_leading_hyphens == 1U) || ((num_leading_hyphens == 2U) && !strict))) {
year = nowComponents.year;
month_or_week = 1U;
ch += 2;
goto parseDayAfterWeek;
} else if (num_leading_hyphens == 1U) {
year = nowComponents.year;
goto parseWeekAndDay;
} else
isValidDate = NO;
} else
isValidDate = NO;
break;
case 8: //YYYY MM DD
if (num_leading_hyphens > 0U)
isValidDate = NO;
else {
day = segment % 100U;
segment /= 100U;
month_or_week = segment % 100U;
year = segment / 100U;
}
break;
case 6: //YYMMDD (implicit century)
if (num_leading_hyphens > 0U)
isValidDate = NO;
else {
day = segment % 100U;
segment /= 100U;
month_or_week = segment % 100U;
year = nowComponents.year;
year -= (year % 100U);
year += segment / 100U;
}
break;
case 4:
switch(num_leading_hyphens) {
case 0: //YYYY
year = segment;
if (*ch == '-') ++ch;
if (!isdigit(*ch)) {
if (*ch == 'W')
goto parseWeekAndDay;
else
month_or_week = day = 1U;
} else {
segment = read_segment(ch, &ch, &num_digits);
switch(num_digits) {
case 4: //MMDD
day = segment % 100U;
month_or_week = segment / 100U;
break;
case 2: //MM
month_or_week = segment;
if (*ch == '-') ++ch;
if (!isdigit(*ch))
day = 1U;
else
day = read_segment(ch, &ch, NULL);
break;
case 3: //DDD
day = segment % 1000U;
dateSpecification = dateOnly;
if (strict && (day > (365U + is_leap_year(year))))
isValidDate = NO;
break;
default:
isValidDate = NO;
}
}
break;
case 1: //YYMM
month_or_week = segment % 100U;
year = segment / 100U;
if (*ch == '-') ++ch;
if (!isdigit(*ch))
day = 1U;
else
day = read_segment(ch, &ch, NULL);
break;
case 2: //MMDD
day = segment % 100U;
month_or_week = segment / 100U;
year = nowComponents.year;
break;
default:
isValidDate = NO;
} //switch(num_leading_hyphens) (4 digits)
break;
case 1:
if (strict) {
//Two digits only - never just one.
if (num_leading_hyphens == 1U) {
if (*ch == '-') ++ch;
if (*++ch == 'W') {
year = nowComponents.year;
year -= (year % 10U);
year += segment;
goto parseWeekAndDay;
} else
isValidDate = NO;
} else
isValidDate = NO;
break;
}
case 2:
switch(num_leading_hyphens) {
case 0:
if (*ch == '-') {
//Implicit century
year = nowComponents.year;
year -= (year % 100U);
year += segment;
if (*++ch == 'W')
goto parseWeekAndDay;
else if (!isdigit(*ch)) {
goto centuryOnly;
} else {
//Get month and/or date.
segment = read_segment_4digits(ch, &ch, &num_digits);
NSLog(@"(%@) parsing month; segment is %lu and ch is %s", string, (unsigned long)segment, ch);
switch(num_digits) {
case 4: //YY-MMDD
day = segment % 100U;
month_or_week = segment / 100U;
break;
case 1: //YY-M; YY-M-DD (extension)
if (strict) {
isValidDate = NO;
break;
}
case 2: //YY-MM; YY-MM-DD
month_or_week = segment;
if (*ch == '-') {
if (isdigit(*++ch))
day = read_segment_2digits(ch, &ch);
else
day = 1U;
} else
day = 1U;
break;
case 3: //Ordinal date.
day = segment;
dateSpecification = dateOnly;
break;
}
}
} else if (*ch == 'W') {
year = nowComponents.year;
year -= (year % 100U);
year += segment;
parseWeekAndDay: //*ch should be 'W' here.
if (!isdigit(*++ch)) {
//Not really a week-based date; just a year followed by '-W'.
if (strict)
isValidDate = NO;
else
month_or_week = day = 1U;
} else {
month_or_week = read_segment_2digits(ch, &ch);
if (*ch == '-') ++ch;
parseDayAfterWeek:
day = isdigit(*ch) ? read_segment_2digits(ch, &ch) : 1U;
dateSpecification = week;
}
} else {
//Century only. Assume current year.
centuryOnly:
year = segment * 100U + nowComponents.year % 100U;
month_or_week = day = 1U;
}
break;
case 1:; //-YY; -YY-MM (implicit century)
NSLog(@"(%@) found %lu digits and one hyphen, so this is either -YY or -YY-MM; segment (year) is %lu", string, (unsigned long)num_digits, (unsigned long)segment);
NSUInteger current_year = nowComponents.year;
NSUInteger current_century = (current_year % 100U);
year = segment + (current_year - current_century);
if (num_digits == 1U) //implied decade
year += current_century - (current_year % 10U);
if (*ch == '-') {
++ch;
month_or_week = read_segment_2digits(ch, &ch);
NSLog(@"(%@) month is %lu", string, (unsigned long)month_or_week);
}
day = 1U;
break;
case 2: //--MM; --MM-DD
year = nowComponents.year;
month_or_week = segment;
if (*ch == '-') {
++ch;
day = read_segment_2digits(ch, &ch);
}
break;
case 3: //---DD
year = nowComponents.year;
month_or_week = nowComponents.month;
day = segment;
break;
default:
isValidDate = NO;
} //switch(num_leading_hyphens) (2 digits)
break;
case 7: //YYYY DDD (ordinal date)
if (num_leading_hyphens > 0U)
isValidDate = NO;
else {
day = segment % 1000U;
year = segment / 1000U;
dateSpecification = dateOnly;
if (strict && (day > (365U + is_leap_year(year))))
isValidDate = NO;
}
break;
case 3: //--DDD (ordinal date, implicit year)
//Technically, the standard only allows one hyphen. But it says that two hyphens is the logical implementation, and one was dropped for brevity. So I have chosen to allow the missing hyphen.
if ((num_leading_hyphens < 1U) || ((num_leading_hyphens > 2U) && !strict))
isValidDate = NO;
else {
day = segment;
year = nowComponents.year;
dateSpecification = dateOnly;
if (strict && (day > (365U + is_leap_year(year))))
isValidDate = NO;
}
break;
default:
isValidDate = NO;
}
}
if (isValidDate) {
if (isspace(*ch) || (*ch == 'T')) ++ch;
if (isdigit(*ch)) {
hour = read_segment_2digits(ch, &ch);
if (*ch == timeSep) {
++ch;
if ((timeSep == ',') || (timeSep == '.')) {
//We can't do fractional minutes when '.' is the segment separator.
//Only allow whole minutes and whole seconds.
minute = read_segment_2digits(ch, &ch);
if (*ch == timeSep) {
++ch;
second = read_segment_2digits(ch, &ch);
}
} else {
//Allow a fractional minute.
//If we don't get a fraction, look for a seconds segment.
//Otherwise, the fraction of a minute is the seconds.
minute = read_double(ch, &ch);
second = modf(minute, &minute);
if (second > DBL_EPSILON)
second *= 60.0; //Convert fraction (e.g. .5) into seconds (e.g. 30).
else if (*ch == timeSep) {
++ch;
second = read_double(ch, &ch);
}
}
}
if (!strict) {
if (isspace(*ch)) ++ch;
}
switch(*ch) {
case 'Z':
timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"];
break;
case '+':
case '-':;
BOOL negative = (*ch == '-');
if (isdigit(*++ch)) {
//Read hour offset.
segment = *ch - '0';
if (isdigit(*++ch)) {
segment *= 10U;
segment += *(ch++) - '0';
}
tz_hour = (NSInteger)segment;
if (negative) tz_hour = -tz_hour;
//Optional separator.
if (*ch == timeSep) ++ch;
if (isdigit(*ch)) {
//Read minute offset.
segment = *ch - '0';
if (isdigit(*++ch)) {
segment *= 10U;
segment += *ch - '0';
}
tz_minute = segment;
if (negative) tz_minute = -tz_minute;
}
NSTimeInterval timeZoneOffset = (tz_hour * 3600) + (tz_minute * 60);
NSNumber *offsetNum = [NSNumber numberWithDouble:timeZoneOffset];
timeZone = [timeZonesByOffset objectForKey:offsetNum];
if (!timeZone) {
timeZone = [NSTimeZone timeZoneForSecondsFromGMT:timeZoneOffset];
if (timeZone)
[timeZonesByOffset setObject:timeZone forKey:offsetNum];
}
}
}
}
}
if (isValidDate) {
components.year = year;
components.day = day;
components.hour = hour;
components.minute = (NSInteger)minute;
components.second = (NSInteger)second;
switch(dateSpecification) {
case monthAndDate:
components.month = month_or_week;
break;
case week:;
//Adapted from <http://personal.ecu.edu/mccartyr/ISOwdALG.txt>.
//This works by converting the week date into an ordinal date, then letting the next case handle it.
NSUInteger prevYear = year - 1U;
NSUInteger YY = prevYear % 100U;
NSUInteger C = prevYear - YY;
NSUInteger G = YY + YY / 4U;
NSUInteger isLeapYear = (((C / 100U) % 4U) * 5U);
NSUInteger Jan1Weekday = (isLeapYear + G) % 7U;
enum { monday, tuesday, wednesday, thursday/*, friday, saturday, sunday*/ };
components.day = ((8U - Jan1Weekday) + (7U * (Jan1Weekday > thursday))) + (day - 1U) + (7U * (month_or_week - 2));
case dateOnly: //An "ordinal date".
break;
}
}
} //if (!(strict && isdigit(ch[0])))
if (outRange) {
if (isValidDate)
range.length = ch - start_of_date;
else
range.location = NSNotFound;
*outRange = range;
}
if (outTimeZone) {
*outTimeZone = timeZone;
}
return components;
}
- (NSDate *) dateFromString:(NSString *)string {
return [self dateFromString:string timeZone:NULL];
}
- (NSDate *) dateFromString:(NSString *)string timeZone:(out NSTimeZone **)outTimeZone {
return [self dateFromString:string timeZone:outTimeZone range:NULL];
}
- (NSDate *) dateFromString:(NSString *)string timeZone:(out NSTimeZone **)outTimeZone range:(out NSRange *)outRange {
NSTimeZone *timeZone = nil;
NSDateComponents *components = [self dateComponentsFromString:string timeZone:&timeZone range:outRange];
if (outTimeZone)
*outTimeZone = timeZone;
parsingCalendar.timeZone = timeZone;
return [parsingCalendar dateFromComponents:components];
}
- (BOOL)getObjectValue:(id *)outValue forString:(NSString *)string errorDescription:(NSString **)error {
NSDate *date = [self dateFromString:string];
if (outValue)
*outValue = date;
return (date != nil);
}
#pragma mark Unparsing
@synthesize format;
@synthesize includeTime;
@synthesize timeSeparator;
- (NSString *) replaceColonsInString:(NSString *)timeFormat withTimeSeparator:(unichar)timeSep {
if (timeSep != ':') {
NSMutableString *timeFormatMutable = [[timeFormat mutableCopy] autorelease];
[timeFormatMutable replaceOccurrencesOfString:@":"
withString:[NSString stringWithCharacters:&timeSep length:1U]
options:NSBackwardsSearch | NSLiteralSearch
range:(NSRange){ 0UL, [timeFormat length] }];
timeFormat = timeFormatMutable;
}
return timeFormat;
}
- (NSString *) stringFromDate:(NSDate *)date {
NSTimeZone *timeZone = self.defaultTimeZone;
if (!timeZone) timeZone = [NSTimeZone defaultTimeZone];
return [self stringFromDate:date timeZone:timeZone];
}
- (NSString *) stringFromDate:(NSDate *)date timeZone:(NSTimeZone *)timeZone {
switch (self.format) {
case ISO8601DateFormatCalendar:
return [self stringFromDate:date formatString:ISO_CALENDAR_DATE_FORMAT timeZone:timeZone];
case ISO8601DateFormatWeek:
return [self weekDateStringForDate:date timeZone:timeZone];
case ISO8601DateFormatOrdinal:
return [self stringFromDate:date formatString:ISO_ORDINAL_DATE_FORMAT timeZone:timeZone];
default:
[NSException raise:NSInternalInconsistencyException format:@"self.format was %ld, not calendar (%d), week (%d), or ordinal (%d)", self.format, ISO8601DateFormatCalendar, ISO8601DateFormatWeek, ISO8601DateFormatOrdinal];
return nil;
}
}
- (NSString *) stringFromDate:(NSDate *)date formatString:(NSString *)dateFormat timeZone:(NSTimeZone *)timeZone {
if (includeTime)
dateFormat = [dateFormat stringByAppendingFormat:@"'T'%@", [self replaceColonsInString:ISO_TIME_FORMAT withTimeSeparator:self.timeSeparator]];
unparsingCalendar.timeZone = timeZone;
if (dateFormat != lastUsedFormatString) {
[unparsingFormatter release];
unparsingFormatter = nil;
[lastUsedFormatString release];
lastUsedFormatString = [dateFormat retain];
}
if (!unparsingFormatter) {
unparsingFormatter = [[NSDateFormatter alloc] init];
unparsingFormatter.formatterBehavior = NSDateFormatterBehavior10_4;
unparsingFormatter.dateFormat = dateFormat;
unparsingFormatter.calendar = unparsingCalendar;
}
NSString *str = [unparsingFormatter stringForObjectValue:date];
if (includeTime) {
NSInteger offset = [timeZone secondsFromGMT];
offset /= 60; //bring down to minutes
if (offset == 0)
str = [str stringByAppendingString:ISO_TIMEZONE_UTC_FORMAT];
else
str = [str stringByAppendingFormat:ISO_TIMEZONE_OFFSET_FORMAT, offset / 60, offset % 60];
}
//Undo the change we made earlier
unparsingCalendar.timeZone = self.defaultTimeZone;
return str;
}
- (NSString *) stringForObjectValue:(id)value {
NSParameterAssert([value isKindOfClass:[NSDate class]]);
return [self stringFromDate:(NSDate *)value];
}
/*Adapted from:
* Algorithm for Converting Gregorian Dates to ISO 8601 Week Date
* Rick McCarty, 1999
* http://personal.ecu.edu/mccartyr/ISOwdALG.txt
*/
- (NSString *) weekDateStringForDate:(NSDate *)date timeZone:(NSTimeZone *)timeZone {
unparsingCalendar.timeZone = timeZone;
NSDateComponents *components = [unparsingCalendar components:NSYearCalendarUnit | NSWeekdayCalendarUnit | NSDayCalendarUnit fromDate:date];
//Determine the ordinal date.
NSDateComponents *startOfYearComponents = [unparsingCalendar components:NSYearCalendarUnit fromDate:date];
startOfYearComponents.month = 1;
startOfYearComponents.day = 1;
NSDateComponents *ordinalComponents = [unparsingCalendar components:NSDayCalendarUnit fromDate:[unparsingCalendar dateFromComponents:startOfYearComponents] toDate:date options:0];
ordinalComponents.day += 1;
enum {
monday, tuesday, wednesday, thursday, friday, saturday, sunday
};
enum {
january = 1, february, march,
april, may, june,
july, august, september,
october, november, december
};
NSInteger year = components.year;
NSInteger week = 0;
//The old unparser added 6 to [calendarDate dayOfWeek], which was zero-based; components.weekday is one-based, so we now add only 5.
NSInteger dayOfWeek = (components.weekday + 5) % 7;
NSInteger dayOfYear = ordinalComponents.day;
NSInteger prevYear = year - 1;
BOOL yearIsLeapYear = is_leap_year(year);
BOOL prevYearIsLeapYear = is_leap_year(prevYear);
NSInteger YY = prevYear % 100;
NSInteger C = prevYear - YY;
NSInteger G = YY + YY / 4;
NSInteger Jan1Weekday = (((((C / 100) % 4) * 5) + G) % 7);
NSInteger weekday = ((dayOfYear + Jan1Weekday) - 1) % 7;
if((dayOfYear <= (7 - Jan1Weekday)) && (Jan1Weekday > thursday)) {
week = 52 + ((Jan1Weekday == friday) || ((Jan1Weekday == saturday) && prevYearIsLeapYear));
--year;
} else {
NSInteger lengthOfYear = 365 + yearIsLeapYear;
if((lengthOfYear - dayOfYear) < (thursday - weekday)) {
++year;
week = 1;
} else {
NSInteger J = dayOfYear + (sunday - weekday) + Jan1Weekday;
week = J / 7 - (Jan1Weekday > thursday);
}
}
NSString *timeString;
if(includeTime) {
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
unichar timeSep = self.timeSeparator;
if (!timeSep) timeSep = ISO8601DefaultTimeSeparatorCharacter;
formatter.dateFormat = [self replaceColonsInString:ISO_TIME_WITH_TIMEZONE_FORMAT withTimeSeparator:timeSep];
timeString = [formatter stringForObjectValue:date];
[formatter release];
} else
timeString = @"";
return [NSString stringWithFormat:@"%lu-W%02lu-%02lu%@", (unsigned long)year, (unsigned long)week, ((unsigned long)dayOfWeek) + 1U, timeString];
}
@end
static NSUInteger read_segment(const unsigned char *str, const unsigned char **next, NSUInteger *out_num_digits) {
NSUInteger num_digits = 0U;
NSUInteger value = 0U;
while(isdigit(*str)) {
value *= 10U;
value += *str - '0';
++num_digits;
++str;
}
if (next) *next = str;
if (out_num_digits) *out_num_digits = num_digits;
return value;
}
static NSUInteger read_segment_4digits(const unsigned char *str, const unsigned char **next, NSUInteger *out_num_digits) {
NSUInteger num_digits = 0U;
NSUInteger value = 0U;
if (isdigit(*str)) {
value += *(str++) - '0';
++num_digits;
}
if (isdigit(*str)) {
value *= 10U;
value += *(str++) - '0';
++num_digits;
}
if (isdigit(*str)) {
value *= 10U;
value += *(str++) - '0';
++num_digits;
}
if (isdigit(*str)) {
value *= 10U;
value += *(str++) - '0';
++num_digits;
}
if (next) *next = str;
if (out_num_digits) *out_num_digits = num_digits;
return value;
}
static NSUInteger read_segment_2digits(const unsigned char *str, const unsigned char **next) {
NSUInteger value = 0U;
if (isdigit(*str))
value += *str - '0';
if (isdigit(*++str)) {
value *= 10U;
value += *(str++) - '0';
}
if (next) *next = str;
return value;
}
//strtod doesn't support ',' as a separator. This does.
static double read_double(const unsigned char *str, const unsigned char **next) {
double value = 0.0;
if (str) {
NSUInteger int_value = 0;
while(isdigit(*str)) {
int_value *= 10U;
int_value += (*(str++) - '0');
}
value = int_value;
if (((*str == ',') || (*str == '.'))) {
++str;
register double multiplier, multiplier_multiplier;
multiplier = multiplier_multiplier = 0.1;
while(isdigit(*str)) {
value += (*(str++) - '0') * multiplier;
multiplier *= multiplier_multiplier;
}
}
}
if (next) *next = str;
return value;
}
static BOOL is_leap_year(NSUInteger year) {
return \
((year % 4U) == 0U)
&& (((year % 100U) != 0U)
|| ((year % 400U) == 0U));
}

BIN
Notifications for YouTube.icns Executable file

Binary file not shown.

244
Notifications for YouTube.xcodeproj/project.pbxproj Normal file → Executable file
View File

@@ -7,15 +7,62 @@
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
3B87C5BD179C1ECE008949FF /* NYTRulesWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3B87C5BB179C1ECE008949FF /* NYTRulesWindowController.m */; };
3B87C5C6179C3B2E008949FF /* NYTAuthentication.m in Sources */ = {isa = PBXBuildFile; fileRef = 3B87C5C5179C3B2E008949FF /* NYTAuthentication.m */; };
3B87C5C9179C3C6E008949FF /* NYTUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = 3B87C5C8179C3C6E008949FF /* NYTUtil.m */; };
3B87C5CC179C6F59008949FF /* NYTChannelRestriction.m in Sources */ = {isa = PBXBuildFile; fileRef = 3B87C5CB179C6F59008949FF /* NYTChannelRestriction.m */; };
3B87C5D2179C81F2008949FF /* ItemCellView.m in Sources */ = {isa = PBXBuildFile; fileRef = 3B87C5D1179C81F2008949FF /* ItemCellView.m */; };
3B87C5D3179CAE6F008949FF /* NYTRulesWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 3B87C5D5179CAE6F008949FF /* NYTRulesWindowController.xib */; };
3B87C5D9179CB26E008949FF /* RulesPredicates.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3B87C5DB179CB26E008949FF /* RulesPredicates.strings */; };
3BAF5F6C178C0DAE00087D7C /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3BAF5F6B178C0DAE00087D7C /* Cocoa.framework */; }; 3BAF5F6C178C0DAE00087D7C /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3BAF5F6B178C0DAE00087D7C /* Cocoa.framework */; };
3BAF5F76178C0DAE00087D7C /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3BAF5F74178C0DAE00087D7C /* InfoPlist.strings */; }; 3BAF5F76178C0DAE00087D7C /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3BAF5F74178C0DAE00087D7C /* InfoPlist.strings */; };
3BAF5F78178C0DAE00087D7C /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 3BAF5F77178C0DAE00087D7C /* main.m */; }; 3BAF5F78178C0DAE00087D7C /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 3BAF5F77178C0DAE00087D7C /* main.m */; };
3BAF5F7C178C0DAE00087D7C /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 3BAF5F7A178C0DAE00087D7C /* Credits.rtf */; }; 3BAF5F7C178C0DAE00087D7C /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 3BAF5F7A178C0DAE00087D7C /* Credits.rtf */; };
3BAF5F7F178C0DAE00087D7C /* NYTAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 3BAF5F7E178C0DAE00087D7C /* NYTAppDelegate.m */; }; 3BAF5F7F178C0DAE00087D7C /* NYTAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 3BAF5F7E178C0DAE00087D7C /* NYTAppDelegate.m */; };
3BAF5F82178C0DAE00087D7C /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 3BAF5F80178C0DAE00087D7C /* MainMenu.xib */; }; 3BAF5F82178C0DAE00087D7C /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 3BAF5F80178C0DAE00087D7C /* MainMenu.xib */; };
3BAF5F90178C322B00087D7C /* NYTUser.m in Sources */ = {isa = PBXBuildFile; fileRef = 3BAF5F8F178C322B00087D7C /* NYTUser.m */; };
3BAF5F92178C3CDD00087D7C /* poweredByYT.png in Resources */ = {isa = PBXBuildFile; fileRef = 3BAF5F91178C3CDD00087D7C /* poweredByYT.png */; };
3BAF5F94178C3CEA00087D7C /* Notifications for YouTube.icns in Resources */ = {isa = PBXBuildFile; fileRef = 3BAF5F93178C3CEA00087D7C /* Notifications for YouTube.icns */; };
3BAF5F9D178C53F600087D7C /* NYTUpdateManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 3BAF5F9C178C53F600087D7C /* NYTUpdateManager.m */; };
3BDCBCE517931FA000517427 /* NYTVideo.m in Sources */ = {isa = PBXBuildFile; fileRef = 3BDCBCE417931FA000517427 /* NYTVideo.m */; };
3BE5037B17905259008808D6 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3BE5037A17905259008808D6 /* WebKit.framework */; };
3BE5037D1790525E008808D6 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3BE5037C1790525E008808D6 /* Security.framework */; };
3BE5037F1790527F008808D6 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3BE5037E1790527F008808D6 /* SystemConfiguration.framework */; };
3BE5038D1790B657008808D6 /* GTMHTTPFetcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 3BE503821790B657008808D6 /* GTMHTTPFetcher.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; };
3BE5038E1790B657008808D6 /* GTMHTTPFetchHistory.m in Sources */ = {isa = PBXBuildFile; fileRef = 3BE503841790B657008808D6 /* GTMHTTPFetchHistory.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; };
3BE5038F1790B657008808D6 /* GTMOAuth2Authentication.m in Sources */ = {isa = PBXBuildFile; fileRef = 3BE503861790B657008808D6 /* GTMOAuth2Authentication.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; };
3BE503901790B657008808D6 /* GTMOAuth2SignIn.m in Sources */ = {isa = PBXBuildFile; fileRef = 3BE503881790B657008808D6 /* GTMOAuth2SignIn.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; };
3BE503921790B657008808D6 /* GTMOAuth2WindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3BE5038B1790B657008808D6 /* GTMOAuth2WindowController.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; };
3BE503961790B678008808D6 /* ISO8601DateFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = 3BE503951790B678008808D6 /* ISO8601DateFormatter.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; };
3BE5039C1790C5B2008808D6 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3BE5039A1790C5B2008808D6 /* Localizable.strings */; };
3BE5039E1790C86A008808D6 /* GTMOAuth2Window.xib in Resources */ = {isa = PBXBuildFile; fileRef = 3BE503A01790C86A008808D6 /* GTMOAuth2Window.xib */; };
3BE64577179A13590086DAA5 /* NumberValueTransformer.m in Sources */ = {isa = PBXBuildFile; fileRef = 3BE64576179A13590086DAA5 /* NumberValueTransformer.m */; };
3BFF3CBB1795D90D00ACAF58 /* play-icon.png in Resources */ = {isa = PBXBuildFile; fileRef = 3BFF3CB51795D90D00ACAF58 /* play-icon.png */; };
3BFF3CBC1795D90D00ACAF58 /* play-icon-bw.png in Resources */ = {isa = PBXBuildFile; fileRef = 3BFF3CB61795D90D00ACAF58 /* play-icon-bw.png */; };
3BFF3CBD1795D90D00ACAF58 /* play-icon-bw@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3BFF3CB71795D90D00ACAF58 /* play-icon-bw@2x.png */; };
3BFF3CBE1795D90D00ACAF58 /* play-icon-s.png in Resources */ = {isa = PBXBuildFile; fileRef = 3BFF3CB81795D90D00ACAF58 /* play-icon-s.png */; };
3BFF3CBF1795D90D00ACAF58 /* play-icon-s@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3BFF3CB91795D90D00ACAF58 /* play-icon-s@2x.png */; };
3BFF3CC01795D90D00ACAF58 /* play-icon@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3BFF3CBA1795D90D00ACAF58 /* play-icon@2x.png */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
3B50712317A07EEB00C83EA0 /* Changelog.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; path = Changelog.rtf; sourceTree = "<group>"; };
3B61BD22179B283100FA4B3B /* de */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = de; path = de.lproj/MainMenu.xib; sourceTree = "<group>"; };
3B61BD23179B28C800FA4B3B /* de */ = {isa = PBXFileReference; fileEncoding = 10; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = "<group>"; };
3B87C5BA179C1ECE008949FF /* NYTRulesWindowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NYTRulesWindowController.h; sourceTree = "<group>"; };
3B87C5BB179C1ECE008949FF /* NYTRulesWindowController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NYTRulesWindowController.m; sourceTree = "<group>"; };
3B87C5C4179C3B2E008949FF /* NYTAuthentication.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NYTAuthentication.h; sourceTree = "<group>"; };
3B87C5C5179C3B2E008949FF /* NYTAuthentication.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NYTAuthentication.m; sourceTree = "<group>"; };
3B87C5C7179C3C6E008949FF /* NYTUtil.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NYTUtil.h; sourceTree = "<group>"; };
3B87C5C8179C3C6E008949FF /* NYTUtil.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NYTUtil.m; sourceTree = "<group>"; };
3B87C5CA179C6F59008949FF /* NYTChannelRestriction.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NYTChannelRestriction.h; sourceTree = "<group>"; };
3B87C5CB179C6F59008949FF /* NYTChannelRestriction.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NYTChannelRestriction.m; sourceTree = "<group>"; };
3B87C5D0179C81F2008949FF /* ItemCellView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ItemCellView.h; sourceTree = "<group>"; };
3B87C5D1179C81F2008949FF /* ItemCellView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ItemCellView.m; sourceTree = "<group>"; };
3B87C5D4179CAE6F008949FF /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/NYTRulesWindowController.xib; sourceTree = "<group>"; };
3B87C5D6179CAE75008949FF /* de */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = de; path = de.lproj/NYTRulesWindowController.xib; sourceTree = "<group>"; };
3B87C5DA179CB26E008949FF /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/RulesPredicates.strings; sourceTree = "<group>"; };
3B87C5DC179CB275008949FF /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/RulesPredicates.strings; sourceTree = "<group>"; };
3BAF5F68178C0DAE00087D7C /* Notifications for YouTube.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Notifications for YouTube.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 3BAF5F68178C0DAE00087D7C /* Notifications for YouTube.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Notifications for YouTube.app"; sourceTree = BUILT_PRODUCTS_DIR; };
3BAF5F6B178C0DAE00087D7C /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; 3BAF5F6B178C0DAE00087D7C /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; };
3BAF5F6E178C0DAE00087D7C /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; }; 3BAF5F6E178C0DAE00087D7C /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; };
@@ -27,8 +74,45 @@
3BAF5F79178C0DAE00087D7C /* Notifications for YouTube-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Notifications for YouTube-Prefix.pch"; sourceTree = "<group>"; }; 3BAF5F79178C0DAE00087D7C /* Notifications for YouTube-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Notifications for YouTube-Prefix.pch"; sourceTree = "<group>"; };
3BAF5F7B178C0DAE00087D7C /* en */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; name = en; path = en.lproj/Credits.rtf; sourceTree = "<group>"; }; 3BAF5F7B178C0DAE00087D7C /* en */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; name = en; path = en.lproj/Credits.rtf; sourceTree = "<group>"; };
3BAF5F7D178C0DAE00087D7C /* NYTAppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NYTAppDelegate.h; sourceTree = "<group>"; }; 3BAF5F7D178C0DAE00087D7C /* NYTAppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NYTAppDelegate.h; sourceTree = "<group>"; };
3BAF5F7E178C0DAE00087D7C /* NYTAppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NYTAppDelegate.m; sourceTree = "<group>"; }; 3BAF5F7E178C0DAE00087D7C /* NYTAppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NYTAppDelegate.m; sourceTree = "<group>"; wrapsLines = 1; };
3BAF5F81178C0DAE00087D7C /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/MainMenu.xib; sourceTree = "<group>"; }; 3BAF5F81178C0DAE00087D7C /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/MainMenu.xib; sourceTree = "<group>"; };
3BAF5F8E178C322B00087D7C /* NYTUser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NYTUser.h; sourceTree = "<group>"; };
3BAF5F8F178C322B00087D7C /* NYTUser.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NYTUser.m; sourceTree = "<group>"; };
3BAF5F91178C3CDD00087D7C /* poweredByYT.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = poweredByYT.png; sourceTree = "<group>"; };
3BAF5F93178C3CEA00087D7C /* Notifications for YouTube.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = "Notifications for YouTube.icns"; sourceTree = "<group>"; };
3BAF5F9B178C53F600087D7C /* NYTUpdateManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NYTUpdateManager.h; sourceTree = "<group>"; };
3BAF5F9C178C53F600087D7C /* NYTUpdateManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NYTUpdateManager.m; sourceTree = "<group>"; };
3BDCBCE317931FA000517427 /* NYTVideo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NYTVideo.h; sourceTree = "<group>"; };
3BDCBCE417931FA000517427 /* NYTVideo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NYTVideo.m; sourceTree = "<group>"; };
3BE5037A17905259008808D6 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; };
3BE5037C1790525E008808D6 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; };
3BE5037E1790527F008808D6 /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; };
3BE503811790B657008808D6 /* GTMHTTPFetcher.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GTMHTTPFetcher.h; sourceTree = "<group>"; };
3BE503821790B657008808D6 /* GTMHTTPFetcher.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTMHTTPFetcher.m; sourceTree = "<group>"; };
3BE503831790B657008808D6 /* GTMHTTPFetchHistory.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GTMHTTPFetchHistory.h; sourceTree = "<group>"; };
3BE503841790B657008808D6 /* GTMHTTPFetchHistory.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTMHTTPFetchHistory.m; sourceTree = "<group>"; };
3BE503851790B657008808D6 /* GTMOAuth2Authentication.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GTMOAuth2Authentication.h; sourceTree = "<group>"; };
3BE503861790B657008808D6 /* GTMOAuth2Authentication.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTMOAuth2Authentication.m; sourceTree = "<group>"; };
3BE503871790B657008808D6 /* GTMOAuth2SignIn.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GTMOAuth2SignIn.h; sourceTree = "<group>"; };
3BE503881790B657008808D6 /* GTMOAuth2SignIn.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTMOAuth2SignIn.m; sourceTree = "<group>"; };
3BE5038A1790B657008808D6 /* GTMOAuth2WindowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GTMOAuth2WindowController.h; sourceTree = "<group>"; };
3BE5038B1790B657008808D6 /* GTMOAuth2WindowController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTMOAuth2WindowController.m; sourceTree = "<group>"; };
3BE5038C1790B657008808D6 /* OAuth2.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OAuth2.h; sourceTree = "<group>"; };
3BE503941790B678008808D6 /* ISO8601DateFormatter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ISO8601DateFormatter.h; sourceTree = "<group>"; };
3BE503951790B678008808D6 /* ISO8601DateFormatter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ISO8601DateFormatter.m; sourceTree = "<group>"; };
3BE503981790C401008808D6 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3BE503991790C402008808D6 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; name = de; path = de.lproj/Credits.rtf; sourceTree = "<group>"; };
3BE5039B1790C5B2008808D6 /* en */ = {isa = PBXFileReference; fileEncoding = 10; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
3BE5039F1790C86A008808D6 /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/GTMOAuth2Window.xib; sourceTree = "<group>"; };
3BE503A11790C870008808D6 /* de */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = de; path = de.lproj/GTMOAuth2Window.xib; sourceTree = "<group>"; };
3BE64575179A13590086DAA5 /* NumberValueTransformer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NumberValueTransformer.h; sourceTree = "<group>"; };
3BE64576179A13590086DAA5 /* NumberValueTransformer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NumberValueTransformer.m; sourceTree = "<group>"; };
3BFF3CB51795D90D00ACAF58 /* play-icon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "play-icon.png"; sourceTree = "<group>"; };
3BFF3CB61795D90D00ACAF58 /* play-icon-bw.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "play-icon-bw.png"; sourceTree = "<group>"; };
3BFF3CB71795D90D00ACAF58 /* play-icon-bw@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "play-icon-bw@2x.png"; sourceTree = "<group>"; };
3BFF3CB81795D90D00ACAF58 /* play-icon-s.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "play-icon-s.png"; sourceTree = "<group>"; };
3BFF3CB91795D90D00ACAF58 /* play-icon-s@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "play-icon-s@2x.png"; sourceTree = "<group>"; };
3BFF3CBA1795D90D00ACAF58 /* play-icon@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "play-icon@2x.png"; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@@ -36,6 +120,9 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
3BE5037F1790527F008808D6 /* SystemConfiguration.framework in Frameworks */,
3BE5037D1790525E008808D6 /* Security.framework in Frameworks */,
3BE5037B17905259008808D6 /* WebKit.framework in Frameworks */,
3BAF5F6C178C0DAE00087D7C /* Cocoa.framework in Frameworks */, 3BAF5F6C178C0DAE00087D7C /* Cocoa.framework in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@@ -46,6 +133,10 @@
3BAF5F5F178C0DAE00087D7C = { 3BAF5F5F178C0DAE00087D7C = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
3BAF5F93178C3CEA00087D7C /* Notifications for YouTube.icns */,
3B50712317A07EEB00C83EA0 /* Changelog.rtf */,
3BE503801790B657008808D6 /* OAuth 2 */,
3BE503931790B678008808D6 /* ISO8601DateFormatter */,
3BAF5F71178C0DAE00087D7C /* Notifications for YouTube */, 3BAF5F71178C0DAE00087D7C /* Notifications for YouTube */,
3BAF5F6A178C0DAE00087D7C /* Frameworks */, 3BAF5F6A178C0DAE00087D7C /* Frameworks */,
3BAF5F69178C0DAE00087D7C /* Products */, 3BAF5F69178C0DAE00087D7C /* Products */,
@@ -63,6 +154,9 @@
3BAF5F6A178C0DAE00087D7C /* Frameworks */ = { 3BAF5F6A178C0DAE00087D7C /* Frameworks */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
3BE5037E1790527F008808D6 /* SystemConfiguration.framework */,
3BE5037C1790525E008808D6 /* Security.framework */,
3BE5037A17905259008808D6 /* WebKit.framework */,
3BAF5F6B178C0DAE00087D7C /* Cocoa.framework */, 3BAF5F6B178C0DAE00087D7C /* Cocoa.framework */,
3BAF5F6D178C0DAE00087D7C /* Other Frameworks */, 3BAF5F6D178C0DAE00087D7C /* Other Frameworks */,
); );
@@ -82,9 +176,17 @@
3BAF5F71178C0DAE00087D7C /* Notifications for YouTube */ = { 3BAF5F71178C0DAE00087D7C /* Notifications for YouTube */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
3BE64574179A13310086DAA5 /* Customization */,
3BAF5F9E178C540800087D7C /* Core */,
3BFF3CC11795D91500ACAF58 /* Resources */,
3BAF5F7D178C0DAE00087D7C /* NYTAppDelegate.h */, 3BAF5F7D178C0DAE00087D7C /* NYTAppDelegate.h */,
3BAF5F7E178C0DAE00087D7C /* NYTAppDelegate.m */, 3BAF5F7E178C0DAE00087D7C /* NYTAppDelegate.m */,
3BAF5F80178C0DAE00087D7C /* MainMenu.xib */, 3BAF5F80178C0DAE00087D7C /* MainMenu.xib */,
3B87C5CA179C6F59008949FF /* NYTChannelRestriction.h */,
3B87C5CB179C6F59008949FF /* NYTChannelRestriction.m */,
3B87C5BA179C1ECE008949FF /* NYTRulesWindowController.h */,
3B87C5BB179C1ECE008949FF /* NYTRulesWindowController.m */,
3B87C5D5179CAE6F008949FF /* NYTRulesWindowController.xib */,
3BAF5F72178C0DAE00087D7C /* Supporting Files */, 3BAF5F72178C0DAE00087D7C /* Supporting Files */,
); );
path = "Notifications for YouTube"; path = "Notifications for YouTube";
@@ -102,6 +204,78 @@
name = "Supporting Files"; name = "Supporting Files";
sourceTree = "<group>"; sourceTree = "<group>";
}; };
3BAF5F9E178C540800087D7C /* Core */ = {
isa = PBXGroup;
children = (
3BAF5F8E178C322B00087D7C /* NYTUser.h */,
3BAF5F8F178C322B00087D7C /* NYTUser.m */,
3BDCBCE317931FA000517427 /* NYTVideo.h */,
3BDCBCE417931FA000517427 /* NYTVideo.m */,
3B87C5C7179C3C6E008949FF /* NYTUtil.h */,
3B87C5C8179C3C6E008949FF /* NYTUtil.m */,
3B87C5C4179C3B2E008949FF /* NYTAuthentication.h */,
3B87C5C5179C3B2E008949FF /* NYTAuthentication.m */,
3BAF5F9B178C53F600087D7C /* NYTUpdateManager.h */,
3BAF5F9C178C53F600087D7C /* NYTUpdateManager.m */,
);
name = Core;
sourceTree = "<group>";
};
3BE503801790B657008808D6 /* OAuth 2 */ = {
isa = PBXGroup;
children = (
3BE503811790B657008808D6 /* GTMHTTPFetcher.h */,
3BE503821790B657008808D6 /* GTMHTTPFetcher.m */,
3BE503831790B657008808D6 /* GTMHTTPFetchHistory.h */,
3BE503841790B657008808D6 /* GTMHTTPFetchHistory.m */,
3BE503851790B657008808D6 /* GTMOAuth2Authentication.h */,
3BE503861790B657008808D6 /* GTMOAuth2Authentication.m */,
3BE503871790B657008808D6 /* GTMOAuth2SignIn.h */,
3BE503881790B657008808D6 /* GTMOAuth2SignIn.m */,
3BE503A01790C86A008808D6 /* GTMOAuth2Window.xib */,
3BE5038A1790B657008808D6 /* GTMOAuth2WindowController.h */,
3BE5038B1790B657008808D6 /* GTMOAuth2WindowController.m */,
3BE5038C1790B657008808D6 /* OAuth2.h */,
);
path = "OAuth 2";
sourceTree = "<group>";
};
3BE503931790B678008808D6 /* ISO8601DateFormatter */ = {
isa = PBXGroup;
children = (
3BE503941790B678008808D6 /* ISO8601DateFormatter.h */,
3BE503951790B678008808D6 /* ISO8601DateFormatter.m */,
);
path = ISO8601DateFormatter;
sourceTree = "<group>";
};
3BE64574179A13310086DAA5 /* Customization */ = {
isa = PBXGroup;
children = (
3BE64575179A13590086DAA5 /* NumberValueTransformer.h */,
3BE64576179A13590086DAA5 /* NumberValueTransformer.m */,
3B87C5D0179C81F2008949FF /* ItemCellView.h */,
3B87C5D1179C81F2008949FF /* ItemCellView.m */,
);
name = Customization;
sourceTree = "<group>";
};
3BFF3CC11795D91500ACAF58 /* Resources */ = {
isa = PBXGroup;
children = (
3BAF5F91178C3CDD00087D7C /* poweredByYT.png */,
3BFF3CB51795D90D00ACAF58 /* play-icon.png */,
3BFF3CBA1795D90D00ACAF58 /* play-icon@2x.png */,
3BFF3CB61795D90D00ACAF58 /* play-icon-bw.png */,
3BFF3CB71795D90D00ACAF58 /* play-icon-bw@2x.png */,
3BFF3CB81795D90D00ACAF58 /* play-icon-s.png */,
3BFF3CB91795D90D00ACAF58 /* play-icon-s@2x.png */,
3BE5039A1790C5B2008808D6 /* Localizable.strings */,
3B87C5DB179CB26E008949FF /* RulesPredicates.strings */,
);
name = Resources;
sourceTree = "<group>";
};
/* End PBXGroup section */ /* End PBXGroup section */
/* Begin PBXNativeTarget section */ /* Begin PBXNativeTarget section */
@@ -138,6 +312,7 @@
hasScannedForEncodings = 0; hasScannedForEncodings = 0;
knownRegions = ( knownRegions = (
en, en,
de,
); );
mainGroup = 3BAF5F5F178C0DAE00087D7C; mainGroup = 3BAF5F5F178C0DAE00087D7C;
productRefGroup = 3BAF5F69178C0DAE00087D7C /* Products */; productRefGroup = 3BAF5F69178C0DAE00087D7C /* Products */;
@@ -157,6 +332,18 @@
3BAF5F76178C0DAE00087D7C /* InfoPlist.strings in Resources */, 3BAF5F76178C0DAE00087D7C /* InfoPlist.strings in Resources */,
3BAF5F7C178C0DAE00087D7C /* Credits.rtf in Resources */, 3BAF5F7C178C0DAE00087D7C /* Credits.rtf in Resources */,
3BAF5F82178C0DAE00087D7C /* MainMenu.xib in Resources */, 3BAF5F82178C0DAE00087D7C /* MainMenu.xib in Resources */,
3BAF5F92178C3CDD00087D7C /* poweredByYT.png in Resources */,
3BAF5F94178C3CEA00087D7C /* Notifications for YouTube.icns in Resources */,
3BE5039E1790C86A008808D6 /* GTMOAuth2Window.xib in Resources */,
3BE5039C1790C5B2008808D6 /* Localizable.strings in Resources */,
3BFF3CBB1795D90D00ACAF58 /* play-icon.png in Resources */,
3BFF3CBC1795D90D00ACAF58 /* play-icon-bw.png in Resources */,
3BFF3CBD1795D90D00ACAF58 /* play-icon-bw@2x.png in Resources */,
3BFF3CBE1795D90D00ACAF58 /* play-icon-s.png in Resources */,
3BFF3CBF1795D90D00ACAF58 /* play-icon-s@2x.png in Resources */,
3BFF3CC01795D90D00ACAF58 /* play-icon@2x.png in Resources */,
3B87C5D3179CAE6F008949FF /* NYTRulesWindowController.xib in Resources */,
3B87C5D9179CB26E008949FF /* RulesPredicates.strings in Resources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -169,16 +356,50 @@
files = ( files = (
3BAF5F78178C0DAE00087D7C /* main.m in Sources */, 3BAF5F78178C0DAE00087D7C /* main.m in Sources */,
3BAF5F7F178C0DAE00087D7C /* NYTAppDelegate.m in Sources */, 3BAF5F7F178C0DAE00087D7C /* NYTAppDelegate.m in Sources */,
3BAF5F90178C322B00087D7C /* NYTUser.m in Sources */,
3BAF5F9D178C53F600087D7C /* NYTUpdateManager.m in Sources */,
3BE5038D1790B657008808D6 /* GTMHTTPFetcher.m in Sources */,
3BE5038E1790B657008808D6 /* GTMHTTPFetchHistory.m in Sources */,
3BE5038F1790B657008808D6 /* GTMOAuth2Authentication.m in Sources */,
3BE503901790B657008808D6 /* GTMOAuth2SignIn.m in Sources */,
3BE503921790B657008808D6 /* GTMOAuth2WindowController.m in Sources */,
3BE503961790B678008808D6 /* ISO8601DateFormatter.m in Sources */,
3BDCBCE517931FA000517427 /* NYTVideo.m in Sources */,
3BE64577179A13590086DAA5 /* NumberValueTransformer.m in Sources */,
3B87C5BD179C1ECE008949FF /* NYTRulesWindowController.m in Sources */,
3B87C5C6179C3B2E008949FF /* NYTAuthentication.m in Sources */,
3B87C5C9179C3C6E008949FF /* NYTUtil.m in Sources */,
3B87C5CC179C6F59008949FF /* NYTChannelRestriction.m in Sources */,
3B87C5D2179C81F2008949FF /* ItemCellView.m in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
/* End PBXSourcesBuildPhase section */ /* End PBXSourcesBuildPhase section */
/* Begin PBXVariantGroup section */ /* Begin PBXVariantGroup section */
3B87C5D5179CAE6F008949FF /* NYTRulesWindowController.xib */ = {
isa = PBXVariantGroup;
children = (
3B87C5D4179CAE6F008949FF /* en */,
3B87C5D6179CAE75008949FF /* de */,
);
name = NYTRulesWindowController.xib;
sourceTree = "<group>";
};
3B87C5DB179CB26E008949FF /* RulesPredicates.strings */ = {
isa = PBXVariantGroup;
children = (
3B87C5DA179CB26E008949FF /* en */,
3B87C5DC179CB275008949FF /* de */,
);
name = RulesPredicates.strings;
sourceTree = "<group>";
};
3BAF5F74178C0DAE00087D7C /* InfoPlist.strings */ = { 3BAF5F74178C0DAE00087D7C /* InfoPlist.strings */ = {
isa = PBXVariantGroup; isa = PBXVariantGroup;
children = ( children = (
3BAF5F75178C0DAE00087D7C /* en */, 3BAF5F75178C0DAE00087D7C /* en */,
3BE503981790C401008808D6 /* de */,
); );
name = InfoPlist.strings; name = InfoPlist.strings;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -187,6 +408,7 @@
isa = PBXVariantGroup; isa = PBXVariantGroup;
children = ( children = (
3BAF5F7B178C0DAE00087D7C /* en */, 3BAF5F7B178C0DAE00087D7C /* en */,
3BE503991790C402008808D6 /* de */,
); );
name = Credits.rtf; name = Credits.rtf;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -195,10 +417,29 @@
isa = PBXVariantGroup; isa = PBXVariantGroup;
children = ( children = (
3BAF5F81178C0DAE00087D7C /* en */, 3BAF5F81178C0DAE00087D7C /* en */,
3B61BD22179B283100FA4B3B /* de */,
); );
name = MainMenu.xib; name = MainMenu.xib;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
3BE5039A1790C5B2008808D6 /* Localizable.strings */ = {
isa = PBXVariantGroup;
children = (
3BE5039B1790C5B2008808D6 /* en */,
3B61BD23179B28C800FA4B3B /* de */,
);
name = Localizable.strings;
sourceTree = "<group>";
};
3BE503A01790C86A008808D6 /* GTMOAuth2Window.xib */ = {
isa = PBXVariantGroup;
children = (
3BE5039F1790C86A008808D6 /* en */,
3BE503A11790C870008808D6 /* de */,
);
name = GTMOAuth2Window.xib;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */ /* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */ /* Begin XCBuildConfiguration section */
@@ -304,6 +545,7 @@
3BAF5F87178C0DAE00087D7C /* Release */, 3BAF5F87178C0DAE00087D7C /* Release */,
); );
defaultConfigurationIsVisible = 0; defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
}; };
/* End XCConfigurationList section */ /* End XCConfigurationList section */
}; };

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:Notifications for YouTube.xcodeproj">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>HasAskedToTakeAutomaticSnapshotBeforeSignificantChanges</key>
<true/>
<key>SnapshotAutomaticallyBeforeSignificantChanges</key>
<false/>
</dict>
</plist>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<Bucket
type = "1"
version = "1.0">
</Bucket>

View File

@@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0460"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "3BAF5F67178C0DAE00087D7C"
BuildableName = "Notifications for YouTube.app"
BlueprintName = "Notifications for YouTube"
ReferencedContainer = "container:Notifications for YouTube.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
buildConfiguration = "Debug">
<Testables>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "3BAF5F67178C0DAE00087D7C"
BuildableName = "Notifications for YouTube.app"
BlueprintName = "Notifications for YouTube"
ReferencedContainer = "container:Notifications for YouTube.xcodeproj">
</BuildableReference>
</MacroExpansion>
</TestAction>
<LaunchAction
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
buildConfiguration = "Debug"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
allowLocationSimulation = "YES">
<BuildableProductRunnable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "3BAF5F67178C0DAE00087D7C"
BuildableName = "Notifications for YouTube.app"
BlueprintName = "Notifications for YouTube"
ReferencedContainer = "container:Notifications for YouTube.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
buildConfiguration = "Release"
debugDocumentVersioning = "YES">
<BuildableProductRunnable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "3BAF5F67178C0DAE00087D7C"
BuildableName = "Notifications for YouTube.app"
BlueprintName = "Notifications for YouTube"
ReferencedContainer = "container:Notifications for YouTube.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>SchemeUserState</key>
<dict>
<key>Notifications for YouTube.xcscheme</key>
<dict>
<key>orderHint</key>
<integer>0</integer>
</dict>
</dict>
<key>SuppressBuildableAutocreation</key>
<dict>
<key>3BAF5F67178C0DAE00087D7C</key>
<dict>
<key>primary</key>
<true/>
</dict>
</dict>
</dict>
</plist>

View 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

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

@@ -6,10 +6,58 @@
// Copyright (c) 2013 Kim Wittenburg. All rights reserved. // 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 @end

256
Notifications for YouTube/NYTAppDelegate.m Normal file → Executable file
View File

@@ -8,11 +8,263 @@
#import "NYTAppDelegate.h" #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 - (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 @end

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

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

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

View 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

View 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

View File

@@ -4,10 +4,12 @@
<dict> <dict>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>en</string> <string>en</string>
<key>CFBundleDisplayName</key>
<string>Notifications for YouTube</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>${EXECUTABLE_NAME}</string> <string>${EXECUTABLE_NAME}</string>
<key>CFBundleIconFile</key> <key>CFBundleIconFile</key>
<string></string> <string>Notifications for YouTube</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
<string>wittenburg.kim.${PRODUCT_NAME:rfc1034identifier}</string> <string>wittenburg.kim.${PRODUCT_NAME:rfc1034identifier}</string>
<key>CFBundleInfoDictionaryVersion</key> <key>CFBundleInfoDictionaryVersion</key>
@@ -17,15 +19,19 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.0</string> <string>1.1.3 Beta</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1</string> <string>Build 42</string>
<key>LSApplicationCategoryType</key> <key>LSApplicationCategoryType</key>
<string>public.app-category.entertainment</string> <string>public.app-category.entertainment</string>
<key>LSHasLocalizedDisplayName</key>
<true/>
<key>LSMinimumSystemVersion</key> <key>LSMinimumSystemVersion</key>
<string>${MACOSX_DEPLOYMENT_TARGET}</string> <string>${MACOSX_DEPLOYMENT_TARGET}</string>
<key>LSUIElement</key>
<true/>
<key>NSHumanReadableCopyright</key> <key>NSHumanReadableCopyright</key>
<string>Copyright © 2013 Kim Wittenburg. All rights reserved.</string> <string>Copyright © 2013 Kim Wittenburg. All rights reserved.</string>
<key>NSMainNibFile</key> <key>NSMainNibFile</key>

View File

View 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

View 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

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

View File

@@ -0,0 +1,4 @@
/* Localized versions of Info.plist keys */
"CFBundleDisplayName" = "Benachrichtigungen für YouTube";
"Notifications for YouTube" = "Benachrichtigungen für YouTube";

Binary file not shown.

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

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

View 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
View 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;} {\colortbl;\red255\green255\blue255;}
\paperw9840\paperh8400 \paperw11900\paperh16840\vieww9600\viewh8400\viewkind0
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720
\f0\b\fs24 \cf0 Engineering: \f0\b\fs24 \cf0 Engineering:
\b0 \ \b0 \
Some people\ Kim Wittenburg\
\ \
\b Human Interface Design: \b Human Interface Design:
\b0 \ \b0 \
Some other people\ Kim Wittenburg\
\ \
\b Testing: \b Testing:
\b0 \ \b0 \
Hopefully not nobody\ Kim Wittenburg}
\
\b Documentation:
\b0 \
Whoever\
\
\b With special thanks to:
\b0 \
Mom\
}

2
Notifications for YouTube/en.lproj/InfoPlist.strings Normal file → Executable file
View File

@@ -1,2 +1,4 @@
/* Localized versions of Info.plist keys */ /* Localized versions of Info.plist keys */
"CFBundleDisplayName" = "Notifications for YouTube";
"Notifications for YouTube" = "Notifications for YouTube";

Binary file not shown.

5657
Notifications for YouTube/en.lproj/MainMenu.xib Normal file → Executable file

File diff suppressed because it is too large Load Diff

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

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

@@ -8,6 +8,8 @@
#import <Cocoa/Cocoa.h> #import <Cocoa/Cocoa.h>
#import "ISO8601DateFormatter.h"
int main(int argc, char *argv[]) int main(int argc, char *argv[])
{ {
return NSApplicationMain(argc, (const char **)argv); return NSApplicationMain(argc, (const char **)argv);

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

187
OAuth 2/GTMHTTPFetchHistory.h Executable file
View File

@@ -0,0 +1,187 @@
/* Copyright (c) 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
//
// GTMHTTPFetchHistory.h
//
//
// Users of the GTMHTTPFetcher class may optionally create and set a fetch
// history object. The fetch history provides "memory" between subsequent
// fetches, including:
//
// - For fetch responses with Etag headers, the fetch history
// remembers the response headers. Future fetcher requests to the same URL
// will be given an "If-None-Match" header, telling the server to return
// a 304 Not Modified status if the response is unchanged, reducing the
// server load and network traffic.
//
// - Optionally, the fetch history can cache the ETagged data that was returned
// in the responses that contained Etag headers. If a later fetch
// results in a 304 status, the fetcher will return the cached ETagged data
// to the client along with a 200 status, hiding the 304.
//
// - The fetch history can track cookies.
//
#pragma once
#import <Foundation/Foundation.h>
#import "GTMHTTPFetcher.h"
#undef _EXTERN
#undef _INITIALIZE_AS
#ifdef GTMHTTPFETCHHISTORY_DEFINE_GLOBALS
#define _EXTERN
#define _INITIALIZE_AS(x) =x
#else
#if defined(__cplusplus)
#define _EXTERN extern "C"
#else
#define _EXTERN extern
#endif
#define _INITIALIZE_AS(x)
#endif
// default data cache size for when we're caching responses to handle "not
// modified" errors for the client
#if GTM_IPHONE
// iPhone: up to 1MB memory
_EXTERN const NSUInteger kGTMDefaultETaggedDataCacheMemoryCapacity _INITIALIZE_AS(1*1024*1024);
#else
// Mac OS X: up to 15MB memory
_EXTERN const NSUInteger kGTMDefaultETaggedDataCacheMemoryCapacity _INITIALIZE_AS(15*1024*1024);
#endif
// forward declarations
@class GTMURLCache;
@class GTMCookieStorage;
@interface GTMHTTPFetchHistory : NSObject <GTMHTTPFetchHistoryProtocol> {
@private
GTMURLCache *etaggedDataCache_;
BOOL shouldRememberETags_;
BOOL shouldCacheETaggedData_; // if NO, then only headers are cached
GTMCookieStorage *cookieStorage_;
}
// With caching enabled, previously-cached data will be returned instead of
// 304 Not Modified responses when repeating a fetch of an URL that previously
// included an ETag header in its response
@property (assign) BOOL shouldRememberETags; // default: NO
@property (assign) BOOL shouldCacheETaggedData; // default: NO
// the default ETag data cache capacity is kGTMDefaultETaggedDataCacheMemoryCapacity
@property (assign) NSUInteger memoryCapacity;
@property (retain) GTMCookieStorage *cookieStorage;
- (id)initWithMemoryCapacity:(NSUInteger)totalBytes
shouldCacheETaggedData:(BOOL)shouldCacheETaggedData;
- (void)updateRequest:(NSMutableURLRequest *)request isHTTPGet:(BOOL)isHTTPGet;
- (void)clearETaggedDataCache;
- (void)clearHistory;
- (void)removeAllCookies;
@end
// GTMURLCache and GTMCachedURLResponse have interfaces similar to their
// NSURLCache counterparts, in hopes that someday the NSURLCache versions
// can be used. But in 10.5.8, those are not reliable enough except when
// used with +setSharedURLCache. Our goal here is just to cache
// responses for handling If-None-Match requests that return
// "Not Modified" responses, not for replacing the general URL
// caches.
@interface GTMCachedURLResponse : NSObject {
@private
NSURLResponse *response_;
NSData *data_;
NSDate *useDate_; // date this response was last saved or used
NSDate *reservationDate_; // date this response's ETag was used
}
@property (readonly) NSURLResponse* response;
@property (readonly) NSData* data;
// date the response was saved or last accessed
@property (retain) NSDate *useDate;
// date the response's ETag header was last used for a fetch request
@property (retain) NSDate *reservationDate;
- (id)initWithResponse:(NSURLResponse *)response data:(NSData *)data;
@end
@interface GTMURLCache : NSObject {
NSMutableDictionary *responses_; // maps request URL to GTMCachedURLResponse
NSUInteger memoryCapacity_; // capacity of NSDatas in the responses
NSUInteger totalDataSize_; // sum of sizes of NSDatas of all responses
NSTimeInterval reservationInterval_; // reservation expiration interval
}
@property (assign) NSUInteger memoryCapacity;
- (id)initWithMemoryCapacity:(NSUInteger)totalBytes;
- (GTMCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request;
- (void)storeCachedResponse:(GTMCachedURLResponse *)cachedResponse forRequest:(NSURLRequest *)request;
- (void)removeCachedResponseForRequest:(NSURLRequest *)request;
- (void)removeAllCachedResponses;
// for unit testing
- (void)setReservationInterval:(NSTimeInterval)secs;
- (NSDictionary *)responses;
- (NSUInteger)totalDataSize;
@end
@interface GTMCookieStorage : NSObject <GTMCookieStorageProtocol> {
@private
// The cookie storage object manages an array holding cookies, but the array
// is allocated externally (it may be in a fetcher object or the static
// fetcher cookie array.) See the fetcher's setCookieStorageMethod:
// for allocation of this object and assignment of its cookies array.
NSMutableArray *cookies_;
}
// add all NSHTTPCookies in the supplied array to the storage array,
// replacing cookies in the storage array as appropriate
// Side effect: removes expired cookies from the storage array
- (void)setCookies:(NSArray *)newCookies;
// retrieve all cookies appropriate for the given URL, considering
// domain, path, cookie name, expiration, security setting.
// Side effect: removes expired cookies from the storage array
- (NSArray *)cookiesForURL:(NSURL *)theURL;
// return a cookie with the same name, domain, and path as the
// given cookie, or else return nil if none found
//
// Both the cookie being tested and all stored cookies should
// be valid (non-nil name, domains, paths)
- (NSHTTPCookie *)cookieMatchingCookie:(NSHTTPCookie *)cookie;
// remove any expired cookies, excluding cookies with nil expirations
- (void)removeExpiredCookies;
- (void)removeAllCookies;
@end

590
OAuth 2/GTMHTTPFetchHistory.m Executable file
View File

@@ -0,0 +1,590 @@
/* Copyright (c) 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
//
// GTMHTTPFetchHistory.m
//
#define GTMHTTPFETCHHISTORY_DEFINE_GLOBALS 1
#import "GTMHTTPFetchHistory.h"
const NSTimeInterval kCachedURLReservationInterval = 60.0; // 1 minute
static NSString* const kGTMIfNoneMatchHeader = @"If-None-Match";
static NSString* const kGTMETagHeader = @"Etag";
@implementation GTMCookieStorage
- (id)init {
self = [super init];
if (self != nil) {
cookies_ = [[NSMutableArray alloc] init];
}
return self;
}
- (void)dealloc {
[cookies_ release];
[super dealloc];
}
// add all cookies in the new cookie array to the storage,
// replacing stored cookies as appropriate
//
// Side effect: removes expired cookies from the storage array
- (void)setCookies:(NSArray *)newCookies {
@synchronized(cookies_) {
[self removeExpiredCookies];
for (NSHTTPCookie *newCookie in newCookies) {
if ([[newCookie name] length] > 0
&& [[newCookie domain] length] > 0
&& [[newCookie path] length] > 0) {
// remove the cookie if it's currently in the array
NSHTTPCookie *oldCookie = [self cookieMatchingCookie:newCookie];
if (oldCookie) {
[cookies_ removeObjectIdenticalTo:oldCookie];
}
// make sure the cookie hasn't already expired
NSDate *expiresDate = [newCookie expiresDate];
if ((!expiresDate) || [expiresDate timeIntervalSinceNow] > 0) {
[cookies_ addObject:newCookie];
}
} else {
NSAssert1(NO, @"Cookie incomplete: %@", newCookie);
}
}
}
}
- (void)deleteCookie:(NSHTTPCookie *)cookie {
@synchronized(cookies_) {
NSHTTPCookie *foundCookie = [self cookieMatchingCookie:cookie];
if (foundCookie) {
[cookies_ removeObjectIdenticalTo:foundCookie];
}
}
}
// retrieve all cookies appropriate for the given URL, considering
// domain, path, cookie name, expiration, security setting.
// Side effect: removed expired cookies from the storage array
- (NSArray *)cookiesForURL:(NSURL *)theURL {
NSMutableArray *foundCookies = nil;
@synchronized(cookies_) {
[self removeExpiredCookies];
// we'll prepend "." to the desired domain, since we want the
// actual domain "nytimes.com" to still match the cookie domain
// ".nytimes.com" when we check it below with hasSuffix
NSString *host = [[theURL host] lowercaseString];
NSString *path = [theURL path];
NSString *scheme = [theURL scheme];
NSString *domain = nil;
BOOL isLocalhostRetrieval = NO;
if ([host isEqual:@"localhost"]) {
isLocalhostRetrieval = YES;
} else {
if (host) {
domain = [@"." stringByAppendingString:host];
}
}
NSUInteger numberOfCookies = [cookies_ count];
for (NSUInteger idx = 0; idx < numberOfCookies; idx++) {
NSHTTPCookie *storedCookie = [cookies_ objectAtIndex:idx];
NSString *cookieDomain = [[storedCookie domain] lowercaseString];
NSString *cookiePath = [storedCookie path];
BOOL cookieIsSecure = [storedCookie isSecure];
BOOL isDomainOK;
if (isLocalhostRetrieval) {
// prior to 10.5.6, the domain stored into NSHTTPCookies for localhost
// is "localhost.local"
isDomainOK = [cookieDomain isEqual:@"localhost"]
|| [cookieDomain isEqual:@"localhost.local"];
} else {
isDomainOK = [domain hasSuffix:cookieDomain];
}
BOOL isPathOK = [cookiePath isEqual:@"/"] || [path hasPrefix:cookiePath];
BOOL isSecureOK = (!cookieIsSecure) || [scheme isEqual:@"https"];
if (isDomainOK && isPathOK && isSecureOK) {
if (foundCookies == nil) {
foundCookies = [NSMutableArray arrayWithCapacity:1];
}
[foundCookies addObject:storedCookie];
}
}
}
return foundCookies;
}
// return a cookie from the array with the same name, domain, and path as the
// given cookie, or else return nil if none found
//
// Both the cookie being tested and all cookies in the storage array should
// be valid (non-nil name, domains, paths)
//
// note: this should only be called from inside a @synchronized(cookies_) block
- (NSHTTPCookie *)cookieMatchingCookie:(NSHTTPCookie *)cookie {
NSUInteger numberOfCookies = [cookies_ count];
NSString *name = [cookie name];
NSString *domain = [cookie domain];
NSString *path = [cookie path];
NSAssert3(name && domain && path, @"Invalid cookie (name:%@ domain:%@ path:%@)",
name, domain, path);
for (NSUInteger idx = 0; idx < numberOfCookies; idx++) {
NSHTTPCookie *storedCookie = [cookies_ objectAtIndex:idx];
if ([[storedCookie name] isEqual:name]
&& [[storedCookie domain] isEqual:domain]
&& [[storedCookie path] isEqual:path]) {
return storedCookie;
}
}
return nil;
}
// internal routine to remove any expired cookies from the array, excluding
// cookies with nil expirations
//
// note: this should only be called from inside a @synchronized(cookies_) block
- (void)removeExpiredCookies {
// count backwards since we're deleting items from the array
for (NSInteger idx = [cookies_ count] - 1; idx >= 0; idx--) {
NSHTTPCookie *storedCookie = [cookies_ objectAtIndex:idx];
NSDate *expiresDate = [storedCookie expiresDate];
if (expiresDate && [expiresDate timeIntervalSinceNow] < 0) {
[cookies_ removeObjectAtIndex:idx];
}
}
}
- (void)removeAllCookies {
@synchronized(cookies_) {
[cookies_ removeAllObjects];
}
}
@end
//
// GTMCachedURLResponse
//
@implementation GTMCachedURLResponse
@synthesize response = response_;
@synthesize data = data_;
@synthesize reservationDate = reservationDate_;
@synthesize useDate = useDate_;
- (id)initWithResponse:(NSURLResponse *)response data:(NSData *)data {
self = [super init];
if (self != nil) {
response_ = [response retain];
data_ = [data retain];
useDate_ = [[NSDate alloc] init];
}
return self;
}
- (void)dealloc {
[response_ release];
[data_ release];
[useDate_ release];
[reservationDate_ release];
[super dealloc];
}
- (NSString *)description {
NSString *reservationStr = reservationDate_ ?
[NSString stringWithFormat:@" resDate:%@", reservationDate_] : @"";
return [NSString stringWithFormat:@"%@ %p: {bytes:%@ useDate:%@%@}",
[self class], self,
data_ ? [NSNumber numberWithInt:(int)[data_ length]] : nil,
useDate_,
reservationStr];
}
- (NSComparisonResult)compareUseDate:(GTMCachedURLResponse *)other {
return [useDate_ compare:[other useDate]];
}
@end
//
// GTMURLCache
//
@implementation GTMURLCache
@dynamic memoryCapacity;
- (id)init {
return [self initWithMemoryCapacity:kGTMDefaultETaggedDataCacheMemoryCapacity];
}
- (id)initWithMemoryCapacity:(NSUInteger)totalBytes {
self = [super init];
if (self != nil) {
memoryCapacity_ = totalBytes;
responses_ = [[NSMutableDictionary alloc] initWithCapacity:5];
reservationInterval_ = kCachedURLReservationInterval;
}
return self;
}
- (void)dealloc {
[responses_ release];
[super dealloc];
}
- (NSString *)description {
return [NSString stringWithFormat:@"%@ %p: {responses:%@}",
[self class], self, [responses_ allValues]];
}
// setters/getters
- (void)pruneCacheResponses {
// internal routine to remove the least-recently-used responses when the
// cache has grown too large
if (memoryCapacity_ >= totalDataSize_) return;
// sort keys by date
SEL sel = @selector(compareUseDate:);
NSArray *sortedKeys = [responses_ keysSortedByValueUsingSelector:sel];
// the least-recently-used keys are at the beginning of the sorted array;
// remove those (except ones still reserved) until the total data size is
// reduced sufficiently
for (NSURL *key in sortedKeys) {
GTMCachedURLResponse *response = [responses_ objectForKey:key];
NSDate *resDate = [response reservationDate];
BOOL isResponseReserved = (resDate != nil)
&& ([resDate timeIntervalSinceNow] > -reservationInterval_);
if (!isResponseReserved) {
// we can remove this response from the cache
NSUInteger storedSize = [[response data] length];
totalDataSize_ -= storedSize;
[responses_ removeObjectForKey:key];
}
// if we've removed enough response data, then we're done
if (memoryCapacity_ >= totalDataSize_) break;
}
}
- (void)storeCachedResponse:(GTMCachedURLResponse *)cachedResponse
forRequest:(NSURLRequest *)request {
@synchronized(self) {
// remove any previous entry for this request
[self removeCachedResponseForRequest:request];
// cache this one only if it's not bigger than our cache
NSUInteger storedSize = [[cachedResponse data] length];
if (storedSize < memoryCapacity_) {
NSURL *key = [request URL];
[responses_ setObject:cachedResponse forKey:key];
totalDataSize_ += storedSize;
[self pruneCacheResponses];
}
}
}
- (GTMCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request {
GTMCachedURLResponse *response;
@synchronized(self) {
NSURL *key = [request URL];
response = [[[responses_ objectForKey:key] retain] autorelease];
// touch the date to indicate this was recently retrieved
[response setUseDate:[NSDate date]];
}
return response;
}
- (void)removeCachedResponseForRequest:(NSURLRequest *)request {
@synchronized(self) {
NSURL *key = [request URL];
totalDataSize_ -= [[[responses_ objectForKey:key] data] length];
[responses_ removeObjectForKey:key];
}
}
- (void)removeAllCachedResponses {
@synchronized(self) {
[responses_ removeAllObjects];
totalDataSize_ = 0;
}
}
- (NSUInteger)memoryCapacity {
return memoryCapacity_;
}
- (void)setMemoryCapacity:(NSUInteger)totalBytes {
@synchronized(self) {
BOOL didShrink = (totalBytes < memoryCapacity_);
memoryCapacity_ = totalBytes;
if (didShrink) {
[self pruneCacheResponses];
}
}
}
// methods for unit testing
- (void)setReservationInterval:(NSTimeInterval)secs {
reservationInterval_ = secs;
}
- (NSDictionary *)responses {
return responses_;
}
- (NSUInteger)totalDataSize {
return totalDataSize_;
}
@end
//
// GTMHTTPFetchHistory
//
@interface GTMHTTPFetchHistory ()
- (NSString *)cachedETagForRequest:(NSURLRequest *)request;
- (void)removeCachedDataForRequest:(NSURLRequest *)request;
@end
@implementation GTMHTTPFetchHistory
@synthesize cookieStorage = cookieStorage_;
@dynamic shouldRememberETags;
@dynamic shouldCacheETaggedData;
@dynamic memoryCapacity;
- (id)init {
return [self initWithMemoryCapacity:kGTMDefaultETaggedDataCacheMemoryCapacity
shouldCacheETaggedData:NO];
}
- (id)initWithMemoryCapacity:(NSUInteger)totalBytes
shouldCacheETaggedData:(BOOL)shouldCacheETaggedData {
self = [super init];
if (self != nil) {
etaggedDataCache_ = [[GTMURLCache alloc] initWithMemoryCapacity:totalBytes];
shouldRememberETags_ = shouldCacheETaggedData;
shouldCacheETaggedData_ = shouldCacheETaggedData;
cookieStorage_ = [[GTMCookieStorage alloc] init];
}
return self;
}
- (void)dealloc {
[etaggedDataCache_ release];
[cookieStorage_ release];
[super dealloc];
}
- (void)updateRequest:(NSMutableURLRequest *)request isHTTPGet:(BOOL)isHTTPGet {
if ([self shouldRememberETags]) {
// If this URL is in the history, and no ETag has been set, then
// set the ETag header field
// if we have a history, we're tracking across fetches, so we don't
// want to pull results from any other cache
[request setCachePolicy:NSURLRequestReloadIgnoringCacheData];
if (isHTTPGet) {
// we'll only add an ETag if there's no ETag specified in the user's
// request
NSString *specifiedETag = [request valueForHTTPHeaderField:kGTMIfNoneMatchHeader];
if (specifiedETag == nil) {
// no ETag: extract the previous ETag for this request from the
// fetch history, and add it to the request
NSString *cachedETag = [self cachedETagForRequest:request];
if (cachedETag != nil) {
[request addValue:cachedETag forHTTPHeaderField:kGTMIfNoneMatchHeader];
}
} else {
// has an ETag: remove any stored response in the fetch history
// for this request, as the If-None-Match header could lead to
// a 304 Not Modified, and we want that error delivered to the
// user since they explicitly specified the ETag
[self removeCachedDataForRequest:request];
}
}
}
}
- (void)updateFetchHistoryWithRequest:(NSURLRequest *)request
response:(NSURLResponse *)response
downloadedData:(NSData *)downloadedData {
if (![self shouldRememberETags]) return;
if (![response respondsToSelector:@selector(allHeaderFields)]) return;
NSInteger statusCode = [(NSHTTPURLResponse *)response statusCode];
if (statusCode != kGTMHTTPFetcherStatusNotModified) {
// save this ETag string for successful results (<300)
// If there's no last modified string, clear the dictionary
// entry for this URL. Also cache or delete the data, if appropriate
// (when etaggedDataCache is non-nil.)
NSDictionary *headers = [(NSHTTPURLResponse *)response allHeaderFields];
NSString* etag = [headers objectForKey:kGTMETagHeader];
if (etag != nil && statusCode < 300) {
// we want to cache responses for the headers, even if the client
// doesn't want the response body data caches
NSData *dataToStore = shouldCacheETaggedData_ ? downloadedData : nil;
GTMCachedURLResponse *cachedResponse;
cachedResponse = [[[GTMCachedURLResponse alloc] initWithResponse:response
data:dataToStore] autorelease];
[etaggedDataCache_ storeCachedResponse:cachedResponse
forRequest:request];
} else {
[etaggedDataCache_ removeCachedResponseForRequest:request];
}
}
}
- (NSString *)cachedETagForRequest:(NSURLRequest *)request {
GTMCachedURLResponse *cachedResponse;
cachedResponse = [etaggedDataCache_ cachedResponseForRequest:request];
NSURLResponse *response = [cachedResponse response];
NSDictionary *headers = [(NSHTTPURLResponse *)response allHeaderFields];
NSString *cachedETag = [headers objectForKey:kGTMETagHeader];
if (cachedETag) {
// since the request having an ETag implies this request is about
// to be fetched again, reserve the cached response to ensure that
// that it will be around at least until the fetch completes
//
// when the fetch completes, either the cached response will be replaced
// with a new response, or the cachedDataForRequest: method below will
// clear the reservation
[cachedResponse setReservationDate:[NSDate date]];
}
return cachedETag;
}
- (NSData *)cachedDataForRequest:(NSURLRequest *)request {
GTMCachedURLResponse *cachedResponse;
cachedResponse = [etaggedDataCache_ cachedResponseForRequest:request];
NSData *cachedData = [cachedResponse data];
// since the data for this cached request is being obtained from the cache,
// we can clear the reservation as the fetch has completed
[cachedResponse setReservationDate:nil];
return cachedData;
}
- (void)removeCachedDataForRequest:(NSURLRequest *)request {
[etaggedDataCache_ removeCachedResponseForRequest:request];
}
- (void)clearETaggedDataCache {
[etaggedDataCache_ removeAllCachedResponses];
}
- (void)clearHistory {
[self clearETaggedDataCache];
[cookieStorage_ removeAllCookies];
}
- (void)removeAllCookies {
[cookieStorage_ removeAllCookies];
}
- (BOOL)shouldRememberETags {
return shouldRememberETags_;
}
- (void)setShouldRememberETags:(BOOL)flag {
BOOL wasRemembering = shouldRememberETags_;
shouldRememberETags_ = flag;
if (wasRemembering && !flag) {
// free up the cache memory
[self clearETaggedDataCache];
}
}
- (BOOL)shouldCacheETaggedData {
return shouldCacheETaggedData_;
}
- (void)setShouldCacheETaggedData:(BOOL)flag {
BOOL wasCaching = shouldCacheETaggedData_;
shouldCacheETaggedData_ = flag;
if (flag) {
self.shouldRememberETags = YES;
}
if (wasCaching && !flag) {
// users expect turning off caching to free up the cache memory
[self clearETaggedDataCache];
}
}
- (NSUInteger)memoryCapacity {
return [etaggedDataCache_ memoryCapacity];
}
- (void)setMemoryCapacity:(NSUInteger)totalBytes {
[etaggedDataCache_ setMemoryCapacity:totalBytes];
}
@end

704
OAuth 2/GTMHTTPFetcher.h Executable file
View File

@@ -0,0 +1,704 @@
/* Copyright (c) 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
//
// GTMHTTPFetcher.h
//
// This is essentially a wrapper around NSURLConnection for POSTs and GETs.
// If setPostData: is called, then POST is assumed.
//
// When would you use this instead of NSURLConnection?
//
// - When you just want the result from a GET, POST, or PUT
// - When you want the "standard" behavior for connections (redirection handling
// an so on)
// - When you want automatic retry on failures
// - When you want to avoid cookie collisions with Safari and other applications
// - When you are fetching resources with ETags and want to avoid the overhead
// of repeated fetches of unchanged data
// - When you need to set a credential for the http operation
//
// This is assumed to be a one-shot fetch request; don't reuse the object
// for a second fetch.
//
// The fetcher may be created auto-released, in which case it will release
// itself after the fetch completion callback. The fetcher is implicitly
// retained as long as a connection is pending.
//
// But if you may need to cancel the fetcher, retain it and have the delegate
// release the fetcher in the callbacks.
//
// Sample usage:
//
// NSURLRequest *request = [NSURLRequest requestWithURL:myURL];
// GTMHTTPFetcher* myFetcher = [GTMHTTPFetcher fetcherWithRequest:request];
//
// // optional upload body data
// [myFetcher setPostData:[postString dataUsingEncoding:NSUTF8StringEncoding]];
//
// [myFetcher beginFetchWithDelegate:self
// didFinishSelector:@selector(myFetcher:finishedWithData:error:)];
//
// Upon fetch completion, the callback selector is invoked; it should have
// this signature (you can use any callback method name you want so long as
// the signature matches this):
//
// - (void)myFetcher:(GTMHTTPFetcher *)fetcher finishedWithData:(NSData *)retrievedData error:(NSError *)error;
//
// The block callback version looks like:
//
// [myFetcher beginFetchWithCompletionHandler:^(NSData *retrievedData, NSError *error) {
// if (error != nil) {
// // status code or network error
// } else {
// // succeeded
// }
// }];
//
// NOTE: Fetches may retrieve data from the server even though the server
// returned an error. The failure selector is called when the server
// status is >= 300, with an NSError having domain
// kGTMHTTPFetcherStatusDomain and code set to the server status.
//
// Status codes are at <http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html>
//
//
// Downloading to disk:
//
// To have downloaded data saved directly to disk, specify either a path for the
// downloadPath property, or a file handle for the downloadFileHandle property.
// When downloading to disk, callbacks will be passed a nil for the NSData*
// arguments.
//
//
// HTTP methods and headers:
//
// Alternative HTTP methods, like PUT, and custom headers can be specified by
// creating the fetcher with an appropriate NSMutableURLRequest
//
//
// Proxies:
//
// Proxy handling is invisible so long as the system has a valid credential in
// the keychain, which is normally true (else most NSURL-based apps would have
// difficulty.) But when there is a proxy authetication error, the the fetcher
// will call the failedWithError: method with the NSURLChallenge in the error's
// userInfo. The error method can get the challenge info like this:
//
// NSURLAuthenticationChallenge *challenge
// = [[error userInfo] objectForKey:kGTMHTTPFetcherErrorChallengeKey];
// BOOL isProxyChallenge = [[challenge protectionSpace] isProxy];
//
// If a proxy error occurs, you can ask the user for the proxy username/password
// and call fetcher's setProxyCredential: to provide those for the
// next attempt to fetch.
//
//
// Cookies:
//
// There are three supported mechanisms for remembering cookies between fetches.
//
// By default, GTMHTTPFetcher uses a mutable array held statically to track
// cookies for all instantiated fetchers. This avoids server cookies being set
// by servers for the application from interfering with Safari cookie settings,
// and vice versa. The fetcher cookies are lost when the application quits.
//
// To rely instead on WebKit's global NSHTTPCookieStorage, call
// setCookieStorageMethod: with kGTMHTTPFetcherCookieStorageMethodSystemDefault.
//
// If the fetcher is created from a GTMHTTPFetcherService object
// then the cookie storage mechanism is set to use the cookie storage in the
// service object rather than the static storage.
//
//
// Fetching for periodic checks:
//
// The fetcher object tracks ETag headers from responses and
// provide an "If-None-Match" header. This allows the server to save
// bandwidth by providing a status message instead of repeated response
// data.
//
// To get this behavior, create the fetcher from an GTMHTTPFetcherService object
// and look for a fetch callback error with code 304
// (kGTMHTTPFetcherStatusNotModified) like this:
//
// - (void)myFetcher:(GTMHTTPFetcher *)fetcher finishedWithData:(NSData *)data error:(NSError *)error {
// if ([error code] == kGTMHTTPFetcherStatusNotModified) {
// // |data| is empty; use the data from the previous finishedWithData: for this URL
// } else {
// // handle other server status code
// }
// }
//
//
// Monitoring received data
//
// The optional received data selector can be set with setReceivedDataSelector:
// and should have the signature
//
// - (void)myFetcher:(GTMHTTPFetcher *)fetcher receivedData:(NSData *)dataReceivedSoFar;
//
// The number bytes received so far is available as [fetcher downloadedLength].
// This number may go down if a redirect causes the download to begin again from
// a new server.
//
// If supplied by the server, the anticipated total download size is available
// as [[myFetcher response] expectedContentLength] (and may be -1 for unknown
// download sizes.)
//
//
// Automatic retrying of fetches
//
// The fetcher can optionally create a timer and reattempt certain kinds of
// fetch failures (status codes 408, request timeout; 503, service unavailable;
// 504, gateway timeout; networking errors NSURLErrorTimedOut and
// NSURLErrorNetworkConnectionLost.) The user may set a retry selector to
// customize the type of errors which will be retried.
//
// Retries are done in an exponential-backoff fashion (that is, after 1 second,
// 2, 4, 8, and so on.)
//
// Enabling automatic retries looks like this:
// [myFetcher setRetryEnabled:YES];
//
// With retries enabled, the success or failure callbacks are called only
// when no more retries will be attempted. Calling the fetcher's stopFetching
// method will terminate the retry timer, without the finished or failure
// selectors being invoked.
//
// Optionally, the client may set the maximum retry interval:
// [myFetcher setMaxRetryInterval:60.0]; // in seconds; default is 60 seconds
// // for downloads, 600 for uploads
//
// Also optionally, the client may provide a callback selector to determine
// if a status code or other error should be retried.
// [myFetcher setRetrySelector:@selector(myFetcher:willRetry:forError:)];
//
// If set, the retry selector should have the signature:
// -(BOOL)fetcher:(GTMHTTPFetcher *)fetcher willRetry:(BOOL)suggestedWillRetry forError:(NSError *)error
// and return YES to set the retry timer or NO to fail without additional
// fetch attempts.
//
// The retry method may return the |suggestedWillRetry| argument to get the
// default retry behavior. Server status codes are present in the
// error argument, and have the domain kGTMHTTPFetcherStatusDomain. The
// user's method may look something like this:
//
// -(BOOL)myFetcher:(GTMHTTPFetcher *)fetcher willRetry:(BOOL)suggestedWillRetry forError:(NSError *)error {
//
// // perhaps examine [error domain] and [error code], or [fetcher retryCount]
// //
// // return YES to start the retry timer, NO to proceed to the failure
// // callback, or |suggestedWillRetry| to get default behavior for the
// // current error domain and code values.
// return suggestedWillRetry;
// }
#pragma once
#import <Foundation/Foundation.h>
#if defined(GTL_TARGET_NAMESPACE)
// we're using target namespace macros
#import "GTLDefines.h"
#elif defined(GDATA_TARGET_NAMESPACE)
#import "GDataDefines.h"
#else
#if TARGET_OS_IPHONE
#ifndef GTM_FOUNDATION_ONLY
#define GTM_FOUNDATION_ONLY 1
#endif
#ifndef GTM_IPHONE
#define GTM_IPHONE 1
#endif
#endif
#endif
#if TARGET_OS_IPHONE && (__IPHONE_OS_VERSION_MAX_ALLOWED >= 40000)
#define GTM_BACKGROUND_FETCHING 1
#endif
#undef _EXTERN
#undef _INITIALIZE_AS
#ifdef GTMHTTPFETCHER_DEFINE_GLOBALS
#define _EXTERN
#define _INITIALIZE_AS(x) =x
#else
#if defined(__cplusplus)
#define _EXTERN extern "C"
#else
#define _EXTERN extern
#endif
#define _INITIALIZE_AS(x)
#endif
// notifications
//
// fetch started and stopped, and fetch retry delay started and stopped
_EXTERN NSString* const kGTMHTTPFetcherStartedNotification _INITIALIZE_AS(@"kGTMHTTPFetcherStartedNotification");
_EXTERN NSString* const kGTMHTTPFetcherStoppedNotification _INITIALIZE_AS(@"kGTMHTTPFetcherStoppedNotification");
_EXTERN NSString* const kGTMHTTPFetcherRetryDelayStartedNotification _INITIALIZE_AS(@"kGTMHTTPFetcherRetryDelayStartedNotification");
_EXTERN NSString* const kGTMHTTPFetcherRetryDelayStoppedNotification _INITIALIZE_AS(@"kGTMHTTPFetcherRetryDelayStoppedNotification");
// callback constants
_EXTERN NSString* const kGTMHTTPFetcherErrorDomain _INITIALIZE_AS(@"com.google.GTMHTTPFetcher");
_EXTERN NSString* const kGTMHTTPFetcherStatusDomain _INITIALIZE_AS(@"com.google.HTTPStatus");
_EXTERN NSString* const kGTMHTTPFetcherErrorChallengeKey _INITIALIZE_AS(@"challenge");
_EXTERN NSString* const kGTMHTTPFetcherStatusDataKey _INITIALIZE_AS(@"data"); // data returned with a kGTMHTTPFetcherStatusDomain error
enum {
kGTMHTTPFetcherErrorDownloadFailed = -1,
kGTMHTTPFetcherErrorAuthenticationChallengeFailed = -2,
kGTMHTTPFetcherErrorChunkUploadFailed = -3,
kGTMHTTPFetcherErrorFileHandleException = -4,
kGTMHTTPFetcherErrorBackgroundExpiration = -6,
// The code kGTMHTTPFetcherErrorAuthorizationFailed (-5) has been removed;
// look for status 401 instead.
kGTMHTTPFetcherStatusNotModified = 304,
kGTMHTTPFetcherStatusBadRequest = 400,
kGTMHTTPFetcherStatusUnauthorized = 401,
kGTMHTTPFetcherStatusForbidden = 403,
kGTMHTTPFetcherStatusPreconditionFailed = 412
};
// cookie storage methods
enum {
kGTMHTTPFetcherCookieStorageMethodStatic = 0,
kGTMHTTPFetcherCookieStorageMethodFetchHistory = 1,
kGTMHTTPFetcherCookieStorageMethodSystemDefault = 2,
kGTMHTTPFetcherCookieStorageMethodNone = 3
};
#ifdef __cplusplus
extern "C" {
#endif
void GTMAssertSelectorNilOrImplementedWithArgs(id obj, SEL sel, ...);
// Utility functions for applications self-identifying to servers via a
// user-agent header
// Make a proper app name without whitespace from the given string, removing
// whitespace and other characters that may be special parsed marks of
// the full user-agent string.
NSString *GTMCleanedUserAgentString(NSString *str);
// Make an identifier like "MacOSX/10.7.1" or "iPod_Touch/4.1"
NSString *GTMSystemVersionString(void);
// Make a generic name and version for the current application, like
// com.example.MyApp/1.2.3 relying on the bundle identifier and the
// CFBundleShortVersionString or CFBundleVersion. If no bundle ID
// is available, the process name preceded by "proc_" is used.
NSString *GTMApplicationIdentifier(NSBundle *bundle);
#ifdef __cplusplus
} // extern "C"
#endif
@class GTMHTTPFetcher;
@protocol GTMCookieStorageProtocol <NSObject>
// This protocol allows us to call into the service without requiring
// GTMCookieStorage sources in this project
//
// The public interface for cookie handling is the GTMCookieStorage class,
// accessible from a fetcher service object's fetchHistory or from the fetcher's
// +staticCookieStorage method.
- (NSArray *)cookiesForURL:(NSURL *)theURL;
- (void)setCookies:(NSArray *)newCookies;
@end
@protocol GTMHTTPFetchHistoryProtocol <NSObject>
// This protocol allows us to call the fetch history object without requiring
// GTMHTTPFetchHistory sources in this project
- (void)updateRequest:(NSMutableURLRequest *)request isHTTPGet:(BOOL)isHTTPGet;
- (BOOL)shouldCacheETaggedData;
- (NSData *)cachedDataForRequest:(NSURLRequest *)request;
- (id <GTMCookieStorageProtocol>)cookieStorage;
- (void)updateFetchHistoryWithRequest:(NSURLRequest *)request
response:(NSURLResponse *)response
downloadedData:(NSData *)downloadedData;
- (void)removeCachedDataForRequest:(NSURLRequest *)request;
@end
@protocol GTMHTTPFetcherServiceProtocol <NSObject>
// This protocol allows us to call into the service without requiring
// GTMHTTPFetcherService sources in this project
- (BOOL)fetcherShouldBeginFetching:(GTMHTTPFetcher *)fetcher;
- (void)fetcherDidStop:(GTMHTTPFetcher *)fetcher;
- (GTMHTTPFetcher *)fetcherWithRequest:(NSURLRequest *)request;
- (BOOL)isDelayingFetcher:(GTMHTTPFetcher *)fetcher;
@end
@protocol GTMFetcherAuthorizationProtocol <NSObject>
@required
// This protocol allows us to call the authorizer without requiring its sources
// in this project
- (void)authorizeRequest:(NSMutableURLRequest *)request
delegate:(id)delegate
didFinishSelector:(SEL)sel;
- (void)stopAuthorization;
- (BOOL)isAuthorizingRequest:(NSURLRequest *)request;
- (BOOL)isAuthorizedRequest:(NSURLRequest *)request;
- (NSString *)userEmail;
@optional
@property (assign) id <GTMHTTPFetcherServiceProtocol> fetcherService; // WEAK
- (BOOL)primeForRefresh;
@end
// GTMHTTPFetcher objects are used for async retrieval of an http get or post
//
// See additional comments at the beginning of this file
@interface GTMHTTPFetcher : NSObject {
@protected
NSMutableURLRequest *request_;
NSURLConnection *connection_;
NSMutableData *downloadedData_;
NSString *downloadPath_;
NSString *temporaryDownloadPath_;
NSFileHandle *downloadFileHandle_;
unsigned long long downloadedLength_;
NSURLCredential *credential_; // username & password
NSURLCredential *proxyCredential_; // credential supplied to proxy servers
NSData *postData_;
NSInputStream *postStream_;
NSMutableData *loggedStreamData_;
NSURLResponse *response_; // set in connection:didReceiveResponse:
id delegate_;
SEL finishedSel_; // should by implemented by delegate
SEL sentDataSel_; // optional, set with setSentDataSelector
SEL receivedDataSel_; // optional, set with setReceivedDataSelector
#if NS_BLOCKS_AVAILABLE
void (^completionBlock_)(NSData *, NSError *);
void (^receivedDataBlock_)(NSData *);
void (^sentDataBlock_)(NSInteger, NSInteger, NSInteger);
BOOL (^retryBlock_)(BOOL, NSError *);
#elif !__LP64__
// placeholders: for 32-bit builds, keep the size of the object's ivar section
// the same with and without blocks
id completionPlaceholder_;
id receivedDataPlaceholder_;
id sentDataPlaceholder_;
id retryPlaceholder_;
#endif
BOOL hasConnectionEnded_; // set if the connection need not be cancelled
BOOL isCancellingChallenge_; // set only when cancelling an auth challenge
BOOL isStopNotificationNeeded_; // set when start notification has been sent
BOOL shouldFetchInBackground_;
#if GTM_BACKGROUND_FETCHING
NSUInteger backgroundTaskIdentifer_; // UIBackgroundTaskIdentifier
#endif
id userData_; // retained, if set by caller
NSMutableDictionary *properties_; // more data retained for caller
NSArray *runLoopModes_; // optional, for 10.5 and later
id <GTMHTTPFetchHistoryProtocol> fetchHistory_; // if supplied by the caller, used for Last-Modified-Since checks and cookies
NSInteger cookieStorageMethod_; // constant from above
id <GTMCookieStorageProtocol> cookieStorage_;
id <GTMFetcherAuthorizationProtocol> authorizer_;
// the service object that created and monitors this fetcher, if any
id <GTMHTTPFetcherServiceProtocol> service_;
NSString *serviceHost_;
NSInteger servicePriority_;
NSThread *thread_;
BOOL isRetryEnabled_; // user wants auto-retry
SEL retrySel_; // optional; set with setRetrySelector
NSTimer *retryTimer_;
NSUInteger retryCount_;
NSTimeInterval maxRetryInterval_; // default 600 seconds
NSTimeInterval minRetryInterval_; // random between 1 and 2 seconds
NSTimeInterval retryFactor_; // default interval multiplier is 2
NSTimeInterval lastRetryInterval_;
BOOL hasAttemptedAuthRefresh_;
NSString *comment_; // comment for log
NSString *log_;
}
// Create a fetcher
//
// fetcherWithRequest will return an autoreleased fetcher, but if
// the connection is successfully created, the connection should retain the
// fetcher for the life of the connection as well. So the caller doesn't have
// to retain the fetcher explicitly unless they want to be able to cancel it.
+ (GTMHTTPFetcher *)fetcherWithRequest:(NSURLRequest *)request;
// Convenience methods that make a request, like +fetcherWithRequest
+ (GTMHTTPFetcher *)fetcherWithURL:(NSURL *)requestURL;
+ (GTMHTTPFetcher *)fetcherWithURLString:(NSString *)requestURLString;
// Designated initializer
- (id)initWithRequest:(NSURLRequest *)request;
// Fetcher request
//
// The underlying request is mutable and may be modified by the caller
@property (retain) NSMutableURLRequest *mutableRequest;
// Setting the credential is optional; it is used if the connection receives
// an authentication challenge
@property (retain) NSURLCredential *credential;
// Setting the proxy credential is optional; it is used if the connection
// receives an authentication challenge from a proxy
@property (retain) NSURLCredential *proxyCredential;
// If post data or stream is not set, then a GET retrieval method is assumed
@property (retain) NSData *postData;
@property (retain) NSInputStream *postStream;
// The default cookie storage method is kGTMHTTPFetcherCookieStorageMethodStatic
// without a fetch history set, and kGTMHTTPFetcherCookieStorageMethodFetchHistory
// with a fetch history set
//
// Applications needing control of cookies across a sequence of fetches should
// create fetchers from a GTMHTTPFetcherService object (which encapsulates
// fetch history) for a well-defined cookie store
@property (assign) NSInteger cookieStorageMethod;
+ (id <GTMCookieStorageProtocol>)staticCookieStorage;
// Object to add authorization to the request, if needed
@property (retain) id <GTMFetcherAuthorizationProtocol> authorizer;
// The service object that created and monitors this fetcher, if any
@property (retain) id <GTMHTTPFetcherServiceProtocol> service;
// The host, if any, used to classify this fetcher in the fetcher service
@property (copy) NSString *serviceHost;
// The priority, if any, used for starting fetchers in the fetcher service
//
// Lower values are higher priority; the default is 0, and values may
// be negative or positive. This priority affects only the start order of
// fetchers that are being delayed by a fetcher service.
@property (assign) NSInteger servicePriority;
// The thread used to run this fetcher in the fetcher service
@property (retain) NSThread *thread;
// The delegate is retained during the connection
@property (retain) id delegate;
// On iOS 4 and later, the fetch may optionally continue while the app is in the
// background until finished or stopped by OS expiration
//
// The default value is NO
//
// For Mac OS X, background fetches are always supported, and this property
// is ignored
@property (assign) BOOL shouldFetchInBackground;
// The delegate's optional sentData selector may be used to monitor upload
// progress. It should have a signature like:
// - (void)myFetcher:(GTMHTTPFetcher *)fetcher
// didSendBytes:(NSInteger)bytesSent
// totalBytesSent:(NSInteger)totalBytesSent
// totalBytesExpectedToSend:(NSInteger)totalBytesExpectedToSend;
//
// +doesSupportSentDataCallback indicates if this delegate method is supported
+ (BOOL)doesSupportSentDataCallback;
@property (assign) SEL sentDataSelector;
// The delegate's optional receivedData selector may be used to monitor download
// progress. It should have a signature like:
// - (void)myFetcher:(GTMHTTPFetcher *)fetcher
// receivedData:(NSData *)dataReceivedSoFar;
//
// The dataReceived argument will be nil when downloading to a path or to a
// file handle.
//
// Applications should not use this method to accumulate the received data;
// the callback method or block supplied to the beginFetch call will have
// the complete NSData received.
@property (assign) SEL receivedDataSelector;
#if NS_BLOCKS_AVAILABLE
// The full interface to the block is provided rather than just a typedef for
// its parameter list in order to get more useful code completion in the Xcode
// editor
@property (copy) void (^sentDataBlock)(NSInteger bytesSent, NSInteger totalBytesSent, NSInteger bytesExpectedToSend);
// The dataReceived argument will be nil when downloading to a path or to
// a file handle
@property (copy) void (^receivedDataBlock)(NSData *dataReceivedSoFar);
#endif
// retrying; see comments at the top of the file. Calling
// setRetryEnabled(YES) resets the min and max retry intervals.
@property (assign, getter=isRetryEnabled) BOOL retryEnabled;
// Retry selector or block is optional for retries.
//
// If present, it should have the signature:
// -(BOOL)fetcher:(GTMHTTPFetcher *)fetcher willRetry:(BOOL)suggestedWillRetry forError:(NSError *)error
// and return YES to cause a retry. See comments at the top of this file.
@property (assign) SEL retrySelector;
#if NS_BLOCKS_AVAILABLE
@property (copy) BOOL (^retryBlock)(BOOL suggestedWillRetry, NSError *error);
#endif
// Retry intervals must be strictly less than maxRetryInterval, else
// they will be limited to maxRetryInterval and no further retries will
// be attempted. Setting maxRetryInterval to 0.0 will reset it to the
// default value, 600 seconds.
@property (assign) NSTimeInterval maxRetryInterval;
// Starting retry interval. Setting minRetryInterval to 0.0 will reset it
// to a random value between 1.0 and 2.0 seconds. Clients should normally not
// call this except for unit testing.
@property (assign) NSTimeInterval minRetryInterval;
// Multiplier used to increase the interval between retries, typically 2.0.
// Clients should not need to call this.
@property (assign) double retryFactor;
// Number of retries attempted
@property (readonly) NSUInteger retryCount;
// interval delay to precede next retry
@property (readonly) NSTimeInterval nextRetryInterval;
// Begin fetching the request
//
// The delegate can optionally implement the finished selectors or pass NULL
// for it.
//
// Returns YES if the fetch is initiated. The delegate is retained between
// the beginFetch call until after the finish callback.
//
// An error is passed to the callback for server statuses 300 or
// higher, with the status stored as the error object's code.
//
// finishedSEL has a signature like:
// - (void)fetcher:(GTMHTTPFetcher *)fetcher finishedWithData:(NSData *)data error:(NSError *)error;
//
// If the application has specified a downloadPath or downloadFileHandle
// for the fetcher, the data parameter passed to the callback will be nil.
- (BOOL)beginFetchWithDelegate:(id)delegate
didFinishSelector:(SEL)finishedSEL;
#if NS_BLOCKS_AVAILABLE
- (BOOL)beginFetchWithCompletionHandler:(void (^)(NSData *data, NSError *error))handler;
#endif
// Returns YES if this is in the process of fetching a URL
- (BOOL)isFetching;
// Cancel the fetch of the request that's currently in progress
- (void)stopFetching;
// Return the status code from the server response
@property (readonly) NSInteger statusCode;
// Return the http headers from the response
@property (retain, readonly) NSDictionary *responseHeaders;
// The response, once it's been received
@property (retain) NSURLResponse *response;
// Bytes downloaded so far
@property (readonly) unsigned long long downloadedLength;
// Buffer of currently-downloaded data
@property (readonly, retain) NSData *downloadedData;
// Path in which to non-atomically create a file for storing the downloaded data
//
// The path must be set before fetching begins. The download file handle
// will be created for the path, and can be used to monitor progress. If a file
// already exists at the path, it will be overwritten.
@property (copy) NSString *downloadPath;
// If downloadFileHandle is set, data received is immediately appended to
// the file handle rather than being accumulated in the downloadedData property
//
// The file handle supplied must allow writing and support seekToFileOffset:,
// and must be set before fetching begins. Setting a download path will
// override the file handle property.
@property (retain) NSFileHandle *downloadFileHandle;
// The optional fetchHistory object is used for a sequence of fetchers to
// remember ETags, cache ETagged data, and store cookies. Typically, this
// is set by a GTMFetcherService object when it creates a fetcher.
//
// Side effect: setting fetch history implicitly calls setCookieStorageMethod:
@property (retain) id <GTMHTTPFetchHistoryProtocol> fetchHistory;
// userData is retained for the convenience of the caller
@property (retain) id userData;
// Stored property values are retained for the convenience of the caller
@property (copy) NSMutableDictionary *properties;
- (void)setProperty:(id)obj forKey:(NSString *)key; // pass nil obj to remove property
- (id)propertyForKey:(NSString *)key;
- (void)addPropertiesFromDictionary:(NSDictionary *)dict;
// Comments are useful for logging
@property (copy) NSString *comment;
- (void)setCommentWithFormat:(id)format, ...;
// Log of request and response, if logging is enabled
@property (copy) NSString *log;
// Using the fetcher while a modal dialog is displayed requires setting the
// run-loop modes to include NSModalPanelRunLoopMode
@property (retain) NSArray *runLoopModes;
// Users who wish to replace GTMHTTPFetcher's use of NSURLConnection
// can do so globally here. The replacement should be a subclass of
// NSURLConnection.
+ (Class)connectionClass;
+ (void)setConnectionClass:(Class)theClass;
// Spin the run loop, discarding events, until the fetch has completed
//
// This is only for use in testing or in tools without a user interface.
//
// Synchronous fetches should never be done by shipping apps; they are
// sufficient reason for rejection from the app store.
- (void)waitForCompletionWithTimeout:(NSTimeInterval)timeoutInSeconds;
#if STRIP_GTM_FETCH_LOGGING
// if logging is stripped, provide a stub for the main method
// for controlling logging
+ (void)setLoggingEnabled:(BOOL)flag;
#endif // STRIP_GTM_FETCH_LOGGING
@end

1746
OAuth 2/GTMHTTPFetcher.m Executable file

File diff suppressed because it is too large Load Diff

325
OAuth 2/GTMOAuth2Authentication.h Executable file
View File

@@ -0,0 +1,325 @@
/* Copyright (c) 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES
// This class implements the OAuth 2 protocol for authorizing requests.
// http://tools.ietf.org/html/draft-ietf-oauth-v2
#import <Foundation/Foundation.h>
// GTMHTTPFetcher.h brings in GTLDefines/GDataDefines
#import "GTMHTTPFetcher.h"
#undef _EXTERN
#undef _INITIALIZE_AS
#ifdef GTMOAUTH2AUTHENTICATION_DEFINE_GLOBALS
#define _EXTERN
#define _INITIALIZE_AS(x) =x
#else
#if defined(__cplusplus)
#define _EXTERN extern "C"
#else
#define _EXTERN extern
#endif
#define _INITIALIZE_AS(x)
#endif
// Until all OAuth 2 providers are up to the same spec, we'll provide a crude
// way here to override the "Bearer" string in the Authorization header
#ifndef GTM_OAUTH2_BEARER
#define GTM_OAUTH2_BEARER "Bearer"
#endif
// Service provider name allows stored authorization to be associated with
// the authorizing service
_EXTERN NSString* const kGTMOAuth2ServiceProviderGoogle _INITIALIZE_AS(@"Google");
//
// GTMOAuth2SignIn constants, included here for use by clients
//
_EXTERN NSString* const kGTMOAuth2ErrorDomain _INITIALIZE_AS(@"com.google.GTMOAuth2");
// Error userInfo keys
_EXTERN NSString* const kGTMOAuth2ErrorMessageKey _INITIALIZE_AS(@"error");
_EXTERN NSString* const kGTMOAuth2ErrorRequestKey _INITIALIZE_AS(@"request");
_EXTERN NSString* const kGTMOAuth2ErrorJSONKey _INITIALIZE_AS(@"json");
enum {
// Error code indicating that the window was prematurely closed
kGTMOAuth2ErrorWindowClosed = -1000,
kGTMOAuth2ErrorAuthorizationFailed = -1001,
kGTMOAuth2ErrorTokenExpired = -1002,
kGTMOAuth2ErrorTokenUnavailable = -1003,
kGTMOAuth2ErrorUnauthorizableRequest = -1004
};
// Notifications for token fetches
_EXTERN NSString* const kGTMOAuth2FetchStarted _INITIALIZE_AS(@"kGTMOAuth2FetchStarted");
_EXTERN NSString* const kGTMOAuth2FetchStopped _INITIALIZE_AS(@"kGTMOAuth2FetchStopped");
_EXTERN NSString* const kGTMOAuth2FetcherKey _INITIALIZE_AS(@"fetcher");
_EXTERN NSString* const kGTMOAuth2FetchTypeKey _INITIALIZE_AS(@"FetchType");
_EXTERN NSString* const kGTMOAuth2FetchTypeToken _INITIALIZE_AS(@"token");
_EXTERN NSString* const kGTMOAuth2FetchTypeRefresh _INITIALIZE_AS(@"refresh");
_EXTERN NSString* const kGTMOAuth2FetchTypeAssertion _INITIALIZE_AS(@"assertion");
_EXTERN NSString* const kGTMOAuth2FetchTypeUserInfo _INITIALIZE_AS(@"userInfo");
// Token-issuance errors
_EXTERN NSString* const kGTMOAuth2ErrorKey _INITIALIZE_AS(@"error");
_EXTERN NSString* const kGTMOAuth2ErrorInvalidRequest _INITIALIZE_AS(@"invalid_request");
_EXTERN NSString* const kGTMOAuth2ErrorInvalidClient _INITIALIZE_AS(@"invalid_client");
_EXTERN NSString* const kGTMOAuth2ErrorInvalidGrant _INITIALIZE_AS(@"invalid_grant");
_EXTERN NSString* const kGTMOAuth2ErrorUnauthorizedClient _INITIALIZE_AS(@"unauthorized_client");
_EXTERN NSString* const kGTMOAuth2ErrorUnsupportedGrantType _INITIALIZE_AS(@"unsupported_grant_type");
_EXTERN NSString* const kGTMOAuth2ErrorInvalidScope _INITIALIZE_AS(@"invalid_scope");
// Notification that sign-in has completed, and token fetches will begin (useful
// for displaying interstitial messages after the window has closed)
_EXTERN NSString* const kGTMOAuth2UserSignedIn _INITIALIZE_AS(@"kGTMOAuth2UserSignedIn");
// Notification for token changes
_EXTERN NSString* const kGTMOAuth2AccessTokenRefreshed _INITIALIZE_AS(@"kGTMOAuth2AccessTokenRefreshed");
_EXTERN NSString* const kGTMOAuth2RefreshTokenChanged _INITIALIZE_AS(@"kGTMOAuth2RefreshTokenChanged");
// Notification for WebView loading
_EXTERN NSString* const kGTMOAuth2WebViewStartedLoading _INITIALIZE_AS(@"kGTMOAuth2WebViewStartedLoading");
_EXTERN NSString* const kGTMOAuth2WebViewStoppedLoading _INITIALIZE_AS(@"kGTMOAuth2WebViewStoppedLoading");
_EXTERN NSString* const kGTMOAuth2WebViewKey _INITIALIZE_AS(@"kGTMOAuth2WebViewKey");
_EXTERN NSString* const kGTMOAuth2WebViewStopKindKey _INITIALIZE_AS(@"kGTMOAuth2WebViewStopKindKey");
_EXTERN NSString* const kGTMOAuth2WebViewFinished _INITIALIZE_AS(@"finished");
_EXTERN NSString* const kGTMOAuth2WebViewFailed _INITIALIZE_AS(@"failed");
_EXTERN NSString* const kGTMOAuth2WebViewCancelled _INITIALIZE_AS(@"cancelled");
// Notification for network loss during html sign-in display
_EXTERN NSString* const kGTMOAuth2NetworkLost _INITIALIZE_AS(@"kGTMOAuthNetworkLost");
_EXTERN NSString* const kGTMOAuth2NetworkFound _INITIALIZE_AS(@"kGTMOAuthNetworkFound");
@interface GTMOAuth2Authentication : NSObject <GTMFetcherAuthorizationProtocol> {
@private
NSString *clientID_;
NSString *clientSecret_;
NSString *redirectURI_;
NSMutableDictionary *parameters_;
// authorization parameters
NSURL *tokenURL_;
NSDate *expirationDate_;
NSDictionary *additionalTokenRequestParameters_;
// queue of requests for authorization waiting for a valid access token
GTMHTTPFetcher *refreshFetcher_;
NSMutableArray *authorizationQueue_;
id <GTMHTTPFetcherServiceProtocol> fetcherService_; // WEAK
Class parserClass_;
BOOL shouldAuthorizeAllRequests_;
// arbitrary data retained for the user
id userData_;
NSMutableDictionary *properties_;
}
// OAuth2 standard protocol parameters
//
// These should be the plain strings; any needed escaping will be provided by
// the library.
// Request properties
@property (copy) NSString *clientID;
@property (copy) NSString *clientSecret;
@property (copy) NSString *redirectURI;
@property (retain) NSString *scope;
@property (retain) NSString *tokenType;
@property (retain) NSString *assertion;
// Apps may optionally add parameters here to be provided to the token
// endpoint on token requests and refreshes
@property (retain) NSDictionary *additionalTokenRequestParameters;
// Response properties
@property (retain) NSMutableDictionary *parameters;
@property (retain) NSString *accessToken;
@property (retain) NSString *refreshToken;
@property (retain) NSNumber *expiresIn;
@property (retain) NSString *code;
@property (retain) NSString *errorString;
// URL for obtaining access tokens
@property (copy) NSURL *tokenURL;
// Calculated expiration date (expiresIn seconds added to the
// time the access token was received.)
@property (copy) NSDate *expirationDate;
// Service identifier, like "Google"; not used for authentication
//
// The provider name is just for allowing stored authorization to be associated
// with the authorizing service.
@property (copy) NSString *serviceProvider;
// User email and verified status; not used for authentication
//
// The verified string can be checked with -boolValue. If the result is false,
// then the email address is listed with the account on the server, but the
// address has not been confirmed as belonging to the owner of the account.
@property (retain) NSString *userEmail;
@property (retain) NSString *userEmailIsVerified;
// Property indicating if this auth has a refresh token so is suitable for
// authorizing a request. This does not guarantee that the token is valid.
@property (readonly) BOOL canAuthorize;
// Property indicating if this object will authorize plain http request
// (as well as any non-https requests.) Default is NO, only requests with the
// scheme https are authorized, since security may be compromised if tokens
// are sent over the wire using an unencrypted protocol like http.
@property (assign) BOOL shouldAuthorizeAllRequests;
// userData is retained for the convenience of the caller
@property (retain) id userData;
// Stored property values are retained for the convenience of the caller
@property (retain) NSDictionary *properties;
// Property for the optional fetcher service instance to be used to create
// fetchers
//
// Fetcher service objects retain authorizations, so this is weak to avoid
// circular retains.
@property (assign) id <GTMHTTPFetcherServiceProtocol> fetcherService; // WEAK
// Alternative JSON parsing class; this should implement the
// GTMOAuth2ParserClass informal protocol. If this property is
// not set, the class SBJSON must be available in the runtime.
@property (assign) Class parserClass;
// Convenience method for creating an authentication object
+ (id)authenticationWithServiceProvider:(NSString *)serviceProvider
tokenURL:(NSURL *)tokenURL
redirectURI:(NSString *)redirectURI
clientID:(NSString *)clientID
clientSecret:(NSString *)clientSecret;
// Clear out any authentication values, prepare for a new request fetch
- (void)reset;
// Main authorization entry points
//
// These will refresh the access token, if necessary, add the access token to
// the request, then invoke the callback.
//
// The request argument may be nil to just force a refresh of the access token,
// if needed.
//
// NOTE: To avoid accidental leaks of bearer tokens, the request must
// be for a URL with the scheme https unless the shouldAuthorizeAllRequests
// property is set.
// The finish selector should have a signature matching
// - (void)authentication:(GTMOAuth2Authentication *)auth
// request:(NSMutableURLRequest *)request
// finishedWithError:(NSError *)error;
- (void)authorizeRequest:(NSMutableURLRequest *)request
delegate:(id)delegate
didFinishSelector:(SEL)sel;
#if NS_BLOCKS_AVAILABLE
- (void)authorizeRequest:(NSMutableURLRequest *)request
completionHandler:(void (^)(NSError *error))handler;
#endif
// Synchronous entry point; authorizing this way cannot refresh an expired
// access token
- (BOOL)authorizeRequest:(NSMutableURLRequest *)request;
// If the authentication is waiting for a refresh to complete, spin the run
// loop, discarding events, until the fetch has completed
//
// This is only for use in testing or in tools without a user interface.
- (void)waitForCompletionWithTimeout:(NSTimeInterval)timeoutInSeconds;
//////////////////////////////////////////////////////////////////////////////
//
// Internal properties and methods for use by GTMOAuth2SignIn
//
// Pending fetcher to get a new access token, if any
@property (retain) GTMHTTPFetcher *refreshFetcher;
// Check if a request is queued up to be authorized
- (BOOL)isAuthorizingRequest:(NSURLRequest *)request;
// Check if a request appears to be authorized
- (BOOL)isAuthorizedRequest:(NSURLRequest *)request;
// Stop any pending refresh fetch
- (void)stopAuthorization;
// OAuth fetch user-agent header value
- (NSString *)userAgent;
// Parse and set token and token secret from response data
- (void)setKeysForResponseString:(NSString *)str;
- (void)setKeysForResponseDictionary:(NSDictionary *)dict;
// Persistent token string for keychain storage
//
// We'll use the format "refresh_token=foo&serviceProvider=bar" so we can
// easily alter what portions of the auth data are stored
//
// Use these methods for serialization
- (NSString *)persistenceResponseString;
- (void)setKeysForPersistenceResponseString:(NSString *)str;
// method to begin fetching an access token, used by the sign-in object
- (GTMHTTPFetcher *)beginTokenFetchWithDelegate:(id)delegate
didFinishSelector:(SEL)finishedSel;
// Entry point to post a notification about a fetcher currently used for
// obtaining or refreshing a token; the sign-in object will also use this
// to indicate when the user's email address is being fetched.
//
// Fetch type constants are above under "notifications for token fetches"
- (void)notifyFetchIsRunning:(BOOL)isStarting
fetcher:(GTMHTTPFetcher *)fetcher
type:(NSString *)fetchType;
// Arbitrary key-value properties retained for the user
- (void)setProperty:(id)obj forKey:(NSString *)key;
- (id)propertyForKey:(NSString *)key;
//
// Utilities
//
+ (NSString *)encodedOAuthValueForString:(NSString *)str;
+ (NSString *)encodedQueryParametersForDictionary:(NSDictionary *)dict;
+ (NSDictionary *)dictionaryWithResponseString:(NSString *)responseStr;
+ (NSString *)scopeWithStrings:(NSString *)firsStr, ... NS_REQUIRES_NIL_TERMINATION;
@end
#endif // GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES

1166
OAuth 2/GTMOAuth2Authentication.m Executable file

File diff suppressed because it is too large Load Diff

184
OAuth 2/GTMOAuth2SignIn.h Executable file
View File

@@ -0,0 +1,184 @@
/* Copyright (c) 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
//
// This sign-in object opens and closes the web view window as needed for
// users to sign in. For signing in to Google, it also obtains
// the authenticated user's email address.
//
// Typically, this will be managed for the application by
// GTMOAuth2ViewControllerTouch or GTMOAuth2WindowController, so this
// class's interface is interesting only if
// you are creating your own window controller for sign-in.
//
//
// Delegate methods implemented by the window controller
//
// The window controller implements two methods for use by the sign-in object,
// the webRequestSelector and the finishedSelector:
//
// webRequestSelector has a signature matching
// - (void)signIn:(GTMOAuth2SignIn *)signIn displayRequest:(NSURLRequest *)request
//
// The web request selector will be invoked with a request to be displayed, or
// nil to close the window when the final callback request has been encountered.
//
//
// finishedSelector has a signature matching
// - (void)signin:(GTMOAuth2SignIn *)signin finishedWithAuth:(GTMOAuth2Authentication *)auth error:(NSError *)error
//
// The finished selector will be invoked when sign-in has completed, except
// when explicitly canceled by calling cancelSigningIn
//
#if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES
#import <Foundation/Foundation.h>
#import <SystemConfiguration/SystemConfiguration.h>
// GTMHTTPFetcher brings in GTLDefines/GDataDefines
#import "GTMHTTPFetcher.h"
#import "GTMOAuth2Authentication.h"
@class GTMOAuth2SignIn;
@interface GTMOAuth2SignIn : NSObject {
@private
GTMOAuth2Authentication *auth_;
// the endpoint for displaying the sign-in page
NSURL *authorizationURL_;
NSDictionary *additionalAuthorizationParameters_;
id delegate_;
SEL webRequestSelector_;
SEL finishedSelector_;
BOOL hasHandledCallback_;
GTMHTTPFetcher *pendingFetcher_;
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
BOOL shouldFetchGoogleUserEmail_;
BOOL shouldFetchGoogleUserProfile_;
NSDictionary *userProfile_;
#endif
SCNetworkReachabilityRef reachabilityRef_;
NSTimer *networkLossTimer_;
NSTimeInterval networkLossTimeoutInterval_;
BOOL hasNotifiedNetworkLoss_;
id userData_;
}
@property (nonatomic, retain) GTMOAuth2Authentication *authentication;
@property (nonatomic, retain) NSURL *authorizationURL;
@property (nonatomic, retain) NSDictionary *additionalAuthorizationParameters;
// The delegate is released when signing in finishes or is cancelled
@property (nonatomic, retain) id delegate;
@property (nonatomic, assign) SEL webRequestSelector;
@property (nonatomic, assign) SEL finishedSelector;
@property (nonatomic, retain) id userData;
// By default, signing in to Google will fetch the user's email, but will not
// fetch the user's profile.
//
// The email is saved in the auth object.
// The profile is available immediately after sign-in.
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
@property (nonatomic, assign) BOOL shouldFetchGoogleUserEmail;
@property (nonatomic, assign) BOOL shouldFetchGoogleUserProfile;
@property (nonatomic, retain, readonly) NSDictionary *userProfile;
#endif
// The default timeout for an unreachable network during display of the
// sign-in page is 30 seconds; set this to 0 to have no timeout
@property (nonatomic, assign) NSTimeInterval networkLossTimeoutInterval;
// The delegate is retained until sign-in has completed or been canceled
//
// designated initializer
- (id)initWithAuthentication:(GTMOAuth2Authentication *)auth
authorizationURL:(NSURL *)authorizationURL
delegate:(id)delegate
webRequestSelector:(SEL)webRequestSelector
finishedSelector:(SEL)finishedSelector;
// A default authentication object for signing in to Google services
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
+ (GTMOAuth2Authentication *)standardGoogleAuthenticationForScope:(NSString *)scope
clientID:(NSString *)clientID
clientSecret:(NSString *)clientSecret;
#endif
#pragma mark Methods used by the Window Controller
// Start the sequence of fetches and sign-in window display for sign-in
- (BOOL)startSigningIn;
// Stop any pending fetches, and close the window (but don't call the
// delegate's finishedSelector)
- (void)cancelSigningIn;
// Window controllers must tell the sign-in object about any redirect
// requested by the web view, and any changes in the webview window title
//
// If these return YES then the event was handled by the
// sign-in object (typically by closing the window) and should be ignored by
// the window controller's web view
- (BOOL)requestRedirectedToRequest:(NSURLRequest *)redirectedRequest;
- (BOOL)titleChanged:(NSString *)title;
- (BOOL)cookiesChanged:(NSHTTPCookieStorage *)cookieStorage;
- (BOOL)loadFailedWithError:(NSError *)error;
// Window controllers must tell the sign-in object if the window was closed
// prematurely by the user (but not by the sign-in object); this calls the
// delegate's finishedSelector
- (void)windowWasClosed;
#pragma mark -
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
// Revocation of an authorized token from Google
+ (void)revokeTokenForGoogleAuthentication:(GTMOAuth2Authentication *)auth;
// Create a fetcher for obtaining the user's Google email address or profile,
// according to the current auth scopes.
//
// The auth object must have been created with appropriate scopes.
//
// The fetcher's response data can be parsed with NSJSONSerialization.
+ (GTMHTTPFetcher *)userInfoFetcherWithAuth:(GTMOAuth2Authentication *)auth;
#endif
#pragma mark -
// Standard authentication values
+ (NSString *)nativeClientRedirectURI;
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
+ (NSURL *)googleAuthorizationURL;
+ (NSURL *)googleTokenURL;
+ (NSURL *)googleUserInfoURL;
#endif
@end
#endif // #if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES

814
OAuth 2/GTMOAuth2SignIn.m Executable file
View File

@@ -0,0 +1,814 @@
/* Copyright (c) 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES
#define GTMOAUTH2SIGNIN_DEFINE_GLOBALS 1
#import "GTMOAuth2SignIn.h"
// we'll default to timing out if the network becomes unreachable for more
// than 30 seconds when the sign-in page is displayed
static const NSTimeInterval kDefaultNetworkLossTimeoutInterval = 30.0;
// URI indicating an installed app is signing in. This is described at
//
// http://code.google.com/apis/accounts/docs/OAuth2.html#IA
//
NSString *const kOOBString = @"urn:ietf:wg:oauth:2.0:oob";
@interface GTMOAuth2Authentication (InternalMethods)
- (NSDictionary *)dictionaryWithJSONData:(NSData *)data;
@end
@interface GTMOAuth2SignIn ()
@property (assign) BOOL hasHandledCallback;
@property (retain) GTMHTTPFetcher *pendingFetcher;
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
@property (nonatomic, retain, readwrite) NSDictionary *userProfile;
#endif
- (void)invokeFinalCallbackWithError:(NSError *)error;
- (BOOL)startWebRequest;
+ (NSMutableURLRequest *)mutableURLRequestWithURL:(NSURL *)oldURL
paramString:(NSString *)paramStr;
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
- (void)fetchGoogleUserInfo;
#endif
- (void)finishSignInWithError:(NSError *)error;
- (void)handleCallbackReached;
- (void)auth:(GTMOAuth2Authentication *)auth
finishedWithFetcher:(GTMHTTPFetcher *)fetcher
error:(NSError *)error;
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
- (void)infoFetcher:(GTMHTTPFetcher *)fetcher
finishedWithData:(NSData *)data
error:(NSError *)error;
#endif
- (void)closeTheWindow;
- (void)startReachabilityCheck;
- (void)stopReachabilityCheck;
- (void)reachabilityTarget:(SCNetworkReachabilityRef)reachabilityRef
changedFlags:(SCNetworkConnectionFlags)flags;
- (void)reachabilityTimerFired:(NSTimer *)timer;
@end
@implementation GTMOAuth2SignIn
@synthesize authentication = auth_;
@synthesize authorizationURL = authorizationURL_;
@synthesize additionalAuthorizationParameters = additionalAuthorizationParameters_;
@synthesize delegate = delegate_;
@synthesize webRequestSelector = webRequestSelector_;
@synthesize finishedSelector = finishedSelector_;
@synthesize hasHandledCallback = hasHandledCallback_;
@synthesize pendingFetcher = pendingFetcher_;
@synthesize userData = userData_;
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
@synthesize shouldFetchGoogleUserEmail = shouldFetchGoogleUserEmail_;
@synthesize shouldFetchGoogleUserProfile = shouldFetchGoogleUserProfile_;
@synthesize userProfile = userProfile_;
#endif
@synthesize networkLossTimeoutInterval = networkLossTimeoutInterval_;
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
+ (NSURL *)googleAuthorizationURL {
NSString *str = @"https://accounts.google.com/o/oauth2/auth";
return [NSURL URLWithString:str];
}
+ (NSURL *)googleTokenURL {
NSString *str = @"https://accounts.google.com/o/oauth2/token";
return [NSURL URLWithString:str];
}
+ (NSURL *)googleRevocationURL {
NSString *urlStr = @"https://accounts.google.com/o/oauth2/revoke";
return [NSURL URLWithString:urlStr];
}
+ (NSURL *)googleUserInfoURL {
NSString *urlStr = @"https://www.googleapis.com/oauth2/v1/userinfo";
return [NSURL URLWithString:urlStr];
}
#endif
+ (NSString *)nativeClientRedirectURI {
return kOOBString;
}
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
+ (GTMOAuth2Authentication *)standardGoogleAuthenticationForScope:(NSString *)scope
clientID:(NSString *)clientID
clientSecret:(NSString *)clientSecret {
NSString *redirectURI = [self nativeClientRedirectURI];
NSURL *tokenURL = [self googleTokenURL];
GTMOAuth2Authentication *auth;
auth = [GTMOAuth2Authentication authenticationWithServiceProvider:kGTMOAuth2ServiceProviderGoogle
tokenURL:tokenURL
redirectURI:redirectURI
clientID:clientID
clientSecret:clientSecret];
auth.scope = scope;
return auth;
}
#endif
- (id)initWithAuthentication:(GTMOAuth2Authentication *)auth
authorizationURL:(NSURL *)authorizationURL
delegate:(id)delegate
webRequestSelector:(SEL)webRequestSelector
finishedSelector:(SEL)finishedSelector {
// check the selectors on debug builds
GTMAssertSelectorNilOrImplementedWithArgs(delegate, webRequestSelector,
@encode(GTMOAuth2SignIn *), @encode(NSURLRequest *), 0);
GTMAssertSelectorNilOrImplementedWithArgs(delegate, finishedSelector,
@encode(GTMOAuth2SignIn *), @encode(GTMOAuth2Authentication *),
@encode(NSError *), 0);
// designated initializer
self = [super init];
if (self) {
auth_ = [auth retain];
authorizationURL_ = [authorizationURL retain];
delegate_ = [delegate retain];
webRequestSelector_ = webRequestSelector;
finishedSelector_ = finishedSelector;
// for Google authentication, we want to automatically fetch user info
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
NSString *host = [authorizationURL host];
if ([host hasSuffix:@".google.com"]) {
shouldFetchGoogleUserEmail_ = YES;
}
#endif
// default timeout for a lost internet connection while the server
// UI is displayed is 30 seconds
networkLossTimeoutInterval_ = kDefaultNetworkLossTimeoutInterval;
}
return self;
}
- (void)dealloc {
[self stopReachabilityCheck];
[auth_ release];
[authorizationURL_ release];
[additionalAuthorizationParameters_ release];
[delegate_ release];
[pendingFetcher_ release];
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
[userProfile_ release];
#endif
[userData_ release];
[super dealloc];
}
#pragma mark Sign-in Sequence Methods
// stop any pending fetches, and close the window (but don't call the
// delegate's finishedSelector)
- (void)cancelSigningIn {
[self.pendingFetcher stopFetching];
self.pendingFetcher = nil;
[self.authentication stopAuthorization];
[self closeTheWindow];
[delegate_ autorelease];
delegate_ = nil;
}
//
// This is the entry point to begin the sequence
// - display the authentication web page, and monitor redirects
// - exchange the code for an access token and a refresh token
// - for Google sign-in, fetch the user's email address
// - tell the delegate we're finished
//
- (BOOL)startSigningIn {
// For signing in to Google, append the scope for obtaining the authenticated
// user email and profile, as appropriate
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
GTMOAuth2Authentication *auth = self.authentication;
if (self.shouldFetchGoogleUserEmail) {
NSString *const emailScope = @"https://www.googleapis.com/auth/userinfo.email";
NSString *scope = auth.scope;
if ([scope rangeOfString:emailScope].location == NSNotFound) {
scope = [GTMOAuth2Authentication scopeWithStrings:scope, emailScope, nil];
auth.scope = scope;
}
}
if (self.shouldFetchGoogleUserProfile) {
NSString *const profileScope = @"https://www.googleapis.com/auth/userinfo.profile";
NSString *scope = auth.scope;
if ([scope rangeOfString:profileScope].location == NSNotFound) {
scope = [GTMOAuth2Authentication scopeWithStrings:scope, profileScope, nil];
auth.scope = scope;
}
}
#endif
// start the authorization
return [self startWebRequest];
}
- (NSMutableDictionary *)parametersForWebRequest {
GTMOAuth2Authentication *auth = self.authentication;
NSString *clientID = auth.clientID;
NSString *redirectURI = auth.redirectURI;
BOOL hasClientID = ([clientID length] > 0);
BOOL hasRedirect = ([redirectURI length] > 0
|| redirectURI == [[self class] nativeClientRedirectURI]);
if (!hasClientID || !hasRedirect) {
#if DEBUG
NSAssert(hasClientID, @"GTMOAuth2SignIn: clientID needed");
NSAssert(hasRedirect, @"GTMOAuth2SignIn: redirectURI needed");
#endif
return NO;
}
// invoke the UI controller's web request selector to display
// the authorization page
// add params to the authorization URL
NSString *scope = auth.scope;
if ([scope length] == 0) scope = nil;
NSMutableDictionary *paramsDict = [NSMutableDictionary dictionaryWithObjectsAndKeys:
@"code", @"response_type",
clientID, @"client_id",
scope, @"scope", // scope may be nil
nil];
if (redirectURI) {
[paramsDict setObject:redirectURI forKey:@"redirect_uri"];
}
return paramsDict;
}
- (BOOL)startWebRequest {
NSMutableDictionary *paramsDict = [self parametersForWebRequest];
NSDictionary *additionalParams = self.additionalAuthorizationParameters;
if (additionalParams) {
[paramsDict addEntriesFromDictionary:additionalParams];
}
NSString *paramStr = [GTMOAuth2Authentication encodedQueryParametersForDictionary:paramsDict];
NSURL *authorizationURL = self.authorizationURL;
NSMutableURLRequest *request;
request = [[self class] mutableURLRequestWithURL:authorizationURL
paramString:paramStr];
[delegate_ performSelector:self.webRequestSelector
withObject:self
withObject:request];
// at this point, we're waiting on the server-driven html UI, so
// we want notification if we lose connectivity to the web server
[self startReachabilityCheck];
return YES;
}
// utility for making a request from an old URL with some additional parameters
+ (NSMutableURLRequest *)mutableURLRequestWithURL:(NSURL *)oldURL
paramString:(NSString *)paramStr {
NSString *query = [oldURL query];
if ([query length] > 0) {
query = [query stringByAppendingFormat:@"&%@", paramStr];
} else {
query = paramStr;
}
NSString *portStr = @"";
NSString *oldPort = [[oldURL port] stringValue];
if ([oldPort length] > 0) {
portStr = [@":" stringByAppendingString:oldPort];
}
NSString *qMark = [query length] > 0 ? @"?" : @"";
NSString *newURLStr = [NSString stringWithFormat:@"%@://%@%@%@%@%@",
[oldURL scheme], [oldURL host], portStr,
[oldURL path], qMark, query];
NSURL *newURL = [NSURL URLWithString:newURLStr];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:newURL];
return request;
}
// entry point for the window controller to tell us that the window
// prematurely closed
- (void)windowWasClosed {
[self stopReachabilityCheck];
NSError *error = [NSError errorWithDomain:kGTMOAuth2ErrorDomain
code:kGTMOAuth2ErrorWindowClosed
userInfo:nil];
[self invokeFinalCallbackWithError:error];
}
// internal method to tell the window controller to close the window
- (void)closeTheWindow {
[self stopReachabilityCheck];
// a nil request means the window should be closed
[delegate_ performSelector:self.webRequestSelector
withObject:self
withObject:nil];
}
// entry point for the window controller to tell us what web page has been
// requested
//
// When the request is for the callback URL, this method invokes
// handleCallbackReached and returns YES
- (BOOL)requestRedirectedToRequest:(NSURLRequest *)redirectedRequest {
// for Google's installed app sign-in protocol, we'll look for the
// end-of-sign-in indicator in the titleChanged: method below
NSString *redirectURI = self.authentication.redirectURI;
if (redirectURI == nil) return NO;
// when we're searching for the window title string, then we can ignore
// redirects
NSString *standardURI = [[self class] nativeClientRedirectURI];
if (standardURI != nil && [redirectURI isEqual:standardURI]) return NO;
// compare the redirectURI, which tells us when the web sign-in is done,
// to the actual redirection
NSURL *redirectURL = [NSURL URLWithString:redirectURI];
NSURL *requestURL = [redirectedRequest URL];
// avoid comparing to nil host and path values (such as when redirected to
// "about:blank")
NSString *requestHost = [requestURL host];
NSString *requestPath = [requestURL path];
BOOL isCallback;
if (requestHost && requestPath) {
isCallback = [[redirectURL host] isEqual:[requestURL host]]
&& [[redirectURL path] isEqual:[requestURL path]];
} else if (requestURL) {
// handle "about:blank"
isCallback = [redirectURL isEqual:requestURL];
} else {
isCallback = NO;
}
if (!isCallback) {
// tell the caller that this request is nothing interesting
return NO;
}
// we've reached the callback URL
// try to get the access code
if (!self.hasHandledCallback) {
NSString *responseStr = [[redirectedRequest URL] query];
[self.authentication setKeysForResponseString:responseStr];
#if DEBUG
NSAssert([self.authentication.code length] > 0
|| [self.authentication.errorString length] > 0,
@"response lacks auth code or error");
#endif
[self handleCallbackReached];
}
// tell the delegate that we did handle this request
return YES;
}
// entry point for the window controller to tell us when a new page title has
// been loadded
//
// When the title indicates sign-in has completed, this method invokes
// handleCallbackReached and returns YES
- (BOOL)titleChanged:(NSString *)title {
// return YES if the OAuth flow ending title was detected
// right now we're just looking for a parameter list following the last space
// in the title string, but hopefully we'll eventually get something better
// from the server to search for
NSRange paramsRange = [title rangeOfString:@" "
options:NSBackwardsSearch];
NSUInteger spaceIndex = paramsRange.location;
if (spaceIndex != NSNotFound) {
NSString *responseStr = [title substringFromIndex:(spaceIndex + 1)];
NSDictionary *dict = [GTMOAuth2Authentication dictionaryWithResponseString:responseStr];
NSString *code = [dict objectForKey:@"code"];
NSString *error = [dict objectForKey:@"error"];
if ([code length] > 0 || [error length] > 0) {
if (!self.hasHandledCallback) {
[self.authentication setKeysForResponseDictionary:dict];
[self handleCallbackReached];
}
return YES;
}
}
return NO;
}
- (BOOL)cookiesChanged:(NSHTTPCookieStorage *)cookieStorage {
// We're ignoring these.
return NO;
};
// entry point for the window controller to tell us when a load has failed
// in the webview
//
// if the initial authorization URL fails, bail out so the user doesn't
// see an empty webview
- (BOOL)loadFailedWithError:(NSError *)error {
NSURL *authorizationURL = self.authorizationURL;
NSURL *failedURL = [[error userInfo] valueForKey:@"NSErrorFailingURLKey"]; // NSURLErrorFailingURLErrorKey defined in 10.6
BOOL isAuthURL = [[failedURL host] isEqual:[authorizationURL host]]
&& [[failedURL path] isEqual:[authorizationURL path]];
if (isAuthURL) {
// We can assume that we have no pending fetchers, since we only
// handle failure to load the initial authorization URL
[self closeTheWindow];
[self invokeFinalCallbackWithError:error];
return YES;
}
return NO;
}
- (void)handleCallbackReached {
// the callback page was requested, or the authenticate code was loaded
// into a page's title, so exchange the auth code for access & refresh tokens
// and tell the window to close
// avoid duplicate signals that the callback point has been reached
self.hasHandledCallback = YES;
[self closeTheWindow];
NSError *error = nil;
GTMOAuth2Authentication *auth = self.authentication;
NSString *code = auth.code;
if ([code length] > 0) {
// exchange the code for a token
SEL sel = @selector(auth:finishedWithFetcher:error:);
GTMHTTPFetcher *fetcher = [auth beginTokenFetchWithDelegate:self
didFinishSelector:sel];
if (fetcher == nil) {
error = [NSError errorWithDomain:kGTMHTTPFetcherStatusDomain
code:-1
userInfo:nil];
} else {
self.pendingFetcher = fetcher;
}
// notify the app so it can put up a post-sign in, pre-token exchange UI
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc postNotificationName:kGTMOAuth2UserSignedIn
object:self
userInfo:nil];
} else {
// the callback lacked an auth code
NSString *errStr = auth.errorString;
NSDictionary *userInfo = nil;
if ([errStr length] > 0) {
userInfo = [NSDictionary dictionaryWithObject:errStr
forKey:kGTMOAuth2ErrorMessageKey];
}
error = [NSError errorWithDomain:kGTMOAuth2ErrorDomain
code:kGTMOAuth2ErrorAuthorizationFailed
userInfo:userInfo];
}
if (error) {
[self finishSignInWithError:error];
}
}
- (void)auth:(GTMOAuth2Authentication *)auth
finishedWithFetcher:(GTMHTTPFetcher *)fetcher
error:(NSError *)error {
self.pendingFetcher = nil;
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
if (error == nil
&& (self.shouldFetchGoogleUserEmail || self.shouldFetchGoogleUserProfile)
&& [self.authentication.serviceProvider isEqual:kGTMOAuth2ServiceProviderGoogle]) {
// fetch the user's information from the Google server
[self fetchGoogleUserInfo];
} else {
// we're not authorizing with Google, so we're done
[self finishSignInWithError:error];
}
#else
[self finishSignInWithError:error];
#endif
}
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
+ (GTMHTTPFetcher *)userInfoFetcherWithAuth:(GTMOAuth2Authentication *)auth {
// create a fetcher for obtaining the user's email or profile
NSURL *infoURL = [[self class] googleUserInfoURL];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:infoURL];
NSString *userAgent = [auth userAgent];
[request setValue:userAgent forHTTPHeaderField:@"User-Agent"];
[request setValue:@"no-cache" forHTTPHeaderField:@"Cache-Control"];
GTMHTTPFetcher *fetcher;
id <GTMHTTPFetcherServiceProtocol> fetcherService = auth.fetcherService;
if (fetcherService) {
fetcher = [fetcherService fetcherWithRequest:request];
} else {
fetcher = [GTMHTTPFetcher fetcherWithRequest:request];
}
fetcher.authorizer = auth;
fetcher.retryEnabled = YES;
fetcher.maxRetryInterval = 15.0;
fetcher.comment = @"user info";
return fetcher;
}
- (void)fetchGoogleUserInfo {
// fetch the user's email address or profile
GTMOAuth2Authentication *auth = self.authentication;
GTMHTTPFetcher *fetcher = [[self class] userInfoFetcherWithAuth:auth];
[fetcher beginFetchWithDelegate:self
didFinishSelector:@selector(infoFetcher:finishedWithData:error:)];
self.pendingFetcher = fetcher;
[auth notifyFetchIsRunning:YES
fetcher:fetcher
type:kGTMOAuth2FetchTypeUserInfo];
}
- (void)infoFetcher:(GTMHTTPFetcher *)fetcher
finishedWithData:(NSData *)data
error:(NSError *)error {
GTMOAuth2Authentication *auth = self.authentication;
[auth notifyFetchIsRunning:NO
fetcher:fetcher
type:nil];
self.pendingFetcher = nil;
if (error) {
#if DEBUG
if (data) {
NSString *dataStr = [[[NSString alloc] initWithData:data
encoding:NSUTF8StringEncoding] autorelease];
NSLog(@"infoFetcher error: %@\n%@", error, dataStr);
}
#endif
} else {
// We have the authenticated user's info
if (data) {
NSDictionary *profileDict = [auth dictionaryWithJSONData:data];
if (profileDict) {
self.userProfile = profileDict;
// Save the email into the auth object
NSString *email = [profileDict objectForKey:@"email"];
[auth setUserEmail:email];
NSNumber *verified = [profileDict objectForKey:@"verified_email"];
[auth setUserEmailIsVerified:[verified stringValue]];
}
}
}
[self finishSignInWithError:error];
}
#endif // !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
- (void)finishSignInWithError:(NSError *)error {
[self invokeFinalCallbackWithError:error];
}
// convenience method for making the final call to our delegate
- (void)invokeFinalCallbackWithError:(NSError *)error {
if (delegate_ && finishedSelector_) {
GTMOAuth2Authentication *auth = self.authentication;
NSMethodSignature *sig = [delegate_ methodSignatureForSelector:finishedSelector_];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
[invocation setSelector:finishedSelector_];
[invocation setTarget:delegate_];
[invocation setArgument:&self atIndex:2];
[invocation setArgument:&auth atIndex:3];
[invocation setArgument:&error atIndex:4];
[invocation invoke];
}
// we'll no longer send messages to the delegate
//
// we want to autorelease it rather than assign to the property in case
// the delegate is below us in the call stack
[delegate_ autorelease];
delegate_ = nil;
}
#pragma mark Reachability monitoring
static void ReachabilityCallBack(SCNetworkReachabilityRef target,
SCNetworkConnectionFlags flags,
void *info) {
// pass the flags to the signIn object
GTMOAuth2SignIn *signIn = (GTMOAuth2SignIn *)info;
[signIn reachabilityTarget:target
changedFlags:flags];
}
- (void)startReachabilityCheck {
// the user may set the timeout to 0 to skip the reachability checking
// during display of the sign-in page
if (networkLossTimeoutInterval_ <= 0.0 || reachabilityRef_ != NULL) {
return;
}
// create a reachability target from the authorization URL, add our callback,
// and schedule it on the run loop so we'll be notified if the network drops
NSURL *url = self.authorizationURL;
const char* host = [[url host] UTF8String];
reachabilityRef_ = SCNetworkReachabilityCreateWithName(kCFAllocatorSystemDefault,
host);
if (reachabilityRef_) {
BOOL isScheduled = NO;
SCNetworkReachabilityContext ctx = { 0, self, NULL, NULL, NULL };
if (SCNetworkReachabilitySetCallback(reachabilityRef_,
ReachabilityCallBack, &ctx)) {
if (SCNetworkReachabilityScheduleWithRunLoop(reachabilityRef_,
CFRunLoopGetCurrent(),
kCFRunLoopDefaultMode)) {
isScheduled = YES;
}
}
if (!isScheduled) {
CFRelease(reachabilityRef_);
reachabilityRef_ = NULL;
}
}
}
- (void)destroyUnreachabilityTimer {
[networkLossTimer_ invalidate];
[networkLossTimer_ autorelease];
networkLossTimer_ = nil;
}
- (void)reachabilityTarget:(SCNetworkReachabilityRef)reachabilityRef
changedFlags:(SCNetworkConnectionFlags)flags {
BOOL isConnected = (flags & kSCNetworkFlagsReachable) != 0
&& (flags & kSCNetworkFlagsConnectionRequired) == 0;
if (isConnected) {
// server is again reachable
[self destroyUnreachabilityTimer];
if (hasNotifiedNetworkLoss_) {
// tell the user that the network has been found
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc postNotificationName:kGTMOAuth2NetworkFound
object:self
userInfo:nil];
hasNotifiedNetworkLoss_ = NO;
}
} else {
// the server has become unreachable; start the timer, if necessary
if (networkLossTimer_ == nil
&& networkLossTimeoutInterval_ > 0
&& !hasNotifiedNetworkLoss_) {
SEL sel = @selector(reachabilityTimerFired:);
networkLossTimer_ = [[NSTimer scheduledTimerWithTimeInterval:networkLossTimeoutInterval_
target:self
selector:sel
userInfo:nil
repeats:NO] retain];
}
}
}
- (void)reachabilityTimerFired:(NSTimer *)timer {
// the user may call [[notification object] cancelSigningIn] to
// dismiss the sign-in
if (!hasNotifiedNetworkLoss_) {
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc postNotificationName:kGTMOAuth2NetworkLost
object:self
userInfo:nil];
hasNotifiedNetworkLoss_ = YES;
}
[self destroyUnreachabilityTimer];
}
- (void)stopReachabilityCheck {
[self destroyUnreachabilityTimer];
if (reachabilityRef_) {
SCNetworkReachabilityUnscheduleFromRunLoop(reachabilityRef_,
CFRunLoopGetCurrent(),
kCFRunLoopDefaultMode);
SCNetworkReachabilitySetCallback(reachabilityRef_, NULL, NULL);
CFRelease(reachabilityRef_);
reachabilityRef_ = NULL;
}
}
#pragma mark Token Revocation
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
+ (void)revokeTokenForGoogleAuthentication:(GTMOAuth2Authentication *)auth {
if (auth.canAuthorize
&& [auth.serviceProvider isEqual:kGTMOAuth2ServiceProviderGoogle]) {
// create a signed revocation request for this authentication object
NSURL *url = [self googleRevocationURL];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
[request setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"];
NSString *token = auth.refreshToken;
NSString *encoded = [GTMOAuth2Authentication encodedOAuthValueForString:token];
NSString *body = [@"token=" stringByAppendingString:encoded];
[request setHTTPBody:[body dataUsingEncoding:NSUTF8StringEncoding]];
[request setHTTPMethod:@"POST"];
NSString *userAgent = [auth userAgent];
[request setValue:userAgent forHTTPHeaderField:@"User-Agent"];
// there's nothing to be done if revocation succeeds or fails
GTMHTTPFetcher *fetcher;
id <GTMHTTPFetcherServiceProtocol> fetcherService = auth.fetcherService;
if (fetcherService) {
fetcher = [fetcherService fetcherWithRequest:request];
} else {
fetcher = [GTMHTTPFetcher fetcherWithRequest:request];
}
fetcher.comment = @"revoke token";
// Use a completion handler fetch for better debugging, but only if we're
// guaranteed that blocks are available in the runtime
#if (!TARGET_OS_IPHONE && (MAC_OS_X_VERSION_MIN_REQUIRED >= 1060)) || \
(TARGET_OS_IPHONE && (__IPHONE_OS_VERSION_MIN_REQUIRED >= 40000))
// Blocks are available
[fetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *error) {
#if DEBUG
if (error) {
NSString *errStr = [[[NSString alloc] initWithData:data
encoding:NSUTF8StringEncoding] autorelease];
NSLog(@"revoke error: %@", errStr);
}
#endif // DEBUG
}];
#else
// Blocks may not be available
[fetcher beginFetchWithDelegate:nil didFinishSelector:NULL];
#endif
}
[auth reset];
}
#endif // !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
@end
#endif // #if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES

View File

@@ -0,0 +1,333 @@
/* Copyright (c) 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// GTMOAuth2WindowController
//
// This window controller for Mac handles sign-in via OAuth2 to Google or
// other services.
//
// This controller is not reusable; create a new instance of this controller
// every time the user will sign in.
//
// Sample usage for signing in to a Google service:
//
// static NSString *const kKeychainItemName = @”My App: Google Plus”;
// NSString *scope = @"https://www.googleapis.com/auth/plus.me";
//
//
// GTMOAuth2WindowController *windowController;
// windowController = [[[GTMOAuth2WindowController alloc] initWithScope:scope
// clientID:clientID
// clientSecret:clientSecret
// keychainItemName:kKeychainItemName
// resourceBundle:nil] autorelease];
//
// [windowController signInSheetModalForWindow:mMainWindow
// delegate:self
// finishedSelector:@selector(windowController:finishedWithAuth:error:)];
//
// The finished selector should have a signature matching this:
//
// - (void)windowController:(GTMOAuth2WindowController *)windowController
// finishedWithAuth:(GTMOAuth2Authentication *)auth
// error:(NSError *)error {
// if (error != nil) {
// // sign in failed
// } else {
// // sign in succeeded
// //
// // with the GTL library, pass the authentication to the service object,
// // like
// // [[self contactService] setAuthorizer:auth];
// //
// // or use it to sign a request directly, like
// // BOOL isAuthorizing = [self authorizeRequest:request
// // delegate:self
// // didFinishSelector:@selector(auth:finishedWithError:)];
// }
// }
//
// To sign in to services other than Google, use the longer init method,
// as shown in the sample application
//
// If the network connection is lost for more than 30 seconds while the sign-in
// html is displayed, the notification kGTLOAuthNetworkLost will be sent.
#if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES
#include <Foundation/Foundation.h>
#if !TARGET_OS_IPHONE
#import <Cocoa/Cocoa.h>
#import <WebKit/WebKit.h>
// GTMHTTPFetcher.h brings in GTLDefines/GDataDefines
#import "GTMHTTPFetcher.h"
#import "GTMOAuth2SignIn.h"
#import "GTMOAuth2Authentication.h"
#import "GTMHTTPFetchHistory.h" // for GTMCookieStorage
@class GTMOAuth2SignIn;
@interface GTMOAuth2WindowController : NSWindowController {
@private
// IBOutlets
NSButton *keychainCheckbox_;
WebView *webView_;
NSButton *webCloseButton_;
NSButton *webBackButton_;
// the object responsible for the sign-in networking sequence; it holds
// onto the authentication object as well
GTMOAuth2SignIn *signIn_;
// the page request to load when awakeFromNib occurs
NSURLRequest *initialRequest_;
// local storage for WebKit cookies so they're not shared with Safari
GTMCookieStorage *cookieStorage_;
// the user we're calling back
//
// the delegate is retained only until the callback is invoked
// or the sign-in is canceled
id delegate_;
SEL finishedSelector_;
#if NS_BLOCKS_AVAILABLE
void (^completionBlock_)(GTMOAuth2Authentication *, NSError *);
#elif !__LP64__
// placeholders: for 32-bit builds, keep the size of the object's ivar section
// the same with and without blocks
#ifndef __clang_analyzer__
id completionPlaceholder_;
#endif
#endif
// flag allowing application to quit during display of sign-in sheet on 10.6
// and later
BOOL shouldAllowApplicationTermination_;
// delegate method for handling URLs to be opened in external windows
SEL externalRequestSelector_;
BOOL isWindowShown_;
// paranoid flag to ensure we only close once during the sign-in sequence
BOOL hasDoneFinalRedirect_;
// paranoid flag to ensure we only call the user back once
BOOL hasCalledFinished_;
// if non-nil, we display as a sheet on the specified window
NSWindow *sheetModalForWindow_;
// if non-empty, the name of the application and service used for the
// keychain item
NSString *keychainItemName_;
// if non-nil, the html string to be displayed immediately upon opening
// of the web view
NSString *initialHTMLString_;
// if true, we allow default WebView handling of cookies, so the
// same user remains signed in each time the dialog is displayed
BOOL shouldPersistUser_;
// user-defined data
id userData_;
NSMutableDictionary *properties_;
}
// User interface elements
@property (nonatomic, assign) IBOutlet NSButton *keychainCheckbox;
@property (nonatomic, assign) IBOutlet WebView *webView;
@property (nonatomic, assign) IBOutlet NSButton *webCloseButton;
@property (nonatomic, assign) IBOutlet NSButton *webBackButton;
// The application and service name to use for saving the auth tokens
// to the keychain
@property (nonatomic, copy) NSString *keychainItemName;
// If true, the sign-in will remember which user was last signed in
//
// Defaults to false, so showing the sign-in window will always ask for
// the username and password, rather than skip to the grant authorization
// page. During development, it may be convenient to set this to true
// to speed up signing in.
@property (nonatomic, assign) BOOL shouldPersistUser;
// Optional html string displayed immediately upon opening the web view
//
// This string is visible just until the sign-in web page loads, and
// may be used for a "Loading..." type of message
@property (nonatomic, copy) NSString *initialHTMLString;
// The default timeout for an unreachable network during display of the
// sign-in page is 30 seconds, after which the notification
// kGTLOAuthNetworkLost is sent; set this to 0 to have no timeout
@property (nonatomic, assign) NSTimeInterval networkLossTimeoutInterval;
// On 10.6 and later, the sheet can allow application termination by calling
// NSWindow's setPreventsApplicationTerminationWhenModal:
@property (nonatomic, assign) BOOL shouldAllowApplicationTermination;
// Selector for a delegate method to handle requests sent to an external
// browser.
//
// Selector should have a signature matching
// - (void)windowController:(GTMOAuth2WindowController *)controller
// opensRequest:(NSURLRequest *)request;
//
// The controller's default behavior is to use NSWorkspace's openURL:
@property (nonatomic, assign) SEL externalRequestSelector;
// The underlying object to hold authentication tokens and authorize http
// requests
@property (nonatomic, retain, readonly) GTMOAuth2Authentication *authentication;
// The underlying object which performs the sign-in networking sequence
@property (nonatomic, retain, readonly) GTMOAuth2SignIn *signIn;
// Any arbitrary data object the user would like the controller to retain
@property (nonatomic, retain) id userData;
// Stored property values are retained for the convenience of the caller
- (void)setProperty:(id)obj forKey:(NSString *)key;
- (id)propertyForKey:(NSString *)key;
@property (nonatomic, retain) NSDictionary *properties;
- (IBAction)closeWindow:(id)sender;
- (IBAction)toggleStorePasswordInKeychain:(id)sender;
// Create a controller for authenticating to Google services
//
// scope is the requested scope of authorization
// (like "http://www.google.com/m8/feeds")
//
// keychainItemName is used for storing the token on the keychain,
// and is required for the "remember for later" checkbox to be shown;
// keychainItemName should be like "My Application: Google Contacts"
// (or set to nil if no persistent keychain storage is desired)
//
// resourceBundle may be nil if the window is in the main bundle's nib
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
+ (id)controllerWithScope:(NSString *)scope
clientID:(NSString *)clientID
clientSecret:(NSString *)clientSecret
keychainItemName:(NSString *)keychainItemName // may be nil
resourceBundle:(NSBundle *)bundle; // may be nil
- (id)initWithScope:(NSString *)scope
clientID:(NSString *)clientID
clientSecret:(NSString *)clientSecret
keychainItemName:(NSString *)keychainItemName
resourceBundle:(NSBundle *)bundle;
#endif
// Create a controller for authenticating to non-Google services, taking
// explicit endpoint URLs and an authentication object
+ (id)controllerWithAuthentication:(GTMOAuth2Authentication *)auth
authorizationURL:(NSURL *)authorizationURL
keychainItemName:(NSString *)keychainItemName // may be nil
resourceBundle:(NSBundle *)bundle; // may be nil
// This is the designated initializer
- (id)initWithAuthentication:(GTMOAuth2Authentication *)auth
authorizationURL:(NSURL *)authorizationURL
keychainItemName:(NSString *)keychainItemName
resourceBundle:(NSBundle *)bundle;
// Entry point to begin displaying the sign-in window
//
// the finished selector should have a signature matching
// - (void)windowController:(GTMOAuth2WindowController *)windowController
// finishedWithAuth:(GTMOAuth2Authentication *)auth
// error:(NSError *)error {
//
// Once the finished method has been invoked with no error, the auth object
// may be used to authorize requests (refreshing the access token, if necessary,
// and adding the auth header) like:
//
// [authorizer authorizeRequest:myNSMutableURLRequest]
// delegate:self
// didFinishSelector:@selector(auth:finishedWithError:)];
//
// or can be stored in a GTL service object like
// GTLServiceGoogleContact *service = [self contactService];
// [service setAuthorizer:auth];
//
// The delegate is retained only until the finished selector is invoked or
// the sign-in is canceled
- (void)signInSheetModalForWindow:(NSWindow *)parentWindowOrNil
delegate:(id)delegate
finishedSelector:(SEL)finishedSelector;
#if NS_BLOCKS_AVAILABLE
- (void)signInSheetModalForWindow:(NSWindow *)parentWindowOrNil
completionHandler:(void (^)(GTMOAuth2Authentication *auth, NSError *error))handler;
#endif
- (void)cancelSigningIn;
// Subclasses may override authNibName to specify a custom name
+ (NSString *)authNibName;
// apps may replace the sign-in class with their own subclass of it
+ (Class)signInClass;
+ (void)setSignInClass:(Class)theClass;
// Revocation of an authorized token from Google
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
+ (void)revokeTokenForGoogleAuthentication:(GTMOAuth2Authentication *)auth;
#endif
// Keychain
//
// The keychain checkbox is shown if the keychain application service
// name (typically set in the initWithScope: method) is non-empty
//
// Create an authentication object for Google services from the access
// token and secret stored in the keychain; if no token is available, return
// an unauthorized auth object
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
+ (GTMOAuth2Authentication *)authForGoogleFromKeychainForName:(NSString *)keychainItemName
clientID:(NSString *)clientID
clientSecret:(NSString *)clientSecret;
#endif
// Add tokens from the keychain, if available, to the authentication object
//
// returns YES if the authentication object was authorized from the keychain
+ (BOOL)authorizeFromKeychainForName:(NSString *)keychainItemName
authentication:(GTMOAuth2Authentication *)auth;
// Method for deleting the stored access token and secret, useful for "signing
// out"
+ (BOOL)removeAuthFromKeychainForName:(NSString *)keychainItemName;
// Method for saving the stored access token and secret; typically, this method
// is used only by the window controller
+ (BOOL)saveAuthToKeychainForName:(NSString *)keychainItemName
authentication:(GTMOAuth2Authentication *)auth;
@end
#endif // #if !TARGET_OS_IPHONE
#endif // #if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES

View File

@@ -0,0 +1,738 @@
/* Copyright (c) 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
#if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES
#if !TARGET_OS_IPHONE
#import "GTMOAuth2WindowController.h"
@interface GTMOAuth2WindowController ()
@property (nonatomic, retain) GTMOAuth2SignIn *signIn;
@property (nonatomic, copy) NSURLRequest *initialRequest;
@property (nonatomic, retain) GTMCookieStorage *cookieStorage;
@property (nonatomic, retain) NSWindow *sheetModalForWindow;
- (void)signInCommonForWindow:(NSWindow *)parentWindowOrNil;
- (void)setupSheetTerminationHandling;
- (void)destroyWindow;
- (void)handlePrematureWindowClose;
- (BOOL)shouldUseKeychain;
- (void)signIn:(GTMOAuth2SignIn *)signIn displayRequest:(NSURLRequest *)request;
- (void)signIn:(GTMOAuth2SignIn *)signIn finishedWithAuth:(GTMOAuth2Authentication *)auth error:(NSError *)error;
- (void)sheetDidEnd:(NSWindow *)sheet returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo;
- (void)handleCookiesForResponse:(NSURLResponse *)response;
- (NSURLRequest *)addCookiesToRequest:(NSURLRequest *)request;
@end
const char *kKeychainAccountName = "OAuth";
@implementation GTMOAuth2WindowController
// IBOutlets
@synthesize keychainCheckbox = keychainCheckbox_,
webView = webView_,
webCloseButton = webCloseButton_,
webBackButton = webBackButton_;
// regular ivars
@synthesize signIn = signIn_,
initialRequest = initialRequest_,
cookieStorage = cookieStorage_,
sheetModalForWindow = sheetModalForWindow_,
keychainItemName = keychainItemName_,
initialHTMLString = initialHTMLString_,
shouldAllowApplicationTermination = shouldAllowApplicationTermination_,
externalRequestSelector = externalRequestSelector_,
shouldPersistUser = shouldPersistUser_,
userData = userData_,
properties = properties_;
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
// Create a controller for authenticating to Google services
+ (id)controllerWithScope:(NSString *)scope
clientID:(NSString *)clientID
clientSecret:(NSString *)clientSecret
keychainItemName:(NSString *)keychainItemName
resourceBundle:(NSBundle *)bundle {
return [[[self alloc] initWithScope:scope
clientID:clientID
clientSecret:clientSecret
keychainItemName:keychainItemName
resourceBundle:bundle] autorelease];
}
- (id)initWithScope:(NSString *)scope
clientID:(NSString *)clientID
clientSecret:(NSString *)clientSecret
keychainItemName:(NSString *)keychainItemName
resourceBundle:(NSBundle *)bundle {
Class signInClass = [[self class] signInClass];
GTMOAuth2Authentication *auth;
auth = [signInClass standardGoogleAuthenticationForScope:scope
clientID:clientID
clientSecret:clientSecret];
NSURL *authorizationURL = [signInClass googleAuthorizationURL];
return [self initWithAuthentication:auth
authorizationURL:authorizationURL
keychainItemName:keychainItemName
resourceBundle:bundle];
}
#endif
// Create a controller for authenticating to any service
+ (id)controllerWithAuthentication:(GTMOAuth2Authentication *)auth
authorizationURL:(NSURL *)authorizationURL
keychainItemName:(NSString *)keychainItemName
resourceBundle:(NSBundle *)bundle {
return [[[self alloc] initWithAuthentication:auth
authorizationURL:authorizationURL
keychainItemName:keychainItemName
resourceBundle:bundle] autorelease];
}
- (id)initWithAuthentication:(GTMOAuth2Authentication *)auth
authorizationURL:(NSURL *)authorizationURL
keychainItemName:(NSString *)keychainItemName
resourceBundle:(NSBundle *)bundle {
if (bundle == nil) {
bundle = [NSBundle mainBundle];
}
NSString *nibName = [[self class] authNibName];
NSString *nibPath = [bundle pathForResource:nibName
ofType:@"nib"];
self = [super initWithWindowNibPath:nibPath
owner:self];
if (self != nil) {
// use the supplied auth and OAuth endpoint URLs
Class signInClass = [[self class] signInClass];
signIn_ = [[signInClass alloc] initWithAuthentication:auth
authorizationURL:authorizationURL
delegate:self
webRequestSelector:@selector(signIn:displayRequest:)
finishedSelector:@selector(signIn:finishedWithAuth:error:)];
keychainItemName_ = [keychainItemName copy];
// create local, temporary storage for WebKit cookies
cookieStorage_ = [[GTMCookieStorage alloc] init];
}
return self;
}
- (void)dealloc {
[signIn_ release];
[initialRequest_ release];
[cookieStorage_ release];
[delegate_ release];
#if NS_BLOCKS_AVAILABLE
[completionBlock_ release];
#endif
[sheetModalForWindow_ release];
[keychainItemName_ release];
[initialHTMLString_ release];
[userData_ release];
[properties_ release];
[super dealloc];
}
- (void)awakeFromNib {
// load the requested initial sign-in page
[self.webView setResourceLoadDelegate:self];
[self.webView setPolicyDelegate:self];
// the app may prefer some html other than blank white to be displayed
// before the sign-in web page loads
NSString *html = self.initialHTMLString;
if ([html length] > 0) {
[[self.webView mainFrame] loadHTMLString:html baseURL:nil];
}
// hide the keychain checkbox if we're not supporting keychain
BOOL hideKeychainCheckbox = ![self shouldUseKeychain];
const NSTimeInterval kJanuary2011 = 1293840000;
BOOL isDateValid = ([[NSDate date] timeIntervalSince1970] > kJanuary2011);
if (isDateValid) {
// start the asynchronous load of the sign-in web page
[[self.webView mainFrame] performSelector:@selector(loadRequest:)
withObject:self.initialRequest
afterDelay:0.01
inModes:[NSArray arrayWithObject:NSRunLoopCommonModes]];
} else {
// clock date is invalid, so signing in would fail with an unhelpful error
// from the server. Warn the user in an html string showing a watch icon,
// question mark, and the system date and time. Hopefully this will clue
// in brighter users, or at least let them make a useful screenshot to show
// to developers.
//
// Even better is for apps to check the system clock and show some more
// helpful, localized instructions for users; this is really a fallback.
NSString *htmlTemplate = @"<html><body><div align=center><font size='7'>"
@"&#x231A; ?<br><i>System Clock Incorrect</i><br>%@"
@"</font></div></body></html>";
NSString *errHTML = [NSString stringWithFormat:htmlTemplate, [NSDate date]];
[[webView_ mainFrame] loadHTMLString:errHTML baseURL:nil];
hideKeychainCheckbox = YES;
}
#if DEBUG
// Verify that Javascript is enabled
BOOL hasJS = [[webView_ preferences] isJavaScriptEnabled];
NSAssert(hasJS, @"GTMOAuth2: Javascript is required");
#endif
[keychainCheckbox_ setHidden:hideKeychainCheckbox];
}
+ (NSString *)authNibName {
// subclasses may override this to specify a custom nib name
return @"GTMOAuth2Window";
}
#pragma mark -
- (void)signInSheetModalForWindow:(NSWindow *)parentWindowOrNil
delegate:(id)delegate
finishedSelector:(SEL)finishedSelector {
// check the selector on debug builds
GTMAssertSelectorNilOrImplementedWithArgs(delegate, finishedSelector,
@encode(GTMOAuth2WindowController *), @encode(GTMOAuth2Authentication *),
@encode(NSError *), 0);
delegate_ = [delegate retain];
finishedSelector_ = finishedSelector;
[self signInCommonForWindow:parentWindowOrNil];
}
#if NS_BLOCKS_AVAILABLE
- (void)signInSheetModalForWindow:(NSWindow *)parentWindowOrNil
completionHandler:(void (^)(GTMOAuth2Authentication *, NSError *))handler {
completionBlock_ = [handler copy];
[self signInCommonForWindow:parentWindowOrNil];
}
#endif
- (void)signInCommonForWindow:(NSWindow *)parentWindowOrNil {
self.sheetModalForWindow = parentWindowOrNil;
hasDoneFinalRedirect_ = NO;
hasCalledFinished_ = NO;
[self.signIn startSigningIn];
}
- (void)cancelSigningIn {
// The user has explicitly asked us to cancel signing in
// (so no further callback is required)
hasCalledFinished_ = YES;
[delegate_ autorelease];
delegate_ = nil;
#if NS_BLOCKS_AVAILABLE
[completionBlock_ autorelease];
completionBlock_ = nil;
#endif
// The signIn object's cancel method will close the window
[self.signIn cancelSigningIn];
hasDoneFinalRedirect_ = YES;
}
- (IBAction)closeWindow:(id)sender {
// dismiss the window/sheet before we call back the client
[self destroyWindow];
[self handlePrematureWindowClose];
}
- (IBAction)toggleStorePasswordInKeychain:(id)sender {
if ([sender state] == NSOffState) {
NSBeginAlertSheet(NSLocalizedString(@"Do you want to disable saving your login data in your keychain?", nil), NSLocalizedString(@"Keep enabled", nil), NSLocalizedString(@"Disable", nil), nil, self.window, self, @selector(disableKeychainSheetDidEnd:returnCode:contextInfo:), nil, NULL, NSLocalizedString(@"If you do not save the password you will have to sign in every time you use this feature.", nil));
}
}
- (void)disableKeychainSheetDidEnd:(NSWindow *)sheet returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo
{
if (returnCode == NSAlertDefaultReturn) {
self.keychainCheckbox.state = NSOnState;
}
}
#pragma mark SignIn callbacks
- (void)signIn:(GTMOAuth2SignIn *)signIn displayRequest:(NSURLRequest *)request {
// this is the signIn object's webRequest method, telling the controller
// to either display the request in the webview, or close the window
//
// All web requests and all window closing goes through this routine
#if DEBUG
if ((isWindowShown_ && request != nil)
|| (!isWindowShown_ && request == nil)) {
NSLog(@"Window state unexpected for request %@", [request URL]);
return;
}
#endif
if (request != nil) {
// display the request
self.initialRequest = request;
NSWindow *parentWindow = self.sheetModalForWindow;
if (parentWindow) {
[self setupSheetTerminationHandling];
NSWindow *sheet = [self window];
[NSApp beginSheet:sheet
modalForWindow:parentWindow
modalDelegate:self
didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:)
contextInfo:nil];
} else {
// modeless
[self showWindow:self];
}
isWindowShown_ = YES;
} else {
// request was nil
[self destroyWindow];
}
}
- (void)setupSheetTerminationHandling {
NSWindow *sheet = [self window];
SEL sel = @selector(setPreventsApplicationTerminationWhenModal:);
if ([sheet respondsToSelector:sel]) {
// setPreventsApplicationTerminationWhenModal is available in NSWindow
// on 10.6 and later
BOOL boolVal = !self.shouldAllowApplicationTermination;
NSMethodSignature *sig = [sheet methodSignatureForSelector:sel];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
[invocation setSelector:sel];
[invocation setTarget:sheet];
[invocation setArgument:&boolVal atIndex:2];
[invocation invoke];
}
}
- (void)destroyWindow {
// no request; close the window (but not immediately, in case
// we're called in response to some window event)
NSWindow *parentWindow = self.sheetModalForWindow;
if (parentWindow) {
[NSApp endSheet:[self window]];
} else {
[[self window] performSelector:@selector(close)
withObject:nil
afterDelay:0.1
inModes:[NSArray arrayWithObject:NSRunLoopCommonModes]];
// Avoid more callbacks after the delayed close happens, as the window
// controller may be gone.
[[self webView] setResourceLoadDelegate:nil];
}
isWindowShown_ = NO;
}
- (void)handlePrematureWindowClose {
if (!hasDoneFinalRedirect_) {
// tell the sign-in object to tell the user's finished method
// that we're done
[self.signIn windowWasClosed];
hasDoneFinalRedirect_ = YES;
}
}
- (void)sheetDidEnd:(NSWindow *)sheet returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo {
[sheet orderOut:self];
self.sheetModalForWindow = nil;
}
- (void)signIn:(GTMOAuth2SignIn *)signIn finishedWithAuth:(GTMOAuth2Authentication *)auth error:(NSError *)error {
if (!hasCalledFinished_) {
hasCalledFinished_ = YES;
if (error == nil) {
BOOL shouldUseKeychain = [self shouldUseKeychain];
if (shouldUseKeychain) {
BOOL canAuthorize = auth.canAuthorize;
BOOL isKeychainChecked = ([keychainCheckbox_ state] == NSOnState);
NSString *keychainItemName = self.keychainItemName;
if (isKeychainChecked && canAuthorize) {
// save the auth params in the keychain
[[self class] saveAuthToKeychainForName:keychainItemName
authentication:auth];
} else {
// remove the auth params from the keychain
[[self class] removeAuthFromKeychainForName:keychainItemName];
}
}
}
if (delegate_ && finishedSelector_) {
SEL sel = finishedSelector_;
NSMethodSignature *sig = [delegate_ methodSignatureForSelector:sel];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
[invocation setSelector:sel];
[invocation setTarget:delegate_];
[invocation setArgument:&self atIndex:2];
[invocation setArgument:&auth atIndex:3];
[invocation setArgument:&error atIndex:4];
[invocation invoke];
}
[delegate_ autorelease];
delegate_ = nil;
#if NS_BLOCKS_AVAILABLE
if (completionBlock_) {
completionBlock_(auth, error);
// release the block here to avoid a retain loop on the controller
[completionBlock_ autorelease];
completionBlock_ = nil;
}
#endif
}
}
static Class gSignInClass = Nil;
+ (Class)signInClass {
if (gSignInClass == Nil) {
gSignInClass = [GTMOAuth2SignIn class];
}
return gSignInClass;
}
+ (void)setSignInClass:(Class)theClass {
gSignInClass = theClass;
}
#pragma mark Token Revocation
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
+ (void)revokeTokenForGoogleAuthentication:(GTMOAuth2Authentication *)auth {
[[self signInClass] revokeTokenForGoogleAuthentication:auth];
}
#endif
#pragma mark WebView methods
- (NSURLRequest *)webView:(WebView *)sender resource:(id)identifier willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)redirectResponse fromDataSource:(WebDataSource *)dataSource {
// override WebKit's cookie storage with our own to avoid cookie persistence
// across sign-ins and interaction with the Safari browser's sign-in state
[self handleCookiesForResponse:redirectResponse];
request = [self addCookiesToRequest:request];
if (!hasDoneFinalRedirect_) {
hasDoneFinalRedirect_ = [self.signIn requestRedirectedToRequest:request];
if (hasDoneFinalRedirect_) {
// signIn has told the window to close
return nil;
}
}
return request;
}
- (void)webView:(WebView *)sender resource:(id)identifier didReceiveResponse:(NSURLResponse *)response fromDataSource:(WebDataSource *)dataSource {
// override WebKit's cookie storage with our own
[self handleCookiesForResponse:response];
}
- (void)webView:(WebView *)sender resource:(id)identifier didFinishLoadingFromDataSource:(WebDataSource *)dataSource {
NSString *title = [sender stringByEvaluatingJavaScriptFromString:@"document.title"];
if ([title length] > 0) {
[self.signIn titleChanged:title];
}
[signIn_ cookiesChanged:(NSHTTPCookieStorage *)cookieStorage_];
}
- (void)webView:(WebView *)sender resource:(id)identifier didFailLoadingWithError:(NSError *)error fromDataSource:(WebDataSource *)dataSource {
[self.signIn loadFailedWithError:error];
}
- (void)windowWillClose:(NSNotification *)note {
if (isWindowShown_) {
[self handlePrematureWindowClose];
}
isWindowShown_ = NO;
}
- (void)webView:(WebView *)webView
decidePolicyForNewWindowAction:(NSDictionary *)actionInformation
request:(NSURLRequest *)request
newFrameName:(NSString *)frameName
decisionListener:(id<WebPolicyDecisionListener>)listener {
SEL sel = self.externalRequestSelector;
if (sel) {
[delegate_ performSelector:sel
withObject:self
withObject:request];
} else {
// default behavior is to open the URL in NSWorkspace's default browser
NSURL *url = [request URL];
[[NSWorkspace sharedWorkspace] openURL:url];
}
[listener ignore];
}
#pragma mark Cookie management
// Rather than let the WebView use Safari's default cookie storage, we intercept
// requests and response to segregate and later discard cookies from signing in.
//
// This allows the application to actually sign out by discarding the auth token
// rather than the user being kept signed in by the cookies.
- (void)handleCookiesForResponse:(NSURLResponse *)response {
if (self.shouldPersistUser) {
// we'll let WebKit handle the cookies; they'll persist across apps
// and across runs of this app
return;
}
if ([response respondsToSelector:@selector(allHeaderFields)]) {
// grab the cookies from the header as NSHTTPCookies and store them locally
NSDictionary *headers = [(NSHTTPURLResponse *)response allHeaderFields];
if (headers) {
NSURL *url = [response URL];
NSArray *cookies = [NSHTTPCookie cookiesWithResponseHeaderFields:headers
forURL:url];
if ([cookies count] > 0) {
[cookieStorage_ setCookies:cookies];
}
}
}
}
- (NSURLRequest *)addCookiesToRequest:(NSURLRequest *)request {
if (self.shouldPersistUser) {
// we'll let WebKit handle the cookies; they'll persist across apps
// and across runs of this app
return request;
}
// override WebKit's usual automatic storage of cookies
NSMutableURLRequest *mutableRequest = [[request mutableCopy] autorelease];
[mutableRequest setHTTPShouldHandleCookies:NO];
// add our locally-stored cookies for this URL, if any
NSArray *cookies = [cookieStorage_ cookiesForURL:[request URL]];
if ([cookies count] > 0) {
NSDictionary *headers = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies];
NSString *cookieHeader = [headers objectForKey:@"Cookie"];
if (cookieHeader) {
[mutableRequest setValue:cookieHeader forHTTPHeaderField:@"Cookie"];
}
}
return mutableRequest;
}
#pragma mark Keychain support
+ (NSString *)prefsKeyForName:(NSString *)keychainItemName {
NSString *result = [@"OAuth2: " stringByAppendingString:keychainItemName];
return result;
}
+ (BOOL)saveAuthToKeychainForName:(NSString *)keychainItemName
authentication:(GTMOAuth2Authentication *)auth {
[self removeAuthFromKeychainForName:keychainItemName];
// don't save unless we have a token that can really authorize requests
if (!auth.canAuthorize) return NO;
// make a response string containing the values we want to save
NSString *password = [auth persistenceResponseString];
SecKeychainRef defaultKeychain = NULL;
SecKeychainItemRef *dontWantItemRef= NULL;
const char *utf8ServiceName = [keychainItemName UTF8String];
const char *utf8Password = [password UTF8String];
OSStatus err = SecKeychainAddGenericPassword(defaultKeychain,
(UInt32) strlen(utf8ServiceName), utf8ServiceName,
(UInt32) strlen(kKeychainAccountName), kKeychainAccountName,
(UInt32) strlen(utf8Password), utf8Password,
dontWantItemRef);
BOOL didSucceed = (err == noErr);
if (didSucceed) {
// write to preferences that we have a keychain item (so we know later
// that we can read from the keychain without raising a permissions dialog)
NSString *prefKey = [self prefsKeyForName:keychainItemName];
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setBool:YES forKey:prefKey];
}
return didSucceed;
}
+ (BOOL)removeAuthFromKeychainForName:(NSString *)keychainItemName {
SecKeychainRef defaultKeychain = NULL;
SecKeychainItemRef itemRef = NULL;
const char *utf8ServiceName = [keychainItemName UTF8String];
// we don't really care about the password here, we just want to
// get the SecKeychainItemRef so we can delete it.
OSStatus err = SecKeychainFindGenericPassword (defaultKeychain,
(UInt32) strlen(utf8ServiceName), utf8ServiceName,
(UInt32) strlen(kKeychainAccountName), kKeychainAccountName,
0, NULL, // ignore password
&itemRef);
if (err != noErr) {
// failure to find is success
return YES;
} else {
// found something, so delete it
err = SecKeychainItemDelete(itemRef);
CFRelease(itemRef);
// remove our preference key
NSString *prefKey = [self prefsKeyForName:keychainItemName];
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults removeObjectForKey:prefKey];
return (err == noErr);
}
}
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
+ (GTMOAuth2Authentication *)authForGoogleFromKeychainForName:(NSString *)keychainItemName
clientID:(NSString *)clientID
clientSecret:(NSString *)clientSecret {
Class signInClass = [self signInClass];
NSURL *tokenURL = [signInClass googleTokenURL];
NSString *redirectURI = [signInClass nativeClientRedirectURI];
GTMOAuth2Authentication *auth;
auth = [GTMOAuth2Authentication authenticationWithServiceProvider:kGTMOAuth2ServiceProviderGoogle
tokenURL:tokenURL
redirectURI:redirectURI
clientID:clientID
clientSecret:clientSecret];
[GTMOAuth2WindowController authorizeFromKeychainForName:keychainItemName
authentication:auth];
return auth;
}
#endif
+ (BOOL)authorizeFromKeychainForName:(NSString *)keychainItemName
authentication:(GTMOAuth2Authentication *)newAuth {
[newAuth setAccessToken:nil];
// before accessing the keychain, check preferences to verify that we've
// previously saved a token to the keychain (so we don't needlessly raise
// a keychain access permission dialog)
NSString *prefKey = [self prefsKeyForName:keychainItemName];
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
BOOL flag = [defaults boolForKey:prefKey];
if (!flag) {
return NO;
}
BOOL didGetTokens = NO;
SecKeychainRef defaultKeychain = NULL;
const char *utf8ServiceName = [keychainItemName UTF8String];
SecKeychainItemRef *dontWantItemRef = NULL;
void *passwordBuff = NULL;
UInt32 passwordBuffLength = 0;
OSStatus err = SecKeychainFindGenericPassword(defaultKeychain,
(UInt32) strlen(utf8ServiceName), utf8ServiceName,
(UInt32) strlen(kKeychainAccountName), kKeychainAccountName,
&passwordBuffLength, &passwordBuff,
dontWantItemRef);
if (err == noErr && passwordBuff != NULL) {
NSString *password = [[[NSString alloc] initWithBytes:passwordBuff
length:passwordBuffLength
encoding:NSUTF8StringEncoding] autorelease];
// free the password buffer that was allocated above
SecKeychainItemFreeContent(NULL, passwordBuff);
if (password != nil) {
[newAuth setKeysForResponseString:password];
didGetTokens = YES;
}
}
return didGetTokens;
}
#pragma mark User Properties
- (void)setProperty:(id)obj forKey:(NSString *)key {
if (obj == nil) {
// User passed in nil, so delete the property
[properties_ removeObjectForKey:key];
} else {
// Be sure the property dictionary exists
if (properties_ == nil) {
[self setProperties:[NSMutableDictionary dictionary]];
}
[properties_ setObject:obj forKey:key];
}
}
- (id)propertyForKey:(NSString *)key {
id obj = [properties_ objectForKey:key];
// Be sure the returned pointer has the life of the autorelease pool,
// in case self is released immediately
return [[obj retain] autorelease];
}
#pragma mark Accessors
- (GTMOAuth2Authentication *)authentication {
return self.signIn.authentication;
}
- (void)setNetworkLossTimeoutInterval:(NSTimeInterval)val {
self.signIn.networkLossTimeoutInterval = val;
}
- (NSTimeInterval)networkLossTimeoutInterval {
return self.signIn.networkLossTimeoutInterval;
}
- (BOOL)shouldUseKeychain {
NSString *name = self.keychainItemName;
return ([name length] > 0);
}
@end
#endif // #if !TARGET_OS_IPHONE
#endif // #if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES

13
OAuth 2/OAuth2.h Executable file
View File

@@ -0,0 +1,13 @@
//
// NSObject_OAuth2.h
// Notifications for YouTube
//
// Created by Kim Wittenburg on 22.09.12.
// Copyright (c) 2012 Kim Wittenburg. All rights reserved.
//
#import "GTMHTTPFetcher.h"
#import "GTMHTTPFetchHistory.h"
#import "GTMOAuth2Authentication.h"
#import "GTMOAuth2SignIn.h"
#import "GTMOAuth2WindowController.h"

View File

@@ -0,0 +1,568 @@
<?xml version="1.0" encoding="UTF-8"?>
<archive type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="7.10">
<data>
<int key="IBDocument.SystemTarget">1050</int>
<string key="IBDocument.SystemVersion">12E55</string>
<string key="IBDocument.InterfaceBuilderVersion">3084</string>
<string key="IBDocument.AppKitVersion">1187.39</string>
<string key="IBDocument.HIToolboxVersion">626.00</string>
<object class="NSMutableDictionary" key="IBDocument.PluginVersions">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.WebKitIBPlugin</string>
</object>
<object class="NSArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>3084</string>
<string>2053</string>
</object>
</object>
<object class="NSArray" key="IBDocument.IntegratedClassDependencies">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>NSButton</string>
<string>NSButtonCell</string>
<string>NSCustomObject</string>
<string>NSUserDefaultsController</string>
<string>NSView</string>
<string>NSWindowTemplate</string>
<string>WebView</string>
</object>
<object class="NSArray" key="IBDocument.PluginDependencies">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.WebKitIBPlugin</string>
</object>
<object class="NSMutableDictionary" key="IBDocument.Metadata">
<string key="NS.key.0">PluginDependencyRecalculationVersion</string>
<integer value="1" key="NS.object.0"/>
</object>
<object class="NSMutableArray" key="IBDocument.RootObjects" id="1000">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSCustomObject" id="1001">
<string key="NSClassName">GTMOAuth2WindowController</string>
</object>
<object class="NSCustomObject" id="1003">
<string key="NSClassName">FirstResponder</string>
</object>
<object class="NSCustomObject" id="1004">
<string key="NSClassName">NSApplication</string>
</object>
<object class="NSWindowTemplate" id="996099970">
<int key="NSWindowStyleMask">11</int>
<int key="NSWindowBacking">2</int>
<string key="NSWindowRect">{{522, 328}, {515, 419}}</string>
<int key="NSWTFlags">536870912</int>
<string key="NSWindowTitle">Anmelden</string>
<string key="NSWindowClass">NSWindow</string>
<nil key="NSViewClass"/>
<nil key="NSUserInterfaceItemIdentifier"/>
<string key="NSWindowContentMinSize">{475, 290}</string>
<object class="NSView" key="NSWindowView" id="563959409">
<reference key="NSNextResponder"/>
<int key="NSvFlags">256</int>
<object class="NSMutableArray" key="NSSubviews">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="WebView" id="697605106">
<reference key="NSNextResponder" ref="563959409"/>
<int key="NSvFlags">274</int>
<object class="NSMutableSet" key="NSDragTypes">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="set.sortedObjects">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>Apple HTML pasteboard type</string>
<string>Apple PDF pasteboard type</string>
<string>Apple PICT pasteboard type</string>
<string>Apple URL pasteboard type</string>
<string>Apple Web Archive pasteboard type</string>
<string>NSColor pasteboard type</string>
<string>NSFilenamesPboardType</string>
<string>NSStringPboardType</string>
<string>NeXT RTFD pasteboard type</string>
<string>NeXT Rich Text Format v1.0 pasteboard type</string>
<string>NeXT TIFF v4.0 pasteboard type</string>
<string>WebURLsWithTitlesPboardType</string>
<string>public.png</string>
<string>public.url</string>
<string>public.url-name</string>
</object>
</object>
<string key="NSFrame">{{0, 20}, {515, 399}}</string>
<reference key="NSSuperview" ref="563959409"/>
<reference key="NSNextKeyView"/>
<string key="FrameName"/>
<string key="GroupName"/>
<object class="WebPreferences" key="Preferences">
<string key="Identifier"/>
<object class="NSMutableDictionary" key="Values">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>WebKitDefaultFixedFontSize</string>
<string>WebKitDefaultFontSize</string>
<string>WebKitMinimumFontSize</string>
</object>
<object class="NSArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<integer value="12"/>
<integer value="12"/>
<integer value="1"/>
</object>
</object>
</object>
<bool key="UseBackForwardList">YES</bool>
<bool key="AllowsUndo">YES</bool>
</object>
<object class="NSButton" id="736908656">
<reference key="NSNextResponder" ref="563959409"/>
<int key="NSvFlags">289</int>
<string key="NSFrame">{{479, 0}, {16, 19}}</string>
<reference key="NSSuperview" ref="563959409"/>
<bool key="NSEnabled">YES</bool>
<object class="NSButtonCell" key="NSCell" id="215388286">
<int key="NSCellFlags">67108864</int>
<int key="NSCellFlags2">134217728</int>
<string key="NSContents"/>
<object class="NSFont" key="NSSupport" id="895134484">
<string key="NSName">LucidaGrande</string>
<double key="NSSize">13</double>
<int key="NSfFlags">1044</int>
</object>
<reference key="NSControlView" ref="736908656"/>
<int key="NSButtonFlags">-2041823232</int>
<int key="NSButtonFlags2">134</int>
<object class="NSCustomResource" key="NSNormalImage">
<string key="NSClassName">NSImage</string>
<string key="NSResourceName">NSStopProgressTemplate</string>
</object>
<string key="NSAlternateContents"/>
<string type="base64-UTF8" key="NSKeyEquivalent">Gw</string>
<int key="NSPeriodicDelay">400</int>
<int key="NSPeriodicInterval">75</int>
</object>
<bool key="NSAllowsLogicalLayoutDirection">NO</bool>
</object>
<object class="NSButton" id="771759786">
<reference key="NSNextResponder" ref="563959409"/>
<int key="NSvFlags">289</int>
<string key="NSFrame">{{437, 0}, {16, 19}}</string>
<reference key="NSSuperview" ref="563959409"/>
<reference key="NSNextKeyView" ref="36322049"/>
<bool key="NSEnabled">YES</bool>
<object class="NSButtonCell" key="NSCell" id="656055052">
<int key="NSCellFlags">67108864</int>
<int key="NSCellFlags2">134217728</int>
<string key="NSContents"/>
<reference key="NSSupport" ref="895134484"/>
<reference key="NSControlView" ref="771759786"/>
<int key="NSButtonFlags">-2041823232</int>
<int key="NSButtonFlags2">134</int>
<object class="NSCustomResource" key="NSNormalImage">
<string key="NSClassName">NSImage</string>
<string key="NSResourceName">NSGoLeftTemplate</string>
</object>
<string key="NSAlternateContents"/>
<string key="NSKeyEquivalent"/>
<int key="NSPeriodicDelay">400</int>
<int key="NSPeriodicInterval">75</int>
</object>
<bool key="NSAllowsLogicalLayoutDirection">NO</bool>
</object>
<object class="NSButton" id="36322049">
<reference key="NSNextResponder" ref="563959409"/>
<int key="NSvFlags">289</int>
<string key="NSFrame">{{456, 0}, {16, 19}}</string>
<reference key="NSSuperview" ref="563959409"/>
<reference key="NSNextKeyView" ref="736908656"/>
<bool key="NSEnabled">YES</bool>
<object class="NSButtonCell" key="NSCell" id="282674264">
<int key="NSCellFlags">67108864</int>
<int key="NSCellFlags2">134217728</int>
<string key="NSContents"/>
<reference key="NSSupport" ref="895134484"/>
<reference key="NSControlView" ref="36322049"/>
<int key="NSButtonFlags">-2042347520</int>
<int key="NSButtonFlags2">134</int>
<object class="NSCustomResource" key="NSNormalImage">
<string key="NSClassName">NSImage</string>
<string key="NSResourceName">NSGoRightTemplate</string>
</object>
<string key="NSAlternateContents"/>
<string key="NSKeyEquivalent"/>
<int key="NSPeriodicDelay">400</int>
<int key="NSPeriodicInterval">75</int>
</object>
<bool key="NSAllowsLogicalLayoutDirection">NO</bool>
</object>
<object class="NSButton" id="823054555">
<reference key="NSNextResponder" ref="563959409"/>
<int key="NSvFlags">292</int>
<string key="NSFrame">{{2, 1}, {429, 18}}</string>
<reference key="NSSuperview" ref="563959409"/>
<reference key="NSNextKeyView" ref="771759786"/>
<bool key="NSEnabled">YES</bool>
<object class="NSButtonCell" key="NSCell" id="817997953">
<int key="NSCellFlags">-2080374784</int>
<int key="NSCellFlags2">131072</int>
<string key="NSContents">Kennwort in meinem Schlüsselbund sichern</string>
<object class="NSFont" key="NSSupport">
<string key="NSName">LucidaGrande</string>
<double key="NSSize">11</double>
<int key="NSfFlags">3100</int>
</object>
<reference key="NSControlView" ref="823054555"/>
<int key="NSButtonFlags">1211912448</int>
<int key="NSButtonFlags2">2</int>
<object class="NSCustomResource" key="NSNormalImage">
<string key="NSClassName">NSImage</string>
<string key="NSResourceName">NSSwitch</string>
</object>
<object class="NSButtonImageSource" key="NSAlternateImage">
<string key="NSImageName">NSSwitch</string>
</object>
<string key="NSAlternateContents"/>
<string key="NSKeyEquivalent"/>
<int key="NSPeriodicDelay">200</int>
<int key="NSPeriodicInterval">25</int>
</object>
<bool key="NSAllowsLogicalLayoutDirection">NO</bool>
</object>
</object>
<string key="NSFrameSize">{515, 419}</string>
<reference key="NSSuperview"/>
<reference key="NSNextKeyView" ref="697605106"/>
</object>
<string key="NSScreenRect">{{0, 0}, {1680, 1028}}</string>
<string key="NSMinSize">{475, 312}</string>
<string key="NSMaxSize">{10000000000000, 10000000000000}</string>
<bool key="NSWindowIsRestorable">YES</bool>
</object>
<object class="NSUserDefaultsController" id="681499023">
<bool key="NSSharedInstance">YES</bool>
</object>
</object>
<object class="IBObjectContainer" key="IBDocument.Objects">
<object class="NSMutableArray" key="connectionRecords">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection">
<string key="label">window</string>
<reference key="source" ref="1001"/>
<reference key="destination" ref="996099970"/>
</object>
<int key="connectionID">8</int>
</object>
<object class="IBConnectionRecord">
<object class="IBActionConnection" key="connection">
<string key="label">closeWindow:</string>
<reference key="source" ref="1001"/>
<reference key="destination" ref="736908656"/>
</object>
<int key="connectionID">42</int>
</object>
<object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection">
<string key="label">keychainCheckbox</string>
<reference key="source" ref="1001"/>
<reference key="destination" ref="823054555"/>
</object>
<int key="connectionID">46</int>
</object>
<object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection">
<string key="label">webBackButton</string>
<reference key="source" ref="1001"/>
<reference key="destination" ref="771759786"/>
</object>
<int key="connectionID">47</int>
</object>
<object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection">
<string key="label">webCloseButton</string>
<reference key="source" ref="1001"/>
<reference key="destination" ref="736908656"/>
</object>
<int key="connectionID">48</int>
</object>
<object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection">
<string key="label">webView</string>
<reference key="source" ref="1001"/>
<reference key="destination" ref="697605106"/>
</object>
<int key="connectionID">49</int>
</object>
<object class="IBConnectionRecord">
<object class="IBActionConnection" key="connection">
<string key="label">toggleStorePasswordInKeychain:</string>
<reference key="source" ref="1001"/>
<reference key="destination" ref="823054555"/>
</object>
<int key="connectionID">50</int>
</object>
<object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection">
<string key="label">delegate</string>
<reference key="source" ref="996099970"/>
<reference key="destination" ref="1001"/>
</object>
<int key="connectionID">7</int>
</object>
<object class="IBConnectionRecord">
<object class="IBActionConnection" key="connection">
<string key="label">goBack:</string>
<reference key="source" ref="697605106"/>
<reference key="destination" ref="771759786"/>
</object>
<int key="connectionID">28</int>
</object>
<object class="IBConnectionRecord">
<object class="IBActionConnection" key="connection">
<string key="label">goForward:</string>
<reference key="source" ref="697605106"/>
<reference key="destination" ref="36322049"/>
</object>
<int key="connectionID">29</int>
</object>
<object class="IBConnectionRecord">
<object class="IBBindingConnection" key="connection">
<string key="label">enabled: webView.canGoBack</string>
<reference key="source" ref="771759786"/>
<reference key="destination" ref="1001"/>
<object class="NSNibBindingConnector" key="connector">
<reference key="NSSource" ref="771759786"/>
<reference key="NSDestination" ref="1001"/>
<string key="NSLabel">enabled: webView.canGoBack</string>
<string key="NSBinding">enabled</string>
<string key="NSKeyPath">webView.canGoBack</string>
<int key="NSNibBindingConnectorVersion">2</int>
</object>
</object>
<int key="connectionID">31</int>
</object>
<object class="IBConnectionRecord">
<object class="IBBindingConnection" key="connection">
<string key="label">enabled: webView.canGoForward</string>
<reference key="source" ref="36322049"/>
<reference key="destination" ref="1001"/>
<object class="NSNibBindingConnector" key="connector">
<reference key="NSSource" ref="36322049"/>
<reference key="NSDestination" ref="1001"/>
<string key="NSLabel">enabled: webView.canGoForward</string>
<string key="NSBinding">enabled</string>
<string key="NSKeyPath">webView.canGoForward</string>
<int key="NSNibBindingConnectorVersion">2</int>
</object>
</object>
<int key="connectionID">35</int>
</object>
</object>
<object class="IBMutableOrderedSet" key="objectRecords">
<object class="NSArray" key="orderedObjects">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="IBObjectRecord">
<int key="objectID">0</int>
<object class="NSArray" key="object" id="0">
<bool key="EncodedWithXMLCoder">YES</bool>
</object>
<reference key="children" ref="1000"/>
<nil key="parent"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">-2</int>
<reference key="object" ref="1001"/>
<reference key="parent" ref="0"/>
<string key="objectName">File's Owner</string>
</object>
<object class="IBObjectRecord">
<int key="objectID">-1</int>
<reference key="object" ref="1003"/>
<reference key="parent" ref="0"/>
<string key="objectName">First Responder</string>
</object>
<object class="IBObjectRecord">
<int key="objectID">-3</int>
<reference key="object" ref="1004"/>
<reference key="parent" ref="0"/>
<string key="objectName">Application</string>
</object>
<object class="IBObjectRecord">
<int key="objectID">3</int>
<reference key="object" ref="996099970"/>
<object class="NSMutableArray" key="children">
<bool key="EncodedWithXMLCoder">YES</bool>
<reference ref="563959409"/>
</object>
<reference key="parent" ref="0"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">4</int>
<reference key="object" ref="563959409"/>
<object class="NSMutableArray" key="children">
<bool key="EncodedWithXMLCoder">YES</bool>
<reference ref="697605106"/>
<reference ref="823054555"/>
<reference ref="36322049"/>
<reference ref="771759786"/>
<reference ref="736908656"/>
</object>
<reference key="parent" ref="996099970"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">5</int>
<reference key="object" ref="697605106"/>
<reference key="parent" ref="563959409"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">17</int>
<reference key="object" ref="736908656"/>
<object class="NSMutableArray" key="children">
<bool key="EncodedWithXMLCoder">YES</bool>
<reference ref="215388286"/>
</object>
<reference key="parent" ref="563959409"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">18</int>
<reference key="object" ref="215388286"/>
<reference key="parent" ref="736908656"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">19</int>
<reference key="object" ref="771759786"/>
<object class="NSMutableArray" key="children">
<bool key="EncodedWithXMLCoder">YES</bool>
<reference ref="656055052"/>
</object>
<reference key="parent" ref="563959409"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">20</int>
<reference key="object" ref="656055052"/>
<reference key="parent" ref="771759786"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">26</int>
<reference key="object" ref="36322049"/>
<object class="NSMutableArray" key="children">
<bool key="EncodedWithXMLCoder">YES</bool>
<reference ref="282674264"/>
</object>
<reference key="parent" ref="563959409"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">27</int>
<reference key="object" ref="282674264"/>
<reference key="parent" ref="36322049"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">32</int>
<reference key="object" ref="681499023"/>
<reference key="parent" ref="0"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">43</int>
<reference key="object" ref="823054555"/>
<object class="NSMutableArray" key="children">
<bool key="EncodedWithXMLCoder">YES</bool>
<reference ref="817997953"/>
</object>
<reference key="parent" ref="563959409"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">44</int>
<reference key="object" ref="817997953"/>
<reference key="parent" ref="823054555"/>
</object>
</object>
</object>
<object class="NSMutableDictionary" key="flattenedProperties">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>-1.IBPluginDependency</string>
<string>-2.IBPluginDependency</string>
<string>-3.IBPluginDependency</string>
<string>17.IBPluginDependency</string>
<string>18.IBPluginDependency</string>
<string>19.IBPluginDependency</string>
<string>20.IBPluginDependency</string>
<string>26.IBPluginDependency</string>
<string>27.IBPluginDependency</string>
<string>3.IBPluginDependency</string>
<string>3.IBWindowTemplateEditedContentRect</string>
<string>3.NSWindowTemplate.visibleAtLaunch</string>
<string>32.IBPluginDependency</string>
<string>4.IBPluginDependency</string>
<string>43.IBPluginDependency</string>
<string>44.IBPluginDependency</string>
<string>5.IBPluginDependency</string>
</object>
<object class="NSArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>{{112, 709}, {515, 419}}</string>
<boolean value="NO"/>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.WebKitIBPlugin</string>
</object>
</object>
<object class="NSMutableDictionary" key="unlocalizedProperties">
<bool key="EncodedWithXMLCoder">YES</bool>
<reference key="dict.sortedKeys" ref="0"/>
<reference key="dict.values" ref="0"/>
</object>
<nil key="activeLocalization"/>
<object class="NSMutableDictionary" key="localizations">
<bool key="EncodedWithXMLCoder">YES</bool>
<reference key="dict.sortedKeys" ref="0"/>
<reference key="dict.values" ref="0"/>
</object>
<nil key="sourceID"/>
<int key="maxID">50</int>
</object>
<object class="IBClassDescriber" key="IBDocument.Classes"/>
<int key="IBDocument.localizationMode">0</int>
<string key="IBDocument.TargetRuntimeIdentifier">IBCocoaFramework</string>
<object class="NSMutableDictionary" key="IBDocument.PluginDeclaredDependencies">
<string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin.macosx</string>
<integer value="1050" key="NS.object.0"/>
</object>
<object class="NSMutableDictionary" key="IBDocument.PluginDeclaredDevelopmentDependencies">
<string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin.InterfaceBuilder3</string>
<integer value="3000" key="NS.object.0"/>
</object>
<bool key="IBDocument.PluginDeclaredDependenciesTrackSystemTargetVersion">YES</bool>
<int key="IBDocument.defaultPropertyAccessControl">3</int>
<object class="NSMutableDictionary" key="IBDocument.LastKnownImageSizes">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>NSGoLeftTemplate</string>
<string>NSGoRightTemplate</string>
<string>NSStopProgressTemplate</string>
<string>NSSwitch</string>
</object>
<object class="NSArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>{9, 9}</string>
<string>{9, 9}</string>
<string>{11, 11}</string>
<string>{15, 15}</string>
</object>
</object>
</data>
</archive>

View File

@@ -0,0 +1,664 @@
<?xml version="1.0" encoding="UTF-8"?>
<archive type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="7.10">
<data>
<int key="IBDocument.SystemTarget">1050</int>
<string key="IBDocument.SystemVersion">12E55</string>
<string key="IBDocument.InterfaceBuilderVersion">3084</string>
<string key="IBDocument.AppKitVersion">1187.39</string>
<string key="IBDocument.HIToolboxVersion">626.00</string>
<object class="NSMutableDictionary" key="IBDocument.PluginVersions">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.WebKitIBPlugin</string>
</object>
<object class="NSArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>3084</string>
<string>2053</string>
</object>
</object>
<object class="NSArray" key="IBDocument.IntegratedClassDependencies">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>NSButton</string>
<string>NSButtonCell</string>
<string>NSCustomObject</string>
<string>NSUserDefaultsController</string>
<string>NSView</string>
<string>NSWindowTemplate</string>
<string>WebView</string>
</object>
<object class="NSArray" key="IBDocument.PluginDependencies">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.WebKitIBPlugin</string>
</object>
<object class="NSMutableDictionary" key="IBDocument.Metadata">
<string key="NS.key.0">PluginDependencyRecalculationVersion</string>
<integer value="1" key="NS.object.0"/>
</object>
<object class="NSMutableArray" key="IBDocument.RootObjects" id="1000">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSCustomObject" id="1001">
<string key="NSClassName">GTMOAuth2WindowController</string>
</object>
<object class="NSCustomObject" id="1003">
<string key="NSClassName">FirstResponder</string>
</object>
<object class="NSCustomObject" id="1004">
<string key="NSClassName">NSApplication</string>
</object>
<object class="NSWindowTemplate" id="996099970">
<int key="NSWindowStyleMask">11</int>
<int key="NSWindowBacking">2</int>
<string key="NSWindowRect">{{558, 328}, {515, 419}}</string>
<int key="NSWTFlags">536870912</int>
<string key="NSWindowTitle">Sign In</string>
<string key="NSWindowClass">NSWindow</string>
<nil key="NSViewClass"/>
<nil key="NSUserInterfaceItemIdentifier"/>
<string key="NSWindowContentMinSize">{475, 290}</string>
<object class="NSView" key="NSWindowView" id="563959409">
<reference key="NSNextResponder"/>
<int key="NSvFlags">256</int>
<object class="NSMutableArray" key="NSSubviews">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="WebView" id="697605106">
<reference key="NSNextResponder" ref="563959409"/>
<int key="NSvFlags">274</int>
<object class="NSMutableSet" key="NSDragTypes">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="set.sortedObjects">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>Apple HTML pasteboard type</string>
<string>Apple PDF pasteboard type</string>
<string>Apple PICT pasteboard type</string>
<string>Apple URL pasteboard type</string>
<string>Apple Web Archive pasteboard type</string>
<string>NSColor pasteboard type</string>
<string>NSFilenamesPboardType</string>
<string>NSStringPboardType</string>
<string>NeXT RTFD pasteboard type</string>
<string>NeXT Rich Text Format v1.0 pasteboard type</string>
<string>NeXT TIFF v4.0 pasteboard type</string>
<string>WebURLsWithTitlesPboardType</string>
<string>public.png</string>
<string>public.url</string>
<string>public.url-name</string>
</object>
</object>
<string key="NSFrame">{{0, 20}, {515, 399}}</string>
<reference key="NSSuperview" ref="563959409"/>
<reference key="NSWindow"/>
<reference key="NSNextKeyView"/>
<string key="FrameName"/>
<string key="GroupName"/>
<object class="WebPreferences" key="Preferences">
<string key="Identifier"/>
<object class="NSMutableDictionary" key="Values">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>WebKitDefaultFixedFontSize</string>
<string>WebKitDefaultFontSize</string>
<string>WebKitMinimumFontSize</string>
</object>
<object class="NSArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<integer value="12"/>
<integer value="12"/>
<integer value="1"/>
</object>
</object>
</object>
<bool key="UseBackForwardList">YES</bool>
<bool key="AllowsUndo">YES</bool>
</object>
<object class="NSButton" id="736908656">
<reference key="NSNextResponder" ref="563959409"/>
<int key="NSvFlags">289</int>
<string key="NSFrame">{{479, 0}, {16, 19}}</string>
<reference key="NSSuperview" ref="563959409"/>
<reference key="NSWindow"/>
<bool key="NSEnabled">YES</bool>
<object class="NSButtonCell" key="NSCell" id="215388286">
<int key="NSCellFlags">67108864</int>
<int key="NSCellFlags2">134217728</int>
<string key="NSContents"/>
<object class="NSFont" key="NSSupport" id="895134484">
<string key="NSName">LucidaGrande</string>
<double key="NSSize">13</double>
<int key="NSfFlags">1044</int>
</object>
<reference key="NSControlView" ref="736908656"/>
<int key="NSButtonFlags">-2041823232</int>
<int key="NSButtonFlags2">134</int>
<object class="NSCustomResource" key="NSNormalImage">
<string key="NSClassName">NSImage</string>
<string key="NSResourceName">NSStopProgressTemplate</string>
</object>
<string key="NSAlternateContents"/>
<string type="base64-UTF8" key="NSKeyEquivalent">Gw</string>
<int key="NSPeriodicDelay">400</int>
<int key="NSPeriodicInterval">75</int>
</object>
<bool key="NSAllowsLogicalLayoutDirection">NO</bool>
</object>
<object class="NSButton" id="771759786">
<reference key="NSNextResponder" ref="563959409"/>
<int key="NSvFlags">289</int>
<string key="NSFrame">{{437, 0}, {16, 19}}</string>
<reference key="NSSuperview" ref="563959409"/>
<reference key="NSWindow"/>
<reference key="NSNextKeyView" ref="36322049"/>
<bool key="NSEnabled">YES</bool>
<object class="NSButtonCell" key="NSCell" id="656055052">
<int key="NSCellFlags">67108864</int>
<int key="NSCellFlags2">134217728</int>
<string key="NSContents"/>
<reference key="NSSupport" ref="895134484"/>
<reference key="NSControlView" ref="771759786"/>
<int key="NSButtonFlags">-2041823232</int>
<int key="NSButtonFlags2">134</int>
<object class="NSCustomResource" key="NSNormalImage">
<string key="NSClassName">NSImage</string>
<string key="NSResourceName">NSGoLeftTemplate</string>
</object>
<string key="NSAlternateContents"/>
<string key="NSKeyEquivalent"/>
<int key="NSPeriodicDelay">400</int>
<int key="NSPeriodicInterval">75</int>
</object>
<bool key="NSAllowsLogicalLayoutDirection">NO</bool>
</object>
<object class="NSButton" id="36322049">
<reference key="NSNextResponder" ref="563959409"/>
<int key="NSvFlags">289</int>
<string key="NSFrame">{{456, 0}, {16, 19}}</string>
<reference key="NSSuperview" ref="563959409"/>
<reference key="NSWindow"/>
<reference key="NSNextKeyView" ref="736908656"/>
<bool key="NSEnabled">YES</bool>
<object class="NSButtonCell" key="NSCell" id="282674264">
<int key="NSCellFlags">67108864</int>
<int key="NSCellFlags2">134217728</int>
<string key="NSContents"/>
<reference key="NSSupport" ref="895134484"/>
<reference key="NSControlView" ref="36322049"/>
<int key="NSButtonFlags">-2042347520</int>
<int key="NSButtonFlags2">134</int>
<object class="NSCustomResource" key="NSNormalImage">
<string key="NSClassName">NSImage</string>
<string key="NSResourceName">NSGoRightTemplate</string>
</object>
<string key="NSAlternateContents"/>
<string key="NSKeyEquivalent"/>
<int key="NSPeriodicDelay">400</int>
<int key="NSPeriodicInterval">75</int>
</object>
<bool key="NSAllowsLogicalLayoutDirection">NO</bool>
</object>
<object class="NSButton" id="823054555">
<reference key="NSNextResponder" ref="563959409"/>
<int key="NSvFlags">292</int>
<string key="NSFrame">{{2, 1}, {429, 18}}</string>
<reference key="NSSuperview" ref="563959409"/>
<reference key="NSWindow"/>
<reference key="NSNextKeyView" ref="771759786"/>
<bool key="NSEnabled">YES</bool>
<object class="NSButtonCell" key="NSCell" id="817997953">
<int key="NSCellFlags">-2080374784</int>
<int key="NSCellFlags2">131072</int>
<string key="NSContents">Save password in my keychain</string>
<object class="NSFont" key="NSSupport">
<string key="NSName">LucidaGrande</string>
<double key="NSSize">11</double>
<int key="NSfFlags">3100</int>
</object>
<reference key="NSControlView" ref="823054555"/>
<int key="NSButtonFlags">1211912448</int>
<int key="NSButtonFlags2">2</int>
<object class="NSCustomResource" key="NSNormalImage">
<string key="NSClassName">NSImage</string>
<string key="NSResourceName">NSSwitch</string>
</object>
<object class="NSButtonImageSource" key="NSAlternateImage">
<string key="NSImageName">NSSwitch</string>
</object>
<string key="NSAlternateContents"/>
<string key="NSKeyEquivalent"/>
<int key="NSPeriodicDelay">200</int>
<int key="NSPeriodicInterval">25</int>
</object>
<bool key="NSAllowsLogicalLayoutDirection">NO</bool>
</object>
</object>
<string key="NSFrameSize">{515, 419}</string>
<reference key="NSSuperview"/>
<reference key="NSWindow"/>
<reference key="NSNextKeyView" ref="697605106"/>
</object>
<string key="NSScreenRect">{{0, 0}, {1680, 1028}}</string>
<string key="NSMinSize">{475, 312}</string>
<string key="NSMaxSize">{10000000000000, 10000000000000}</string>
<bool key="NSWindowIsRestorable">YES</bool>
</object>
<object class="NSUserDefaultsController" id="681499023">
<bool key="NSSharedInstance">YES</bool>
</object>
</object>
<object class="IBObjectContainer" key="IBDocument.Objects">
<object class="NSMutableArray" key="connectionRecords">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection">
<string key="label">window</string>
<reference key="source" ref="1001"/>
<reference key="destination" ref="996099970"/>
</object>
<int key="connectionID">8</int>
</object>
<object class="IBConnectionRecord">
<object class="IBActionConnection" key="connection">
<string key="label">closeWindow:</string>
<reference key="source" ref="1001"/>
<reference key="destination" ref="736908656"/>
</object>
<int key="connectionID">42</int>
</object>
<object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection">
<string key="label">keychainCheckbox</string>
<reference key="source" ref="1001"/>
<reference key="destination" ref="823054555"/>
</object>
<int key="connectionID">46</int>
</object>
<object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection">
<string key="label">webBackButton</string>
<reference key="source" ref="1001"/>
<reference key="destination" ref="771759786"/>
</object>
<int key="connectionID">47</int>
</object>
<object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection">
<string key="label">webCloseButton</string>
<reference key="source" ref="1001"/>
<reference key="destination" ref="736908656"/>
</object>
<int key="connectionID">48</int>
</object>
<object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection">
<string key="label">webView</string>
<reference key="source" ref="1001"/>
<reference key="destination" ref="697605106"/>
</object>
<int key="connectionID">49</int>
</object>
<object class="IBConnectionRecord">
<object class="IBActionConnection" key="connection">
<string key="label">toggleStorePasswordInKeychain:</string>
<reference key="source" ref="1001"/>
<reference key="destination" ref="823054555"/>
</object>
<int key="connectionID">50</int>
</object>
<object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection">
<string key="label">delegate</string>
<reference key="source" ref="996099970"/>
<reference key="destination" ref="1001"/>
</object>
<int key="connectionID">7</int>
</object>
<object class="IBConnectionRecord">
<object class="IBActionConnection" key="connection">
<string key="label">goBack:</string>
<reference key="source" ref="697605106"/>
<reference key="destination" ref="771759786"/>
</object>
<int key="connectionID">28</int>
</object>
<object class="IBConnectionRecord">
<object class="IBActionConnection" key="connection">
<string key="label">goForward:</string>
<reference key="source" ref="697605106"/>
<reference key="destination" ref="36322049"/>
</object>
<int key="connectionID">29</int>
</object>
<object class="IBConnectionRecord">
<object class="IBBindingConnection" key="connection">
<string key="label">enabled: webView.canGoBack</string>
<reference key="source" ref="771759786"/>
<reference key="destination" ref="1001"/>
<object class="NSNibBindingConnector" key="connector">
<reference key="NSSource" ref="771759786"/>
<reference key="NSDestination" ref="1001"/>
<string key="NSLabel">enabled: webView.canGoBack</string>
<string key="NSBinding">enabled</string>
<string key="NSKeyPath">webView.canGoBack</string>
<int key="NSNibBindingConnectorVersion">2</int>
</object>
</object>
<int key="connectionID">31</int>
</object>
<object class="IBConnectionRecord">
<object class="IBBindingConnection" key="connection">
<string key="label">enabled: webView.canGoForward</string>
<reference key="source" ref="36322049"/>
<reference key="destination" ref="1001"/>
<object class="NSNibBindingConnector" key="connector">
<reference key="NSSource" ref="36322049"/>
<reference key="NSDestination" ref="1001"/>
<string key="NSLabel">enabled: webView.canGoForward</string>
<string key="NSBinding">enabled</string>
<string key="NSKeyPath">webView.canGoForward</string>
<int key="NSNibBindingConnectorVersion">2</int>
</object>
</object>
<int key="connectionID">35</int>
</object>
</object>
<object class="IBMutableOrderedSet" key="objectRecords">
<object class="NSArray" key="orderedObjects">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="IBObjectRecord">
<int key="objectID">0</int>
<object class="NSArray" key="object" id="0">
<bool key="EncodedWithXMLCoder">YES</bool>
</object>
<reference key="children" ref="1000"/>
<nil key="parent"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">-2</int>
<reference key="object" ref="1001"/>
<reference key="parent" ref="0"/>
<string key="objectName">File's Owner</string>
</object>
<object class="IBObjectRecord">
<int key="objectID">-1</int>
<reference key="object" ref="1003"/>
<reference key="parent" ref="0"/>
<string key="objectName">First Responder</string>
</object>
<object class="IBObjectRecord">
<int key="objectID">-3</int>
<reference key="object" ref="1004"/>
<reference key="parent" ref="0"/>
<string key="objectName">Application</string>
</object>
<object class="IBObjectRecord">
<int key="objectID">3</int>
<reference key="object" ref="996099970"/>
<object class="NSMutableArray" key="children">
<bool key="EncodedWithXMLCoder">YES</bool>
<reference ref="563959409"/>
</object>
<reference key="parent" ref="0"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">4</int>
<reference key="object" ref="563959409"/>
<object class="NSMutableArray" key="children">
<bool key="EncodedWithXMLCoder">YES</bool>
<reference ref="697605106"/>
<reference ref="823054555"/>
<reference ref="36322049"/>
<reference ref="771759786"/>
<reference ref="736908656"/>
</object>
<reference key="parent" ref="996099970"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">5</int>
<reference key="object" ref="697605106"/>
<reference key="parent" ref="563959409"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">17</int>
<reference key="object" ref="736908656"/>
<object class="NSMutableArray" key="children">
<bool key="EncodedWithXMLCoder">YES</bool>
<reference ref="215388286"/>
</object>
<reference key="parent" ref="563959409"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">18</int>
<reference key="object" ref="215388286"/>
<reference key="parent" ref="736908656"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">19</int>
<reference key="object" ref="771759786"/>
<object class="NSMutableArray" key="children">
<bool key="EncodedWithXMLCoder">YES</bool>
<reference ref="656055052"/>
</object>
<reference key="parent" ref="563959409"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">20</int>
<reference key="object" ref="656055052"/>
<reference key="parent" ref="771759786"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">26</int>
<reference key="object" ref="36322049"/>
<object class="NSMutableArray" key="children">
<bool key="EncodedWithXMLCoder">YES</bool>
<reference ref="282674264"/>
</object>
<reference key="parent" ref="563959409"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">27</int>
<reference key="object" ref="282674264"/>
<reference key="parent" ref="36322049"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">32</int>
<reference key="object" ref="681499023"/>
<reference key="parent" ref="0"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">43</int>
<reference key="object" ref="823054555"/>
<object class="NSMutableArray" key="children">
<bool key="EncodedWithXMLCoder">YES</bool>
<reference ref="817997953"/>
</object>
<reference key="parent" ref="563959409"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">44</int>
<reference key="object" ref="817997953"/>
<reference key="parent" ref="823054555"/>
</object>
</object>
</object>
<object class="NSMutableDictionary" key="flattenedProperties">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>-1.IBPluginDependency</string>
<string>-2.IBPluginDependency</string>
<string>-3.IBPluginDependency</string>
<string>17.IBPluginDependency</string>
<string>18.IBPluginDependency</string>
<string>19.IBPluginDependency</string>
<string>20.IBPluginDependency</string>
<string>26.IBPluginDependency</string>
<string>27.IBPluginDependency</string>
<string>3.IBPluginDependency</string>
<string>3.IBWindowTemplateEditedContentRect</string>
<string>3.NSWindowTemplate.visibleAtLaunch</string>
<string>32.IBPluginDependency</string>
<string>4.IBPluginDependency</string>
<string>43.IBPluginDependency</string>
<string>44.IBPluginDependency</string>
<string>5.IBPluginDependency</string>
</object>
<object class="NSArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>{{112, 709}, {515, 419}}</string>
<boolean value="NO"/>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.WebKitIBPlugin</string>
</object>
</object>
<object class="NSMutableDictionary" key="unlocalizedProperties">
<bool key="EncodedWithXMLCoder">YES</bool>
<reference key="dict.sortedKeys" ref="0"/>
<reference key="dict.values" ref="0"/>
</object>
<nil key="activeLocalization"/>
<object class="NSMutableDictionary" key="localizations">
<bool key="EncodedWithXMLCoder">YES</bool>
<reference key="dict.sortedKeys" ref="0"/>
<reference key="dict.values" ref="0"/>
</object>
<nil key="sourceID"/>
<int key="maxID">50</int>
</object>
<object class="IBClassDescriber" key="IBDocument.Classes">
<object class="NSMutableArray" key="referencedPartialClassDescriptions">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="IBPartialClassDescription">
<string key="className">GTMOAuth2WindowController</string>
<string key="superclassName">NSWindowController</string>
<object class="NSMutableDictionary" key="actions">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>closeWindow:</string>
<string>toggleStorePasswordInKeychain:</string>
</object>
<object class="NSArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>id</string>
<string>id</string>
</object>
</object>
<object class="NSMutableDictionary" key="actionInfosByName">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>closeWindow:</string>
<string>toggleStorePasswordInKeychain:</string>
</object>
<object class="NSArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="IBActionInfo">
<string key="name">closeWindow:</string>
<string key="candidateClassName">id</string>
</object>
<object class="IBActionInfo">
<string key="name">toggleStorePasswordInKeychain:</string>
<string key="candidateClassName">id</string>
</object>
</object>
</object>
<object class="NSMutableDictionary" key="outlets">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>keychainCheckbox</string>
<string>webBackButton</string>
<string>webCloseButton</string>
<string>webView</string>
</object>
<object class="NSArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>NSButton</string>
<string>NSButton</string>
<string>NSButton</string>
<string>WebView</string>
</object>
</object>
<object class="NSMutableDictionary" key="toOneOutletInfosByName">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>keychainCheckbox</string>
<string>webBackButton</string>
<string>webCloseButton</string>
<string>webView</string>
</object>
<object class="NSArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="IBToOneOutletInfo">
<string key="name">keychainCheckbox</string>
<string key="candidateClassName">NSButton</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">webBackButton</string>
<string key="candidateClassName">NSButton</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">webCloseButton</string>
<string key="candidateClassName">NSButton</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">webView</string>
<string key="candidateClassName">WebView</string>
</object>
</object>
</object>
<object class="IBClassDescriptionSource" key="sourceIdentifier">
<string key="majorKey">IBProjectSource</string>
<string key="minorKey">./Classes/GTMOAuth2WindowController.h</string>
</object>
</object>
</object>
</object>
<int key="IBDocument.localizationMode">0</int>
<string key="IBDocument.TargetRuntimeIdentifier">IBCocoaFramework</string>
<object class="NSMutableDictionary" key="IBDocument.PluginDeclaredDependencies">
<string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin.macosx</string>
<integer value="1050" key="NS.object.0"/>
</object>
<object class="NSMutableDictionary" key="IBDocument.PluginDeclaredDevelopmentDependencies">
<string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin.InterfaceBuilder3</string>
<integer value="3000" key="NS.object.0"/>
</object>
<bool key="IBDocument.PluginDeclaredDependenciesTrackSystemTargetVersion">YES</bool>
<int key="IBDocument.defaultPropertyAccessControl">3</int>
<object class="NSMutableDictionary" key="IBDocument.LastKnownImageSizes">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>NSGoLeftTemplate</string>
<string>NSGoRightTemplate</string>
<string>NSStopProgressTemplate</string>
<string>NSSwitch</string>
</object>
<object class="NSArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>{9, 9}</string>
<string>{9, 9}</string>
<string>{11, 11}</string>
<string>{15, 15}</string>
</object>
</object>
</data>
</archive>