diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..d56c3e61 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +.DS_Store +__MACOSX +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +*.xcworkspace +!default.xcworkspace +xcuserdata +profile +*.moved-aside +DerivedData +.idea/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..6e2feff0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +Copyright (c) 2015 Guilherme Rambo + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..ada1f82e --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# WWDC app for OS X + +Don't like WWDC's website? Use this app to watch WWDC sessions on your Mac. + +To download the latest release, [click here](https://github.com/insidegui/WWDC/blob/master/Releases/WWDC_latest.zip?raw=true). + +![screenshot](https://raw.githubusercontent.com/insidegui/WWDC/master/screenshot.png) \ No newline at end of file diff --git a/Releases/WWDC_latest.zip b/Releases/WWDC_latest.zip new file mode 100644 index 00000000..9727fb2b Binary files /dev/null and b/Releases/WWDC_latest.zip differ diff --git a/ViewUtils/ViewUtils.xcodeproj/project.pbxproj b/ViewUtils/ViewUtils.xcodeproj/project.pbxproj new file mode 100644 index 00000000..27255eda --- /dev/null +++ b/ViewUtils/ViewUtils.xcodeproj/project.pbxproj @@ -0,0 +1,312 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + DD2016471AE401B500DD8155 /* ViewUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = DD2016461AE401B500DD8155 /* ViewUtils.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD20165F1AE401C200DD8155 /* GRWindowMovingView.h in Headers */ = {isa = PBXBuildFile; fileRef = DD20165D1AE401C200DD8155 /* GRWindowMovingView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD2016601AE401C200DD8155 /* GRWindowMovingView.m in Sources */ = {isa = PBXBuildFile; fileRef = DD20165E1AE401C200DD8155 /* GRWindowMovingView.m */; }; + DD20167B1AE4158100DD8155 /* GRPlayerWindow.h in Headers */ = {isa = PBXBuildFile; fileRef = DD2016791AE4158100DD8155 /* GRPlayerWindow.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD20167C1AE4158100DD8155 /* GRPlayerWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = DD20167A1AE4158100DD8155 /* GRPlayerWindow.m */; }; + DD20167F1AE41DD600DD8155 /* GRPlayerView.h in Headers */ = {isa = PBXBuildFile; fileRef = DD20167D1AE41DD600DD8155 /* GRPlayerView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD2016801AE41DD600DD8155 /* GRPlayerView.m in Sources */ = {isa = PBXBuildFile; fileRef = DD20167E1AE41DD600DD8155 /* GRPlayerView.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + DD2016411AE401B500DD8155 /* ViewUtils.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ViewUtils.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + DD2016451AE401B500DD8155 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + DD2016461AE401B500DD8155 /* ViewUtils.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewUtils.h; sourceTree = ""; }; + DD20165D1AE401C200DD8155 /* GRWindowMovingView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GRWindowMovingView.h; sourceTree = ""; }; + DD20165E1AE401C200DD8155 /* GRWindowMovingView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GRWindowMovingView.m; sourceTree = ""; }; + DD2016791AE4158100DD8155 /* GRPlayerWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GRPlayerWindow.h; sourceTree = ""; }; + DD20167A1AE4158100DD8155 /* GRPlayerWindow.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GRPlayerWindow.m; sourceTree = ""; }; + DD20167D1AE41DD600DD8155 /* GRPlayerView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GRPlayerView.h; sourceTree = ""; }; + DD20167E1AE41DD600DD8155 /* GRPlayerView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GRPlayerView.m; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + DD20163D1AE401B500DD8155 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + DD2016371AE401B500DD8155 = { + isa = PBXGroup; + children = ( + DD2016431AE401B500DD8155 /* ViewUtils */, + DD2016421AE401B500DD8155 /* Products */, + ); + sourceTree = ""; + }; + DD2016421AE401B500DD8155 /* Products */ = { + isa = PBXGroup; + children = ( + DD2016411AE401B500DD8155 /* ViewUtils.framework */, + ); + name = Products; + sourceTree = ""; + }; + DD2016431AE401B500DD8155 /* ViewUtils */ = { + isa = PBXGroup; + children = ( + DD2016791AE4158100DD8155 /* GRPlayerWindow.h */, + DD20167A1AE4158100DD8155 /* GRPlayerWindow.m */, + DD20165D1AE401C200DD8155 /* GRWindowMovingView.h */, + DD20165E1AE401C200DD8155 /* GRWindowMovingView.m */, + DD2016461AE401B500DD8155 /* ViewUtils.h */, + DD2016441AE401B500DD8155 /* Supporting Files */, + DD20167D1AE41DD600DD8155 /* GRPlayerView.h */, + DD20167E1AE41DD600DD8155 /* GRPlayerView.m */, + ); + path = ViewUtils; + sourceTree = ""; + }; + DD2016441AE401B500DD8155 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + DD2016451AE401B500DD8155 /* Info.plist */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + DD20163E1AE401B500DD8155 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + DD20165F1AE401C200DD8155 /* GRWindowMovingView.h in Headers */, + DD20167F1AE41DD600DD8155 /* GRPlayerView.h in Headers */, + DD20167B1AE4158100DD8155 /* GRPlayerWindow.h in Headers */, + DD2016471AE401B500DD8155 /* ViewUtils.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + DD2016401AE401B500DD8155 /* ViewUtils */ = { + isa = PBXNativeTarget; + buildConfigurationList = DD2016571AE401B500DD8155 /* Build configuration list for PBXNativeTarget "ViewUtils" */; + buildPhases = ( + DD20163C1AE401B500DD8155 /* Sources */, + DD20163D1AE401B500DD8155 /* Frameworks */, + DD20163E1AE401B500DD8155 /* Headers */, + DD20163F1AE401B500DD8155 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = ViewUtils; + productName = ViewUtils; + productReference = DD2016411AE401B500DD8155 /* ViewUtils.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + DD2016381AE401B500DD8155 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0630; + ORGANIZATIONNAME = "Guilherme Rambo"; + TargetAttributes = { + DD2016401AE401B500DD8155 = { + CreatedOnToolsVersion = 6.3; + }; + }; + }; + buildConfigurationList = DD20163B1AE401B500DD8155 /* Build configuration list for PBXProject "ViewUtils" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = DD2016371AE401B500DD8155; + productRefGroup = DD2016421AE401B500DD8155 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + DD2016401AE401B500DD8155 /* ViewUtils */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + DD20163F1AE401B500DD8155 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + DD20163C1AE401B500DD8155 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DD2016801AE41DD600DD8155 /* GRPlayerView.m in Sources */, + DD20167C1AE4158100DD8155 /* GRPlayerWindow.m in Sources */, + DD2016601AE401C200DD8155 /* GRWindowMovingView.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + DD2016551AE401B500DD8155 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.10; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + DD2016561AE401B500DD8155 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.10; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + DD2016581AE401B500DD8155 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + INFOPLIST_FILE = ViewUtils/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + }; + name = Debug; + }; + DD2016591AE401B500DD8155 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + INFOPLIST_FILE = ViewUtils/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + DD20163B1AE401B500DD8155 /* Build configuration list for PBXProject "ViewUtils" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DD2016551AE401B500DD8155 /* Debug */, + DD2016561AE401B500DD8155 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + DD2016571AE401B500DD8155 /* Build configuration list for PBXNativeTarget "ViewUtils" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DD2016581AE401B500DD8155 /* Debug */, + DD2016591AE401B500DD8155 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = DD2016381AE401B500DD8155 /* Project object */; +} diff --git a/ViewUtils/ViewUtils/GRPlayerView.h b/ViewUtils/ViewUtils/GRPlayerView.h new file mode 100644 index 00000000..71412a48 --- /dev/null +++ b/ViewUtils/ViewUtils/GRPlayerView.h @@ -0,0 +1,15 @@ +// +// GRPlayerView.h +// ViewUtils +// +// Created by Guilherme Rambo on 19/04/15. +// Copyright (c) 2015 Guilherme Rambo. All rights reserved. +// + +#import + +@import AVKit; + +@interface GRPlayerView : AVPlayerView + +@end diff --git a/ViewUtils/ViewUtils/GRPlayerView.m b/ViewUtils/ViewUtils/GRPlayerView.m new file mode 100644 index 00000000..d556b1f7 --- /dev/null +++ b/ViewUtils/ViewUtils/GRPlayerView.m @@ -0,0 +1,72 @@ +// +// GRPlayerView.m +// ViewUtils +// +// Created by Guilherme Rambo on 19/04/15. +// Copyright (c) 2015 Guilherme Rambo. All rights reserved. +// + +#import "GRPlayerView.h" +#import "GRPlayerWindow.h" + +@interface AVPlayerView () + +- (BOOL)_mouseInNoHideArea; +- (BOOL)canHideControls; + +@end + +@interface GRPlayerView () + +@property (readonly) GRPlayerWindow *playerWindow; +@property (strong) NSTimer *titlebarTimer; + +@end + +#define kHideTitlebarTimerInterval 3.0f + +@implementation GRPlayerView + +- (void)setupHideTitlebarTimer +{ + [self.titlebarTimer invalidate]; + self.titlebarTimer = [NSTimer scheduledTimerWithTimeInterval:kHideTitlebarTimerInterval target:self selector:@selector(hideTitlebar) userInfo:nil repeats:YES]; +} + +- (void)viewDidMoveToWindow +{ + [super viewDidMoveToWindow]; + + [self setupHideTitlebarTimer]; +} + +- (void)hideTitlebar +{ + if (![self _mouseInNoHideArea] && [self canHideControls] && !(self.window.styleMask & NSFullScreenWindowMask)) { + [self.playerWindow hideTitlebarAnimated:YES]; + } + + [self.titlebarTimer invalidate]; +} + +- (void)mouseMoved:(NSEvent *)theEvent +{ + [super mouseMoved:theEvent]; + + [self.playerWindow showTitlebarAnimated:YES]; + [self setupHideTitlebarTimer]; +} + +- (void)mouseExited:(NSEvent *)theEvent +{ + [super mouseExited:theEvent]; + + [self hideTitlebar]; +} + +- (GRPlayerWindow *)playerWindow +{ + return (GRPlayerWindow *)self.window; +} + +@end diff --git a/ViewUtils/ViewUtils/GRPlayerWindow.h b/ViewUtils/ViewUtils/GRPlayerWindow.h new file mode 100644 index 00000000..62fefb86 --- /dev/null +++ b/ViewUtils/ViewUtils/GRPlayerWindow.h @@ -0,0 +1,18 @@ +// +// VXPlayerWindow.h +// VLCX +// +// Created by Guilherme Rambo on 15/12/14. +// Copyright (c) 2014 Guilherme Rambo. All rights reserved. +// + +#import + +@interface GRPlayerWindow : NSWindow + +@property (readonly) BOOL titlebarVisible; + +- (void)hideTitlebarAnimated:(BOOL)animated; +- (void)showTitlebarAnimated:(BOOL)animated; + +@end diff --git a/ViewUtils/ViewUtils/GRPlayerWindow.m b/ViewUtils/ViewUtils/GRPlayerWindow.m new file mode 100644 index 00000000..de622bb9 --- /dev/null +++ b/ViewUtils/ViewUtils/GRPlayerWindow.m @@ -0,0 +1,72 @@ +// +// VXPlayerWindow.m +// VLCX +// +// Created by Guilherme Rambo on 15/12/14. +// Copyright (c) 2014 Guilherme Rambo. All rights reserved. +// + +#import "GRPlayerWindow.h" + +@interface GRPlayerWindow () + +@property (readonly) NSVisualEffectView *titlebarView; + +@end + +// this is just a hack so we can access titlebarView without warnings from the compiler, +// titlebarView is actually a property of NSThemeFrame, which is a subclass of NSView ;) +@interface NSView (Titlebar) +- (NSVisualEffectView *)titlebarView; +@end + +@implementation GRPlayerWindow + +- (instancetype)initWithContentRect:(NSRect)contentRect styleMask:(NSUInteger)aStyle backing:(NSBackingStoreType)bufferingType defer:(BOOL)flag +{ + if (!(self = [super initWithContentRect:contentRect styleMask:aStyle backing:bufferingType defer:flag])) return nil; + + self.movableByWindowBackground = YES; + self.styleMask |= NSFullSizeContentViewWindowMask; + self.titlebarView.material = NSVisualEffectMaterialDark; + self.titlebarView.state = NSVisualEffectStateActive; + + return self; +} + +- (void)hideTitlebarAnimated:(BOOL)animated +{ + // do not hide the titlebar when in fullscreen mode + if ((self.styleMask & NSFullScreenWindowMask)) return; + + [NSAnimationContext runAnimationGroup:^(NSAnimationContext *context) { + if (!animated) [context setDuration:0]; + + self.titlebarView.animator.alphaValue = 0; + } completionHandler:^{ + _titlebarVisible = NO; + + self.titlebarView.hidden = YES; + }]; +} + +- (void)showTitlebarAnimated:(BOOL)animated +{ + self.titlebarView.hidden = NO; + + [NSAnimationContext runAnimationGroup:^(NSAnimationContext *context) { + if (!animated) [context setDuration:0]; + + self.titlebarView.animator.alphaValue = 1; + } completionHandler:^{ + _titlebarVisible = YES; + }]; +} + +- (NSVisualEffectView *)titlebarView +{ + // [self.contentView superview] is an instance of NSThemeFrame, which has a property called titlebarView + return [[self.contentView superview] titlebarView]; +} + +@end \ No newline at end of file diff --git a/ViewUtils/ViewUtils/GRWindowMovingView.h b/ViewUtils/ViewUtils/GRWindowMovingView.h new file mode 100644 index 00000000..ce0be44f --- /dev/null +++ b/ViewUtils/ViewUtils/GRWindowMovingView.h @@ -0,0 +1,16 @@ +// +// GRWindowMovingView.h +// GRWindowMovingView +// +// Created by Guilherme Rambo on 19/04/15. +// Copyright (c) 2015 Guilherme Rambo. All rights reserved. +// + +#import + +IB_DESIGNABLE +@interface GRWindowMovingView : NSView + +@property (nonatomic, copy) IBInspectable NSColor *backgroundColor; + +@end diff --git a/ViewUtils/ViewUtils/GRWindowMovingView.m b/ViewUtils/ViewUtils/GRWindowMovingView.m new file mode 100644 index 00000000..7644892d --- /dev/null +++ b/ViewUtils/ViewUtils/GRWindowMovingView.m @@ -0,0 +1,49 @@ +// +// GRWindowMovingView.m +// GRWindowMovingView +// +// Created by Guilherme Rambo on 19/04/15. +// Copyright (c) 2015 Guilherme Rambo. All rights reserved. +// + +#import "GRWindowMovingView.h" + +typedef int CGSConnectionID; + +extern CGSConnectionID CGSMainConnectionID(void); +extern CGError CGSDragWindowRelativeToMouse(CGSConnectionID cid, CGWindowID wid, CGPoint point); + +@implementation GRWindowMovingView + +- (void)drawRect:(NSRect)dirtyRect +{ + [super drawRect:dirtyRect]; + + if (self.backgroundColor) { + [self.backgroundColor setFill]; + NSRectFill(dirtyRect); + } +} + +- (void)mouseDown:(NSEvent *)theEvent +{ + NSPoint point = NSMakePoint(theEvent.locationInWindow.x, NSHeight(self.window.frame)-theEvent.locationInWindow.y); + + if ([NSEvent pressedMouseButtons] & 1) { + CGSDragWindowRelativeToMouse(CGSMainConnectionID(), (CGWindowID)self.window.windowNumber, point); + } +} + +- (BOOL)acceptsFirstMouse:(NSEvent *)theEvent +{ + return YES; +} + +- (void)setBackgroundColor:(NSColor *)backgroundColor +{ + _backgroundColor = [backgroundColor copy]; + + [self setNeedsDisplay:YES]; +} + +@end diff --git a/ViewUtils/ViewUtils/Info.plist b/ViewUtils/ViewUtils/Info.plist new file mode 100644 index 00000000..0ffe0b2f --- /dev/null +++ b/ViewUtils/ViewUtils/Info.plist @@ -0,0 +1,28 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + br.com.guilhermerambo.$(PRODUCT_NAME:rfc1034identifier) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSHumanReadableCopyright + Copyright © 2015 Guilherme Rambo. All rights reserved. + NSPrincipalClass + + + diff --git a/ViewUtils/ViewUtils/ViewUtils.h b/ViewUtils/ViewUtils/ViewUtils.h new file mode 100644 index 00000000..e6f65747 --- /dev/null +++ b/ViewUtils/ViewUtils/ViewUtils.h @@ -0,0 +1,21 @@ +// +// ViewUtils.h +// ViewUtils +// +// Created by Guilherme Rambo on 19/04/15. +// Copyright (c) 2015 Guilherme Rambo. All rights reserved. +// + +#import + +//! Project version number for ViewUtils. +FOUNDATION_EXPORT double ViewUtilsVersionNumber; + +//! Project version string for ViewUtils. +FOUNDATION_EXPORT const unsigned char ViewUtilsVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + +#import +#import +#import \ No newline at end of file diff --git a/WWDC.xcodeproj/project.pbxproj b/WWDC.xcodeproj/project.pbxproj new file mode 100644 index 00000000..fda5a1c4 --- /dev/null +++ b/WWDC.xcodeproj/project.pbxproj @@ -0,0 +1,479 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + DD2016061AE3FA6600DD8155 /* SessionProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2016051AE3FA6600DD8155 /* SessionProgressView.swift */; }; + DD2016681AE402B400DD8155 /* ViewUtils.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD2016661AE4029100DD8155 /* ViewUtils.framework */; }; + DD2016691AE402B400DD8155 /* ViewUtils.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DD2016661AE4029100DD8155 /* ViewUtils.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + DD20166E1AE40D0100DD8155 /* ContentBackgroundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD20166D1AE40D0100DD8155 /* ContentBackgroundView.swift */; }; + DD2016751AE411C800DD8155 /* VideoWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2016731AE411C800DD8155 /* VideoWindowController.swift */; }; + DD2016761AE411C800DD8155 /* VideoWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = DD2016741AE411C800DD8155 /* VideoWindowController.xib */; }; + DD2016781AE4141900DD8155 /* ArrayExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2016771AE4141900DD8155 /* ArrayExtensions.swift */; }; + DD4F6E521AE2D11D0099BEB5 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4F6E511AE2D11D0099BEB5 /* AppDelegate.swift */; }; + DD4F6E541AE2D11D0099BEB5 /* VideosViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4F6E531AE2D11D0099BEB5 /* VideosViewController.swift */; }; + DD4F6E561AE2D11D0099BEB5 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DD4F6E551AE2D11D0099BEB5 /* Images.xcassets */; }; + DD4F6E591AE2D11D0099BEB5 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DD4F6E571AE2D11D0099BEB5 /* Main.storyboard */; }; + DD4F6E6F1AE2D14A0099BEB5 /* MainWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4F6E6E1AE2D14A0099BEB5 /* MainWindowController.swift */; }; + DD4F6E711AE2D1C60099BEB5 /* VideoDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4F6E701AE2D1C60099BEB5 /* VideoDetailsViewController.swift */; }; + DD4F6E731AE2D5980099BEB5 /* VideoTableCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4F6E721AE2D5980099BEB5 /* VideoTableCellView.swift */; }; + DD4F6E761AE2D82A0099BEB5 /* VideosHeaderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4F6E741AE2D82A0099BEB5 /* VideosHeaderViewController.swift */; }; + DD4F6E771AE2D82A0099BEB5 /* VideosHeaderViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = DD4F6E751AE2D82A0099BEB5 /* VideosHeaderViewController.xib */; }; + DD4F6E791AE2D98F0099BEB5 /* HeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4F6E781AE2D98F0099BEB5 /* HeaderView.swift */; }; + DDD7A4331AE2DF14000A0102 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD7A4321AE2DF14000A0102 /* Theme.swift */; }; + DDD7A4351AE2E02A000A0102 /* SeparatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD7A4341AE2E02A000A0102 /* SeparatorView.swift */; }; + DDD7A4381AE2E265000A0102 /* Session.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD7A4371AE2E265000A0102 /* Session.swift */; }; + DDD7A43A1AE2E305000A0102 /* DataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD7A4391AE2E305000A0102 /* DataStore.swift */; }; + DDD7A43D1AE2E39A000A0102 /* SwiftyJSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD7A43C1AE2E39A000A0102 /* SwiftyJSON.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + DD2016651AE4029100DD8155 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DD2016611AE4029100DD8155 /* ViewUtils.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = DD2016411AE401B500DD8155; + remoteInfo = ViewUtils; + }; + DD20166A1AE402B400DD8155 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DD2016611AE4029100DD8155 /* ViewUtils.xcodeproj */; + proxyType = 1; + remoteGlobalIDString = DD2016401AE401B500DD8155; + remoteInfo = ViewUtils; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + DD20166C1AE402B400DD8155 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + DD2016691AE402B400DD8155 /* ViewUtils.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + DD2016051AE3FA6600DD8155 /* SessionProgressView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionProgressView.swift; sourceTree = ""; }; + DD2016611AE4029100DD8155 /* ViewUtils.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = ViewUtils.xcodeproj; path = ViewUtils/ViewUtils.xcodeproj; sourceTree = ""; }; + DD20166D1AE40D0100DD8155 /* ContentBackgroundView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentBackgroundView.swift; sourceTree = ""; }; + DD2016731AE411C800DD8155 /* VideoWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoWindowController.swift; sourceTree = ""; }; + DD2016741AE411C800DD8155 /* VideoWindowController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = VideoWindowController.xib; sourceTree = ""; }; + DD2016771AE4141900DD8155 /* ArrayExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArrayExtensions.swift; sourceTree = ""; }; + DD4F6E4C1AE2D11D0099BEB5 /* WWDC.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = WWDC.app; sourceTree = BUILT_PRODUCTS_DIR; }; + DD4F6E501AE2D11D0099BEB5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + DD4F6E511AE2D11D0099BEB5 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + DD4F6E531AE2D11D0099BEB5 /* VideosViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideosViewController.swift; sourceTree = ""; }; + DD4F6E551AE2D11D0099BEB5 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; + DD4F6E581AE2D11D0099BEB5 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + DD4F6E6E1AE2D14A0099BEB5 /* MainWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainWindowController.swift; sourceTree = ""; }; + DD4F6E701AE2D1C60099BEB5 /* VideoDetailsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoDetailsViewController.swift; sourceTree = ""; }; + DD4F6E721AE2D5980099BEB5 /* VideoTableCellView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoTableCellView.swift; sourceTree = ""; }; + DD4F6E741AE2D82A0099BEB5 /* VideosHeaderViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideosHeaderViewController.swift; sourceTree = ""; }; + DD4F6E751AE2D82A0099BEB5 /* VideosHeaderViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = VideosHeaderViewController.xib; sourceTree = ""; }; + DD4F6E781AE2D98F0099BEB5 /* HeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HeaderView.swift; sourceTree = ""; }; + DDD7A4321AE2DF14000A0102 /* Theme.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = ""; }; + DDD7A4341AE2E02A000A0102 /* SeparatorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SeparatorView.swift; sourceTree = ""; }; + DDD7A4371AE2E265000A0102 /* Session.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Session.swift; sourceTree = ""; }; + DDD7A4391AE2E305000A0102 /* DataStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataStore.swift; sourceTree = ""; }; + DDD7A43C1AE2E39A000A0102 /* SwiftyJSON.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftyJSON.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + DD4F6E491AE2D11D0099BEB5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + DD2016681AE402B400DD8155 /* ViewUtils.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + DD2016621AE4029100DD8155 /* Products */ = { + isa = PBXGroup; + children = ( + DD2016661AE4029100DD8155 /* ViewUtils.framework */, + ); + name = Products; + sourceTree = ""; + }; + DD4F6E431AE2D11D0099BEB5 = { + isa = PBXGroup; + children = ( + DD2016611AE4029100DD8155 /* ViewUtils.xcodeproj */, + DD4F6E4E1AE2D11D0099BEB5 /* WWDC */, + DD4F6E4D1AE2D11D0099BEB5 /* Products */, + ); + sourceTree = ""; + }; + DD4F6E4D1AE2D11D0099BEB5 /* Products */ = { + isa = PBXGroup; + children = ( + DD4F6E4C1AE2D11D0099BEB5 /* WWDC.app */, + ); + name = Products; + sourceTree = ""; + }; + DD4F6E4E1AE2D11D0099BEB5 /* WWDC */ = { + isa = PBXGroup; + children = ( + DDD7A43B1AE2E392000A0102 /* Vendor */, + DDD7A4361AE2E253000A0102 /* Models */, + DDD7A4311AE2DF06000A0102 /* Helpers */, + DDD7A4301AE2DEFD000A0102 /* Resources */, + DDD7A42F1AE2DEF2000A0102 /* Aux View Controllers */, + DDD7A42E1AE2DEDD000A0102 /* Main Controllers */, + DDD7A42D1AE2DECE000A0102 /* Views */, + DD4F6E511AE2D11D0099BEB5 /* AppDelegate.swift */, + DD4F6E551AE2D11D0099BEB5 /* Images.xcassets */, + DD4F6E4F1AE2D11D0099BEB5 /* Supporting Files */, + ); + path = WWDC; + sourceTree = ""; + }; + DD4F6E4F1AE2D11D0099BEB5 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + DD4F6E501AE2D11D0099BEB5 /* Info.plist */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + DDD7A42D1AE2DECE000A0102 /* Views */ = { + isa = PBXGroup; + children = ( + DD4F6E721AE2D5980099BEB5 /* VideoTableCellView.swift */, + DD4F6E781AE2D98F0099BEB5 /* HeaderView.swift */, + DDD7A4341AE2E02A000A0102 /* SeparatorView.swift */, + DD2016051AE3FA6600DD8155 /* SessionProgressView.swift */, + DD20166D1AE40D0100DD8155 /* ContentBackgroundView.swift */, + ); + name = Views; + sourceTree = ""; + }; + DDD7A42E1AE2DEDD000A0102 /* Main Controllers */ = { + isa = PBXGroup; + children = ( + DD4F6E701AE2D1C60099BEB5 /* VideoDetailsViewController.swift */, + DD4F6E531AE2D11D0099BEB5 /* VideosViewController.swift */, + DD2016731AE411C800DD8155 /* VideoWindowController.swift */, + ); + name = "Main Controllers"; + sourceTree = ""; + }; + DDD7A42F1AE2DEF2000A0102 /* Aux View Controllers */ = { + isa = PBXGroup; + children = ( + DD4F6E741AE2D82A0099BEB5 /* VideosHeaderViewController.swift */, + DD4F6E6E1AE2D14A0099BEB5 /* MainWindowController.swift */, + ); + name = "Aux View Controllers"; + sourceTree = ""; + }; + DDD7A4301AE2DEFD000A0102 /* Resources */ = { + isa = PBXGroup; + children = ( + DD2016741AE411C800DD8155 /* VideoWindowController.xib */, + DD4F6E571AE2D11D0099BEB5 /* Main.storyboard */, + DD4F6E751AE2D82A0099BEB5 /* VideosHeaderViewController.xib */, + ); + name = Resources; + sourceTree = ""; + }; + DDD7A4311AE2DF06000A0102 /* Helpers */ = { + isa = PBXGroup; + children = ( + DDD7A4321AE2DF14000A0102 /* Theme.swift */, + DD2016771AE4141900DD8155 /* ArrayExtensions.swift */, + ); + name = Helpers; + sourceTree = ""; + }; + DDD7A4361AE2E253000A0102 /* Models */ = { + isa = PBXGroup; + children = ( + DDD7A4371AE2E265000A0102 /* Session.swift */, + DDD7A4391AE2E305000A0102 /* DataStore.swift */, + ); + name = Models; + sourceTree = ""; + }; + DDD7A43B1AE2E392000A0102 /* Vendor */ = { + isa = PBXGroup; + children = ( + DDD7A43C1AE2E39A000A0102 /* SwiftyJSON.swift */, + ); + name = Vendor; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + DD4F6E4B1AE2D11D0099BEB5 /* WWDC */ = { + isa = PBXNativeTarget; + buildConfigurationList = DD4F6E681AE2D11D0099BEB5 /* Build configuration list for PBXNativeTarget "WWDC" */; + buildPhases = ( + DD4F6E481AE2D11D0099BEB5 /* Sources */, + DD4F6E491AE2D11D0099BEB5 /* Frameworks */, + DD4F6E4A1AE2D11D0099BEB5 /* Resources */, + DD20166C1AE402B400DD8155 /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + DD20166B1AE402B400DD8155 /* PBXTargetDependency */, + ); + name = WWDC; + productName = WWDC; + productReference = DD4F6E4C1AE2D11D0099BEB5 /* WWDC.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + DD4F6E441AE2D11D0099BEB5 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0630; + ORGANIZATIONNAME = "Guilherme Rambo"; + TargetAttributes = { + DD4F6E4B1AE2D11D0099BEB5 = { + CreatedOnToolsVersion = 6.3; + DevelopmentTeam = 8C7439RJLG; + }; + }; + }; + buildConfigurationList = DD4F6E471AE2D11D0099BEB5 /* Build configuration list for PBXProject "WWDC" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = DD4F6E431AE2D11D0099BEB5; + productRefGroup = DD4F6E4D1AE2D11D0099BEB5 /* Products */; + projectDirPath = ""; + projectReferences = ( + { + ProductGroup = DD2016621AE4029100DD8155 /* Products */; + ProjectRef = DD2016611AE4029100DD8155 /* ViewUtils.xcodeproj */; + }, + ); + projectRoot = ""; + targets = ( + DD4F6E4B1AE2D11D0099BEB5 /* WWDC */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXReferenceProxy section */ + DD2016661AE4029100DD8155 /* ViewUtils.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = ViewUtils.framework; + remoteRef = DD2016651AE4029100DD8155 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; +/* End PBXReferenceProxy section */ + +/* Begin PBXResourcesBuildPhase section */ + DD4F6E4A1AE2D11D0099BEB5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DD4F6E561AE2D11D0099BEB5 /* Images.xcassets in Resources */, + DD2016761AE411C800DD8155 /* VideoWindowController.xib in Resources */, + DD4F6E591AE2D11D0099BEB5 /* Main.storyboard in Resources */, + DD4F6E771AE2D82A0099BEB5 /* VideosHeaderViewController.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + DD4F6E481AE2D11D0099BEB5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DD20166E1AE40D0100DD8155 /* ContentBackgroundView.swift in Sources */, + DDD7A4351AE2E02A000A0102 /* SeparatorView.swift in Sources */, + DD4F6E761AE2D82A0099BEB5 /* VideosHeaderViewController.swift in Sources */, + DDD7A4331AE2DF14000A0102 /* Theme.swift in Sources */, + DDD7A43D1AE2E39A000A0102 /* SwiftyJSON.swift in Sources */, + DD2016751AE411C800DD8155 /* VideoWindowController.swift in Sources */, + DD4F6E541AE2D11D0099BEB5 /* VideosViewController.swift in Sources */, + DDD7A43A1AE2E305000A0102 /* DataStore.swift in Sources */, + DD2016061AE3FA6600DD8155 /* SessionProgressView.swift in Sources */, + DD4F6E521AE2D11D0099BEB5 /* AppDelegate.swift in Sources */, + DD4F6E731AE2D5980099BEB5 /* VideoTableCellView.swift in Sources */, + DD4F6E711AE2D1C60099BEB5 /* VideoDetailsViewController.swift in Sources */, + DD4F6E6F1AE2D14A0099BEB5 /* MainWindowController.swift in Sources */, + DD4F6E791AE2D98F0099BEB5 /* HeaderView.swift in Sources */, + DDD7A4381AE2E265000A0102 /* Session.swift in Sources */, + DD2016781AE4141900DD8155 /* ArrayExtensions.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + DD20166B1AE402B400DD8155 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = ViewUtils; + targetProxy = DD20166A1AE402B400DD8155 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + DD4F6E571AE2D11D0099BEB5 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + DD4F6E581AE2D11D0099BEB5 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + DD4F6E661AE2D11D0099BEB5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.10; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + DD4F6E671AE2D11D0099BEB5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.10; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + }; + name = Release; + }; + DD4F6E691AE2D11D0099BEB5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "Developer ID Application"; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = WWDC/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + DD4F6E6A1AE2D11D0099BEB5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "Developer ID Application"; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = WWDC/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + DD4F6E471AE2D11D0099BEB5 /* Build configuration list for PBXProject "WWDC" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DD4F6E661AE2D11D0099BEB5 /* Debug */, + DD4F6E671AE2D11D0099BEB5 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + DD4F6E681AE2D11D0099BEB5 /* Build configuration list for PBXNativeTarget "WWDC" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DD4F6E691AE2D11D0099BEB5 /* Debug */, + DD4F6E6A1AE2D11D0099BEB5 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = DD4F6E441AE2D11D0099BEB5 /* Project object */; +} diff --git a/WWDC/AppDelegate.swift b/WWDC/AppDelegate.swift new file mode 100644 index 00000000..7a508cd6 --- /dev/null +++ b/WWDC/AppDelegate.swift @@ -0,0 +1,26 @@ +// +// AppDelegate.swift +// WWDC +// +// Created by Guilherme Rambo on 18/04/15. +// Copyright (c) 2015 Guilherme Rambo. All rights reserved. +// + +import Cocoa + +@NSApplicationMain +class AppDelegate: NSObject, NSApplicationDelegate { + + + + func applicationDidFinishLaunching(aNotification: NSNotification) { + // Insert code here to initialize your application + } + + func applicationWillTerminate(aNotification: NSNotification) { + // Insert code here to tear down your application + } + + +} + diff --git a/WWDC/ArrayExtensions.swift b/WWDC/ArrayExtensions.swift new file mode 100644 index 00000000..83f3c9ab --- /dev/null +++ b/WWDC/ArrayExtensions.swift @@ -0,0 +1,26 @@ +// +// ArrayExtensions.swift +// WWDC +// +// Created by Guilherme Rambo on 19/04/15. +// Copyright (c) 2015 Guilherme Rambo. All rights reserved. +// + +import Foundation + +extension Array { + mutating func removeObject(object: U) { + var index: Int? + for (idx, objectToCompare) in enumerate(self) { + if let to = objectToCompare as? U { + if object == to { + index = idx + } + } + } + + if(index != nil) { + self.removeAtIndex(index!) + } + } +} \ No newline at end of file diff --git a/WWDC/Base.lproj/Main.storyboard b/WWDC/Base.lproj/Main.storyboard new file mode 100644 index 00000000..71c80965 --- /dev/null +++ b/WWDC/Base.lproj/Main.storyboard @@ -0,0 +1,951 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Default + + + + + + + Left to Right + + + + + + + Right to Left + + + + + + + + + + + Default + + + + + + + Left to Right + + + + + + + Right to Left + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WWDC/ContentBackgroundView.swift b/WWDC/ContentBackgroundView.swift new file mode 100644 index 00000000..447dd637 --- /dev/null +++ b/WWDC/ContentBackgroundView.swift @@ -0,0 +1,21 @@ +// +// ContentBackgroundView.swift +// WWDC +// +// Created by Guilherme Rambo on 19/04/15. +// Copyright (c) 2015 Guilherme Rambo. All rights reserved. +// + +import Cocoa + +@IBDesignable +class ContentBackgroundView: NSView { + + override func drawRect(dirtyRect: NSRect) { + super.drawRect(dirtyRect) + + Theme.WWDCTheme.backgroundColor.set() + NSRectFill(dirtyRect) + } + +} diff --git a/WWDC/DataStore.swift b/WWDC/DataStore.swift new file mode 100644 index 00000000..fe574074 --- /dev/null +++ b/WWDC/DataStore.swift @@ -0,0 +1,107 @@ +// +// DataStore.swift +// WWDC +// +// Created by Guilherme Rambo on 18/04/15. +// Copyright (c) 2015 Guilherme Rambo. All rights reserved. +// + +import Foundation + +private let _internalServiceURL = "http://wwdc.guilhermerambo.me/index.json" +private let _SharedStore = DataStore() + +class DataStore: NSObject { + + class var SharedStore: DataStore { + return _SharedStore + } + + typealias fetchSessionsCompletionHandler = (Bool, [Session]) -> Void + +// var cachedAppleURL: NSURL? = NSUserDefaults.standardUserDefaults().URLForKey("Apple URL") + var cachedAppleURL: NSURL? = nil + + let session = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration()) + + func fetchSessions(completionHandler: fetchSessionsCompletionHandler) { + if let appleURL = cachedAppleURL { + doFetchSessions(completionHandler) + } else { + let internalServiceURL = NSURL(string: _internalServiceURL) + + session.dataTaskWithURL(internalServiceURL!, completionHandler: { [unowned self] data, response, error in + if data == nil { + completionHandler(false, []) + return + } + + let internalServiceJSON = JSON(data: data!).dictionary! + let appleURL = internalServiceJSON["url"]!.string! + + NSUserDefaults.standardUserDefaults().setURL(NSURL(string: appleURL)!, forKey: "Apple URL") + NSUserDefaults.standardUserDefaults().synchronize() + self.cachedAppleURL = NSURL(string: appleURL)! + + self.doFetchSessions(completionHandler) + }).resume() + } + } + + func doFetchSessions(completionHandler: fetchSessionsCompletionHandler) { + session.dataTaskWithURL(cachedAppleURL!, completionHandler: { data, response, error in + if data == nil { + completionHandler(false, []) + return + } + + if let container = JSON(data: data).dictionary { + let jsonSessions = container["sessions"]!.array! + + var sessions: [Session] = [] + + for jsonSession:JSON in jsonSessions { + var focuses:[String] = [] + for focus:JSON in jsonSession["focus"].array! { + focuses.append(focus.string!) + } + + let session = Session(date: jsonSession["date"].string, + description: jsonSession["description"].string!, + focus: focuses, + id: jsonSession["id"].int!, + slides: jsonSession["slides"].string, + title: jsonSession["title"].string!, + track: jsonSession["track"].string!, + url: jsonSession["url"].string!, + year: jsonSession["year"].int!) + + sessions.append(session) + } + + completionHandler(true, sessions) + } else { + completionHandler(false, []) + } + }).resume() + } + + let defaults = NSUserDefaults.standardUserDefaults() + + func fetchSessionProgress(session: Session) -> Double { + return defaults.doubleForKey(session.progressKey) + } + + func putSessionProgress(session: Session, progress: Double) { + defaults.setDouble(progress, forKey: session.progressKey) + } + + func fetchSessionCurrentPosition(session: Session) -> Double { + return defaults.doubleForKey(session.currentPositionKey) + } + + func putSessionCurrentPosition(session: Session, position: Double) { + defaults.setDouble(position, forKey: session.currentPositionKey) + } + +} diff --git a/WWDC/HeaderView.swift b/WWDC/HeaderView.swift new file mode 100644 index 00000000..6a15b7fb --- /dev/null +++ b/WWDC/HeaderView.swift @@ -0,0 +1,20 @@ +// +// HeaderView.swift +// WWDC +// +// Created by Guilherme Rambo on 18/04/15. +// Copyright (c) 2015 Guilherme Rambo. All rights reserved. +// + +import Cocoa + +class HeaderView: NSView { + + override func drawRect(dirtyRect: NSRect) { + super.drawRect(dirtyRect) + + NSColor.whiteColor().set() + NSRectFill(dirtyRect) + } + +} diff --git a/WWDC/Images.xcassets/AppIcon.appiconset/Contents.json b/WWDC/Images.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..7cd4f8e1 --- /dev/null +++ b/WWDC/Images.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "icon_16x16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "icon_16x16@2x.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "icon_32x32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "icon_32x32@2x.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "icon_128x128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "icon_128x128@2x.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "icon_256x256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "icon_256x256@2x.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "icon_512x512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "icon_512x512@2x.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/WWDC/Images.xcassets/AppIcon.appiconset/icon_128x128.png b/WWDC/Images.xcassets/AppIcon.appiconset/icon_128x128.png new file mode 100644 index 00000000..fa9c453e Binary files /dev/null and b/WWDC/Images.xcassets/AppIcon.appiconset/icon_128x128.png differ diff --git a/WWDC/Images.xcassets/AppIcon.appiconset/icon_128x128@2x.png b/WWDC/Images.xcassets/AppIcon.appiconset/icon_128x128@2x.png new file mode 100644 index 00000000..e2f26aee Binary files /dev/null and b/WWDC/Images.xcassets/AppIcon.appiconset/icon_128x128@2x.png differ diff --git a/WWDC/Images.xcassets/AppIcon.appiconset/icon_16x16.png b/WWDC/Images.xcassets/AppIcon.appiconset/icon_16x16.png new file mode 100644 index 00000000..5abb2263 Binary files /dev/null and b/WWDC/Images.xcassets/AppIcon.appiconset/icon_16x16.png differ diff --git a/WWDC/Images.xcassets/AppIcon.appiconset/icon_16x16@2x.png b/WWDC/Images.xcassets/AppIcon.appiconset/icon_16x16@2x.png new file mode 100644 index 00000000..ba52c34a Binary files /dev/null and b/WWDC/Images.xcassets/AppIcon.appiconset/icon_16x16@2x.png differ diff --git a/WWDC/Images.xcassets/AppIcon.appiconset/icon_256x256.png b/WWDC/Images.xcassets/AppIcon.appiconset/icon_256x256.png new file mode 100644 index 00000000..e2f26aee Binary files /dev/null and b/WWDC/Images.xcassets/AppIcon.appiconset/icon_256x256.png differ diff --git a/WWDC/Images.xcassets/AppIcon.appiconset/icon_256x256@2x.png b/WWDC/Images.xcassets/AppIcon.appiconset/icon_256x256@2x.png new file mode 100644 index 00000000..158aa8fb Binary files /dev/null and b/WWDC/Images.xcassets/AppIcon.appiconset/icon_256x256@2x.png differ diff --git a/WWDC/Images.xcassets/AppIcon.appiconset/icon_32x32.png b/WWDC/Images.xcassets/AppIcon.appiconset/icon_32x32.png new file mode 100644 index 00000000..ba52c34a Binary files /dev/null and b/WWDC/Images.xcassets/AppIcon.appiconset/icon_32x32.png differ diff --git a/WWDC/Images.xcassets/AppIcon.appiconset/icon_32x32@2x.png b/WWDC/Images.xcassets/AppIcon.appiconset/icon_32x32@2x.png new file mode 100644 index 00000000..985c9935 Binary files /dev/null and b/WWDC/Images.xcassets/AppIcon.appiconset/icon_32x32@2x.png differ diff --git a/WWDC/Images.xcassets/AppIcon.appiconset/icon_512x512.png b/WWDC/Images.xcassets/AppIcon.appiconset/icon_512x512.png new file mode 100644 index 00000000..158aa8fb Binary files /dev/null and b/WWDC/Images.xcassets/AppIcon.appiconset/icon_512x512.png differ diff --git a/WWDC/Images.xcassets/AppIcon.appiconset/icon_512x512@2x.png b/WWDC/Images.xcassets/AppIcon.appiconset/icon_512x512@2x.png new file mode 100644 index 00000000..9d73d3ef Binary files /dev/null and b/WWDC/Images.xcassets/AppIcon.appiconset/icon_512x512@2x.png differ diff --git a/WWDC/Info.plist b/WWDC/Info.plist new file mode 100644 index 00000000..e453526f --- /dev/null +++ b/WWDC/Info.plist @@ -0,0 +1,34 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + br.com.guilhermerambo.$(PRODUCT_NAME:rfc1034identifier) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + Copyright © 2015 Guilherme Rambo. All rights reserved. + NSMainStoryboardFile + Main + NSPrincipalClass + NSApplication + + diff --git a/WWDC/MainWindowController.swift b/WWDC/MainWindowController.swift new file mode 100644 index 00000000..b50d2647 --- /dev/null +++ b/WWDC/MainWindowController.swift @@ -0,0 +1,25 @@ +// +// MainWindowController.swift +// WWDC +// +// Created by Guilherme Rambo on 18/04/15. +// Copyright (c) 2015 Guilherme Rambo. All rights reserved. +// + +import Cocoa + +class MainWindowController: NSWindowController { + + override func windowDidLoad() { + super.windowDidLoad() + + if let view = window?.contentView as? NSView { + view.wantsLayer = true + } + + window?.styleMask |= NSFullSizeContentViewWindowMask + window?.titleVisibility = .Hidden + window?.titlebarAppearsTransparent = true + } + +} diff --git a/WWDC/SeparatorView.swift b/WWDC/SeparatorView.swift new file mode 100644 index 00000000..79e44aa2 --- /dev/null +++ b/WWDC/SeparatorView.swift @@ -0,0 +1,20 @@ +// +// SeparatorView.swift +// WWDC +// +// Created by Guilherme Rambo on 18/04/15. +// Copyright (c) 2015 Guilherme Rambo. All rights reserved. +// + +import Cocoa + +class SeparatorView: NSView { + + override func drawRect(dirtyRect: NSRect) { + super.drawRect(dirtyRect) + + Theme.WWDCTheme.separatorColor.set() + NSRectFill(dirtyRect) + } + +} diff --git a/WWDC/Session.swift b/WWDC/Session.swift new file mode 100644 index 00000000..55002289 --- /dev/null +++ b/WWDC/Session.swift @@ -0,0 +1,68 @@ +// +// Video.swift +// WWDC +// +// Created by Guilherme Rambo on 18/04/15. +// Copyright (c) 2015 Guilherme Rambo. All rights reserved. +// + +import Foundation + +let SessionProgressDidChangeNotification = "SessionProgressDidChangeNotification" + +struct Session { + + var date: String? + var description: String + var focus: [String] + var id: Int + var slides: String? + var title: String + var track: String + var url: String + var year: Int + + var progress: Double { + get { + return DataStore.SharedStore.fetchSessionProgress(self) + } + set { + DataStore.SharedStore.putSessionProgress(self, progress: newValue) + NSNotificationCenter.defaultCenter().postNotificationName(SessionProgressDidChangeNotification, object: self.progressKey) + } + } + + var currentPosition: Double { + get { + return DataStore.SharedStore.fetchSessionCurrentPosition(self) + } + set { + DataStore.SharedStore.putSessionCurrentPosition(self, position: newValue) + } + } + + var progressKey: String { + get { + return "\(year)-\(id)-progress" + } + } + var currentPositionKey: String { + get { + return "\(year)-\(id)-currentPosition" + } + } + + init(date: String?, description: String, focus: [String], id: Int, slides: String?, title: String, track: String, url: String, year: Int) + { + self.date = date + self.description = description + self.focus = focus + self.id = id + self.slides = slides + self.title = title + self.track = track + self.url = url + self.year = year + } + +} \ No newline at end of file diff --git a/WWDC/SessionProgressView.swift b/WWDC/SessionProgressView.swift new file mode 100644 index 00000000..5998d617 --- /dev/null +++ b/WWDC/SessionProgressView.swift @@ -0,0 +1,43 @@ +// +// SessionStatusView.swift +// WWDC +// +// Created by Guilherme Rambo on 19/04/15. +// Copyright (c) 2015 Guilherme Rambo. All rights reserved. +// + +import Cocoa + +@IBDesignable +class SessionProgressView: NSView { + + @IBInspectable var progress: Double = 0 { + didSet { + setNeedsDisplayInRect(bounds) + } + } + + override func drawRect(dirtyRect: NSRect) { + super.drawRect(dirtyRect) + + if progress >= 1 { + return + } + + let circle = NSBezierPath(ovalInRect: NSInsetRect(bounds, 5.0, 5.0)) + circle.addClip() + + Theme.WWDCTheme.backgroundColor.set() + NSRectFill(bounds) + Theme.WWDCTheme.fillColor.set() + circle.stroke() + + if progress == 0 { + NSRectFill(bounds) + } else if progress < 1 { + NSBezierPath(rect: NSMakeRect(0, 0, round(NSWidth(bounds)/2), NSHeight(bounds))).addClip() + NSRectFill(bounds) + } + } + +} diff --git a/WWDC/SwiftyJSON.swift b/WWDC/SwiftyJSON.swift new file mode 100755 index 00000000..40fef595 --- /dev/null +++ b/WWDC/SwiftyJSON.swift @@ -0,0 +1,1335 @@ +// SwiftyJSON.swift +// +// Copyright (c) 2014 Ruoyu Fu, Pinglin Tang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +// MARK: - Error + +///Error domain +public let ErrorDomain: String! = "SwiftyJSONErrorDomain" + +///Error code +public let ErrorUnsupportedType: Int! = 999 +public let ErrorIndexOutOfBounds: Int! = 900 +public let ErrorWrongType: Int! = 901 +public let ErrorNotExist: Int! = 500 + +// MARK: - JSON Type + +/** +JSON's type definitions. + +See http://tools.ietf.org/html/rfc7231#section-4.3 +*/ +public enum Type :Int{ + + case Number + case String + case Bool + case Array + case Dictionary + case Null + case Unknown +} + +// MARK: - JSON Base + +public struct JSON { + + /** + Creates a JSON using the data. + + :param: data The NSData used to convert to json.Top level object in data is an NSArray or NSDictionary + :param: opt The JSON serialization reading options. `.AllowFragments` by default. + :param: error error The NSErrorPointer used to return the error. `nil` by default. + + :returns: The created JSON + */ + public init(data:NSData, options opt: NSJSONReadingOptions = .AllowFragments, error: NSErrorPointer = nil) { + if let object: AnyObject = NSJSONSerialization.JSONObjectWithData(data, options: opt, error: error) { + self.init(object) + } else { + self.init(NSNull()) + } + } + + /** + Creates a JSON using the object. + + :param: object The object must have the following properties: All objects are NSString/String, NSNumber/Int/Float/Double/Bool, NSArray/Array, NSDictionary/Dictionary, or NSNull; All dictionary keys are NSStrings/String; NSNumbers are not NaN or infinity. + + :returns: The created JSON + */ + public init(_ object: AnyObject) { + self.object = object + } + + /// Private object + private var _object: AnyObject = NSNull() + /// Private type + private var _type: Type = .Null + /// prviate error + private var _error: NSError? + + /// Object in JSON + public var object: AnyObject { + get { + return _object + } + set { + _object = newValue + switch newValue { + case let number as NSNumber: + if number.isBool { + _type = .Bool + } else { + _type = .Number + } + case let string as NSString: + _type = .String + case let null as NSNull: + _type = .Null + case let array as [AnyObject]: + _type = .Array + case let dictionary as [String : AnyObject]: + _type = .Dictionary + default: + _type = .Unknown + _object = NSNull() + _error = NSError(domain: ErrorDomain, code: ErrorUnsupportedType, userInfo: [NSLocalizedDescriptionKey: "It is a unsupported type"]) + } + } + } + + /// json type + public var type: Type { get { return _type } } + + /// Error in JSON + public var error: NSError? { get { return self._error } } + + /// The static null json + public static var nullJSON: JSON { get { return JSON(NSNull()) } } + +} + +// MARK: - SequenceType +extension JSON: SequenceType{ + + /// If `type` is `.Array` or `.Dictionary`, return `array.empty` or `dictonary.empty` otherwise return `false`. + public var isEmpty: Bool { + get { + switch self.type { + case .Array: + return (self.object as! [AnyObject]).isEmpty + case .Dictionary: + return (self.object as! [String : AnyObject]).isEmpty + default: + return false + } + } + } + + /// If `type` is `.Array` or `.Dictionary`, return `array.count` or `dictonary.count` otherwise return `0`. + public var count: Int { + get { + switch self.type { + case .Array: + return self.arrayValue.count + case .Dictionary: + return self.dictionaryValue.count + default: + return 0 + } + } + } + + /** + If `type` is `.Array` or `.Dictionary`, return a generator over the elements like `Array` or `Dictionary`, otherwise return a generator over empty. + + :returns: Return a *generator* over the elements of this *sequence*. + */ + public func generate() -> GeneratorOf <(String, JSON)> { + switch self.type { + case .Array: + let array_ = object as! [AnyObject] + var generate_ = array_.generate() + var index_: Int = 0 + return GeneratorOf<(String, JSON)> { + if let element_: AnyObject = generate_.next() { + return ("\(index_++)", JSON(element_)) + } else { + return nil + } + } + case .Dictionary: + let dictionary_ = object as! [String : AnyObject] + var generate_ = dictionary_.generate() + return GeneratorOf<(String, JSON)> { + if let (key_: String, value_: AnyObject) = generate_.next() { + return (key_, JSON(value_)) + } else { + return nil + } + } + default: + return GeneratorOf<(String, JSON)> { + return nil + } + } + } +} + +// MARK: - Subscript + +/** +* To mark both String and Int can be used in subscript. +*/ +public protocol SubscriptType {} + +extension Int: SubscriptType {} + +extension String: SubscriptType {} + +extension JSON { + + /// If `type` is `.Array`, return json which's object is `array[index]`, otherwise return null json with error. + private subscript(#index: Int) -> JSON { + get { + + if self.type != .Array { + var errorResult_ = JSON.nullJSON + errorResult_._error = self._error ?? NSError(domain: ErrorDomain, code: ErrorWrongType, userInfo: [NSLocalizedDescriptionKey: "Array[\(index)] failure, It is not an array"]) + return errorResult_ + } + + let array_ = self.object as! [AnyObject] + + if index >= 0 && index < array_.count { + return JSON(array_[index]) + } + + var errorResult_ = JSON.nullJSON + errorResult_._error = NSError(domain: ErrorDomain, code:ErrorIndexOutOfBounds , userInfo: [NSLocalizedDescriptionKey: "Array[\(index)] is out of bounds"]) + return errorResult_ + } + set { + if self.type == .Array { + var array_ = self.object as! [AnyObject] + if array_.count > index { + array_[index] = newValue.object + self.object = array_ + } + } + } + } + + /// If `type` is `.Dictionary`, return json which's object is `dictionary[key]` , otherwise return null json with error. + private subscript(#key: String) -> JSON { + get { + var returnJSON = JSON.nullJSON + if self.type == .Dictionary { + if let object_: AnyObject = self.object[key] { + returnJSON = JSON(object_) + } else { + returnJSON._error = NSError(domain: ErrorDomain, code: ErrorNotExist, userInfo: [NSLocalizedDescriptionKey: "Dictionary[\"\(key)\"] does not exist"]) + } + } else { + returnJSON._error = self._error ?? NSError(domain: ErrorDomain, code: ErrorWrongType, userInfo: [NSLocalizedDescriptionKey: "Dictionary[\"\(key)\"] failure, It is not an dictionary"]) + } + return returnJSON + } + set { + if self.type == .Dictionary { + var dictionary_ = self.object as! [String : AnyObject] + dictionary_[key] = newValue.object + self.object = dictionary_ + } + } + } + + /// If `sub` is `Int`, return `subscript(index:)`; If `sub` is `String`, return `subscript(key:)`. + private subscript(#sub: SubscriptType) -> JSON { + get { + if sub is String { + return self[key:sub as! String] + } else { + return self[index:sub as! Int] + } + } + set { + if sub is String { + self[key:sub as! String] = newValue + } else { + self[index:sub as! Int] = newValue + } + } + } + + /** + Find a json in the complex data structuresby using the Int/String's array. + + :param: path The target json's path. Example: + + let json = JSON[data] + let path = [9,"list","person","name"] + let name = json[path] + + The same as: let name = json[9]["list"]["person"]["name"] + + :returns: Return a json found by the path or a null json with error + */ + public subscript(path: [SubscriptType]) -> JSON { + get { + if path.count == 0 { + return JSON.nullJSON + } + + var next = self + for sub in path { + next = next[sub:sub] + } + return next + } + set { + + switch path.count { + case 0: return + case 1: self[sub:path[0]] = newValue + default: + var last = newValue + var newPath = path + newPath.removeLast() + for sub in path.reverse() { + var previousLast = self[newPath] + previousLast[sub:sub] = last + last = previousLast + if newPath.count <= 1 { + break + } + newPath.removeLast() + } + self[sub:newPath[0]] = last + } + } + } + + /** + Find a json in the complex data structuresby using the Int/String's array. + + :param: path The target json's path. Example: + + let name = json[9,"list","person","name"] + + The same as: let name = json[9]["list"]["person"]["name"] + + :returns: Return a json found by the path or a null json with error + */ + public subscript(path: SubscriptType...) -> JSON { + get { + return self[path] + } + set { + self[path] = newValue + } + } +} + +// MARK: - LiteralConvertible + +extension JSON: StringLiteralConvertible { + + public init(stringLiteral value: StringLiteralType) { + self.init(value) + } + + public init(extendedGraphemeClusterLiteral value: StringLiteralType) { + self.init(value) + } + + public init(unicodeScalarLiteral value: StringLiteralType) { + self.init(value) + } +} + +extension JSON: IntegerLiteralConvertible { + + public init(integerLiteral value: IntegerLiteralType) { + self.init(value) + } +} + +extension JSON: BooleanLiteralConvertible { + + public init(booleanLiteral value: BooleanLiteralType) { + self.init(value) + } +} + +extension JSON: FloatLiteralConvertible { + + public init(floatLiteral value: FloatLiteralType) { + self.init(value) + } +} + +extension JSON: DictionaryLiteralConvertible { + + public init(dictionaryLiteral elements: (String, AnyObject)...) { + var dictionary_ = [String : AnyObject]() + for (key_, value) in elements { + dictionary_[key_] = value + } + self.init(dictionary_) + } +} + +extension JSON: ArrayLiteralConvertible { + + public init(arrayLiteral elements: AnyObject...) { + self.init(elements) + } +} + +extension JSON: NilLiteralConvertible { + + public init(nilLiteral: ()) { + self.init(NSNull()) + } +} + +// MARK: - Raw + +extension JSON: RawRepresentable { + + public init?(rawValue: AnyObject) { + if JSON(rawValue).type == .Unknown { + return nil + } else { + self.init(rawValue) + } + } + + public var rawValue: AnyObject { + return self.object + } + + public func rawData(options opt: NSJSONWritingOptions = NSJSONWritingOptions(0), error: NSErrorPointer = nil) -> NSData? { + return NSJSONSerialization.dataWithJSONObject(self.object, options: opt, error:error) + } + + public func rawString(encoding: UInt = NSUTF8StringEncoding, options opt: NSJSONWritingOptions = .PrettyPrinted) -> String? { + switch self.type { + case .Array, .Dictionary: + if let data = self.rawData(options: opt) { + return NSString(data: data, encoding: encoding) as? String + } else { + return nil + } + case .String: + return (self.object as! String) + case .Number: + return (self.object as! NSNumber).stringValue + case .Bool: + return (self.object as! Bool).description + case .Null: + return "null" + default: + return nil + } + } +} + +// MARK: - Printable, DebugPrintable + +extension JSON: Printable, DebugPrintable { + + public var description: String { + if let string = self.rawString(options:.PrettyPrinted) { + return string + } else { + return "unknown" + } + } + + public var debugDescription: String { + return description + } +} + +// MARK: - Array + +extension JSON { + + //Optional [JSON] + public var array: [JSON]? { + get { + if self.type == .Array { + return map(self.object as! [AnyObject]){ JSON($0) } + } else { + return nil + } + } + } + + //Non-optional [JSON] + public var arrayValue: [JSON] { + get { + return self.array ?? [] + } + } + + //Optional [AnyObject] + public var arrayObject: [AnyObject]? { + get { + switch self.type { + case .Array: + return self.object as? [AnyObject] + default: + return nil + } + } + set { + if newValue != nil { + self.object = NSMutableArray(array: newValue!, copyItems: true) + } else { + self.object = NSNull() + } + } + } +} + +// MARK: - Dictionary + +extension JSON { + + private func _map(source: [Key: Value], transform: Value -> NewValue) -> [Key: NewValue] { + var result = [Key: NewValue](minimumCapacity:source.count) + for (key,value) in source { + result[key] = transform(value) + } + return result + } + + //Optional [String : JSON] + public var dictionary: [String : JSON]? { + get { + if self.type == .Dictionary { + return _map(self.object as! [String : AnyObject]){ JSON($0) } + } else { + return nil + } + } + } + + //Non-optional [String : JSON] + public var dictionaryValue: [String : JSON] { + get { + return self.dictionary ?? [:] + } + } + + //Optional [String : AnyObject] + public var dictionaryObject: [String : AnyObject]? { + get { + switch self.type { + case .Dictionary: + return self.object as? [String : AnyObject] + default: + return nil + } + } + set { + if newValue != nil { + self.object = NSMutableDictionary(dictionary: newValue!, copyItems: true) + } else { + self.object = NSNull() + } + } + } +} + +// MARK: - Bool + +extension JSON: BooleanType { + + //Optional bool + public var bool: Bool? { + get { + switch self.type { + case .Bool: + return self.object.boolValue + default: + return nil + } + } + set { + if newValue != nil { + self.object = NSNumber(bool: newValue!) + } else { + self.object = NSNull() + } + } + } + + //Non-optional bool + public var boolValue: Bool { + get { + switch self.type { + case .Bool, .Number, .String: + return self.object.boolValue + default: + return false + } + } + set { + self.object = NSNumber(bool: newValue) + } + } +} + +// MARK: - String + +extension JSON { + + //Optional string + public var string: String? { + get { + switch self.type { + case .String: + return self.object as? String + default: + return nil + } + } + set { + if newValue != nil { + self.object = NSString(string:newValue!) + } else { + self.object = NSNull() + } + } + } + + //Non-optional string + public var stringValue: String { + get { + switch self.type { + case .String: + return self.object as! String + case .Number: + return self.object.stringValue + case .Bool: + return (self.object as! Bool).description + default: + return "" + } + } + set { + self.object = NSString(string:newValue) + } + } +} + +// MARK: - Number +extension JSON { + + //Optional number + public var number: NSNumber? { + get { + switch self.type { + case .Number, .Bool: + return self.object as? NSNumber + default: + return nil + } + } + set { + self.object = newValue?.copy() ?? NSNull() + } + } + + //Non-optional number + public var numberValue: NSNumber { + get { + switch self.type { + case .String: + let scanner = NSScanner(string: self.object as! String) + if scanner.scanDouble(nil){ + if (scanner.atEnd) { + return NSNumber(double:(self.object as! NSString).doubleValue) + } + } + return NSNumber(double: 0.0) + case .Number, .Bool: + return self.object as! NSNumber + default: + return NSNumber(double: 0.0) + } + } + set { + self.object = newValue.copy() + } + } +} + +//MARK: - Null +extension JSON { + + public var null: NSNull? { + get { + switch self.type { + case .Null: + return NSNull() + default: + return nil + } + } + set { + self.object = NSNull() + } + } +} + +//MARK: - URL +extension JSON { + + //Optional URL + public var URL: NSURL? { + get { + switch self.type { + case .String: + if let encodedString_ = self.object.stringByAddingPercentEscapesUsingEncoding(NSUTF8StringEncoding) { + return NSURL(string: encodedString_) + } else { + return nil + } + default: + return nil + } + } + set { + self.object = newValue?.absoluteString ?? NSNull() + } + } +} + +// MARK: - Int, Double, Float, Int8, Int16, Int32, Int64 + +extension JSON { + + public var double: Double? { + get { + return self.number?.doubleValue + } + set { + if newValue != nil { + self.object = NSNumber(double: newValue!) + } else { + self.object = NSNull() + } + } + } + + public var doubleValue: Double { + get { + return self.numberValue.doubleValue + } + set { + self.object = NSNumber(double: newValue) + } + } + + public var float: Float? { + get { + return self.number?.floatValue + } + set { + if newValue != nil { + self.object = NSNumber(float: newValue!) + } else { + self.object = NSNull() + } + } + } + + public var floatValue: Float { + get { + return self.numberValue.floatValue + } + set { + self.object = NSNumber(float: newValue) + } + } + + public var int: Int? { + get { + return self.number?.longValue + } + set { + if newValue != nil { + self.object = NSNumber(integer: newValue!) + } else { + self.object = NSNull() + } + } + } + + public var intValue: Int { + get { + return self.numberValue.integerValue + } + set { + self.object = NSNumber(integer: newValue) + } + } + + public var uInt: UInt? { + get { + return self.number?.unsignedLongValue + } + set { + if newValue != nil { + self.object = NSNumber(unsignedLong: newValue!) + } else { + self.object = NSNull() + } + } + } + + public var uIntValue: UInt { + get { + return self.numberValue.unsignedLongValue + } + set { + self.object = NSNumber(unsignedLong: newValue) + } + } + + public var int8: Int8? { + get { + return self.number?.charValue + } + set { + if newValue != nil { + self.object = NSNumber(char: newValue!) + } else { + self.object = NSNull() + } + } + } + + public var int8Value: Int8 { + get { + return self.numberValue.charValue + } + set { + self.object = NSNumber(char: newValue) + } + } + + public var uInt8: UInt8? { + get { + return self.number?.unsignedCharValue + } + set { + if newValue != nil { + self.object = NSNumber(unsignedChar: newValue!) + } else { + self.object = NSNull() + } + } + } + + public var uInt8Value: UInt8 { + get { + return self.numberValue.unsignedCharValue + } + set { + self.object = NSNumber(unsignedChar: newValue) + } + } + + public var int16: Int16? { + get { + return self.number?.shortValue + } + set { + if newValue != nil { + self.object = NSNumber(short: newValue!) + } else { + self.object = NSNull() + } + } + } + + public var int16Value: Int16 { + get { + return self.numberValue.shortValue + } + set { + self.object = NSNumber(short: newValue) + } + } + + public var uInt16: UInt16? { + get { + return self.number?.unsignedShortValue + } + set { + if newValue != nil { + self.object = NSNumber(unsignedShort: newValue!) + } else { + self.object = NSNull() + } + } + } + + public var uInt16Value: UInt16 { + get { + return self.numberValue.unsignedShortValue + } + set { + self.object = NSNumber(unsignedShort: newValue) + } + } + + public var int32: Int32? { + get { + return self.number?.intValue + } + set { + if newValue != nil { + self.object = NSNumber(int: newValue!) + } else { + self.object = NSNull() + } + } + } + + public var int32Value: Int32 { + get { + return self.numberValue.intValue + } + set { + self.object = NSNumber(int: newValue) + } + } + + public var uInt32: UInt32? { + get { + return self.number?.unsignedIntValue + } + set { + if newValue != nil { + self.object = NSNumber(unsignedInt: newValue!) + } else { + self.object = NSNull() + } + } + } + + public var uInt32Value: UInt32 { + get { + return self.numberValue.unsignedIntValue + } + set { + self.object = NSNumber(unsignedInt: newValue) + } + } + + public var int64: Int64? { + get { + return self.number?.longLongValue + } + set { + if newValue != nil { + self.object = NSNumber(longLong: newValue!) + } else { + self.object = NSNull() + } + } + } + + public var int64Value: Int64 { + get { + return self.numberValue.longLongValue + } + set { + self.object = NSNumber(longLong: newValue) + } + } + + public var uInt64: UInt64? { + get { + return self.number?.unsignedLongLongValue + } + set { + if newValue != nil { + self.object = NSNumber(unsignedLongLong: newValue!) + } else { + self.object = NSNull() + } + } + } + + public var uInt64Value: UInt64 { + get { + return self.numberValue.unsignedLongLongValue + } + set { + self.object = NSNumber(unsignedLongLong: newValue) + } + } +} + +//MARK: - Comparable +extension JSON: Comparable {} + +public func ==(lhs: JSON, rhs: JSON) -> Bool { + + switch (lhs.type, rhs.type) { + case (.Number, .Number): + return (lhs.object as! NSNumber) == (rhs.object as! NSNumber) + case (.String, .String): + return (lhs.object as! String) == (rhs.object as! String) + case (.Bool, .Bool): + return (lhs.object as! Bool) == (rhs.object as! Bool) + case (.Array, .Array): + return (lhs.object as! NSArray) == (rhs.object as! NSArray) + case (.Dictionary, .Dictionary): + return (lhs.object as! NSDictionary) == (rhs.object as! NSDictionary) + case (.Null, .Null): + return true + default: + return false + } +} + +public func <=(lhs: JSON, rhs: JSON) -> Bool { + + switch (lhs.type, rhs.type) { + case (.Number, .Number): + return (lhs.object as! NSNumber) <= (rhs.object as! NSNumber) + case (.String, .String): + return (lhs.object as! String) <= (rhs.object as! String) + case (.Bool, .Bool): + return (lhs.object as! Bool) == (rhs.object as! Bool) + case (.Array, .Array): + return (lhs.object as! NSArray) == (rhs.object as! NSArray) + case (.Dictionary, .Dictionary): + return (lhs.object as! NSDictionary) == (rhs.object as! NSDictionary) + case (.Null, .Null): + return true + default: + return false + } +} + +public func >=(lhs: JSON, rhs: JSON) -> Bool { + + switch (lhs.type, rhs.type) { + case (.Number, .Number): + return (lhs.object as! NSNumber) >= (rhs.object as! NSNumber) + case (.String, .String): + return (lhs.object as! String) >= (rhs.object as! String) + case (.Bool, .Bool): + return (lhs.object as! Bool) == (rhs.object as! Bool) + case (.Array, .Array): + return (lhs.object as! NSArray) == (rhs.object as! NSArray) + case (.Dictionary, .Dictionary): + return (lhs.object as! NSDictionary) == (rhs.object as! NSDictionary) + case (.Null, .Null): + return true + default: + return false + } +} + +public func >(lhs: JSON, rhs: JSON) -> Bool { + + switch (lhs.type, rhs.type) { + case (.Number, .Number): + return (lhs.object as! NSNumber) > (rhs.object as! NSNumber) + case (.String, .String): + return (lhs.object as! String) > (rhs.object as! String) + default: + return false + } +} + +public func <(lhs: JSON, rhs: JSON) -> Bool { + + switch (lhs.type, rhs.type) { + case (.Number, .Number): + return (lhs.object as! NSNumber) < (rhs.object as! NSNumber) + case (.String, .String): + return (lhs.object as! String) < (rhs.object as! String) + default: + return false + } +} + +private let trueNumber = NSNumber(bool: true) +private let falseNumber = NSNumber(bool: false) +private let trueObjCType = String.fromCString(trueNumber.objCType) +private let falseObjCType = String.fromCString(falseNumber.objCType) + +// MARK: - NSNumber: Comparable + +extension NSNumber: Comparable { + var isBool:Bool { + get { + let objCType = String.fromCString(self.objCType) + if (self.compare(trueNumber) == NSComparisonResult.OrderedSame && objCType == trueObjCType) || (self.compare(falseNumber) == NSComparisonResult.OrderedSame && objCType == falseObjCType){ + return true + } else { + return false + } + } + } +} + +public func ==(lhs: NSNumber, rhs: NSNumber) -> Bool { + switch (lhs.isBool, rhs.isBool) { + case (false, true): + return false + case (true, false): + return false + default: + return lhs.compare(rhs) == NSComparisonResult.OrderedSame + } +} + +public func !=(lhs: NSNumber, rhs: NSNumber) -> Bool { + return !(lhs == rhs) +} + +public func <(lhs: NSNumber, rhs: NSNumber) -> Bool { + + switch (lhs.isBool, rhs.isBool) { + case (false, true): + return false + case (true, false): + return false + default: + return lhs.compare(rhs) == NSComparisonResult.OrderedAscending + } +} + +public func >(lhs: NSNumber, rhs: NSNumber) -> Bool { + + switch (lhs.isBool, rhs.isBool) { + case (false, true): + return false + case (true, false): + return false + default: + return lhs.compare(rhs) == NSComparisonResult.OrderedDescending + } +} + +public func <=(lhs: NSNumber, rhs: NSNumber) -> Bool { + + switch (lhs.isBool, rhs.isBool) { + case (false, true): + return false + case (true, false): + return false + default: + return lhs.compare(rhs) != NSComparisonResult.OrderedDescending + } +} + +public func >=(lhs: NSNumber, rhs: NSNumber) -> Bool { + + switch (lhs.isBool, rhs.isBool) { + case (false, true): + return false + case (true, false): + return false + default: + return lhs.compare(rhs) != NSComparisonResult.OrderedAscending + } +} + +//MARK:- Unavailable + +@availability(*, unavailable, renamed="JSON") +public typealias JSONValue = JSON + +extension JSON { + + @availability(*, unavailable, message="use 'init(_ object:AnyObject)' instead") + public init(object: AnyObject) { + self = JSON(object) + } + + @availability(*, unavailable, renamed="dictionaryObject") + public var dictionaryObjects: [String : AnyObject]? { + get { return self.dictionaryObject } + } + + @availability(*, unavailable, renamed="arrayObject") + public var arrayObjects: [AnyObject]? { + get { return self.arrayObject } + } + + @availability(*, unavailable, renamed="int8") + public var char: Int8? { + get { + return self.number?.charValue + } + } + + @availability(*, unavailable, renamed="int8Value") + public var charValue: Int8 { + get { + return self.numberValue.charValue + } + } + + @availability(*, unavailable, renamed="uInt8") + public var unsignedChar: UInt8? { + get{ + return self.number?.unsignedCharValue + } + } + + @availability(*, unavailable, renamed="uInt8Value") + public var unsignedCharValue: UInt8 { + get{ + return self.numberValue.unsignedCharValue + } + } + + @availability(*, unavailable, renamed="int16") + public var short: Int16? { + get{ + return self.number?.shortValue + } + } + + @availability(*, unavailable, renamed="int16Value") + public var shortValue: Int16 { + get{ + return self.numberValue.shortValue + } + } + + @availability(*, unavailable, renamed="uInt16") + public var unsignedShort: UInt16? { + get{ + return self.number?.unsignedShortValue + } + } + + @availability(*, unavailable, renamed="uInt16Value") + public var unsignedShortValue: UInt16 { + get{ + return self.numberValue.unsignedShortValue + } + } + + @availability(*, unavailable, renamed="int") + public var long: Int? { + get{ + return self.number?.longValue + } + } + + @availability(*, unavailable, renamed="intValue") + public var longValue: Int { + get{ + return self.numberValue.longValue + } + } + + @availability(*, unavailable, renamed="uInt") + public var unsignedLong: UInt? { + get{ + return self.number?.unsignedLongValue + } + } + + @availability(*, unavailable, renamed="uIntValue") + public var unsignedLongValue: UInt { + get{ + return self.numberValue.unsignedLongValue + } + } + + @availability(*, unavailable, renamed="int64") + public var longLong: Int64? { + get{ + return self.number?.longLongValue + } + } + + @availability(*, unavailable, renamed="int64Value") + public var longLongValue: Int64 { + get{ + return self.numberValue.longLongValue + } + } + + @availability(*, unavailable, renamed="uInt64") + public var unsignedLongLong: UInt64? { + get{ + return self.number?.unsignedLongLongValue + } + } + + @availability(*, unavailable, renamed="uInt64Value") + public var unsignedLongLongValue: UInt64 { + get{ + return self.numberValue.unsignedLongLongValue + } + } + + @availability(*, unavailable, renamed="int") + public var integer: Int? { + get { + return self.number?.integerValue + } + } + + @availability(*, unavailable, renamed="intValue") + public var integerValue: Int { + get { + return self.numberValue.integerValue + } + } + + @availability(*, unavailable, renamed="uInt") + public var unsignedInteger: Int? { + get { + return self.number?.unsignedIntegerValue + } + } + + @availability(*, unavailable, renamed="uIntValue") + public var unsignedIntegerValue: Int { + get { + return self.numberValue.unsignedIntegerValue + } + } +} diff --git a/WWDC/Theme.swift b/WWDC/Theme.swift new file mode 100644 index 00000000..9a47e10a --- /dev/null +++ b/WWDC/Theme.swift @@ -0,0 +1,23 @@ +// +// Theme.swift +// WWDC +// +// Created by Guilherme Rambo on 18/04/15. +// Copyright (c) 2015 Guilherme Rambo. All rights reserved. +// + +import Cocoa + +private let _SharedThemeInstance = Theme() + +class Theme: NSObject { + + class var WWDCTheme: Theme { + return _SharedThemeInstance + } + + let separatorColor = NSColor.grayColor().colorWithAlphaComponent(0.3) + let backgroundColor = NSColor.whiteColor() + let fillColor = NSColor(calibratedRed: 0, green: 0.49, blue: 1, alpha: 1) + +} diff --git a/WWDC/VideoDetailsViewController.swift b/WWDC/VideoDetailsViewController.swift new file mode 100644 index 00000000..f6bd6e34 --- /dev/null +++ b/WWDC/VideoDetailsViewController.swift @@ -0,0 +1,74 @@ +// +// VideoDetailsViewController.swift +// WWDC +// +// Created by Guilherme Rambo on 18/04/15. +// Copyright (c) 2015 Guilherme Rambo. All rights reserved. +// + +import Cocoa + +class VideoDetailsViewController: NSViewController { + + var players: [VideoWindowController] = [] + + var session: Session? { + didSet { + if let session = session { + titleLabel.stringValue = session.title + subtitleLabel.stringValue = "\(session.track) | Session \(session.id)" + descriptionLabel.stringValue = session.description + descriptionLabel.hidden = false + watchVideoButton.hidden = false + if session.slides != nil { + viewSlidesButton.hidden = false + } + if session.progress > 0 && session.progress < 1 { + markAsUnwatchedButton.hidden = false + } + } else { + titleLabel.stringValue = "No session selected" + subtitleLabel.stringValue = "Select a session to see It here" + descriptionLabel.hidden = true + watchVideoButton.hidden = true + viewSlidesButton.hidden = true + markAsUnwatchedButton.hidden = true + } + } + } + + @IBOutlet weak var titleLabel: NSTextField! + @IBOutlet weak var subtitleLabel: NSTextField! + @IBOutlet weak var descriptionLabel: NSTextField! + @IBOutlet weak var watchVideoButton: NSButton! + @IBOutlet weak var viewSlidesButton: NSButton! + @IBOutlet weak var markAsUnwatchedButton: NSButton! + + @IBAction func watchVideo(sender: NSButton) { + let playerVC = VideoWindowController(session: session!) + players.append(playerVC) + playerVC.showWindow(sender) + + let nc = NSNotificationCenter.defaultCenter() + nc.addObserverForName(NSWindowWillCloseNotification, object: playerVC.window!, queue: nil) { _ in + self.players.removeObject(playerVC) + } + } + + @IBAction func viewSlides(sender: NSButton) { + if let url = NSURL(string: session!.slides!) { + NSWorkspace.sharedWorkspace().openURL(url) + } + } + + @IBAction func markAsUnwatched(sender: NSButton) { + session?.progress = 0 + sender.hidden = true + } + + override func viewDidLoad() { + super.viewDidLoad() + // Do view setup here. + } + +} diff --git a/WWDC/VideoTableCellView.swift b/WWDC/VideoTableCellView.swift new file mode 100644 index 00000000..3611b3ff --- /dev/null +++ b/WWDC/VideoTableCellView.swift @@ -0,0 +1,17 @@ +// +// VideoTableCellView.swift +// WWDC +// +// Created by Guilherme Rambo on 18/04/15. +// Copyright (c) 2015 Guilherme Rambo. All rights reserved. +// + +import Cocoa + +class VideoTableCellView: NSTableCellView { + + @IBOutlet weak var titleField: NSTextField! + @IBOutlet weak var detailsField: NSTextField! + @IBOutlet weak var progressView: SessionProgressView! + +} diff --git a/WWDC/VideoWindowController.swift b/WWDC/VideoWindowController.swift new file mode 100644 index 00000000..a0414c94 --- /dev/null +++ b/WWDC/VideoWindowController.swift @@ -0,0 +1,68 @@ +// +// VideoWindowController.swift +// WWDC +// +// Created by Guilherme Rambo on 19/04/15. +// Copyright (c) 2015 Guilherme Rambo. All rights reserved. +// + +import Cocoa +import AVFoundation +import AVKit + +class VideoWindowController: NSWindowController { + + var session: Session? + + convenience init(session: Session) { + self.init(windowNibName: "VideoWindowController") + self.session = session + } + + @IBOutlet weak var playerView: AVPlayerView! + var player: AVPlayer? + + override func windowDidLoad() { + super.windowDidLoad() + + window?.backgroundColor = NSColor.blackColor() + + if let session = session { + if let url = NSURL(string: session.url) { + player = AVPlayer(URL: url) + playerView.player = player + setupTimeObserver() + if session.currentPosition > 0 { + player?.seekToTime(CMTimeMakeWithSeconds(session.currentPosition, 1)) + } + player?.play() + } + + window?.title = "WWDC \(session.year) | \(session.title)" + } + + NSNotificationCenter.defaultCenter().addObserverForName(NSWindowWillCloseNotification, object: self.window, queue: nil) { _ in + player?.pause() + } + } + + var timeObserver: AnyObject? + + func setupTimeObserver() { + timeObserver = player?.addPeriodicTimeObserverForInterval(CMTimeMakeWithSeconds(5, 1), queue: dispatch_get_main_queue()) { [unowned self] currentTime in + let progress = Double(CMTimeGetSeconds(currentTime)/CMTimeGetSeconds(self.player!.currentItem.duration)) + println("p: \(progress)") + self.session!.progress = progress + self.session!.currentPosition = CMTimeGetSeconds(currentTime) + } + } + + deinit { + NSNotificationCenter.defaultCenter().removeObserver(self) + + if let observer: AnyObject = timeObserver { + player?.removeTimeObserver(observer) + } + } + +} diff --git a/WWDC/VideoWindowController.xib b/WWDC/VideoWindowController.xib new file mode 100644 index 00000000..f9ed7c31 --- /dev/null +++ b/WWDC/VideoWindowController.xib @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WWDC/VideosHeaderViewController.swift b/WWDC/VideosHeaderViewController.swift new file mode 100644 index 00000000..064c0067 --- /dev/null +++ b/WWDC/VideosHeaderViewController.swift @@ -0,0 +1,43 @@ +// +// VideosHeaderViewController.swift +// WWDC +// +// Created by Guilherme Rambo on 18/04/15. +// Copyright (c) 2015 Guilherme Rambo. All rights reserved. +// + +import Cocoa + +class VideosHeaderViewController: NSViewController { + + @IBOutlet weak var searchBarBottomConstraint: NSLayoutConstraint! + + var performSearch: ((term: String) -> Void)? + + class func loadDefaultController() -> VideosHeaderViewController? { + return VideosHeaderViewController(nibName: "VideosHeaderViewController", bundle: nil) + } + + override func viewDidLoad() { + super.viewDidLoad() + } + + override func viewDidAppear() { + super.viewDidAppear() + + let nc = NSNotificationCenter.defaultCenter() + + nc.addObserverForName(NSWindowWillEnterFullScreenNotification, object: view.window!, queue: nil) { object in + self.searchBarBottomConstraint.constant = 19 + } + nc.addObserverForName(NSWindowWillExitFullScreenNotification, object: view.window!, queue: nil) { object in + self.searchBarBottomConstraint.constant = 12 + } + } + + @IBAction func search(sender: NSSearchField) { + if let callback = performSearch { + callback(term: sender.stringValue) + } + } +} diff --git a/WWDC/VideosHeaderViewController.xib b/WWDC/VideosHeaderViewController.xib new file mode 100644 index 00000000..5fcb4545 --- /dev/null +++ b/WWDC/VideosHeaderViewController.xib @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WWDC/VideosViewController.swift b/WWDC/VideosViewController.swift new file mode 100644 index 00000000..157a11e6 --- /dev/null +++ b/WWDC/VideosViewController.swift @@ -0,0 +1,170 @@ +// +// ViewController.swift +// WWDC +// +// Created by Guilherme Rambo on 18/04/15. +// Copyright (c) 2015 Guilherme Rambo. All rights reserved. +// + +import Cocoa + +class VideosViewController: NSViewController, NSTableViewDelegate, NSTableViewDataSource { + + @IBOutlet weak var scrollView: NSScrollView! + @IBOutlet weak var tableView: NSTableView! + + var indexOfLastSelectedRow = -1 + + lazy var headerController: VideosHeaderViewController! = VideosHeaderViewController.loadDefaultController() + + override func viewDidLoad() { + super.viewDidLoad() + + setupScrollView() + + tableView.gridColor = Theme.WWDCTheme.separatorColor + + loadSessions() + + NSNotificationCenter.defaultCenter().addObserverForName(SessionProgressDidChangeNotification, object: nil, queue: nil) { _ in + self.reloadTablePreservingSelection() + } + } + + func setupScrollView() { + let insetHeight = NSHeight(headerController.view.frame) + scrollView.automaticallyAdjustsContentInsets = false + scrollView.contentInsets = NSEdgeInsets(top: insetHeight, left: 0, bottom: 0, right: 0) + + setupViewHeader(insetHeight) + } + + func setupViewHeader(insetHeight: CGFloat) { + if let superview = scrollView.superview { + superview.addSubview(headerController.view) + headerController.view.frame = CGRectMake(0, NSHeight(superview.frame)-insetHeight, NSWidth(superview.frame), insetHeight) + headerController.view.autoresizingMask = NSAutoresizingMaskOptions.ViewWidthSizable | NSAutoresizingMaskOptions.ViewMinYMargin + headerController.performSearch = search + } + } + + var sessions: [Session]! { + didSet { + reloadTablePreservingSelection() + } + } + + // MARK: Session loading + + func loadSessions() { + DataStore.SharedStore.fetchSessions() { success, sessions in + dispatch_async(dispatch_get_main_queue()) { + self.sessions = sessions + } + } + } + + // MARK: TableView + + func reloadTablePreservingSelection() { + tableView.reloadData() + + if indexOfLastSelectedRow > -1 { + tableView.selectRowIndexes(NSIndexSet(index: indexOfLastSelectedRow), byExtendingSelection: false) + } + } + + func numberOfRowsInTableView(tableView: NSTableView) -> Int { + if let count = displayedSessions?.count { + return count + } else { + return 0 + } + } + + func tableView(tableView: NSTableView, viewForTableColumn tableColumn: NSTableColumn?, row: Int) -> NSView? { + let cell = tableView.makeViewWithIdentifier("video", owner: tableView) as! VideoTableCellView + + let session = displayedSessions[row] + cell.titleField.stringValue = session.title + cell.detailsField.stringValue = "\(session.year) - Session \(session.id)" + cell.progressView.progress = DataStore.SharedStore.fetchSessionProgress(session) + + return cell + } + + func tableView(tableView: NSTableView, heightOfRow row: Int) -> CGFloat { + return 40.0 + } + + // MARK: Navigation + + var detailsViewController: VideoDetailsViewController? { + get { + if let splitViewController = parentViewController as? NSSplitViewController { + return splitViewController.childViewControllers[1] as? VideoDetailsViewController + } else { + return nil + } + } + } + + func tableViewSelectionDidChange(notification: NSNotification) { + if tableView.selectedRow >= 0 { + indexOfLastSelectedRow = tableView.selectedRow + + let session = displayedSessions[tableView.selectedRow] + if let detailsVC = detailsViewController { + detailsVC.session = session + } + } else { + if let detailsVC = detailsViewController { + detailsVC.session = nil + } + } + } + + // MARK: Search + + var currentSearchTerm: String? { + didSet { + reloadTablePreservingSelection() + } + } + + func search(term: String) { + currentSearchTerm = term + } + + var displayedSessions: [Session]! { + get { + if let term = currentSearchTerm { + var term = term + if term != "" { + indexOfLastSelectedRow = -1 + return sessions.filter { session in + if term.lowercaseString == "osx" || term.lowercaseString == "os x" { + term = "OS X" + } else if term.lowercaseString == "ios" { + term = "iOS" + } + if contains(session.focus, term) { + return true + } + if let range = session.title.rangeOfString(term, options: .CaseInsensitiveSearch | .DiacriticInsensitiveSearch, range: nil, locale: nil) { + return true + } else { + return false + } + } + } else { + return sessions + } + } else { + return sessions + } + } + } + +} + diff --git a/screenshot.png b/screenshot.png new file mode 100644 index 00000000..badd2e5e Binary files /dev/null and b/screenshot.png differ