diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..18bd751 Binary files /dev/null and b/.DS_Store differ diff --git a/Baker/.DS_Store b/Baker/.DS_Store new file mode 100644 index 0000000..9b15c77 Binary files /dev/null and b/Baker/.DS_Store differ diff --git a/Baker/BKRAppDelegate.h b/Baker/BKRAppDelegate.h new file mode 100644 index 0000000..e489817 --- /dev/null +++ b/Baker/BKRAppDelegate.h @@ -0,0 +1,44 @@ +// +// AppDelegate.h +// Baker +// +// ========================================================================================== +// +// Copyright (c) 2010-2013, Davide Casali, Marco Colombo, Alessandro Morandi +// Copyright (c) 2014, Andrew Krowczyk, Cédric Mériau, Pieter Claerhout +// All rights reserved. +// +// 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. +// Neither the name of the Baker Framework nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written +// permission. +// 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. +// + +#import + +#import "BKRInterceptorWindow.h" +#import "BKRShelfViewController.h" + +@interface BKRAppDelegate : UIResponder + +@property (nonatomic, strong) BKRInterceptorWindow *window; +@property (nonatomic, strong) UIViewController *rootViewController; +@property (nonatomic, strong) UINavigationController *rootNavigationController; + +@end diff --git a/Baker/BKRAppDelegate.m b/Baker/BKRAppDelegate.m new file mode 100644 index 0000000..d091af0 --- /dev/null +++ b/Baker/BKRAppDelegate.m @@ -0,0 +1,271 @@ +// +// AppDelegate.m +// Baker +// +// ========================================================================================== +// +// Copyright (c) 2010-2013, Davide Casali, Marco Colombo, Alessandro Morandi +// Copyright (c) 2014, Andrew Krowczyk, Cédric Mériau, Pieter Claerhout +// All rights reserved. +// +// 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. +// Neither the name of the Baker Framework nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written +// permission. +// 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. +// + +#import "BKRAppDelegate.h" +#import "BKRCustomNavigationController.h" +#import "BKRCustomNavigationBar.h" +#import "BKRIssuesManager.h" +#import "BKRBakerAPI.h" +#import "UIColor+BakerExtensions.h" +#import "BKRUtils.h" + +#import "BKRSettings.h" +#import "BKRBookViewController.h" +#import "BKRAnalyticsEvents.h" + +#pragma mark - Initialization + +@implementation BKRAppDelegate + ++ (void)initialize { + // Set user agent (the only problem is that we can't modify the User-Agent later in the program) + // We use a more browser-like User-Agent in order to allow browser detection scripts to run (like Tumult Hype). + NSDictionary *userAgent = @{@"UserAgent": @"Mozilla/5.0 (compatible; BakerFramework) AppleWebKit/533.00+ (KHTML, like Gecko) Mobile"}; + [[NSUserDefaults standardUserDefaults] registerDefaults:userAgent]; +} + +- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions { + + if ([BKRSettings sharedSettings].isNewsstand) { + [self configureNewsstandApp:application options:launchOptions]; + } else { + [self configureStandAloneApp:application options:launchOptions]; + } + + self.rootNavigationController = [[BKRCustomNavigationController alloc] initWithRootViewController:self.rootViewController]; + + [self configureNavigationBar]; + [self configureAnalytics]; + + self.window = [[BKRInterceptorWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + self.window.backgroundColor = [UIColor whiteColor]; + self.window.rootViewController = self.rootNavigationController; + [self.window makeKeyAndVisible]; + + return YES; +} + +- (void)configureNewsstandApp:(UIApplication*)application options:(NSDictionary*)launchOptions { + + NSLog(@"====== Baker Newsstand Mode enabled ======"); + [BKRBakerAPI generateUUIDOnce]; + + // Let the device know we want to handle Newsstand push notifications + if ([application respondsToSelector:@selector(registerUserNotificationSettings:)]) { + UIUserNotificationSettings *notificationSettings = [UIUserNotificationSettings settingsForTypes:(UIRemoteNotificationTypeNewsstandContentAvailability|UIRemoteNotificationTypeBadge|UIRemoteNotificationTypeSound|UIRemoteNotificationTypeAlert) categories:nil]; + [application registerUserNotificationSettings:notificationSettings]; + } else { + [application registerForRemoteNotificationTypes:(UIRemoteNotificationTypeNewsstandContentAvailability|UIRemoteNotificationTypeBadge|UIRemoteNotificationTypeSound|UIRemoteNotificationTypeAlert)]; + } + +#ifdef DEBUG + // For debug only... so that you can download multiple issues per day during development + [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"NKDontThrottleNewsstandContentNotifications"]; + [[NSUserDefaults standardUserDefaults] synchronize]; +#endif + + // Check if the app is runnig in response to a notification + NSDictionary *payload = launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey]; + if (payload) { + NSDictionary *aps = payload[@"aps"]; + if (aps && aps[@"content-available"]) { + + __block UIBackgroundTaskIdentifier backgroundTask = [application beginBackgroundTaskWithExpirationHandler:^{ + [application endBackgroundTask:backgroundTask]; + backgroundTask = UIBackgroundTaskInvalid; + }]; + + // Credit where credit is due. This semaphore solution found here: + // http://stackoverflow.com/a/4326754/2998 + dispatch_semaphore_t sema = NULL; + sema = dispatch_semaphore_create(0); + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{ + [self applicationWillHandleNewsstandNotificationOfContent:payload[@"content-name"]]; + [application endBackgroundTask:backgroundTask]; + backgroundTask = UIBackgroundTaskInvalid; + dispatch_semaphore_signal(sema); + }); + + dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); + } + } + + self.rootViewController = [[BKRShelfViewController alloc] init]; + +} + +- (void)configureStandAloneApp:(UIApplication*)application options:(NSDictionary*)launchOptions { + + NSLog(@"====== Baker Standalone Mode enabled ======"); + NSArray *books = [BKRIssuesManager localBooksList]; + if (books.count == 1) { + BKRBook *book = [books[0] bakerBook]; + self.rootViewController = [[BKRBookViewController alloc] initWithBook:book]; + } else { + self.rootViewController = [[BKRShelfViewController alloc] initWithBooks:books]; + } + +} + +- (void)configureNavigationBar { + BKRCustomNavigationBar *navigationBar = (BKRCustomNavigationBar*)self.rootNavigationController.navigationBar; + navigationBar.tintColor = [UIColor bkrColorWithHexString:[BKRSettings sharedSettings].issuesActionBackgroundColor]; + navigationBar.barTintColor = [UIColor bkrColorWithHexString:@"ffffff"]; + navigationBar.titleTextAttributes = @{NSForegroundColorAttributeName: [UIColor bkrColorWithHexString:@"000000"]}; + [navigationBar setBackgroundImage:[UIImage imageNamed:@"navigation-bar-bg"] forBarMetrics:UIBarMetricsDefault]; +} + +- (void)configureAnalytics { + [BKRAnalyticsEvents sharedInstance]; // Initialization + [[NSNotificationCenter defaultCenter] postNotificationName:@"BakerApplicationStart" object:self]; // -> Baker Analytics Event +} + +#pragma mark - Push Notifications + +- (void)application:(UIApplication*)application didRegisterUserNotificationSettings:(UIUserNotificationSettings*)notificationSettings { + [application registerForRemoteNotifications]; +} + +- (void)application:(UIApplication*)application didFailToRegisterForRemoteNotificationsWithError:(NSError*)error { + NSLog(@"[AppDelegate] Push Notification - Device Token, review: %@", error); +} + +- (void)application:(UIApplication*)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData*)deviceToken { + + if (![BKRSettings sharedSettings].isNewsstand) { + return; + } + + NSString *apnsToken = [[deviceToken description] stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"<>"]]; + apnsToken = [apnsToken stringByReplacingOccurrencesOfString:@" " withString:@""]; + + NSLog(@"[AppDelegate] My token (as NSData) is: %@", deviceToken); + NSLog(@"[AppDelegate] My token (as NSString) is: %@", apnsToken); + + [[NSUserDefaults standardUserDefaults] setObject:apnsToken forKey:@"apns_token"]; + [[NSUserDefaults standardUserDefaults] synchronize]; + + BKRBakerAPI *api = [BKRBakerAPI sharedInstance]; + [api postAPNSToken:apnsToken]; + +} + +- (void)application:(UIApplication*)application didReceiveRemoteNotification:(NSDictionary*)userInfo { + + if (![BKRSettings sharedSettings].isNewsstand) { + return; + } + + NSDictionary *aps = userInfo[@"aps"]; + if (aps && aps[@"content-available"]) { + [self applicationWillHandleNewsstandNotificationOfContent:userInfo[@"content-name"]]; + } + +} + +/* +- (void)application:(UIApplication*)application didReceiveRemoteNotification:(NSDictionary*)userInfo fetchCompletionHandler:(void(^)(UIBackgroundFetchResult result))handler { + + if (![BKRSettings sharedSettings].isNewsstand) { + return; + } + + NSDictionary *aps = userInfo[@"aps"]; + if (aps && aps[@"content-available"]) { + [self applicationWillHandleNewsstandNotificationOfContent:userInfo[@"content-name"]]; + } + +} +*/ + +- (void)applicationWillHandleNewsstandNotificationOfContent:(NSString*)contentName { + + if (![BKRSettings sharedSettings].isNewsstand) { + return; + } + + BKRIssuesManager *issuesManager = [BKRIssuesManager sharedInstance]; + BKRPurchasesManager *purchasesManager = [BKRPurchasesManager sharedInstance]; + __block BKRIssue *targetIssue = nil; + + [issuesManager refresh:^(BOOL status) { + if (contentName) { + for (BKRIssue *issue in issuesManager.issues) { + if ([issue.ID isEqualToString:contentName]) { + targetIssue = issue; + break; + } + } + } else { + targetIssue = (issuesManager.issues)[0]; + } + + [purchasesManager retrievePurchasesFor:issuesManager.productIDs withCallback:^(NSDictionary *_purchases) { + + NSString *targetStatus = [targetIssue getStatus]; + NSLog(@"[AppDelegate] Push Notification - Target status: %@", targetStatus); + + if ([targetStatus isEqualToString:@"remote"] || [targetStatus isEqualToString:@"purchased"]) { + [targetIssue download]; + } else if ([targetStatus isEqualToString:@"purchasable"] || [targetStatus isEqualToString:@"unpriced"]) { + NSLog(@"[AppDelegate] Push Notification - You are not entitled to download issue '%@', issue not purchased yet", targetIssue.ID); + } else if (![targetStatus isEqualToString:@"remote"]) { + NSLog(@"[AppDelegate] Push Notification - Issue '%@' in download or already downloaded", targetIssue.ID); + } + }]; + }]; + +} + +#pragma mark - Application Lifecycle + +- (void)applicationWillResignActive:(UIApplication*)application { + [[NSNotificationCenter defaultCenter] postNotificationName:@"applicationWillResignActiveNotification" object:nil]; +} + +- (void)applicationDidEnterBackground:(UIApplication*)application { + [self resetApplicationBadge]; +} + +- (void)applicationDidBecomeActive:(UIApplication*)application { + [self resetApplicationBadge]; +} + +- (void)resetApplicationBadge { + if (![BKRSettings sharedSettings].isNewsstand) { + return; + } + [[UIApplication sharedApplication] setApplicationIconBadgeNumber:0]; +} + +@end diff --git a/Baker/BakerAssets.xcassets/.DS_Store b/Baker/BakerAssets.xcassets/.DS_Store new file mode 100644 index 0000000..cabe325 Binary files /dev/null and b/Baker/BakerAssets.xcassets/.DS_Store differ diff --git a/Baker/BakerAssets.xcassets/AppIcon.appiconset/Contents.json b/Baker/BakerAssets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..777db21 --- /dev/null +++ b/Baker/BakerAssets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,101 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "size" : "57x57", + "idiom" : "iphone", + "filename" : "iPhone App iOS6.png", + "scale" : "1x" + }, + { + "size" : "57x57", + "idiom" : "iphone", + "filename" : "iPhone App iOS6@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "iPhone App iOS7@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "iPhone-App-iOS7@3x.png", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "50x50", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "50x50", + "scale" : "2x" + }, + { + "size" : "72x72", + "idiom" : "ipad", + "filename" : "iPad App iOS6.png", + "scale" : "1x" + }, + { + "size" : "72x72", + "idiom" : "ipad", + "filename" : "iPad App iOS6@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "iPad App iOS7.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "iPad App iOS7@2x.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Baker/BakerAssets.xcassets/AppIcon.appiconset/iPad App iOS6.png b/Baker/BakerAssets.xcassets/AppIcon.appiconset/iPad App iOS6.png new file mode 100644 index 0000000..decb798 Binary files /dev/null and b/Baker/BakerAssets.xcassets/AppIcon.appiconset/iPad App iOS6.png differ diff --git a/Baker/BakerAssets.xcassets/AppIcon.appiconset/iPad App iOS6@2x.png b/Baker/BakerAssets.xcassets/AppIcon.appiconset/iPad App iOS6@2x.png new file mode 100644 index 0000000..df11f0c Binary files /dev/null and b/Baker/BakerAssets.xcassets/AppIcon.appiconset/iPad App iOS6@2x.png differ diff --git a/Baker/BakerAssets.xcassets/AppIcon.appiconset/iPad App iOS7.png b/Baker/BakerAssets.xcassets/AppIcon.appiconset/iPad App iOS7.png new file mode 100644 index 0000000..3095efd Binary files /dev/null and b/Baker/BakerAssets.xcassets/AppIcon.appiconset/iPad App iOS7.png differ diff --git a/Baker/BakerAssets.xcassets/AppIcon.appiconset/iPad App iOS7@2x.png b/Baker/BakerAssets.xcassets/AppIcon.appiconset/iPad App iOS7@2x.png new file mode 100644 index 0000000..4660c6a Binary files /dev/null and b/Baker/BakerAssets.xcassets/AppIcon.appiconset/iPad App iOS7@2x.png differ diff --git a/Baker/BakerAssets.xcassets/AppIcon.appiconset/iPhone App iOS6.png b/Baker/BakerAssets.xcassets/AppIcon.appiconset/iPhone App iOS6.png new file mode 100644 index 0000000..3219dd0 Binary files /dev/null and b/Baker/BakerAssets.xcassets/AppIcon.appiconset/iPhone App iOS6.png differ diff --git a/Baker/BakerAssets.xcassets/AppIcon.appiconset/iPhone App iOS6@2x.png b/Baker/BakerAssets.xcassets/AppIcon.appiconset/iPhone App iOS6@2x.png new file mode 100644 index 0000000..f732eec Binary files /dev/null and b/Baker/BakerAssets.xcassets/AppIcon.appiconset/iPhone App iOS6@2x.png differ diff --git a/Baker/BakerAssets.xcassets/AppIcon.appiconset/iPhone App iOS7@2x.png b/Baker/BakerAssets.xcassets/AppIcon.appiconset/iPhone App iOS7@2x.png new file mode 100644 index 0000000..01d5711 Binary files /dev/null and b/Baker/BakerAssets.xcassets/AppIcon.appiconset/iPhone App iOS7@2x.png differ diff --git a/Baker/BakerAssets.xcassets/AppIcon.appiconset/iPhone-App-iOS7@3x.png b/Baker/BakerAssets.xcassets/AppIcon.appiconset/iPhone-App-iOS7@3x.png new file mode 100644 index 0000000..3eee7ad Binary files /dev/null and b/Baker/BakerAssets.xcassets/AppIcon.appiconset/iPhone-App-iOS7@3x.png differ diff --git a/Baker/BakerAssets.xcassets/LaunchImage.launchimage/Contents.json b/Baker/BakerAssets.xcassets/LaunchImage.launchimage/Contents.json new file mode 100644 index 0000000..60ae271 --- /dev/null +++ b/Baker/BakerAssets.xcassets/LaunchImage.launchimage/Contents.json @@ -0,0 +1,112 @@ +{ + "images" : [ + { + "extent" : "full-screen", + "idiom" : "iphone", + "subtype" : "736h", + "filename" : "iPhone-Portrait-iOS7-R55.png", + "minimum-system-version" : "8.0", + "orientation" : "portrait", + "scale" : "3x" + }, + { + "extent" : "full-screen", + "idiom" : "iphone", + "subtype" : "736h", + "filename" : "iPhone-Landscape-iOS7-R55.png", + "minimum-system-version" : "8.0", + "orientation" : "landscape", + "scale" : "3x" + }, + { + "extent" : "full-screen", + "idiom" : "iphone", + "subtype" : "667h", + "filename" : "iPhone-Portrait-iOS7-R47.png", + "minimum-system-version" : "8.0", + "orientation" : "portrait", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "iphone", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "filename" : "iPhone Portrait iOS7@2x.png", + "scale" : "2x" + }, + { + "extent" : "full-screen", + "idiom" : "iphone", + "subtype" : "retina4", + "filename" : "iPhone Portrait iOS7 R4.png", + "minimum-system-version" : "7.0", + "orientation" : "portrait", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "filename" : "iPad Portrait iOS7.png", + "scale" : "1x" + }, + { + "orientation" : "landscape", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "filename" : "iPad Landscape iOS7.png", + "scale" : "1x" + }, + { + "orientation" : "portrait", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "filename" : "iPad Portrait iOS7@2x.png", + "scale" : "2x" + }, + { + "orientation" : "landscape", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "filename" : "iPad Landscape iOS7@2x.png", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "ipad", + "extent" : "to-status-bar", + "filename" : "iPad Portrait iOS6 no status bar.png", + "scale" : "1x" + }, + { + "orientation" : "landscape", + "idiom" : "ipad", + "extent" : "to-status-bar", + "filename" : "iPad Landscape iOS6 no status bar.png", + "scale" : "1x" + }, + { + "orientation" : "portrait", + "idiom" : "ipad", + "extent" : "to-status-bar", + "filename" : "iPad Portrait iOS6 no status bar@2x.png", + "scale" : "2x" + }, + { + "orientation" : "landscape", + "idiom" : "ipad", + "extent" : "to-status-bar", + "filename" : "iPad Landscape iOS6 no status bar@2x-1.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Baker/BakerAssets.xcassets/LaunchImage.launchimage/iPad Landscape iOS6 no status bar.png b/Baker/BakerAssets.xcassets/LaunchImage.launchimage/iPad Landscape iOS6 no status bar.png new file mode 100644 index 0000000..21a055e Binary files /dev/null and b/Baker/BakerAssets.xcassets/LaunchImage.launchimage/iPad Landscape iOS6 no status bar.png differ diff --git a/Baker/BakerAssets.xcassets/LaunchImage.launchimage/iPad Landscape iOS6 no status bar@2x-1.png b/Baker/BakerAssets.xcassets/LaunchImage.launchimage/iPad Landscape iOS6 no status bar@2x-1.png new file mode 100644 index 0000000..3220d06 Binary files /dev/null and b/Baker/BakerAssets.xcassets/LaunchImage.launchimage/iPad Landscape iOS6 no status bar@2x-1.png differ diff --git a/Baker/BakerAssets.xcassets/LaunchImage.launchimage/iPad Landscape iOS7.png b/Baker/BakerAssets.xcassets/LaunchImage.launchimage/iPad Landscape iOS7.png new file mode 100644 index 0000000..5905250 Binary files /dev/null and b/Baker/BakerAssets.xcassets/LaunchImage.launchimage/iPad Landscape iOS7.png differ diff --git a/Baker/BakerAssets.xcassets/LaunchImage.launchimage/iPad Landscape iOS7@2x.png b/Baker/BakerAssets.xcassets/LaunchImage.launchimage/iPad Landscape iOS7@2x.png new file mode 100644 index 0000000..b1583ca Binary files /dev/null and b/Baker/BakerAssets.xcassets/LaunchImage.launchimage/iPad Landscape iOS7@2x.png differ diff --git a/Baker/BakerAssets.xcassets/LaunchImage.launchimage/iPad Portrait iOS6 no status bar.png b/Baker/BakerAssets.xcassets/LaunchImage.launchimage/iPad Portrait iOS6 no status bar.png new file mode 100644 index 0000000..c9c5ba2 Binary files /dev/null and b/Baker/BakerAssets.xcassets/LaunchImage.launchimage/iPad Portrait iOS6 no status bar.png differ diff --git a/Baker/BakerAssets.xcassets/LaunchImage.launchimage/iPad Portrait iOS6 no status bar@2x.png b/Baker/BakerAssets.xcassets/LaunchImage.launchimage/iPad Portrait iOS6 no status bar@2x.png new file mode 100644 index 0000000..920e961 Binary files /dev/null and b/Baker/BakerAssets.xcassets/LaunchImage.launchimage/iPad Portrait iOS6 no status bar@2x.png differ diff --git a/Baker/BakerAssets.xcassets/LaunchImage.launchimage/iPad Portrait iOS7.png b/Baker/BakerAssets.xcassets/LaunchImage.launchimage/iPad Portrait iOS7.png new file mode 100644 index 0000000..e79f1e8 Binary files /dev/null and b/Baker/BakerAssets.xcassets/LaunchImage.launchimage/iPad Portrait iOS7.png differ diff --git a/Baker/BakerAssets.xcassets/LaunchImage.launchimage/iPad Portrait iOS7@2x.png b/Baker/BakerAssets.xcassets/LaunchImage.launchimage/iPad Portrait iOS7@2x.png new file mode 100644 index 0000000..9c30c6c Binary files /dev/null and b/Baker/BakerAssets.xcassets/LaunchImage.launchimage/iPad Portrait iOS7@2x.png differ diff --git a/Baker/BakerAssets.xcassets/LaunchImage.launchimage/iPhone Portrait iOS7 R4.png b/Baker/BakerAssets.xcassets/LaunchImage.launchimage/iPhone Portrait iOS7 R4.png new file mode 100644 index 0000000..78a8a07 Binary files /dev/null and b/Baker/BakerAssets.xcassets/LaunchImage.launchimage/iPhone Portrait iOS7 R4.png differ diff --git a/Baker/BakerAssets.xcassets/LaunchImage.launchimage/iPhone Portrait iOS7@2x.png b/Baker/BakerAssets.xcassets/LaunchImage.launchimage/iPhone Portrait iOS7@2x.png new file mode 100644 index 0000000..d9569bd Binary files /dev/null and b/Baker/BakerAssets.xcassets/LaunchImage.launchimage/iPhone Portrait iOS7@2x.png differ diff --git a/Baker/BakerAssets.xcassets/LaunchImage.launchimage/iPhone-Landscape-iOS7-R55.png b/Baker/BakerAssets.xcassets/LaunchImage.launchimage/iPhone-Landscape-iOS7-R55.png new file mode 100644 index 0000000..63f221d Binary files /dev/null and b/Baker/BakerAssets.xcassets/LaunchImage.launchimage/iPhone-Landscape-iOS7-R55.png differ diff --git a/Baker/BakerAssets.xcassets/LaunchImage.launchimage/iPhone-Portrait-iOS7-R47.png b/Baker/BakerAssets.xcassets/LaunchImage.launchimage/iPhone-Portrait-iOS7-R47.png new file mode 100644 index 0000000..102843e Binary files /dev/null and b/Baker/BakerAssets.xcassets/LaunchImage.launchimage/iPhone-Portrait-iOS7-R47.png differ diff --git a/Baker/BakerAssets.xcassets/LaunchImage.launchimage/iPhone-Portrait-iOS7-R55.png b/Baker/BakerAssets.xcassets/LaunchImage.launchimage/iPhone-Portrait-iOS7-R55.png new file mode 100644 index 0000000..4899059 Binary files /dev/null and b/Baker/BakerAssets.xcassets/LaunchImage.launchimage/iPhone-Portrait-iOS7-R55.png differ diff --git a/Baker/BakerAssets.xcassets/back.imageset/Contents.json b/Baker/BakerAssets.xcassets/back.imageset/Contents.json new file mode 100644 index 0000000..0eaf7cb --- /dev/null +++ b/Baker/BakerAssets.xcassets/back.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "back.png" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "back@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Baker/BakerAssets.xcassets/back.imageset/back.png b/Baker/BakerAssets.xcassets/back.imageset/back.png new file mode 100644 index 0000000..6a2dd4e Binary files /dev/null and b/Baker/BakerAssets.xcassets/back.imageset/back.png differ diff --git a/Baker/BakerAssets.xcassets/back.imageset/back@2x.png b/Baker/BakerAssets.xcassets/back.imageset/back@2x.png new file mode 100644 index 0000000..4b87578 Binary files /dev/null and b/Baker/BakerAssets.xcassets/back.imageset/back@2x.png differ diff --git a/Baker/BakerAssets.xcassets/forward.imageset/Contents.json b/Baker/BakerAssets.xcassets/forward.imageset/Contents.json new file mode 100644 index 0000000..590096e --- /dev/null +++ b/Baker/BakerAssets.xcassets/forward.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "forward.png" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "forward@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Baker/BakerAssets.xcassets/forward.imageset/forward.png b/Baker/BakerAssets.xcassets/forward.imageset/forward.png new file mode 100644 index 0000000..79b5cb9 Binary files /dev/null and b/Baker/BakerAssets.xcassets/forward.imageset/forward.png differ diff --git a/Baker/BakerAssets.xcassets/forward.imageset/forward@2x.png b/Baker/BakerAssets.xcassets/forward.imageset/forward@2x.png new file mode 100644 index 0000000..ef87c75 Binary files /dev/null and b/Baker/BakerAssets.xcassets/forward.imageset/forward@2x.png differ diff --git a/Baker/BakerAssets.xcassets/navigation-bar-bg-ios6.imageset/Contents.json b/Baker/BakerAssets.xcassets/navigation-bar-bg-ios6.imageset/Contents.json new file mode 100644 index 0000000..8e88998 --- /dev/null +++ b/Baker/BakerAssets.xcassets/navigation-bar-bg-ios6.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "navigation-bar-bg-ios6.png" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "navigation-bar-bg-ios6@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Baker/BakerAssets.xcassets/navigation-bar-bg-ios6.imageset/navigation-bar-bg-ios6.png b/Baker/BakerAssets.xcassets/navigation-bar-bg-ios6.imageset/navigation-bar-bg-ios6.png new file mode 100644 index 0000000..81406e8 Binary files /dev/null and b/Baker/BakerAssets.xcassets/navigation-bar-bg-ios6.imageset/navigation-bar-bg-ios6.png differ diff --git a/Baker/BakerAssets.xcassets/navigation-bar-bg-ios6.imageset/navigation-bar-bg-ios6@2x.png b/Baker/BakerAssets.xcassets/navigation-bar-bg-ios6.imageset/navigation-bar-bg-ios6@2x.png new file mode 100644 index 0000000..174feba Binary files /dev/null and b/Baker/BakerAssets.xcassets/navigation-bar-bg-ios6.imageset/navigation-bar-bg-ios6@2x.png differ diff --git a/Baker/BakerAssets.xcassets/navigation-bar-bg.imageset/Contents.json b/Baker/BakerAssets.xcassets/navigation-bar-bg.imageset/Contents.json new file mode 100644 index 0000000..52ebad3 --- /dev/null +++ b/Baker/BakerAssets.xcassets/navigation-bar-bg.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "navigation-bar-bg.png" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "navigation-bar-bg@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Baker/BakerAssets.xcassets/navigation-bar-bg.imageset/navigation-bar-bg.png b/Baker/BakerAssets.xcassets/navigation-bar-bg.imageset/navigation-bar-bg.png new file mode 100644 index 0000000..61d587a Binary files /dev/null and b/Baker/BakerAssets.xcassets/navigation-bar-bg.imageset/navigation-bar-bg.png differ diff --git a/Baker/BakerAssets.xcassets/navigation-bar-bg.imageset/navigation-bar-bg@2x.png b/Baker/BakerAssets.xcassets/navigation-bar-bg.imageset/navigation-bar-bg@2x.png new file mode 100644 index 0000000..96f8cc7 Binary files /dev/null and b/Baker/BakerAssets.xcassets/navigation-bar-bg.imageset/navigation-bar-bg@2x.png differ diff --git a/Baker/BakerAssets.xcassets/newsstand-app-icon.imageset/Contents.json b/Baker/BakerAssets.xcassets/newsstand-app-icon.imageset/Contents.json new file mode 100644 index 0000000..83fc075 --- /dev/null +++ b/Baker/BakerAssets.xcassets/newsstand-app-icon.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "newsstand-app-icon.png" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "newsstand-app-icon@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Baker/BakerAssets.xcassets/newsstand-app-icon.imageset/newsstand-app-icon.png b/Baker/BakerAssets.xcassets/newsstand-app-icon.imageset/newsstand-app-icon.png new file mode 100644 index 0000000..90567a1 Binary files /dev/null and b/Baker/BakerAssets.xcassets/newsstand-app-icon.imageset/newsstand-app-icon.png differ diff --git a/Baker/BakerAssets.xcassets/newsstand-app-icon.imageset/newsstand-app-icon@2x.png b/Baker/BakerAssets.xcassets/newsstand-app-icon.imageset/newsstand-app-icon@2x.png new file mode 100644 index 0000000..06b0f2a Binary files /dev/null and b/Baker/BakerAssets.xcassets/newsstand-app-icon.imageset/newsstand-app-icon@2x.png differ diff --git a/Baker/BakerAssets.xcassets/shelf-background.imageset/Contents.json b/Baker/BakerAssets.xcassets/shelf-background.imageset/Contents.json new file mode 100644 index 0000000..6466baf --- /dev/null +++ b/Baker/BakerAssets.xcassets/shelf-background.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "shelf-pattern.png" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "shelf-pattern@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x", + "filename" : "shelf-pattern@2x-1.png" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Baker/BakerAssets.xcassets/shelf-background.imageset/shelf-pattern.png b/Baker/BakerAssets.xcassets/shelf-background.imageset/shelf-pattern.png new file mode 100644 index 0000000..63b6fa1 Binary files /dev/null and b/Baker/BakerAssets.xcassets/shelf-background.imageset/shelf-pattern.png differ diff --git a/Baker/BakerAssets.xcassets/shelf-background.imageset/shelf-pattern@2x-1.png b/Baker/BakerAssets.xcassets/shelf-background.imageset/shelf-pattern@2x-1.png new file mode 100644 index 0000000..171541a Binary files /dev/null and b/Baker/BakerAssets.xcassets/shelf-background.imageset/shelf-pattern@2x-1.png differ diff --git a/Baker/BakerAssets.xcassets/shelf-background.imageset/shelf-pattern@2x.png b/Baker/BakerAssets.xcassets/shelf-background.imageset/shelf-pattern@2x.png new file mode 100644 index 0000000..171541a Binary files /dev/null and b/Baker/BakerAssets.xcassets/shelf-background.imageset/shelf-pattern@2x.png differ diff --git a/Baker/BakerAssets.xcassets/shelf-header.imageset/Contents.json b/Baker/BakerAssets.xcassets/shelf-header.imageset/Contents.json new file mode 100644 index 0000000..6ea0090 --- /dev/null +++ b/Baker/BakerAssets.xcassets/shelf-header.imageset/Contents.json @@ -0,0 +1,33 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "1x", + "filename" : "shelf-header.png" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "filename" : "shelf-header@2x.png" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "filename" : "shelf-header@2x-1.png" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "filename" : "shelf-header-1.png" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "filename" : "shelf-header@2x-2.png" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Baker/BakerAssets.xcassets/shelf-header.imageset/shelf-header-1.png b/Baker/BakerAssets.xcassets/shelf-header.imageset/shelf-header-1.png new file mode 100644 index 0000000..c1c9cda Binary files /dev/null and b/Baker/BakerAssets.xcassets/shelf-header.imageset/shelf-header-1.png differ diff --git a/Baker/BakerAssets.xcassets/shelf-header.imageset/shelf-header.png b/Baker/BakerAssets.xcassets/shelf-header.imageset/shelf-header.png new file mode 100644 index 0000000..263be66 Binary files /dev/null and b/Baker/BakerAssets.xcassets/shelf-header.imageset/shelf-header.png differ diff --git a/Baker/BakerAssets.xcassets/shelf-header.imageset/shelf-header@2x-1.png b/Baker/BakerAssets.xcassets/shelf-header.imageset/shelf-header@2x-1.png new file mode 100644 index 0000000..18bf08d Binary files /dev/null and b/Baker/BakerAssets.xcassets/shelf-header.imageset/shelf-header@2x-1.png differ diff --git a/Baker/BakerAssets.xcassets/shelf-header.imageset/shelf-header@2x-2.png b/Baker/BakerAssets.xcassets/shelf-header.imageset/shelf-header@2x-2.png new file mode 100644 index 0000000..98859a2 Binary files /dev/null and b/Baker/BakerAssets.xcassets/shelf-header.imageset/shelf-header@2x-2.png differ diff --git a/Baker/BakerAssets.xcassets/shelf-header.imageset/shelf-header@2x.png b/Baker/BakerAssets.xcassets/shelf-header.imageset/shelf-header@2x.png new file mode 100644 index 0000000..f8bcf17 Binary files /dev/null and b/Baker/BakerAssets.xcassets/shelf-header.imageset/shelf-header@2x.png differ diff --git a/Baker/One Small World-Info.plist b/Baker/One Small World-Info.plist new file mode 100644 index 0000000..dc8b626 --- /dev/null +++ b/Baker/One Small World-Info.plist @@ -0,0 +1,69 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + ${PRODUCT_NAME} + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIcons + + UINewsstandIcon + + CFBundleIconFiles + + newsstand-app-icon + + UINewsstandBindingEdge + UINewsstandBindingEdgeLeft + UINewsstandBindingType + UINewsstandBindingTypeMagazine + + + CFBundleIdentifier + com.yourcompany.${PRODUCT_NAME:rfc1034identifier} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + APPL + CFBundleShortVersionString + 4.3 + CFBundleSignature + ???? + CFBundleVersion + 4.3 + LSRequiresIPhoneOS + + UIBackgroundModes + + newsstand-content + remote-notification + + UINewsstandApp + + UIRequiredDeviceCapabilities + + armv7 + + UIStatusBarHidden + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationPortraitUpsideDown + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/Baker/One Small World-Prefix.pch b/Baker/One Small World-Prefix.pch new file mode 100644 index 0000000..697b53b --- /dev/null +++ b/Baker/One Small World-Prefix.pch @@ -0,0 +1,14 @@ +// +// Prefix header for all source files of the 'Baker' target in the 'Baker' project +// + +#import + +#ifndef __IPHONE_8_1 + #error "Baker expects you to use iOS SDK 8.1. +#endif + +#ifdef __OBJC__ + #import + #import +#endif diff --git a/Baker/en.lproj/Localizable.strings b/Baker/en.lproj/Localizable.strings new file mode 100644 index 0000000..dcc7e0b --- /dev/null +++ b/Baker/en.lproj/Localizable.strings @@ -0,0 +1,113 @@ + +// Shelf Navigation Title at the top of the main Issues List +"SHELF_NAVIGATION_TITLE" = "One Small World"; + +// Products alert +"PRODUCTS_REQUEST_FAILED_TITLE" = "Failure connecting to the App Store"; +"PRODUCTS_REQUEST_FAILED_CLOSE" = "Close"; + +// Info button +"INFO_BUTTON_TEXT" = "Info"; + +// Subscription button +"SUBSCRIBE_BUTTON_TEXT" = "Subscribe"; +"SUBSCRIBE_BUTTON_DISABLED_TEXT" = "Subscribing..."; +"SUBSCRIBE_BUTTON_SUBSCRIBED_TEXT" = "Subscribed"; + +"SUBSCRIPTIONS_SHEET_FREE" = "Subscribe for free"; +"SUBSCRIPTIONS_SHEET_RESTORE" = "Restore purchases"; +"SUBSCRIPTIONS_SHEET_CLOSE" = "Close"; + +"SUBSCRIPTIONS_SHEET_GENERIC" = "Subscription options"; +"SUBSCRIPTIONS_SHEET_SUBSCRIBED" = "You are subscribed"; +"SUBSCRIPTIONS_SHEET_NOT_SUBSCRIBED" = "You are not subscribed"; + +// Subscription alerts +"SUBSCRIPTION_SUCCESSFUL_TITLE" = "Subscribed successfully"; +"SUBSCRIPTION_SUCCESSFUL_MESSAGE" = "You have successfully subscribed"; +"SUBSCRIPTION_SUCCESSFUL_CLOSE" = "Close"; +"SUBSCRIPTION_FAILED_TITLE" = "Subscription failure"; +"SUBSCRIPTION_FAILED_CLOSE" = "Close"; + +// Issue purchase alerts +"ISSUE_PURCHASE_SUCCESSFUL_TITLE" = "Purchase successful"; +"ISSUE_PURCHASE_SUCCESSFUL_MESSAGE" = "You have successfully purchased %@"; +"ISSUE_PURCHASE_SUCCESSFUL_CLOSE" = "Close"; +"ISSUE_PURCHASE_FAILED_TITLE" = "Purchase failure"; +"ISSUE_PURCHASE_FAILED_CLOSE" = "Close"; + +// Restore alerts +"RESTORE_FAILED_TITLE" = "Restore failure"; +"RESTORE_FAILED_CLOSE" = "Close"; + +"RESTORED_ISSUE_NOT_RECOGNISED_TITLE" = "Some issues could not be restored"; +"RESTORED_ISSUE_NOT_RECOGNISED_MESSAGE" = "The following product IDs are not defined and the corresponding issues could not be restored:\n%@.\n\nContact the app creator for more info."; +"RESTORED_ISSUE_NOT_RECOGNISED_CLOSE" = "Close"; + +// Download alert +"DOWNLOAD_FAILED_TITLE" = "Download failed"; +"DOWNLOAD_FAILED_MESSAGE" = "The issue could not be downloaded"; +"DOWNLOAD_FAILED_CLOSE" = "Close"; + +// Unzip alert +"UNZIP_FAILED_TITLE" = "Opening failed"; +"UNZIP_FAILED_MESSAGE" = "The issue could not be opened"; +"UNZIP_FAILED_CLOSE" = "Close"; + +// Connection alert +"INTERNET_CONNECTION_UNAVAILABLE_TITLE" = "No Internet Connection"; +"INTERNET_CONNECTION_UNAVAILABLE_MESSAGE" = "An Internet connection is unavailable. Some features of this application require an active connection."; +"INTERNET_CONNECTION_UNAVAILABLE_CLOSE" = "OK"; + +// Issue buttons +"ACTION_REMOTE_TEXT" = "DOWNLOAD"; +"ACTION_DOWNLOADED_TEXT" = "READ"; +"ACTION_BUY_TEXT" = "BUY"; +"ARCHIVE_TEXT" = "ARCHIVE"; +"CONNECTING_TEXT" = "CONNECTING..."; +"DOWNLOADING_TEXT" = "DOWNLOADING ..."; +"OPENING_TEXT" = "Loading..."; +"BUYING_TEXT" = "BUYING..."; +"RETRIEVING_TEXT" = "UPDATING PRICE..."; + +// Issue states +"FREE_TEXT" = "FREE"; +"PURCHASED_TEXT" = "PURCHASED"; + +// Archiving alert +"ARCHIVE_ALERT_TITLE" = "Are you sure you want to archive this issue?"; +"ARCHIVE_ALERT_MESSAGE" = "This issue will be removed from your device. You may download it at anytime for free."; +"ARCHIVE_ALERT_BUTTON_CANCEL" = "Cancel"; +"ARCHIVE_ALERT_BUTTON_OK" = "Archive"; + +// Mail alert +"MAILTO_ALERT_TITLE" = "Failure"; +"MAILTO_ALERT_MESSAGE" = "Your device doesn't support the sending of emails!"; +"MAILTO_ALERT_CLOSE" = "OK"; + +// Modal web view +"WEB_MODAL_FAILURE_MESSAGE" = "Unable to connect."; +"WEB_MODAL_CLOSE_BUTTON_TEXT" = "Back"; + +// Transaction recording alert +"TRANSACTION_RECORDING_FAILED_TITLE" = "Could not confirm purchase with remote server"; +"TRANSACTION_RECORDING_FAILED_MESSAGE" = "The operation will be retried until it is successful"; +"TRANSACTION_RECORDING_FAILED_CLOSE" = "Close"; + +// Issue opening failure alert +"ISSUE_OPENING_FAILED_TITLE" = "Could not open issue"; +"ISSUE_OPENING_FAILED_MESSAGE" = "There was an error opening this issue. Try downloading it again or contact the publisher."; +"ISSUE_OPENING_FAILED_CLOSE" = "Close"; + +// Subscriptions +// (These are just examples: you'll need to provide your own product IDs and names) +//"com.example.MyBook.subscription.3months" = "3 Months Subscription"; +//"com.example.MyBook.subscription.6months" = "Half Year Subscription"; + +// Categories +"ALL_CATEGORIES_TITLE" = "All Categories"; + +//Social share button +//Im' reading the ISSUE_NAME from APPNAME. Checkit at BOOK.URL +"SOCIAL_SHARE_BUTTON_MESSAGE" = "I'm reading \"%@\" from \"%@\". Check it at %@"; + diff --git a/Baker/main.m b/Baker/main.m new file mode 100644 index 0000000..ac6c117 --- /dev/null +++ b/Baker/main.m @@ -0,0 +1,41 @@ +// +// main.m +// Baker +// +// ========================================================================================== +// +// Copyright (c) 2010-2013, Davide Casali, Marco Colombo, Alessandro Morandi +// Copyright (c) 2014, Andrew Krowczyk, Cédric Mériau, Pieter Claerhout +// All rights reserved. +// +// 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. +// Neither the name of the Baker Framework nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written +// permission. +// 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. +// + +#import + +#import "BKRAppDelegate.h" + +int main(int argc, char *argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([BKRAppDelegate class])); + } +} diff --git a/Baker/newsstand-app-icon.png b/Baker/newsstand-app-icon.png new file mode 100644 index 0000000..3095efd Binary files /dev/null and b/Baker/newsstand-app-icon.png differ diff --git a/Baker/newsstand-app-icon@2x.png b/Baker/newsstand-app-icon@2x.png new file mode 100644 index 0000000..01d5711 Binary files /dev/null and b/Baker/newsstand-app-icon@2x.png differ diff --git a/Baker/newsstand-app-icon@3x.png b/Baker/newsstand-app-icon@3x.png new file mode 100644 index 0000000..3eee7ad Binary files /dev/null and b/Baker/newsstand-app-icon@3x.png differ diff --git a/Baker/settings.plist b/Baker/settings.plist new file mode 100644 index 0000000..171e2ea --- /dev/null +++ b/Baker/settings.plist @@ -0,0 +1,93 @@ + + + + + useiTunesConnectLocalizations + + isNewsstand + + newsstandLatestIssueCover + + newsstandManifestUrl + https://www.freefallmotion.com/machines/one_world.json + purchaseConfirmationUrl + + purchasesUrl + + postApnsTokenUrl + + freeSubscriptionProductId + com.freefallmotion.OneSmallWorld.sub.free + autoRenewableSubscriptionProductIds + + requestTimeout + 15 + issuesCoverBackgroundColor + #ffffff + issuesShelfOptions + + backgroundFillStyle + Color + backgroundFillGradientStart + #AD4F24 + backgroundFillGradientStop + #AD4F24 + backgroundFillColor + #AD4F24 + headerSticky + + headerStretch + + headerBackgroundColor + transparent + headerImageFill + + headerHeightPadLandscape + 215 + headerHeightPadPortrait + 215 + headerHeightPhoneLandscape + 107 + headerHeightPhonePortrait + 107 + + issuesTitleFont + AppleGothic + issuesTitleFontSize + 15 + issuesTitleColor + #000000 + issuesInfoFont + AppleGothic + issuesInfoFontSize + 15 + issuesInfoColor + #000000 + issuesPriceColor + #000000 + issuesActionFont + AppleGothic + issuesActionFontSize + 11 + issuesActionBackgroundColor + #020000 + issuesActionButtonColor + #ffffff + issuesArchiveFont + AppleGothic + issuesArchiveFontSize + 11 + issuesArchiveBackgroundColor + #020000 + issuesArchiveButtonColor + #ffffff + issuesLoadingLabelColor + #020000 + issuesLoadingSpinnerColor + #020000 + issuesProgressbarTintColor + #020000 + showSocialShareButton + + + diff --git a/BakerComponents/BKRCategoryFilterItem.h b/BakerComponents/BKRCategoryFilterItem.h new file mode 100644 index 0000000..af4e522 --- /dev/null +++ b/BakerComponents/BKRCategoryFilterItem.h @@ -0,0 +1,52 @@ +// +// BKRCategoryFilterItem.h +// Baker +// +// ========================================================================================== +// +// Copyright (c) 2010-2013, Davide Casali, Marco Colombo, Alessandro Morandi +// Copyright (c) 2014, Andrew Krowczyk, Cédric Mériau, Pieter Claerhout, Tobias Strebitzer +// All rights reserved. +// +// 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. +// Neither the name of the Baker Framework nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written +// permission. +// 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. +// + +#import + +@protocol BKRCategoryFilterItemDelegate +@required +- (void)categoryFilterItem:(id)categoryFilterItem clickedAction:(NSString *)action; +@end + +@interface BKRCategoryFilterItem : UIBarButtonItem { + id delegate; +} + +@property (retain) id delegate; +@property (nonatomic, strong) UIActionSheet *categoriesActionSheet; +@property (nonatomic, strong) NSArray *categoriesActionSheetActions; +@property (nonatomic, strong) NSArray *categories; + +-(id)initWithCategories:(NSArray *)aCategories delegate:(NSObject *)aDelegate; +-(id)initWithDelegate:(NSObject *)aDelegate; + +@end \ No newline at end of file diff --git a/BakerComponents/BKRCategoryFilterItem.m b/BakerComponents/BKRCategoryFilterItem.m new file mode 100644 index 0000000..426ae7c --- /dev/null +++ b/BakerComponents/BKRCategoryFilterItem.m @@ -0,0 +1,106 @@ +// +// BKRCategoryFilterItem.m +// Baker +// +// ========================================================================================== +// +// Copyright (c) 2010-2013, Davide Casali, Marco Colombo, Alessandro Morandi +// Copyright (c) 2014, Andrew Krowczyk, Cédric Mériau, Pieter Claerhout, Tobias Strebitzer +// All rights reserved. +// +// 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. +// Neither the name of the Baker Framework nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written +// permission. +// 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. +// + +#import "BKRCategoryFilterItem.h" + +@implementation BKRCategoryFilterItem { + +} + +@synthesize delegate; + +-(id)initWithCategories:(NSArray *)aCategories delegate:(NSObject *)aDelegate { + + // Initialize dropdown + if(self = [super initWithTitle:NSLocalizedString(@"ALL_CATEGORIES_TITLE", nil) style:UIBarButtonItemStylePlain target:self action:@selector(categoryFilterItemTouched:)]) { + + // Set categories + self.categories = aCategories; + + // Set delegate + self.delegate = aDelegate; + } + return self; +} + +-(id)initWithDelegate:(NSObject *)aDelegate { + return [self initWithCategories:[NSArray array] delegate:aDelegate]; +} + +- (IBAction)categoryFilterItemTouched:(UIBarButtonItem *)sender { + if (self.categoriesActionSheet.visible) { + [self.categoriesActionSheet dismissWithClickedButtonIndex:(self.categoriesActionSheet.numberOfButtons - 1) animated:YES]; + } else { + self.categoriesActionSheet = [self buildCategoriesActionSheet]; + [self.categoriesActionSheet showFromBarButtonItem:sender animated:YES]; + } +} + +- (UIActionSheet *)buildCategoriesActionSheet { + UIActionSheet *sheet = [[UIActionSheet alloc] initWithTitle:nil + delegate:self + cancelButtonTitle:nil + destructiveButtonTitle:nil + otherButtonTitles:nil]; + NSMutableArray *actions = [NSMutableArray array]; + + // Prepend "All Categories" item + [sheet addButtonWithTitle:NSLocalizedString(@"ALL_CATEGORIES_TITLE", nil)]; + [actions addObject:@"reset-filter"]; + + // Add categories + for (NSString *categoryName in self.categories) { + [sheet addButtonWithTitle:categoryName]; + [actions addObject:categoryName]; + } + + self.categoriesActionSheetActions = actions; + + return sheet; +} + + +#pragma mark - Action Sheet Delegates + +- (void) actionSheet:(UIActionSheet*)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex { + if (actionSheet == self.categoriesActionSheet && buttonIndex > -1) { + NSString *action = [self.categoriesActionSheetActions objectAtIndex:buttonIndex]; + if ([action isEqualToString:@"reset-filter"]) { + [self setTitle:NSLocalizedString(@"ALL_CATEGORIES_TITLE", nil)]; + } else { + [self setTitle:action]; + } + [self.delegate categoryFilterItem:self clickedAction:action]; + } +} + +@end diff --git a/BakerShelf/BKRBakerAPI.h b/BakerShelf/BKRBakerAPI.h new file mode 100644 index 0000000..88717b7 --- /dev/null +++ b/BakerShelf/BKRBakerAPI.h @@ -0,0 +1,69 @@ +// +// BakerAPI.h +// Baker +// +// ========================================================================================== +// +// Copyright (c) 2010-2013, Davide Casali, Marco Colombo, Alessandro Morandi +// Copyright (c) 2014, Andrew Krowczyk, Cédric Mériau, Pieter Claerhout +// All rights reserved. +// +// 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. +// Neither the name of the Baker Framework nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written +// permission. +// 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. +// + +#import + +@interface BKRBakerAPI : NSObject + +#pragma mark - Singleton + ++ (BKRBakerAPI*)sharedInstance; + +#pragma mark - Shelf + +- (BOOL)canGetShelfJSON; +- (void)getShelfJSON:(void (^)(NSData*)) callback ; + +#pragma mark - Purchases + +- (BOOL)canGetPurchasesJSON; +- (void)getPurchasesJSON:(void (^)(NSData*)) callback ; + +- (BOOL)canPostPurchaseReceipt; +- (BOOL)postPurchaseReceipt:(NSString*)receipt ofType:(NSString*)type; + +#pragma mark - APNS + +- (BOOL)canPostAPNSToken; +- (BOOL)postAPNSToken:(NSString*)apnsToken; + +#pragma mark - User ID + ++ (BOOL)generateUUIDOnce; ++ (NSString*)UUID; + +#pragma mark - Helpers + +- (NSURLRequest*)requestForURL:(NSURL*)url method:(NSString*)method; +- (NSURLRequest*)requestForURL:(NSURL*)url parameters:(NSDictionary*)parameters method:(NSString*)method cachePolicy:(NSURLRequestCachePolicy)cachePolicy; + +@end diff --git a/BakerShelf/BKRBakerAPI.m b/BakerShelf/BKRBakerAPI.m new file mode 100644 index 0000000..7f32d8e --- /dev/null +++ b/BakerShelf/BKRBakerAPI.m @@ -0,0 +1,288 @@ +// +// BakerAPI.m +// Baker +// +// ========================================================================================== +// +// Copyright (c) 2010-2013, Davide Casali, Marco Colombo, Alessandro Morandi +// Copyright (c) 2014, Andrew Krowczyk, Cédric Mériau, Pieter Claerhout +// All rights reserved. +// +// 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. +// Neither the name of the Baker Framework nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written +// permission. +// 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. +// + +#import "BKRBakerAPI.h" +#import "BKRUtils.h" +#import "BKRSettings.h" + +#import "NSMutableURLRequest+BakerExtensions.h" +#import "NSURL+BakerExtensions.h" +#import "NSString+BakerExtensions.h" + +@implementation BKRBakerAPI + +#pragma mark - Singleton + ++ (BKRBakerAPI*)sharedInstance { + static dispatch_once_t once; + static BKRBakerAPI *sharedInstance; + dispatch_once(&once, ^{ + sharedInstance = [[self alloc] init]; + }); + return sharedInstance; +} + +#pragma mark - Shelf + +- (BOOL)canGetShelfJSON { + return ([self manifestURL] != nil); +} + +- (void)getShelfJSON:(void (^)(NSData*)) callback { + + if ([NSThread isMainThread]) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSData *data = [self getFromURL:[self manifestURL] cachePolicy:NSURLRequestReloadIgnoringLocalCacheData]; + if (callback) { + dispatch_sync(dispatch_get_main_queue(), ^{ + callback(data); + }); + } + }); + } else { + NSData *data = [self getFromURL:[self manifestURL] cachePolicy:NSURLRequestReloadIgnoringLocalCacheData]; + if (callback) { + callback(data); + } + } +} + +#pragma mark - Purchases + +- (BOOL)canGetPurchasesJSON { + return ([self purchasesURL] != nil); +} + +- (void)getPurchasesJSON:(void (^)(NSData*)) callback { + + if ([self canGetPurchasesJSON]) { + if ([NSThread isMainThread]) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSData *data = [self getFromURL:[self purchasesURL] cachePolicy:NSURLRequestUseProtocolCachePolicy]; + if (callback) { + dispatch_sync(dispatch_get_main_queue(), ^{ + callback(data); + }); + } + }); + } else { + NSData *data = [self getFromURL:[self purchasesURL] cachePolicy:NSURLRequestUseProtocolCachePolicy]; + if (callback) { + callback(data); + } + } + } else if (callback) { + callback(nil); + } +} + +- (BOOL)canPostPurchaseReceipt { + return ([self purchaseConfirmationURL] != nil); +} +- (BOOL)postPurchaseReceipt:(NSString*)receipt ofType:(NSString*)type { + if ([self canPostPurchaseReceipt]) { + NSDictionary *params = @{@"type": type, + @"receipt_data": receipt}; + + return [self postParams:params toURL:[self purchaseConfirmationURL]]; + } + return NO; +} + +#pragma mark - APNS + +- (BOOL)canPostAPNSToken { + return ([self postAPNSTokenURL] != nil); +} +- (BOOL)postAPNSToken:(NSString*)apnsToken { + if ([self canPostAPNSToken]) { + NSDictionary *params = @{@"apns_token": apnsToken}; + + return [self postParams:params toURL:[self postAPNSTokenURL]]; + } + return NO; +} + +#pragma mark - User ID + ++ (BOOL)generateUUIDOnce { + if (![self UUID]) { + [[NSUserDefaults standardUserDefaults] setObject:[NSString bkrUUID] forKey:@"UUID"]; + [[NSUserDefaults standardUserDefaults] synchronize]; + return YES; + } else { + return NO; + } +} + ++ (NSString*)UUID { + return [[NSUserDefaults standardUserDefaults] stringForKey:@"UUID"]; +} + +#pragma mark - Helpers + +- (NSURLRequest*)requestForURL:(NSURL*)url method:(NSString*)method { + return [self requestForURL:url parameters:@{} method:method cachePolicy:NSURLRequestUseProtocolCachePolicy]; +} +- (NSURLRequest*)requestForURL:(NSURL*)url parameters:(NSDictionary*)parameters method:(NSString*)method cachePolicy:(NSURLRequestCachePolicy)cachePolicy { + NSMutableDictionary *requestParams = [NSMutableDictionary dictionaryWithDictionary:parameters]; + requestParams[@"app_id"] = [BKRUtils appID]; + requestParams[@"user_id"] = [BKRBakerAPI UUID]; + + #if DEBUG + requestParams[@"environment"] = @"debug"; + #else + [requestParams setObject:@"production" forKey:@"environment"]; + #endif + + if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad){ + requestParams[@"devicetype"] = @"tablet"; + } + else{ + requestParams[@"devicetype"] = @"phone"; + } + + NSURL *requestURL = [self replaceParameters:requestParams inURL:url]; + NSMutableURLRequest *request = nil; + + if ([method isEqualToString:@"GET"]) { + NSString *queryString = [self queryStringFromParameters:requestParams]; + requestURL = [requestURL bkrURLByAppendingQueryString:queryString]; + request = [[NSURLRequest requestWithURL:requestURL cachePolicy:cachePolicy timeoutInterval:[BKRSettings sharedSettings].requestTimeout] mutableCopy]; + } else if ([method isEqualToString:@"POST"]) { + request = [[NSMutableURLRequest alloc] initWithURL:requestURL cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:[BKRSettings sharedSettings].requestTimeout]; + [request setHTTPMethod:@"POST"]; + [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; + [request bkrSetFormPostParameters:requestParams]; + } + + return request; +} + +- (BOOL)postParams:(NSDictionary*)params toURL:(NSURL*)url { + NSError *error = nil; + NSHTTPURLResponse *response = nil; + NSURLRequest *request = [self requestForURL:url parameters:params method:@"POST" cachePolicy:NSURLRequestUseProtocolCachePolicy]; + + [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error]; + + if (error) { + NSLog(@"[ERROR] Failed POST request to %@: %@", [request URL], [error localizedDescription]); + return NO; + } else if ([response statusCode] == 200) { + return YES; + } else { + NSLog(@"[ERROR] Failed POST request to %@: response was %ld %@", + [request URL], + (long)[response statusCode], + [NSHTTPURLResponse localizedStringForStatusCode:[response statusCode]]); + return NO; + } +} + +- (NSData*)getFromURL:(NSURL*)url cachePolicy:(NSURLRequestCachePolicy)cachePolicy { + NSError *error = nil; + NSHTTPURLResponse *response = nil; + NSURLRequest *request = [self requestForURL:url parameters:@{} method:@"GET" cachePolicy:cachePolicy]; + + NSData *data = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error]; + + if (error) { + NSLog(@"[ERROR] Failed GET request to %@: %@", [request URL], [error localizedDescription]); + return nil; + } else if ([response statusCode] == 200) { + return data; + } else { + NSLog(@"[ERROR] Failed GET request to %@: response was %ld %@", + [request URL], + (long)[response statusCode], + [NSHTTPURLResponse localizedStringForStatusCode:[response statusCode]]); + return nil; + } +} + +- (NSURL*)replaceParameters:(NSMutableDictionary*)parameters inURL:(NSURL*)url { + NSMutableString *urlString = [NSMutableString stringWithString:[url absoluteString]]; + NSDictionary *allParameters = [NSDictionary dictionaryWithDictionary:parameters]; + [allParameters enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + NSString *keyToReplace = [@":" stringByAppendingString:key]; + NSRange range = [urlString rangeOfString:keyToReplace options:NSCaseInsensitiveSearch]; + if (range.location != NSNotFound) { + [urlString replaceCharactersInRange:range withString:obj]; + [parameters removeObjectForKey:key]; + } + }]; + return [NSURL URLWithString:urlString]; +} + +- (NSString*)queryStringFromParameters:(NSDictionary*)parameters { + NSMutableString *queryString = [NSMutableString stringWithString:@""]; + if ([parameters count] > 0) { + [parameters enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + NSString *queryParameter = [NSString stringWithFormat:@"%@=%@&", key, obj]; + [queryString appendString:queryParameter]; + }]; + // Remove the last "&" + [queryString deleteCharactersInRange:NSMakeRange([queryString length] - 1, 1)]; + } + return queryString; +} + +- (NSURL*)manifestURL { + if ([BKRSettings sharedSettings].isNewsstand && [BKRSettings sharedSettings].newsstandManifestUrl.length > 0) { + return [NSURL URLWithString:[BKRSettings sharedSettings].newsstandManifestUrl]; + } + return nil; +} + +- (NSURL*)purchasesURL { + if ([BKRSettings sharedSettings].isNewsstand && [BKRSettings sharedSettings].purchasesUrl.length > 0) { + return [NSURL URLWithString:[BKRSettings sharedSettings].purchasesUrl]; + } + return nil; +} + +- (NSURL*)purchaseConfirmationURL { + if ([BKRSettings sharedSettings].isNewsstand && [BKRSettings sharedSettings].purchaseConfirmationUrl.length > 0) { + return [NSURL URLWithString:[BKRSettings sharedSettings].purchaseConfirmationUrl]; + } + return nil; +} + +- (NSURL*)postAPNSTokenURL { + if ([BKRSettings sharedSettings].isNewsstand && [BKRSettings sharedSettings].postApnsTokenUrl.length > 0) { + return [NSURL URLWithString:[BKRSettings sharedSettings].postApnsTokenUrl]; + } + return nil; +} + +@end diff --git a/BakerShelf/BKRCustomNavigationBar.h b/BakerShelf/BKRCustomNavigationBar.h new file mode 100644 index 0000000..4eb0027 --- /dev/null +++ b/BakerShelf/BKRCustomNavigationBar.h @@ -0,0 +1,40 @@ +// +// UICustomNavigationBar.h +// Baker +// +// ========================================================================================== +// +// Copyright (c) 2010-2013, Davide Casali, Marco Colombo, Alessandro Morandi +// Copyright (c) 2014, Andrew Krowczyk, Cédric Mériau, Pieter Claerhout +// All rights reserved. +// +// 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. +// Neither the name of the Baker Framework nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written +// permission. +// 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. +// + +#import + +@interface BKRCustomNavigationBar : UINavigationBar { + UIImageView *backgroundImageView; + NSMutableDictionary *backgroundImages; +} + +@end diff --git a/BakerShelf/BKRCustomNavigationBar.m b/BakerShelf/BKRCustomNavigationBar.m new file mode 100644 index 0000000..04cea0d --- /dev/null +++ b/BakerShelf/BKRCustomNavigationBar.m @@ -0,0 +1,86 @@ +// +// UICustomNavigationBar.m +// Baker +// +// ========================================================================================== +// +// Copyright (c) 2010-2013, Davide Casali, Marco Colombo, Alessandro Morandi +// Copyright (c) 2014, Andrew Krowczyk, Cédric Mériau, Pieter Claerhout +// All rights reserved. +// +// 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. +// Neither the name of the Baker Framework nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written +// permission. +// 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. +// + +#import "BKRCustomNavigationBar.h" + +@implementation BKRCustomNavigationBar + +- (NSMutableDictionary*)backgroundImages { + if (!backgroundImages) { + backgroundImages = [[NSMutableDictionary alloc] init]; + } + return backgroundImages; +} + +- (UIImageView*)backgroundImageView { + if (!backgroundImageView) { + backgroundImageView = [[UIImageView alloc] initWithFrame:[self bounds]]; + [backgroundImageView setAutoresizingMask:UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight]; + [self insertSubview:backgroundImageView atIndex:0]; + } + return backgroundImageView; +} + +- (void)setBackgroundImage:(UIImage*)backgroundImage forBarMetrics:(UIBarMetrics)barMetrics { + if ([UINavigationBar instancesRespondToSelector:@selector(setBackgroundImage:forBarMetrics:)]) { + [super setBackgroundImage:backgroundImage forBarMetrics:barMetrics]; + } else { + [self backgroundImages][[NSNumber numberWithInt:barMetrics]] = backgroundImage; + [self updateBackgroundImage]; + } +} + +- (void)updateBackgroundImage { + UIBarMetrics metrics = UIBarMetricsLandscapePhone; + if ([self bounds].size.height > 40) { + metrics = UIBarMetricsDefault; + } + + UIImage *image = [self backgroundImages][[NSNumber numberWithInt:metrics]]; + if (!image && metrics != UIBarMetricsDefault) { + image = [self backgroundImages][[NSNumber numberWithInt:UIBarMetricsDefault]]; + } + + if (image) { + [[self backgroundImageView] setImage:image]; + } +} + +- (void)layoutSubviews { + [super layoutSubviews]; + if (backgroundImageView) { + [self updateBackgroundImage]; + [self sendSubviewToBack:backgroundImageView]; + } +} + +@end diff --git a/BakerShelf/BKRCustomNavigationController.h b/BakerShelf/BKRCustomNavigationController.h new file mode 100644 index 0000000..36d9458 --- /dev/null +++ b/BakerShelf/BKRCustomNavigationController.h @@ -0,0 +1,37 @@ +// +// UICustomNavigationController.h +// Baker +// +// ========================================================================================== +// +// Copyright (c) 2010-2013, Davide Casali, Marco Colombo, Alessandro Morandi +// Copyright (c) 2014, Andrew Krowczyk, Cédric Mériau, Pieter Claerhout +// All rights reserved. +// +// 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. +// Neither the name of the Baker Framework nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written +// permission. +// 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. +// + +#import + +@interface BKRCustomNavigationController : UINavigationController + +@end diff --git a/BakerShelf/BKRCustomNavigationController.m b/BakerShelf/BKRCustomNavigationController.m new file mode 100644 index 0000000..a8c3f2e --- /dev/null +++ b/BakerShelf/BKRCustomNavigationController.m @@ -0,0 +1,46 @@ +// +// UICustomNavigationController.m +// Baker +// +// ========================================================================================== +// +// Copyright (c) 2010-2013, Davide Casali, Marco Colombo, Alessandro Morandi +// Copyright (c) 2014, Andrew Krowczyk, Cédric Mériau, Pieter Claerhout +// All rights reserved. +// +// 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. +// Neither the name of the Baker Framework nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written +// permission. +// 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. +// + +#import "BKRCustomNavigationController.h" +#import "BKRCustomNavigationBar.h" + +@implementation BKRCustomNavigationController + +- (NSUInteger)supportedInterfaceOrientations { + return [self.topViewController supportedInterfaceOrientations]; +} + +- (BOOL)shouldAutorotate { + return [self.topViewController shouldAutorotate]; +} + +@end \ No newline at end of file diff --git a/BakerShelf/BKRIssue.h b/BakerShelf/BKRIssue.h new file mode 100644 index 0000000..5c1da1a --- /dev/null +++ b/BakerShelf/BKRIssue.h @@ -0,0 +1,84 @@ +// +// BakerIssue.h +// Baker +// +// ========================================================================================== +// +// Copyright (c) 2010-2013, Davide Casali, Marco Colombo, Alessandro Morandi +// Copyright (c) 2014, Andrew Krowczyk, Cédric Mériau +// All rights reserved. +// +// 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. +// Neither the name of the Baker Framework nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written +// permission. +// 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. +// + +#import + +#import +#import "BKRPurchasesManager.h" + +#import "BKRBook.h" + +typedef enum transientStates { + BakerIssueTransientStatusNone, + BakerIssueTransientStatusDownloading, + BakerIssueTransientStatusOpening, + BakerIssueTransientStatusPurchasing, + BakerIssueTransientStatusUnpriced +} BakerIssueTransientStatus; + +@interface BKRIssue : NSObject { + BKRPurchasesManager *purchasesManager; +} + +@property (nonatomic, copy) NSString *ID; +@property (nonatomic, copy) NSString *title; +@property (nonatomic, copy) NSString *info; +@property (nonatomic, copy) NSString *date; +@property (nonatomic, copy) NSURL *url; +@property (nonatomic, copy) NSString *path; +@property (nonatomic, copy) NSArray *categories; + +@property (nonatomic, copy) NSString *coverPath; +@property (nonatomic, copy) NSURL *coverURL; + +@property (nonatomic, copy) NSString *productID; +@property (nonatomic, copy) NSString *price; + +@property (nonatomic, strong) BKRBook *bakerBook; + +@property (nonatomic, assign) BakerIssueTransientStatus transientStatus; + +@property (nonatomic, copy) NSString *notificationDownloadStartedName; +@property (nonatomic, copy) NSString *notificationDownloadProgressingName; +@property (nonatomic, copy) NSString *notificationDownloadFinishedName; +@property (nonatomic, copy) NSString *notificationDownloadErrorName; +@property (nonatomic, copy) NSString *notificationUnzipErrorName; + +- (id)initWithBakerBook:(BKRBook*)bakerBook; +- (void)getCoverWithCache:(bool)cache andBlock:(void(^)(UIImage *img))completionBlock; +- (NSString*)getStatus; + +- (id)initWithIssueData:(NSDictionary*)issueData; +- (void)download; +- (void)downloadWithAsset:(NKAssetDownload*)asset; + +@end diff --git a/BakerShelf/BKRIssue.m b/BakerShelf/BKRIssue.m new file mode 100755 index 0000000..35de6a9 --- /dev/null +++ b/BakerShelf/BKRIssue.m @@ -0,0 +1,287 @@ +// +// BakerIssue.m +// Baker +// +// ========================================================================================== +// +// Copyright (c) 2010-2013, Davide Casali, Marco Colombo, Alessandro Morandi +// Copyright (c) 2014, Andrew Krowczyk, Cédric Mériau, Pieter Claerhout +// All rights reserved. +// +// 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. +// Neither the name of the Baker Framework nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written +// permission. +// 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. +// + +#import "BKRIssue.h" +#import "BKRBakerAPI.h" +#import "BKRSettings.h" + +#import "BKRZipArchive.h" +#import "BKRReachability.h" +#import "BKRUtils.h" +#import "NSURL+BakerExtensions.h" +#import "NSObject+BakerExtensions.h" + +@implementation BKRIssue + +#pragma mark - Initialization + +- (id)initWithBakerBook:(BKRBook*)book { + self = [super init]; + if (self) { + _ID = book.ID; + _title = book.title; + _info = @""; + _date = book.date; + _url = [NSURL URLWithString:book.url]; + _path = book.path; + _categories = book.categories; + _productID = @""; + _price = nil; + _bakerBook = book; + + _coverPath = @""; + if (book.cover == nil) { + // TODO: set path to a default cover (right now a blank box will be displayed) + NSLog(@"Cover not specified for %@, probably missing from book.json", book.ID); + } else { + _coverPath = [book.path stringByAppendingPathComponent:book.cover]; + } + + _transientStatus = BakerIssueTransientStatusNone; + + [self setNotificationDownloadNames]; + } + return self; +} + +- (void)setNotificationDownloadNames { + self.notificationDownloadStartedName = [NSString stringWithFormat:@"notification_download_started_%@", self.ID]; + self.notificationDownloadProgressingName = [NSString stringWithFormat:@"notification_download_progressing_%@", self.ID]; + self.notificationDownloadFinishedName = [NSString stringWithFormat:@"notification_download_finished_%@", self.ID]; + self.notificationDownloadErrorName = [NSString stringWithFormat:@"notification_download_error_%@", self.ID]; + self.notificationUnzipErrorName = [NSString stringWithFormat:@"notification_unzip_error_%@", self.ID]; +} + +#pragma mark - Newsstand + +- (id)initWithIssueData:(NSDictionary*)issueData { + self = [super init]; + if (self) { + self.ID = issueData[@"name"]; + self.title = issueData[@"title"]; + self.info = issueData[@"info"]; + self.date = issueData[@"date"]; + self.categories = issueData[@"categories"]; + self.coverURL = [NSURL URLWithString:issueData[@"cover"]]; + self.url = [NSURL URLWithString:issueData[@"url"]]; + if (issueData[@"product_id"] != [NSNull null]) { + self.productID = issueData[@"product_id"]; + } + self.price = nil; + + purchasesManager = [BKRPurchasesManager sharedInstance]; + + self.coverPath = [self.bkrCachePath stringByAppendingPathComponent:self.ID]; + + NKLibrary *nkLib = [NKLibrary sharedLibrary]; + NKIssue *nkIssue = [nkLib issueWithName:self.ID]; + if (nkIssue) { + self.path = [[nkIssue contentURL] path]; + } else { + self.path = nil; + } + + self.bakerBook = nil; + + self.transientStatus = BakerIssueTransientStatusNone; + + [self setNotificationDownloadNames]; + } + return self; +} + +- (NSString*)nkIssueContentStatusToString:(NKIssueContentStatus) contentStatus{ + if (contentStatus == NKIssueContentStatusNone) { + return @"remote"; + } else if (contentStatus == NKIssueContentStatusDownloading) { + return @"connecting"; + } else if (contentStatus == NKIssueContentStatusAvailable) { + return @"downloaded"; + } + return @""; +} + +- (void)download { + BKRReachability *reach = [BKRReachability reachabilityWithHostname:@"www.google.com"]; + if ([reach isReachable]) { + BKRBakerAPI *api = [BKRBakerAPI sharedInstance]; + NSURLRequest *req = [api requestForURL:self.url method:@"GET"]; + + NKLibrary *nkLib = [NKLibrary sharedLibrary]; + NKIssue *nkIssue = [nkLib issueWithName:self.ID]; + + NKAssetDownload *assetDownload = [nkIssue addAssetWithRequest:req]; + [self downloadWithAsset:assetDownload]; + } else { + [[NSNotificationCenter defaultCenter] postNotificationName:self.notificationDownloadErrorName object:self userInfo:nil]; + } +} + +- (void)downloadWithAsset:(NKAssetDownload*)asset { + [asset downloadWithDelegate:self]; + [[NSNotificationCenter defaultCenter] postNotificationName:self.notificationDownloadStartedName object:self userInfo:nil]; +} + +#pragma mark - Newsstand download management + +- (void)connection:(NSURLConnection*)connection didWriteData:(long long)bytesWritten totalBytesWritten:(long long)totalBytesWritten expectedTotalBytes:(long long)expectedTotalBytes { + NSDictionary *userInfo = @{@"totalBytesWritten": @(totalBytesWritten), + @"expectedTotalBytes": @(expectedTotalBytes)}; + [[NSNotificationCenter defaultCenter] postNotificationName:self.notificationDownloadProgressingName object:self userInfo:userInfo]; +} + +- (void)connectionDidFinishDownloading:(NSURLConnection*)connection destinationURL:(NSURL*)destinationURL { + if ([BKRSettings sharedSettings].isNewsstand) { + [self unpackAssetDownload:connection.newsstandAssetDownload toURL:destinationURL]; + } +} + +- (void)unpackAssetDownload:(NKAssetDownload*)newsstandAssetDownload toURL:(NSURL*)destinationURL { + + UIApplication *application = [UIApplication sharedApplication]; + NKIssue *nkIssue = newsstandAssetDownload.issue; + NSString *destinationPath = [[nkIssue contentURL] path]; + + __block UIBackgroundTaskIdentifier backgroundTask = [application beginBackgroundTaskWithExpirationHandler:^{ + [application endBackgroundTask:backgroundTask]; + backgroundTask = UIBackgroundTaskInvalid; + }]; + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSLog(@"[BakerShelf] Newsstand - File is being unzipped to %@", destinationPath); + BOOL unzipSuccessful = NO; + unzipSuccessful = [BKRZipArchive unzipFileAtPath:[destinationURL path] toDestination:destinationPath]; + if (!unzipSuccessful) { + NSLog(@"[BakerShelf] Newsstand - Unable to unzip file: %@. The file may not be a valid HPUB archive.", [destinationURL path]); + dispatch_async(dispatch_get_main_queue(), ^(void) { + [[NSNotificationCenter defaultCenter] postNotificationName:self.notificationUnzipErrorName object:self userInfo:nil]; + }); + } + + NSLog(@"[BakerShelf] Newsstand - Removing temporary downloaded file %@", [destinationURL path]); + NSFileManager *fileMgr = [NSFileManager defaultManager]; + NSError *error; + if ([fileMgr removeItemAtPath:[destinationURL path] error:&error] != YES){ + NSLog(@"[BakerShelf] Newsstand - Unable to delete file: %@", [error localizedDescription]); + } + + if (unzipSuccessful) { + // Notification and UI update have to be handled on the main thread + dispatch_async(dispatch_get_main_queue(), ^(void) { + [[NSNotificationCenter defaultCenter] postNotificationName:self.notificationDownloadFinishedName object:self userInfo:nil]; + }); + } + + [self updateNewsstandIcon]; + + [application endBackgroundTask:backgroundTask]; + backgroundTask = UIBackgroundTaskInvalid; + }); +} + +- (void)updateNewsstandIcon { + [[UIApplication sharedApplication] setApplicationIconBadgeNumber:1]; + + UIImage *coverImage = [UIImage imageWithContentsOfFile:self.coverPath]; + if (coverImage) { + [[UIApplication sharedApplication] setNewsstandIconImage:coverImage]; + } +} + +- (void)connectionDidResumeDownloading:(NSURLConnection*)connection totalBytesWritten:(long long)totalBytesWritten expectedTotalBytes:(long long)expectedTotalBytes { +} + +- (void)connection:(NSURLConnection*)connection didFailWithError:(NSError*)error { + NSLog(@"Connection error when trying to download %@: %@", [connection currentRequest].URL, [error localizedDescription]); + + [connection cancel]; + + NSDictionary *userInfo = @{@"error": error}; + [[NSNotificationCenter defaultCenter] postNotificationName:self.notificationDownloadErrorName object:self userInfo:userInfo]; +} + +- (void)getCoverWithCache:(bool)cache andBlock:(void(^)(UIImage *img))completionBlock { + UIImage *image = [UIImage imageWithContentsOfFile:self.coverPath]; + if (cache && image) { + completionBlock(image); + } else { + if (self.coverURL) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) { + NSData *imageData = [NSData dataWithContentsOfURL:self.coverURL]; + UIImage *image = [UIImage imageWithData:imageData]; + if (image) { + [imageData writeToFile:self.coverPath atomically:YES]; + dispatch_async(dispatch_get_main_queue(), ^(void) { + completionBlock(image); + }); + } + }); + } + } +} + +- (NSString*)getStatus { + if ([BKRSettings sharedSettings].isNewsstand) { + switch (self.transientStatus) { + case BakerIssueTransientStatusDownloading: + return @"downloading"; + break; + case BakerIssueTransientStatusOpening: + return @"opening"; + break; + case BakerIssueTransientStatusPurchasing: + return @"purchasing"; + break; + default: + break; + } + + NKLibrary *nkLib = [NKLibrary sharedLibrary]; + NKIssue *nkIssue = [nkLib issueWithName:self.ID]; + NSString *nkIssueStatus = [self nkIssueContentStatusToString:[nkIssue status]]; + if ([nkIssueStatus isEqualToString:@"remote"] && self.productID) { + if ([purchasesManager isPurchased:self.productID]) { + return @"purchased"; + } else if (self.price) { + return @"purchasable"; + } else { + return @"unpriced"; + } + } else { + return nkIssueStatus; + } + } else { + return @"bundled"; + } +} + +@end diff --git a/BakerShelf/BKRIssueViewController.h b/BakerShelf/BKRIssueViewController.h new file mode 100644 index 0000000..5d685f7 --- /dev/null +++ b/BakerShelf/BKRIssueViewController.h @@ -0,0 +1,93 @@ +// +// IssueViewController.h +// Baker +// +// ========================================================================================== +// +// Copyright (c) 2010-2013, Davide Casali, Marco Colombo, Alessandro Morandi +// Copyright (c) 2014, Andrew Krowczyk, Cédric Mériau, Pieter Claerhout +// All rights reserved. +// +// 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. +// Neither the name of the Baker Framework nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written +// permission. +// 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. +// + +#import +#import "BKRIssue.h" +#import "BKRPurchasesManager.h" + +@interface BKRIssueViewController : UIViewController { + NSString *currentAction; + BOOL purchaseDelayed; + BKRPurchasesManager *purchasesManager; +} + +@property (nonatomic, strong) BKRIssue *issue; +@property (nonatomic, strong) UIButton *actionButton; +@property (nonatomic, strong) UIButton *archiveButton; +@property (nonatomic, strong) UIProgressView *progressBar; +@property (nonatomic, strong) UIActivityIndicatorView *spinner; +@property (nonatomic, strong) UILabel *loadingLabel; + +@property (nonatomic, strong) UIButton *issueCover; +@property (nonatomic, strong) UILabel *titleLabel; +@property (nonatomic, strong) UILabel *infoLabel; + +@property (nonatomic, copy) NSString *currentStatus; + +#pragma mark - Structs +typedef struct { + int cellPadding; + int thumbWidth; + int thumbHeight; + int contentOffset; +} UI; + +#pragma mark - Init +- (id)initWithBakerIssue:(BKRIssue*)bakerIssue; + +#pragma mark - View Lifecycle +- (void)refresh; +- (void)refresh:(NSString*)status; +- (void)refresh:(NSString*)status cache:(BOOL)cache; +- (void)refreshWithCache:(BOOL)cache; +- (void)refreshContentWithCache:(bool)cache; +- (void)preferredContentSizeChanged:(NSNotification*)notification; + +#pragma mark - Issue management +- (void)actionButtonPressed:(UIButton*)sender; +- (void)download; +- (void)setPrice:(NSString*)price; +- (void)buy; +- (void)read; + +#pragma mark - Newsstand archive management +- (void)archiveButtonPressed:(UIButton*)sender; + +#pragma mark - Helper methods ++ (UI)getIssueContentMeasures; ++ (int)getIssueCellHeight; ++ (CGSize)getIssueCellSizeForOrientation:(UIInterfaceOrientation)orientation; + +@end + +@interface alertView: UIAlertView +@end diff --git a/BakerShelf/BKRIssueViewController.m b/BakerShelf/BKRIssueViewController.m new file mode 100644 index 0000000..bb21930 --- /dev/null +++ b/BakerShelf/BKRIssueViewController.m @@ -0,0 +1,649 @@ +// +// IssueViewController.m +// Baker +// +// ========================================================================================== +// +// Copyright (c) 2010-2013, Davide Casali, Marco Colombo, Alessandro Morandi +// Copyright (c) 2014, Andrew Krowczyk, Cédric Mériau, Pieter Claerhout +// All rights reserved. +// +// 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. +// Neither the name of the Baker Framework nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written +// permission. +// 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. +// + +#import + +#import "BKRSettings.h" +#import "BKRIssueViewController.h" +#import "BKRZipArchive.h" +#import "BKRPurchasesManager.h" + +#import "UIColor+BakerExtensions.h" +#import "UIScreen+BakerExtensions.h" +#import "BKRUtils.h" + +@implementation BKRIssueViewController + +#pragma mark - Init + +- (id)initWithBakerIssue:(BKRIssue*)bakerIssue { + self = [super init]; + if (self) { + _issue = bakerIssue; + _currentStatus = nil; + + purchaseDelayed = NO; + + if ([BKRSettings sharedSettings].isNewsstand) { + purchasesManager = [BKRPurchasesManager sharedInstance]; + [self addPurchaseObserver:@selector(handleIssueRestored:) name:@"notification_issue_restored"]; + + [self addIssueObserver:@selector(handleDownloadStarted:) name:self.issue.notificationDownloadStartedName]; + [self addIssueObserver:@selector(handleDownloadProgressing:) name:self.issue.notificationDownloadProgressingName]; + [self addIssueObserver:@selector(handleDownloadFinished:) name:self.issue.notificationDownloadFinishedName]; + [self addIssueObserver:@selector(handleDownloadError:) name:self.issue.notificationDownloadErrorName]; + [self addIssueObserver:@selector(handleUnzipError:) name:self.issue.notificationUnzipErrorName]; + } + } + return self; +} + +#pragma mark - View Lifecycle + +- (void)viewDidLoad { + [super viewDidLoad]; + + CGSize cellSize = [BKRIssueViewController getIssueCellSizeForOrientation:self.interfaceOrientation]; + + self.view.frame = CGRectMake(0, 0, cellSize.width, cellSize.height); + self.view.backgroundColor = [UIColor clearColor]; + self.view.tag = 42; + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(preferredContentSizeChanged:) name:UIContentSizeCategoryDidChangeNotification object:nil]; + + UI ui = [BKRIssueViewController getIssueContentMeasures]; + + self.issueCover = [UIButton buttonWithType:UIButtonTypeCustom]; + self.issueCover.frame = CGRectMake(ui.cellPadding, ui.cellPadding, ui.thumbWidth, ui.thumbHeight); + + self.issueCover.backgroundColor = [UIColor bkrColorWithHexString:[BKRSettings sharedSettings].issuesCoverBackgroundColor]; + self.issueCover.adjustsImageWhenHighlighted = NO; + self.issueCover.adjustsImageWhenDisabled = NO; + + self.issueCover.layer.shadowOpacity = 0.5; + self.issueCover.layer.shadowOffset = CGSizeMake(0, 2); + self.issueCover.layer.shouldRasterize = YES; + self.issueCover.layer.rasterizationScale = [UIScreen mainScreen].scale; + + [self.issueCover addTarget:self action:@selector(actionButtonPressed:) forControlEvents:UIControlEventTouchUpInside]; + [self.view addSubview:self.issueCover]; + + // SETUP TITLE LABEL + self.titleLabel = [[UILabel alloc] init]; + self.titleLabel.textColor = [UIColor bkrColorWithHexString:[BKRSettings sharedSettings].issuesTitleColor]; + self.titleLabel.backgroundColor = [UIColor clearColor]; + self.titleLabel.lineBreakMode = NSLineBreakByTruncatingTail; + self.titleLabel.textAlignment = NSTextAlignmentLeft; + + [self.view addSubview:self.titleLabel]; + + // SETUP INFO LABEL + self.infoLabel = [[UILabel alloc] init]; + self.infoLabel.textColor = [UIColor bkrColorWithHexString:[BKRSettings sharedSettings].issuesInfoColor]; + self.infoLabel.backgroundColor = [UIColor clearColor]; + self.infoLabel.lineBreakMode = NSLineBreakByTruncatingTail; + self.infoLabel.textAlignment = NSTextAlignmentLeft; + + [self.view addSubview:self.infoLabel]; + + // SETUP ACTION BUTTON + self.actionButton = [UIButton buttonWithType:UIButtonTypeCustom]; + self.actionButton.backgroundColor = [UIColor bkrColorWithHexString:[BKRSettings sharedSettings].issuesActionBackgroundColor]; + + [self.actionButton setTitle:NSLocalizedString(@"ACTION_DOWNLOADED_TEXT", nil) forState:UIControlStateNormal]; + [self.actionButton setTitleColor:[UIColor bkrColorWithHexString:[BKRSettings sharedSettings].issuesActionButtonColor] forState:UIControlStateNormal]; + [self.actionButton addTarget:self action:@selector(actionButtonPressed:) forControlEvents:UIControlEventTouchUpInside]; + + [self.view addSubview:self.actionButton]; + + // SETUP ARCHIVE BUTTON + self.archiveButton = [UIButton buttonWithType:UIButtonTypeCustom]; + self.archiveButton.backgroundColor = [UIColor bkrColorWithHexString:[BKRSettings sharedSettings].issuesArchiveBackgroundColor]; + + [self.archiveButton setTitle:NSLocalizedString(@"ARCHIVE_TEXT", nil) forState:UIControlStateNormal]; + [self.archiveButton setTitleColor:[UIColor bkrColorWithHexString:[BKRSettings sharedSettings].issuesArchiveButtonColor] forState:UIControlStateNormal]; + + if ([BKRSettings sharedSettings].isNewsstand) { + [self.archiveButton addTarget:self action:@selector(archiveButtonPressed:) forControlEvents:UIControlEventTouchUpInside]; + [self.view addSubview:self.archiveButton]; + } + + // SETUP DOWN/LOADING SPINNER AND LABEL + self.spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; + self.spinner.color = [UIColor bkrColorWithHexString:[BKRSettings sharedSettings].issuesLoadingSpinnerColor]; + self.spinner.backgroundColor = [UIColor clearColor]; + self.spinner.hidesWhenStopped = YES; + + self.loadingLabel = [[UILabel alloc] init]; + self.loadingLabel.textColor = [UIColor bkrColorWithHexString:[BKRSettings sharedSettings].issuesLoadingLabelColor]; + self.loadingLabel.backgroundColor = [UIColor clearColor]; + self.loadingLabel.textAlignment = NSTextAlignmentLeft; + self.loadingLabel.text = NSLocalizedString(@"DOWNLOADING_TEXT", nil); + + [self.view addSubview:self.spinner]; + [self.view addSubview:self.loadingLabel]; + + // SETUP PROGRESS BAR + self.progressBar = [[UIProgressView alloc] initWithProgressViewStyle:UIProgressViewStyleDefault]; + self.progressBar.progressTintColor = [UIColor bkrColorWithHexString:[BKRSettings sharedSettings].issuesProgressbarTintColor]; + + [self.view addSubview:self.progressBar]; + + if ([BKRSettings sharedSettings].isNewsstand) { + // RESUME PENDING NEWSSTAND DOWNLOAD + NKLibrary *nkLib = [NKLibrary sharedLibrary]; + for (NKAssetDownload *asset in [nkLib downloadingAssets]) { + if ([asset.issue.name isEqualToString:self.issue.ID]) { + NSLog(@"[BakerShelf] Resuming abandoned Newsstand download: %@", asset.issue.name); + [self.issue downloadWithAsset:asset]; + } + } + } + + [self refreshContentWithCache:NO]; +} + +- (void)refreshContentWithCache:(bool)cache { + UIFont *titleFont = [UIFont fontWithName:[BKRSettings sharedSettings].issuesTitleFont + size:[BKRSettings sharedSettings].issuesTitleFontSize + ]; + UIFont *infoFont = [UIFont fontWithName:[BKRSettings sharedSettings].issuesInfoFont + size:[BKRSettings sharedSettings].issuesInfoFontSize + ]; + UIFont *actionFont = [UIFont fontWithName:[BKRSettings sharedSettings].issuesActionFont + size:[BKRSettings sharedSettings].issuesActionFontSize + ]; + UIFont *archiveFont = [UIFont fontWithName:[BKRSettings sharedSettings].issuesArchiveFont + size:[BKRSettings sharedSettings].issuesArchiveFontSize + ]; + + UI ui = [BKRIssueViewController getIssueContentMeasures]; + int heightOffset = ui.cellPadding; + uint textLineheight = [@"The brown fox jumps over the lazy dog" boundingRectWithSize:CGSizeMake(MAXFLOAT,MAXFLOAT) + options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingTruncatesLastVisibleLine + attributes:@{NSFontAttributeName: infoFont} + context:nil].size.height; + + // SETUP COVER IMAGE + [self.issue getCoverWithCache:cache andBlock:^(UIImage *image) { + [self.issueCover setBackgroundImage:image forState:UIControlStateNormal]; + }]; + + CGFloat labelWidth = self.view.frame.size.width - ui.contentOffset - 60; + + // SETUP TITLE LABEL + self.titleLabel.font = titleFont; + self.titleLabel.frame = CGRectMake(ui.contentOffset, heightOffset, labelWidth, 60); + self.titleLabel.numberOfLines = 3; + self.titleLabel.text = self.issue.title; + [self.titleLabel sizeToFit]; + + heightOffset = heightOffset + self.titleLabel.frame.size.height + 5; + + // SETUP INFO LABEL + self.infoLabel.font = infoFont; + self.infoLabel.frame = CGRectMake(ui.contentOffset, heightOffset, labelWidth, 60); + self.infoLabel.numberOfLines = 3; + self.infoLabel.text = self.issue.info; + [self.infoLabel sizeToFit]; + +// heightOffset = heightOffset + self.infoLabel.frame.size.height + 5; + + heightOffset = 130 + textLineheight + 10; + + // SETUP ACTION BUTTON + NSString *status = [self.issue getStatus]; + if ([status isEqualToString:@"remote"] || [status isEqualToString:@"purchasable"] || [status isEqualToString:@"purchased"]) { + self.actionButton.frame = CGRectMake(ui.contentOffset, heightOffset, 110, 30); + } else if ([status isEqualToString:@"downloaded"] || [status isEqualToString:@"bundled"]) { + self.actionButton.frame = CGRectMake(ui.contentOffset, heightOffset, 80, 30); + } + self.actionButton.titleLabel.font = actionFont; + + // SETUP ARCHIVE BUTTON + self.archiveButton.frame = CGRectMake(ui.contentOffset + 80 + 10, heightOffset, 80, 30); + self.archiveButton.titleLabel.font = archiveFont; + + // SETUP DOWN/LOADING SPINNER AND LABEL + self.spinner.frame = CGRectMake(ui.contentOffset, heightOffset, 30, 30); + self.loadingLabel.frame = CGRectMake(ui.contentOffset + self.spinner.frame.size.width + 10, heightOffset, 135, 30); + self.loadingLabel.font = actionFont; + +// heightOffset = heightOffset + self.loadingLabel.frame.size.height + 5; + + // SETUP PROGRESS BAR + self.progressBar.frame = CGRectMake(ui.contentOffset, 136, labelWidth, 30); +} + +- (void)preferredContentSizeChanged:(NSNotification*)notification { + [self refreshContentWithCache:YES]; +} + +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + [self refresh]; +} + +- (void)refresh { + [self refresh:[self.issue getStatus]]; +} + +- (void)refresh:(NSString*)status { + [self refresh:[self.issue getStatus] cache:YES]; +} + +- (void)refreshWithCache:(BOOL)cache { + [self refresh:[self.issue getStatus] cache:cache]; +} + +- (void)refresh:(NSString*)status cache:(BOOL)cache { + // NSLog(@"[BakerShelf] Shelf UI - Refreshing %@ item with status from <%@> to <%@>", self.issue.ID, self.currentStatus, status); + if ([status isEqualToString:@"remote"]) { + [self.actionButton setTitle:NSLocalizedString(@"FREE_TEXT", nil) forState:UIControlStateNormal]; + [self.spinner stopAnimating]; + + self.actionButton.hidden = NO; + self.archiveButton.hidden = YES; + self.progressBar.hidden = YES; + self.loadingLabel.hidden = YES; + } else if ([status isEqualToString:@"connecting"]) { + NSLog(@"[BakerShelf] '%@' is Connecting...", self.issue.ID); + [self.spinner startAnimating]; + + self.actionButton.hidden = YES; + self.archiveButton.hidden = YES; + self.progressBar.progress = 0; + self.loadingLabel.text = NSLocalizedString(@"CONNECTING_TEXT", nil); + self.loadingLabel.hidden = NO; + self.progressBar.hidden = YES; + } else if ([status isEqualToString:@"downloading"]) { + NSLog(@"[BakerShelf] '%@' is Downloading...", self.issue.ID); + [self.spinner startAnimating]; + + self.actionButton.hidden = YES; + self.archiveButton.hidden = YES; + self.progressBar.progress = 0; + self.loadingLabel.text = NSLocalizedString(@"DOWNLOADING_TEXT", nil); + self.loadingLabel.hidden = NO; + self.progressBar.hidden = NO; + } else if ([status isEqualToString:@"downloaded"]) { + NSLog(@"[BakerShelf] '%@' is Ready to be Read.", self.issue.ID); + [self.actionButton setTitle:NSLocalizedString(@"ACTION_DOWNLOADED_TEXT", nil) forState:UIControlStateNormal]; + [self.spinner stopAnimating]; + + self.actionButton.hidden = NO; + self.archiveButton.hidden = NO; + self.loadingLabel.hidden = YES; + self.progressBar.hidden = YES; + } else if ([status isEqualToString:@"bundled"]) { + [self.actionButton setTitle:NSLocalizedString(@"ACTION_DOWNLOADED_TEXT", nil) forState:UIControlStateNormal]; + [self.spinner stopAnimating]; + + self.actionButton.hidden = NO; + self.archiveButton.hidden = YES; + self.loadingLabel.hidden = YES; + self.progressBar.hidden = YES; + } else if ([status isEqualToString:@"opening"]) { + [self.spinner startAnimating]; + + self.actionButton.hidden = YES; + self.archiveButton.hidden = YES; + self.loadingLabel.text = NSLocalizedString(@"OPENING_TEXT", nil); + self.loadingLabel.hidden = NO; + self.progressBar.hidden = YES; + } else if ([status isEqualToString:@"purchasable"]) { + [self.actionButton setTitle:self.issue.price forState:UIControlStateNormal]; + [self.spinner stopAnimating]; + + self.actionButton.hidden = NO; + self.archiveButton.hidden = YES; + self.progressBar.hidden = YES; + self.loadingLabel.hidden = YES; + } else if ([status isEqualToString:@"purchasing"]) { + NSLog(@"[BakerShelf] '%@' is being Purchased...", self.issue.ID); + [self.spinner startAnimating]; + + self.loadingLabel.text = NSLocalizedString(@"BUYING_TEXT", nil); + + self.actionButton.hidden = YES; + self.archiveButton.hidden = YES; + self.progressBar.hidden = YES; + self.loadingLabel.hidden = NO; + } else if ([status isEqualToString:@"purchased"]) { + NSLog(@"[BakerShelf] '%@' is Purchased.", self.issue.ID); + + [self.actionButton setTitle:NSLocalizedString(@"ACTION_REMOTE_TEXT", nil) forState:UIControlStateNormal]; + [self.spinner stopAnimating]; + + self.actionButton.hidden = NO; + self.archiveButton.hidden = YES; + self.progressBar.hidden = YES; + self.loadingLabel.hidden = YES; + } else if ([status isEqualToString:@"unpriced"]) { + [self.spinner startAnimating]; + + self.loadingLabel.text = NSLocalizedString(@"RETRIEVING_TEXT", nil); + + self.actionButton.hidden = YES; + self.archiveButton.hidden = YES; + self.progressBar.hidden = YES; + self.loadingLabel.hidden = NO; + } + + [self refreshContentWithCache:cache]; + + self.currentStatus = status; +} + +#pragma mark - Issue management + +- (void)actionButtonPressed:(UIButton*)sender { + NSString *status = [self.issue getStatus]; + if ([status isEqualToString:@"remote"] || [status isEqualToString:@"purchased"]) { + if ([BKRSettings sharedSettings].isNewsstand) { + [[NSNotificationCenter defaultCenter] postNotificationName:@"BakerIssueDownload" object:self]; // -> Baker Analytics Event + [self download]; + } + } else if ([status isEqualToString:@"downloaded"] || [status isEqualToString:@"bundled"]) { + [[NSNotificationCenter defaultCenter] postNotificationName:@"BakerIssueOpen" object:self]; // -> Baker Analytics Event + [self read]; + } else if ([status isEqualToString:@"downloading"]) { + // TODO: assuming it is supported by NewsstandKit, implement a "Cancel" operation + } else if ([status isEqualToString:@"purchasable"]) { + if ([BKRSettings sharedSettings].isNewsstand) { + [[NSNotificationCenter defaultCenter] postNotificationName:@"BakerIssuePurchase" object:self]; // -> Baker Analytics Event + [self buy]; + } + } +} + +- (void)download { + [self.issue download]; +} + +- (void)buy { + [self addPurchaseObserver:@selector(handleIssuePurchased:) name:@"notification_issue_purchased"]; + [self addPurchaseObserver:@selector(handleIssuePurchaseFailed:) name:@"notification_issue_purchase_failed"]; + + if (![purchasesManager purchase:self.issue.productID]) { + // Still retrieving SKProduct: delay purchase + purchaseDelayed = YES; + + [self removePurchaseObserver:@"notification_issue_purchased"]; + [self removePurchaseObserver:@"notification_issue_purchase_failed"]; + + [purchasesManager retrievePriceFor:self.issue.productID]; + + self.issue.transientStatus = BakerIssueTransientStatusUnpriced; + [self refresh]; + } else { + self.issue.transientStatus = BakerIssueTransientStatusPurchasing; + [self refresh]; + } +} + +- (void)handleIssuePurchased:(NSNotification*)notification { + SKPaymentTransaction *transaction = notification.userInfo[@"transaction"]; + + if ([transaction.payment.productIdentifier isEqualToString:self.issue.productID]) { + + [self removePurchaseObserver:@"notification_issue_purchased"]; + [self removePurchaseObserver:@"notification_issue_purchase_failed"]; + + [purchasesManager markAsPurchased:transaction.payment.productIdentifier]; + + if ([purchasesManager finishTransaction:transaction]) { + if (!transaction.originalTransaction) { + // Do not show alert on restoring a transaction + [BKRUtils showAlertWithTitle:NSLocalizedString(@"ISSUE_PURCHASE_SUCCESSFUL_TITLE", nil) + message:[NSString stringWithFormat:NSLocalizedString(@"ISSUE_PURCHASE_SUCCESSFUL_MESSAGE", nil), self.issue.title] + buttonTitle:NSLocalizedString(@"ISSUE_PURCHASE_SUCCESSFUL_CLOSE", nil)]; + } + } else { + [BKRUtils showAlertWithTitle:NSLocalizedString(@"TRANSACTION_RECORDING_FAILED_TITLE", nil) + message:NSLocalizedString(@"TRANSACTION_RECORDING_FAILED_MESSAGE", nil) + buttonTitle:NSLocalizedString(@"TRANSACTION_RECORDING_FAILED_CLOSE", nil)]; + } + + self.issue.transientStatus = BakerIssueTransientStatusNone; + + [purchasesManager retrievePurchasesFor:[NSSet setWithObject:self.issue.productID] withCallback:^(NSDictionary *purchases) { + [self refresh]; + }]; + } +} + +- (void)handleIssuePurchaseFailed:(NSNotification*)notification { + SKPaymentTransaction *transaction = (notification.userInfo)[@"transaction"]; + + if ([transaction.payment.productIdentifier isEqualToString:self.issue.productID]) { + // Show an error, unless it was the user who cancelled the transaction + if (transaction.error.code != SKErrorPaymentCancelled) { + [BKRUtils showAlertWithTitle:NSLocalizedString(@"ISSUE_PURCHASE_FAILED_TITLE", nil) + message:[transaction.error localizedDescription] + buttonTitle:NSLocalizedString(@"ISSUE_PURCHASE_FAILED_CLOSE", nil)]; + } + + [self removePurchaseObserver:@"notification_issue_purchased"]; + [self removePurchaseObserver:@"notification_issue_purchase_failed"]; + + self.issue.transientStatus = BakerIssueTransientStatusNone; + [self refresh]; + } +} + +- (void)handleIssueRestored:(NSNotification*)notification { + SKPaymentTransaction *transaction = (notification.userInfo)[@"transaction"]; + + if ([transaction.payment.productIdentifier isEqualToString:self.issue.productID]) { + [purchasesManager markAsPurchased:transaction.payment.productIdentifier]; + + if (![purchasesManager finishTransaction:transaction]) { + NSLog(@"[BakerShelf] Could not confirm purchase restore with remote server for %@", transaction.payment.productIdentifier); + } + + self.issue.transientStatus = BakerIssueTransientStatusNone; + [self refresh]; + } +} + +- (void)setPrice:(NSString*)price { + self.issue.price = price; + if (purchaseDelayed) { + purchaseDelayed = NO; + [self buy]; + } else { + [self refresh]; + } +} + +- (void)read { + self.issue.transientStatus = BakerIssueTransientStatusOpening; + [self refresh]; + [[NSNotificationCenter defaultCenter] postNotificationName:@"read_issue_request" object:self]; +} + +#pragma mark - Newsstand download management + +- (void)handleDownloadStarted:(NSNotification*)notification { + [self refresh]; +} + +- (void)handleDownloadProgressing:(NSNotification*)notification { + float bytesWritten = [(notification.userInfo)[@"totalBytesWritten"] floatValue]; + float bytesExpected = [(notification.userInfo)[@"expectedTotalBytes"] floatValue]; + + if ([self.currentStatus isEqualToString:@"connecting"]) { + self.issue.transientStatus = BakerIssueTransientStatusDownloading; + [self refresh]; + } + [self.progressBar setProgress:(bytesWritten / bytesExpected) animated:YES]; +} + +- (void)handleDownloadFinished:(NSNotification*)notification { + self.issue.transientStatus = BakerIssueTransientStatusNone; + [self refresh]; +} + +- (void)handleDownloadError:(NSNotification*)notification { + [BKRUtils showAlertWithTitle:NSLocalizedString(@"DOWNLOAD_FAILED_TITLE", nil) + message:NSLocalizedString(@"DOWNLOAD_FAILED_MESSAGE", nil) + buttonTitle:NSLocalizedString(@"DOWNLOAD_FAILED_CLOSE", nil)]; + + self.issue.transientStatus = BakerIssueTransientStatusNone; + [self refresh]; +} + +- (void)handleUnzipError:(NSNotification*)notification { + [BKRUtils showAlertWithTitle:NSLocalizedString(@"UNZIP_FAILED_TITLE", nil) + message:NSLocalizedString(@"UNZIP_FAILED_MESSAGE", nil) + buttonTitle:NSLocalizedString(@"UNZIP_FAILED_CLOSE", nil)]; + + self.issue.transientStatus = BakerIssueTransientStatusNone; + [self refresh]; +} + +#pragma mark - Newsstand archive management + +- (void)archiveButtonPressed:(UIButton*)sender { + UIAlertView *updateAlert = [[UIAlertView alloc] initWithTitle:NSLocalizedString(@"ARCHIVE_ALERT_TITLE", nil) + message:NSLocalizedString(@"ARCHIVE_ALERT_MESSAGE", nil) + delegate:self + cancelButtonTitle:NSLocalizedString(@"ARCHIVE_ALERT_BUTTON_CANCEL", nil) + otherButtonTitles:NSLocalizedString(@"ARCHIVE_ALERT_BUTTON_OK", nil), nil + ]; + [updateAlert show]; +} + +- (void)alertView:(UIAlertView*)alertView clickedButtonAtIndex:(NSInteger)buttonIndex { + if (buttonIndex == 1){ + [[NSNotificationCenter defaultCenter] postNotificationName:@"BakerIssueArchive" object:self]; // -> Baker Analytics Event + + NKLibrary *nkLib = [NKLibrary sharedLibrary]; + NKIssue *nkIssue = [nkLib issueWithName:self.issue.ID]; + NSString *name = nkIssue.name; + NSDate *date = nkIssue.date; + + [nkLib removeIssue:nkIssue]; + + nkIssue = [nkLib addIssueWithName:name date:date]; + self.issue.path = [[nkIssue contentURL] path]; + + [self refresh]; + } +} + +#pragma mark - Helper methods + +- (void)addPurchaseObserver:(SEL)notificationSelector name:(NSString*)notificationName { + if ([BKRSettings sharedSettings].isNewsstand) { + [[NSNotificationCenter defaultCenter] addObserver:self + selector:notificationSelector + name:notificationName + object:purchasesManager]; + } +} + +- (void)removePurchaseObserver:(NSString*)notificationName { + if ([BKRSettings sharedSettings].isNewsstand) { + [[NSNotificationCenter defaultCenter] removeObserver:self + name:notificationName + object:purchasesManager]; + } +} + +- (void)addIssueObserver:(SEL)notificationSelector name:(NSString*)notificationName { + if ([BKRSettings sharedSettings].isNewsstand) { + [[NSNotificationCenter defaultCenter] addObserver:self + selector:notificationSelector + name:notificationName + object:nil]; + } +} + ++ (UI)getIssueContentMeasures { + if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) { + UI iPad = { + .cellPadding = 30, + .thumbWidth = 135, + .thumbHeight = 180, + .contentOffset = 184 + }; + return iPad; + } else { + UI iPhone = { + .cellPadding = 22, + .thumbWidth = 87, + .thumbHeight = 116, + .contentOffset = 128 + }; + return iPhone; + } +} + ++ (int)getIssueCellHeight { + if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) { + return 240; + } else { + return 190; + } +} + ++ (CGSize)getIssueCellSizeForOrientation:(UIInterfaceOrientation)orientation { + + CGFloat screenWidth = [[UIScreen mainScreen] bkrWidthForOrientation:orientation]; + int cellHeight = [BKRIssueViewController getIssueCellHeight]; + + if (screenWidth > 700) { + return CGSizeMake(screenWidth/2, cellHeight); + } else { + return CGSizeMake(screenWidth, cellHeight); + } + + /* + CGRect screenRect = [UIScreen mainScreen].bounds; + CGFloat screenWidth = screenRect.size.width; + if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) { + return CGSizeMake(screenWidth / 2, [IssueViewController getIssueCellHeight]); + //return CGSizeMake((MIN(screenRect.size.width, screenRect.size.height) - 10) / 2, [IssueViewController getIssueCellHeight]); + } else { + if (screenWidth > 700) { + return CGSizeMake((screenWidth - 10) / 2, [IssueViewController getIssueCellHeight]); + } else { + return CGSizeMake(MIN(screenRect.size.width, screenRect.size.height) - 10, [IssueViewController getIssueCellHeight]); + } + } + */ + +} + +@end diff --git a/BakerShelf/BKRIssuesManager.h b/BakerShelf/BKRIssuesManager.h new file mode 100644 index 0000000..1fcf8c6 --- /dev/null +++ b/BakerShelf/BKRIssuesManager.h @@ -0,0 +1,53 @@ +// +// IssuesManager.h +// Baker +// +// ========================================================================================== +// +// Copyright (c) 2010-2013, Davide Casali, Marco Colombo, Alessandro Morandi +// Copyright (c) 2014, Andrew Krowczyk, Cédric Mériau, Pieter Claerhout +// All rights reserved. +// +// 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. +// Neither the name of the Baker Framework nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written +// permission. +// 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. +// + +#import +#import +#import "BKRIssue.h" + +@interface BKRIssuesManager : NSObject + +@property (nonatomic, copy) NSArray *issues; +@property (nonatomic, strong) NSString *shelfManifestPath; +@property (nonatomic, copy) NSArray *categories; + +#pragma mark - Singleton + ++ (BKRIssuesManager*)sharedInstance; + +- (void)refresh:(void (^)(BOOL))callback; +- (NSSet*)productIDs; +- (BOOL)hasProductIDs; +- (BKRIssue*)latestIssue; ++ (NSArray*)localBooksList; + +@end diff --git a/BakerShelf/BKRIssuesManager.m b/BakerShelf/BKRIssuesManager.m new file mode 100644 index 0000000..bdcc2fa --- /dev/null +++ b/BakerShelf/BKRIssuesManager.m @@ -0,0 +1,245 @@ +// +// IssuesManager.m +// Baker +// +// ========================================================================================== +// +// Copyright (c) 2010-2013, Davide Casali, Marco Colombo, Alessandro Morandi +// Copyright (c) 2014, Andrew Krowczyk, Cédric Mériau, Pieter Claerhout +// All rights reserved. +// +// 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. +// Neither the name of the Baker Framework nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written +// permission. +// 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. +// + +#import "BKRIssuesManager.h" +#import "BKRIssue.h" +#import "BKRUtils.h" +#import "BKRBakerAPI.h" +#import "BKRSettings.h" +#import "NSObject+BakerExtensions.h" + +@implementation BKRIssuesManager + +- (id)init { + self = [super init]; + + if (self) { + _issues = nil; + _shelfManifestPath = [self.bkrCachePath stringByAppendingPathComponent:@"shelf.json"]; + } + + return self; +} + +#pragma mark - Singleton + ++ (BKRIssuesManager*)sharedInstance { + static dispatch_once_t once; + static BKRIssuesManager *sharedInstance; + dispatch_once(&once, ^{ + sharedInstance = [[self alloc] init]; + }); + return sharedInstance; +} + +- (void)refresh:(void (^)(BOOL))callback { + [self getShelfJSON:^(NSData *json) { + if (json) { + NSError* error = nil; + NSArray* jsonArr = [NSJSONSerialization JSONObjectWithData:json + options:0 + error:&error]; + + [self updateNewsstandIssuesList:jsonArr]; + + NSMutableArray *tmpIssues = [NSMutableArray array]; + NSMutableArray *tmpCategories = [NSMutableArray array]; + [jsonArr enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { + BKRIssue *issue = [[BKRIssue alloc] initWithIssueData:obj]; + + // Append categories + [issue.categories enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { + if(![tmpCategories containsObject:obj]) { + [tmpCategories addObject:obj]; + } + }]; + + // Add issue to temporary issue list + [tmpIssues addObject:issue]; + }]; + + // Sort categories + [tmpCategories sortUsingSelector:@selector(compare:)]; + _categories = tmpCategories; + + // Sort issues + self.issues = [tmpIssues sortedArrayUsingComparator:^NSComparisonResult(id a, id b) { + NSDate *first = [BKRUtils dateWithFormattedString:[(BKRIssue*)a date]]; + NSDate *second = [BKRUtils dateWithFormattedString:[(BKRIssue*)b date]]; + return [second compare:first]; + }]; + + [self updateNewsstandIcon]; + + if (callback) { + callback(YES); + } + } + else { + if (callback) { + callback(NO); + } + } + }]; +} + +- (void)updateNewsstandIcon { + + if ([BKRSettings sharedSettings].newsstandLatestIssueCover) { + BKRIssue *latestIssue = nil; + for (BKRIssue *issue in self.issues) { + if ([[issue getStatus] isEqualToString:@"downloaded"]) { + return; + } + latestIssue = issue; + break; + } + + NSLog(@"Setting newsstand cover icon from latest issue: %@ at %@", latestIssue.title, latestIssue.date); + [latestIssue getCoverWithCache:YES andBlock:^(UIImage *image) { + if (image) { + [[UIApplication sharedApplication] setNewsstandIconImage:image]; + } + }]; + } else { + UIImage *image = [UIImage imageNamed:@"newsstand-app-icon"]; + [[UIApplication sharedApplication] setNewsstandIconImage:image]; + } + +} + +- (void)getShelfJSON:(void(^)(NSData*))callback { + BKRBakerAPI *api = [BKRBakerAPI sharedInstance]; + [api getShelfJSON:^(NSData *json) { + NSError *cachedShelfError = nil; + + if (json) { + // Cache the shelf manifest + [[NSFileManager defaultManager] createFileAtPath:self.shelfManifestPath contents:nil attributes:nil]; + NSError *error = nil; + [json writeToFile:self.shelfManifestPath + options:NSDataWritingAtomic + error:&error]; + if (cachedShelfError) { + NSLog(@"[BakerShelf] ERROR: Unable to cache 'shelf.json' manifest: %@", cachedShelfError); + } + } else { + if ([[NSFileManager defaultManager] fileExistsAtPath:self.shelfManifestPath]) { + NSLog(@"[BakerShelf] Loading cached Shelf manifest from %@", self.shelfManifestPath); + json = [NSData dataWithContentsOfFile:self.shelfManifestPath options:NSDataReadingMappedIfSafe error:&cachedShelfError]; + if (cachedShelfError) { + NSLog(@"[BakerShelf] Error loading cached copy of 'shelf.json': %@", cachedShelfError); + } + } else { + NSLog(@"[BakerShelf] No cached 'shelf.json' manifest found at %@", self.shelfManifestPath); + json = nil; + } + } + + if (callback) { + callback(json); + }; + }]; +} + +- (void)updateNewsstandIssuesList:(NSArray*)issuesList { + NKLibrary *nkLib = [NKLibrary sharedLibrary]; + NSMutableArray *discardedIssues = [NSMutableArray arrayWithArray:[nkLib issues]]; + + for (NSDictionary *issue in issuesList) { + NSDate *date = [BKRUtils dateWithFormattedString:issue[@"date"]]; + NSString *name = issue[@"name"]; + + NKIssue *nkIssue = [nkLib issueWithName:name]; + if(!nkIssue) { + // Add issue to Newsstand Library + @try { + nkIssue = [nkLib addIssueWithName:name date:date]; + NSLog(@"[BakerShelf] Newsstand - Added %@ %@", name, date); + } @catch (NSException *exception) { + NSLog(@"[BakerShelf] ERROR: Exception %@", exception); + } + } else { + // Issue already in Newsstand Library + [discardedIssues removeObject:nkIssue]; + } + } + + for (NKIssue *discardedIssue in discardedIssues) { + [nkLib removeIssue:discardedIssue]; + NSLog(@"[BakerShelf] Newsstand - Removed %@", discardedIssue.name); + } +} + +- (NSSet*)productIDs { + NSMutableSet *set = [NSMutableSet set]; + for (BKRIssue *issue in self.issues) { + if (issue.productID) { + [set addObject:issue.productID]; + } + } + return set; +} + +- (BOOL)hasProductIDs { + return self.productIDs.count > 0; +} + +- (BKRIssue*)latestIssue { + return self.issues[0]; +} + ++ (NSArray*)localBooksList { + NSMutableArray *booksList = [NSMutableArray array]; + NSFileManager *localFileManager = [NSFileManager defaultManager]; + NSString *booksDir = [[NSBundle mainBundle] pathForResource:@"books" ofType:nil]; + + NSArray *dirContents = [localFileManager contentsOfDirectoryAtPath:booksDir error:nil]; + for (NSString *file in dirContents) { + NSString *manifestFile = [booksDir stringByAppendingPathComponent:[file stringByAppendingPathComponent:@"book.json"]]; + if ([localFileManager fileExistsAtPath:manifestFile]) { + BKRBook *book = [[BKRBook alloc] initWithBookPath:[booksDir stringByAppendingPathComponent:file] bundled:YES]; + if (book) { + BKRIssue *issue = [[BKRIssue alloc] initWithBakerBook:book]; + [booksList addObject:issue]; + } else { + NSLog(@"[BakerShelf] ERROR: Book %@ could not be initialized. Is 'book.json' correct and valid?", file); + } + } else { + NSLog(@"[BakerShelf] ERROR: Cannot find 'book.json'. Is it present? Should be here: %@", manifestFile); + } + } + + return [NSArray arrayWithArray:booksList]; +} + +@end diff --git a/BakerShelf/BKRPurchasesManager.h b/BakerShelf/BKRPurchasesManager.h new file mode 100644 index 0000000..a979735 --- /dev/null +++ b/BakerShelf/BKRPurchasesManager.h @@ -0,0 +1,81 @@ +// +// PurchasesManager.h +// Baker +// +// ========================================================================================== +// +// Copyright (c) 2010-2013, Davide Casali, Marco Colombo, Alessandro Morandi +// Copyright (c) 2014, Andrew Krowczyk, Cédric Mériau, Pieter Claerhout +// All rights reserved. +// +// 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. +// Neither the name of the Baker Framework nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written +// permission. +// 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. +// + +#import +#import + +@interface BKRPurchasesManager : NSObject { + NSMutableDictionary *_purchases; + BOOL _enableProductRequestFailureNotifications; +} + +@property (nonatomic, strong) NSMutableDictionary *products; +@property (nonatomic, strong) NSNumberFormatter *numberFormatter; +@property (nonatomic, assign) BOOL subscribed; + +#pragma mark - Singleton + ++ (BKRPurchasesManager*)sharedInstance; + +#pragma mark - Purchased flag + +- (BOOL)isMarkedAsPurchased:(NSString*)productID; +- (void)markAsPurchased:(NSString*)productID; + +#pragma mark - Prices and display information + +- (void)retrievePricesFor:(NSSet*)productIDs; +- (void)retrievePricesFor:(NSSet*)productIDs andEnableFailureNotifications:(BOOL)enable; + +- (void)retrievePriceFor:(NSString*)productID; +- (void)retrievePriceFor:(NSString*)productID andEnableFailureNotification:(BOOL)enable; + +- (NSString*)priceFor:(NSString*)productID; +- (NSString*)displayTitleFor:(NSString*)productID; + +#pragma mark - Purchases + +- (BOOL)purchase:(NSString*)productID; +- (BOOL)finishTransaction:(SKPaymentTransaction*)transaction; +- (void)restore; +- (void)retrievePurchasesFor:(NSSet*)productIDs withCallback:(void (^)(NSDictionary*))callback; +- (BOOL)isPurchased:(NSString*)productID; + +#pragma mark - Products + +- (SKProduct*)productFor:(NSString*)productID; + +#pragma mark - Subscriptions + +- (BOOL)hasSubscriptions; + +@end diff --git a/BakerShelf/BKRPurchasesManager.m b/BakerShelf/BKRPurchasesManager.m new file mode 100644 index 0000000..ff0b915 --- /dev/null +++ b/BakerShelf/BKRPurchasesManager.m @@ -0,0 +1,395 @@ +// +// PurchasesManager.m +// Baker +// +// ========================================================================================== +// +// Copyright (c) 2010-2013, Davide Casali, Marco Colombo, Alessandro Morandi +// Copyright (c) 2014, Andrew Krowczyk, Cédric Mériau, Pieter Claerhout +// All rights reserved. +// +// 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. +// Neither the name of the Baker Framework nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written +// permission. +// 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. +// + +#import "BKRPurchasesManager.h" +#import "BKRBakerAPI.h" +#import "BKRSettings.h" + +#import "NSData+BakerExtensions.h" +#import "NSMutableURLRequest+BakerExtensions.h" +#import "BKRUtils.h" +#import "NSURL+BakerExtensions.h" + +@implementation BKRPurchasesManager + +- (id)init { + self = [super init]; + + if (self) { + _products = [[NSMutableDictionary alloc] init]; + _subscribed = NO; + + _purchases = [[NSMutableDictionary alloc] init]; + + _numberFormatter = [[NSNumberFormatter alloc] init]; + [_numberFormatter setFormatterBehavior:NSNumberFormatterBehavior10_4]; + [_numberFormatter setNumberStyle:NSNumberFormatterCurrencyStyle]; + + _enableProductRequestFailureNotifications = YES; + } + + return self; +} + +#pragma mark - Singleton + ++ (BKRPurchasesManager*)sharedInstance { + static dispatch_once_t once; + static BKRPurchasesManager *sharedInstance; + dispatch_once(&once, ^{ + sharedInstance = [[self alloc] init]; + }); + return sharedInstance; +} + +#pragma mark - Purchased flag + +- (BOOL)isMarkedAsPurchased:(NSString*)productID { + return [[NSUserDefaults standardUserDefaults] boolForKey:productID]; +} + +- (void)markAsPurchased:(NSString*)productID { + [[NSUserDefaults standardUserDefaults] setBool:YES forKey:productID]; + [[NSUserDefaults standardUserDefaults] synchronize]; +} + +#pragma mark - Prices + +- (void)retrievePricesFor:(NSSet*)productIDs { + [self retrievePricesFor:productIDs andEnableFailureNotifications:YES]; +} +- (void)retrievePricesFor:(NSSet*)productIDs andEnableFailureNotifications:(BOOL)enable { + if ([productIDs count] > 0) { + _enableProductRequestFailureNotifications = enable; + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) { + SKProductsRequest *productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:productIDs]; + productsRequest.delegate = self; + [productsRequest start]; + }); + } +} + +- (void)retrievePriceFor:(NSString*)productID { + [self retrievePriceFor:productID andEnableFailureNotification:YES]; +} +- (void)retrievePriceFor:(NSString*)productID andEnableFailureNotification:(BOOL)enable { + NSSet *productIDs = [NSSet setWithObject:productID]; + [self retrievePricesFor:productIDs andEnableFailureNotifications:enable]; +} + +- (void)productsRequest:(SKProductsRequest*)request didReceiveResponse:(SKProductsResponse*)response { + [self logProducts:response.products]; + + for (NSString *productID in response.invalidProductIdentifiers) { + NSLog(@"Invalid product identifier: %@", productID); + } + + NSMutableSet *ids = [NSMutableSet setWithCapacity:response.products.count]; + for (SKProduct *skProduct in response.products) { + (self.products)[skProduct.productIdentifier] = skProduct; + [ids addObject:skProduct.productIdentifier]; + } + + NSDictionary *userInfo = @{@"ids": ids}; + [[NSNotificationCenter defaultCenter] postNotificationName:@"notification_products_retrieved" object:self userInfo:userInfo]; + +} + +- (void)logProducts:(NSArray*)skProducts { + NSLog(@"Received %lu products from App Store", (unsigned long)[skProducts count]); + for (SKProduct *skProduct in skProducts) { + NSLog(@"- %@", skProduct.productIdentifier); + } +} + +- (void)request:(SKRequest*)request didFailWithError:(NSError*)error { + NSLog(@"App Store request failure: %@", error); + + if (_enableProductRequestFailureNotifications) { + NSDictionary *userInfo = @{@"error": error}; + [[NSNotificationCenter defaultCenter] postNotificationName:@"notification_products_request_failed" object:self userInfo:userInfo]; + } + + // @TODO: REMOVE CODE + NSDictionary *userInfo = @{@"ids": @[]}; + [[NSNotificationCenter defaultCenter] postNotificationName:@"notification_products_retrieved" object:self userInfo:userInfo]; + +} + +- (NSString*)priceFor:(NSString*)productID { + SKProduct *product = self.products[productID]; + if (product) { + [_numberFormatter setLocale:product.priceLocale]; + return [_numberFormatter stringFromNumber:product.price]; + } + return nil; +} + +- (NSString*)displayTitleFor:(NSString*)productID { + + if ([BKRSettings sharedSettings].useiTunesConnectLocalizations) { + SKProduct *product = self.products[productID]; + if(product) { + return product.localizedTitle; + } + } + return NSLocalizedString(productID, nil); +} + +#pragma mark - Purchases + +- (BOOL)purchase:(NSString*)productID { + SKProduct *product = [self productFor:productID]; + if (product) { + SKPayment *payment = [SKPayment paymentWithProduct:product]; + [[SKPaymentQueue defaultQueue] addPayment:payment]; + + return YES; + } else { + NSLog(@"Trying to buy unavailable product %@", productID); + + return NO; + } +} + +- (BOOL)finishTransaction:(SKPaymentTransaction*)transaction { + if ([self recordTransaction:transaction]) { + [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; + return YES; + } else { + return NO; + } +} + +- (BOOL)recordTransaction:(SKPaymentTransaction*)transaction { + [[NSUserDefaults standardUserDefaults] setObject:transaction.transactionIdentifier forKey:@"receipt"]; + + BKRBakerAPI *api = [BKRBakerAPI sharedInstance]; + if ([api canPostPurchaseReceipt]) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + NSString *receipt = [transaction.transactionReceipt bkrBase64EncodedString]; +#pragma clang diagnostic pop +// NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL]; +// NSString *receipt = [[NSData dataWithContentsOfURL:receiptURL] bkrBase64EncodedString]; +// NSLog(@"receipt1: %@", receipt1); +// NSLog(@"receipt: %@", receipt); + + NSString *type = [self transactionType:transaction]; + return [api postPurchaseReceipt:receipt ofType:type]; + } + + return YES; +} + +- (NSString*)transactionType:(SKPaymentTransaction*)transaction { + NSString *productID = transaction.payment.productIdentifier; + if ([productID isEqualToString:[BKRSettings sharedSettings].freeSubscriptionProductId]) { + return @"free-subscription"; + } else if ([[BKRSettings sharedSettings].autoRenewableSubscriptionProductIds containsObject:productID]) { + return @"auto-renewable-subscription"; + } else { + return @"issue"; + } +} + +- (void)retrievePurchasesFor:(NSSet*)productIDs withCallback:(void (^)(NSDictionary*))callback { + BKRBakerAPI *api = [BKRBakerAPI sharedInstance]; + + if ([api canGetPurchasesJSON]) { + [api getPurchasesJSON:^(NSData* jsonResponse) { + if (jsonResponse) { + NSError* error = nil; + NSDictionary *purchasesResponse = [NSJSONSerialization JSONObjectWithData:jsonResponse + options:0 + error:&error]; + // TODO: handle error + + if (purchasesResponse) { + NSArray *purchasedIssues = purchasesResponse[@"issues"]; + self.subscribed = [purchasesResponse[@"subscribed"] boolValue]; + + [productIDs enumerateObjectsUsingBlock:^(id obj, BOOL *stop) { + _purchases[obj] = @([purchasedIssues containsObject:obj]); + }]; + } else { + NSLog(@"ERROR: Could not parse response from purchases API call. Received: %@", jsonResponse); + } + } + + if (callback) { + callback([NSDictionary dictionaryWithDictionary:_purchases]); + } + }]; + } else if (callback) { + callback(nil); + } +} + +- (BOOL)isPurchased:(NSString*)productID { + id purchased = _purchases[productID]; + if (purchased) { + return [purchased boolValue]; + } else { + return [self isMarkedAsPurchased:productID]; + } +} + +#pragma mark - Payment queue + +- (void)paymentQueue:(SKPaymentQueue*)queue updatedTransactions:(NSArray*)transactions { + [self logTransactions:transactions]; + + BOOL isRestoring = NO; + for(SKPaymentTransaction *transaction in transactions) { + switch (transaction.transactionState) { + case SKPaymentTransactionStatePurchasing: + // Nothing to do at the moment + break; + case SKPaymentTransactionStatePurchased: + [self completeTransaction:transaction]; + break; + case SKPaymentTransactionStateFailed: + [self failedTransaction:transaction]; + break; + case SKPaymentTransactionStateRestored: + isRestoring = YES; + [self restoreTransaction:transaction]; + break; + default: + break; + } + } + + if (isRestoring) { + [[NSNotificationCenter defaultCenter] postNotificationName:@"notification_multiple_restores" object:self userInfo:nil]; + } +} + +- (void)logTransactions:(NSArray*)transactions { + NSLog(@"Received %lu transactions from App Store", (unsigned long)[transactions count]); + for(SKPaymentTransaction *transaction in transactions) { + switch (transaction.transactionState) { + case SKPaymentTransactionStatePurchasing: + NSLog(@"- purchasing: %@", transaction.payment.productIdentifier); + break; + case SKPaymentTransactionStatePurchased: + NSLog(@"- purchased: %@", transaction.payment.productIdentifier); + break; + case SKPaymentTransactionStateFailed: + NSLog(@"- failed: %@", transaction.payment.productIdentifier); + break; + case SKPaymentTransactionStateRestored: + NSLog(@"- restored: %@", transaction.payment.productIdentifier); + break; + default: + NSLog(@"- unsupported transaction type: %@", transaction.payment.productIdentifier); + break; + } + } +} + +- (void)completeTransaction:(SKPaymentTransaction*)transaction { + NSDictionary *userInfo = @{@"transaction": transaction}; + NSString *productId = transaction.payment.productIdentifier; + + if ([productId isEqualToString:[BKRSettings sharedSettings].freeSubscriptionProductId] || [[BKRSettings sharedSettings].autoRenewableSubscriptionProductIds containsObject:productId]) { + [[NSNotificationCenter defaultCenter] postNotificationName:@"notification_subscription_purchased" object:self userInfo:userInfo]; + } else if ([self productFor:productId]) { + [[NSNotificationCenter defaultCenter] postNotificationName:@"notification_issue_purchased" object:self userInfo:userInfo]; + } else { + NSLog(@"ERROR: Completed transaction for %@, which is not a Product ID this app recognises", productId); + } +} + +- (void)restoreTransaction:(SKPaymentTransaction*)transaction { + NSDictionary *userInfo = @{@"transaction": transaction}; + NSString *productId = transaction.payment.productIdentifier; + + if ([productId isEqualToString:[BKRSettings sharedSettings].freeSubscriptionProductId] || [[BKRSettings sharedSettings].autoRenewableSubscriptionProductIds containsObject:productId]) { + [[NSNotificationCenter defaultCenter] postNotificationName:@"notification_subscription_restored" object:self userInfo:userInfo]; + } else if ([self productFor:productId]) { + [[NSNotificationCenter defaultCenter] postNotificationName:@"notification_issue_restored" object:self userInfo:userInfo]; + } else { + NSLog(@"ERROR: Trying to restore %@, which is not a Product ID this app recognises", productId); + [[NSNotificationCenter defaultCenter] postNotificationName:@"notification_restored_issue_not_recognised" object:self userInfo:userInfo]; + } +} + +- (void)failedTransaction:(SKPaymentTransaction*)transaction { + NSLog(@"Payment transaction failure: %@", transaction.error); + + NSDictionary *userInfo = @{@"transaction": transaction}; + NSString *productId = transaction.payment.productIdentifier; + + if ([productId isEqualToString:[BKRSettings sharedSettings].freeSubscriptionProductId] || [[BKRSettings sharedSettings].autoRenewableSubscriptionProductIds containsObject:productId]) { + [[NSNotificationCenter defaultCenter] postNotificationName:@"notification_subscription_failed" object:self userInfo:userInfo]; + } else { + [[NSNotificationCenter defaultCenter] postNotificationName:@"notification_issue_purchase_failed" object:self userInfo:userInfo]; + } + + [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; +} + +- (void)restore { + [[SKPaymentQueue defaultQueue] restoreCompletedTransactions]; +} + +- (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue*)queue { + [[NSNotificationCenter defaultCenter] postNotificationName:@"notification_restore_finished" object:self userInfo:nil]; +} + +- (void)paymentQueue:(SKPaymentQueue*)queue restoreCompletedTransactionsFailedWithError:(NSError*)error { + NSLog(@"Transaction restore failure: %@", error); + + NSDictionary *userInfo = @{@"error": error}; + + [[NSNotificationCenter defaultCenter] postNotificationName:@"notification_restore_failed" object:self userInfo:userInfo]; +} + +#pragma mark - Products + +- (SKProduct*)productFor:(NSString*)productID { + return (self.products)[productID]; +} + +#pragma mark - Subscriptions + +- (BOOL)hasSubscriptions { + return [[BKRSettings sharedSettings].freeSubscriptionProductId length] > 0 || [[BKRSettings sharedSettings].autoRenewableSubscriptionProductIds count] > 0; +} + +#pragma mark - Memory management + + +@end diff --git a/BakerShelf/BKRShelfHeaderView.h b/BakerShelf/BKRShelfHeaderView.h new file mode 100644 index 0000000..181889f --- /dev/null +++ b/BakerShelf/BKRShelfHeaderView.h @@ -0,0 +1,38 @@ +// +// BakerAPI.h +// Baker +// +// ========================================================================================== +// +// Copyright (c) 2014, Pieter Claerhout +// All rights reserved. +// +// 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. +// Neither the name of the Baker Framework nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written +// permission. +// 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. +// + +#import + +@interface BKRShelfHeaderView : UICollectionReusableView + +@property (nonatomic, strong) UIImageView *headerImage; + +@end diff --git a/BakerShelf/BKRShelfHeaderView.m b/BakerShelf/BKRShelfHeaderView.m new file mode 100644 index 0000000..f2c7505 --- /dev/null +++ b/BakerShelf/BKRShelfHeaderView.m @@ -0,0 +1,59 @@ +// +// BakerAPI.h +// Baker +// +// ========================================================================================== +// +// Copyright (c) 2014, Pieter Claerhout +// All rights reserved. +// +// 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. +// Neither the name of the Baker Framework nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written +// permission. +// 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. +// + +#import "BKRShelfHeaderView.h" +#import "BKRSettings.h" +#import "BKRUtils.h" + +@implementation BKRShelfHeaderView + +- (id)initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + if (self) { + + _headerImage = [[UIImageView alloc] initWithFrame:self.frame]; + _headerImage.autoresizingMask = UIViewAutoresizingFlexibleHeight|UIViewAutoresizingFlexibleWidth; + [_headerImage setClipsToBounds:YES]; + _headerImage.image = [UIImage imageNamed:@"shelf-header"]; + if([[BKRSettings sharedSettings].issuesShelfOptions[@"headerImageFill"] boolValue] == TRUE) { + _headerImage.contentMode = UIViewContentModeScaleAspectFill; + }else{ + _headerImage.contentMode = UIViewContentModeScaleAspectFit; + } + _headerImage.backgroundColor = [BKRUtils colorWithHexString:[BKRSettings sharedSettings].issuesShelfOptions[@"headerBackgroundColor"]]; + + [self addSubview:_headerImage]; + + } + return self; +} + +@end diff --git a/BakerShelf/BKRShelfStatus.h b/BakerShelf/BKRShelfStatus.h new file mode 100644 index 0000000..702c9a2 --- /dev/null +++ b/BakerShelf/BKRShelfStatus.h @@ -0,0 +1,45 @@ +// +// ShelfStatus.h +// Baker +// +// ========================================================================================== +// +// Copyright (c) 2010-2013, Davide Casali, Marco Colombo, Alessandro Morandi +// Copyright (c) 2014, Andrew Krowczyk, Cédric Mériau, Pieter Claerhout +// All rights reserved. +// +// 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. +// Neither the name of the Baker Framework nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written +// permission. +// 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. +// + +#import +#import "BKRJSONStatus.h" + +@interface BKRShelfStatus : BKRJSONStatus + +@property (nonatomic, strong) NSMutableDictionary *prices; + +- (id)init; +- (void)save; +- (NSString*)priceFor:(NSString*)productID; +- (void)setPrice:(NSString*)price for:(NSString*)productID; + +@end diff --git a/BakerShelf/BKRShelfStatus.m b/BakerShelf/BKRShelfStatus.m new file mode 100644 index 0000000..5b5b0de --- /dev/null +++ b/BakerShelf/BKRShelfStatus.m @@ -0,0 +1,72 @@ +// +// ShelfStatus.m +// Baker +// +// ========================================================================================== +// +// Copyright (c) 2010-2013, Davide Casali, Marco Colombo, Alessandro Morandi +// Copyright (c) 2014, Andrew Krowczyk, Cédric Mériau +// All rights reserved. +// +// 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. +// Neither the name of the Baker Framework nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written +// permission. +// 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. +// + +#import "BKRShelfStatus.h" +#import "NSObject+BakerExtensions.h" + +@implementation BKRShelfStatus + +- (id)init { + NSString *statusPath = [[self.bkrCachePath stringByAppendingPathComponent:@"shelf-status"] stringByAppendingPathExtension:@"json"]; + + self = [super initWithJSONPath:statusPath]; + if (self) { + _prices = [[NSMutableDictionary alloc] init]; + } + return self; +} + +- (NSDictionary*)load { + NSDictionary *jsonDict = [super load]; + + NSDictionary *jsonPrices = jsonDict[@"prices"]; + [jsonPrices enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + [self setPrice:obj for:key]; + }]; + + return jsonDict; +} + +- (void)save { + NSDictionary *jsonDict = @{@"prices": self.prices}; + [super save:jsonDict]; +} + +- (NSString*)priceFor:(NSString*)productID { + return self.prices[productID]; +} + +- (void)setPrice:(NSString*)price for:(NSString*)productID { + self.prices[productID] = price; +} + +@end diff --git a/BakerShelf/BKRShelfViewController.h b/BakerShelf/BKRShelfViewController.h new file mode 100644 index 0000000..a94a12b --- /dev/null +++ b/BakerShelf/BKRShelfViewController.h @@ -0,0 +1,99 @@ +// +// ShelfViewController.h +// Baker +// +// ========================================================================================== +// +// Copyright (c) 2010-2013, Davide Casali, Marco Colombo, Alessandro Morandi +// Copyright (c) 2014, Andrew Krowczyk, Cédric Mériau, Pieter Claerhout +// All rights reserved. +// +// 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. +// Neither the name of the Baker Framework nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written +// permission. +// 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. +// + +#import +#import + +#import "BKRIssue.h" +#import "BKRIssuesManager.h" +#import "BKRShelfStatus.h" +#import "BKRBakerAPI.h" +#import "BKRPurchasesManager.h" +#import "BKRCategoryFilterItem.h" + +@class BKRShelfHeaderView; + +@interface BKRShelfViewController : UIViewController { + BKRBakerAPI *api; + BKRIssuesManager *issuesManager; + NSMutableArray *notRecognisedTransactions; + UIPopoverController *infoPopover; + BKRPurchasesManager *purchasesManager; + UIInterfaceOrientation realInterfaceOrientation; +} + +@property (nonatomic, copy) NSArray *issues; +@property (nonatomic, copy) NSArray *supportedOrientation; + +@property (nonatomic, strong) NSMutableArray *issueViewControllers; +@property (nonatomic, strong) BKRShelfStatus *shelfStatus; + +@property (nonatomic, strong) UICollectionView *gridView; +@property (nonatomic, strong) CAGradientLayer *gradientLayer; +@property (nonatomic, strong) BKRShelfHeaderView *headerView; +@property (nonatomic, strong) UIBarButtonItem *refreshButton; +@property (nonatomic, strong) UIBarButtonItem *subscribeButton; +@property (strong, nonatomic) UIBarButtonItem *infoItem; +@property (strong, nonatomic) BKRCategoryFilterItem *categoryItem; + +@property (nonatomic, strong) UIActionSheet *subscriptionsActionSheet; +@property (nonatomic, strong) NSArray *subscriptionsActionSheetActions; +@property (nonatomic, strong) UIAlertView *blockingProgressView; + +@property (nonatomic, copy) NSString *bookToBeProcessed; + +#pragma mark - Init +- (id)init; +- (id)initWithBooks:(NSArray*)currentBooks; + +#pragma mark - Shelf data source +- (void)handleRefresh:(NSNotification*)notification; + +#pragma mark - Store Kit +- (void)handleSubscription:(NSNotification*)notification; + +#pragma mark - Navigation management +- (void)readIssue:(BKRIssue*)issue; +- (void)handleReadIssue:(NSNotification*)notification; +- (void)receiveBookProtocolNotification:(NSNotification*)notification; +- (void)handleBookToBeProcessed; +- (void)pushViewControllerWithBook:(BKRBook*)book; + +#pragma mark - Buttons management +- (void)setrefreshButtonEnabled:(BOOL)enabled; +- (void)setSubscribeButtonEnabled:(BOOL)enabled; +- (void)handleSubscribeButtonPressed:(NSNotification *)notification; + +#pragma mark - Helper methods +- (int)getBannerHeight; + +@end diff --git a/BakerShelf/BKRShelfViewController.m b/BakerShelf/BKRShelfViewController.m new file mode 100644 index 0000000..5a6fb28 --- /dev/null +++ b/BakerShelf/BKRShelfViewController.m @@ -0,0 +1,848 @@ +// +// ShelfViewController.m +// Baker +// +// ========================================================================================== +// +// Copyright (c) 2010-2013, Davide Casali, Marco Colombo, Alessandro Morandi +// Copyright (c) 2014, Andrew Krowczyk, Cédric Mériau, Pieter Claerhout +// All rights reserved. +// +// 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. +// Neither the name of the Baker Framework nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written +// permission. +// 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. +// + +#import "BKRShelfViewController.h" +#import "BKRCustomNavigationBar.h" + +#import "BKRBookViewController.h" +#import "BKRIssueViewController.h" +#import "BKRShelfHeaderView.h" +#import "BKRShelfViewLayout.h" +#import "BKRSettings.h" + +#import "NSData+BakerExtensions.h" +#import "NSString+BakerExtensions.h" +#import "BKRUtils.h" + +#import "MBProgressHUD.h" + +@interface BKRShelfViewController () + +@property (nonatomic, strong) UICollectionViewFlowLayout *layout; + +@end + +@implementation BKRShelfViewController + +#pragma mark - Init + +- (id)init { + self = [super init]; + if (self) { + if ([BKRSettings sharedSettings].isNewsstand) { + purchasesManager = [BKRPurchasesManager sharedInstance]; + [self addPurchaseObserver:@selector(handleProductsRetrieved:) + name:@"notification_products_retrieved"]; + [self addPurchaseObserver:@selector(handleProductsRequestFailed:) + name:@"notification_products_request_failed"]; + [self addPurchaseObserver:@selector(handleSubscriptionPurchased:) + name:@"notification_subscription_purchased"]; + [self addPurchaseObserver:@selector(handleSubscriptionFailed:) + name:@"notification_subscription_failed"]; + [self addPurchaseObserver:@selector(handleSubscriptionRestored:) + name:@"notification_subscription_restored"]; + [self addPurchaseObserver:@selector(handleRestoreFailed:) + name:@"notification_restore_failed"]; + [self addPurchaseObserver:@selector(handleMultipleRestores:) + name:@"notification_multiple_restores"]; + [self addPurchaseObserver:@selector(handleRestoredIssueNotRecognised:) + name:@"notification_restored_issue_not_recognised"]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(receiveBookProtocolNotification:) + name:@"notification_book_protocol" + object:nil]; + + [[SKPaymentQueue defaultQueue] addTransactionObserver:purchasesManager]; + } + + api = [BKRBakerAPI sharedInstance]; + issuesManager = [BKRIssuesManager sharedInstance]; + notRecognisedTransactions = [[NSMutableArray alloc] init]; + + _shelfStatus = [[BKRShelfStatus alloc] init]; + _issueViewControllers = [[NSMutableArray alloc] init]; + _supportedOrientation = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UISupportedInterfaceOrientations"]; + _bookToBeProcessed = nil; + + if ([BKRSettings sharedSettings].isNewsstand) { + [self handleRefresh:nil]; + } + } + return self; +} + +- (id)initWithBooks:(NSArray*)currentBooks { + self = [self init]; + if (self) { + self.issues = currentBooks; + + NSMutableArray *controllers = [NSMutableArray array]; + for (BKRIssue *issue in self.issues) { + BKRIssueViewController *controller = [self createIssueViewControllerWithIssue:issue]; + [controllers addObject:controller]; + } + self.issueViewControllers = [NSMutableArray arrayWithArray:controllers]; + } + return self; +} + +#pragma mark - View lifecycle + +- (void)viewDidLoad { + [super viewDidLoad]; + + self.navigationItem.title = NSLocalizedString(@"SHELF_NAVIGATION_TITLE", nil); + + self.layout = [[BKRShelfViewLayout alloc] initWithSticky:[[BKRSettings sharedSettings].issuesShelfOptions[@"headerSticky"] boolValue] + stretch:[[BKRSettings sharedSettings].issuesShelfOptions[@"headerStretch"] boolValue]]; + + [self.layout setHeaderReferenceSize:[self getBannerSize]]; + self.layout.minimumInteritemSpacing = 0; + self.layout.minimumLineSpacing = 0; + + self.gridView = [[UICollectionView alloc] initWithFrame:self.view.frame collectionViewLayout:self.layout]; + self.gridView.dataSource = self; + self.gridView.delegate = self; + self.gridView.backgroundColor = [UIColor clearColor]; + self.gridView.autoresizingMask = UIViewAutoresizingFlexibleHeight|UIViewAutoresizingFlexibleWidth; + + [self.gridView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"cellIdentifier"]; + [self.gridView registerClass:[BKRShelfHeaderView class] forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"headerIdentifier"]; + + NSString *backgroundFillStyle = [BKRSettings sharedSettings].issuesShelfOptions[@"backgroundFillStyle"]; + if([backgroundFillStyle isEqualToString:@"Gradient"]) { + self.gradientLayer = [BKRUtils gradientLayerFromHexString:[BKRSettings sharedSettings].issuesShelfOptions[@"backgroundFillGradientStart"] + toHexString:[BKRSettings sharedSettings].issuesShelfOptions[@"backgroundFillGradientStop"]]; + self.gradientLayer.frame = self.gridView.bounds; + self.gridView.backgroundColor = [UIColor clearColor]; + self.gridView.backgroundView = [[UIView alloc] init]; + [self.gridView.backgroundView.layer insertSublayer:self.gradientLayer atIndex:0]; + self.gridView.backgroundColor = [BKRUtils colorWithHexString:[BKRSettings sharedSettings].issuesShelfOptions[@"backgroundFillColor"]]; + }else if([backgroundFillStyle isEqualToString:@"Image"]) { + UIImage *backgroundImage = [UIImage imageNamed:@"shelf-background"]; + UIImageView *backgroundView = [[UIImageView alloc] initWithImage:backgroundImage]; + self.gridView.backgroundView = backgroundView; + }else if([backgroundFillStyle isEqualToString:@"Pattern"]) { + self.gridView.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:@"shelf-background"]]; + }else if([backgroundFillStyle isEqualToString:@"Color"]) { + self.gridView.backgroundColor = [BKRUtils colorWithHexString:[BKRSettings sharedSettings].issuesShelfOptions[@"backgroundFillColor"]]; + } + + [self.view addSubview:self.gridView]; + + [self willRotateToInterfaceOrientation:self.interfaceOrientation duration:0]; + [self.gridView reloadData]; + + if ([BKRSettings sharedSettings].isNewsstand) { + self.refreshButton = [[UIBarButtonItem alloc] + initWithBarButtonSystemItem:UIBarButtonSystemItemRefresh + target:self + action:@selector(handleRefresh:)]; + + self.subscribeButton = [[UIBarButtonItem alloc] + initWithTitle: NSLocalizedString(@"SUBSCRIBE_BUTTON_TEXT", nil) + style:UIBarButtonItemStylePlain + target:self + action:@selector(handleSubscribeButtonPressed:)]; + + self.categoryItem = [[BKRCategoryFilterItem alloc] initWithCategories:issuesManager.categories delegate:self]; + + self.blockingProgressView = [[UIAlertView alloc] + initWithTitle:@"Processing..." + message:@"\n" + delegate:nil + cancelButtonTitle:nil + otherButtonTitles:nil]; + UIActivityIndicatorView *spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge]; + spinner.center = CGPointMake(139.5, 75.5); // .5 so it doesn't blur + [self.blockingProgressView addSubview:spinner]; + [spinner startAnimating]; + + NSMutableSet *subscriptions = [NSMutableSet setWithArray:[BKRSettings sharedSettings].autoRenewableSubscriptionProductIds]; + if ([[BKRSettings sharedSettings].freeSubscriptionProductId length] > 0 && ![purchasesManager isPurchased:[BKRSettings sharedSettings].freeSubscriptionProductId]) { + [subscriptions addObject:[BKRSettings sharedSettings].freeSubscriptionProductId]; + } + [purchasesManager retrievePricesFor:subscriptions andEnableFailureNotifications:NO]; + + } +} + +- (void)viewWillAppear:(BOOL)animated { + + [super viewWillAppear:animated]; + [self.navigationController.navigationBar setTranslucent:NO]; + [self willRotateToInterfaceOrientation:self.interfaceOrientation duration:0]; + + for (BKRIssueViewController *controller in self.issueViewControllers) { + controller.issue.transientStatus = BakerIssueTransientStatusNone; + [controller refresh]; + } + + if ([BKRSettings sharedSettings].isNewsstand) { + + NSMutableArray *buttonItems = [NSMutableArray arrayWithObject:self.refreshButton]; + if ([purchasesManager hasSubscriptions] || [issuesManager hasProductIDs]) { + [buttonItems addObject:self.subscribeButton]; + } + self.navigationItem.leftBarButtonItems = buttonItems; + + // Remove limbo transactions + // take current payment queue + SKPaymentQueue* currentQueue = [SKPaymentQueue defaultQueue]; + // finish ALL transactions in queue + [currentQueue.transactions enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { + [currentQueue finishTransaction:(SKPaymentTransaction*)obj]; + }]; + + [[SKPaymentQueue defaultQueue] addTransactionObserver:purchasesManager]; + } + + // Add info button + UIButton *infoButton = [UIButton buttonWithType:UIButtonTypeInfoLight]; + [infoButton addTarget:self action:@selector(handleInfoButtonPressed:) forControlEvents:UIControlEventTouchUpInside]; + self.infoItem = [[UIBarButtonItem alloc] initWithCustomView:infoButton]; + + // Remove file info.html if you don't want the info button to be added to the shelf navigation bar + NSString *infoPath = [[NSBundle mainBundle] pathForResource:@"info" ofType:@"html" inDirectory:@"info"]; + if ([[NSFileManager defaultManager] fileExistsAtPath:infoPath]) { + self.navigationItem.rightBarButtonItem = self.infoItem; + } +} + +- (void)viewDidAppear:(BOOL)animated { + [super viewDidAppear:animated]; + if (self.bookToBeProcessed) { + [self handleBookToBeProcessed]; + } +} + +- (NSUInteger)supportedInterfaceOrientations { + return UIInterfaceOrientationMaskAll; +} + +- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation { + return [self.supportedOrientation indexOfObject:[NSString bkrStringFromInterfaceOrientation:interfaceOrientation]] != NSNotFound; +} + +- (BOOL)shouldAutorotate { + return YES; +} + +- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration { + realInterfaceOrientation = toInterfaceOrientation; + [super willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration]; +} + +- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration { + [super willAnimateRotationToInterfaceOrientation:toInterfaceOrientation duration:duration]; + + // Update label widths + [self.issueViewControllers enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { + [(BKRIssueViewController*)obj refreshContentWithCache:NO]; + }]; +} + +- (void)viewDidLayoutSubviews { + // Update gradient background (if set) + if(self.gradientLayer) { + [self.gradientLayer setFrame:self.gridView.bounds]; + } + + // Update header size + [self.layout setHeaderReferenceSize:[self getBannerSize]]; + + // Invalidate layout + [self.layout invalidateLayout]; + +} + +- (BKRIssueViewController*)createIssueViewControllerWithIssue:(BKRIssue*)issue { + BKRIssueViewController *controller = [[BKRIssueViewController alloc] initWithBakerIssue:issue]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleReadIssue:) name:@"read_issue_request" object:controller]; + return controller; +} + +- (BOOL)prefersStatusBarHidden { + return NO; +} + +#pragma mark - Shelf data source + +- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView*)collectionView { + return 1; +} + +- (NSInteger)collectionView:(UICollectionView*)collectionView numberOfItemsInSection:(NSInteger)section { + return [self.issueViewControllers count]; +} + +- (UICollectionViewCell*)collectionView:(UICollectionView*)collectionView cellForItemAtIndexPath:(NSIndexPath*)indexPath { + CGSize cellSize = [BKRIssueViewController getIssueCellSizeForOrientation:self.interfaceOrientation]; + CGRect cellFrame = CGRectMake(0, 0, cellSize.width, cellSize.height); + + static NSString *cellIdentifier = @"cellIdentifier"; + UICollectionViewCell* cell = [self.gridView dequeueReusableCellWithReuseIdentifier:cellIdentifier forIndexPath:indexPath]; + if (cell == nil) { + UICollectionViewCell* cell = [[UICollectionViewCell alloc] initWithFrame:cellFrame]; + cell.contentView.backgroundColor = [UIColor clearColor]; + cell.backgroundColor = [UIColor clearColor]; + } + + BKRIssueViewController *controller = (self.issueViewControllers)[indexPath.row]; + UIView *removableIssueView = [cell.contentView viewWithTag:42]; + if (removableIssueView) { + [removableIssueView removeFromSuperview]; + } + [cell.contentView addSubview:controller.view]; + + return cell; +} + + +- (CGSize)collectionView:(UICollectionView*)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath*)indexPath { + return [BKRIssueViewController getIssueCellSizeForOrientation:realInterfaceOrientation]; +} + +- (UICollectionReusableView*)collectionView:(UICollectionView*)collectionView viewForSupplementaryElementOfKind:(NSString*)kind atIndexPath:(NSIndexPath*)indexPath { + if (!self.headerView) { + self.headerView = [collectionView dequeueReusableSupplementaryViewOfKind:kind withReuseIdentifier:@"headerIdentifier" forIndexPath:indexPath]; + } + return self.headerView; +} + +- (CGSize)collectionView:(UICollectionView*)collectionView layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section { + return [self getBannerSize]; +} + +- (void)handleRefresh:(NSNotification*)notification { + [self setrefreshButtonEnabled:NO]; + + MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES]; + hud.labelText = NSLocalizedString(@"Loading", @""); + + [issuesManager refresh:^(BOOL status) { + if(status) { + + // Set dropdown categories + self.categoryItem.categories = issuesManager.categories; + + // Show / Hide category button + if(issuesManager.categories.count == 0) { + self.navigationItem.rightBarButtonItems = [NSArray arrayWithObjects:self.infoItem, nil]; + }else{ + self.navigationItem.rightBarButtonItems = [NSArray arrayWithObjects:self.infoItem, self.categoryItem, nil]; + } + + // Set issues + self.issues = issuesManager.issues; + + // Refresh issue list + [self refreshIssueList]; + + } else { + [BKRUtils showAlertWithTitle:NSLocalizedString(@"INTERNET_CONNECTION_UNAVAILABLE_TITLE", nil) + message:NSLocalizedString(@"INTERNET_CONNECTION_UNAVAILABLE_MESSAGE", nil) + buttonTitle:NSLocalizedString(@"INTERNET_CONNECTION_UNAVAILABLE_CLOSE", nil)]; + + [self setrefreshButtonEnabled:YES]; + + dispatch_async(dispatch_get_main_queue(), ^{ + [MBProgressHUD hideAllHUDsForView:self.view animated:YES]; + }); + + } + }]; +} + +- (BKRIssueViewController*)issueViewControllerWithID:(NSString*)ID { + __block BKRIssueViewController* foundController = nil; + [self.issueViewControllers enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { + BKRIssueViewController *ivc = (BKRIssueViewController*)obj; + if ([ivc.issue.ID isEqualToString:ID]) { + foundController = ivc; + *stop = YES; + } + }]; + return foundController; +} + +- (BKRIssue*)bakerIssueWithID:(NSString*)ID { + __block BKRIssue *foundIssue = nil; + [self.issues enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { + BKRIssue *issue = (BKRIssue*)obj; + if ([issue.ID isEqualToString:ID]) { + foundIssue = issue; + *stop = YES; + } + }]; + return foundIssue; +} + +- (void)refreshIssueList { + + // Filter issues + __block NSMutableArray *filteredIssues = [NSMutableArray array]; + [issuesManager.issues enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { + BKRIssue *issue = (BKRIssue *)obj; + + // Test if category exists + if([self.categoryItem.title isEqualToString:NSLocalizedString(@"ALL_CATEGORIES_TITLE", nil)] || [issue.categories containsObject:self.categoryItem.title]) { + [filteredIssues addObject:issue]; + } + }]; + + // Assign filtered issues + self.issues = [filteredIssues copy]; + + [self.shelfStatus load]; + for (BKRIssue *issue in self.issues) { + issue.price = [self.shelfStatus priceFor:issue.productID]; + } + + void (^updateIssues)() = ^{ + // Step 1: remove controllers for issues that no longer exist + __weak NSMutableArray *discardedControllers = [NSMutableArray array]; + [self.issueViewControllers enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { + BKRIssueViewController *ivc = (BKRIssueViewController*)obj; + + if (![self bakerIssueWithID:ivc.issue.ID]) { + [discardedControllers addObject:ivc]; + [self.gridView deleteItemsAtIndexPaths:@[[NSIndexPath indexPathForItem:idx inSection:0]]]; + } + }]; + [self.issueViewControllers removeObjectsInArray:discardedControllers]; + + // Step 2: add controllers for issues that did not yet exist (and refresh the ones that do exist) + [self.issues enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { + // NOTE: this block changes the issueViewController array while looping + BKRIssue *issue = (BKRIssue*)obj; + + BKRIssueViewController *existingIvc = [self issueViewControllerWithID:issue.ID]; + + if (existingIvc) { + existingIvc.issue = issue; + } else { + BKRIssueViewController *newIvc = [self createIssueViewControllerWithIssue:issue]; + [self.issueViewControllers insertObject:newIvc atIndex:idx]; + [self.gridView insertItemsAtIndexPaths:@[[NSIndexPath indexPathForItem:idx inSection:0]] ]; + } + }]; + + [self.gridView reloadData]; + }; + + // When first launched, the grid is not initialised, so we can't + // call in the "batch update" method of the grid view + if (self.gridView) { + [self.gridView performBatchUpdates:updateIssues completion:nil]; + } + else { + updateIssues(); + } + + [purchasesManager retrievePurchasesFor:[issuesManager productIDs] withCallback:^(NSDictionary *purchases) { + // List of purchases has been returned, so we can refresh all issues + [self.issueViewControllers enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { + [(BKRIssueViewController*)obj refreshWithCache:NO]; + }]; + [self setrefreshButtonEnabled:YES]; + + dispatch_async(dispatch_get_main_queue(), ^{ + [MBProgressHUD hideAllHUDsForView:self.view animated:YES]; + }); + }]; + + [purchasesManager retrievePricesFor:issuesManager.productIDs andEnableFailureNotifications:NO]; +} + +#pragma mark - Store Kit +- (void)handleSubscribeButtonPressed:(NSNotification*)notification { + if (self.subscriptionsActionSheet.visible) { + [self.subscriptionsActionSheet dismissWithClickedButtonIndex:(self.subscriptionsActionSheet.numberOfButtons - 1) animated:YES]; + } else { + self.subscriptionsActionSheet = [self buildSubscriptionsActionSheet]; + [self.subscriptionsActionSheet showFromBarButtonItem:self.subscribeButton animated:YES]; + } +} + +- (UIActionSheet*)buildSubscriptionsActionSheet { + NSString *title; + if ([api canGetPurchasesJSON]) { + if (purchasesManager.subscribed) { + title = NSLocalizedString(@"SUBSCRIPTIONS_SHEET_SUBSCRIBED", nil); + } else { + title = NSLocalizedString(@"SUBSCRIPTIONS_SHEET_NOT_SUBSCRIBED", nil); + } + } else { + title = NSLocalizedString(@"SUBSCRIPTIONS_SHEET_GENERIC", nil); + } + + UIActionSheet *sheet = [[UIActionSheet alloc]initWithTitle:title + delegate:self + cancelButtonTitle:nil + destructiveButtonTitle:nil + otherButtonTitles: nil]; + NSMutableArray *actions = [NSMutableArray array]; + + if (!purchasesManager.subscribed) { + if ([[BKRSettings sharedSettings].freeSubscriptionProductId length] > 0 && ![purchasesManager isPurchased:[BKRSettings sharedSettings].freeSubscriptionProductId]) { + [sheet addButtonWithTitle:NSLocalizedString(@"SUBSCRIPTIONS_SHEET_FREE", nil)]; + [actions addObject:[BKRSettings sharedSettings].freeSubscriptionProductId]; + } + + for (NSString *productId in [BKRSettings sharedSettings].autoRenewableSubscriptionProductIds) { + NSString *title = [purchasesManager displayTitleFor:productId]; + NSString *price = [purchasesManager priceFor:productId]; + if (price) { + [sheet addButtonWithTitle:[NSString stringWithFormat:@"%@ %@", title, price]]; + [actions addObject:productId]; + } + } + } + + if ([issuesManager hasProductIDs]) { + [sheet addButtonWithTitle:NSLocalizedString(@"SUBSCRIPTIONS_SHEET_RESTORE", nil)]; + [actions addObject:@"restore"]; + } + + [sheet addButtonWithTitle:NSLocalizedString(@"SUBSCRIPTIONS_SHEET_CLOSE", nil)]; + [actions addObject:@"cancel"]; + + self.subscriptionsActionSheetActions = actions; + + sheet.cancelButtonIndex = sheet.numberOfButtons - 1; + return sheet; +} + +- (void) actionSheet:(UIActionSheet*)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex { + if (actionSheet == self.subscriptionsActionSheet) { + NSString *action = (self.subscriptionsActionSheetActions)[buttonIndex]; + if ([action isEqualToString:@"cancel"]) { + NSLog(@"Action sheet: cancel"); + [self setSubscribeButtonEnabled:YES]; + } else if ([action isEqualToString:@"restore"]) { + [self.blockingProgressView show]; + [purchasesManager restore]; + NSLog(@"Action sheet: restore"); + } else { + NSLog(@"Action sheet: %@", action); + [[NSNotificationCenter defaultCenter] postNotificationName:@"BakerSubscriptionPurchase" object:self]; // -> Baker Analytics Event + [self setSubscribeButtonEnabled:NO]; + if (![purchasesManager purchase:action]){ + [BKRUtils showAlertWithTitle:NSLocalizedString(@"SUBSCRIPTION_FAILED_TITLE", nil) + message:nil + buttonTitle:NSLocalizedString(@"SUBSCRIPTION_FAILED_CLOSE", nil)]; + [self setSubscribeButtonEnabled:YES]; + } + } + } +} + +- (void)handleRestoreFailed:(NSNotification*)notification { + NSError *error = (notification.userInfo)[@"error"]; + [BKRUtils showAlertWithTitle:NSLocalizedString(@"RESTORE_FAILED_TITLE", nil) + message:[error localizedDescription] + buttonTitle:NSLocalizedString(@"RESTORE_FAILED_CLOSE", nil)]; + + [self.blockingProgressView dismissWithClickedButtonIndex:0 animated:YES]; + +} + +- (void)handleMultipleRestores:(NSNotification*)notification { + if ([BKRSettings sharedSettings].isNewsstand) { + + if ([notRecognisedTransactions count] > 0) { + NSSet *productIDs = [NSSet setWithArray:[[notRecognisedTransactions valueForKey:@"payment"] valueForKey:@"productIdentifier"]]; + NSString *productsList = [[productIDs allObjects] componentsJoinedByString:@", "]; + + [BKRUtils showAlertWithTitle:NSLocalizedString(@"RESTORED_ISSUE_NOT_RECOGNISED_TITLE", nil) + message:[NSString stringWithFormat:NSLocalizedString(@"RESTORED_ISSUE_NOT_RECOGNISED_MESSAGE", nil), productsList] + buttonTitle:NSLocalizedString(@"RESTORED_ISSUE_NOT_RECOGNISED_CLOSE", nil)]; + + for (SKPaymentTransaction *transaction in notRecognisedTransactions) { + [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; + } + [notRecognisedTransactions removeAllObjects]; + } + } + + [self handleRefresh:nil]; + [self.blockingProgressView dismissWithClickedButtonIndex:0 animated:YES]; +} + +- (void)handleRestoredIssueNotRecognised:(NSNotification*)notification { + SKPaymentTransaction *transaction = (notification.userInfo)[@"transaction"]; + [notRecognisedTransactions addObject:transaction]; +} + +// TODO: this can probably be removed +- (void)handleSubscription:(NSNotification*)notification { + [self setSubscribeButtonEnabled:NO]; + [purchasesManager purchase:[BKRSettings sharedSettings].freeSubscriptionProductId]; +} + +- (void)handleSubscriptionPurchased:(NSNotification*)notification { + SKPaymentTransaction *transaction = (notification.userInfo)[@"transaction"]; + + [purchasesManager markAsPurchased:transaction.payment.productIdentifier]; + [self setSubscribeButtonEnabled:YES]; + + if ([purchasesManager finishTransaction:transaction]) { + if (!purchasesManager.subscribed) { + [BKRUtils showAlertWithTitle:NSLocalizedString(@"SUBSCRIPTION_SUCCESSFUL_TITLE", nil) + message:NSLocalizedString(@"SUBSCRIPTION_SUCCESSFUL_MESSAGE", nil) + buttonTitle:NSLocalizedString(@"SUBSCRIPTION_SUCCESSFUL_CLOSE", nil)]; + + [self handleRefresh:nil]; + } + } else { + [BKRUtils showAlertWithTitle:NSLocalizedString(@"TRANSACTION_RECORDING_FAILED_TITLE", nil) + message:NSLocalizedString(@"TRANSACTION_RECORDING_FAILED_MESSAGE", nil) + buttonTitle:NSLocalizedString(@"TRANSACTION_RECORDING_FAILED_CLOSE", nil)]; + } +} + +- (void)handleSubscriptionFailed:(NSNotification*)notification { + SKPaymentTransaction *transaction = (notification.userInfo)[@"transaction"]; + + // Show an error, unless it was the user who cancelled the transaction + if (transaction.error.code != SKErrorPaymentCancelled) { + [BKRUtils showAlertWithTitle:NSLocalizedString(@"SUBSCRIPTION_FAILED_TITLE", nil) + message:[transaction.error localizedDescription] + buttonTitle:NSLocalizedString(@"SUBSCRIPTION_FAILED_CLOSE", nil)]; + } + + [self setSubscribeButtonEnabled:YES]; +} + +- (void)handleSubscriptionRestored:(NSNotification*)notification { + SKPaymentTransaction *transaction = (notification.userInfo)[@"transaction"]; + + [purchasesManager markAsPurchased:transaction.payment.productIdentifier]; + + if (![purchasesManager finishTransaction:transaction]) { + NSLog(@"Could not confirm purchase restore with remote server for %@", transaction.payment.productIdentifier); + } +} + +- (void)handleProductsRetrieved:(NSNotification*)notification { + NSSet *ids = (notification.userInfo)[@"ids"]; + BOOL issuesRetrieved = NO; + + for (NSString *productId in ids) { + if ([productId isEqualToString:[BKRSettings sharedSettings].freeSubscriptionProductId]) { + // ID is for a free subscription + [self setSubscribeButtonEnabled:YES]; + } else if ([[BKRSettings sharedSettings].autoRenewableSubscriptionProductIds containsObject:productId]) { + // ID is for an auto-renewable subscription + [self setSubscribeButtonEnabled:YES]; + } else { + // ID is for an issue + issuesRetrieved = YES; + } + } + + if (issuesRetrieved) { + NSString *price; + for (BKRIssueViewController *controller in self.issueViewControllers) { + price = [purchasesManager priceFor:controller.issue.productID]; + if (price) { + [controller setPrice:price]; + [self.shelfStatus setPrice:price for:controller.issue.productID]; + } + } + [self.shelfStatus save]; + } +} + +- (void)handleProductsRequestFailed:(NSNotification*)notification { + NSError *error = (notification.userInfo)[@"error"]; + + [BKRUtils showAlertWithTitle:NSLocalizedString(@"PRODUCTS_REQUEST_FAILED_TITLE", nil) + message:[error localizedDescription] + buttonTitle:NSLocalizedString(@"PRODUCTS_REQUEST_FAILED_CLOSE", nil)]; +} + +#pragma mark - Navigation management + +- (void)collectionView:(UICollectionView*)collectionView didSelectItemAtIndexPath:(NSIndexPath*)indexPath { + [collectionView deselectItemAtIndexPath:indexPath animated:YES]; +} + +- (void)readIssue:(BKRIssue*)issue +{ + BKRBook *book = nil; + NSString *status = [issue getStatus]; + + if ([BKRSettings sharedSettings].isNewsstand) { + if ([status isEqual:@"opening"]) { + book = [[BKRBook alloc] initWithBookPath:issue.path bundled:NO]; + if (book) { + [self pushViewControllerWithBook:book]; + } else { + NSLog(@"[ERROR] Book %@ could not be initialized", issue.ID); + issue.transientStatus = BakerIssueTransientStatusNone; + // Let's refresh everything as it's easier. This is an edge case anyway ;) + for (BKRIssueViewController *controller in self.issueViewControllers) { + [controller refresh]; + } + [BKRUtils showAlertWithTitle:NSLocalizedString(@"ISSUE_OPENING_FAILED_TITLE", nil) + message:NSLocalizedString(@"ISSUE_OPENING_FAILED_MESSAGE", nil) + buttonTitle:NSLocalizedString(@"ISSUE_OPENING_FAILED_CLOSE", nil)]; + } + } + } else { + if ([status isEqual:@"bundled"]) { + book = [issue bakerBook]; + [self pushViewControllerWithBook:book]; + } + } +} +- (void)handleReadIssue:(NSNotification*)notification +{ + BKRIssueViewController *controller = notification.object; + [self readIssue:controller.issue]; +} +- (void)receiveBookProtocolNotification:(NSNotification*)notification +{ + self.bookToBeProcessed = (notification.userInfo)[@"ID"]; + [self.navigationController popToRootViewControllerAnimated:YES]; +} +- (void)handleBookToBeProcessed +{ + for (BKRIssueViewController *issueViewController in self.issueViewControllers) { + if ([issueViewController.issue.ID isEqualToString:self.bookToBeProcessed]) { + [issueViewController actionButtonPressed:nil]; + break; + } + } + + self.bookToBeProcessed = nil; +} +- (void)pushViewControllerWithBook:(BKRBook*)book +{ + BKRBookViewController *bakerViewController = [[BKRBookViewController alloc] initWithBook:book]; + [self.navigationController pushViewController:bakerViewController animated:YES]; +} + +#pragma mark - Buttons management + +- (void)setrefreshButtonEnabled:(BOOL)enabled { + self.refreshButton.enabled = enabled; +} + +- (void)setSubscribeButtonEnabled:(BOOL)enabled { + self.subscribeButton.enabled = enabled; + if (enabled) { + self.subscribeButton.title = NSLocalizedString(@"SUBSCRIBE_BUTTON_TEXT", nil); + } else { + self.subscribeButton.title = NSLocalizedString(@"SUBSCRIBE_BUTTON_DISABLED_TEXT", nil); + } +} + +- (void)webViewDidFinishLoad:(UIWebView*)webView { + // Inject user_id + [BKRUtils webView:webView dispatchHTMLEvent:@"init" withParams:@{@"user_id": [BKRBakerAPI UUID], + @"app_id": [BKRUtils appID]}]; +} + +- (void)handleInfoButtonPressed:(id)sender { + + // If the button is pressed when the info box is open, close it + if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) { + if (infoPopover.isPopoverVisible) { + [infoPopover dismissPopoverAnimated:YES]; + return; + } + } + + // Prepare new view + UIViewController *popoverContent = [[UIViewController alloc] init]; + UIWebView *popoverView = [[UIWebView alloc] init]; + + popoverView.backgroundColor = [UIColor whiteColor]; + popoverView.delegate = self; + popoverContent.view = popoverView; + + // Load HTML file + NSString *path = [[NSBundle mainBundle] pathForResource:@"info" ofType:@"html" inDirectory:@"info"]; + [popoverView loadRequest:[NSURLRequest requestWithURL:[NSURL fileURLWithPath:path]]]; + + // Open view + if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) { + // On iPad use the UIPopoverController + infoPopover = [[UIPopoverController alloc] initWithContentViewController:popoverContent]; + [infoPopover presentPopoverFromBarButtonItem:self.infoItem + permittedArrowDirections:UIPopoverArrowDirectionUp + animated:YES]; + } else { + // On iPhone push the view controller to the navigation + [self.navigationController pushViewController:popoverContent animated:YES]; + } + +} + +#pragma mark - Helper methods + +- (void)addPurchaseObserver:(SEL)notificationSelector name:(NSString*)notificationName { + if ([BKRSettings sharedSettings].isNewsstand) { + [[NSNotificationCenter defaultCenter] addObserver:self + selector:notificationSelector + name:notificationName + object:purchasesManager]; + } +} + +- (int)getBannerHeight { + return [[BKRSettings sharedSettings].issuesShelfOptions[[NSString stringWithFormat:@"headerHeight%@%@", [self getDeviceString], [self getOrientationString]]] intValue]; +} + +- (CGSize)getBannerSize { + return CGSizeMake(self.view.frame.size.width, [self getBannerHeight]); +} + +- (NSString *)getDeviceString { + return (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) ? @"Pad" : @"Phone"; +} + +- (NSString *)getOrientationString { + return UIInterfaceOrientationIsLandscape(self.interfaceOrientation) ? @"Landscape" : @"Portrait"; +} + +#pragma mark - BKRCategoryFilterItemDelegate + +- (void)categoryFilterItem:(BKRCategoryFilterItem *)categoryFilterItem clickedAction:(NSString *)action { + [self refreshIssueList]; +} + +@end diff --git a/BakerShelf/info/info.html b/BakerShelf/info/info.html new file mode 100644 index 0000000..3c3bafc --- /dev/null +++ b/BakerShelf/info/info.html @@ -0,0 +1,25 @@ + + + + + Baker Framework - Publication Info + + + +

Baker Framework

+ +

About

+

+ Baker is a HTML5 ebook framework to publish interactive books and magazines on iPad & iPhone using simply open web standards. +

+ +

How to use it

+

+ Baker uses a format called HPub that is basically a self-contained static HTML5 microsite zipped with some metadata. +

+ +

+ +

+ + \ No newline at end of file diff --git a/BakerShelf/info/made-with-baker.png b/BakerShelf/info/made-with-baker.png new file mode 100644 index 0000000..003a769 Binary files /dev/null and b/BakerShelf/info/made-with-baker.png differ diff --git a/BakerShelf/info/style.css b/BakerShelf/info/style.css new file mode 100644 index 0000000..660ec4f --- /dev/null +++ b/BakerShelf/info/style.css @@ -0,0 +1,101 @@ +/* + * Baker Framework - Info Bos + * last update: 2013-07-15 + * + * Copyright (C) 2011-13 by Davide S. Casali + * + */ + +/**************************************************************************************************** + * General + */ +body { + background: #f0f0f0; + font-family: "HelveticaNeue-Light", "HelveticaNeue", Helvetica, Arial, sans-serif; + font-size: 16px; + margin: 0; + padding: 10px; +} + +a { + color: #b50303; +} + +/**************************************************************************************************** + * Typography + */ +p, ul, ol { + font-size: 15px; +} + +h1, h2, h3, h4, h5, h6 { + font-family: "BodoniSvtyTwoOSITCTT-Bold", Times, serif; + font-weight: normal; + text-align: left; + + border: 0; + margin: 0; + padding: 0; +} + +strong { + font-weight: normal; +} + +h1 { + color: #bc242a; + font-size: 25px; + padding: 0; + margin: 15px 0 15px 0; +} + +h2 { + margin: 30px 0 8px; + font-size: 20px; + text-transform: uppercase; +} + +/**************************************************************************************************** + * Mini Divider + */ +#minivider { + font-family: "HelveticaNeue", Helvetica, Arial, sans-serif; + font-size: 14px; + text-transform: uppercase; + padding: 25px 0 6px; +} + +#minivider strong { + color: #b50303; +} + +.imageframe-black { + background: #000000; + text-align: center; + padding: 10px; +} + +/**************************************************************************************************** + * Extend + */ +.grid-50 { + padding-left: 0; + padding-right: 20px; +} + +/**************************************************************************************************** + * Buttons + */ +.button { + display: inline-block; + background: #bc242a; + color: #ffffff; + padding: 8px 14px; + text-decoration: none; +} + + + + + + diff --git a/BakerShelf/lib/BKRReachability.h b/BakerShelf/lib/BKRReachability.h new file mode 100644 index 0000000..2b9dafb --- /dev/null +++ b/BakerShelf/lib/BKRReachability.h @@ -0,0 +1,102 @@ +/* + Copyright (c) 2011, Tony Million. + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. 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. + */ + +#import +#import + +#import +#import +#import +#import +#import +#import + +/** + * Does ARC support support GCD objects? + * It does if the minimum deployment target is iOS 6+ or Mac OS X 8+ + * + * @see http://opensource.apple.com/source/libdispatch/libdispatch-228.18/os/object.h + **/ +#if OS_OBJECT_USE_OBJC +#define NEEDS_DISPATCH_RETAIN_RELEASE 0 +#else +#define NEEDS_DISPATCH_RETAIN_RELEASE 1 +#endif + + +extern NSString *const kReachabilityChangedNotification; + +typedef enum +{ + // Apple NetworkStatus Compatible Names. + NotReachable = 0, + ReachableViaWiFi = 2, + ReachableViaWWAN = 1 +} NetworkStatus; + +@class BKRReachability; + +typedef void (^NetworkReachable)(BKRReachability * reachability); +typedef void (^NetworkUnreachable)(BKRReachability * reachability); + +@interface BKRReachability : NSObject + +@property (nonatomic, copy) NetworkReachable reachableBlock; +@property (nonatomic, copy) NetworkUnreachable unreachableBlock; + + +@property (nonatomic, assign) BOOL reachableOnWWAN; + ++ (BKRReachability*)reachabilityWithHostname:(NSString*)hostname; ++ (BKRReachability*)reachabilityForInternetConnection; ++ (BKRReachability*)reachabilityWithAddress:(const struct sockaddr_in*)hostAddress; ++ (BKRReachability*)reachabilityForLocalWiFi; + +- (BKRReachability *)initWithReachabilityRef:(SCNetworkReachabilityRef)ref; + +- (BOOL)startNotifier; +- (void)stopNotifier; + +- (BOOL)isReachable; +- (BOOL)isReachableViaWWAN; +- (BOOL)isReachableViaWiFi; + +// WWAN may be available, but not active until a connection has been established. +// WiFi may require a connection for VPN on Demand. +- (BOOL)isConnectionRequired; // Identical DDG variant. +- (BOOL)connectionRequired; // Apple's routine. +// Dynamic, on demand connection? +- (BOOL)isConnectionOnDemand; +// Is user intervention required? +- (BOOL)isInterventionRequired; + +- (NetworkStatus)currentReachabilityStatus; +- (SCNetworkReachabilityFlags)reachabilityFlags; +- (NSString*)currentReachabilityString; +- (NSString*)currentReachabilityFlags; + +@end diff --git a/BakerShelf/lib/BKRReachability.m b/BakerShelf/lib/BKRReachability.m new file mode 100644 index 0000000..f4d8849 --- /dev/null +++ b/BakerShelf/lib/BKRReachability.m @@ -0,0 +1,519 @@ +/* + Copyright (c) 2011, Tony Million. + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. 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. + */ + +#import "BKRReachability.h" + + +NSString *const kReachabilityChangedNotification = @"kReachabilityChangedNotification"; + +@interface BKRReachability () + +@property (nonatomic, assign) SCNetworkReachabilityRef reachabilityRef; + + +#if NEEDS_DISPATCH_RETAIN_RELEASE +@property (nonatomic, assign) dispatch_queue_t reachabilitySerialQueue; +#else +@property (nonatomic, strong) dispatch_queue_t reachabilitySerialQueue; +#endif + + +@property (nonatomic, strong) id reachabilityObject; + +- (void)reachabilityChanged:(SCNetworkReachabilityFlags)flags; +- (BOOL)isReachableWithFlags:(SCNetworkReachabilityFlags)flags; + +@end + +static NSString *reachabilityFlags(SCNetworkReachabilityFlags flags) +{ + return [NSString stringWithFormat:@"%c%c %c%c%c%c%c%c%c", +#if TARGET_OS_IPHONE + (flags & kSCNetworkReachabilityFlagsIsWWAN) ? 'W' : '-', +#else + 'X', +#endif + (flags & kSCNetworkReachabilityFlagsReachable) ? 'R' : '-', + (flags & kSCNetworkReachabilityFlagsConnectionRequired) ? 'c' : '-', + (flags & kSCNetworkReachabilityFlagsTransientConnection) ? 't' : '-', + (flags & kSCNetworkReachabilityFlagsInterventionRequired) ? 'i' : '-', + (flags & kSCNetworkReachabilityFlagsConnectionOnTraffic) ? 'C' : '-', + (flags & kSCNetworkReachabilityFlagsConnectionOnDemand) ? 'D' : '-', + (flags & kSCNetworkReachabilityFlagsIsLocalAddress) ? 'l' : '-', + (flags & kSCNetworkReachabilityFlagsIsDirect) ? 'd' : '-']; +} + +//Start listening for reachability notifications on the current run loop +static void TMReachabilityCallback(SCNetworkReachabilityRef target, SCNetworkReachabilityFlags flags, void* info) +{ +#pragma unused (target) +#if __has_feature(objc_arc) + BKRReachability *reachability = ((__bridge BKRReachability*)info); +#else + BKRReachability *reachability = ((BKRReachability*)info); +#endif + + // we probably dont need an autoreleasepool here as GCD docs state each queue has its own autorelease pool + // but what the heck eh? + @autoreleasepool + { + [reachability reachabilityChanged:flags]; + } +} + + +@implementation BKRReachability + +@synthesize reachabilityRef; +@synthesize reachabilitySerialQueue; + +@synthesize reachableOnWWAN; + +@synthesize reachableBlock; +@synthesize unreachableBlock; + +@synthesize reachabilityObject; + +#pragma mark - class constructor methods ++ (BKRReachability*)reachabilityWithHostname:(NSString*)hostname +{ + SCNetworkReachabilityRef ref = SCNetworkReachabilityCreateWithName(NULL, [hostname UTF8String]); + if (ref) + { + id reachability = [[self alloc] initWithReachabilityRef:ref]; + +#if __has_feature(objc_arc) + return reachability; +#else + return [reachability autorelease]; +#endif + + } + + return nil; +} + ++ (BKRReachability *)reachabilityWithAddress:(const struct sockaddr_in *)hostAddress +{ + SCNetworkReachabilityRef ref = SCNetworkReachabilityCreateWithAddress(kCFAllocatorDefault, (const struct sockaddr*)hostAddress); + if (ref) + { + id reachability = [[self alloc] initWithReachabilityRef:ref]; + +#if __has_feature(objc_arc) + return reachability; +#else + return [reachability autorelease]; +#endif + } + + return nil; +} + ++ (BKRReachability *)reachabilityForInternetConnection +{ + struct sockaddr_in zeroAddress; + bzero(&zeroAddress, sizeof(zeroAddress)); + zeroAddress.sin_len = sizeof(zeroAddress); + zeroAddress.sin_family = AF_INET; + + return [self reachabilityWithAddress:&zeroAddress]; +} + ++ (BKRReachability*)reachabilityForLocalWiFi +{ + struct sockaddr_in localWifiAddress; + bzero(&localWifiAddress, sizeof(localWifiAddress)); + localWifiAddress.sin_len = sizeof(localWifiAddress); + localWifiAddress.sin_family = AF_INET; + // IN_LINKLOCALNETNUM is defined in as 169.254.0.0 + localWifiAddress.sin_addr.s_addr = htonl(IN_LINKLOCALNETNUM); + + return [self reachabilityWithAddress:&localWifiAddress]; +} + + +// initialization methods + +- (BKRReachability *)initWithReachabilityRef:(SCNetworkReachabilityRef)ref +{ + self = [super init]; + if (self != nil) + { + self.reachableOnWWAN = YES; + self.reachabilityRef = ref; + } + + return self; +} + +- (void)dealloc +{ + [self stopNotifier]; + + if(self.reachabilityRef) + { + CFRelease(self.reachabilityRef); + self.reachabilityRef = nil; + } + + +#if !(__has_feature(objc_arc)) + [super dealloc]; +#endif + + +} + +#pragma mark - notifier methods + +// Notifier +// NOTE: this uses GCD to trigger the blocks - they *WILL NOT* be called on THE MAIN THREAD +// - In other words DO NOT DO ANY UI UPDATES IN THE BLOCKS. +// INSTEAD USE dispatch_async(dispatch_get_main_queue(), ^{UISTUFF}) (or dispatch_sync if you want) + +- (BOOL)startNotifier +{ + SCNetworkReachabilityContext context = { 0, NULL, NULL, NULL, NULL }; + + // this should do a retain on ourself, so as long as we're in notifier mode we shouldn't disappear out from under ourselves + // woah + self.reachabilityObject = self; + + + + // first we need to create a serial queue + // we allocate this once for the lifetime of the notifier + self.reachabilitySerialQueue = dispatch_queue_create("com.tonymillion.reachability", NULL); + if(!self.reachabilitySerialQueue) + { + return NO; + } + +#if __has_feature(objc_arc) + context.info = (__bridge void *)self; +#else + context.info = (void *)self; +#endif + + if (!SCNetworkReachabilitySetCallback(self.reachabilityRef, TMReachabilityCallback, &context)) + { +#ifdef DEBUG + NSLog(@"SCNetworkReachabilitySetCallback() failed: %s", SCErrorString(SCError())); +#endif + + //clear out the dispatch queue + if(self.reachabilitySerialQueue) + { +#if NEEDS_DISPATCH_RETAIN_RELEASE + dispatch_release(self.reachabilitySerialQueue); +#endif + self.reachabilitySerialQueue = nil; + } + + self.reachabilityObject = nil; + + return NO; + } + + // set it as our reachability queue which will retain the queue + if(!SCNetworkReachabilitySetDispatchQueue(self.reachabilityRef, self.reachabilitySerialQueue)) + { +#ifdef DEBUG + NSLog(@"SCNetworkReachabilitySetDispatchQueue() failed: %s", SCErrorString(SCError())); +#endif + + //UH OH - FAILURE! + + // first stop any callbacks! + SCNetworkReachabilitySetCallback(self.reachabilityRef, NULL, NULL); + + // then clear out the dispatch queue + if(self.reachabilitySerialQueue) + { +#if NEEDS_DISPATCH_RETAIN_RELEASE + dispatch_release(self.reachabilitySerialQueue); +#endif + self.reachabilitySerialQueue = nil; + } + + self.reachabilityObject = nil; + + return NO; + } + + return YES; +} + +- (void)stopNotifier +{ + // first stop any callbacks! + SCNetworkReachabilitySetCallback(self.reachabilityRef, NULL, NULL); + + // unregister target from the GCD serial dispatch queue + SCNetworkReachabilitySetDispatchQueue(self.reachabilityRef, NULL); + + if(self.reachabilitySerialQueue) + { +#if NEEDS_DISPATCH_RETAIN_RELEASE + dispatch_release(self.reachabilitySerialQueue); +#endif + self.reachabilitySerialQueue = nil; + } + + self.reachabilityObject = nil; +} + +#pragma mark - reachability tests + +// this is for the case where you flick the airplane mode +// you end up getting something like this: +//Reachability: WR ct----- +//Reachability: -- ------- +//Reachability: WR ct----- +//Reachability: -- ------- +// we treat this as 4 UNREACHABLE triggers - really apple should do better than this + +#define testcase (kSCNetworkReachabilityFlagsConnectionRequired | kSCNetworkReachabilityFlagsTransientConnection) + +- (BOOL)isReachableWithFlags:(SCNetworkReachabilityFlags)flags +{ + BOOL connectionUP = YES; + + if(!(flags & kSCNetworkReachabilityFlagsReachable)) + connectionUP = NO; + + if( (flags & testcase) == testcase ) + connectionUP = NO; + +#if TARGET_OS_IPHONE + if(flags & kSCNetworkReachabilityFlagsIsWWAN) + { + // we're on 3G + if(!self.reachableOnWWAN) + { + // we dont want to connect when on 3G + connectionUP = NO; + } + } +#endif + + return connectionUP; +} + +- (BOOL)isReachable +{ + SCNetworkReachabilityFlags flags; + + if(!SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags)) + return NO; + + return [self isReachableWithFlags:flags]; +} + +- (BOOL)isReachableViaWWAN +{ +#if TARGET_OS_IPHONE + + SCNetworkReachabilityFlags flags = 0; + + if(SCNetworkReachabilityGetFlags(reachabilityRef, &flags)) + { + // check we're REACHABLE + if(flags & kSCNetworkReachabilityFlagsReachable) + { + // now, check we're on WWAN + if(flags & kSCNetworkReachabilityFlagsIsWWAN) + { + return YES; + } + } + } +#endif + + return NO; +} + +- (BOOL)isReachableViaWiFi +{ + SCNetworkReachabilityFlags flags = 0; + + if(SCNetworkReachabilityGetFlags(reachabilityRef, &flags)) + { + // check we're reachable + if((flags & kSCNetworkReachabilityFlagsReachable)) + { +#if TARGET_OS_IPHONE + // check we're NOT on WWAN + if((flags & kSCNetworkReachabilityFlagsIsWWAN)) + { + return NO; + } +#endif + return YES; + } + } + + return NO; +} + + +// WWAN may be available, but not active until a connection has been established. +// WiFi may require a connection for VPN on Demand. +- (BOOL)isConnectionRequired +{ + return [self connectionRequired]; +} + +- (BOOL)connectionRequired +{ + SCNetworkReachabilityFlags flags; + + if(SCNetworkReachabilityGetFlags(reachabilityRef, &flags)) + { + return (flags & kSCNetworkReachabilityFlagsConnectionRequired); + } + + return NO; +} + +// Dynamic, on demand connection? +- (BOOL)isConnectionOnDemand +{ + SCNetworkReachabilityFlags flags; + + if (SCNetworkReachabilityGetFlags(reachabilityRef, &flags)) + { + return ((flags & kSCNetworkReachabilityFlagsConnectionRequired) && + (flags & (kSCNetworkReachabilityFlagsConnectionOnTraffic | kSCNetworkReachabilityFlagsConnectionOnDemand))); + } + + return NO; +} + +// Is user intervention required? +- (BOOL)isInterventionRequired +{ + SCNetworkReachabilityFlags flags; + + if (SCNetworkReachabilityGetFlags(reachabilityRef, &flags)) + { + return ((flags & kSCNetworkReachabilityFlagsConnectionRequired) && + (flags & kSCNetworkReachabilityFlagsInterventionRequired)); + } + + return NO; +} + + +#pragma mark - reachability status stuff + +- (NetworkStatus)currentReachabilityStatus +{ + if([self isReachable]) + { + if([self isReachableViaWiFi]) + return ReachableViaWiFi; + +#if TARGET_OS_IPHONE + return ReachableViaWWAN; +#endif + } + + return NotReachable; +} + +- (SCNetworkReachabilityFlags)reachabilityFlags +{ + SCNetworkReachabilityFlags flags = 0; + + if(SCNetworkReachabilityGetFlags(reachabilityRef, &flags)) + { + return flags; + } + + return 0; +} + +- (NSString*)currentReachabilityString +{ + NetworkStatus temp = [self currentReachabilityStatus]; + + if(temp == reachableOnWWAN) + { + // updated for the fact we have CDMA phones now! + return NSLocalizedString(@"Cellular", @""); + } + if (temp == ReachableViaWiFi) + { + return NSLocalizedString(@"WiFi", @""); + } + + return NSLocalizedString(@"No Connection", @""); +} + +- (NSString*)currentReachabilityFlags +{ + return reachabilityFlags([self reachabilityFlags]); +} + +#pragma mark - callback function calls this method + +- (void)reachabilityChanged:(SCNetworkReachabilityFlags)flags +{ + if([self isReachableWithFlags:flags]) + { + if(self.reachableBlock) + { + self.reachableBlock(self); + } + } + else + { + if(self.unreachableBlock) + { + self.unreachableBlock(self); + } + } + + // this makes sure the change notification happens on the MAIN THREAD + dispatch_async(dispatch_get_main_queue(), ^{ + [[NSNotificationCenter defaultCenter] postNotificationName:kReachabilityChangedNotification + object:self]; + }); +} + +#pragma mark - Debug Description + +- (NSString *) description; +{ + NSString *description = [NSString stringWithFormat:@"<%@: %#x>", + NSStringFromClass([self class]), (unsigned int) self]; + return description; +} + +@end diff --git a/BakerShelf/lib/BKRZipArchive.h b/BakerShelf/lib/BKRZipArchive.h new file mode 100755 index 0000000..1f5843a --- /dev/null +++ b/BakerShelf/lib/BKRZipArchive.h @@ -0,0 +1,51 @@ +// +// SSZipArchive.h +// SSZipArchive +// +// Created by Sam Soffes on 7/21/10. +// Copyright (c) Sam Soffes 2010-2013. All rights reserved. +// + +#ifndef _BKRZIPARCHIVE_H +#define _BKRZIPARCHIVE_H + +#import +#include "minizip/unzip.h" + +@protocol BKRZipArchiveDelegate; + +@interface BKRZipArchive : NSObject + +// Unzip ++ (BOOL)unzipFileAtPath:(NSString*)path toDestination:(NSString*)destination; ++ (BOOL)unzipFileAtPath:(NSString*)path toDestination:(NSString*)destination overwrite:(BOOL)overwrite password:(NSString*)password error:(NSError **)error; + ++ (BOOL)unzipFileAtPath:(NSString*)path toDestination:(NSString*)destination delegate:(id)delegate; ++ (BOOL)unzipFileAtPath:(NSString*)path toDestination:(NSString*)destination overwrite:(BOOL)overwrite password:(NSString*)password error:(NSError **)error delegate:(id)delegate; + +// Zip ++ (BOOL)createZipFileAtPath:(NSString*)path withFilesAtPaths:(NSArray*)filenames; ++ (BOOL)createZipFileAtPath:(NSString*)path withContentsOfDirectory:(NSString*)directoryPath; + +- (id)initWithPath:(NSString*)path; +- (BOOL)open; +- (BOOL)writeFile:(NSString*)path; +- (BOOL)writeData:(NSData*)data filename:(NSString*)filename; +- (BOOL)close; + +@end + + +@protocol BKRZipArchiveDelegate + +@optional + +- (void)zipArchiveWillUnzipArchiveAtPath:(NSString*)path zipInfo:(unz_global_info)zipInfo; +- (void)zipArchiveDidUnzipArchiveAtPath:(NSString*)path zipInfo:(unz_global_info)zipInfo unzippedPath:(NSString*)unzippedPath; + +- (void)zipArchiveWillUnzipFileAtIndex:(NSInteger)fileIndex totalFiles:(NSInteger)totalFiles archivePath:(NSString*)archivePath fileInfo:(unz_file_info)fileInfo; +- (void)zipArchiveDidUnzipFileAtIndex:(NSInteger)fileIndex totalFiles:(NSInteger)totalFiles archivePath:(NSString*)archivePath fileInfo:(unz_file_info)fileInfo; + +@end + +#endif /* _BKRZIPARCHIVE_H */ diff --git a/BakerShelf/lib/BKRZipArchive.m b/BakerShelf/lib/BKRZipArchive.m new file mode 100755 index 0000000..37d0696 --- /dev/null +++ b/BakerShelf/lib/BKRZipArchive.m @@ -0,0 +1,524 @@ +// +// SSZipArchive.m +// SSZipArchive +// +// Created by Sam Soffes on 7/21/10. +// Copyright (c) Sam Soffes 2010-2013. All rights reserved. +// + +#import "BKRZipArchive.h" +#include "minizip/zip.h" +#import "zlib.h" +#import "zconf.h" + +#include + +#define CHUNK 16384 + +@interface BKRZipArchive () ++ (NSDate*)_dateWithMSDOSFormat:(UInt32)msdosDateTime; +@end + + +@implementation BKRZipArchive { + NSString *_path; + NSString *_filename; + zipFile _zip; +} + + +#pragma mark - Unzipping + ++ (BOOL)unzipFileAtPath:(NSString*)path toDestination:(NSString*)destination { + return [self unzipFileAtPath:path toDestination:destination delegate:nil]; +} + + ++ (BOOL)unzipFileAtPath:(NSString*)path toDestination:(NSString*)destination overwrite:(BOOL)overwrite password:(NSString*)password error:(NSError **)error { + return [self unzipFileAtPath:path toDestination:destination overwrite:overwrite password:password error:error delegate:nil]; +} + + ++ (BOOL)unzipFileAtPath:(NSString*)path toDestination:(NSString*)destination delegate:(id)delegate { + return [self unzipFileAtPath:path toDestination:destination overwrite:YES password:nil error:nil delegate:delegate]; +} + + ++ (BOOL)unzipFileAtPath:(NSString*)path toDestination:(NSString*)destination overwrite:(BOOL)overwrite password:(NSString*)password error:(NSError **)error delegate:(id)delegate { + // Begin opening + zipFile zip = unzOpen((const char*)[path UTF8String]); + if (zip == NULL) { + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:@"failed to open zip file" forKey:NSLocalizedDescriptionKey]; + if (error) { + *error = [NSError errorWithDomain:@"SSZipArchiveErrorDomain" code:-1 userInfo:userInfo]; + } + return NO; + } + + unz_global_info globalInfo = {0ul, 0ul}; + unzGetGlobalInfo(zip, &globalInfo); + + // Begin unzipping + if (unzGoToFirstFile(zip) != UNZ_OK) { + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:@"failed to open first file in zip file" forKey:NSLocalizedDescriptionKey]; + if (error) { + *error = [NSError errorWithDomain:@"SSZipArchiveErrorDomain" code:-2 userInfo:userInfo]; + } + return NO; + } + + BOOL success = YES; + int ret = 0; + unsigned char buffer[4096] = {0}; + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSMutableSet *directoriesModificationDates = [[NSMutableSet alloc] init]; + + // Message delegate + if ([delegate respondsToSelector:@selector(zipArchiveWillUnzipArchiveAtPath:zipInfo:)]) { + [delegate zipArchiveWillUnzipArchiveAtPath:path zipInfo:globalInfo]; + } + + NSInteger currentFileNumber = 0; + do { + @autoreleasepool { + if ([password length] == 0) { + ret = unzOpenCurrentFile(zip); + } else { + ret = unzOpenCurrentFilePassword(zip, [password cStringUsingEncoding:NSASCIIStringEncoding]); + } + + if (ret != UNZ_OK) { + success = NO; + break; + } + + // Reading data and write to file + unz_file_info fileInfo; + memset(&fileInfo, 0, sizeof(unz_file_info)); + + ret = unzGetCurrentFileInfo(zip, &fileInfo, NULL, 0, NULL, 0, NULL, 0); + if (ret != UNZ_OK) { + success = NO; + unzCloseCurrentFile(zip); + break; + } + + // Message delegate + if ([delegate respondsToSelector:@selector(zipArchiveWillUnzipFileAtIndex:totalFiles:archivePath:fileInfo:)]) { + [delegate zipArchiveWillUnzipFileAtIndex:currentFileNumber totalFiles:(NSInteger)globalInfo.number_entry + archivePath:path fileInfo:fileInfo]; + } + + char *filename = (char*)malloc(fileInfo.size_filename + 1); + unzGetCurrentFileInfo(zip, &fileInfo, filename, fileInfo.size_filename + 1, NULL, 0, NULL, 0); + filename[fileInfo.size_filename] = '\0'; + + // + // NOTE + // I used the ZIP spec from here: + // http://www.pkware.com/documents/casestudies/APPNOTE.TXT + // + // ...to deduce this method of detecting whether the file in the ZIP is a symbolic link. + // If it is, it is listed as a directory but has a data size greater than zero (real + // directories have it equal to 0) and the included, uncompressed data is the symbolic link path. + // + // ZIP files did not originally include support for symbolic links so the specification + // doesn't include anything in them that isn't part of a unix extension that isn't being used + // by the archivers we're testing. Most of this is figured out through trial and error and + // reading ZIP headers in hex editors. This seems to do the trick though. + // + + const uLong ZipCompressionMethodStore = 0; + + BOOL fileIsSymbolicLink = NO; + + if((fileInfo.compression_method == ZipCompressionMethodStore) && // Is it compressed? + (S_ISDIR(fileInfo.external_fa)) && // Is it marked as a directory + (fileInfo.compressed_size > 0)) // Is there any data? + { + fileIsSymbolicLink = YES; + } + + // Check if it contains directory + NSString *strPath = [NSString stringWithCString:filename encoding:NSUTF8StringEncoding]; + BOOL isDirectory = NO; + if (filename[fileInfo.size_filename-1] == '/' || filename[fileInfo.size_filename-1] == '\\') { + isDirectory = YES; + } + free(filename); + + // Contains a path + if ([strPath rangeOfCharacterFromSet:[NSCharacterSet characterSetWithCharactersInString:@"/\\"]].location != NSNotFound) { + strPath = [strPath stringByReplacingOccurrencesOfString:@"\\" withString:@"/"]; + } + + NSString *fullPath = [destination stringByAppendingPathComponent:strPath]; + NSError *err = nil; + NSDate *modDate = [[self class] _dateWithMSDOSFormat:(UInt32)fileInfo.dosDate]; + NSDictionary *directoryAttr = [NSDictionary dictionaryWithObjectsAndKeys:modDate, NSFileCreationDate, modDate, NSFileModificationDate, nil]; + + if (isDirectory) { + [fileManager createDirectoryAtPath:fullPath withIntermediateDirectories:YES attributes:directoryAttr error:&err]; + } else { + [fileManager createDirectoryAtPath:[fullPath stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:directoryAttr error:&err]; + } + if (nil != err) { + NSLog(@"[SSZipArchive] Error: %@", err.localizedDescription); + } + + if(!fileIsSymbolicLink) + [directoriesModificationDates addObject: [NSDictionary dictionaryWithObjectsAndKeys:fullPath, @"path", modDate, @"modDate", nil]]; + + if ([fileManager fileExistsAtPath:fullPath] && !isDirectory && !overwrite) { + unzCloseCurrentFile(zip); + ret = unzGoToNextFile(zip); + continue; + } + + if(!fileIsSymbolicLink) + { + FILE *fp = fopen((const char*)[fullPath UTF8String], "wb"); + while (fp) { + int readBytes = unzReadCurrentFile(zip, buffer, 4096); + + if (readBytes > 0) { + fwrite(buffer, readBytes, 1, fp ); + } else { + break; + } + } + + if (fp) { + fclose(fp); + + // Set the original datetime property + if (fileInfo.dosDate != 0) { + NSDate *orgDate = [[self class] _dateWithMSDOSFormat:(UInt32)fileInfo.dosDate]; + NSDictionary *attr = [NSDictionary dictionaryWithObject:orgDate forKey:NSFileModificationDate]; + + if (attr) { + if ([fileManager setAttributes:attr ofItemAtPath:fullPath error:nil] == NO) { + // Can't set attributes + NSLog(@"[SSZipArchive] Failed to set attributes - whilst setting modification date"); + } + } + } + + // Set the original permissions on the file + uLong permissions = fileInfo.external_fa >> 16; + if (permissions != 0) { + // Store it into a NSNumber + NSNumber *permissionsValue = @(permissions); + + // Retrieve any existing attributes + NSMutableDictionary *attrs = [[NSMutableDictionary alloc] initWithDictionary:[fileManager attributesOfItemAtPath:fullPath error:nil]]; + + // Set the value in the attributes dict + attrs[NSFilePosixPermissions] = permissionsValue; + + // Update attributes + if ([fileManager setAttributes:attrs ofItemAtPath:fullPath error:nil] == NO) { + // Unable to set the permissions attribute + NSLog(@"[SSZipArchive] Failed to set attributes - whilst setting permissions"); + } + } + } + } + else + { + // Get the path for the symbolic link + + NSURL* symlinkURL = [NSURL fileURLWithPath:fullPath]; + NSMutableString* destinationPath = [NSMutableString string]; + + int bytesRead = 0; + while((bytesRead = unzReadCurrentFile(zip, buffer, 4096)) > 0) + { + buffer[bytesRead] = 0; + [destinationPath appendString:[NSString stringWithUTF8String:(const char*)buffer]]; + } + + //NSLog(@"Symlinking to: %@", destinationPath); + + NSURL* destinationURL = [NSURL fileURLWithPath:destinationPath]; + + // Create the symbolic link + NSError* symlinkError = nil; + [fileManager createSymbolicLinkAtURL:symlinkURL withDestinationURL:destinationURL error:&symlinkError]; + + if(symlinkError != nil) + { + NSLog(@"Failed to create symbolic link at \"%@\" to \"%@\". Error: %@", symlinkURL.absoluteString, destinationURL.absoluteString, symlinkError.localizedDescription); + } + } + + unzCloseCurrentFile( zip ); + ret = unzGoToNextFile( zip ); + + // Message delegate + if ([delegate respondsToSelector:@selector(zipArchiveDidUnzipFileAtIndex:totalFiles:archivePath:fileInfo:)]) { + [delegate zipArchiveDidUnzipFileAtIndex:currentFileNumber totalFiles:(NSInteger)globalInfo.number_entry + archivePath:path fileInfo:fileInfo]; + } + + currentFileNumber++; + } + } while(ret == UNZ_OK && ret != UNZ_END_OF_LIST_OF_FILE); + + // Close + unzClose(zip); + + // The process of decompressing the .zip archive causes the modification times on the folders + // to be set to the present time. So, when we are done, they need to be explicitly set. + // set the modification date on all of the directories. + NSError * err = nil; + for (NSDictionary * d in directoriesModificationDates) { + if (![[NSFileManager defaultManager] setAttributes:[NSDictionary dictionaryWithObjectsAndKeys:[d objectForKey:@"modDate"], NSFileModificationDate, nil] ofItemAtPath:[d objectForKey:@"path"] error:&err]) { + NSLog(@"[SSZipArchive] Set attributes failed for directory: %@.", [d objectForKey:@"path"]); + } + if (err) { + NSLog(@"[SSZipArchive] Error setting directory file modification date attribute: %@",err.localizedDescription); + } + } + +#if !__has_feature(objc_arc) + [directoriesModificationDates release]; +#endif + + // Message delegate + if (success && [delegate respondsToSelector:@selector(zipArchiveDidUnzipArchiveAtPath:zipInfo:unzippedPath:)]) { + [delegate zipArchiveDidUnzipArchiveAtPath:path zipInfo:globalInfo unzippedPath:destination]; + } + + return success; +} + + +#pragma mark - Zipping + ++ (BOOL)createZipFileAtPath:(NSString*)path withFilesAtPaths:(NSArray*)paths { + BOOL success = NO; + BKRZipArchive *zipArchive = [[BKRZipArchive alloc] initWithPath:path]; + if ([zipArchive open]) { + for (NSString *path in paths) { + [zipArchive writeFile:path]; + } + success = [zipArchive close]; + } + +#if !__has_feature(objc_arc) + [zipArchive release]; +#endif + + return success; +} + + ++ (BOOL)createZipFileAtPath:(NSString*)path withContentsOfDirectory:(NSString*)directoryPath { + BOOL success = NO; + + NSFileManager *fileManager = nil; + BKRZipArchive *zipArchive = [[BKRZipArchive alloc] initWithPath:path]; + + if ([zipArchive open]) { + // use a local filemanager (queue/thread compatibility) + fileManager = [[NSFileManager alloc] init]; + NSDirectoryEnumerator *dirEnumerator = [fileManager enumeratorAtPath:directoryPath]; + + NSString *fileName; + while ((fileName = [dirEnumerator nextObject])) { + BOOL isDir; + NSString *fullFilePath = [directoryPath stringByAppendingPathComponent:fileName]; + [fileManager fileExistsAtPath:fullFilePath isDirectory:&isDir]; + if (!isDir) { + [zipArchive writeFileAtPath:fullFilePath withFileName:fileName]; + } + } + success = [zipArchive close]; + } + +#if !__has_feature(objc_arc) + [fileManager release]; + [zipArchive release]; +#endif + + return success; +} + + +- (id)initWithPath:(NSString*)path { + if ((self = [super init])) { + _path = [path copy]; + } + return self; +} + + +#if !__has_feature(objc_arc) +- (void)dealloc { + [_path release]; + [super dealloc]; +} +#endif + + +- (BOOL)open { + NSAssert((_zip == NULL), @"Attempting open an archive which is already open"); + _zip = zipOpen([_path UTF8String], APPEND_STATUS_CREATE); + return (NULL != _zip); +} + + +- (void)zipInfo:(zip_fileinfo*)zipInfo setDate:(NSDate*)date { + NSCalendar *currentCalendar = [NSCalendar currentCalendar]; + uint flags = NSYearCalendarUnit | NSMonthCalendarUnit | NSDayCalendarUnit | NSHourCalendarUnit | NSMinuteCalendarUnit | NSSecondCalendarUnit; + NSDateComponents *components = [currentCalendar components:flags fromDate:date]; + zipInfo->tmz_date.tm_sec = (unsigned int)components.second; + zipInfo->tmz_date.tm_min = (unsigned int)components.minute; + zipInfo->tmz_date.tm_hour = (unsigned int)components.hour; + zipInfo->tmz_date.tm_mday = (unsigned int)components.day; + zipInfo->tmz_date.tm_mon = (unsigned int)components.month - 1; + zipInfo->tmz_date.tm_year = (unsigned int)components.year; +} + + +- (BOOL)writeFile:(NSString*)path +{ + return [self writeFileAtPath:path withFileName:nil]; +} + +// supports writing files with logical folder/directory structure +// *path* is the absolute path of the file that will be compressed +// *fileName* is the relative name of the file how it is stored within the zip e.g. /folder/subfolder/text1.txt +- (BOOL)writeFileAtPath:(NSString*)path withFileName:(NSString*)fileName { + NSAssert((_zip != NULL), @"Attempting to write to an archive which was never opened"); + + FILE *input = fopen([path UTF8String], "r"); + if (NULL == input) { + return NO; + } + + const char *afileName; + if (!fileName) { + afileName = [path.lastPathComponent UTF8String]; + } + else { + afileName = [fileName UTF8String]; + } + + zip_fileinfo zipInfo = {{0}}; + + NSDictionary *attr = [[NSFileManager defaultManager] attributesOfItemAtPath:path error: nil]; + if( attr ) + { + NSDate *fileDate = (NSDate*)[attr objectForKey:NSFileModificationDate]; + if( fileDate ) + { + [self zipInfo:&zipInfo setDate: fileDate ]; + } + + // Write permissions into the external attributes, for details on this see here: http://unix.stackexchange.com/a/14727 + // Get the permissions value from the files attributes + NSNumber *permissionsValue = (NSNumber*)[attr objectForKey:NSFilePosixPermissions]; + if (permissionsValue) { + // Get the short value for the permissions + short permissionsShort = permissionsValue.shortValue; + + // Convert this into an octal by adding 010000, 010000 being the flag for a regular file + NSInteger permissionsOctal = 0100000 + permissionsShort; + + // Convert this into a long value + uLong permissionsLong = @(permissionsOctal).unsignedLongValue; + + // Store this into the external file attributes once it has been shifted 16 places left to form part of the second from last byte + zipInfo.external_fa = permissionsLong << 16L; + } + } + + zipOpenNewFileInZip(_zip, afileName, &zipInfo, NULL, 0, NULL, 0, NULL, Z_DEFLATED, Z_DEFAULT_COMPRESSION); + + void *buffer = malloc(CHUNK); + unsigned int len = 0; + + while (!feof(input)) + { + len = (unsigned int) fread(buffer, 1, CHUNK, input); + zipWriteInFileInZip(_zip, buffer, len); + } + + zipCloseFileInZip(_zip); + free(buffer); + return YES; +} + + +- (BOOL)writeData:(NSData*)data filename:(NSString*)filename { + if (!_zip) { + return NO; + } + if (!data) { + return NO; + } + zip_fileinfo zipInfo = {{0,0,0,0,0,0},0,0,0}; + [self zipInfo:&zipInfo setDate:[NSDate date]]; + + zipOpenNewFileInZip(_zip, [filename UTF8String], &zipInfo, NULL, 0, NULL, 0, NULL, Z_DEFLATED, Z_DEFAULT_COMPRESSION); + + zipWriteInFileInZip(_zip, data.bytes, (unsigned int)data.length); + + zipCloseFileInZip(_zip); + return YES; +} + + +- (BOOL)close { + NSAssert((_zip != NULL), @"[SSZipArchive] Attempting to close an archive which was never opened"); + zipClose(_zip, NULL); + return YES; +} + + +#pragma mark - Private + +// Format from http://newsgroups.derkeiler.com/Archive/Comp/comp.os.msdos.programmer/2009-04/msg00060.html +// Two consecutive words, or a longword, YYYYYYYMMMMDDDDD hhhhhmmmmmmsssss +// YYYYYYY is years from 1980 = 0 +// sssss is (seconds/2). +// +// 3658 = 0011 0110 0101 1000 = 0011011 0010 11000 = 27 2 24 = 2007-02-24 +// 7423 = 0111 0100 0010 0011 - 01110 100001 00011 = 14 33 2 = 14:33:06 ++ (NSDate*)_dateWithMSDOSFormat:(UInt32)msdosDateTime { + static const UInt32 kYearMask = 0xFE000000; + static const UInt32 kMonthMask = 0x1E00000; + static const UInt32 kDayMask = 0x1F0000; + static const UInt32 kHourMask = 0xF800; + static const UInt32 kMinuteMask = 0x7E0; + static const UInt32 kSecondMask = 0x1F; + + static NSCalendar *gregorian; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar]; + }); + + NSDateComponents *components = [[NSDateComponents alloc] init]; + + NSAssert(0xFFFFFFFF == (kYearMask | kMonthMask | kDayMask | kHourMask | kMinuteMask | kSecondMask), @"[SSZipArchive] MSDOS date masks don't add up"); + + [components setYear:1980 + ((msdosDateTime & kYearMask) >> 25)]; + [components setMonth:(msdosDateTime & kMonthMask) >> 21]; + [components setDay:(msdosDateTime & kDayMask) >> 16]; + [components setHour:(msdosDateTime & kHourMask) >> 11]; + [components setMinute:(msdosDateTime & kMinuteMask) >> 5]; + [components setSecond:(msdosDateTime & kSecondMask) * 2]; + + NSDate *date = [NSDate dateWithTimeInterval:0 sinceDate:[gregorian dateFromComponents:components]]; + +#if !__has_feature(objc_arc) + [components release]; +#endif + + return date; +} + +@end diff --git a/BakerShelf/lib/MBProgressHUD.h b/BakerShelf/lib/MBProgressHUD.h new file mode 100644 index 0000000..1caa3ca --- /dev/null +++ b/BakerShelf/lib/MBProgressHUD.h @@ -0,0 +1,515 @@ +// +// MBProgressHUD.h +// Version 0.9 +// Created by Matej Bukovinski on 2.4.09. +// + +// This code is distributed under the terms and conditions of the MIT license. + +// Copyright (c) 2013 Matej Bukovinski +// +// 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 +#import +#import + +@protocol MBProgressHUDDelegate; + + +typedef enum { + /** Progress is shown using an UIActivityIndicatorView. This is the default. */ + MBProgressHUDModeIndeterminate, + /** Progress is shown using a round, pie-chart like, progress view. */ + MBProgressHUDModeDeterminate, + /** Progress is shown using a horizontal progress bar */ + MBProgressHUDModeDeterminateHorizontalBar, + /** Progress is shown using a ring-shaped progress view. */ + MBProgressHUDModeAnnularDeterminate, + /** Shows a custom view */ + MBProgressHUDModeCustomView, + /** Shows only labels */ + MBProgressHUDModeText +} MBProgressHUDMode; + +typedef enum { + /** Opacity animation */ + MBProgressHUDAnimationFade, + /** Opacity + scale animation */ + MBProgressHUDAnimationZoom, + MBProgressHUDAnimationZoomOut = MBProgressHUDAnimationZoom, + MBProgressHUDAnimationZoomIn +} MBProgressHUDAnimation; + + +#ifndef MB_INSTANCETYPE +#if __has_feature(objc_instancetype) + #define MB_INSTANCETYPE instancetype +#else + #define MB_INSTANCETYPE id +#endif +#endif + +#ifndef MB_STRONG +#if __has_feature(objc_arc) + #define MB_STRONG strong +#else + #define MB_STRONG retain +#endif +#endif + +#ifndef MB_WEAK +#if __has_feature(objc_arc_weak) + #define MB_WEAK weak +#elif __has_feature(objc_arc) + #define MB_WEAK unsafe_unretained +#else + #define MB_WEAK assign +#endif +#endif + +#if NS_BLOCKS_AVAILABLE +typedef void (^MBProgressHUDCompletionBlock)(); +#endif + + +/** + * Displays a simple HUD window containing a progress indicator and two optional labels for short messages. + * + * This is a simple drop-in class for displaying a progress HUD view similar to Apple's private UIProgressHUD class. + * The MBProgressHUD window spans over the entire space given to it by the initWithFrame constructor and catches all + * user input on this region, thereby preventing the user operations on components below the view. The HUD itself is + * drawn centered as a rounded semi-transparent view which resizes depending on the user specified content. + * + * This view supports four modes of operation: + * - MBProgressHUDModeIndeterminate - shows a UIActivityIndicatorView + * - MBProgressHUDModeDeterminate - shows a custom round progress indicator + * - MBProgressHUDModeAnnularDeterminate - shows a custom annular progress indicator + * - MBProgressHUDModeCustomView - shows an arbitrary, user specified view (@see customView) + * + * All three modes can have optional labels assigned: + * - If the labelText property is set and non-empty then a label containing the provided content is placed below the + * indicator view. + * - If also the detailsLabelText property is set then another label is placed below the first label. + */ +@interface MBProgressHUD : UIView + +/** + * Creates a new HUD, adds it to provided view and shows it. The counterpart to this method is hideHUDForView:animated:. + * + * @param view The view that the HUD will be added to + * @param animated If set to YES the HUD will appear using the current animationType. If set to NO the HUD will not use + * animations while appearing. + * @return A reference to the created HUD. + * + * @see hideHUDForView:animated: + * @see animationType + */ ++ (MB_INSTANCETYPE)showHUDAddedTo:(UIView *)view animated:(BOOL)animated; + +/** + * Finds the top-most HUD subview and hides it. The counterpart to this method is showHUDAddedTo:animated:. + * + * @param view The view that is going to be searched for a HUD subview. + * @param animated If set to YES the HUD will disappear using the current animationType. If set to NO the HUD will not use + * animations while disappearing. + * @return YES if a HUD was found and removed, NO otherwise. + * + * @see showHUDAddedTo:animated: + * @see animationType + */ ++ (BOOL)hideHUDForView:(UIView *)view animated:(BOOL)animated; + +/** + * Finds all the HUD subviews and hides them. + * + * @param view The view that is going to be searched for HUD subviews. + * @param animated If set to YES the HUDs will disappear using the current animationType. If set to NO the HUDs will not use + * animations while disappearing. + * @return the number of HUDs found and removed. + * + * @see hideHUDForView:animated: + * @see animationType + */ ++ (NSUInteger)hideAllHUDsForView:(UIView *)view animated:(BOOL)animated; + +/** + * Finds the top-most HUD subview and returns it. + * + * @param view The view that is going to be searched. + * @return A reference to the last HUD subview discovered. + */ ++ (MB_INSTANCETYPE)HUDForView:(UIView *)view; + +/** + * Finds all HUD subviews and returns them. + * + * @param view The view that is going to be searched. + * @return All found HUD views (array of MBProgressHUD objects). + */ ++ (NSArray *)allHUDsForView:(UIView *)view; + +/** + * A convenience constructor that initializes the HUD with the window's bounds. Calls the designated constructor with + * window.bounds as the parameter. + * + * @param window The window instance that will provide the bounds for the HUD. Should be the same instance as + * the HUD's superview (i.e., the window that the HUD will be added to). + */ +- (id)initWithWindow:(UIWindow *)window; + +/** + * A convenience constructor that initializes the HUD with the view's bounds. Calls the designated constructor with + * view.bounds as the parameter + * + * @param view The view instance that will provide the bounds for the HUD. Should be the same instance as + * the HUD's superview (i.e., the view that the HUD will be added to). + */ +- (id)initWithView:(UIView *)view; + +/** + * Display the HUD. You need to make sure that the main thread completes its run loop soon after this method call so + * the user interface can be updated. Call this method when your task is already set-up to be executed in a new thread + * (e.g., when using something like NSOperation or calling an asynchronous call like NSURLRequest). + * + * @param animated If set to YES the HUD will appear using the current animationType. If set to NO the HUD will not use + * animations while appearing. + * + * @see animationType + */ +- (void)show:(BOOL)animated; + +/** + * Hide the HUD. This still calls the hudWasHidden: delegate. This is the counterpart of the show: method. Use it to + * hide the HUD when your task completes. + * + * @param animated If set to YES the HUD will disappear using the current animationType. If set to NO the HUD will not use + * animations while disappearing. + * + * @see animationType + */ +- (void)hide:(BOOL)animated; + +/** + * Hide the HUD after a delay. This still calls the hudWasHidden: delegate. This is the counterpart of the show: method. Use it to + * hide the HUD when your task completes. + * + * @param animated If set to YES the HUD will disappear using the current animationType. If set to NO the HUD will not use + * animations while disappearing. + * @param delay Delay in seconds until the HUD is hidden. + * + * @see animationType + */ +- (void)hide:(BOOL)animated afterDelay:(NSTimeInterval)delay; + +/** + * Shows the HUD while a background task is executing in a new thread, then hides the HUD. + * + * This method also takes care of autorelease pools so your method does not have to be concerned with setting up a + * pool. + * + * @param method The method to be executed while the HUD is shown. This method will be executed in a new thread. + * @param target The object that the target method belongs to. + * @param object An optional object to be passed to the method. + * @param animated If set to YES the HUD will (dis)appear using the current animationType. If set to NO the HUD will not use + * animations while (dis)appearing. + */ +- (void)showWhileExecuting:(SEL)method onTarget:(id)target withObject:(id)object animated:(BOOL)animated; + +#if NS_BLOCKS_AVAILABLE + +/** + * Shows the HUD while a block is executing on a background queue, then hides the HUD. + * + * @see showAnimated:whileExecutingBlock:onQueue:completionBlock: + */ +- (void)showAnimated:(BOOL)animated whileExecutingBlock:(dispatch_block_t)block; + +/** + * Shows the HUD while a block is executing on a background queue, then hides the HUD. + * + * @see showAnimated:whileExecutingBlock:onQueue:completionBlock: + */ +- (void)showAnimated:(BOOL)animated whileExecutingBlock:(dispatch_block_t)block completionBlock:(MBProgressHUDCompletionBlock)completion; + +/** + * Shows the HUD while a block is executing on the specified dispatch queue, then hides the HUD. + * + * @see showAnimated:whileExecutingBlock:onQueue:completionBlock: + */ +- (void)showAnimated:(BOOL)animated whileExecutingBlock:(dispatch_block_t)block onQueue:(dispatch_queue_t)queue; + +/** + * Shows the HUD while a block is executing on the specified dispatch queue, executes completion block on the main queue, and then hides the HUD. + * + * @param animated If set to YES the HUD will (dis)appear using the current animationType. If set to NO the HUD will + * not use animations while (dis)appearing. + * @param block The block to be executed while the HUD is shown. + * @param queue The dispatch queue on which the block should be executed. + * @param completion The block to be executed on completion. + * + * @see completionBlock + */ +- (void)showAnimated:(BOOL)animated whileExecutingBlock:(dispatch_block_t)block onQueue:(dispatch_queue_t)queue + completionBlock:(MBProgressHUDCompletionBlock)completion; + +/** + * A block that gets called after the HUD was completely hidden. + */ +@property (copy) MBProgressHUDCompletionBlock completionBlock; + +#endif + +/** + * MBProgressHUD operation mode. The default is MBProgressHUDModeIndeterminate. + * + * @see MBProgressHUDMode + */ +@property (assign) MBProgressHUDMode mode; + +/** + * The animation type that should be used when the HUD is shown and hidden. + * + * @see MBProgressHUDAnimation + */ +@property (assign) MBProgressHUDAnimation animationType; + +/** + * The UIView (e.g., a UIImageView) to be shown when the HUD is in MBProgressHUDModeCustomView. + * For best results use a 37 by 37 pixel view (so the bounds match the built in indicator bounds). + */ +@property (MB_STRONG) UIView *customView; + +/** + * The HUD delegate object. + * + * @see MBProgressHUDDelegate + */ +@property (MB_WEAK) id delegate; + +/** + * An optional short message to be displayed below the activity indicator. The HUD is automatically resized to fit + * the entire text. If the text is too long it will get clipped by displaying "..." at the end. If left unchanged or + * set to @"", then no message is displayed. + */ +@property (copy) NSString *labelText; + +/** + * An optional details message displayed below the labelText message. This message is displayed only if the labelText + * property is also set and is different from an empty string (@""). The details text can span multiple lines. + */ +@property (copy) NSString *detailsLabelText; + +/** + * The opacity of the HUD window. Defaults to 0.8 (80% opacity). + */ +@property (assign) float opacity; + +/** + * The color of the HUD window. Defaults to black. If this property is set, color is set using + * this UIColor and the opacity property is not used. using retain because performing copy on + * UIColor base colors (like [UIColor greenColor]) cause problems with the copyZone. + */ +@property (MB_STRONG) UIColor *color; + +/** + * The x-axis offset of the HUD relative to the centre of the superview. + */ +@property (assign) float xOffset; + +/** + * The y-axis offset of the HUD relative to the centre of the superview. + */ +@property (assign) float yOffset; + +/** + * The amount of space between the HUD edge and the HUD elements (labels, indicators or custom views). + * Defaults to 20.0 + */ +@property (assign) float margin; + +/** + * The corner radius for the HUD + * Defaults to 10.0 + */ +@property (assign) float cornerRadius; + +/** + * Cover the HUD background view with a radial gradient. + */ +@property (assign) BOOL dimBackground; + +/* + * Grace period is the time (in seconds) that the invoked method may be run without + * showing the HUD. If the task finishes before the grace time runs out, the HUD will + * not be shown at all. + * This may be used to prevent HUD display for very short tasks. + * Defaults to 0 (no grace time). + * Grace time functionality is only supported when the task status is known! + * @see taskInProgress + */ +@property (assign) float graceTime; + +/** + * The minimum time (in seconds) that the HUD is shown. + * This avoids the problem of the HUD being shown and than instantly hidden. + * Defaults to 0 (no minimum show time). + */ +@property (assign) float minShowTime; + +/** + * Indicates that the executed operation is in progress. Needed for correct graceTime operation. + * If you don't set a graceTime (different than 0.0) this does nothing. + * This property is automatically set when using showWhileExecuting:onTarget:withObject:animated:. + * When threading is done outside of the HUD (i.e., when the show: and hide: methods are used directly), + * you need to set this property when your task starts and completes in order to have normal graceTime + * functionality. + */ +@property (assign) BOOL taskInProgress; + +/** + * Removes the HUD from its parent view when hidden. + * Defaults to NO. + */ +@property (assign) BOOL removeFromSuperViewOnHide; + +/** + * Font to be used for the main label. Set this property if the default is not adequate. + */ +@property (MB_STRONG) UIFont* labelFont; + +/** + * Color to be used for the main label. Set this property if the default is not adequate. + */ +@property (MB_STRONG) UIColor* labelColor; + +/** + * Font to be used for the details label. Set this property if the default is not adequate. + */ +@property (MB_STRONG) UIFont* detailsLabelFont; + +/** + * Color to be used for the details label. Set this property if the default is not adequate. + */ +@property (MB_STRONG) UIColor* detailsLabelColor; + +/** + * The color of the activity indicator. Defaults to [UIColor whiteColor] + * Does nothing on pre iOS 5. + */ +@property (MB_STRONG) UIColor *activityIndicatorColor; + +/** + * The progress of the progress indicator, from 0.0 to 1.0. Defaults to 0.0. + */ +@property (assign) float progress; + +/** + * The minimum size of the HUD bezel. Defaults to CGSizeZero (no minimum size). + */ +@property (assign) CGSize minSize; + + +/** + * The actual size of the HUD bezel. + * You can use this to limit touch handling on the bezel aria only. + * @see https://github.com/jdg/MBProgressHUD/pull/200 + */ +@property (atomic, assign, readonly) CGSize size; + + +/** + * Force the HUD dimensions to be equal if possible. + */ +@property (assign, getter = isSquare) BOOL square; + +@end + + +@protocol MBProgressHUDDelegate + +@optional + +/** + * Called after the HUD was fully hidden from the screen. + */ +- (void)hudWasHidden:(MBProgressHUD *)hud; + +@end + + +/** + * A progress view for showing definite progress by filling up a circle (pie chart). + */ +@interface MBRoundProgressView : UIView + +/** + * Progress (0.0 to 1.0) + */ +@property (nonatomic, assign) float progress; + +/** + * Indicator progress color. + * Defaults to white [UIColor whiteColor] + */ +@property (nonatomic, MB_STRONG) UIColor *progressTintColor; + +/** + * Indicator background (non-progress) color. + * Defaults to translucent white (alpha 0.1) + */ +@property (nonatomic, MB_STRONG) UIColor *backgroundTintColor; + +/* + * Display mode - NO = round or YES = annular. Defaults to round. + */ +@property (nonatomic, assign, getter = isAnnular) BOOL annular; + +@end + + +/** + * A flat bar progress view. + */ +@interface MBBarProgressView : UIView + +/** + * Progress (0.0 to 1.0) + */ +@property (nonatomic, assign) float progress; + +/** + * Bar border line color. + * Defaults to white [UIColor whiteColor]. + */ +@property (nonatomic, MB_STRONG) UIColor *lineColor; + +/** + * Bar background color. + * Defaults to clear [UIColor clearColor]; + */ +@property (nonatomic, MB_STRONG) UIColor *progressRemainingColor; + +/** + * Bar progress color. + * Defaults to white [UIColor whiteColor]. + */ +@property (nonatomic, MB_STRONG) UIColor *progressColor; + +@end diff --git a/BakerShelf/lib/MBProgressHUD.m b/BakerShelf/lib/MBProgressHUD.m new file mode 100644 index 0000000..8e44ff8 --- /dev/null +++ b/BakerShelf/lib/MBProgressHUD.m @@ -0,0 +1,1024 @@ +// +// MBProgressHUD.m +// Version 0.9 +// Created by Matej Bukovinski on 2.4.09. +// + +#import "MBProgressHUD.h" +#import + + +#if __has_feature(objc_arc) + #define MB_AUTORELEASE(exp) exp + #define MB_RELEASE(exp) exp + #define MB_RETAIN(exp) exp +#else + #define MB_AUTORELEASE(exp) [exp autorelease] + #define MB_RELEASE(exp) [exp release] + #define MB_RETAIN(exp) [exp retain] +#endif + +#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 60000 + #define MBLabelAlignmentCenter NSTextAlignmentCenter +#else + #define MBLabelAlignmentCenter UITextAlignmentCenter +#endif + +#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 70000 + #define MB_TEXTSIZE(text, font) [text length] > 0 ? [text \ + sizeWithAttributes:@{NSFontAttributeName:font}] : CGSizeZero; +#else + #define MB_TEXTSIZE(text, font) [text length] > 0 ? [text sizeWithFont:font] : CGSizeZero; +#endif + +#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 70000 + #define MB_MULTILINE_TEXTSIZE(text, font, maxSize, mode) [text length] > 0 ? [text \ + boundingRectWithSize:maxSize options:(NSStringDrawingUsesLineFragmentOrigin) \ + attributes:@{NSFontAttributeName:font} context:nil].size : CGSizeZero; +#else + #define MB_MULTILINE_TEXTSIZE(text, font, maxSize, mode) [text length] > 0 ? [text \ + sizeWithFont:font constrainedToSize:maxSize lineBreakMode:mode] : CGSizeZero; +#endif + +#ifndef kCFCoreFoundationVersionNumber_iOS_7_0 + #define kCFCoreFoundationVersionNumber_iOS_7_0 847.20 +#endif + +#ifndef kCFCoreFoundationVersionNumber_iOS_8_0 + #define kCFCoreFoundationVersionNumber_iOS_8_0 1129.15 +#endif + + +static const CGFloat kPadding = 4.f; +static const CGFloat kLabelFontSize = 16.f; +static const CGFloat kDetailsLabelFontSize = 12.f; + + +@interface MBProgressHUD () { + BOOL useAnimation; + SEL methodForExecution; + id targetForExecution; + id objectForExecution; + UILabel *label; + UILabel *detailsLabel; + BOOL isFinished; + CGAffineTransform rotationTransform; +} + +@property (atomic, MB_STRONG) UIView *indicator; +@property (atomic, MB_STRONG) NSTimer *graceTimer; +@property (atomic, MB_STRONG) NSTimer *minShowTimer; +@property (atomic, MB_STRONG) NSDate *showStarted; + + +@end + + +@implementation MBProgressHUD + +#pragma mark - Properties + +@synthesize animationType; +@synthesize delegate; +@synthesize opacity; +@synthesize color; +@synthesize labelFont; +@synthesize labelColor; +@synthesize detailsLabelFont; +@synthesize detailsLabelColor; +@synthesize indicator; +@synthesize xOffset; +@synthesize yOffset; +@synthesize minSize; +@synthesize square; +@synthesize margin; +@synthesize dimBackground; +@synthesize graceTime; +@synthesize minShowTime; +@synthesize graceTimer; +@synthesize minShowTimer; +@synthesize taskInProgress; +@synthesize removeFromSuperViewOnHide; +@synthesize customView; +@synthesize showStarted; +@synthesize mode; +@synthesize labelText; +@synthesize detailsLabelText; +@synthesize progress; +@synthesize size; +@synthesize activityIndicatorColor; +#if NS_BLOCKS_AVAILABLE +@synthesize completionBlock; +#endif + +#pragma mark - Class methods + ++ (MB_INSTANCETYPE)showHUDAddedTo:(UIView *)view animated:(BOOL)animated { + MBProgressHUD *hud = [[self alloc] initWithView:view]; + hud.removeFromSuperViewOnHide = YES; + [view addSubview:hud]; + [hud show:animated]; + return MB_AUTORELEASE(hud); +} + ++ (BOOL)hideHUDForView:(UIView *)view animated:(BOOL)animated { + MBProgressHUD *hud = [self HUDForView:view]; + if (hud != nil) { + hud.removeFromSuperViewOnHide = YES; + [hud hide:animated]; + return YES; + } + return NO; +} + ++ (NSUInteger)hideAllHUDsForView:(UIView *)view animated:(BOOL)animated { + NSArray *huds = [MBProgressHUD allHUDsForView:view]; + for (MBProgressHUD *hud in huds) { + hud.removeFromSuperViewOnHide = YES; + [hud hide:animated]; + } + return [huds count]; +} + ++ (MB_INSTANCETYPE)HUDForView:(UIView *)view { + NSEnumerator *subviewsEnum = [view.subviews reverseObjectEnumerator]; + for (UIView *subview in subviewsEnum) { + if ([subview isKindOfClass:self]) { + return (MBProgressHUD *)subview; + } + } + return nil; +} + ++ (NSArray *)allHUDsForView:(UIView *)view { + NSMutableArray *huds = [NSMutableArray array]; + NSArray *subviews = view.subviews; + for (UIView *aView in subviews) { + if ([aView isKindOfClass:self]) { + [huds addObject:aView]; + } + } + return [NSArray arrayWithArray:huds]; +} + +#pragma mark - Lifecycle + +- (id)initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + if (self) { + // Set default values for properties + self.animationType = MBProgressHUDAnimationFade; + self.mode = MBProgressHUDModeIndeterminate; + self.labelText = nil; + self.detailsLabelText = nil; + self.opacity = 0.8f; + self.color = nil; + self.labelFont = [UIFont boldSystemFontOfSize:kLabelFontSize]; + self.labelColor = [UIColor whiteColor]; + self.detailsLabelFont = [UIFont boldSystemFontOfSize:kDetailsLabelFontSize]; + self.detailsLabelColor = [UIColor whiteColor]; + self.activityIndicatorColor = [UIColor whiteColor]; + self.xOffset = 0.0f; + self.yOffset = 0.0f; + self.dimBackground = NO; + self.margin = 20.0f; + self.cornerRadius = 10.0f; + self.graceTime = 0.0f; + self.minShowTime = 0.0f; + self.removeFromSuperViewOnHide = NO; + self.minSize = CGSizeZero; + self.square = NO; + self.contentMode = UIViewContentModeCenter; + self.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin + | UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin; + + // Transparent background + self.opaque = NO; + self.backgroundColor = [UIColor clearColor]; + // Make it invisible for now + self.alpha = 0.0f; + + taskInProgress = NO; + rotationTransform = CGAffineTransformIdentity; + + [self setupLabels]; + [self updateIndicators]; + [self registerForKVO]; + [self registerForNotifications]; + } + return self; +} + +- (id)initWithView:(UIView *)view { + NSAssert(view, @"View must not be nil."); + return [self initWithFrame:view.bounds]; +} + +- (id)initWithWindow:(UIWindow *)window { + return [self initWithView:window]; +} + +- (void)dealloc { + [self unregisterFromNotifications]; + [self unregisterFromKVO]; +#if !__has_feature(objc_arc) + [color release]; + [indicator release]; + [label release]; + [detailsLabel release]; + [labelText release]; + [detailsLabelText release]; + [graceTimer release]; + [minShowTimer release]; + [showStarted release]; + [customView release]; + [labelFont release]; + [labelColor release]; + [detailsLabelFont release]; + [detailsLabelColor release]; +#if NS_BLOCKS_AVAILABLE + [completionBlock release]; +#endif + [super dealloc]; +#endif +} + +#pragma mark - Show & hide + +- (void)show:(BOOL)animated { + useAnimation = animated; + // If the grace time is set postpone the HUD display + if (self.graceTime > 0.0) { + self.graceTimer = [NSTimer scheduledTimerWithTimeInterval:self.graceTime target:self + selector:@selector(handleGraceTimer:) userInfo:nil repeats:NO]; + } + // ... otherwise show the HUD imediately + else { + [self setNeedsDisplay]; + [self showUsingAnimation:useAnimation]; + } +} + +- (void)hide:(BOOL)animated { + useAnimation = animated; + // If the minShow time is set, calculate how long the hud was shown, + // and pospone the hiding operation if necessary + if (self.minShowTime > 0.0 && showStarted) { + NSTimeInterval interv = [[NSDate date] timeIntervalSinceDate:showStarted]; + if (interv < self.minShowTime) { + self.minShowTimer = [NSTimer scheduledTimerWithTimeInterval:(self.minShowTime - interv) target:self + selector:@selector(handleMinShowTimer:) userInfo:nil repeats:NO]; + return; + } + } + // ... otherwise hide the HUD immediately + [self hideUsingAnimation:useAnimation]; +} + +- (void)hide:(BOOL)animated afterDelay:(NSTimeInterval)delay { + [self performSelector:@selector(hideDelayed:) withObject:[NSNumber numberWithBool:animated] afterDelay:delay]; +} + +- (void)hideDelayed:(NSNumber *)animated { + [self hide:[animated boolValue]]; +} + +#pragma mark - Timer callbacks + +- (void)handleGraceTimer:(NSTimer *)theTimer { + // Show the HUD only if the task is still running + if (taskInProgress) { + [self setNeedsDisplay]; + [self showUsingAnimation:useAnimation]; + } +} + +- (void)handleMinShowTimer:(NSTimer *)theTimer { + [self hideUsingAnimation:useAnimation]; +} + +#pragma mark - View Hierrarchy + +- (BOOL)shouldPerformOrientationTransform { + BOOL isPreiOS8 = NSFoundationVersionNumber < kCFCoreFoundationVersionNumber_iOS_8_0; + // prior to iOS8 code needs to take care of rotation if it is being added to the window + return isPreiOS8 && [self.superview isKindOfClass:[UIWindow class]]; +} + +- (void)didMoveToSuperview { + if ([self shouldPerformOrientationTransform]) { + [self setTransformForCurrentOrientation:NO]; + } +} + +#pragma mark - Internal show & hide operations + +- (void)showUsingAnimation:(BOOL)animated { + if (animated && animationType == MBProgressHUDAnimationZoomIn) { + self.transform = CGAffineTransformConcat(rotationTransform, CGAffineTransformMakeScale(0.5f, 0.5f)); + } else if (animated && animationType == MBProgressHUDAnimationZoomOut) { + self.transform = CGAffineTransformConcat(rotationTransform, CGAffineTransformMakeScale(1.5f, 1.5f)); + } + self.showStarted = [NSDate date]; + // Fade in + if (animated) { + [UIView beginAnimations:nil context:NULL]; + [UIView setAnimationDuration:0.30]; + self.alpha = 1.0f; + if (animationType == MBProgressHUDAnimationZoomIn || animationType == MBProgressHUDAnimationZoomOut) { + self.transform = rotationTransform; + } + [UIView commitAnimations]; + } + else { + self.alpha = 1.0f; + } +} + +- (void)hideUsingAnimation:(BOOL)animated { + // Fade out + if (animated && showStarted) { + [UIView beginAnimations:nil context:NULL]; + [UIView setAnimationDuration:0.30]; + [UIView setAnimationDelegate:self]; + [UIView setAnimationDidStopSelector:@selector(animationFinished:finished:context:)]; + // 0.02 prevents the hud from passing through touches during the animation the hud will get completely hidden + // in the done method + if (animationType == MBProgressHUDAnimationZoomIn) { + self.transform = CGAffineTransformConcat(rotationTransform, CGAffineTransformMakeScale(1.5f, 1.5f)); + } else if (animationType == MBProgressHUDAnimationZoomOut) { + self.transform = CGAffineTransformConcat(rotationTransform, CGAffineTransformMakeScale(0.5f, 0.5f)); + } + + self.alpha = 0.02f; + [UIView commitAnimations]; + } + else { + self.alpha = 0.0f; + [self done]; + } + self.showStarted = nil; +} + +- (void)animationFinished:(NSString *)animationID finished:(BOOL)finished context:(void*)context { + [self done]; +} + +- (void)done { + [NSObject cancelPreviousPerformRequestsWithTarget:self]; + isFinished = YES; + self.alpha = 0.0f; + if (removeFromSuperViewOnHide) { + [self removeFromSuperview]; + } +#if NS_BLOCKS_AVAILABLE + if (self.completionBlock) { + self.completionBlock(); + self.completionBlock = NULL; + } +#endif + if ([delegate respondsToSelector:@selector(hudWasHidden:)]) { + [delegate performSelector:@selector(hudWasHidden:) withObject:self]; + } +} + +#pragma mark - Threading + +- (void)showWhileExecuting:(SEL)method onTarget:(id)target withObject:(id)object animated:(BOOL)animated { + methodForExecution = method; + targetForExecution = MB_RETAIN(target); + objectForExecution = MB_RETAIN(object); + // Launch execution in new thread + self.taskInProgress = YES; + [NSThread detachNewThreadSelector:@selector(launchExecution) toTarget:self withObject:nil]; + // Show HUD view + [self show:animated]; +} + +#if NS_BLOCKS_AVAILABLE + +- (void)showAnimated:(BOOL)animated whileExecutingBlock:(dispatch_block_t)block { + dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + [self showAnimated:animated whileExecutingBlock:block onQueue:queue completionBlock:NULL]; +} + +- (void)showAnimated:(BOOL)animated whileExecutingBlock:(dispatch_block_t)block completionBlock:(void (^)())completion { + dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + [self showAnimated:animated whileExecutingBlock:block onQueue:queue completionBlock:completion]; +} + +- (void)showAnimated:(BOOL)animated whileExecutingBlock:(dispatch_block_t)block onQueue:(dispatch_queue_t)queue { + [self showAnimated:animated whileExecutingBlock:block onQueue:queue completionBlock:NULL]; +} + +- (void)showAnimated:(BOOL)animated whileExecutingBlock:(dispatch_block_t)block onQueue:(dispatch_queue_t)queue + completionBlock:(MBProgressHUDCompletionBlock)completion { + self.taskInProgress = YES; + self.completionBlock = completion; + dispatch_async(queue, ^(void) { + block(); + dispatch_async(dispatch_get_main_queue(), ^(void) { + [self cleanUp]; + }); + }); + [self show:animated]; +} + +#endif + +- (void)launchExecution { + @autoreleasepool { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-performSelector-leaks" + // Start executing the requested task + [targetForExecution performSelector:methodForExecution withObject:objectForExecution]; +#pragma clang diagnostic pop + // Task completed, update view in main thread (note: view operations should + // be done only in the main thread) + [self performSelectorOnMainThread:@selector(cleanUp) withObject:nil waitUntilDone:NO]; + } +} + +- (void)cleanUp { + taskInProgress = NO; +#if !__has_feature(objc_arc) + [targetForExecution release]; + [objectForExecution release]; +#else + targetForExecution = nil; + objectForExecution = nil; +#endif + [self hide:useAnimation]; +} + +#pragma mark - UI + +- (void)setupLabels { + label = [[UILabel alloc] initWithFrame:self.bounds]; + label.adjustsFontSizeToFitWidth = NO; + label.textAlignment = MBLabelAlignmentCenter; + label.opaque = NO; + label.backgroundColor = [UIColor clearColor]; + label.textColor = self.labelColor; + label.font = self.labelFont; + label.text = self.labelText; + [self addSubview:label]; + + detailsLabel = [[UILabel alloc] initWithFrame:self.bounds]; + detailsLabel.font = self.detailsLabelFont; + detailsLabel.adjustsFontSizeToFitWidth = NO; + detailsLabel.textAlignment = MBLabelAlignmentCenter; + detailsLabel.opaque = NO; + detailsLabel.backgroundColor = [UIColor clearColor]; + detailsLabel.textColor = self.detailsLabelColor; + detailsLabel.numberOfLines = 0; + detailsLabel.font = self.detailsLabelFont; + detailsLabel.text = self.detailsLabelText; + [self addSubview:detailsLabel]; +} + +- (void)updateIndicators { + + BOOL isActivityIndicator = [indicator isKindOfClass:[UIActivityIndicatorView class]]; + BOOL isRoundIndicator = [indicator isKindOfClass:[MBRoundProgressView class]]; + + if (mode == MBProgressHUDModeIndeterminate) { + if (!isActivityIndicator) { + // Update to indeterminate indicator + [indicator removeFromSuperview]; + self.indicator = MB_AUTORELEASE([[UIActivityIndicatorView alloc] + initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge]); + [(UIActivityIndicatorView *)indicator startAnimating]; + [self addSubview:indicator]; + } +#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 50000 + [(UIActivityIndicatorView *)indicator setColor:self.activityIndicatorColor]; +#endif + } + else if (mode == MBProgressHUDModeDeterminateHorizontalBar) { + // Update to bar determinate indicator + [indicator removeFromSuperview]; + self.indicator = MB_AUTORELEASE([[MBBarProgressView alloc] init]); + [self addSubview:indicator]; + } + else if (mode == MBProgressHUDModeDeterminate || mode == MBProgressHUDModeAnnularDeterminate) { + if (!isRoundIndicator) { + // Update to determinante indicator + [indicator removeFromSuperview]; + self.indicator = MB_AUTORELEASE([[MBRoundProgressView alloc] init]); + [self addSubview:indicator]; + } + if (mode == MBProgressHUDModeAnnularDeterminate) { + [(MBRoundProgressView *)indicator setAnnular:YES]; + } + } + else if (mode == MBProgressHUDModeCustomView && customView != indicator) { + // Update custom view indicator + [indicator removeFromSuperview]; + self.indicator = customView; + [self addSubview:indicator]; + } else if (mode == MBProgressHUDModeText) { + [indicator removeFromSuperview]; + self.indicator = nil; + } +} + +#pragma mark - Layout + +- (void)layoutSubviews { + [super layoutSubviews]; + + // Entirely cover the parent view + UIView *parent = self.superview; + if (parent) { + self.frame = parent.bounds; + } + CGRect bounds = self.bounds; + + // Determine the total widt and height needed + CGFloat maxWidth = bounds.size.width - 4 * margin; + CGSize totalSize = CGSizeZero; + + CGRect indicatorF = indicator.bounds; + indicatorF.size.width = MIN(indicatorF.size.width, maxWidth); + totalSize.width = MAX(totalSize.width, indicatorF.size.width); + totalSize.height += indicatorF.size.height; + + CGSize labelSize = MB_TEXTSIZE(label.text, label.font); + labelSize.width = MIN(labelSize.width, maxWidth); + totalSize.width = MAX(totalSize.width, labelSize.width); + totalSize.height += labelSize.height; + if (labelSize.height > 0.f && indicatorF.size.height > 0.f) { + totalSize.height += kPadding; + } + + CGFloat remainingHeight = bounds.size.height - totalSize.height - kPadding - 4 * margin; + CGSize maxSize = CGSizeMake(maxWidth, remainingHeight); + CGSize detailsLabelSize = MB_MULTILINE_TEXTSIZE(detailsLabel.text, detailsLabel.font, maxSize, detailsLabel.lineBreakMode); + totalSize.width = MAX(totalSize.width, detailsLabelSize.width); + totalSize.height += detailsLabelSize.height; + if (detailsLabelSize.height > 0.f && (indicatorF.size.height > 0.f || labelSize.height > 0.f)) { + totalSize.height += kPadding; + } + + totalSize.width += 2 * margin; + totalSize.height += 2 * margin; + + // Position elements + CGFloat yPos = round(((bounds.size.height - totalSize.height) / 2)) + margin + yOffset; + CGFloat xPos = xOffset; + indicatorF.origin.y = yPos; + indicatorF.origin.x = round((bounds.size.width - indicatorF.size.width) / 2) + xPos; + indicator.frame = indicatorF; + yPos += indicatorF.size.height; + + if (labelSize.height > 0.f && indicatorF.size.height > 0.f) { + yPos += kPadding; + } + CGRect labelF; + labelF.origin.y = yPos; + labelF.origin.x = round((bounds.size.width - labelSize.width) / 2) + xPos; + labelF.size = labelSize; + label.frame = labelF; + yPos += labelF.size.height; + + if (detailsLabelSize.height > 0.f && (indicatorF.size.height > 0.f || labelSize.height > 0.f)) { + yPos += kPadding; + } + CGRect detailsLabelF; + detailsLabelF.origin.y = yPos; + detailsLabelF.origin.x = round((bounds.size.width - detailsLabelSize.width) / 2) + xPos; + detailsLabelF.size = detailsLabelSize; + detailsLabel.frame = detailsLabelF; + + // Enforce minsize and quare rules + if (square) { + CGFloat max = MAX(totalSize.width, totalSize.height); + if (max <= bounds.size.width - 2 * margin) { + totalSize.width = max; + } + if (max <= bounds.size.height - 2 * margin) { + totalSize.height = max; + } + } + if (totalSize.width < minSize.width) { + totalSize.width = minSize.width; + } + if (totalSize.height < minSize.height) { + totalSize.height = minSize.height; + } + + size = totalSize; +} + +#pragma mark BG Drawing + +- (void)drawRect:(CGRect)rect { + + CGContextRef context = UIGraphicsGetCurrentContext(); + UIGraphicsPushContext(context); + + if (self.dimBackground) { + //Gradient colours + size_t gradLocationsNum = 2; + CGFloat gradLocations[2] = {0.0f, 1.0f}; + CGFloat gradColors[8] = {0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.75f}; + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); + CGGradientRef gradient = CGGradientCreateWithColorComponents(colorSpace, gradColors, gradLocations, gradLocationsNum); + CGColorSpaceRelease(colorSpace); + //Gradient center + CGPoint gradCenter= CGPointMake(self.bounds.size.width/2, self.bounds.size.height/2); + //Gradient radius + float gradRadius = MIN(self.bounds.size.width , self.bounds.size.height) ; + //Gradient draw + CGContextDrawRadialGradient (context, gradient, gradCenter, + 0, gradCenter, gradRadius, + kCGGradientDrawsAfterEndLocation); + CGGradientRelease(gradient); + } + + // Set background rect color + if (self.color) { + CGContextSetFillColorWithColor(context, self.color.CGColor); + } else { + CGContextSetGrayFillColor(context, 0.0f, self.opacity); + } + + + // Center HUD + CGRect allRect = self.bounds; + // Draw rounded HUD backgroud rect + CGRect boxRect = CGRectMake(round((allRect.size.width - size.width) / 2) + self.xOffset, + round((allRect.size.height - size.height) / 2) + self.yOffset, size.width, size.height); + float radius = self.cornerRadius; + CGContextBeginPath(context); + CGContextMoveToPoint(context, CGRectGetMinX(boxRect) + radius, CGRectGetMinY(boxRect)); + CGContextAddArc(context, CGRectGetMaxX(boxRect) - radius, CGRectGetMinY(boxRect) + radius, radius, 3 * (float)M_PI / 2, 0, 0); + CGContextAddArc(context, CGRectGetMaxX(boxRect) - radius, CGRectGetMaxY(boxRect) - radius, radius, 0, (float)M_PI / 2, 0); + CGContextAddArc(context, CGRectGetMinX(boxRect) + radius, CGRectGetMaxY(boxRect) - radius, radius, (float)M_PI / 2, (float)M_PI, 0); + CGContextAddArc(context, CGRectGetMinX(boxRect) + radius, CGRectGetMinY(boxRect) + radius, radius, (float)M_PI, 3 * (float)M_PI / 2, 0); + CGContextClosePath(context); + CGContextFillPath(context); + + UIGraphicsPopContext(); +} + +#pragma mark - KVO + +- (void)registerForKVO { + for (NSString *keyPath in [self observableKeypaths]) { + [self addObserver:self forKeyPath:keyPath options:NSKeyValueObservingOptionNew context:NULL]; + } +} + +- (void)unregisterFromKVO { + for (NSString *keyPath in [self observableKeypaths]) { + [self removeObserver:self forKeyPath:keyPath]; + } +} + +- (NSArray *)observableKeypaths { + return [NSArray arrayWithObjects:@"mode", @"customView", @"labelText", @"labelFont", @"labelColor", + @"detailsLabelText", @"detailsLabelFont", @"detailsLabelColor", @"progress", @"activityIndicatorColor", nil]; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { + if (![NSThread isMainThread]) { + [self performSelectorOnMainThread:@selector(updateUIForKeypath:) withObject:keyPath waitUntilDone:NO]; + } else { + [self updateUIForKeypath:keyPath]; + } +} + +- (void)updateUIForKeypath:(NSString *)keyPath { + if ([keyPath isEqualToString:@"mode"] || [keyPath isEqualToString:@"customView"] || + [keyPath isEqualToString:@"activityIndicatorColor"]) { + [self updateIndicators]; + } else if ([keyPath isEqualToString:@"labelText"]) { + label.text = self.labelText; + } else if ([keyPath isEqualToString:@"labelFont"]) { + label.font = self.labelFont; + } else if ([keyPath isEqualToString:@"labelColor"]) { + label.textColor = self.labelColor; + } else if ([keyPath isEqualToString:@"detailsLabelText"]) { + detailsLabel.text = self.detailsLabelText; + } else if ([keyPath isEqualToString:@"detailsLabelFont"]) { + detailsLabel.font = self.detailsLabelFont; + } else if ([keyPath isEqualToString:@"detailsLabelColor"]) { + detailsLabel.textColor = self.detailsLabelColor; + } else if ([keyPath isEqualToString:@"progress"]) { + if ([indicator respondsToSelector:@selector(setProgress:)]) { + [(id)indicator setValue:@(progress) forKey:@"progress"]; + } + return; + } + [self setNeedsLayout]; + [self setNeedsDisplay]; +} + +#pragma mark - Notifications + +- (void)registerForNotifications { + NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; + + [nc addObserver:self selector:@selector(statusBarOrientationDidChange:) + name:UIApplicationDidChangeStatusBarOrientationNotification object:nil]; +} + +- (void)unregisterFromNotifications { + NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; + [nc removeObserver:self name:UIApplicationDidChangeStatusBarOrientationNotification object:nil]; +} + +- (void)statusBarOrientationDidChange:(NSNotification *)notification { + UIView *superview = self.superview; + if (!superview) { + return; + } else if ([self shouldPerformOrientationTransform]) { + [self setTransformForCurrentOrientation:YES]; + } else { + self.frame = self.superview.bounds; + [self setNeedsDisplay]; + } +} + +- (void)setTransformForCurrentOrientation:(BOOL)animated { + // Stay in sync with the superview + if (self.superview) { + self.bounds = self.superview.bounds; + [self setNeedsDisplay]; + } + + UIInterfaceOrientation orientation = [UIApplication sharedApplication].statusBarOrientation; + CGFloat radians = 0; + if (UIInterfaceOrientationIsLandscape(orientation)) { + if (orientation == UIInterfaceOrientationLandscapeLeft) { radians = -(CGFloat)M_PI_2; } + else { radians = (CGFloat)M_PI_2; } + // Window coordinates differ! + self.bounds = CGRectMake(0, 0, self.bounds.size.height, self.bounds.size.width); + } else { + if (orientation == UIInterfaceOrientationPortraitUpsideDown) { radians = (CGFloat)M_PI; } + else { radians = 0; } + } + rotationTransform = CGAffineTransformMakeRotation(radians); + + if (animated) { + [UIView beginAnimations:nil context:nil]; + [UIView setAnimationDuration:0.3]; + } + [self setTransform:rotationTransform]; + if (animated) { + [UIView commitAnimations]; + } +} + +@end + + +@implementation MBRoundProgressView + +#pragma mark - Lifecycle + +- (id)init { + return [self initWithFrame:CGRectMake(0.f, 0.f, 37.f, 37.f)]; +} + +- (id)initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + if (self) { + self.backgroundColor = [UIColor clearColor]; + self.opaque = NO; + _progress = 0.f; + _annular = NO; + _progressTintColor = [[UIColor alloc] initWithWhite:1.f alpha:1.f]; + _backgroundTintColor = [[UIColor alloc] initWithWhite:1.f alpha:.1f]; + [self registerForKVO]; + } + return self; +} + +- (void)dealloc { + [self unregisterFromKVO]; +#if !__has_feature(objc_arc) + [_progressTintColor release]; + [_backgroundTintColor release]; + [super dealloc]; +#endif +} + +#pragma mark - Drawing + +- (void)drawRect:(CGRect)rect { + + CGRect allRect = self.bounds; + CGRect circleRect = CGRectInset(allRect, 2.0f, 2.0f); + CGContextRef context = UIGraphicsGetCurrentContext(); + + if (_annular) { + // Draw background + BOOL isPreiOS7 = NSFoundationVersionNumber < kCFCoreFoundationVersionNumber_iOS_7_0; + CGFloat lineWidth = isPreiOS7 ? 5.f : 2.f; + UIBezierPath *processBackgroundPath = [UIBezierPath bezierPath]; + processBackgroundPath.lineWidth = lineWidth; + processBackgroundPath.lineCapStyle = kCGLineCapButt; + CGPoint center = CGPointMake(self.bounds.size.width/2, self.bounds.size.height/2); + CGFloat radius = (self.bounds.size.width - lineWidth)/2; + CGFloat startAngle = - ((float)M_PI / 2); // 90 degrees + CGFloat endAngle = (2 * (float)M_PI) + startAngle; + [processBackgroundPath addArcWithCenter:center radius:radius startAngle:startAngle endAngle:endAngle clockwise:YES]; + [_backgroundTintColor set]; + [processBackgroundPath stroke]; + // Draw progress + UIBezierPath *processPath = [UIBezierPath bezierPath]; + processPath.lineCapStyle = isPreiOS7 ? kCGLineCapRound : kCGLineCapSquare; + processPath.lineWidth = lineWidth; + endAngle = (self.progress * 2 * (float)M_PI) + startAngle; + [processPath addArcWithCenter:center radius:radius startAngle:startAngle endAngle:endAngle clockwise:YES]; + [_progressTintColor set]; + [processPath stroke]; + } else { + // Draw background + [_progressTintColor setStroke]; + [_backgroundTintColor setFill]; + CGContextSetLineWidth(context, 2.0f); + CGContextFillEllipseInRect(context, circleRect); + CGContextStrokeEllipseInRect(context, circleRect); + // Draw progress + CGPoint center = CGPointMake(allRect.size.width / 2, allRect.size.height / 2); + CGFloat radius = (allRect.size.width - 4) / 2; + CGFloat startAngle = - ((float)M_PI / 2); // 90 degrees + CGFloat endAngle = (self.progress * 2 * (float)M_PI) + startAngle; + CGContextSetRGBFillColor(context, 1.0f, 1.0f, 1.0f, 1.0f); // white + CGContextMoveToPoint(context, center.x, center.y); + CGContextAddArc(context, center.x, center.y, radius, startAngle, endAngle, 0); + CGContextClosePath(context); + CGContextFillPath(context); + } +} + +#pragma mark - KVO + +- (void)registerForKVO { + for (NSString *keyPath in [self observableKeypaths]) { + [self addObserver:self forKeyPath:keyPath options:NSKeyValueObservingOptionNew context:NULL]; + } +} + +- (void)unregisterFromKVO { + for (NSString *keyPath in [self observableKeypaths]) { + [self removeObserver:self forKeyPath:keyPath]; + } +} + +- (NSArray *)observableKeypaths { + return [NSArray arrayWithObjects:@"progressTintColor", @"backgroundTintColor", @"progress", @"annular", nil]; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { + [self setNeedsDisplay]; +} + +@end + + +@implementation MBBarProgressView + +#pragma mark - Lifecycle + +- (id)init { + return [self initWithFrame:CGRectMake(.0f, .0f, 120.0f, 20.0f)]; +} + +- (id)initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + if (self) { + _progress = 0.f; + _lineColor = [UIColor whiteColor]; + _progressColor = [UIColor whiteColor]; + _progressRemainingColor = [UIColor clearColor]; + self.backgroundColor = [UIColor clearColor]; + self.opaque = NO; + [self registerForKVO]; + } + return self; +} + +- (void)dealloc { + [self unregisterFromKVO]; +#if !__has_feature(objc_arc) + [_lineColor release]; + [_progressColor release]; + [_progressRemainingColor release]; + [super dealloc]; +#endif +} + +#pragma mark - Drawing + +- (void)drawRect:(CGRect)rect { + CGContextRef context = UIGraphicsGetCurrentContext(); + + CGContextSetLineWidth(context, 2); + CGContextSetStrokeColorWithColor(context,[_lineColor CGColor]); + CGContextSetFillColorWithColor(context, [_progressRemainingColor CGColor]); + + // Draw background + float radius = (rect.size.height / 2) - 2; + CGContextMoveToPoint(context, 2, rect.size.height/2); + CGContextAddArcToPoint(context, 2, 2, radius + 2, 2, radius); + CGContextAddLineToPoint(context, rect.size.width - radius - 2, 2); + CGContextAddArcToPoint(context, rect.size.width - 2, 2, rect.size.width - 2, rect.size.height / 2, radius); + CGContextAddArcToPoint(context, rect.size.width - 2, rect.size.height - 2, rect.size.width - radius - 2, rect.size.height - 2, radius); + CGContextAddLineToPoint(context, radius + 2, rect.size.height - 2); + CGContextAddArcToPoint(context, 2, rect.size.height - 2, 2, rect.size.height/2, radius); + CGContextFillPath(context); + + // Draw border + CGContextMoveToPoint(context, 2, rect.size.height/2); + CGContextAddArcToPoint(context, 2, 2, radius + 2, 2, radius); + CGContextAddLineToPoint(context, rect.size.width - radius - 2, 2); + CGContextAddArcToPoint(context, rect.size.width - 2, 2, rect.size.width - 2, rect.size.height / 2, radius); + CGContextAddArcToPoint(context, rect.size.width - 2, rect.size.height - 2, rect.size.width - radius - 2, rect.size.height - 2, radius); + CGContextAddLineToPoint(context, radius + 2, rect.size.height - 2); + CGContextAddArcToPoint(context, 2, rect.size.height - 2, 2, rect.size.height/2, radius); + CGContextStrokePath(context); + + CGContextSetFillColorWithColor(context, [_progressColor CGColor]); + radius = radius - 2; + float amount = self.progress * rect.size.width; + + // Progress in the middle area + if (amount >= radius + 4 && amount <= (rect.size.width - radius - 4)) { + CGContextMoveToPoint(context, 4, rect.size.height/2); + CGContextAddArcToPoint(context, 4, 4, radius + 4, 4, radius); + CGContextAddLineToPoint(context, amount, 4); + CGContextAddLineToPoint(context, amount, radius + 4); + + CGContextMoveToPoint(context, 4, rect.size.height/2); + CGContextAddArcToPoint(context, 4, rect.size.height - 4, radius + 4, rect.size.height - 4, radius); + CGContextAddLineToPoint(context, amount, rect.size.height - 4); + CGContextAddLineToPoint(context, amount, radius + 4); + + CGContextFillPath(context); + } + + // Progress in the right arc + else if (amount > radius + 4) { + float x = amount - (rect.size.width - radius - 4); + + CGContextMoveToPoint(context, 4, rect.size.height/2); + CGContextAddArcToPoint(context, 4, 4, radius + 4, 4, radius); + CGContextAddLineToPoint(context, rect.size.width - radius - 4, 4); + float angle = -acos(x/radius); + if (isnan(angle)) angle = 0; + CGContextAddArc(context, rect.size.width - radius - 4, rect.size.height/2, radius, M_PI, angle, 0); + CGContextAddLineToPoint(context, amount, rect.size.height/2); + + CGContextMoveToPoint(context, 4, rect.size.height/2); + CGContextAddArcToPoint(context, 4, rect.size.height - 4, radius + 4, rect.size.height - 4, radius); + CGContextAddLineToPoint(context, rect.size.width - radius - 4, rect.size.height - 4); + angle = acos(x/radius); + if (isnan(angle)) angle = 0; + CGContextAddArc(context, rect.size.width - radius - 4, rect.size.height/2, radius, -M_PI, angle, 1); + CGContextAddLineToPoint(context, amount, rect.size.height/2); + + CGContextFillPath(context); + } + + // Progress is in the left arc + else if (amount < radius + 4 && amount > 0) { + CGContextMoveToPoint(context, 4, rect.size.height/2); + CGContextAddArcToPoint(context, 4, 4, radius + 4, 4, radius); + CGContextAddLineToPoint(context, radius + 4, rect.size.height/2); + + CGContextMoveToPoint(context, 4, rect.size.height/2); + CGContextAddArcToPoint(context, 4, rect.size.height - 4, radius + 4, rect.size.height - 4, radius); + CGContextAddLineToPoint(context, radius + 4, rect.size.height/2); + + CGContextFillPath(context); + } +} + +#pragma mark - KVO + +- (void)registerForKVO { + for (NSString *keyPath in [self observableKeypaths]) { + [self addObserver:self forKeyPath:keyPath options:NSKeyValueObservingOptionNew context:NULL]; + } +} + +- (void)unregisterFromKVO { + for (NSString *keyPath in [self observableKeypaths]) { + [self removeObserver:self forKeyPath:keyPath]; + } +} + +- (NSArray *)observableKeypaths { + return [NSArray arrayWithObjects:@"lineColor", @"progressRemainingColor", @"progressColor", @"progress", nil]; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { + [self setNeedsDisplay]; +} + +@end diff --git a/BakerShelf/lib/NSData+BakerExtensions.h b/BakerShelf/lib/NSData+BakerExtensions.h new file mode 100644 index 0000000..1e57eb9 --- /dev/null +++ b/BakerShelf/lib/NSData+BakerExtensions.h @@ -0,0 +1,42 @@ +// +// NSData+Base64.h +// +// Version 1.0.2 +// +// Created by Nick Lockwood on 12/01/2012. +// Copyright (C) 2012 Charcoal Design +// +// Distributed under the permissive zlib License +// Get the latest version from here: +// +// https://github.com/nicklockwood/Base64 +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// +// 3. This notice may not be removed or altered from any source distribution. +// + +#import + +@interface NSData (Base64) + ++ (NSData*)bkrDataWithBase64EncodedString:(NSString*)string; +- (NSString*)bkrBase64EncodedStringWithWrapWidth:(NSUInteger)wrapWidth; +- (NSString*)bkrBase64EncodedString; + +@end + diff --git a/BakerShelf/lib/NSData+BakerExtensions.m b/BakerShelf/lib/NSData+BakerExtensions.m new file mode 100644 index 0000000..a187a2d --- /dev/null +++ b/BakerShelf/lib/NSData+BakerExtensions.m @@ -0,0 +1,156 @@ +// +// NSData+Base64.m +// +// Version 1.0.2 +// +// Created by Nick Lockwood on 12/01/2012. +// Copyright (C) 2012 Charcoal Design +// +// Distributed under the permissive zlib License +// Get the latest version from here: +// +// https://github.com/nicklockwood/Base64 +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// +// 3. This notice may not be removed or altered from any source distribution. +// + +#import "NSData+BakerExtensions.h" + +@implementation NSData (Base64) + ++ (NSData *)bkrDataWithBase64EncodedString:(NSString *)string +{ + const char lookup[] = + { + 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 62, 99, 99, 99, 63, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 99, 99, 99, 99, 99, 99, + 99, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 99, 99, 99, 99, 99, + 99, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 99, 99, 99, 99, 99 + }; + + NSData *inputData = [string dataUsingEncoding:NSASCIIStringEncoding allowLossyConversion:YES]; + long long inputLength = [inputData length]; + const unsigned char *inputBytes = [inputData bytes]; + + long long maxOutputLength = (inputLength / 4 + 1) * 3; + NSMutableData *outputData = [NSMutableData dataWithLength:(NSUInteger)maxOutputLength]; + unsigned char *outputBytes = (unsigned char *)[outputData mutableBytes]; + + int accumulator = 0; + long long outputLength = 0; + unsigned char accumulated[] = {0, 0, 0, 0}; + for (long long i = 0; i < inputLength; i++) + { + unsigned char decoded = lookup[inputBytes[i] & 0x7F]; + if (decoded != 99) + { + accumulated[accumulator] = decoded; + if (accumulator == 3) + { + outputBytes[outputLength++] = (accumulated[0] << 2) | (accumulated[1] >> 4); + outputBytes[outputLength++] = (accumulated[1] << 4) | (accumulated[2] >> 2); + outputBytes[outputLength++] = (accumulated[2] << 6) | accumulated[3]; + } + accumulator = (accumulator + 1) % 4; + } + } + + //handle left-over data + if (accumulator > 0) outputBytes[outputLength] = (accumulated[0] << 2) | (accumulated[1] >> 4); + if (accumulator > 1) outputBytes[++outputLength] = (accumulated[1] << 4) | (accumulated[2] >> 2); + if (accumulator > 2) outputLength++; + + //truncate data to match actual output length + outputData.length = (NSUInteger)outputLength; + return outputLength? outputData: nil; +} + +- (NSString *)bkrBase64EncodedStringWithWrapWidth:(NSUInteger)wrapWidth +{ + //ensure wrapWidth is a multiple of 4 + wrapWidth = (wrapWidth / 4) * 4; + + const char lookup[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + + long long inputLength = [self length]; + const unsigned char *inputBytes = [self bytes]; + + long long maxOutputLength = (inputLength / 3 + 1) * 4; + maxOutputLength += wrapWidth? (maxOutputLength / wrapWidth) * 2: 0; + unsigned char *outputBytes = (unsigned char *)malloc((unsigned long)maxOutputLength); + + long long i; + long long outputLength = 0; + for (i = 0; i < inputLength - 2; i += 3) + { + outputBytes[outputLength++] = lookup[(inputBytes[i] & 0xFC) >> 2]; + outputBytes[outputLength++] = lookup[((inputBytes[i] & 0x03) << 4) | ((inputBytes[i + 1] & 0xF0) >> 4)]; + outputBytes[outputLength++] = lookup[((inputBytes[i + 1] & 0x0F) << 2) | ((inputBytes[i + 2] & 0xC0) >> 6)]; + outputBytes[outputLength++] = lookup[inputBytes[i + 2] & 0x3F]; + + //add line break + if (wrapWidth && (outputLength + 2) % (wrapWidth + 2) == 0) + { + outputBytes[outputLength++] = '\r'; + outputBytes[outputLength++] = '\n'; + } + } + + //handle left-over data + if (i == inputLength - 2) + { + // = terminator + outputBytes[outputLength++] = lookup[(inputBytes[i] & 0xFC) >> 2]; + outputBytes[outputLength++] = lookup[((inputBytes[i] & 0x03) << 4) | ((inputBytes[i + 1] & 0xF0) >> 4)]; + outputBytes[outputLength++] = lookup[(inputBytes[i + 1] & 0x0F) << 2]; + outputBytes[outputLength++] = '='; + } + else if (i == inputLength - 1) + { + // == terminator + outputBytes[outputLength++] = lookup[(inputBytes[i] & 0xFC) >> 2]; + outputBytes[outputLength++] = lookup[(inputBytes[i] & 0x03) << 4]; + outputBytes[outputLength++] = '='; + outputBytes[outputLength++] = '='; + } + + //truncate data to match actual output length + if (outputLength > 0) { + outputBytes = realloc(outputBytes, (unsigned long)outputLength); + } + NSString *result = [[NSString alloc] initWithBytesNoCopy:outputBytes length:(NSUInteger)outputLength encoding:NSASCIIStringEncoding freeWhenDone:YES]; + +#if !__has_feature(objc_arc) + [result autorelease]; +#endif + + return (outputLength >= 4)? result: nil; +} + +- (NSString *)bkrBase64EncodedString +{ + return [self bkrBase64EncodedStringWithWrapWidth:0]; +} + +@end + diff --git a/BakerShelf/lib/NSMutableURLRequest+BakerExtensions.h b/BakerShelf/lib/NSMutableURLRequest+BakerExtensions.h new file mode 100644 index 0000000..07bcfea --- /dev/null +++ b/BakerShelf/lib/NSMutableURLRequest+BakerExtensions.h @@ -0,0 +1,41 @@ +// +// NSMutableURLRequest+WebServiceClient.h +// Baker +// See: http://stackoverflow.com/a/1289735/551557 +// +// ========================================================================================== +// +// Copyright (c) 2010-2013, Davide Casali, Marco Colombo, Alessandro Morandi +// Copyright (c) 2014, Andrew Krowczyk, Cédric Mériau +// All rights reserved. +// +// 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. +// Neither the name of the Baker Framework nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written +// permission. +// 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. +// + +#import + +@interface NSMutableURLRequest (BakerExtensions) + ++ (NSString*)bkrEncodeFormPostParameters:(NSDictionary*)parameters; +- (void)bkrSetFormPostParameters:(NSDictionary*)parameters; + +@end diff --git a/BakerShelf/lib/NSMutableURLRequest+BakerExtensions.m b/BakerShelf/lib/NSMutableURLRequest+BakerExtensions.m new file mode 100644 index 0000000..1057f80 --- /dev/null +++ b/BakerShelf/lib/NSMutableURLRequest+BakerExtensions.m @@ -0,0 +1,68 @@ +// +// NSMutableURLRequest+WebServiceClient.m +// Baker +// See: http://stackoverflow.com/a/1289735/551557 +// +// ========================================================================================== +// +// Copyright (c) 2010-2013, Davide Casali, Marco Colombo, Alessandro Morandi +// Copyright (c) 2014, Andrew Krowczyk, Cédric Mériau +// All rights reserved. +// +// 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. +// Neither the name of the Baker Framework nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written +// permission. +// 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. +// + +#import "NSMutableURLRequest+BakerExtensions.h" + +@implementation NSMutableURLRequest (BakerExtensions) + ++ (NSString*)bkrEncodeFormPostParameters:(NSDictionary*)parameters { + NSMutableString *formPostParams = [[NSMutableString alloc] init]; + + NSEnumerator *keys = [parameters keyEnumerator]; + + NSString *name = [keys nextObject]; + while (nil != name) { + NSString *encodedValue = ((NSString *) CFBridgingRelease(CFURLCreateStringByAddingPercentEscapes(NULL, (CFStringRef) [parameters objectForKey: name], NULL, CFSTR("=/:"), kCFStringEncodingUTF8))); + + [formPostParams appendString: name]; + [formPostParams appendString: @"="]; + [formPostParams appendString: encodedValue]; + + name = [keys nextObject]; + + if (nil != name) { + [formPostParams appendString: @"&"]; + } + } + + return formPostParams; +} + +- (void)bkrSetFormPostParameters:(NSDictionary*)parameters { + NSString *formPostParams = [NSMutableURLRequest bkrEncodeFormPostParameters: parameters]; + + [self setHTTPBody: [formPostParams dataUsingEncoding: NSUTF8StringEncoding]]; + [self setValue: @"application/x-www-form-urlencoded; charset=utf-8" forHTTPHeaderField: @"Content-Type"]; +} + +@end diff --git a/BakerShelf/lib/NSURL+BakerExtensions.h b/BakerShelf/lib/NSURL+BakerExtensions.h new file mode 100644 index 0000000..ea8039b --- /dev/null +++ b/BakerShelf/lib/NSURL+BakerExtensions.h @@ -0,0 +1,40 @@ +// +// NSURL+Extensions.h +// Baker +// See: http://stackoverflow.com/a/6312153/551557 +// +// ========================================================================================== +// +// Copyright (c) 2010-2013, Davide Casali, Marco Colombo, Alessandro Morandi +// Copyright (c) 2014, Andrew Krowczyk, Cédric Mériau +// All rights reserved. +// +// 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. +// Neither the name of the Baker Framework nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written +// permission. +// 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. +// + +#import + +@interface NSURL (BakerExtensions) + +- (NSURL*)bkrURLByAppendingQueryString:(NSString*)queryString; + +@end diff --git a/BakerShelf/lib/NSURL+BakerExtensions.m b/BakerShelf/lib/NSURL+BakerExtensions.m new file mode 100644 index 0000000..950e503 --- /dev/null +++ b/BakerShelf/lib/NSURL+BakerExtensions.m @@ -0,0 +1,49 @@ +// +// NSURL+Extensions.m +// Baker +// See: http://stackoverflow.com/a/6312153/551557 +// +// ========================================================================================== +// +// Copyright (c) 2010-2013, Davide Casali, Marco Colombo, Alessandro Morandi +// Copyright (c) 2014, Andrew Krowczyk, Cédric Mériau +// All rights reserved. +// +// 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. +// Neither the name of the Baker Framework nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written +// permission. +// 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. +// + +#import "NSURL+BakerExtensions.h" + +@implementation NSURL (BakerExtensions) + +- (NSURL*)bkrURLByAppendingQueryString:(NSString*)queryString { + if (![queryString length]) { + return self; + } + + NSString *URLString = [[NSString alloc] initWithFormat:@"%@%@%@", [self absoluteString], + [self query] ? @"&" : @"?", queryString]; + NSURL *theURL = [NSURL URLWithString:URLString]; + return theURL; +} + +@end diff --git a/BakerShelf/lib/minizip/crypt.h b/BakerShelf/lib/minizip/crypt.h new file mode 100755 index 0000000..a01d08d --- /dev/null +++ b/BakerShelf/lib/minizip/crypt.h @@ -0,0 +1,131 @@ +/* crypt.h -- base code for crypt/uncrypt ZIPfile + + + Version 1.01e, February 12th, 2005 + + Copyright (C) 1998-2005 Gilles Vollant + + This code is a modified version of crypting code in Infozip distribution + + The encryption/decryption parts of this source code (as opposed to the + non-echoing password parts) were originally written in Europe. The + whole source package can be freely distributed, including from the USA. + (Prior to January 2000, re-export from the US was a violation of US law.) + + This encryption code is a direct transcription of the algorithm from + Roger Schlafly, described by Phil Katz in the file appnote.txt. This + file (appnote.txt) is distributed with the PKZIP program (even in the + version without encryption capabilities). + + If you don't need crypting in your application, just define symbols + NOCRYPT and NOUNCRYPT. + + This code support the "Traditional PKWARE Encryption". + + The new AES encryption added on Zip format by Winzip (see the page + http://www.winzip.com/aes_info.htm ) and PKWare PKZip 5.x Strong + Encryption is not supported. +*/ + +#define CRC32(c, b) ((*(pcrc_32_tab+(((int)(c) ^ (b)) & 0xff))) ^ ((c) >> 8)) + +/*********************************************************************** + * Return the next byte in the pseudo-random sequence + */ +static int decrypt_byte(unsigned long* pkeys, const unsigned long* pcrc_32_tab) +{ + unsigned temp; /* POTENTIAL BUG: temp*(temp^1) may overflow in an + * unpredictable manner on 16-bit systems; not a problem + * with any known compiler so far, though */ + + temp = ((unsigned)(*(pkeys+2)) & 0xffff) | 2; + return (int)(((temp * (temp ^ 1)) >> 8) & 0xff); +} + +/*********************************************************************** + * Update the encryption keys with the next byte of plain text + */ +static int update_keys(unsigned long* pkeys,const unsigned long* pcrc_32_tab,int c) +{ + (*(pkeys+0)) = CRC32((*(pkeys+0)), c); + (*(pkeys+1)) += (*(pkeys+0)) & 0xff; + (*(pkeys+1)) = (*(pkeys+1)) * 134775813L + 1; + { + register int keyshift = (int)((*(pkeys+1)) >> 24); + (*(pkeys+2)) = CRC32((*(pkeys+2)), keyshift); + } + return c; +} + + +/*********************************************************************** + * Initialize the encryption keys and the random header according to + * the given password. + */ +static void init_keys(const char* passwd,unsigned long* pkeys,const unsigned long* pcrc_32_tab) +{ + *(pkeys+0) = 305419896L; + *(pkeys+1) = 591751049L; + *(pkeys+2) = 878082192L; + while (*passwd != '\0') { + update_keys(pkeys,pcrc_32_tab,(int)*passwd); + passwd++; + } +} + +#define zdecode(pkeys,pcrc_32_tab,c) \ + (update_keys(pkeys,pcrc_32_tab,c ^= decrypt_byte(pkeys,pcrc_32_tab))) + +#define zencode(pkeys,pcrc_32_tab,c,t) \ + (t=decrypt_byte(pkeys,pcrc_32_tab), update_keys(pkeys,pcrc_32_tab,c), t^(c)) + +#ifdef INCLUDECRYPTINGCODE_IFCRYPTALLOWED + +#define RAND_HEAD_LEN 12 + /* "last resort" source for second part of crypt seed pattern */ +# ifndef ZCR_SEED2 +# define ZCR_SEED2 3141592654UL /* use PI as default pattern */ +# endif + +static int crypthead(const char* passwd, /* password string */ + unsigned char* buf, /* where to write header */ + int bufSize, + unsigned long* pkeys, + const unsigned long* pcrc_32_tab, + unsigned long crcForCrypting) +{ + int n; /* index in random header */ + int t; /* temporary */ + int c; /* random byte */ + unsigned char header[RAND_HEAD_LEN-2]; /* random header */ + static unsigned calls = 0; /* ensure different random header each time */ + + if (bufSize> 7) & 0xff; + header[n] = (unsigned char)zencode(pkeys, pcrc_32_tab, c, t); + } + /* Encrypt random header (last two bytes is high word of crc) */ + init_keys(passwd, pkeys, pcrc_32_tab); + for (n = 0; n < RAND_HEAD_LEN-2; n++) + { + buf[n] = (unsigned char)zencode(pkeys, pcrc_32_tab, header[n], t); + } + buf[n++] = (unsigned char)zencode(pkeys, pcrc_32_tab, (int)(crcForCrypting >> 16) & 0xff, t); + buf[n++] = (unsigned char)zencode(pkeys, pcrc_32_tab, (int)(crcForCrypting >> 24) & 0xff, t); + return n; +} + +#endif diff --git a/BakerShelf/lib/minizip/ioapi.c b/BakerShelf/lib/minizip/ioapi.c new file mode 100755 index 0000000..bd6dd95 --- /dev/null +++ b/BakerShelf/lib/minizip/ioapi.c @@ -0,0 +1,239 @@ +/* ioapi.h -- IO base function header for compress/uncompress .zip + part of the MiniZip project - ( http://www.winimage.com/zLibDll/minizip.html ) + + Copyright (C) 1998-2010 Gilles Vollant (minizip) ( http://www.winimage.com/zLibDll/minizip.html ) + + Modifications for Zip64 support + Copyright (C) 2009-2010 Mathias Svensson ( http://result42.com ) + + For more info read MiniZip_info.txt + +*/ + +#if (defined(_WIN32)) + #define _CRT_SECURE_NO_WARNINGS +#endif + +#include "ioapi.h" + +voidpf call_zopen64 (const zlib_filefunc64_32_def* pfilefunc,const void*filename,int mode) +{ + if (pfilefunc->zfile_func64.zopen64_file != NULL) + return (*(pfilefunc->zfile_func64.zopen64_file)) (pfilefunc->zfile_func64.opaque,filename,mode); + else + { + return (*(pfilefunc->zopen32_file))(pfilefunc->zfile_func64.opaque,(const char*)filename,mode); + } +} + +long call_zseek64 (const zlib_filefunc64_32_def* pfilefunc,voidpf filestream, ZPOS64_T offset, int origin) +{ + if (pfilefunc->zfile_func64.zseek64_file != NULL) + return (*(pfilefunc->zfile_func64.zseek64_file)) (pfilefunc->zfile_func64.opaque,filestream,offset,origin); + else + { + uLong offsetTruncated = (uLong)offset; + if (offsetTruncated != offset) + return -1; + else + return (*(pfilefunc->zseek32_file))(pfilefunc->zfile_func64.opaque,filestream,offsetTruncated,origin); + } +} + +ZPOS64_T call_ztell64 (const zlib_filefunc64_32_def* pfilefunc,voidpf filestream) +{ + if (pfilefunc->zfile_func64.zseek64_file != NULL) + return (*(pfilefunc->zfile_func64.ztell64_file)) (pfilefunc->zfile_func64.opaque,filestream); + else + { + uLong tell_uLong = (*(pfilefunc->ztell32_file))(pfilefunc->zfile_func64.opaque,filestream); + if ((tell_uLong) == ((uLong)-1)) + return (ZPOS64_T)-1; + else + return tell_uLong; + } +} + +void fill_zlib_filefunc64_32_def_from_filefunc32(zlib_filefunc64_32_def* p_filefunc64_32,const zlib_filefunc_def* p_filefunc32) +{ + p_filefunc64_32->zfile_func64.zopen64_file = NULL; + p_filefunc64_32->zopen32_file = p_filefunc32->zopen_file; + p_filefunc64_32->zfile_func64.zerror_file = p_filefunc32->zerror_file; + p_filefunc64_32->zfile_func64.zread_file = p_filefunc32->zread_file; + p_filefunc64_32->zfile_func64.zwrite_file = p_filefunc32->zwrite_file; + p_filefunc64_32->zfile_func64.ztell64_file = NULL; + p_filefunc64_32->zfile_func64.zseek64_file = NULL; + p_filefunc64_32->zfile_func64.zclose_file = p_filefunc32->zclose_file; + +#ifndef __clang_analyzer__ + p_filefunc64_32->zfile_func64.zerror_file = p_filefunc32->zerror_file; +#endif + + p_filefunc64_32->zfile_func64.opaque = p_filefunc32->opaque; + p_filefunc64_32->zseek32_file = p_filefunc32->zseek_file; + p_filefunc64_32->ztell32_file = p_filefunc32->ztell_file; +} + + + +static voidpf ZCALLBACK fopen_file_func OF((voidpf opaque, const char* filename, int mode)); +static uLong ZCALLBACK fread_file_func OF((voidpf opaque, voidpf stream, void* buf, uLong size)); +static uLong ZCALLBACK fwrite_file_func OF((voidpf opaque, voidpf stream, const void* buf,uLong size)); +static ZPOS64_T ZCALLBACK ftell64_file_func OF((voidpf opaque, voidpf stream)); +static long ZCALLBACK fseek64_file_func OF((voidpf opaque, voidpf stream, ZPOS64_T offset, int origin)); +static int ZCALLBACK fclose_file_func OF((voidpf opaque, voidpf stream)); +static int ZCALLBACK ferror_file_func OF((voidpf opaque, voidpf stream)); + +static voidpf ZCALLBACK fopen_file_func (voidpf opaque, const char* filename, int mode) +{ + FILE* file = NULL; + const char* mode_fopen = NULL; + if ((mode & ZLIB_FILEFUNC_MODE_READWRITEFILTER)==ZLIB_FILEFUNC_MODE_READ) + mode_fopen = "rb"; + else + if (mode & ZLIB_FILEFUNC_MODE_EXISTING) + mode_fopen = "r+b"; + else + if (mode & ZLIB_FILEFUNC_MODE_CREATE) + mode_fopen = "wb"; + + if ((filename!=NULL) && (mode_fopen != NULL)) + file = fopen(filename, mode_fopen); + return file; +} + +static voidpf ZCALLBACK fopen64_file_func (voidpf opaque, const void* filename, int mode) +{ + FILE* file = NULL; + const char* mode_fopen = NULL; + if ((mode & ZLIB_FILEFUNC_MODE_READWRITEFILTER)==ZLIB_FILEFUNC_MODE_READ) + mode_fopen = "rb"; + else + if (mode & ZLIB_FILEFUNC_MODE_EXISTING) + mode_fopen = "r+b"; + else + if (mode & ZLIB_FILEFUNC_MODE_CREATE) + mode_fopen = "wb"; + + if ((filename!=NULL) && (mode_fopen != NULL)) + file = fopen64((const char*)filename, mode_fopen); + return file; +} + + +static uLong ZCALLBACK fread_file_func (voidpf opaque, voidpf stream, void* buf, uLong size) +{ + uLong ret; + ret = (uLong)fread(buf, 1, (size_t)size, (FILE *)stream); + return ret; +} + +static uLong ZCALLBACK fwrite_file_func (voidpf opaque, voidpf stream, const void* buf, uLong size) +{ + uLong ret; + ret = (uLong)fwrite(buf, 1, (size_t)size, (FILE *)stream); + return ret; +} + +static long ZCALLBACK ftell_file_func (voidpf opaque, voidpf stream) +{ + long ret; + ret = ftell((FILE *)stream); + return ret; +} + + +static ZPOS64_T ZCALLBACK ftell64_file_func (voidpf opaque, voidpf stream) +{ + ZPOS64_T ret; + ret = ftello64((FILE *)stream); + return ret; +} + +static long ZCALLBACK fseek_file_func (voidpf opaque, voidpf stream, uLong offset, int origin) +{ + int fseek_origin=0; + long ret; + switch (origin) + { + case ZLIB_FILEFUNC_SEEK_CUR : + fseek_origin = SEEK_CUR; + break; + case ZLIB_FILEFUNC_SEEK_END : + fseek_origin = SEEK_END; + break; + case ZLIB_FILEFUNC_SEEK_SET : + fseek_origin = SEEK_SET; + break; + default: return -1; + } + ret = 0; + if (fseek((FILE *)stream, offset, fseek_origin) != 0) + ret = -1; + return ret; +} + +static long ZCALLBACK fseek64_file_func (voidpf opaque, voidpf stream, ZPOS64_T offset, int origin) +{ + int fseek_origin=0; + long ret; + switch (origin) + { + case ZLIB_FILEFUNC_SEEK_CUR : + fseek_origin = SEEK_CUR; + break; + case ZLIB_FILEFUNC_SEEK_END : + fseek_origin = SEEK_END; + break; + case ZLIB_FILEFUNC_SEEK_SET : + fseek_origin = SEEK_SET; + break; + default: return -1; + } + ret = 0; + + if(fseeko64((FILE *)stream, (long)offset, fseek_origin) != 0) + ret = -1; + + return ret; +} + + +static int ZCALLBACK fclose_file_func (voidpf opaque, voidpf stream) +{ + int ret; + ret = fclose((FILE *)stream); + return ret; +} + +static int ZCALLBACK ferror_file_func (voidpf opaque, voidpf stream) +{ + int ret; + ret = ferror((FILE *)stream); + return ret; +} + +void fill_fopen_filefunc (pzlib_filefunc_def) + zlib_filefunc_def* pzlib_filefunc_def; +{ + pzlib_filefunc_def->zopen_file = fopen_file_func; + pzlib_filefunc_def->zread_file = fread_file_func; + pzlib_filefunc_def->zwrite_file = fwrite_file_func; + pzlib_filefunc_def->ztell_file = ftell_file_func; + pzlib_filefunc_def->zseek_file = fseek_file_func; + pzlib_filefunc_def->zclose_file = fclose_file_func; + pzlib_filefunc_def->zerror_file = ferror_file_func; + pzlib_filefunc_def->opaque = NULL; +} + +void fill_fopen64_filefunc (zlib_filefunc64_def* pzlib_filefunc_def) +{ + pzlib_filefunc_def->zopen64_file = fopen64_file_func; + pzlib_filefunc_def->zread_file = fread_file_func; + pzlib_filefunc_def->zwrite_file = fwrite_file_func; + pzlib_filefunc_def->ztell64_file = ftell64_file_func; + pzlib_filefunc_def->zseek64_file = fseek64_file_func; + pzlib_filefunc_def->zclose_file = fclose_file_func; + pzlib_filefunc_def->zerror_file = ferror_file_func; + pzlib_filefunc_def->opaque = NULL; +} diff --git a/BakerShelf/lib/minizip/ioapi.h b/BakerShelf/lib/minizip/ioapi.h new file mode 100755 index 0000000..7e20e95 --- /dev/null +++ b/BakerShelf/lib/minizip/ioapi.h @@ -0,0 +1,201 @@ +/* ioapi.h -- IO base function header for compress/uncompress .zip + part of the MiniZip project - ( http://www.winimage.com/zLibDll/minizip.html ) + + Copyright (C) 1998-2010 Gilles Vollant (minizip) ( http://www.winimage.com/zLibDll/minizip.html ) + + Modifications for Zip64 support + Copyright (C) 2009-2010 Mathias Svensson ( http://result42.com ) + + For more info read MiniZip_info.txt + + Changes + + Oct-2009 - Defined ZPOS64_T to fpos_t on windows and u_int64_t on linux. (might need to find a better why for this) + Oct-2009 - Change to fseeko64, ftello64 and fopen64 so large files would work on linux. + More if/def section may be needed to support other platforms + Oct-2009 - Defined fxxxx64 calls to normal fopen/ftell/fseek so they would compile on windows. + (but you should use iowin32.c for windows instead) + +*/ + +#ifndef _ZLIBIOAPI64_H +#define _ZLIBIOAPI64_H + +#if (!defined(_WIN32)) && (!defined(WIN32)) + + // Linux needs this to support file operation on files larger then 4+GB + // But might need better if/def to select just the platforms that needs them. + + #ifndef __USE_FILE_OFFSET64 + #define __USE_FILE_OFFSET64 + #endif + #ifndef __USE_LARGEFILE64 + #define __USE_LARGEFILE64 + #endif + #ifndef _LARGEFILE64_SOURCE + #define _LARGEFILE64_SOURCE + #endif + #ifndef _FILE_OFFSET_BIT + #define _FILE_OFFSET_BIT 64 + #endif +#endif + +#include +#include +#include "zlib.h" + +#define USE_FILE32API +#if defined(USE_FILE32API) +#define fopen64 fopen +#define ftello64 ftell +#define fseeko64 fseek +#else +#ifdef _MSC_VER + #define fopen64 fopen + #if (_MSC_VER >= 1400) && (!(defined(NO_MSCVER_FILE64_FUNC))) + #define ftello64 _ftelli64 + #define fseeko64 _fseeki64 + #else // old MSC + #define ftello64 ftell + #define fseeko64 fseek + #endif +#endif +#endif + +/* +#ifndef ZPOS64_T + #ifdef _WIN32 + #define ZPOS64_T fpos_t + #else + #include + #define ZPOS64_T uint64_t + #endif +#endif +*/ + +#ifdef HAVE_MINIZIP64_CONF_H +#include "mz64conf.h" +#endif + +/* a type choosen by DEFINE */ +#ifdef HAVE_64BIT_INT_CUSTOM +typedef 64BIT_INT_CUSTOM_TYPE ZPOS64_T; +#else +#ifdef HAS_STDINT_H +#include "stdint.h" +typedef uint64_t ZPOS64_T; +#else + + +#if defined(_MSC_VER) || defined(__BORLANDC__) +typedef unsigned __int64 ZPOS64_T; +#else +typedef unsigned long long int ZPOS64_T; +#endif +#endif +#endif + + + +#ifdef __cplusplus +extern "C" { +#endif + + +#define ZLIB_FILEFUNC_SEEK_CUR (1) +#define ZLIB_FILEFUNC_SEEK_END (2) +#define ZLIB_FILEFUNC_SEEK_SET (0) + +#define ZLIB_FILEFUNC_MODE_READ (1) +#define ZLIB_FILEFUNC_MODE_WRITE (2) +#define ZLIB_FILEFUNC_MODE_READWRITEFILTER (3) + +#define ZLIB_FILEFUNC_MODE_EXISTING (4) +#define ZLIB_FILEFUNC_MODE_CREATE (8) + + +#ifndef ZCALLBACK + #if (defined(WIN32) || defined(_WIN32) || defined (WINDOWS) || defined (_WINDOWS)) && defined(CALLBACK) && defined (USEWINDOWS_CALLBACK) + #define ZCALLBACK CALLBACK + #else + #define ZCALLBACK + #endif +#endif + + + + +typedef voidpf (ZCALLBACK *open_file_func) OF((voidpf opaque, const char* filename, int mode)); +typedef uLong (ZCALLBACK *read_file_func) OF((voidpf opaque, voidpf stream, void* buf, uLong size)); +typedef uLong (ZCALLBACK *write_file_func) OF((voidpf opaque, voidpf stream, const void* buf, uLong size)); +typedef int (ZCALLBACK *close_file_func) OF((voidpf opaque, voidpf stream)); +typedef int (ZCALLBACK *testerror_file_func) OF((voidpf opaque, voidpf stream)); + +typedef long (ZCALLBACK *tell_file_func) OF((voidpf opaque, voidpf stream)); +typedef long (ZCALLBACK *seek_file_func) OF((voidpf opaque, voidpf stream, uLong offset, int origin)); + + +/* here is the "old" 32 bits structure structure */ +typedef struct zlib_filefunc_def_s +{ + open_file_func zopen_file; + read_file_func zread_file; + write_file_func zwrite_file; + tell_file_func ztell_file; + seek_file_func zseek_file; + close_file_func zclose_file; + testerror_file_func zerror_file; + voidpf opaque; +} zlib_filefunc_def; + +typedef ZPOS64_T (ZCALLBACK *tell64_file_func) OF((voidpf opaque, voidpf stream)); +typedef long (ZCALLBACK *seek64_file_func) OF((voidpf opaque, voidpf stream, ZPOS64_T offset, int origin)); +typedef voidpf (ZCALLBACK *open64_file_func) OF((voidpf opaque, const void* filename, int mode)); + +typedef struct zlib_filefunc64_def_s +{ + open64_file_func zopen64_file; + read_file_func zread_file; + write_file_func zwrite_file; + tell64_file_func ztell64_file; + seek64_file_func zseek64_file; + close_file_func zclose_file; + testerror_file_func zerror_file; + voidpf opaque; +} zlib_filefunc64_def; + +void fill_fopen64_filefunc OF((zlib_filefunc64_def* pzlib_filefunc_def)); +void fill_fopen_filefunc OF((zlib_filefunc_def* pzlib_filefunc_def)); + +/* now internal definition, only for zip.c and unzip.h */ +typedef struct zlib_filefunc64_32_def_s +{ + zlib_filefunc64_def zfile_func64; + open_file_func zopen32_file; + tell_file_func ztell32_file; + seek_file_func zseek32_file; +} zlib_filefunc64_32_def; + + +#define ZREAD64(filefunc,filestream,buf,size) ((*((filefunc).zfile_func64.zread_file)) ((filefunc).zfile_func64.opaque,filestream,buf,size)) +#define ZWRITE64(filefunc,filestream,buf,size) ((*((filefunc).zfile_func64.zwrite_file)) ((filefunc).zfile_func64.opaque,filestream,buf,size)) +//#define ZTELL64(filefunc,filestream) ((*((filefunc).ztell64_file)) ((filefunc).opaque,filestream)) +//#define ZSEEK64(filefunc,filestream,pos,mode) ((*((filefunc).zseek64_file)) ((filefunc).opaque,filestream,pos,mode)) +#define ZCLOSE64(filefunc,filestream) ((*((filefunc).zfile_func64.zclose_file)) ((filefunc).zfile_func64.opaque,filestream)) +#define ZERROR64(filefunc,filestream) ((*((filefunc).zfile_func64.zerror_file)) ((filefunc).zfile_func64.opaque,filestream)) + +voidpf call_zopen64 OF((const zlib_filefunc64_32_def* pfilefunc,const void*filename,int mode)); +long call_zseek64 OF((const zlib_filefunc64_32_def* pfilefunc,voidpf filestream, ZPOS64_T offset, int origin)); +ZPOS64_T call_ztell64 OF((const zlib_filefunc64_32_def* pfilefunc,voidpf filestream)); + +void fill_zlib_filefunc64_32_def_from_filefunc32(zlib_filefunc64_32_def* p_filefunc64_32,const zlib_filefunc_def* p_filefunc32); + +#define ZOPEN64(filefunc,filename,mode) (call_zopen64((&(filefunc)),(filename),(mode))) +#define ZTELL64(filefunc,filestream) (call_ztell64((&(filefunc)),(filestream))) +#define ZSEEK64(filefunc,filestream,pos,mode) (call_zseek64((&(filefunc)),(filestream),(pos),(mode))) + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/BakerShelf/lib/minizip/mztools.c b/BakerShelf/lib/minizip/mztools.c new file mode 100755 index 0000000..80d50e0 --- /dev/null +++ b/BakerShelf/lib/minizip/mztools.c @@ -0,0 +1,284 @@ +/* + Additional tools for Minizip + Code: Xavier Roche '2004 + License: Same as ZLIB (www.gzip.org) +*/ + +/* Code */ +#include +#include +#include +#include "zlib.h" +#include "unzip.h" +#include "mztools.h" + +#define READ_8(adr) ((unsigned char)*(adr)) +#define READ_16(adr) ( READ_8(adr) | (READ_8(adr+1) << 8) ) +#define READ_32(adr) ( READ_16(adr) | (READ_16((adr)+2) << 16) ) + +#define WRITE_8(buff, n) do { \ + *((unsigned char*)(buff)) = (unsigned char) ((n) & 0xff); \ +} while(0) +#define WRITE_16(buff, n) do { \ + WRITE_8((unsigned char*)(buff), n); \ + WRITE_8(((unsigned char*)(buff)) + 1, (n) >> 8); \ +} while(0) +#define WRITE_32(buff, n) do { \ + WRITE_16((unsigned char*)(buff), (n) & 0xffff); \ + WRITE_16((unsigned char*)(buff) + 2, (n) >> 16); \ +} while(0) + +extern int ZEXPORT unzRepair(file, fileOut, fileOutTmp, nRecovered, bytesRecovered) +const char* file; +const char* fileOut; +const char* fileOutTmp; +uLong* nRecovered; +uLong* bytesRecovered; +{ + int err = Z_OK; + FILE* fpZip = fopen(file, "rb"); + FILE* fpOut = fopen(fileOut, "wb"); + FILE* fpOutCD = fopen(fileOutTmp, "wb"); + if (fpZip != NULL && fpOut != NULL) { + int entries = 0; + uLong totalBytes = 0; + char header[30]; + char filename[256]; + char extra[1024]; + int offset = 0; + int offsetCD = 0; + while ( fread(header, 1, 30, fpZip) == 30 ) { + int currentOffset = offset; + + /* File entry */ + if (READ_32(header) == 0x04034b50) { + unsigned int version = READ_16(header + 4); + unsigned int gpflag = READ_16(header + 6); + unsigned int method = READ_16(header + 8); + unsigned int filetime = READ_16(header + 10); + unsigned int filedate = READ_16(header + 12); + unsigned int crc = READ_32(header + 14); /* crc */ + unsigned int cpsize = READ_32(header + 18); /* compressed size */ + unsigned int uncpsize = READ_32(header + 22); /* uncompressed sz */ + unsigned int fnsize = READ_16(header + 26); /* file name length */ + unsigned int extsize = READ_16(header + 28); /* extra field length */ + filename[0] = extra[0] = '\0'; + + /* Header */ + if (fwrite(header, 1, 30, fpOut) == 30) { + offset += 30; + } else { + err = Z_ERRNO; + break; + } + + /* Filename */ + if (fnsize > 0) { + if (fread(filename, 1, fnsize, fpZip) == fnsize) { + if (fwrite(filename, 1, fnsize, fpOut) == fnsize) { + offset += fnsize; + } else { + err = Z_ERRNO; + break; + } + } else { + err = Z_ERRNO; + break; + } + } else { + err = Z_STREAM_ERROR; + break; + } + + /* Extra field */ + if (extsize > 0) { + if (fread(extra, 1, extsize, fpZip) == extsize) { + if (fwrite(extra, 1, extsize, fpOut) == extsize) { + offset += extsize; + } else { + err = Z_ERRNO; + break; + } + } else { + err = Z_ERRNO; + break; + } + } + + /* Data */ + { + int dataSize = cpsize; + if (dataSize == 0) { + dataSize = uncpsize; + } + if (dataSize > 0) { + char* data = malloc(dataSize); + if (data != NULL) { + if ((int)fread(data, 1, dataSize, fpZip) == dataSize) { + if ((int)fwrite(data, 1, dataSize, fpOut) == dataSize) { + offset += dataSize; + totalBytes += dataSize; + } else { + err = Z_ERRNO; + } + } else { + err = Z_ERRNO; + } + free(data); + if (err != Z_OK) { + break; + } + } else { + err = Z_MEM_ERROR; + break; + } + } + } + + /* Central directory entry */ + { + char centralDirectoryEntryHeader[46]; + //char* comment = ""; + //int comsize = (int) strlen(comment); + WRITE_32(centralDirectoryEntryHeader, 0x02014b50); + WRITE_16(centralDirectoryEntryHeader + 4, version); + WRITE_16(centralDirectoryEntryHeader + 6, version); + WRITE_16(centralDirectoryEntryHeader + 8, gpflag); + WRITE_16(centralDirectoryEntryHeader + 10, method); + WRITE_16(centralDirectoryEntryHeader + 12, filetime); + WRITE_16(centralDirectoryEntryHeader + 14, filedate); + WRITE_32(centralDirectoryEntryHeader + 16, crc); + WRITE_32(centralDirectoryEntryHeader + 20, cpsize); + WRITE_32(centralDirectoryEntryHeader + 24, uncpsize); + WRITE_16(centralDirectoryEntryHeader + 28, fnsize); + WRITE_16(centralDirectoryEntryHeader + 30, extsize); + WRITE_16(centralDirectoryEntryHeader + 32, 0 /*comsize*/); + WRITE_16(centralDirectoryEntryHeader + 34, 0); /* disk # */ + WRITE_16(centralDirectoryEntryHeader + 36, 0); /* int attrb */ + WRITE_32(centralDirectoryEntryHeader + 38, 0); /* ext attrb */ + WRITE_32(centralDirectoryEntryHeader + 42, currentOffset); + /* Header */ + if (fwrite(centralDirectoryEntryHeader, 1, 46, fpOutCD) == 46) { + offsetCD += 46; + + /* Filename */ + if (fnsize > 0) { + if (fwrite(filename, 1, fnsize, fpOutCD) == fnsize) { + offsetCD += fnsize; + } else { + err = Z_ERRNO; + break; + } + } else { + err = Z_STREAM_ERROR; + break; + } + + /* Extra field */ + if (extsize > 0) { + if (fwrite(extra, 1, extsize, fpOutCD) == extsize) { + offsetCD += extsize; + } else { + err = Z_ERRNO; + break; + } + } + + /* Comment field */ + /* + if (comsize > 0) { + if ((int)fwrite(comment, 1, comsize, fpOutCD) == comsize) { + offsetCD += comsize; + } else { + err = Z_ERRNO; + break; + } + } + */ + + } else { + err = Z_ERRNO; + break; + } + } + + /* Success */ + entries++; + + } else { + break; + } + } + + /* Final central directory */ + { + int entriesZip = entries; + char finalCentralDirectoryHeader[22]; + //char* comment = ""; // "ZIP File recovered by zlib/minizip/mztools"; + //int comsize = (int) strlen(comment); + if (entriesZip > 0xffff) { + entriesZip = 0xffff; + } + WRITE_32(finalCentralDirectoryHeader, 0x06054b50); + WRITE_16(finalCentralDirectoryHeader + 4, 0); /* disk # */ + WRITE_16(finalCentralDirectoryHeader + 6, 0); /* disk # */ + WRITE_16(finalCentralDirectoryHeader + 8, entriesZip); /* hack */ + WRITE_16(finalCentralDirectoryHeader + 10, entriesZip); /* hack */ + WRITE_32(finalCentralDirectoryHeader + 12, offsetCD); /* size of CD */ + WRITE_32(finalCentralDirectoryHeader + 16, offset); /* offset to CD */ + WRITE_16(finalCentralDirectoryHeader + 20, 0 /*comsize*/); /* comment */ + + /* Header */ + if (fwrite(finalCentralDirectoryHeader, 1, 22, fpOutCD) == 22) { + + /* Comment field */ + /* + if (comsize > 0) { + if ((int)fwrite(comment, 1, comsize, fpOutCD) != comsize) { + err = Z_ERRNO; + } + } + */ + } else { + err = Z_ERRNO; + } + } + + /* Final merge (file + central directory) */ + fclose(fpOutCD); + if (err == Z_OK) { + fpOutCD = fopen(fileOutTmp, "rb"); + if (fpOutCD != NULL) { + int nRead; + char buffer[8192]; + while ( (nRead = (int)fread(buffer, 1, sizeof(buffer), fpOutCD)) > 0) { + if ((int)fwrite(buffer, 1, nRead, fpOut) != nRead) { + err = Z_ERRNO; + break; + } + } + fclose(fpOutCD); + } + } + + /* Close */ + fclose(fpZip); + fclose(fpOut); + + /* Wipe temporary file */ + (void)remove(fileOutTmp); + + /* Number of recovered entries */ + if (err == Z_OK) { + if (nRecovered != NULL) { + *nRecovered = entries; + } + if (bytesRecovered != NULL) { + *bytesRecovered = totalBytes; + } + } + } else { + err = Z_STREAM_ERROR; + } + return err; +} diff --git a/BakerShelf/lib/minizip/mztools.h b/BakerShelf/lib/minizip/mztools.h new file mode 100755 index 0000000..88b3459 --- /dev/null +++ b/BakerShelf/lib/minizip/mztools.h @@ -0,0 +1,31 @@ +/* + Additional tools for Minizip + Code: Xavier Roche '2004 + License: Same as ZLIB (www.gzip.org) +*/ + +#ifndef _zip_tools_H +#define _zip_tools_H + +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef _ZLIB_H +#include "zlib.h" +#endif + +#include "unzip.h" + +/* Repair a ZIP file (missing central directory) + file: file to recover + fileOut: output file after recovery + fileOutTmp: temporary file name used for recovery +*/ +extern int ZEXPORT unzRepair(const char* file, + const char* fileOut, + const char* fileOutTmp, + uLong* nRecovered, + uLong* bytesRecovered); + +#endif diff --git a/BakerShelf/lib/minizip/unzip.c b/BakerShelf/lib/minizip/unzip.c new file mode 100755 index 0000000..9cb1743 --- /dev/null +++ b/BakerShelf/lib/minizip/unzip.c @@ -0,0 +1,2152 @@ +/* unzip.c -- IO for uncompress .zip files using zlib + Version 1.1, February 14h, 2010 + part of the MiniZip project - ( http://www.winimage.com/zLibDll/minizip.html ) + + Copyright (C) 1998-2010 Gilles Vollant (minizip) ( http://www.winimage.com/zLibDll/minizip.html ) + + Modifications of Unzip for Zip64 + Copyright (C) 2007-2008 Even Rouault + + Modifications for Zip64 support on both zip and unzip + Copyright (C) 2009-2010 Mathias Svensson ( http://result42.com ) + + For more info read MiniZip_info.txt + + + ------------------------------------------------------------------------------------ + Decryption code comes from crypt.c by Info-ZIP but has been greatly reduced in terms of + compatibility with older software. The following is from the original crypt.c. + Code woven in by Terry Thorsen 1/2003. + + Copyright (c) 1990-2000 Info-ZIP. All rights reserved. + + See the accompanying file LICENSE, version 2000-Apr-09 or later + (the contents of which are also included in zip.h) for terms of use. + If, for some reason, all these files are missing, the Info-ZIP license + also may be found at: ftp://ftp.info-zip.org/pub/infozip/license.html + + crypt.c (full version) by Info-ZIP. Last revised: [see crypt.h] + + The encryption/decryption parts of this source code (as opposed to the + non-echoing password parts) were originally written in Europe. The + whole source package can be freely distributed, including from the USA. + (Prior to January 2000, re-export from the US was a violation of US law.) + + This encryption code is a direct transcription of the algorithm from + Roger Schlafly, described by Phil Katz in the file appnote.txt. This + file (appnote.txt) is distributed with the PKZIP program (even in the + version without encryption capabilities). + + ------------------------------------------------------------------------------------ + + Changes in unzip.c + + 2007-2008 - Even Rouault - Addition of cpl_unzGetCurrentFileZStreamPos + 2007-2008 - Even Rouault - Decoration of symbol names unz* -> cpl_unz* + 2007-2008 - Even Rouault - Remove old C style function prototypes + 2007-2008 - Even Rouault - Add unzip support for ZIP64 + + Copyright (C) 2007-2008 Even Rouault + + + Oct-2009 - Mathias Svensson - Removed cpl_* from symbol names (Even Rouault added them but since this is now moved to a new project (minizip64) I renamed them again). + Oct-2009 - Mathias Svensson - Fixed problem if uncompressed size was > 4G and compressed size was <4G + should only read the compressed/uncompressed size from the Zip64 format if + the size from normal header was 0xFFFFFFFF + Oct-2009 - Mathias Svensson - Applied some bug fixes from paches recived from Gilles Vollant + Oct-2009 - Mathias Svensson - Applied support to unzip files with compression mathod BZIP2 (bzip2 lib is required) + Patch created by Daniel Borca + + Jan-2010 - back to unzip and minizip 1.0 name scheme, with compatibility layer + + Copyright (C) 1998 - 2010 Gilles Vollant, Even Rouault, Mathias Svensson + +*/ + + +#include +#include +#include + +//#ifndef NOUNCRYPT +// #define NOUNCRYPT +//#endif + +#include "zlib.h" +#include "unzip.h" + +#ifdef STDC +# include +# include +# include +#endif +#ifdef NO_ERRNO_H + extern int errno; +#else +# include +#endif + + +#ifndef local +# define local static +#endif +/* compile with -Dlocal if your debugger can't find static symbols */ + + +#ifndef CASESENSITIVITYDEFAULT_NO +# if !defined(unix) && !defined(CASESENSITIVITYDEFAULT_YES) +# define CASESENSITIVITYDEFAULT_NO +# endif +#endif + + +#ifndef UNZ_BUFSIZE +#define UNZ_BUFSIZE (16384) +#endif + +#ifndef UNZ_MAXFILENAMEINZIP +#define UNZ_MAXFILENAMEINZIP (256) +#endif + +#ifndef ALLOC +# define ALLOC(size) (malloc(size)) +#endif +#ifndef TRYFREE +# define TRYFREE(p) {if (p) free(p);} +#endif + +#define SIZECENTRALDIRITEM (0x2e) +#define SIZEZIPLOCALHEADER (0x1e) + + +const char unz_copyright[] = + " unzip 1.01 Copyright 1998-2004 Gilles Vollant - http://www.winimage.com/zLibDll"; + +/* unz_file_info_interntal contain internal info about a file in zipfile*/ +typedef struct unz_file_info64_internal_s +{ + ZPOS64_T offset_curfile;/* relative offset of local header 8 bytes */ +} unz_file_info64_internal; + + +/* file_in_zip_read_info_s contain internal information about a file in zipfile, + when reading and decompress it */ +typedef struct +{ + char *read_buffer; /* internal buffer for compressed data */ + z_stream stream; /* zLib stream structure for inflate */ + +#ifdef HAVE_BZIP2 + bz_stream bstream; /* bzLib stream structure for bziped */ +#endif + + ZPOS64_T pos_in_zipfile; /* position in byte on the zipfile, for fseek*/ + uLong stream_initialised; /* flag set if stream structure is initialised*/ + + ZPOS64_T offset_local_extrafield;/* offset of the local extra field */ + uInt size_local_extrafield;/* size of the local extra field */ + ZPOS64_T pos_local_extrafield; /* position in the local extra field in read*/ + ZPOS64_T total_out_64; + + uLong crc32; /* crc32 of all data uncompressed */ + uLong crc32_wait; /* crc32 we must obtain after decompress all */ + ZPOS64_T rest_read_compressed; /* number of byte to be decompressed */ + ZPOS64_T rest_read_uncompressed;/*number of byte to be obtained after decomp*/ + zlib_filefunc64_32_def z_filefunc; + voidpf filestream; /* io structore of the zipfile */ + uLong compression_method; /* compression method (0==store) */ + ZPOS64_T byte_before_the_zipfile;/* byte before the zipfile, (>0 for sfx)*/ + int raw; +} file_in_zip64_read_info_s; + + +/* unz64_s contain internal information about the zipfile +*/ +typedef struct +{ + zlib_filefunc64_32_def z_filefunc; + int is64bitOpenFunction; + voidpf filestream; /* io structore of the zipfile */ + unz_global_info64 gi; /* public global information */ + ZPOS64_T byte_before_the_zipfile;/* byte before the zipfile, (>0 for sfx)*/ + ZPOS64_T num_file; /* number of the current file in the zipfile*/ + ZPOS64_T pos_in_central_dir; /* pos of the current file in the central dir*/ + ZPOS64_T current_file_ok; /* flag about the usability of the current file*/ + ZPOS64_T central_pos; /* position of the beginning of the central dir*/ + + ZPOS64_T size_central_dir; /* size of the central directory */ + ZPOS64_T offset_central_dir; /* offset of start of central directory with + respect to the starting disk number */ + + unz_file_info64 cur_file_info; /* public info about the current file in zip*/ + unz_file_info64_internal cur_file_info_internal; /* private info about it*/ + file_in_zip64_read_info_s* pfile_in_zip_read; /* structure about the current + file if we are decompressing it */ + int encrypted; + + int isZip64; + +# ifndef NOUNCRYPT + unsigned long keys[3]; /* keys defining the pseudo-random sequence */ + const unsigned long* pcrc_32_tab; +# endif +} unz64_s; + + +#ifndef NOUNCRYPT +#include "crypt.h" +#endif + +/* =========================================================================== + Read a byte from a gz_stream; update next_in and avail_in. Return EOF + for end of file. + IN assertion: the stream s has been sucessfully opened for reading. +*/ + + +local int unz64local_getByte OF(( + const zlib_filefunc64_32_def* pzlib_filefunc_def, + voidpf filestream, + int *pi)); + +local int unz64local_getByte(const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, int *pi) +{ + unsigned char c; + int err = (int)ZREAD64(*pzlib_filefunc_def,filestream,&c,1); + if (err==1) + { + *pi = (int)c; + return UNZ_OK; + } + else + { + if (ZERROR64(*pzlib_filefunc_def,filestream)) + return UNZ_ERRNO; + else + return UNZ_EOF; + } +} + + +/* =========================================================================== + Reads a long in LSB order from the given gz_stream. Sets +*/ +local int unz64local_getShort OF(( + const zlib_filefunc64_32_def* pzlib_filefunc_def, + voidpf filestream, + uLong *pX)); + +local int unz64local_getShort (const zlib_filefunc64_32_def* pzlib_filefunc_def, + voidpf filestream, + uLong *pX) +{ + uLong x ; + int i = 0; + int err; + + err = unz64local_getByte(pzlib_filefunc_def,filestream,&i); + x = (uLong)i; + + if (err==UNZ_OK) + err = unz64local_getByte(pzlib_filefunc_def,filestream,&i); + x |= ((uLong)i)<<8; + + if (err==UNZ_OK) + *pX = x; + else + *pX = 0; + return err; +} + +local int unz64local_getLong OF(( + const zlib_filefunc64_32_def* pzlib_filefunc_def, + voidpf filestream, + uLong *pX)); + +local int unz64local_getLong (const zlib_filefunc64_32_def* pzlib_filefunc_def, + voidpf filestream, + uLong *pX) +{ + uLong x ; + int i = 0; + int err; + + err = unz64local_getByte(pzlib_filefunc_def,filestream,&i); + x = (uLong)i; + + if (err==UNZ_OK) + err = unz64local_getByte(pzlib_filefunc_def,filestream,&i); + x |= ((uLong)i)<<8; + + if (err==UNZ_OK) + err = unz64local_getByte(pzlib_filefunc_def,filestream,&i); + x |= ((uLong)i)<<16; + + if (err==UNZ_OK) + err = unz64local_getByte(pzlib_filefunc_def,filestream,&i); + x += ((uLong)i)<<24; + + if (err==UNZ_OK) + *pX = x; + else + *pX = 0; + return err; +} + +local int unz64local_getLong64 OF(( + const zlib_filefunc64_32_def* pzlib_filefunc_def, + voidpf filestream, + ZPOS64_T *pX)); + + +local int unz64local_getLong64 (const zlib_filefunc64_32_def* pzlib_filefunc_def, + voidpf filestream, + ZPOS64_T *pX) +{ + ZPOS64_T x ; + int i = 0; + int err; + + err = unz64local_getByte(pzlib_filefunc_def,filestream,&i); + x = (ZPOS64_T)i; + + if (err==UNZ_OK) + err = unz64local_getByte(pzlib_filefunc_def,filestream,&i); + x |= ((ZPOS64_T)i)<<8; + + if (err==UNZ_OK) + err = unz64local_getByte(pzlib_filefunc_def,filestream,&i); + x |= ((ZPOS64_T)i)<<16; + + if (err==UNZ_OK) + err = unz64local_getByte(pzlib_filefunc_def,filestream,&i); + x |= ((ZPOS64_T)i)<<24; + + if (err==UNZ_OK) + err = unz64local_getByte(pzlib_filefunc_def,filestream,&i); + x |= ((ZPOS64_T)i)<<32; + + if (err==UNZ_OK) + err = unz64local_getByte(pzlib_filefunc_def,filestream,&i); + x |= ((ZPOS64_T)i)<<40; + + if (err==UNZ_OK) + err = unz64local_getByte(pzlib_filefunc_def,filestream,&i); + x |= ((ZPOS64_T)i)<<48; + + if (err==UNZ_OK) + err = unz64local_getByte(pzlib_filefunc_def,filestream,&i); + x |= ((ZPOS64_T)i)<<56; + + if (err==UNZ_OK) + *pX = x; + else + *pX = 0; + return err; +} + +/* My own strcmpi / strcasecmp */ +local int strcmpcasenosensitive_internal (const char* fileName1, const char* fileName2) +{ + for (;;) + { + char c1=*(fileName1++); + char c2=*(fileName2++); + if ((c1>='a') && (c1<='z')) + c1 -= 0x20; + if ((c2>='a') && (c2<='z')) + c2 -= 0x20; + if (c1=='\0') + return ((c2=='\0') ? 0 : -1); + if (c2=='\0') + return 1; + if (c1c2) + return 1; + } +} + + +#ifdef CASESENSITIVITYDEFAULT_NO +#define CASESENSITIVITYDEFAULTVALUE 2 +#else +#define CASESENSITIVITYDEFAULTVALUE 1 +#endif + +#ifndef STRCMPCASENOSENTIVEFUNCTION +#define STRCMPCASENOSENTIVEFUNCTION strcmpcasenosensitive_internal +#endif + +/* + Compare two filename (fileName1,fileName2). + If iCaseSenisivity = 1, comparision is case sensitivity (like strcmp) + If iCaseSenisivity = 2, comparision is not case sensitivity (like strcmpi + or strcasecmp) + If iCaseSenisivity = 0, case sensitivity is defaut of your operating system + (like 1 on Unix, 2 on Windows) + +*/ +extern int ZEXPORT unzStringFileNameCompare (const char* fileName1, + const char* fileName2, + int iCaseSensitivity) + +{ + if (iCaseSensitivity==0) + iCaseSensitivity=CASESENSITIVITYDEFAULTVALUE; + + if (iCaseSensitivity==1) + return strcmp(fileName1,fileName2); + + return STRCMPCASENOSENTIVEFUNCTION(fileName1,fileName2); +} + +#ifndef BUFREADCOMMENT +#define BUFREADCOMMENT (0x400) +#endif + +/* + Locate the Central directory of a zipfile (at the end, just before + the global comment) +*/ +local ZPOS64_T unz64local_SearchCentralDir OF((const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream)); +local ZPOS64_T unz64local_SearchCentralDir(const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream) +{ + unsigned char* buf; + ZPOS64_T uSizeFile; + ZPOS64_T uBackRead; + ZPOS64_T uMaxBack=0xffff; /* maximum size of global comment */ + ZPOS64_T uPosFound=0; + + if (ZSEEK64(*pzlib_filefunc_def,filestream,0,ZLIB_FILEFUNC_SEEK_END) != 0) + return 0; + + + uSizeFile = ZTELL64(*pzlib_filefunc_def,filestream); + + if (uMaxBack>uSizeFile) + uMaxBack = uSizeFile; + + buf = (unsigned char*)ALLOC(BUFREADCOMMENT+4); + if (buf==NULL) + return 0; + + uBackRead = 4; + while (uBackReaduMaxBack) + uBackRead = uMaxBack; + else + uBackRead+=BUFREADCOMMENT; + uReadPos = uSizeFile-uBackRead ; + + uReadSize = ((BUFREADCOMMENT+4) < (uSizeFile-uReadPos)) ? + (BUFREADCOMMENT+4) : (uLong)(uSizeFile-uReadPos); + if (ZSEEK64(*pzlib_filefunc_def,filestream,uReadPos,ZLIB_FILEFUNC_SEEK_SET)!=0) + break; + + if (ZREAD64(*pzlib_filefunc_def,filestream,buf,uReadSize)!=uReadSize) + break; + + for (i=(int)uReadSize-3; (i--)>0;) + if (((*(buf+i))==0x50) && ((*(buf+i+1))==0x4b) && + ((*(buf+i+2))==0x05) && ((*(buf+i+3))==0x06)) + { + uPosFound = uReadPos+i; + break; + } + + if (uPosFound!=0) + break; + } + TRYFREE(buf); + return uPosFound; +} + + +/* + Locate the Central directory 64 of a zipfile (at the end, just before + the global comment) +*/ +local ZPOS64_T unz64local_SearchCentralDir64 OF(( + const zlib_filefunc64_32_def* pzlib_filefunc_def, + voidpf filestream)); + +local ZPOS64_T unz64local_SearchCentralDir64(const zlib_filefunc64_32_def* pzlib_filefunc_def, + voidpf filestream) +{ + unsigned char* buf; + ZPOS64_T uSizeFile; + ZPOS64_T uBackRead; + ZPOS64_T uMaxBack=0xffff; /* maximum size of global comment */ + ZPOS64_T uPosFound=0; + uLong uL; + ZPOS64_T relativeOffset; + + if (ZSEEK64(*pzlib_filefunc_def,filestream,0,ZLIB_FILEFUNC_SEEK_END) != 0) + return 0; + + + uSizeFile = ZTELL64(*pzlib_filefunc_def,filestream); + + if (uMaxBack>uSizeFile) + uMaxBack = uSizeFile; + + buf = (unsigned char*)ALLOC(BUFREADCOMMENT+4); + if (buf==NULL) + return 0; + + uBackRead = 4; + while (uBackReaduMaxBack) + uBackRead = uMaxBack; + else + uBackRead+=BUFREADCOMMENT; + uReadPos = uSizeFile-uBackRead ; + + uReadSize = ((BUFREADCOMMENT+4) < (uSizeFile-uReadPos)) ? + (BUFREADCOMMENT+4) : (uLong)(uSizeFile-uReadPos); + if (ZSEEK64(*pzlib_filefunc_def,filestream,uReadPos,ZLIB_FILEFUNC_SEEK_SET)!=0) + break; + + if (ZREAD64(*pzlib_filefunc_def,filestream,buf,uReadSize)!=uReadSize) + break; + + for (i=(int)uReadSize-3; (i--)>0;) + if (((*(buf+i))==0x50) && ((*(buf+i+1))==0x4b) && + ((*(buf+i+2))==0x06) && ((*(buf+i+3))==0x07)) + { + uPosFound = uReadPos+i; + break; + } + + if (uPosFound!=0) + break; + } + TRYFREE(buf); + if (uPosFound == 0) + return 0; + + /* Zip64 end of central directory locator */ + if (ZSEEK64(*pzlib_filefunc_def,filestream, uPosFound,ZLIB_FILEFUNC_SEEK_SET)!=0) + return 0; + + /* the signature, already checked */ + if (unz64local_getLong(pzlib_filefunc_def,filestream,&uL)!=UNZ_OK) + return 0; + + /* number of the disk with the start of the zip64 end of central directory */ + if (unz64local_getLong(pzlib_filefunc_def,filestream,&uL)!=UNZ_OK) + return 0; + if (uL != 0) + return 0; + + /* relative offset of the zip64 end of central directory record */ + if (unz64local_getLong64(pzlib_filefunc_def,filestream,&relativeOffset)!=UNZ_OK) + return 0; + + /* total number of disks */ + if (unz64local_getLong(pzlib_filefunc_def,filestream,&uL)!=UNZ_OK) + return 0; + if (uL != 1) + return 0; + + /* Goto end of central directory record */ + if (ZSEEK64(*pzlib_filefunc_def,filestream, relativeOffset,ZLIB_FILEFUNC_SEEK_SET)!=0) + return 0; + + /* the signature */ + if (unz64local_getLong(pzlib_filefunc_def,filestream,&uL)!=UNZ_OK) + return 0; + + if (uL != 0x06064b50) + return 0; + + return relativeOffset; +} + +/* + Open a Zip file. path contain the full pathname (by example, + on a Windows NT computer "c:\\test\\zlib114.zip" or on an Unix computer + "zlib/zlib114.zip". + If the zipfile cannot be opened (file doesn't exist or in not valid), the + return value is NULL. + Else, the return value is a unzFile Handle, usable with other function + of this unzip package. +*/ +local unzFile unzOpenInternal (const void *path, + zlib_filefunc64_32_def* pzlib_filefunc64_32_def, + int is64bitOpenFunction) +{ + unz64_s us; + unz64_s *s; + ZPOS64_T central_pos; + uLong uL; + + uLong number_disk; /* number of the current dist, used for + spaning ZIP, unsupported, always 0*/ + uLong number_disk_with_CD; /* number the the disk with central dir, used + for spaning ZIP, unsupported, always 0*/ + ZPOS64_T number_entry_CD; /* total number of entries in + the central dir + (same than number_entry on nospan) */ + + int err=UNZ_OK; + + if (unz_copyright[0]!=' ') + return NULL; + + us.z_filefunc.zseek32_file = NULL; + us.z_filefunc.ztell32_file = NULL; + if (pzlib_filefunc64_32_def==NULL) + fill_fopen64_filefunc(&us.z_filefunc.zfile_func64); + else + us.z_filefunc = *pzlib_filefunc64_32_def; + us.is64bitOpenFunction = is64bitOpenFunction; + + + + us.filestream = ZOPEN64(us.z_filefunc, + path, + ZLIB_FILEFUNC_MODE_READ | + ZLIB_FILEFUNC_MODE_EXISTING); + if (us.filestream==NULL) + return NULL; + + central_pos = unz64local_SearchCentralDir64(&us.z_filefunc,us.filestream); + if (central_pos) + { + uLong uS; + ZPOS64_T uL64; + + us.isZip64 = 1; + + if (ZSEEK64(us.z_filefunc, us.filestream, + central_pos,ZLIB_FILEFUNC_SEEK_SET)!=0) + err=UNZ_ERRNO; + + /* the signature, already checked */ + if (unz64local_getLong(&us.z_filefunc, us.filestream,&uL)!=UNZ_OK) + err=UNZ_ERRNO; + + /* size of zip64 end of central directory record */ + if (unz64local_getLong64(&us.z_filefunc, us.filestream,&uL64)!=UNZ_OK) + err=UNZ_ERRNO; + + /* version made by */ + if (unz64local_getShort(&us.z_filefunc, us.filestream,&uS)!=UNZ_OK) + err=UNZ_ERRNO; + + /* version needed to extract */ + if (unz64local_getShort(&us.z_filefunc, us.filestream,&uS)!=UNZ_OK) + err=UNZ_ERRNO; + + /* number of this disk */ + if (unz64local_getLong(&us.z_filefunc, us.filestream,&number_disk)!=UNZ_OK) + err=UNZ_ERRNO; + + /* number of the disk with the start of the central directory */ + if (unz64local_getLong(&us.z_filefunc, us.filestream,&number_disk_with_CD)!=UNZ_OK) + err=UNZ_ERRNO; + + /* total number of entries in the central directory on this disk */ + if (unz64local_getLong64(&us.z_filefunc, us.filestream,&us.gi.number_entry)!=UNZ_OK) + err=UNZ_ERRNO; + + /* total number of entries in the central directory */ + if (unz64local_getLong64(&us.z_filefunc, us.filestream,&number_entry_CD)!=UNZ_OK) + err=UNZ_ERRNO; + + if ((number_entry_CD!=us.gi.number_entry) || + (number_disk_with_CD!=0) || + (number_disk!=0)) + err=UNZ_BADZIPFILE; + + /* size of the central directory */ + if (unz64local_getLong64(&us.z_filefunc, us.filestream,&us.size_central_dir)!=UNZ_OK) + err=UNZ_ERRNO; + + /* offset of start of central directory with respect to the + starting disk number */ + if (unz64local_getLong64(&us.z_filefunc, us.filestream,&us.offset_central_dir)!=UNZ_OK) + err=UNZ_ERRNO; + + us.gi.size_comment = 0; + } + else + { + central_pos = unz64local_SearchCentralDir(&us.z_filefunc,us.filestream); + if (central_pos==0) + err=UNZ_ERRNO; + + us.isZip64 = 0; + + if (ZSEEK64(us.z_filefunc, us.filestream, + central_pos,ZLIB_FILEFUNC_SEEK_SET)!=0) + err=UNZ_ERRNO; + + /* the signature, already checked */ + if (unz64local_getLong(&us.z_filefunc, us.filestream,&uL)!=UNZ_OK) + err=UNZ_ERRNO; + + /* number of this disk */ + if (unz64local_getShort(&us.z_filefunc, us.filestream,&number_disk)!=UNZ_OK) + err=UNZ_ERRNO; + + /* number of the disk with the start of the central directory */ + if (unz64local_getShort(&us.z_filefunc, us.filestream,&number_disk_with_CD)!=UNZ_OK) + err=UNZ_ERRNO; + + /* total number of entries in the central dir on this disk */ + if (unz64local_getShort(&us.z_filefunc, us.filestream,&uL)!=UNZ_OK) + err=UNZ_ERRNO; + us.gi.number_entry = uL; + + /* total number of entries in the central dir */ + if (unz64local_getShort(&us.z_filefunc, us.filestream,&uL)!=UNZ_OK) + err=UNZ_ERRNO; + number_entry_CD = uL; + + if ((number_entry_CD!=us.gi.number_entry) || + (number_disk_with_CD!=0) || + (number_disk!=0)) + err=UNZ_BADZIPFILE; + + /* size of the central directory */ + if (unz64local_getLong(&us.z_filefunc, us.filestream,&uL)!=UNZ_OK) + err=UNZ_ERRNO; + us.size_central_dir = uL; + + /* offset of start of central directory with respect to the + starting disk number */ + if (unz64local_getLong(&us.z_filefunc, us.filestream,&uL)!=UNZ_OK) + err=UNZ_ERRNO; + us.offset_central_dir = uL; + + /* zipfile comment length */ + if (unz64local_getShort(&us.z_filefunc, us.filestream,&us.gi.size_comment)!=UNZ_OK) + err=UNZ_ERRNO; + } + + if ((central_pospfile_in_zip_read!=NULL) + unzCloseCurrentFile(file); + + ZCLOSE64(s->z_filefunc, s->filestream); + TRYFREE(s); + return UNZ_OK; +} + + +/* + Write info about the ZipFile in the *pglobal_info structure. + No preparation of the structure is needed + return UNZ_OK if there is no problem. */ +extern int ZEXPORT unzGetGlobalInfo64 (unzFile file, unz_global_info64* pglobal_info) +{ + unz64_s* s; + if (file==NULL) + return UNZ_PARAMERROR; + s=(unz64_s*)file; + *pglobal_info=s->gi; + return UNZ_OK; +} + +extern int ZEXPORT unzGetGlobalInfo (unzFile file, unz_global_info* pglobal_info32) +{ + unz64_s* s; + if (file==NULL) + return UNZ_PARAMERROR; + s=(unz64_s*)file; + /* to do : check if number_entry is not truncated */ + pglobal_info32->number_entry = (uLong)s->gi.number_entry; + pglobal_info32->size_comment = s->gi.size_comment; + return UNZ_OK; +} +/* + Translate date/time from Dos format to tm_unz (readable more easilty) +*/ +local void unz64local_DosDateToTmuDate (ZPOS64_T ulDosDate, tm_unz* ptm) +{ + ZPOS64_T uDate; + uDate = (ZPOS64_T)(ulDosDate>>16); + ptm->tm_mday = (uInt)(uDate&0x1f) ; + ptm->tm_mon = (uInt)((((uDate)&0x1E0)/0x20)-1) ; + ptm->tm_year = (uInt)(((uDate&0x0FE00)/0x0200)+1980) ; + + ptm->tm_hour = (uInt) ((ulDosDate &0xF800)/0x800); + ptm->tm_min = (uInt) ((ulDosDate&0x7E0)/0x20) ; + ptm->tm_sec = (uInt) (2*(ulDosDate&0x1f)) ; +} + +/* + Get Info about the current file in the zipfile, with internal only info +*/ +local int unz64local_GetCurrentFileInfoInternal OF((unzFile file, + unz_file_info64 *pfile_info, + unz_file_info64_internal + *pfile_info_internal, + char *szFileName, + uLong fileNameBufferSize, + void *extraField, + uLong extraFieldBufferSize, + char *szComment, + uLong commentBufferSize)); + +local int unz64local_GetCurrentFileInfoInternal (unzFile file, + unz_file_info64 *pfile_info, + unz_file_info64_internal + *pfile_info_internal, + char *szFileName, + uLong fileNameBufferSize, + void *extraField, + uLong extraFieldBufferSize, + char *szComment, + uLong commentBufferSize) +{ + unz64_s* s; + unz_file_info64 file_info; + unz_file_info64_internal file_info_internal; + int err=UNZ_OK; + uLong uMagic; + long lSeek=0; + uLong uL; + + if (file==NULL) + return UNZ_PARAMERROR; + s=(unz64_s*)file; + if (ZSEEK64(s->z_filefunc, s->filestream, + s->pos_in_central_dir+s->byte_before_the_zipfile, + ZLIB_FILEFUNC_SEEK_SET)!=0) + err=UNZ_ERRNO; + + + /* we check the magic */ + if (err==UNZ_OK) + { + if (unz64local_getLong(&s->z_filefunc, s->filestream,&uMagic) != UNZ_OK) + err=UNZ_ERRNO; + else if (uMagic!=0x02014b50) + err=UNZ_BADZIPFILE; + } + + if (unz64local_getShort(&s->z_filefunc, s->filestream,&file_info.version) != UNZ_OK) + err=UNZ_ERRNO; + + if (unz64local_getShort(&s->z_filefunc, s->filestream,&file_info.version_needed) != UNZ_OK) + err=UNZ_ERRNO; + + if (unz64local_getShort(&s->z_filefunc, s->filestream,&file_info.flag) != UNZ_OK) + err=UNZ_ERRNO; + + if (unz64local_getShort(&s->z_filefunc, s->filestream,&file_info.compression_method) != UNZ_OK) + err=UNZ_ERRNO; + + if (unz64local_getLong(&s->z_filefunc, s->filestream,&file_info.dosDate) != UNZ_OK) + err=UNZ_ERRNO; + + unz64local_DosDateToTmuDate(file_info.dosDate,&file_info.tmu_date); + + if (unz64local_getLong(&s->z_filefunc, s->filestream,&file_info.crc) != UNZ_OK) + err=UNZ_ERRNO; + + if (unz64local_getLong(&s->z_filefunc, s->filestream,&uL) != UNZ_OK) + err=UNZ_ERRNO; + file_info.compressed_size = uL; + + if (unz64local_getLong(&s->z_filefunc, s->filestream,&uL) != UNZ_OK) + err=UNZ_ERRNO; + file_info.uncompressed_size = uL; + + if (unz64local_getShort(&s->z_filefunc, s->filestream,&file_info.size_filename) != UNZ_OK) + err=UNZ_ERRNO; + + if (unz64local_getShort(&s->z_filefunc, s->filestream,&file_info.size_file_extra) != UNZ_OK) + err=UNZ_ERRNO; + + if (unz64local_getShort(&s->z_filefunc, s->filestream,&file_info.size_file_comment) != UNZ_OK) + err=UNZ_ERRNO; + + if (unz64local_getShort(&s->z_filefunc, s->filestream,&file_info.disk_num_start) != UNZ_OK) + err=UNZ_ERRNO; + + if (unz64local_getShort(&s->z_filefunc, s->filestream,&file_info.internal_fa) != UNZ_OK) + err=UNZ_ERRNO; + + if (unz64local_getLong(&s->z_filefunc, s->filestream,&file_info.external_fa) != UNZ_OK) + err=UNZ_ERRNO; + + // relative offset of local header + if (unz64local_getLong(&s->z_filefunc, s->filestream,&uL) != UNZ_OK) + err=UNZ_ERRNO; + file_info_internal.offset_curfile = uL; + + lSeek+=file_info.size_filename; + if ((err==UNZ_OK) && (szFileName!=NULL)) + { + uLong uSizeRead ; + if (file_info.size_filename0) && (fileNameBufferSize>0)) + if (ZREAD64(s->z_filefunc, s->filestream,szFileName,uSizeRead)!=uSizeRead) + err=UNZ_ERRNO; + lSeek -= uSizeRead; + } + + // Read extrafield + if ((err==UNZ_OK) && (extraField!=NULL)) + { + ZPOS64_T uSizeRead ; + if (file_info.size_file_extraz_filefunc, s->filestream,lSeek,ZLIB_FILEFUNC_SEEK_CUR)==0) + lSeek=0; + else + err=UNZ_ERRNO; + } + + if ((file_info.size_file_extra>0) && (extraFieldBufferSize>0)) + if (ZREAD64(s->z_filefunc, s->filestream,extraField,(uLong)uSizeRead)!=uSizeRead) + err=UNZ_ERRNO; + + lSeek += file_info.size_file_extra - (uLong)uSizeRead; + } + else + lSeek += file_info.size_file_extra; + + + if ((err==UNZ_OK) && (file_info.size_file_extra != 0)) + { + uLong acc = 0; + + // since lSeek now points to after the extra field we need to move back + lSeek -= file_info.size_file_extra; + + if (lSeek!=0) + { + if (ZSEEK64(s->z_filefunc, s->filestream,lSeek,ZLIB_FILEFUNC_SEEK_CUR)==0) + lSeek=0; + else + err=UNZ_ERRNO; + } + + while(acc < file_info.size_file_extra) + { + uLong headerId; + uLong dataSize; + + if (unz64local_getShort(&s->z_filefunc, s->filestream,&headerId) != UNZ_OK) + err=UNZ_ERRNO; + + if (unz64local_getShort(&s->z_filefunc, s->filestream,&dataSize) != UNZ_OK) + err=UNZ_ERRNO; + + /* ZIP64 extra fields */ + if (headerId == 0x0001) + { + uLong uL; + + if(file_info.uncompressed_size == (ZPOS64_T)(unsigned long)-1) + { + if (unz64local_getLong64(&s->z_filefunc, s->filestream,&file_info.uncompressed_size) != UNZ_OK) + err=UNZ_ERRNO; + } + + if(file_info.compressed_size == (ZPOS64_T)(unsigned long)-1) + { + if (unz64local_getLong64(&s->z_filefunc, s->filestream,&file_info.compressed_size) != UNZ_OK) + err=UNZ_ERRNO; + } + + if(file_info_internal.offset_curfile == (ZPOS64_T)(unsigned long)-1) + { + /* Relative Header offset */ + if (unz64local_getLong64(&s->z_filefunc, s->filestream,&file_info_internal.offset_curfile) != UNZ_OK) + err=UNZ_ERRNO; + } + + if(file_info.disk_num_start == (unsigned long)-1) + { + /* Disk Start Number */ + if (unz64local_getLong(&s->z_filefunc, s->filestream,&uL) != UNZ_OK) + err=UNZ_ERRNO; + } + + } + else + { + if (ZSEEK64(s->z_filefunc, s->filestream,dataSize,ZLIB_FILEFUNC_SEEK_CUR)!=0) + err=UNZ_ERRNO; + } + + acc += 2 + 2 + dataSize; + } + } + + if ((err==UNZ_OK) && (szComment!=NULL)) + { + uLong uSizeRead ; + if (file_info.size_file_commentz_filefunc, s->filestream,lSeek,ZLIB_FILEFUNC_SEEK_CUR)==0) + { +#ifndef __clang_analyzer__ + lSeek=0; +#endif + } + else + err=UNZ_ERRNO; + } + + if ((file_info.size_file_comment>0) && (commentBufferSize>0)) + if (ZREAD64(s->z_filefunc, s->filestream,szComment,uSizeRead)!=uSizeRead) + err=UNZ_ERRNO; +#ifndef __clang_analyzer__ + lSeek+=file_info.size_file_comment - uSizeRead; +#endif + } +#ifndef __clang_analyzer__ + else + lSeek+=file_info.size_file_comment; +#endif + + + if ((err==UNZ_OK) && (pfile_info!=NULL)) + *pfile_info=file_info; + + if ((err==UNZ_OK) && (pfile_info_internal!=NULL)) + *pfile_info_internal=file_info_internal; + + return err; +} + + + +/* + Write info about the ZipFile in the *pglobal_info structure. + No preparation of the structure is needed + return UNZ_OK if there is no problem. +*/ +extern int ZEXPORT unzGetCurrentFileInfo64 (unzFile file, + unz_file_info64 * pfile_info, + char * szFileName, uLong fileNameBufferSize, + void *extraField, uLong extraFieldBufferSize, + char* szComment, uLong commentBufferSize) +{ + return unz64local_GetCurrentFileInfoInternal(file,pfile_info,NULL, + szFileName,fileNameBufferSize, + extraField,extraFieldBufferSize, + szComment,commentBufferSize); +} + +extern int ZEXPORT unzGetCurrentFileInfo (unzFile file, + unz_file_info * pfile_info, + char * szFileName, uLong fileNameBufferSize, + void *extraField, uLong extraFieldBufferSize, + char* szComment, uLong commentBufferSize) +{ + int err; + unz_file_info64 file_info64; + err = unz64local_GetCurrentFileInfoInternal(file,&file_info64,NULL, + szFileName,fileNameBufferSize, + extraField,extraFieldBufferSize, + szComment,commentBufferSize); + if (err==UNZ_OK) + { + pfile_info->version = file_info64.version; + pfile_info->version_needed = file_info64.version_needed; + pfile_info->flag = file_info64.flag; + pfile_info->compression_method = file_info64.compression_method; + pfile_info->dosDate = file_info64.dosDate; + pfile_info->crc = file_info64.crc; + + pfile_info->size_filename = file_info64.size_filename; + pfile_info->size_file_extra = file_info64.size_file_extra; + pfile_info->size_file_comment = file_info64.size_file_comment; + + pfile_info->disk_num_start = file_info64.disk_num_start; + pfile_info->internal_fa = file_info64.internal_fa; + pfile_info->external_fa = file_info64.external_fa; + + pfile_info->tmu_date = file_info64.tmu_date, + + + pfile_info->compressed_size = (uLong)file_info64.compressed_size; + pfile_info->uncompressed_size = (uLong)file_info64.uncompressed_size; + + } + return err; +} +/* + Set the current file of the zipfile to the first file. + return UNZ_OK if there is no problem +*/ +extern int ZEXPORT unzGoToFirstFile (unzFile file) +{ + int err=UNZ_OK; + unz64_s* s; + if (file==NULL) + return UNZ_PARAMERROR; + s=(unz64_s*)file; + s->pos_in_central_dir=s->offset_central_dir; + s->num_file=0; + err=unz64local_GetCurrentFileInfoInternal(file,&s->cur_file_info, + &s->cur_file_info_internal, + NULL,0,NULL,0,NULL,0); + s->current_file_ok = (err == UNZ_OK); + return err; +} + +/* + Set the current file of the zipfile to the next file. + return UNZ_OK if there is no problem + return UNZ_END_OF_LIST_OF_FILE if the actual file was the latest. +*/ +extern int ZEXPORT unzGoToNextFile (unzFile file) +{ + unz64_s* s; + int err; + + if (file==NULL) + return UNZ_PARAMERROR; + s=(unz64_s*)file; + if (!s->current_file_ok) + return UNZ_END_OF_LIST_OF_FILE; + if (s->gi.number_entry != 0xffff) /* 2^16 files overflow hack */ + if (s->num_file+1==s->gi.number_entry) + return UNZ_END_OF_LIST_OF_FILE; + + s->pos_in_central_dir += SIZECENTRALDIRITEM + s->cur_file_info.size_filename + + s->cur_file_info.size_file_extra + s->cur_file_info.size_file_comment ; + s->num_file++; + err = unz64local_GetCurrentFileInfoInternal(file,&s->cur_file_info, + &s->cur_file_info_internal, + NULL,0,NULL,0,NULL,0); + s->current_file_ok = (err == UNZ_OK); + return err; +} + + +/* + Try locate the file szFileName in the zipfile. + For the iCaseSensitivity signification, see unzipStringFileNameCompare + + return value : + UNZ_OK if the file is found. It becomes the current file. + UNZ_END_OF_LIST_OF_FILE if the file is not found +*/ +extern int ZEXPORT unzLocateFile (unzFile file, const char *szFileName, int iCaseSensitivity) +{ + unz64_s* s; + int err; + + /* We remember the 'current' position in the file so that we can jump + * back there if we fail. + */ + unz_file_info64 cur_file_infoSaved; + unz_file_info64_internal cur_file_info_internalSaved; + ZPOS64_T num_fileSaved; + ZPOS64_T pos_in_central_dirSaved; + + + if (file==NULL) + return UNZ_PARAMERROR; + + if (strlen(szFileName)>=UNZ_MAXFILENAMEINZIP) + return UNZ_PARAMERROR; + + s=(unz64_s*)file; + if (!s->current_file_ok) + return UNZ_END_OF_LIST_OF_FILE; + + /* Save the current state */ + num_fileSaved = s->num_file; + pos_in_central_dirSaved = s->pos_in_central_dir; + cur_file_infoSaved = s->cur_file_info; + cur_file_info_internalSaved = s->cur_file_info_internal; + + err = unzGoToFirstFile(file); + + while (err == UNZ_OK) + { + char szCurrentFileName[UNZ_MAXFILENAMEINZIP+1]; + err = unzGetCurrentFileInfo64(file,NULL, + szCurrentFileName,sizeof(szCurrentFileName)-1, + NULL,0,NULL,0); + if (err == UNZ_OK) + { + if (unzStringFileNameCompare(szCurrentFileName, + szFileName,iCaseSensitivity)==0) + return UNZ_OK; + err = unzGoToNextFile(file); + } + } + + /* We failed, so restore the state of the 'current file' to where we + * were. + */ + s->num_file = num_fileSaved ; + s->pos_in_central_dir = pos_in_central_dirSaved ; + s->cur_file_info = cur_file_infoSaved; + s->cur_file_info_internal = cur_file_info_internalSaved; + return err; +} + + +/* +/////////////////////////////////////////// +// Contributed by Ryan Haksi (mailto://cryogen@infoserve.net) +// I need random access +// +// Further optimization could be realized by adding an ability +// to cache the directory in memory. The goal being a single +// comprehensive file read to put the file I need in a memory. +*/ + +/* +typedef struct unz_file_pos_s +{ + ZPOS64_T pos_in_zip_directory; // offset in file + ZPOS64_T num_of_file; // # of file +} unz_file_pos; +*/ + +extern int ZEXPORT unzGetFilePos64(unzFile file, unz64_file_pos* file_pos) +{ + unz64_s* s; + + if (file==NULL || file_pos==NULL) + return UNZ_PARAMERROR; + s=(unz64_s*)file; + if (!s->current_file_ok) + return UNZ_END_OF_LIST_OF_FILE; + + file_pos->pos_in_zip_directory = s->pos_in_central_dir; + file_pos->num_of_file = s->num_file; + + return UNZ_OK; +} + +extern int ZEXPORT unzGetFilePos( + unzFile file, + unz_file_pos* file_pos) +{ + unz64_file_pos file_pos64; + int err = unzGetFilePos64(file,&file_pos64); + if (err==UNZ_OK) + { + file_pos->pos_in_zip_directory = (uLong)file_pos64.pos_in_zip_directory; + file_pos->num_of_file = (uLong)file_pos64.num_of_file; + } + return err; +} + +extern int ZEXPORT unzGoToFilePos64(unzFile file, const unz64_file_pos* file_pos) +{ + unz64_s* s; + int err; + + if (file==NULL || file_pos==NULL) + return UNZ_PARAMERROR; + s=(unz64_s*)file; + + /* jump to the right spot */ + s->pos_in_central_dir = file_pos->pos_in_zip_directory; + s->num_file = file_pos->num_of_file; + + /* set the current file */ + err = unz64local_GetCurrentFileInfoInternal(file,&s->cur_file_info, + &s->cur_file_info_internal, + NULL,0,NULL,0,NULL,0); + /* return results */ + s->current_file_ok = (err == UNZ_OK); + return err; +} + +extern int ZEXPORT unzGoToFilePos( + unzFile file, + unz_file_pos* file_pos) +{ + unz64_file_pos file_pos64; + if (file_pos == NULL) + return UNZ_PARAMERROR; + + file_pos64.pos_in_zip_directory = file_pos->pos_in_zip_directory; + file_pos64.num_of_file = file_pos->num_of_file; + return unzGoToFilePos64(file,&file_pos64); +} + +/* +// Unzip Helper Functions - should be here? +/////////////////////////////////////////// +*/ + +/* + Read the local header of the current zipfile + Check the coherency of the local header and info in the end of central + directory about this file + store in *piSizeVar the size of extra info in local header + (filename and size of extra field data) +*/ +local int unz64local_CheckCurrentFileCoherencyHeader (unz64_s* s, uInt* piSizeVar, + ZPOS64_T * poffset_local_extrafield, + uInt * psize_local_extrafield) +{ + uLong uMagic,uData,uFlags; + uLong size_filename; + uLong size_extra_field; + int err=UNZ_OK; + + *piSizeVar = 0; + *poffset_local_extrafield = 0; + *psize_local_extrafield = 0; + + if (ZSEEK64(s->z_filefunc, s->filestream,s->cur_file_info_internal.offset_curfile + + s->byte_before_the_zipfile,ZLIB_FILEFUNC_SEEK_SET)!=0) + return UNZ_ERRNO; + + + if (err==UNZ_OK) + { + if (unz64local_getLong(&s->z_filefunc, s->filestream,&uMagic) != UNZ_OK) + err=UNZ_ERRNO; + else if (uMagic!=0x04034b50) + err=UNZ_BADZIPFILE; + } + + if (unz64local_getShort(&s->z_filefunc, s->filestream,&uData) != UNZ_OK) + err=UNZ_ERRNO; +/* + else if ((err==UNZ_OK) && (uData!=s->cur_file_info.wVersion)) + err=UNZ_BADZIPFILE; +*/ + if (unz64local_getShort(&s->z_filefunc, s->filestream,&uFlags) != UNZ_OK) + err=UNZ_ERRNO; + + if (unz64local_getShort(&s->z_filefunc, s->filestream,&uData) != UNZ_OK) + err=UNZ_ERRNO; + else if ((err==UNZ_OK) && (uData!=s->cur_file_info.compression_method)) + err=UNZ_BADZIPFILE; + + if ((err==UNZ_OK) && (s->cur_file_info.compression_method!=0) && +/* #ifdef HAVE_BZIP2 */ + (s->cur_file_info.compression_method!=Z_BZIP2ED) && +/* #endif */ + (s->cur_file_info.compression_method!=Z_DEFLATED)) + err=UNZ_BADZIPFILE; + + if (unz64local_getLong(&s->z_filefunc, s->filestream,&uData) != UNZ_OK) /* date/time */ + err=UNZ_ERRNO; + + if (unz64local_getLong(&s->z_filefunc, s->filestream,&uData) != UNZ_OK) /* crc */ + err=UNZ_ERRNO; + else if ((err==UNZ_OK) && (uData!=s->cur_file_info.crc) && ((uFlags & 8)==0)) + err=UNZ_BADZIPFILE; + + if (unz64local_getLong(&s->z_filefunc, s->filestream,&uData) != UNZ_OK) /* size compr */ + err=UNZ_ERRNO; + else if (uData != 0xFFFFFFFF && (err==UNZ_OK) && (uData!=s->cur_file_info.compressed_size) && ((uFlags & 8)==0)) + err=UNZ_BADZIPFILE; + + if (unz64local_getLong(&s->z_filefunc, s->filestream,&uData) != UNZ_OK) /* size uncompr */ + err=UNZ_ERRNO; + else if (uData != 0xFFFFFFFF && (err==UNZ_OK) && (uData!=s->cur_file_info.uncompressed_size) && ((uFlags & 8)==0)) + err=UNZ_BADZIPFILE; + + if (unz64local_getShort(&s->z_filefunc, s->filestream,&size_filename) != UNZ_OK) + err=UNZ_ERRNO; + else if ((err==UNZ_OK) && (size_filename!=s->cur_file_info.size_filename)) + err=UNZ_BADZIPFILE; + + *piSizeVar += (uInt)size_filename; + + if (unz64local_getShort(&s->z_filefunc, s->filestream,&size_extra_field) != UNZ_OK) + err=UNZ_ERRNO; + *poffset_local_extrafield= s->cur_file_info_internal.offset_curfile + + SIZEZIPLOCALHEADER + size_filename; + *psize_local_extrafield = (uInt)size_extra_field; + + *piSizeVar += (uInt)size_extra_field; + + return err; +} + +/* + Open for reading data the current file in the zipfile. + If there is no error and the file is opened, the return value is UNZ_OK. +*/ +extern int ZEXPORT unzOpenCurrentFile3 (unzFile file, int* method, + int* level, int raw, const char* password) +{ + int err=UNZ_OK; + uInt iSizeVar; + unz64_s* s; + file_in_zip64_read_info_s* pfile_in_zip_read_info; + ZPOS64_T offset_local_extrafield; /* offset of the local extra field */ + uInt size_local_extrafield; /* size of the local extra field */ +# ifndef NOUNCRYPT + char source[12]; +# else + if (password != NULL) + return UNZ_PARAMERROR; +# endif + + if (file==NULL) + return UNZ_PARAMERROR; + s=(unz64_s*)file; + if (!s->current_file_ok) + return UNZ_PARAMERROR; + + if (s->pfile_in_zip_read != NULL) + unzCloseCurrentFile(file); + + if (unz64local_CheckCurrentFileCoherencyHeader(s,&iSizeVar, &offset_local_extrafield,&size_local_extrafield)!=UNZ_OK) + return UNZ_BADZIPFILE; + + pfile_in_zip_read_info = (file_in_zip64_read_info_s*)ALLOC(sizeof(file_in_zip64_read_info_s)); + if (pfile_in_zip_read_info==NULL) + return UNZ_INTERNALERROR; + + pfile_in_zip_read_info->read_buffer=(char*)ALLOC(UNZ_BUFSIZE); + pfile_in_zip_read_info->offset_local_extrafield = offset_local_extrafield; + pfile_in_zip_read_info->size_local_extrafield = size_local_extrafield; + pfile_in_zip_read_info->pos_local_extrafield=0; + pfile_in_zip_read_info->raw=raw; + + if (pfile_in_zip_read_info->read_buffer==NULL) + { + TRYFREE(pfile_in_zip_read_info); + return UNZ_INTERNALERROR; + } + + pfile_in_zip_read_info->stream_initialised=0; + + if (method!=NULL) + *method = (int)s->cur_file_info.compression_method; + + if (level!=NULL) + { + *level = 6; + switch (s->cur_file_info.flag & 0x06) + { + case 6 : *level = 1; break; + case 4 : *level = 2; break; + case 2 : *level = 9; break; + } + } + + if ((s->cur_file_info.compression_method!=0) && +/* #ifdef HAVE_BZIP2 */ + (s->cur_file_info.compression_method!=Z_BZIP2ED) && +/* #endif */ + (s->cur_file_info.compression_method!=Z_DEFLATED)) + { +#ifndef __clang_analyzer__ + err=UNZ_BADZIPFILE; +#endif + } + + pfile_in_zip_read_info->crc32_wait=s->cur_file_info.crc; + pfile_in_zip_read_info->crc32=0; + pfile_in_zip_read_info->total_out_64=0; + pfile_in_zip_read_info->compression_method = s->cur_file_info.compression_method; + pfile_in_zip_read_info->filestream=s->filestream; + pfile_in_zip_read_info->z_filefunc=s->z_filefunc; +#ifndef __clang_analyzer__ + pfile_in_zip_read_info->byte_before_the_zipfile=s->byte_before_the_zipfile; +#endif + + pfile_in_zip_read_info->stream.total_out = 0; + + if ((s->cur_file_info.compression_method==Z_BZIP2ED) && (!raw)) + { +#ifdef HAVE_BZIP2 + pfile_in_zip_read_info->bstream.bzalloc = (void *(*) (void *, int, int))0; + pfile_in_zip_read_info->bstream.bzfree = (free_func)0; + pfile_in_zip_read_info->bstream.opaque = (voidpf)0; + pfile_in_zip_read_info->bstream.state = (voidpf)0; + + pfile_in_zip_read_info->stream.zalloc = (alloc_func)0; + pfile_in_zip_read_info->stream.zfree = (free_func)0; + pfile_in_zip_read_info->stream.opaque = (voidpf)0; + pfile_in_zip_read_info->stream.next_in = (voidpf)0; + pfile_in_zip_read_info->stream.avail_in = 0; + + err=BZ2_bzDecompressInit(&pfile_in_zip_read_info->bstream, 0, 0); + if (err == Z_OK) + pfile_in_zip_read_info->stream_initialised=Z_BZIP2ED; + else + { + TRYFREE(pfile_in_zip_read_info); + return err; + } +#else + pfile_in_zip_read_info->raw=1; +#endif + } + else if ((s->cur_file_info.compression_method==Z_DEFLATED) && (!raw)) + { + pfile_in_zip_read_info->stream.zalloc = (alloc_func)0; + pfile_in_zip_read_info->stream.zfree = (free_func)0; + pfile_in_zip_read_info->stream.opaque = (voidpf)0; + pfile_in_zip_read_info->stream.next_in = 0; + pfile_in_zip_read_info->stream.avail_in = 0; + + err=inflateInit2(&pfile_in_zip_read_info->stream, -MAX_WBITS); + if (err == Z_OK) + pfile_in_zip_read_info->stream_initialised=Z_DEFLATED; + else + { + TRYFREE(pfile_in_zip_read_info); + return err; + } + /* windowBits is passed < 0 to tell that there is no zlib header. + * Note that in this case inflate *requires* an extra "dummy" byte + * after the compressed stream in order to complete decompression and + * return Z_STREAM_END. + * In unzip, i don't wait absolutely Z_STREAM_END because I known the + * size of both compressed and uncompressed data + */ + } + pfile_in_zip_read_info->rest_read_compressed = + s->cur_file_info.compressed_size ; + pfile_in_zip_read_info->rest_read_uncompressed = + s->cur_file_info.uncompressed_size ; + + + pfile_in_zip_read_info->pos_in_zipfile = + s->cur_file_info_internal.offset_curfile + SIZEZIPLOCALHEADER + + iSizeVar; + + pfile_in_zip_read_info->stream.avail_in = (uInt)0; + + s->pfile_in_zip_read = pfile_in_zip_read_info; + s->encrypted = 0; + +# ifndef NOUNCRYPT + if (password != NULL) + { + int i; + s->pcrc_32_tab = (const unsigned long*)get_crc_table(); + init_keys(password,s->keys,s->pcrc_32_tab); + if (ZSEEK64(s->z_filefunc, s->filestream, + s->pfile_in_zip_read->pos_in_zipfile + + s->pfile_in_zip_read->byte_before_the_zipfile, + SEEK_SET)!=0) + return UNZ_INTERNALERROR; + if(ZREAD64(s->z_filefunc, s->filestream,source, 12)<12) + return UNZ_INTERNALERROR; + + for (i = 0; i<12; i++) + zdecode(s->keys,s->pcrc_32_tab,source[i]); + + s->pfile_in_zip_read->pos_in_zipfile+=12; + s->encrypted=1; + } +# endif + + + return UNZ_OK; +} + +extern int ZEXPORT unzOpenCurrentFile (unzFile file) +{ + return unzOpenCurrentFile3(file, NULL, NULL, 0, NULL); +} + +extern int ZEXPORT unzOpenCurrentFilePassword (unzFile file, const char* password) +{ + return unzOpenCurrentFile3(file, NULL, NULL, 0, password); +} + +extern int ZEXPORT unzOpenCurrentFile2 (unzFile file, int* method, int* level, int raw) +{ + return unzOpenCurrentFile3(file, method, level, raw, NULL); +} + +/** Addition for GDAL : START */ + +extern ZPOS64_T ZEXPORT unzGetCurrentFileZStreamPos64( unzFile file) +{ + unz64_s* s; + file_in_zip64_read_info_s* pfile_in_zip_read_info; + s=(unz64_s*)file; + if (file==NULL) + return 0; //UNZ_PARAMERROR; + pfile_in_zip_read_info=s->pfile_in_zip_read; + if (pfile_in_zip_read_info==NULL) + return 0; //UNZ_PARAMERROR; + return pfile_in_zip_read_info->pos_in_zipfile + + pfile_in_zip_read_info->byte_before_the_zipfile; +} + +/** Addition for GDAL : END */ + +/* + Read bytes from the current file. + buf contain buffer where data must be copied + len the size of buf. + + return the number of byte copied if somes bytes are copied + return 0 if the end of file was reached + return <0 with error code if there is an error + (UNZ_ERRNO for IO error, or zLib error for uncompress error) +*/ +extern int ZEXPORT unzReadCurrentFile (unzFile file, voidp buf, unsigned len) +{ + int err=UNZ_OK; + uInt iRead = 0; + unz64_s* s; + file_in_zip64_read_info_s* pfile_in_zip_read_info; + if (file==NULL) + return UNZ_PARAMERROR; + s=(unz64_s*)file; + pfile_in_zip_read_info=s->pfile_in_zip_read; + + if (pfile_in_zip_read_info==NULL) + return UNZ_PARAMERROR; + + + if (pfile_in_zip_read_info->read_buffer == NULL) + return UNZ_END_OF_LIST_OF_FILE; + if (len==0) + return 0; + + pfile_in_zip_read_info->stream.next_out = (Bytef*)buf; + + pfile_in_zip_read_info->stream.avail_out = (uInt)len; + + // NOTE: + // This bit of code seems to try to set the amount of space in the output buffer based on the + // value stored in the headers stored in the .zip file. However, if those values are incorrect + // it may result in a loss of data when uncompresssing that file. The compressed data is still + // legit and will deflate without knowing the uncompressed code so this tidbit is unnecessary and + // may cause issues for some .zip files. + // + // It's removed in here to fix those issues. + // + // See: https://github.com/samsoffes/ssziparchive/issues/16 + // + + /* + if ((len>pfile_in_zip_read_info->rest_read_uncompressed) && + (!(pfile_in_zip_read_info->raw))) + pfile_in_zip_read_info->stream.avail_out = + (uInt)pfile_in_zip_read_info->rest_read_uncompressed; + */ + + if ((len>pfile_in_zip_read_info->rest_read_compressed+ + pfile_in_zip_read_info->stream.avail_in) && + (pfile_in_zip_read_info->raw)) + pfile_in_zip_read_info->stream.avail_out = + (uInt)pfile_in_zip_read_info->rest_read_compressed+ + pfile_in_zip_read_info->stream.avail_in; + + while (pfile_in_zip_read_info->stream.avail_out>0) + { + if ((pfile_in_zip_read_info->stream.avail_in==0) && + (pfile_in_zip_read_info->rest_read_compressed>0)) + { + uInt uReadThis = UNZ_BUFSIZE; + if (pfile_in_zip_read_info->rest_read_compressedrest_read_compressed; + if (uReadThis == 0) + return UNZ_EOF; + if (ZSEEK64(pfile_in_zip_read_info->z_filefunc, + pfile_in_zip_read_info->filestream, + pfile_in_zip_read_info->pos_in_zipfile + + pfile_in_zip_read_info->byte_before_the_zipfile, + ZLIB_FILEFUNC_SEEK_SET)!=0) + return UNZ_ERRNO; + if (ZREAD64(pfile_in_zip_read_info->z_filefunc, + pfile_in_zip_read_info->filestream, + pfile_in_zip_read_info->read_buffer, + uReadThis)!=uReadThis) + return UNZ_ERRNO; + + +# ifndef NOUNCRYPT + if(s->encrypted) + { + uInt i; + for(i=0;iread_buffer[i] = + zdecode(s->keys,s->pcrc_32_tab, + pfile_in_zip_read_info->read_buffer[i]); + } +# endif + + + pfile_in_zip_read_info->pos_in_zipfile += uReadThis; + + pfile_in_zip_read_info->rest_read_compressed-=uReadThis; + + pfile_in_zip_read_info->stream.next_in = + (Bytef*)pfile_in_zip_read_info->read_buffer; + pfile_in_zip_read_info->stream.avail_in = (uInt)uReadThis; + } + + if ((pfile_in_zip_read_info->compression_method==0) || (pfile_in_zip_read_info->raw)) + { + uInt uDoCopy,i ; + + if ((pfile_in_zip_read_info->stream.avail_in == 0) && + (pfile_in_zip_read_info->rest_read_compressed == 0)) + return (iRead==0) ? UNZ_EOF : iRead; + + if (pfile_in_zip_read_info->stream.avail_out < + pfile_in_zip_read_info->stream.avail_in) + uDoCopy = pfile_in_zip_read_info->stream.avail_out ; + else + uDoCopy = pfile_in_zip_read_info->stream.avail_in ; + + for (i=0;istream.next_out+i) = + *(pfile_in_zip_read_info->stream.next_in+i); + + pfile_in_zip_read_info->total_out_64 = pfile_in_zip_read_info->total_out_64 + uDoCopy; + + pfile_in_zip_read_info->crc32 = crc32(pfile_in_zip_read_info->crc32, + pfile_in_zip_read_info->stream.next_out, + uDoCopy); + pfile_in_zip_read_info->rest_read_uncompressed-=uDoCopy; + pfile_in_zip_read_info->stream.avail_in -= uDoCopy; + pfile_in_zip_read_info->stream.avail_out -= uDoCopy; + pfile_in_zip_read_info->stream.next_out += uDoCopy; + pfile_in_zip_read_info->stream.next_in += uDoCopy; + pfile_in_zip_read_info->stream.total_out += uDoCopy; + iRead += uDoCopy; + } + else if (pfile_in_zip_read_info->compression_method==Z_BZIP2ED) + { +#ifdef HAVE_BZIP2 + uLong uTotalOutBefore,uTotalOutAfter; + const Bytef *bufBefore; + uLong uOutThis; + + pfile_in_zip_read_info->bstream.next_in = (char*)pfile_in_zip_read_info->stream.next_in; + pfile_in_zip_read_info->bstream.avail_in = pfile_in_zip_read_info->stream.avail_in; + pfile_in_zip_read_info->bstream.total_in_lo32 = pfile_in_zip_read_info->stream.total_in; + pfile_in_zip_read_info->bstream.total_in_hi32 = 0; + pfile_in_zip_read_info->bstream.next_out = (char*)pfile_in_zip_read_info->stream.next_out; + pfile_in_zip_read_info->bstream.avail_out = pfile_in_zip_read_info->stream.avail_out; + pfile_in_zip_read_info->bstream.total_out_lo32 = pfile_in_zip_read_info->stream.total_out; + pfile_in_zip_read_info->bstream.total_out_hi32 = 0; + + uTotalOutBefore = pfile_in_zip_read_info->bstream.total_out_lo32; + bufBefore = (const Bytef *)pfile_in_zip_read_info->bstream.next_out; + + err=BZ2_bzDecompress(&pfile_in_zip_read_info->bstream); + + uTotalOutAfter = pfile_in_zip_read_info->bstream.total_out_lo32; + uOutThis = uTotalOutAfter-uTotalOutBefore; + + pfile_in_zip_read_info->total_out_64 = pfile_in_zip_read_info->total_out_64 + uOutThis; + + pfile_in_zip_read_info->crc32 = crc32(pfile_in_zip_read_info->crc32,bufBefore, (uInt)(uOutThis)); + pfile_in_zip_read_info->rest_read_uncompressed -= uOutThis; + iRead += (uInt)(uTotalOutAfter - uTotalOutBefore); + + pfile_in_zip_read_info->stream.next_in = (Bytef*)pfile_in_zip_read_info->bstream.next_in; + pfile_in_zip_read_info->stream.avail_in = pfile_in_zip_read_info->bstream.avail_in; + pfile_in_zip_read_info->stream.total_in = pfile_in_zip_read_info->bstream.total_in_lo32; + pfile_in_zip_read_info->stream.next_out = (Bytef*)pfile_in_zip_read_info->bstream.next_out; + pfile_in_zip_read_info->stream.avail_out = pfile_in_zip_read_info->bstream.avail_out; + pfile_in_zip_read_info->stream.total_out = pfile_in_zip_read_info->bstream.total_out_lo32; + + if (err==BZ_STREAM_END) + return (iRead==0) ? UNZ_EOF : iRead; + if (err!=BZ_OK) + break; +#endif + } // end Z_BZIP2ED + else + { + ZPOS64_T uTotalOutBefore,uTotalOutAfter; + const Bytef *bufBefore; + ZPOS64_T uOutThis; + int flush=Z_SYNC_FLUSH; + + uTotalOutBefore = pfile_in_zip_read_info->stream.total_out; + bufBefore = pfile_in_zip_read_info->stream.next_out; + + /* + if ((pfile_in_zip_read_info->rest_read_uncompressed == + pfile_in_zip_read_info->stream.avail_out) && + (pfile_in_zip_read_info->rest_read_compressed == 0)) + flush = Z_FINISH; + */ + err=inflate(&pfile_in_zip_read_info->stream,flush); + + if ((err>=0) && (pfile_in_zip_read_info->stream.msg!=NULL)) + err = Z_DATA_ERROR; + + uTotalOutAfter = pfile_in_zip_read_info->stream.total_out; + uOutThis = uTotalOutAfter-uTotalOutBefore; + + pfile_in_zip_read_info->total_out_64 = pfile_in_zip_read_info->total_out_64 + uOutThis; + + pfile_in_zip_read_info->crc32 = + crc32(pfile_in_zip_read_info->crc32,bufBefore, + (uInt)(uOutThis)); + + pfile_in_zip_read_info->rest_read_uncompressed -= + uOutThis; + + iRead += (uInt)(uTotalOutAfter - uTotalOutBefore); + + if (err==Z_STREAM_END) + return (iRead==0) ? UNZ_EOF : iRead; + if (err!=Z_OK) + break; + } + } + + if (err==Z_OK) + return iRead; + return err; +} + + +/* + Give the current position in uncompressed data +*/ +extern z_off_t ZEXPORT unztell (unzFile file) +{ + unz64_s* s; + file_in_zip64_read_info_s* pfile_in_zip_read_info; + if (file==NULL) + return UNZ_PARAMERROR; + s=(unz64_s*)file; + pfile_in_zip_read_info=s->pfile_in_zip_read; + + if (pfile_in_zip_read_info==NULL) + return UNZ_PARAMERROR; + + return (z_off_t)pfile_in_zip_read_info->stream.total_out; +} + +extern ZPOS64_T ZEXPORT unztell64 (unzFile file) +{ + + unz64_s* s; + file_in_zip64_read_info_s* pfile_in_zip_read_info; + if (file==NULL) + return (ZPOS64_T)-1; + s=(unz64_s*)file; + pfile_in_zip_read_info=s->pfile_in_zip_read; + + if (pfile_in_zip_read_info==NULL) + return (ZPOS64_T)-1; + + return pfile_in_zip_read_info->total_out_64; +} + + +/* + return 1 if the end of file was reached, 0 elsewhere +*/ +extern int ZEXPORT unzeof (unzFile file) +{ + unz64_s* s; + file_in_zip64_read_info_s* pfile_in_zip_read_info; + if (file==NULL) + return UNZ_PARAMERROR; + s=(unz64_s*)file; + pfile_in_zip_read_info=s->pfile_in_zip_read; + + if (pfile_in_zip_read_info==NULL) + return UNZ_PARAMERROR; + + if (pfile_in_zip_read_info->rest_read_uncompressed == 0) + return 1; + else + return 0; +} + + + +/* +Read extra field from the current file (opened by unzOpenCurrentFile) +This is the local-header version of the extra field (sometimes, there is +more info in the local-header version than in the central-header) + + if buf==NULL, it return the size of the local extra field that can be read + + if buf!=NULL, len is the size of the buffer, the extra header is copied in + buf. + the return value is the number of bytes copied in buf, or (if <0) + the error code +*/ +extern int ZEXPORT unzGetLocalExtrafield (unzFile file, voidp buf, unsigned len) +{ + unz64_s* s; + file_in_zip64_read_info_s* pfile_in_zip_read_info; + uInt read_now; + ZPOS64_T size_to_read; + + if (file==NULL) + return UNZ_PARAMERROR; + s=(unz64_s*)file; + pfile_in_zip_read_info=s->pfile_in_zip_read; + + if (pfile_in_zip_read_info==NULL) + return UNZ_PARAMERROR; + + size_to_read = (pfile_in_zip_read_info->size_local_extrafield - + pfile_in_zip_read_info->pos_local_extrafield); + + if (buf==NULL) + return (int)size_to_read; + + if (len>size_to_read) + read_now = (uInt)size_to_read; + else + read_now = (uInt)len ; + + if (read_now==0) + return 0; + + if (ZSEEK64(pfile_in_zip_read_info->z_filefunc, + pfile_in_zip_read_info->filestream, + pfile_in_zip_read_info->offset_local_extrafield + + pfile_in_zip_read_info->pos_local_extrafield, + ZLIB_FILEFUNC_SEEK_SET)!=0) + return UNZ_ERRNO; + + if (ZREAD64(pfile_in_zip_read_info->z_filefunc, + pfile_in_zip_read_info->filestream, + buf,read_now)!=read_now) + return UNZ_ERRNO; + + return (int)read_now; +} + +/* + Close the file in zip opened with unzipOpenCurrentFile + Return UNZ_CRCERROR if all the file was read but the CRC is not good +*/ +extern int ZEXPORT unzCloseCurrentFile (unzFile file) +{ + int err=UNZ_OK; + + unz64_s* s; + file_in_zip64_read_info_s* pfile_in_zip_read_info; + if (file==NULL) + return UNZ_PARAMERROR; + s=(unz64_s*)file; + pfile_in_zip_read_info=s->pfile_in_zip_read; + + if (pfile_in_zip_read_info==NULL) + return UNZ_PARAMERROR; + + + if ((pfile_in_zip_read_info->rest_read_uncompressed == 0) && + (!pfile_in_zip_read_info->raw)) + { + if (pfile_in_zip_read_info->crc32 != pfile_in_zip_read_info->crc32_wait) + err=UNZ_CRCERROR; + } + + + TRYFREE(pfile_in_zip_read_info->read_buffer); + pfile_in_zip_read_info->read_buffer = NULL; + if (pfile_in_zip_read_info->stream_initialised == Z_DEFLATED) + inflateEnd(&pfile_in_zip_read_info->stream); +#ifdef HAVE_BZIP2 + else if (pfile_in_zip_read_info->stream_initialised == Z_BZIP2ED) + BZ2_bzDecompressEnd(&pfile_in_zip_read_info->bstream); +#endif + + + pfile_in_zip_read_info->stream_initialised = 0; + TRYFREE(pfile_in_zip_read_info); + + s->pfile_in_zip_read=NULL; + + return err; +} + + +/* + Get the global comment string of the ZipFile, in the szComment buffer. + uSizeBuf is the size of the szComment buffer. + return the number of byte copied or an error code <0 +*/ +extern int ZEXPORT unzGetGlobalComment (unzFile file, char * szComment, uLong uSizeBuf) +{ + unz64_s* s; + uLong uReadThis ; + if (file==NULL) + return (int)UNZ_PARAMERROR; + s=(unz64_s*)file; + + uReadThis = uSizeBuf; + if (uReadThis>s->gi.size_comment) + uReadThis = s->gi.size_comment; + + if (ZSEEK64(s->z_filefunc,s->filestream,s->central_pos+22,ZLIB_FILEFUNC_SEEK_SET)!=0) + return UNZ_ERRNO; + + if (uReadThis>0) + { + *szComment='\0'; + if (ZREAD64(s->z_filefunc,s->filestream,szComment,uReadThis)!=uReadThis) + return UNZ_ERRNO; + } + + if ((szComment != NULL) && (uSizeBuf > s->gi.size_comment)) + *(szComment+s->gi.size_comment)='\0'; + return (int)uReadThis; +} + +/* Additions by RX '2004 */ +extern ZPOS64_T ZEXPORT unzGetOffset64(unzFile file) +{ + unz64_s* s; + + if (file==NULL) + return 0; //UNZ_PARAMERROR; + s=(unz64_s*)file; + if (!s->current_file_ok) + return 0; + if (s->gi.number_entry != 0 && s->gi.number_entry != 0xffff) + if (s->num_file==s->gi.number_entry) + return 0; + return s->pos_in_central_dir; +} + +extern uLong ZEXPORT unzGetOffset (unzFile file) +{ + ZPOS64_T offset64; + + if (file==NULL) + return 0; //UNZ_PARAMERROR; + offset64 = unzGetOffset64(file); + return (uLong)offset64; +} + +extern int ZEXPORT unzSetOffset64(unzFile file, ZPOS64_T pos) +{ + unz64_s* s; + int err; + + if (file==NULL) + return UNZ_PARAMERROR; + s=(unz64_s*)file; + + s->pos_in_central_dir = pos; + s->num_file = s->gi.number_entry; /* hack */ + err = unz64local_GetCurrentFileInfoInternal(file,&s->cur_file_info, + &s->cur_file_info_internal, + NULL,0,NULL,0,NULL,0); + s->current_file_ok = (err == UNZ_OK); + return err; +} + +extern int ZEXPORT unzSetOffset (unzFile file, uLong pos) +{ + return unzSetOffset64(file,pos); +} diff --git a/BakerShelf/lib/minizip/unzip.h b/BakerShelf/lib/minizip/unzip.h new file mode 100755 index 0000000..3183968 --- /dev/null +++ b/BakerShelf/lib/minizip/unzip.h @@ -0,0 +1,437 @@ +/* unzip.h -- IO for uncompress .zip files using zlib + Version 1.1, February 14h, 2010 + part of the MiniZip project - ( http://www.winimage.com/zLibDll/minizip.html ) + + Copyright (C) 1998-2010 Gilles Vollant (minizip) ( http://www.winimage.com/zLibDll/minizip.html ) + + Modifications of Unzip for Zip64 + Copyright (C) 2007-2008 Even Rouault + + Modifications for Zip64 support on both zip and unzip + Copyright (C) 2009-2010 Mathias Svensson ( http://result42.com ) + + For more info read MiniZip_info.txt + + --------------------------------------------------------------------------------- + + Condition of use and distribution are the same than zlib : + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + --------------------------------------------------------------------------------- + + Changes + + See header of unzip64.c + +*/ + +#ifndef _unz64_H +#define _unz64_H + +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef _ZLIB_H +#include "zlib.h" +#endif + +#ifndef _ZLIBIOAPI_H +#include "ioapi.h" +#endif + +#ifdef HAVE_BZIP2 +#include "bzlib.h" +#endif + +#define Z_BZIP2ED 12 + +#if defined(STRICTUNZIP) || defined(STRICTZIPUNZIP) +/* like the STRICT of WIN32, we define a pointer that cannot be converted + from (void*) without cast */ +typedef struct TagunzFile__ { int unused; } unzFile__; +typedef unzFile__ *unzFile; +#else +typedef voidp unzFile; +#endif + + +#define UNZ_OK (0) +#define UNZ_END_OF_LIST_OF_FILE (-100) +#define UNZ_ERRNO (Z_ERRNO) +#define UNZ_EOF (0) +#define UNZ_PARAMERROR (-102) +#define UNZ_BADZIPFILE (-103) +#define UNZ_INTERNALERROR (-104) +#define UNZ_CRCERROR (-105) + +/* tm_unz contain date/time info */ +typedef struct tm_unz_s +{ + uInt tm_sec; /* seconds after the minute - [0,59] */ + uInt tm_min; /* minutes after the hour - [0,59] */ + uInt tm_hour; /* hours since midnight - [0,23] */ + uInt tm_mday; /* day of the month - [1,31] */ + uInt tm_mon; /* months since January - [0,11] */ + uInt tm_year; /* years - [1980..2044] */ +} tm_unz; + +/* unz_global_info structure contain global data about the ZIPfile + These data comes from the end of central dir */ +typedef struct unz_global_info64_s +{ + ZPOS64_T number_entry; /* total number of entries in + the central dir on this disk */ + uLong size_comment; /* size of the global comment of the zipfile */ +} unz_global_info64; + +typedef struct unz_global_info_s +{ + uLong number_entry; /* total number of entries in + the central dir on this disk */ + uLong size_comment; /* size of the global comment of the zipfile */ +} unz_global_info; + +/* unz_file_info contain information about a file in the zipfile */ +typedef struct unz_file_info64_s +{ + uLong version; /* version made by 2 bytes */ + uLong version_needed; /* version needed to extract 2 bytes */ + uLong flag; /* general purpose bit flag 2 bytes */ + uLong compression_method; /* compression method 2 bytes */ + uLong dosDate; /* last mod file date in Dos fmt 4 bytes */ + uLong crc; /* crc-32 4 bytes */ + ZPOS64_T compressed_size; /* compressed size 8 bytes */ + ZPOS64_T uncompressed_size; /* uncompressed size 8 bytes */ + uLong size_filename; /* filename length 2 bytes */ + uLong size_file_extra; /* extra field length 2 bytes */ + uLong size_file_comment; /* file comment length 2 bytes */ + + uLong disk_num_start; /* disk number start 2 bytes */ + uLong internal_fa; /* internal file attributes 2 bytes */ + uLong external_fa; /* external file attributes 4 bytes */ + + tm_unz tmu_date; +} unz_file_info64; + +typedef struct unz_file_info_s +{ + uLong version; /* version made by 2 bytes */ + uLong version_needed; /* version needed to extract 2 bytes */ + uLong flag; /* general purpose bit flag 2 bytes */ + uLong compression_method; /* compression method 2 bytes */ + uLong dosDate; /* last mod file date in Dos fmt 4 bytes */ + uLong crc; /* crc-32 4 bytes */ + uLong compressed_size; /* compressed size 4 bytes */ + uLong uncompressed_size; /* uncompressed size 4 bytes */ + uLong size_filename; /* filename length 2 bytes */ + uLong size_file_extra; /* extra field length 2 bytes */ + uLong size_file_comment; /* file comment length 2 bytes */ + + uLong disk_num_start; /* disk number start 2 bytes */ + uLong internal_fa; /* internal file attributes 2 bytes */ + uLong external_fa; /* external file attributes 4 bytes */ + + tm_unz tmu_date; +} unz_file_info; + +extern int ZEXPORT unzStringFileNameCompare OF ((const char* fileName1, + const char* fileName2, + int iCaseSensitivity)); +/* + Compare two filename (fileName1,fileName2). + If iCaseSenisivity = 1, comparision is case sensitivity (like strcmp) + If iCaseSenisivity = 2, comparision is not case sensitivity (like strcmpi + or strcasecmp) + If iCaseSenisivity = 0, case sensitivity is defaut of your operating system + (like 1 on Unix, 2 on Windows) +*/ + + +extern unzFile ZEXPORT unzOpen OF((const char *path)); +extern unzFile ZEXPORT unzOpen64 OF((const void *path)); +/* + Open a Zip file. path contain the full pathname (by example, + on a Windows XP computer "c:\\zlib\\zlib113.zip" or on an Unix computer + "zlib/zlib113.zip". + If the zipfile cannot be opened (file don't exist or in not valid), the + return value is NULL. + Else, the return value is a unzFile Handle, usable with other function + of this unzip package. + the "64" function take a const void* pointer, because the path is just the + value passed to the open64_file_func callback. + Under Windows, if UNICODE is defined, using fill_fopen64_filefunc, the path + is a pointer to a wide unicode string (LPCTSTR is LPCWSTR), so const char* + does not describe the reality +*/ + + +extern unzFile ZEXPORT unzOpen2 OF((const char *path, + zlib_filefunc_def* pzlib_filefunc_def)); +/* + Open a Zip file, like unzOpen, but provide a set of file low level API + for read/write the zip file (see ioapi.h) +*/ + +extern unzFile ZEXPORT unzOpen2_64 OF((const void *path, + zlib_filefunc64_def* pzlib_filefunc_def)); +/* + Open a Zip file, like unz64Open, but provide a set of file low level API + for read/write the zip file (see ioapi.h) +*/ + +extern int ZEXPORT unzClose OF((unzFile file)); +/* + Close a ZipFile opened with unzipOpen. + If there is files inside the .Zip opened with unzOpenCurrentFile (see later), + these files MUST be closed with unzipCloseCurrentFile before call unzipClose. + return UNZ_OK if there is no problem. */ + +extern int ZEXPORT unzGetGlobalInfo OF((unzFile file, + unz_global_info *pglobal_info)); + +extern int ZEXPORT unzGetGlobalInfo64 OF((unzFile file, + unz_global_info64 *pglobal_info)); +/* + Write info about the ZipFile in the *pglobal_info structure. + No preparation of the structure is needed + return UNZ_OK if there is no problem. */ + + +extern int ZEXPORT unzGetGlobalComment OF((unzFile file, + char *szComment, + uLong uSizeBuf)); +/* + Get the global comment string of the ZipFile, in the szComment buffer. + uSizeBuf is the size of the szComment buffer. + return the number of byte copied or an error code <0 +*/ + + +/***************************************************************************/ +/* Unzip package allow you browse the directory of the zipfile */ + +extern int ZEXPORT unzGoToFirstFile OF((unzFile file)); +/* + Set the current file of the zipfile to the first file. + return UNZ_OK if there is no problem +*/ + +extern int ZEXPORT unzGoToNextFile OF((unzFile file)); +/* + Set the current file of the zipfile to the next file. + return UNZ_OK if there is no problem + return UNZ_END_OF_LIST_OF_FILE if the actual file was the latest. +*/ + +extern int ZEXPORT unzLocateFile OF((unzFile file, + const char *szFileName, + int iCaseSensitivity)); +/* + Try locate the file szFileName in the zipfile. + For the iCaseSensitivity signification, see unzStringFileNameCompare + + return value : + UNZ_OK if the file is found. It becomes the current file. + UNZ_END_OF_LIST_OF_FILE if the file is not found +*/ + + +/* ****************************************** */ +/* Ryan supplied functions */ +/* unz_file_info contain information about a file in the zipfile */ +typedef struct unz_file_pos_s +{ + uLong pos_in_zip_directory; /* offset in zip file directory */ + uLong num_of_file; /* # of file */ +} unz_file_pos; + +extern int ZEXPORT unzGetFilePos( + unzFile file, + unz_file_pos* file_pos); + +extern int ZEXPORT unzGoToFilePos( + unzFile file, + unz_file_pos* file_pos); + +typedef struct unz64_file_pos_s +{ + ZPOS64_T pos_in_zip_directory; /* offset in zip file directory */ + ZPOS64_T num_of_file; /* # of file */ +} unz64_file_pos; + +extern int ZEXPORT unzGetFilePos64( + unzFile file, + unz64_file_pos* file_pos); + +extern int ZEXPORT unzGoToFilePos64( + unzFile file, + const unz64_file_pos* file_pos); + +/* ****************************************** */ + +extern int ZEXPORT unzGetCurrentFileInfo64 OF((unzFile file, + unz_file_info64 *pfile_info, + char *szFileName, + uLong fileNameBufferSize, + void *extraField, + uLong extraFieldBufferSize, + char *szComment, + uLong commentBufferSize)); + +extern int ZEXPORT unzGetCurrentFileInfo OF((unzFile file, + unz_file_info *pfile_info, + char *szFileName, + uLong fileNameBufferSize, + void *extraField, + uLong extraFieldBufferSize, + char *szComment, + uLong commentBufferSize)); +/* + Get Info about the current file + if pfile_info!=NULL, the *pfile_info structure will contain somes info about + the current file + if szFileName!=NULL, the filemane string will be copied in szFileName + (fileNameBufferSize is the size of the buffer) + if extraField!=NULL, the extra field information will be copied in extraField + (extraFieldBufferSize is the size of the buffer). + This is the Central-header version of the extra field + if szComment!=NULL, the comment string of the file will be copied in szComment + (commentBufferSize is the size of the buffer) +*/ + + +/** Addition for GDAL : START */ + +extern ZPOS64_T ZEXPORT unzGetCurrentFileZStreamPos64 OF((unzFile file)); + +/** Addition for GDAL : END */ + + +/***************************************************************************/ +/* for reading the content of the current zipfile, you can open it, read data + from it, and close it (you can close it before reading all the file) + */ + +extern int ZEXPORT unzOpenCurrentFile OF((unzFile file)); +/* + Open for reading data the current file in the zipfile. + If there is no error, the return value is UNZ_OK. +*/ + +extern int ZEXPORT unzOpenCurrentFilePassword OF((unzFile file, + const char* password)); +/* + Open for reading data the current file in the zipfile. + password is a crypting password + If there is no error, the return value is UNZ_OK. +*/ + +extern int ZEXPORT unzOpenCurrentFile2 OF((unzFile file, + int* method, + int* level, + int raw)); +/* + Same than unzOpenCurrentFile, but open for read raw the file (not uncompress) + if raw==1 + *method will receive method of compression, *level will receive level of + compression + note : you can set level parameter as NULL (if you did not want known level, + but you CANNOT set method parameter as NULL +*/ + +extern int ZEXPORT unzOpenCurrentFile3 OF((unzFile file, + int* method, + int* level, + int raw, + const char* password)); +/* + Same than unzOpenCurrentFile, but open for read raw the file (not uncompress) + if raw==1 + *method will receive method of compression, *level will receive level of + compression + note : you can set level parameter as NULL (if you did not want known level, + but you CANNOT set method parameter as NULL +*/ + + +extern int ZEXPORT unzCloseCurrentFile OF((unzFile file)); +/* + Close the file in zip opened with unzOpenCurrentFile + Return UNZ_CRCERROR if all the file was read but the CRC is not good +*/ + +extern int ZEXPORT unzReadCurrentFile OF((unzFile file, + voidp buf, + unsigned len)); +/* + Read bytes from the current file (opened by unzOpenCurrentFile) + buf contain buffer where data must be copied + len the size of buf. + + return the number of byte copied if somes bytes are copied + return 0 if the end of file was reached + return <0 with error code if there is an error + (UNZ_ERRNO for IO error, or zLib error for uncompress error) +*/ + +extern z_off_t ZEXPORT unztell OF((unzFile file)); + +extern ZPOS64_T ZEXPORT unztell64 OF((unzFile file)); +/* + Give the current position in uncompressed data +*/ + +extern int ZEXPORT unzeof OF((unzFile file)); +/* + return 1 if the end of file was reached, 0 elsewhere +*/ + +extern int ZEXPORT unzGetLocalExtrafield OF((unzFile file, + voidp buf, + unsigned len)); +/* + Read extra field from the current file (opened by unzOpenCurrentFile) + This is the local-header version of the extra field (sometimes, there is + more info in the local-header version than in the central-header) + + if buf==NULL, it return the size of the local extra field + + if buf!=NULL, len is the size of the buffer, the extra header is copied in + buf. + the return value is the number of bytes copied in buf, or (if <0) + the error code +*/ + +/***************************************************************************/ + +/* Get the current file offset */ +extern ZPOS64_T ZEXPORT unzGetOffset64 (unzFile file); +extern uLong ZEXPORT unzGetOffset (unzFile file); + +/* Set the current file offset */ +extern int ZEXPORT unzSetOffset64 (unzFile file, ZPOS64_T pos); +extern int ZEXPORT unzSetOffset (unzFile file, uLong pos); + + + +#ifdef __cplusplus +} +#endif + +#endif /* _unz64_H */ diff --git a/BakerShelf/lib/minizip/zip.c b/BakerShelf/lib/minizip/zip.c new file mode 100755 index 0000000..8e03a96 --- /dev/null +++ b/BakerShelf/lib/minizip/zip.c @@ -0,0 +1,2022 @@ +/* zip.c -- IO on .zip files using zlib + Version 1.1, February 14h, 2010 + part of the MiniZip project - ( http://www.winimage.com/zLibDll/minizip.html ) + + Copyright (C) 1998-2010 Gilles Vollant (minizip) ( http://www.winimage.com/zLibDll/minizip.html ) + + Modifications for Zip64 support + Copyright (C) 2009-2010 Mathias Svensson ( http://result42.com ) + + For more info read MiniZip_info.txt + + Changes + Oct-2009 - Mathias Svensson - Remove old C style function prototypes + Oct-2009 - Mathias Svensson - Added Zip64 Support when creating new file archives + Oct-2009 - Mathias Svensson - Did some code cleanup and refactoring to get better overview of some functions. + Oct-2009 - Mathias Svensson - Added zipRemoveExtraInfoBlock to strip extra field data from its ZIP64 data + It is used when recreting zip archive with RAW when deleting items from a zip. + ZIP64 data is automaticly added to items that needs it, and existing ZIP64 data need to be removed. + Oct-2009 - Mathias Svensson - Added support for BZIP2 as compression mode (bzip2 lib is required) + Jan-2010 - back to unzip and minizip 1.0 name scheme, with compatibility layer + +*/ + + +#include +#include +#include +#include +#include "zlib.h" +#include "zip.h" + +#ifdef STDC +# include +# include +# include +#endif +#ifdef NO_ERRNO_H + extern int errno; +#else +# include +#endif + + +#ifndef local +# define local static +#endif +/* compile with -Dlocal if your debugger can't find static symbols */ + +#ifndef VERSIONMADEBY +# define VERSIONMADEBY (0x0) /* platform depedent */ +#endif + +#ifndef Z_BUFSIZE +#define Z_BUFSIZE (64*1024) //(16384) +#endif + +#ifndef Z_MAXFILENAMEINZIP +#define Z_MAXFILENAMEINZIP (256) +#endif + +#ifndef ALLOC +# define ALLOC(size) (malloc(size)) +#endif +#ifndef TRYFREE +# define TRYFREE(p) {if (p) free(p);} +#endif + +/* +#define SIZECENTRALDIRITEM (0x2e) +#define SIZEZIPLOCALHEADER (0x1e) +*/ + +/* I've found an old Unix (a SunOS 4.1.3_U1) without all SEEK_* defined.... */ + + +// NOT sure that this work on ALL platform +#define MAKEULONG64(a, b) ((ZPOS64_T)(((unsigned long)(a)) | ((ZPOS64_T)((unsigned long)(b))) << 32)) + +#ifndef SEEK_CUR +#define SEEK_CUR 1 +#endif + +#ifndef SEEK_END +#define SEEK_END 2 +#endif + +#ifndef SEEK_SET +#define SEEK_SET 0 +#endif + +#ifndef DEF_MEM_LEVEL +#if MAX_MEM_LEVEL >= 8 +# define DEF_MEM_LEVEL 8 +#else +# define DEF_MEM_LEVEL MAX_MEM_LEVEL +#endif +#endif +const char zip_copyright[] =" zip 1.01 Copyright 1998-2004 Gilles Vollant - http://www.winimage.com/zLibDll"; + + +#define SIZEDATA_INDATABLOCK (4096- (4*4)) + +#define LOCALHEADERMAGIC (0x04034b50) +#define CENTRALHEADERMAGIC (0x02014b50) +#define ENDHEADERMAGIC (0x06054b50) +#define ZIP64ENDHEADERMAGIC (0x6064b50) +#define ZIP64ENDLOCHEADERMAGIC (0x7064b50) + +#define FLAG_LOCALHEADER_OFFSET (0x06) +#define CRC_LOCALHEADER_OFFSET (0x0e) + +#define SIZECENTRALHEADER (0x2e) /* 46 */ + +typedef struct linkedlist_datablock_internal_s +{ + struct linkedlist_datablock_internal_s* next_datablock; + uLong avail_in_this_block; + uLong filled_in_this_block; + uLong unused; /* for future use and alignement */ + unsigned char data[SIZEDATA_INDATABLOCK]; +} linkedlist_datablock_internal; + +typedef struct linkedlist_data_s +{ + linkedlist_datablock_internal* first_block; + linkedlist_datablock_internal* last_block; +} linkedlist_data; + + +typedef struct +{ + z_stream stream; /* zLib stream structure for inflate */ +#ifdef HAVE_BZIP2 + bz_stream bstream; /* bzLib stream structure for bziped */ +#endif + + int stream_initialised; /* 1 is stream is initialised */ + uInt pos_in_buffered_data; /* last written byte in buffered_data */ + + ZPOS64_T pos_local_header; /* offset of the local header of the file + currenty writing */ + char* central_header; /* central header data for the current file */ + uLong size_centralExtra; + uLong size_centralheader; /* size of the central header for cur file */ + uLong size_centralExtraFree; /* Extra bytes allocated to the centralheader but that are not used */ + uLong flag; /* flag of the file currently writing */ + + int method; /* compression method of file currenty wr.*/ + int raw; /* 1 for directly writing raw data */ + Byte buffered_data[Z_BUFSIZE];/* buffer contain compressed data to be writ*/ + uLong dosDate; + uLong crc32; + int encrypt; + int zip64; /* Add ZIP64 extened information in the extra field */ + ZPOS64_T pos_zip64extrainfo; + ZPOS64_T totalCompressedData; + ZPOS64_T totalUncompressedData; +#ifndef NOCRYPT + unsigned long keys[3]; /* keys defining the pseudo-random sequence */ + const unsigned long* pcrc_32_tab; + int crypt_header_size; +#endif +} curfile64_info; + +typedef struct +{ + zlib_filefunc64_32_def z_filefunc; + voidpf filestream; /* io structore of the zipfile */ + linkedlist_data central_dir;/* datablock with central dir in construction*/ + int in_opened_file_inzip; /* 1 if a file in the zip is currently writ.*/ + curfile64_info ci; /* info on the file curretly writing */ + + ZPOS64_T begin_pos; /* position of the beginning of the zipfile */ + ZPOS64_T add_position_when_writting_offset; + ZPOS64_T number_entry; + +#ifndef NO_ADDFILEINEXISTINGZIP + char *globalcomment; +#endif + +} zip64_internal; + + +#ifndef NOCRYPT +#define INCLUDECRYPTINGCODE_IFCRYPTALLOWED +#include "crypt.h" +#endif + +local linkedlist_datablock_internal* allocate_new_datablock() +{ + linkedlist_datablock_internal* ldi; + ldi = (linkedlist_datablock_internal*) + ALLOC(sizeof(linkedlist_datablock_internal)); + if (ldi!=NULL) + { + ldi->next_datablock = NULL ; + ldi->filled_in_this_block = 0 ; + ldi->avail_in_this_block = SIZEDATA_INDATABLOCK ; + } + return ldi; +} + +local void free_datablock(linkedlist_datablock_internal* ldi) +{ + while (ldi!=NULL) + { + linkedlist_datablock_internal* ldinext = ldi->next_datablock; + TRYFREE(ldi); + ldi = ldinext; + } +} + +local void init_linkedlist(linkedlist_data* ll) +{ + ll->first_block = ll->last_block = NULL; +} + +local void free_linkedlist(linkedlist_data* ll) +{ + free_datablock(ll->first_block); + ll->first_block = ll->last_block = NULL; +} + + +local int add_data_in_datablock(linkedlist_data* ll, const void* buf, uLong len) +{ + linkedlist_datablock_internal* ldi; + const unsigned char* from_copy; + + if (ll==NULL) + return ZIP_INTERNALERROR; + + if (ll->last_block == NULL) + { + ll->first_block = ll->last_block = allocate_new_datablock(); + if (ll->first_block == NULL) + return ZIP_INTERNALERROR; + } + + ldi = ll->last_block; + from_copy = (unsigned char*)buf; + + while (len>0) + { + uInt copy_this; + uInt i; + unsigned char* to_copy; + + if (ldi->avail_in_this_block==0) + { + ldi->next_datablock = allocate_new_datablock(); + if (ldi->next_datablock == NULL) + return ZIP_INTERNALERROR; + ldi = ldi->next_datablock ; + ll->last_block = ldi; + } + + if (ldi->avail_in_this_block < len) + copy_this = (uInt)ldi->avail_in_this_block; + else + copy_this = (uInt)len; + + to_copy = &(ldi->data[ldi->filled_in_this_block]); + + for (i=0;ifilled_in_this_block += copy_this; + ldi->avail_in_this_block -= copy_this; + from_copy += copy_this ; + len -= copy_this; + } + return ZIP_OK; +} + + + +/****************************************************************************/ + +#ifndef NO_ADDFILEINEXISTINGZIP +/* =========================================================================== + Inputs a long in LSB order to the given file + nbByte == 1, 2 ,4 or 8 (byte, short or long, ZPOS64_T) +*/ + +local int zip64local_putValue OF((const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, ZPOS64_T x, int nbByte)); +local int zip64local_putValue (const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, ZPOS64_T x, int nbByte) +{ + unsigned char buf[8]; + int n; + for (n = 0; n < nbByte; n++) + { + buf[n] = (unsigned char)(x & 0xff); + x >>= 8; + } + if (x != 0) + { /* data overflow - hack for ZIP64 (X Roche) */ + for (n = 0; n < nbByte; n++) + { + buf[n] = 0xff; + } + } + + if (ZWRITE64(*pzlib_filefunc_def,filestream,buf,nbByte)!=(uLong)nbByte) + return ZIP_ERRNO; + else + return ZIP_OK; +} + +local void zip64local_putValue_inmemory OF((void* dest, ZPOS64_T x, int nbByte)); +local void zip64local_putValue_inmemory (void* dest, ZPOS64_T x, int nbByte) +{ + unsigned char* buf=(unsigned char*)dest; + int n; + for (n = 0; n < nbByte; n++) { + buf[n] = (unsigned char)(x & 0xff); + x >>= 8; + } + + if (x != 0) + { /* data overflow - hack for ZIP64 */ + for (n = 0; n < nbByte; n++) + { + buf[n] = 0xff; + } + } +} + +/****************************************************************************/ + + +local uLong zip64local_TmzDateToDosDate(const tm_zip* ptm) +{ + uLong year = (uLong)ptm->tm_year; + if (year>=1980) + year-=1980; + else if (year>=80) + year-=80; + return + (uLong) (((ptm->tm_mday) + (32 * (ptm->tm_mon+1)) + (512 * year)) << 16) | + ((ptm->tm_sec/2) + (32* ptm->tm_min) + (2048 * (uLong)ptm->tm_hour)); +} + + +/****************************************************************************/ + +local int zip64local_getByte OF((const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, int *pi)); + +local int zip64local_getByte(const zlib_filefunc64_32_def* pzlib_filefunc_def,voidpf filestream,int* pi) +{ + unsigned char c; + int err = (int)ZREAD64(*pzlib_filefunc_def,filestream,&c,1); + if (err==1) + { + *pi = (int)c; + return ZIP_OK; + } + else + { + if (ZERROR64(*pzlib_filefunc_def,filestream)) + return ZIP_ERRNO; + else + return ZIP_EOF; + } +} + + +/* =========================================================================== + Reads a long in LSB order from the given gz_stream. Sets +*/ +local int zip64local_getShort OF((const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, uLong *pX)); + +local int zip64local_getShort (const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, uLong* pX) +{ + uLong x ; + int i = 0; + int err; + + err = zip64local_getByte(pzlib_filefunc_def,filestream,&i); + x = (uLong)i; + + if (err==ZIP_OK) + err = zip64local_getByte(pzlib_filefunc_def,filestream,&i); + x += ((uLong)i)<<8; + + if (err==ZIP_OK) + *pX = x; + else + *pX = 0; + return err; +} + +local int zip64local_getLong OF((const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, uLong *pX)); + +local int zip64local_getLong (const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, uLong* pX) +{ + uLong x ; + int i = 0; + int err; + + err = zip64local_getByte(pzlib_filefunc_def,filestream,&i); + x = (uLong)i; + + if (err==ZIP_OK) + err = zip64local_getByte(pzlib_filefunc_def,filestream,&i); + x += ((uLong)i)<<8; + + if (err==ZIP_OK) + err = zip64local_getByte(pzlib_filefunc_def,filestream,&i); + x += ((uLong)i)<<16; + + if (err==ZIP_OK) + err = zip64local_getByte(pzlib_filefunc_def,filestream,&i); + x += ((uLong)i)<<24; + + if (err==ZIP_OK) + *pX = x; + else + *pX = 0; + return err; +} + +local int zip64local_getLong64 OF((const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, ZPOS64_T *pX)); + + +local int zip64local_getLong64 (const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, ZPOS64_T *pX) +{ + ZPOS64_T x; + int i = 0; + int err; + + err = zip64local_getByte(pzlib_filefunc_def,filestream,&i); + x = (ZPOS64_T)i; + + if (err==ZIP_OK) + err = zip64local_getByte(pzlib_filefunc_def,filestream,&i); + x += ((ZPOS64_T)i)<<8; + + if (err==ZIP_OK) + err = zip64local_getByte(pzlib_filefunc_def,filestream,&i); + x += ((ZPOS64_T)i)<<16; + + if (err==ZIP_OK) + err = zip64local_getByte(pzlib_filefunc_def,filestream,&i); + x += ((ZPOS64_T)i)<<24; + + if (err==ZIP_OK) + err = zip64local_getByte(pzlib_filefunc_def,filestream,&i); + x += ((ZPOS64_T)i)<<32; + + if (err==ZIP_OK) + err = zip64local_getByte(pzlib_filefunc_def,filestream,&i); + x += ((ZPOS64_T)i)<<40; + + if (err==ZIP_OK) + err = zip64local_getByte(pzlib_filefunc_def,filestream,&i); + x += ((ZPOS64_T)i)<<48; + + if (err==ZIP_OK) + err = zip64local_getByte(pzlib_filefunc_def,filestream,&i); + x += ((ZPOS64_T)i)<<56; + + if (err==ZIP_OK) + *pX = x; + else + *pX = 0; + + return err; +} + +#ifndef BUFREADCOMMENT +#define BUFREADCOMMENT (0x400) +#endif +/* + Locate the Central directory of a zipfile (at the end, just before + the global comment) +*/ +local ZPOS64_T zip64local_SearchCentralDir OF((const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream)); + +local ZPOS64_T zip64local_SearchCentralDir(const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream) +{ + unsigned char* buf; + ZPOS64_T uSizeFile; + ZPOS64_T uBackRead; + ZPOS64_T uMaxBack=0xffff; /* maximum size of global comment */ + ZPOS64_T uPosFound=0; + + if (ZSEEK64(*pzlib_filefunc_def,filestream,0,ZLIB_FILEFUNC_SEEK_END) != 0) + return 0; + + + uSizeFile = ZTELL64(*pzlib_filefunc_def,filestream); + + if (uMaxBack>uSizeFile) + uMaxBack = uSizeFile; + + buf = (unsigned char*)ALLOC(BUFREADCOMMENT+4); + if (buf==NULL) + return 0; + + uBackRead = 4; + while (uBackReaduMaxBack) + uBackRead = uMaxBack; + else + uBackRead+=BUFREADCOMMENT; + uReadPos = uSizeFile-uBackRead ; + + uReadSize = ((BUFREADCOMMENT+4) < (uSizeFile-uReadPos)) ? + (BUFREADCOMMENT+4) : (uLong)(uSizeFile-uReadPos); + if (ZSEEK64(*pzlib_filefunc_def,filestream,uReadPos,ZLIB_FILEFUNC_SEEK_SET)!=0) + break; + + if (ZREAD64(*pzlib_filefunc_def,filestream,buf,uReadSize)!=uReadSize) + break; + + for (i=(int)uReadSize-3; (i--)>0;) + if (((*(buf+i))==0x50) && ((*(buf+i+1))==0x4b) && + ((*(buf+i+2))==0x05) && ((*(buf+i+3))==0x06)) + { + uPosFound = uReadPos+i; + break; + } + + if (uPosFound!=0) + break; + } + TRYFREE(buf); + return uPosFound; +} + +/* +Locate the End of Zip64 Central directory locator and from there find the CD of a zipfile (at the end, just before +the global comment) +*/ +local ZPOS64_T zip64local_SearchCentralDir64 OF((const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream)); + +local ZPOS64_T zip64local_SearchCentralDir64(const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream) +{ + unsigned char* buf; + ZPOS64_T uSizeFile; + ZPOS64_T uBackRead; + ZPOS64_T uMaxBack=0xffff; /* maximum size of global comment */ + ZPOS64_T uPosFound=0; + uLong uL; + ZPOS64_T relativeOffset; + + if (ZSEEK64(*pzlib_filefunc_def,filestream,0,ZLIB_FILEFUNC_SEEK_END) != 0) + return 0; + + uSizeFile = ZTELL64(*pzlib_filefunc_def,filestream); + + if (uMaxBack>uSizeFile) + uMaxBack = uSizeFile; + + buf = (unsigned char*)ALLOC(BUFREADCOMMENT+4); + if (buf==NULL) + return 0; + + uBackRead = 4; + while (uBackReaduMaxBack) + uBackRead = uMaxBack; + else + uBackRead+=BUFREADCOMMENT; + uReadPos = uSizeFile-uBackRead ; + + uReadSize = ((BUFREADCOMMENT+4) < (uSizeFile-uReadPos)) ? + (BUFREADCOMMENT+4) : (uLong)(uSizeFile-uReadPos); + if (ZSEEK64(*pzlib_filefunc_def,filestream,uReadPos,ZLIB_FILEFUNC_SEEK_SET)!=0) + break; + + if (ZREAD64(*pzlib_filefunc_def,filestream,buf,uReadSize)!=uReadSize) + break; + + for (i=(int)uReadSize-3; (i--)>0;) + { + // Signature "0x07064b50" Zip64 end of central directory locater + if (((*(buf+i))==0x50) && ((*(buf+i+1))==0x4b) && ((*(buf+i+2))==0x06) && ((*(buf+i+3))==0x07)) + { + uPosFound = uReadPos+i; + break; + } + } + + if (uPosFound!=0) + break; + } + + TRYFREE(buf); + if (uPosFound == 0) + return 0; + + /* Zip64 end of central directory locator */ + if (ZSEEK64(*pzlib_filefunc_def,filestream, uPosFound,ZLIB_FILEFUNC_SEEK_SET)!=0) + return 0; + + /* the signature, already checked */ + if (zip64local_getLong(pzlib_filefunc_def,filestream,&uL)!=ZIP_OK) + return 0; + + /* number of the disk with the start of the zip64 end of central directory */ + if (zip64local_getLong(pzlib_filefunc_def,filestream,&uL)!=ZIP_OK) + return 0; + if (uL != 0) + return 0; + + /* relative offset of the zip64 end of central directory record */ + if (zip64local_getLong64(pzlib_filefunc_def,filestream,&relativeOffset)!=ZIP_OK) + return 0; + + /* total number of disks */ + if (zip64local_getLong(pzlib_filefunc_def,filestream,&uL)!=ZIP_OK) + return 0; + if (uL != 1) + return 0; + + /* Goto Zip64 end of central directory record */ + if (ZSEEK64(*pzlib_filefunc_def,filestream, relativeOffset,ZLIB_FILEFUNC_SEEK_SET)!=0) + return 0; + + /* the signature */ + if (zip64local_getLong(pzlib_filefunc_def,filestream,&uL)!=ZIP_OK) + return 0; + + if (uL != 0x06064b50) // signature of 'Zip64 end of central directory' + return 0; + + return relativeOffset; +} + +int LoadCentralDirectoryRecord(zip64_internal* pziinit); +int LoadCentralDirectoryRecord(zip64_internal* pziinit) +{ + int err=ZIP_OK; + ZPOS64_T byte_before_the_zipfile;/* byte before the zipfile, (>0 for sfx)*/ + + ZPOS64_T size_central_dir; /* size of the central directory */ + ZPOS64_T offset_central_dir; /* offset of start of central directory */ + ZPOS64_T central_pos; + uLong uL; + + uLong number_disk; /* number of the current dist, used for + spaning ZIP, unsupported, always 0*/ + uLong number_disk_with_CD; /* number the the disk with central dir, used + for spaning ZIP, unsupported, always 0*/ + ZPOS64_T number_entry; + ZPOS64_T number_entry_CD; /* total number of entries in + the central dir + (same than number_entry on nospan) */ + uLong VersionMadeBy; + uLong VersionNeeded; + uLong size_comment; + + int hasZIP64Record = 0; + + // check first if we find a ZIP64 record + central_pos = zip64local_SearchCentralDir64(&pziinit->z_filefunc,pziinit->filestream); + if(central_pos > 0) + { + hasZIP64Record = 1; + } + else if(central_pos == 0) + { + central_pos = zip64local_SearchCentralDir(&pziinit->z_filefunc,pziinit->filestream); + } + +/* disable to allow appending to empty ZIP archive + if (central_pos==0) + err=ZIP_ERRNO; +*/ + + if(hasZIP64Record) + { + ZPOS64_T sizeEndOfCentralDirectory; + if (ZSEEK64(pziinit->z_filefunc, pziinit->filestream, central_pos, ZLIB_FILEFUNC_SEEK_SET) != 0) + err=ZIP_ERRNO; + + /* the signature, already checked */ + if (zip64local_getLong(&pziinit->z_filefunc, pziinit->filestream,&uL)!=ZIP_OK) + err=ZIP_ERRNO; + + /* size of zip64 end of central directory record */ + if (zip64local_getLong64(&pziinit->z_filefunc, pziinit->filestream, &sizeEndOfCentralDirectory)!=ZIP_OK) + err=ZIP_ERRNO; + + /* version made by */ + if (zip64local_getShort(&pziinit->z_filefunc, pziinit->filestream, &VersionMadeBy)!=ZIP_OK) + err=ZIP_ERRNO; + + /* version needed to extract */ + if (zip64local_getShort(&pziinit->z_filefunc, pziinit->filestream, &VersionNeeded)!=ZIP_OK) + err=ZIP_ERRNO; + + /* number of this disk */ + if (zip64local_getLong(&pziinit->z_filefunc, pziinit->filestream,&number_disk)!=ZIP_OK) + err=ZIP_ERRNO; + + /* number of the disk with the start of the central directory */ + if (zip64local_getLong(&pziinit->z_filefunc, pziinit->filestream,&number_disk_with_CD)!=ZIP_OK) + err=ZIP_ERRNO; + + /* total number of entries in the central directory on this disk */ + if (zip64local_getLong64(&pziinit->z_filefunc, pziinit->filestream, &number_entry)!=ZIP_OK) + err=ZIP_ERRNO; + + /* total number of entries in the central directory */ + if (zip64local_getLong64(&pziinit->z_filefunc, pziinit->filestream,&number_entry_CD)!=ZIP_OK) + err=ZIP_ERRNO; + + if ((number_entry_CD!=number_entry) || (number_disk_with_CD!=0) || (number_disk!=0)) + err=ZIP_BADZIPFILE; + + /* size of the central directory */ + if (zip64local_getLong64(&pziinit->z_filefunc, pziinit->filestream,&size_central_dir)!=ZIP_OK) + err=ZIP_ERRNO; + + /* offset of start of central directory with respect to the + starting disk number */ + if (zip64local_getLong64(&pziinit->z_filefunc, pziinit->filestream,&offset_central_dir)!=ZIP_OK) + err=ZIP_ERRNO; + + // TODO.. + // read the comment from the standard central header. + size_comment = 0; + } + else + { + // Read End of central Directory info + if (ZSEEK64(pziinit->z_filefunc, pziinit->filestream, central_pos,ZLIB_FILEFUNC_SEEK_SET)!=0) + err=ZIP_ERRNO; + + /* the signature, already checked */ + if (zip64local_getLong(&pziinit->z_filefunc, pziinit->filestream,&uL)!=ZIP_OK) + err=ZIP_ERRNO; + + /* number of this disk */ + if (zip64local_getShort(&pziinit->z_filefunc, pziinit->filestream,&number_disk)!=ZIP_OK) + err=ZIP_ERRNO; + + /* number of the disk with the start of the central directory */ + if (zip64local_getShort(&pziinit->z_filefunc, pziinit->filestream,&number_disk_with_CD)!=ZIP_OK) + err=ZIP_ERRNO; + + /* total number of entries in the central dir on this disk */ + number_entry = 0; + if (zip64local_getShort(&pziinit->z_filefunc, pziinit->filestream, &uL)!=ZIP_OK) + err=ZIP_ERRNO; + else + number_entry = uL; + + /* total number of entries in the central dir */ + number_entry_CD = 0; + if (zip64local_getShort(&pziinit->z_filefunc, pziinit->filestream, &uL)!=ZIP_OK) + err=ZIP_ERRNO; + else + number_entry_CD = uL; + + if ((number_entry_CD!=number_entry) || (number_disk_with_CD!=0) || (number_disk!=0)) + err=ZIP_BADZIPFILE; + + /* size of the central directory */ + size_central_dir = 0; + if (zip64local_getLong(&pziinit->z_filefunc, pziinit->filestream, &uL)!=ZIP_OK) + err=ZIP_ERRNO; + else + size_central_dir = uL; + + /* offset of start of central directory with respect to the starting disk number */ + offset_central_dir = 0; + if (zip64local_getLong(&pziinit->z_filefunc, pziinit->filestream, &uL)!=ZIP_OK) + err=ZIP_ERRNO; + else + offset_central_dir = uL; + + + /* zipfile global comment length */ + if (zip64local_getShort(&pziinit->z_filefunc, pziinit->filestream, &size_comment)!=ZIP_OK) + err=ZIP_ERRNO; + } + + if ((central_posz_filefunc, pziinit->filestream); + return ZIP_ERRNO; + } + + if (size_comment>0) + { + pziinit->globalcomment = (char*)ALLOC(size_comment+1); + if (pziinit->globalcomment) + { + size_comment = ZREAD64(pziinit->z_filefunc, pziinit->filestream, pziinit->globalcomment,size_comment); + pziinit->globalcomment[size_comment]=0; + } + } + + byte_before_the_zipfile = central_pos - (offset_central_dir+size_central_dir); + pziinit->add_position_when_writting_offset = byte_before_the_zipfile; + + { + ZPOS64_T size_central_dir_to_read = size_central_dir; + size_t buf_size = SIZEDATA_INDATABLOCK; + void* buf_read = (void*)ALLOC(buf_size); + if (ZSEEK64(pziinit->z_filefunc, pziinit->filestream, offset_central_dir + byte_before_the_zipfile, ZLIB_FILEFUNC_SEEK_SET) != 0) + err=ZIP_ERRNO; + + while ((size_central_dir_to_read>0) && (err==ZIP_OK)) + { + ZPOS64_T read_this = SIZEDATA_INDATABLOCK; + if (read_this > size_central_dir_to_read) + read_this = size_central_dir_to_read; + + if (ZREAD64(pziinit->z_filefunc, pziinit->filestream,buf_read,(uLong)read_this) != read_this) + err=ZIP_ERRNO; + + if (err==ZIP_OK) + err = add_data_in_datablock(&pziinit->central_dir,buf_read, (uLong)read_this); + + size_central_dir_to_read-=read_this; + } + TRYFREE(buf_read); + } + pziinit->begin_pos = byte_before_the_zipfile; + pziinit->number_entry = number_entry_CD; + + if (ZSEEK64(pziinit->z_filefunc, pziinit->filestream, offset_central_dir+byte_before_the_zipfile,ZLIB_FILEFUNC_SEEK_SET) != 0) + err=ZIP_ERRNO; + + return err; +} + + +#endif /* !NO_ADDFILEINEXISTINGZIP*/ + + +/************************************************************/ +extern zipFile ZEXPORT zipOpen3 (const void *pathname, int append, zipcharpc* globalcomment, zlib_filefunc64_32_def* pzlib_filefunc64_32_def); +extern zipFile ZEXPORT zipOpen3 (const void *pathname, int append, zipcharpc* globalcomment, zlib_filefunc64_32_def* pzlib_filefunc64_32_def) +{ + zip64_internal ziinit; + zip64_internal* zi; + int err=ZIP_OK; + + ziinit.z_filefunc.zseek32_file = NULL; + ziinit.z_filefunc.ztell32_file = NULL; + if (pzlib_filefunc64_32_def==NULL) + fill_fopen64_filefunc(&ziinit.z_filefunc.zfile_func64); + else + ziinit.z_filefunc = *pzlib_filefunc64_32_def; + + ziinit.filestream = ZOPEN64(ziinit.z_filefunc, + pathname, + (append == APPEND_STATUS_CREATE) ? + (ZLIB_FILEFUNC_MODE_READ | ZLIB_FILEFUNC_MODE_WRITE | ZLIB_FILEFUNC_MODE_CREATE) : + (ZLIB_FILEFUNC_MODE_READ | ZLIB_FILEFUNC_MODE_WRITE | ZLIB_FILEFUNC_MODE_EXISTING)); + + if (ziinit.filestream == NULL) + return NULL; + + if (append == APPEND_STATUS_CREATEAFTER) + ZSEEK64(ziinit.z_filefunc,ziinit.filestream,0,SEEK_END); + + ziinit.begin_pos = ZTELL64(ziinit.z_filefunc,ziinit.filestream); + ziinit.in_opened_file_inzip = 0; + ziinit.ci.stream_initialised = 0; + ziinit.number_entry = 0; + ziinit.add_position_when_writting_offset = 0; + init_linkedlist(&(ziinit.central_dir)); + + + + zi = (zip64_internal*)ALLOC(sizeof(zip64_internal)); + if (zi==NULL) + { + ZCLOSE64(ziinit.z_filefunc,ziinit.filestream); + return NULL; + } + + /* now we add file in a zipfile */ +# ifndef NO_ADDFILEINEXISTINGZIP + ziinit.globalcomment = NULL; + if (append == APPEND_STATUS_ADDINZIP) + { + // Read and Cache Central Directory Records + err = LoadCentralDirectoryRecord(&ziinit); + } + + if (globalcomment) + { + *globalcomment = ziinit.globalcomment; + } +# endif /* !NO_ADDFILEINEXISTINGZIP*/ + + if (err != ZIP_OK) + { +# ifndef NO_ADDFILEINEXISTINGZIP + TRYFREE(ziinit.globalcomment); +# endif /* !NO_ADDFILEINEXISTINGZIP*/ + TRYFREE(zi); + return NULL; + } + else + { + *zi = ziinit; + return (zipFile)zi; + } +} + +extern zipFile ZEXPORT zipOpen2 (const char *pathname, int append, zipcharpc* globalcomment, zlib_filefunc_def* pzlib_filefunc32_def) +{ + if (pzlib_filefunc32_def != NULL) + { + zlib_filefunc64_32_def zlib_filefunc64_32_def_fill; + fill_zlib_filefunc64_32_def_from_filefunc32(&zlib_filefunc64_32_def_fill,pzlib_filefunc32_def); + return zipOpen3(pathname, append, globalcomment, &zlib_filefunc64_32_def_fill); + } + else + return zipOpen3(pathname, append, globalcomment, NULL); +} + +extern zipFile ZEXPORT zipOpen2_64 (const void *pathname, int append, zipcharpc* globalcomment, zlib_filefunc64_def* pzlib_filefunc_def) +{ + if (pzlib_filefunc_def != NULL) + { + zlib_filefunc64_32_def zlib_filefunc64_32_def_fill; + zlib_filefunc64_32_def_fill.zfile_func64 = *pzlib_filefunc_def; + zlib_filefunc64_32_def_fill.ztell32_file = NULL; + zlib_filefunc64_32_def_fill.zseek32_file = NULL; + return zipOpen3(pathname, append, globalcomment, &zlib_filefunc64_32_def_fill); + } + else + return zipOpen3(pathname, append, globalcomment, NULL); +} + + + +extern zipFile ZEXPORT zipOpen (const char* pathname, int append) +{ + return zipOpen3((const void*)pathname,append,NULL,NULL); +} + +extern zipFile ZEXPORT zipOpen64 (const void* pathname, int append) +{ + return zipOpen3(pathname,append,NULL,NULL); +} + +int Write_LocalFileHeader(zip64_internal* zi, const char* filename, uInt size_extrafield_local, const void* extrafield_local); +int Write_LocalFileHeader(zip64_internal* zi, const char* filename, uInt size_extrafield_local, const void* extrafield_local) +{ + /* write the local header */ + int err; + uInt size_filename = (uInt)strlen(filename); + uInt size_extrafield = size_extrafield_local; + + err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)LOCALHEADERMAGIC, 4); + + if (err==ZIP_OK) + { + if(zi->ci.zip64) + err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)45,2);/* version needed to extract */ + else + err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)20,2);/* version needed to extract */ + } + + if (err==ZIP_OK) + err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)zi->ci.flag,2); + + if (err==ZIP_OK) + err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)zi->ci.method,2); + + if (err==ZIP_OK) + err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)zi->ci.dosDate,4); + + // CRC / Compressed size / Uncompressed size will be filled in later and rewritten later + if (err==ZIP_OK) + err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0,4); /* crc 32, unknown */ + if (err==ZIP_OK) + { + if(zi->ci.zip64) + err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0xFFFFFFFF,4); /* compressed size, unknown */ + else + err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0,4); /* compressed size, unknown */ + } + if (err==ZIP_OK) + { + if(zi->ci.zip64) + err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0xFFFFFFFF,4); /* uncompressed size, unknown */ + else + err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0,4); /* uncompressed size, unknown */ + } + + if (err==ZIP_OK) + err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)size_filename,2); + + if(zi->ci.zip64) + { + size_extrafield += 20; + } + + if (err==ZIP_OK) + err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)size_extrafield,2); + + if ((err==ZIP_OK) && (size_filename > 0)) + { + if (ZWRITE64(zi->z_filefunc,zi->filestream,filename,size_filename)!=size_filename) + err = ZIP_ERRNO; + } + + if ((err==ZIP_OK) && (size_extrafield_local > 0)) + { + if (ZWRITE64(zi->z_filefunc, zi->filestream, extrafield_local, size_extrafield_local) != size_extrafield_local) + err = ZIP_ERRNO; + } + + + if ((err==ZIP_OK) && (zi->ci.zip64)) + { + // write the Zip64 extended info + short HeaderID = 1; + short DataSize = 16; + ZPOS64_T CompressedSize = 0; + ZPOS64_T UncompressedSize = 0; + + // Remember position of Zip64 extended info for the local file header. (needed when we update size after done with file) + zi->ci.pos_zip64extrainfo = ZTELL64(zi->z_filefunc,zi->filestream); + +#ifndef __clang_analyzer__ + err = zip64local_putValue(&zi->z_filefunc, zi->filestream, (short)HeaderID,2); + err = zip64local_putValue(&zi->z_filefunc, zi->filestream, (short)DataSize,2); + + err = zip64local_putValue(&zi->z_filefunc, zi->filestream, (ZPOS64_T)UncompressedSize,8); + err = zip64local_putValue(&zi->z_filefunc, zi->filestream, (ZPOS64_T)CompressedSize,8); +#endif + } + + return err; +} + +/* + NOTE. + When writing RAW the ZIP64 extended information in extrafield_local and extrafield_global needs to be stripped + before calling this function it can be done with zipRemoveExtraInfoBlock + + It is not done here because then we need to realloc a new buffer since parameters are 'const' and I want to minimize + unnecessary allocations. + */ +extern int ZEXPORT zipOpenNewFileInZip4_64 (zipFile file, const char* filename, const zip_fileinfo* zipfi, + const void* extrafield_local, uInt size_extrafield_local, + const void* extrafield_global, uInt size_extrafield_global, + const char* comment, int method, int level, int raw, + int windowBits,int memLevel, int strategy, + const char* password, uLong crcForCrypting, + uLong versionMadeBy, uLong flagBase, int zip64) +{ + zip64_internal* zi; + uInt size_filename; + uInt size_comment; + uInt i; + int err = ZIP_OK; + +# ifdef NOCRYPT + if (password != NULL) + return ZIP_PARAMERROR; +# endif + + if (file == NULL) + return ZIP_PARAMERROR; + +#ifdef HAVE_BZIP2 + if ((method!=0) && (method!=Z_DEFLATED) && (method!=Z_BZIP2ED)) + return ZIP_PARAMERROR; +#else + if ((method!=0) && (method!=Z_DEFLATED)) + return ZIP_PARAMERROR; +#endif + + zi = (zip64_internal*)file; + + if (zi->in_opened_file_inzip == 1) + { + err = zipCloseFileInZip (file); + if (err != ZIP_OK) + return err; + } + + if (filename==NULL) + filename="-"; + + if (comment==NULL) + size_comment = 0; + else + size_comment = (uInt)strlen(comment); + + size_filename = (uInt)strlen(filename); + + if (zipfi == NULL) + zi->ci.dosDate = 0; + else + { + if (zipfi->dosDate != 0) + zi->ci.dosDate = zipfi->dosDate; + else + zi->ci.dosDate = zip64local_TmzDateToDosDate(&zipfi->tmz_date); + } + + zi->ci.flag = flagBase; + if (level==8 || level==9) + zi->ci.flag |= 2; + if (level==2) + zi->ci.flag |= 4; + if (level==1) + zi->ci.flag |= 6; + if (password != NULL) + zi->ci.flag |= 1; + + zi->ci.crc32 = 0; + zi->ci.method = method; + zi->ci.encrypt = 0; + zi->ci.stream_initialised = 0; + zi->ci.pos_in_buffered_data = 0; + zi->ci.raw = raw; + zi->ci.pos_local_header = ZTELL64(zi->z_filefunc,zi->filestream); + + zi->ci.size_centralheader = SIZECENTRALHEADER + size_filename + size_extrafield_global + size_comment; + zi->ci.size_centralExtraFree = 32; // Extra space we have reserved in case we need to add ZIP64 extra info data + + zi->ci.central_header = (char*)ALLOC((uInt)zi->ci.size_centralheader + zi->ci.size_centralExtraFree); + + zi->ci.size_centralExtra = size_extrafield_global; + zip64local_putValue_inmemory(zi->ci.central_header,(uLong)CENTRALHEADERMAGIC,4); + /* version info */ + zip64local_putValue_inmemory(zi->ci.central_header+4,(uLong)versionMadeBy,2); + zip64local_putValue_inmemory(zi->ci.central_header+6,(uLong)20,2); + zip64local_putValue_inmemory(zi->ci.central_header+8,(uLong)zi->ci.flag,2); + zip64local_putValue_inmemory(zi->ci.central_header+10,(uLong)zi->ci.method,2); + zip64local_putValue_inmemory(zi->ci.central_header+12,(uLong)zi->ci.dosDate,4); + zip64local_putValue_inmemory(zi->ci.central_header+16,(uLong)0,4); /*crc*/ + zip64local_putValue_inmemory(zi->ci.central_header+20,(uLong)0,4); /*compr size*/ + zip64local_putValue_inmemory(zi->ci.central_header+24,(uLong)0,4); /*uncompr size*/ + zip64local_putValue_inmemory(zi->ci.central_header+28,(uLong)size_filename,2); + zip64local_putValue_inmemory(zi->ci.central_header+30,(uLong)size_extrafield_global,2); + zip64local_putValue_inmemory(zi->ci.central_header+32,(uLong)size_comment,2); + zip64local_putValue_inmemory(zi->ci.central_header+34,(uLong)0,2); /*disk nm start*/ + + if (zipfi==NULL) + zip64local_putValue_inmemory(zi->ci.central_header+36,(uLong)0,2); + else + zip64local_putValue_inmemory(zi->ci.central_header+36,(uLong)zipfi->internal_fa,2); + + if (zipfi==NULL) + zip64local_putValue_inmemory(zi->ci.central_header+38,(uLong)0,4); + else + zip64local_putValue_inmemory(zi->ci.central_header+38,(uLong)zipfi->external_fa,4); + + if(zi->ci.pos_local_header >= 0xffffffff) + zip64local_putValue_inmemory(zi->ci.central_header+42,(uLong)0xffffffff,4); + else + zip64local_putValue_inmemory(zi->ci.central_header+42,(uLong)zi->ci.pos_local_header - zi->add_position_when_writting_offset,4); + + for (i=0;ici.central_header+SIZECENTRALHEADER+i) = *(filename+i); + + for (i=0;ici.central_header+SIZECENTRALHEADER+size_filename+i) = + *(((const char*)extrafield_global)+i); + + for (i=0;ici.central_header+SIZECENTRALHEADER+size_filename+ + size_extrafield_global+i) = *(comment+i); + if (zi->ci.central_header == NULL) + return ZIP_INTERNALERROR; + + zi->ci.zip64 = zip64; + zi->ci.totalCompressedData = 0; + zi->ci.totalUncompressedData = 0; + zi->ci.pos_zip64extrainfo = 0; + + err = Write_LocalFileHeader(zi, filename, size_extrafield_local, extrafield_local); + +#ifdef HAVE_BZIP2 + zi->ci.bstream.avail_in = (uInt)0; + zi->ci.bstream.avail_out = (uInt)Z_BUFSIZE; + zi->ci.bstream.next_out = (char*)zi->ci.buffered_data; + zi->ci.bstream.total_in_hi32 = 0; + zi->ci.bstream.total_in_lo32 = 0; + zi->ci.bstream.total_out_hi32 = 0; + zi->ci.bstream.total_out_lo32 = 0; +#endif + + zi->ci.stream.avail_in = (uInt)0; + zi->ci.stream.avail_out = (uInt)Z_BUFSIZE; + zi->ci.stream.next_out = zi->ci.buffered_data; + zi->ci.stream.total_in = 0; + zi->ci.stream.total_out = 0; + zi->ci.stream.data_type = Z_BINARY; + +#ifdef HAVE_BZIP2 + if ((err==ZIP_OK) && (zi->ci.method == Z_DEFLATED || zi->ci.method == Z_BZIP2ED) && (!zi->ci.raw)) +#else + if ((err==ZIP_OK) && (zi->ci.method == Z_DEFLATED) && (!zi->ci.raw)) +#endif + { + if(zi->ci.method == Z_DEFLATED) + { + zi->ci.stream.zalloc = (alloc_func)0; + zi->ci.stream.zfree = (free_func)0; + zi->ci.stream.opaque = (voidpf)0; + + if (windowBits>0) + windowBits = -windowBits; + + err = deflateInit2(&zi->ci.stream, level, Z_DEFLATED, windowBits, memLevel, strategy); + + if (err==Z_OK) + zi->ci.stream_initialised = Z_DEFLATED; + } + else if(zi->ci.method == Z_BZIP2ED) + { +#ifdef HAVE_BZIP2 + // Init BZip stuff here + zi->ci.bstream.bzalloc = 0; + zi->ci.bstream.bzfree = 0; + zi->ci.bstream.opaque = (voidpf)0; + + err = BZ2_bzCompressInit(&zi->ci.bstream, level, 0,35); + if(err == BZ_OK) + zi->ci.stream_initialised = Z_BZIP2ED; +#endif + } + + } + +# ifndef NOCRYPT + zi->ci.crypt_header_size = 0; + if ((err==Z_OK) && (password != NULL)) + { + unsigned char bufHead[RAND_HEAD_LEN]; + unsigned int sizeHead; + zi->ci.encrypt = 1; + zi->ci.pcrc_32_tab = (const unsigned long*)get_crc_table(); + /*init_keys(password,zi->ci.keys,zi->ci.pcrc_32_tab);*/ + + sizeHead=crypthead(password,bufHead,RAND_HEAD_LEN,zi->ci.keys,zi->ci.pcrc_32_tab,crcForCrypting); + zi->ci.crypt_header_size = sizeHead; + + if (ZWRITE64(zi->z_filefunc,zi->filestream,bufHead,sizeHead) != sizeHead) + err = ZIP_ERRNO; + } +# endif + + if (err==Z_OK) + zi->in_opened_file_inzip = 1; + return err; +} + +extern int ZEXPORT zipOpenNewFileInZip4 (zipFile file, const char* filename, const zip_fileinfo* zipfi, + const void* extrafield_local, uInt size_extrafield_local, + const void* extrafield_global, uInt size_extrafield_global, + const char* comment, int method, int level, int raw, + int windowBits,int memLevel, int strategy, + const char* password, uLong crcForCrypting, + uLong versionMadeBy, uLong flagBase) +{ + return zipOpenNewFileInZip4_64 (file, filename, zipfi, + extrafield_local, size_extrafield_local, + extrafield_global, size_extrafield_global, + comment, method, level, raw, + windowBits, memLevel, strategy, + password, crcForCrypting, versionMadeBy, flagBase, 0); +} + +extern int ZEXPORT zipOpenNewFileInZip3 (zipFile file, const char* filename, const zip_fileinfo* zipfi, + const void* extrafield_local, uInt size_extrafield_local, + const void* extrafield_global, uInt size_extrafield_global, + const char* comment, int method, int level, int raw, + int windowBits,int memLevel, int strategy, + const char* password, uLong crcForCrypting) +{ + return zipOpenNewFileInZip4_64 (file, filename, zipfi, + extrafield_local, size_extrafield_local, + extrafield_global, size_extrafield_global, + comment, method, level, raw, + windowBits, memLevel, strategy, + password, crcForCrypting, VERSIONMADEBY, 0, 0); +} + +extern int ZEXPORT zipOpenNewFileInZip3_64(zipFile file, const char* filename, const zip_fileinfo* zipfi, + const void* extrafield_local, uInt size_extrafield_local, + const void* extrafield_global, uInt size_extrafield_global, + const char* comment, int method, int level, int raw, + int windowBits,int memLevel, int strategy, + const char* password, uLong crcForCrypting, int zip64) +{ + return zipOpenNewFileInZip4_64 (file, filename, zipfi, + extrafield_local, size_extrafield_local, + extrafield_global, size_extrafield_global, + comment, method, level, raw, + windowBits, memLevel, strategy, + password, crcForCrypting, VERSIONMADEBY, 0, zip64); +} + +extern int ZEXPORT zipOpenNewFileInZip2(zipFile file, const char* filename, const zip_fileinfo* zipfi, + const void* extrafield_local, uInt size_extrafield_local, + const void* extrafield_global, uInt size_extrafield_global, + const char* comment, int method, int level, int raw) +{ + return zipOpenNewFileInZip4_64 (file, filename, zipfi, + extrafield_local, size_extrafield_local, + extrafield_global, size_extrafield_global, + comment, method, level, raw, + -MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY, + NULL, 0, VERSIONMADEBY, 0, 0); +} + +extern int ZEXPORT zipOpenNewFileInZip2_64(zipFile file, const char* filename, const zip_fileinfo* zipfi, + const void* extrafield_local, uInt size_extrafield_local, + const void* extrafield_global, uInt size_extrafield_global, + const char* comment, int method, int level, int raw, int zip64) +{ + return zipOpenNewFileInZip4_64 (file, filename, zipfi, + extrafield_local, size_extrafield_local, + extrafield_global, size_extrafield_global, + comment, method, level, raw, + -MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY, + NULL, 0, VERSIONMADEBY, 0, zip64); +} + +extern int ZEXPORT zipOpenNewFileInZip64 (zipFile file, const char* filename, const zip_fileinfo* zipfi, + const void* extrafield_local, uInt size_extrafield_local, + const void*extrafield_global, uInt size_extrafield_global, + const char* comment, int method, int level, int zip64) +{ + return zipOpenNewFileInZip4_64 (file, filename, zipfi, + extrafield_local, size_extrafield_local, + extrafield_global, size_extrafield_global, + comment, method, level, 0, + -MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY, + NULL, 0, VERSIONMADEBY, 0, zip64); +} + +extern int ZEXPORT zipOpenNewFileInZip (zipFile file, const char* filename, const zip_fileinfo* zipfi, + const void* extrafield_local, uInt size_extrafield_local, + const void*extrafield_global, uInt size_extrafield_global, + const char* comment, int method, int level) +{ + return zipOpenNewFileInZip4_64 (file, filename, zipfi, + extrafield_local, size_extrafield_local, + extrafield_global, size_extrafield_global, + comment, method, level, 0, + -MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY, + NULL, 0, VERSIONMADEBY, 0, 0); +} + +local int zip64FlushWriteBuffer(zip64_internal* zi) +{ + int err=ZIP_OK; + + if (zi->ci.encrypt != 0) + { +#ifndef NOCRYPT + uInt i; + int t; + for (i=0;ici.pos_in_buffered_data;i++) + zi->ci.buffered_data[i] = zencode(zi->ci.keys, zi->ci.pcrc_32_tab, zi->ci.buffered_data[i],t); +#endif + } + + if (ZWRITE64(zi->z_filefunc,zi->filestream,zi->ci.buffered_data,zi->ci.pos_in_buffered_data) != zi->ci.pos_in_buffered_data) + err = ZIP_ERRNO; + + zi->ci.totalCompressedData += zi->ci.pos_in_buffered_data; + +#ifdef HAVE_BZIP2 + if(zi->ci.method == Z_BZIP2ED) + { + zi->ci.totalUncompressedData += zi->ci.bstream.total_in_lo32; + zi->ci.bstream.total_in_lo32 = 0; + zi->ci.bstream.total_in_hi32 = 0; + } + else +#endif + { + zi->ci.totalUncompressedData += zi->ci.stream.total_in; + zi->ci.stream.total_in = 0; + } + + + zi->ci.pos_in_buffered_data = 0; + + return err; +} + +extern int ZEXPORT zipWriteInFileInZip (zipFile file,const void* buf,unsigned int len) +{ + zip64_internal* zi; + int err=ZIP_OK; + + if (file == NULL) + return ZIP_PARAMERROR; + zi = (zip64_internal*)file; + + if (zi->in_opened_file_inzip == 0) + return ZIP_PARAMERROR; + + zi->ci.crc32 = crc32(zi->ci.crc32,buf,(uInt)len); + +#ifdef HAVE_BZIP2 + if(zi->ci.method == Z_BZIP2ED && (!zi->ci.raw)) + { + zi->ci.bstream.next_in = (void*)buf; + zi->ci.bstream.avail_in = len; + err = BZ_RUN_OK; + + while ((err==BZ_RUN_OK) && (zi->ci.bstream.avail_in>0)) + { + if (zi->ci.bstream.avail_out == 0) + { + if (zip64FlushWriteBuffer(zi) == ZIP_ERRNO) + err = ZIP_ERRNO; + zi->ci.bstream.avail_out = (uInt)Z_BUFSIZE; + zi->ci.bstream.next_out = (char*)zi->ci.buffered_data; + } + + + if(err != BZ_RUN_OK) + break; + + if ((zi->ci.method == Z_BZIP2ED) && (!zi->ci.raw)) + { + uLong uTotalOutBefore_lo = zi->ci.bstream.total_out_lo32; +// uLong uTotalOutBefore_hi = zi->ci.bstream.total_out_hi32; + err=BZ2_bzCompress(&zi->ci.bstream, BZ_RUN); + + zi->ci.pos_in_buffered_data += (uInt)(zi->ci.bstream.total_out_lo32 - uTotalOutBefore_lo) ; + } + } + + if(err == BZ_RUN_OK) + err = ZIP_OK; + } + else +#endif + { + zi->ci.stream.next_in = (Bytef*)buf; + zi->ci.stream.avail_in = len; + + while ((err==ZIP_OK) && (zi->ci.stream.avail_in>0)) + { + if (zi->ci.stream.avail_out == 0) + { + if (zip64FlushWriteBuffer(zi) == ZIP_ERRNO) + err = ZIP_ERRNO; + zi->ci.stream.avail_out = (uInt)Z_BUFSIZE; + zi->ci.stream.next_out = zi->ci.buffered_data; + } + + + if(err != ZIP_OK) + break; + + if ((zi->ci.method == Z_DEFLATED) && (!zi->ci.raw)) + { + uLong uTotalOutBefore = zi->ci.stream.total_out; + err=deflate(&zi->ci.stream, Z_NO_FLUSH); + if(uTotalOutBefore > zi->ci.stream.total_out) + { + int bBreak = 0; + bBreak++; + } + + zi->ci.pos_in_buffered_data += (uInt)(zi->ci.stream.total_out - uTotalOutBefore) ; + } + else + { + uInt copy_this,i; + if (zi->ci.stream.avail_in < zi->ci.stream.avail_out) + copy_this = zi->ci.stream.avail_in; + else + copy_this = zi->ci.stream.avail_out; + + for (i = 0; i < copy_this; i++) + *(((char*)zi->ci.stream.next_out)+i) = + *(((const char*)zi->ci.stream.next_in)+i); + { + zi->ci.stream.avail_in -= copy_this; + zi->ci.stream.avail_out-= copy_this; + zi->ci.stream.next_in+= copy_this; + zi->ci.stream.next_out+= copy_this; + zi->ci.stream.total_in+= copy_this; + zi->ci.stream.total_out+= copy_this; + zi->ci.pos_in_buffered_data += copy_this; + } + } + }// while(...) + } + + return err; +} + +extern int ZEXPORT zipCloseFileInZipRaw (zipFile file, uLong uncompressed_size, uLong crc32) +{ + return zipCloseFileInZipRaw64 (file, uncompressed_size, crc32); +} + +extern int ZEXPORT zipCloseFileInZipRaw64 (zipFile file, ZPOS64_T uncompressed_size, uLong crc32) +{ + zip64_internal* zi; + ZPOS64_T compressed_size; + uLong invalidValue = 0xffffffff; + short datasize = 0; + int err=ZIP_OK; + + if (file == NULL) + return ZIP_PARAMERROR; + zi = (zip64_internal*)file; + + if (zi->in_opened_file_inzip == 0) + return ZIP_PARAMERROR; + zi->ci.stream.avail_in = 0; + + if ((zi->ci.method == Z_DEFLATED) && (!zi->ci.raw)) + { + while (err==ZIP_OK) + { + uLong uTotalOutBefore; + if (zi->ci.stream.avail_out == 0) + { + if (zip64FlushWriteBuffer(zi) == ZIP_ERRNO) + { +#ifndef __clang_analyzer__ + err = ZIP_ERRNO; +#endif + } + zi->ci.stream.avail_out = (uInt)Z_BUFSIZE; +#ifndef __clang_analyzer__ + zi->ci.stream.next_out = zi->ci.buffered_data; +#endif + } + uTotalOutBefore = zi->ci.stream.total_out; + err=deflate(&zi->ci.stream, Z_FINISH); + zi->ci.pos_in_buffered_data += (uInt)(zi->ci.stream.total_out - uTotalOutBefore) ; + } + } + else if ((zi->ci.method == Z_BZIP2ED) && (!zi->ci.raw)) + { +#ifdef HAVE_BZIP2 + err = BZ_FINISH_OK; + while (err==BZ_FINISH_OK) + { + uLong uTotalOutBefore; + if (zi->ci.bstream.avail_out == 0) + { + if (zip64FlushWriteBuffer(zi) == ZIP_ERRNO) + err = ZIP_ERRNO; + zi->ci.bstream.avail_out = (uInt)Z_BUFSIZE; + zi->ci.bstream.next_out = (char*)zi->ci.buffered_data; + } + uTotalOutBefore = zi->ci.bstream.total_out_lo32; + err=BZ2_bzCompress(&zi->ci.bstream, BZ_FINISH); + if(err == BZ_STREAM_END) + err = Z_STREAM_END; + + zi->ci.pos_in_buffered_data += (uInt)(zi->ci.bstream.total_out_lo32 - uTotalOutBefore); + } + + if(err == BZ_FINISH_OK) + err = ZIP_OK; +#endif + } + + if (err==Z_STREAM_END) + err=ZIP_OK; /* this is normal */ + + if ((zi->ci.pos_in_buffered_data>0) && (err==ZIP_OK)) + { + if (zip64FlushWriteBuffer(zi)==ZIP_ERRNO) + err = ZIP_ERRNO; + } + + if ((zi->ci.method == Z_DEFLATED) && (!zi->ci.raw)) + { + int tmp_err = deflateEnd(&zi->ci.stream); + if (err == ZIP_OK) + err = tmp_err; + zi->ci.stream_initialised = 0; + } +#ifdef HAVE_BZIP2 + else if((zi->ci.method == Z_BZIP2ED) && (!zi->ci.raw)) + { + int tmperr = BZ2_bzCompressEnd(&zi->ci.bstream); + if (err==ZIP_OK) + err = tmperr; + zi->ci.stream_initialised = 0; + } +#endif + + if (!zi->ci.raw) + { + crc32 = (uLong)zi->ci.crc32; + uncompressed_size = zi->ci.totalUncompressedData; + } + compressed_size = zi->ci.totalCompressedData; + +# ifndef NOCRYPT + compressed_size += zi->ci.crypt_header_size; +# endif + + // update Current Item crc and sizes, + if(compressed_size >= 0xffffffff || uncompressed_size >= 0xffffffff || zi->ci.pos_local_header >= 0xffffffff) + { + /*version Made by*/ + zip64local_putValue_inmemory(zi->ci.central_header+4,(uLong)45,2); + /*version needed*/ + zip64local_putValue_inmemory(zi->ci.central_header+6,(uLong)45,2); + + } + + zip64local_putValue_inmemory(zi->ci.central_header+16,crc32,4); /*crc*/ + + + if(compressed_size >= 0xffffffff) + zip64local_putValue_inmemory(zi->ci.central_header+20, invalidValue,4); /*compr size*/ + else + zip64local_putValue_inmemory(zi->ci.central_header+20, compressed_size,4); /*compr size*/ + + /// set internal file attributes field + if (zi->ci.stream.data_type == Z_ASCII) + zip64local_putValue_inmemory(zi->ci.central_header+36,(uLong)Z_ASCII,2); + + if(uncompressed_size >= 0xffffffff) + zip64local_putValue_inmemory(zi->ci.central_header+24, invalidValue,4); /*uncompr size*/ + else + zip64local_putValue_inmemory(zi->ci.central_header+24, uncompressed_size,4); /*uncompr size*/ + + // Add ZIP64 extra info field for uncompressed size + if(uncompressed_size >= 0xffffffff) + datasize += 8; + + // Add ZIP64 extra info field for compressed size + if(compressed_size >= 0xffffffff) + datasize += 8; + + // Add ZIP64 extra info field for relative offset to local file header of current file + if(zi->ci.pos_local_header >= 0xffffffff) + datasize += 8; + + if(datasize > 0) + { + char* p = NULL; + + if((uLong)(datasize + 4) > zi->ci.size_centralExtraFree) + { + // we can not write more data to the buffer that we have room for. + return ZIP_BADZIPFILE; + } + + p = zi->ci.central_header + zi->ci.size_centralheader; + + // Add Extra Information Header for 'ZIP64 information' + zip64local_putValue_inmemory(p, 0x0001, 2); // HeaderID + p += 2; + zip64local_putValue_inmemory(p, datasize, 2); // DataSize + p += 2; + + if(uncompressed_size >= 0xffffffff) + { + zip64local_putValue_inmemory(p, uncompressed_size, 8); + p += 8; + } + + if(compressed_size >= 0xffffffff) + { + zip64local_putValue_inmemory(p, compressed_size, 8); + p += 8; + } + + if(zi->ci.pos_local_header >= 0xffffffff) + { + zip64local_putValue_inmemory(p, zi->ci.pos_local_header, 8); +#ifndef __clang_analyzer__ + p += 8; +#endif + } + + // Update how much extra free space we got in the memory buffer + // and increase the centralheader size so the new ZIP64 fields are included + // ( 4 below is the size of HeaderID and DataSize field ) + zi->ci.size_centralExtraFree -= datasize + 4; + zi->ci.size_centralheader += datasize + 4; + + // Update the extra info size field + zi->ci.size_centralExtra += datasize + 4; + zip64local_putValue_inmemory(zi->ci.central_header+30,(uLong)zi->ci.size_centralExtra,2); + } + + if (err==ZIP_OK) + err = add_data_in_datablock(&zi->central_dir, zi->ci.central_header, (uLong)zi->ci.size_centralheader); + + free(zi->ci.central_header); + + if (err==ZIP_OK) + { + // Update the LocalFileHeader with the new values. + + ZPOS64_T cur_pos_inzip = ZTELL64(zi->z_filefunc,zi->filestream); + + if (ZSEEK64(zi->z_filefunc,zi->filestream, zi->ci.pos_local_header + 14,ZLIB_FILEFUNC_SEEK_SET)!=0) + err = ZIP_ERRNO; + + if (err==ZIP_OK) + err = zip64local_putValue(&zi->z_filefunc,zi->filestream,crc32,4); /* crc 32, unknown */ + + if(uncompressed_size >= 0xffffffff) + { + if(zi->ci.pos_zip64extrainfo > 0) + { + // Update the size in the ZIP64 extended field. + if (ZSEEK64(zi->z_filefunc,zi->filestream, zi->ci.pos_zip64extrainfo + 4,ZLIB_FILEFUNC_SEEK_SET)!=0) + err = ZIP_ERRNO; + + if (err==ZIP_OK) /* compressed size, unknown */ + err = zip64local_putValue(&zi->z_filefunc, zi->filestream, uncompressed_size, 8); + + if (err==ZIP_OK) /* uncompressed size, unknown */ + err = zip64local_putValue(&zi->z_filefunc, zi->filestream, compressed_size, 8); + } + } + else + { + if (err==ZIP_OK) /* compressed size, unknown */ + err = zip64local_putValue(&zi->z_filefunc,zi->filestream,compressed_size,4); + + if (err==ZIP_OK) /* uncompressed size, unknown */ + err = zip64local_putValue(&zi->z_filefunc,zi->filestream,uncompressed_size,4); + } + + if (ZSEEK64(zi->z_filefunc,zi->filestream, cur_pos_inzip,ZLIB_FILEFUNC_SEEK_SET)!=0) + err = ZIP_ERRNO; + } + + zi->number_entry ++; + zi->in_opened_file_inzip = 0; + + return err; +} + +extern int ZEXPORT zipCloseFileInZip (zipFile file) +{ + return zipCloseFileInZipRaw (file,0,0); +} + +int Write_Zip64EndOfCentralDirectoryLocator(zip64_internal* zi, ZPOS64_T zip64eocd_pos_inzip); +int Write_Zip64EndOfCentralDirectoryLocator(zip64_internal* zi, ZPOS64_T zip64eocd_pos_inzip) +{ + int err = ZIP_OK; + ZPOS64_T pos = zip64eocd_pos_inzip - zi->add_position_when_writting_offset; + + err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)ZIP64ENDLOCHEADERMAGIC,4); + + /*num disks*/ + if (err==ZIP_OK) /* number of the disk with the start of the central directory */ + err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0,4); + + /*relative offset*/ + if (err==ZIP_OK) /* Relative offset to the Zip64EndOfCentralDirectory */ + err = zip64local_putValue(&zi->z_filefunc,zi->filestream, pos,8); + + /*total disks*/ /* Do not support spawning of disk so always say 1 here*/ + if (err==ZIP_OK) /* number of the disk with the start of the central directory */ + err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)1,4); + + return err; +} + +int Write_Zip64EndOfCentralDirectoryRecord(zip64_internal* zi, uLong size_centraldir, ZPOS64_T centraldir_pos_inzip); +int Write_Zip64EndOfCentralDirectoryRecord(zip64_internal* zi, uLong size_centraldir, ZPOS64_T centraldir_pos_inzip) +{ + int err = ZIP_OK; + + uLong Zip64DataSize = 44; + + err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)ZIP64ENDHEADERMAGIC,4); + + if (err==ZIP_OK) /* size of this 'zip64 end of central directory' */ + err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(ZPOS64_T)Zip64DataSize,8); // why ZPOS64_T of this ? + + if (err==ZIP_OK) /* version made by */ + err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)45,2); + + if (err==ZIP_OK) /* version needed */ + err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)45,2); + + if (err==ZIP_OK) /* number of this disk */ + err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0,4); + + if (err==ZIP_OK) /* number of the disk with the start of the central directory */ + err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0,4); + + if (err==ZIP_OK) /* total number of entries in the central dir on this disk */ + err = zip64local_putValue(&zi->z_filefunc, zi->filestream, zi->number_entry, 8); + + if (err==ZIP_OK) /* total number of entries in the central dir */ + err = zip64local_putValue(&zi->z_filefunc, zi->filestream, zi->number_entry, 8); + + if (err==ZIP_OK) /* size of the central directory */ + err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(ZPOS64_T)size_centraldir,8); + + if (err==ZIP_OK) /* offset of start of central directory with respect to the starting disk number */ + { + ZPOS64_T pos = centraldir_pos_inzip - zi->add_position_when_writting_offset; + err = zip64local_putValue(&zi->z_filefunc,zi->filestream, (ZPOS64_T)pos,8); + } + return err; +} + +int Write_EndOfCentralDirectoryRecord(zip64_internal* zi, uLong size_centraldir, ZPOS64_T centraldir_pos_inzip); +int Write_EndOfCentralDirectoryRecord(zip64_internal* zi, uLong size_centraldir, ZPOS64_T centraldir_pos_inzip) +{ + int err = ZIP_OK; + + /*signature*/ + err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)ENDHEADERMAGIC,4); + + if (err==ZIP_OK) /* number of this disk */ + err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0,2); + + if (err==ZIP_OK) /* number of the disk with the start of the central directory */ + err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0,2); + + if (err==ZIP_OK) /* total number of entries in the central dir on this disk */ + { + { + if(zi->number_entry >= 0xFFFF) + err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0xffff,2); // use value in ZIP64 record + else + err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)zi->number_entry,2); + } + } + + if (err==ZIP_OK) /* total number of entries in the central dir */ + { + if(zi->number_entry >= 0xFFFF) + err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0xffff,2); // use value in ZIP64 record + else + err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)zi->number_entry,2); + } + + if (err==ZIP_OK) /* size of the central directory */ + err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)size_centraldir,4); + + if (err==ZIP_OK) /* offset of start of central directory with respect to the starting disk number */ + { + ZPOS64_T pos = centraldir_pos_inzip - zi->add_position_when_writting_offset; + if(pos >= 0xffffffff) + { + err = zip64local_putValue(&zi->z_filefunc,zi->filestream, (uLong)0xffffffff,4); + } + else + err = zip64local_putValue(&zi->z_filefunc,zi->filestream, (uLong)(centraldir_pos_inzip - zi->add_position_when_writting_offset),4); + } + + return err; +} + +int Write_GlobalComment(zip64_internal* zi, const char* global_comment); +int Write_GlobalComment(zip64_internal* zi, const char* global_comment) +{ + int err = ZIP_OK; + uInt size_global_comment = 0; + + if(global_comment != NULL) + size_global_comment = (uInt)strlen(global_comment); + + err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)size_global_comment,2); + + if (err == ZIP_OK && size_global_comment > 0) + { + if (ZWRITE64(zi->z_filefunc,zi->filestream, global_comment, size_global_comment) != size_global_comment) + err = ZIP_ERRNO; + } + return err; +} + +extern int ZEXPORT zipClose (zipFile file, const char* global_comment) +{ + zip64_internal* zi; + int err = 0; + uLong size_centraldir = 0; + ZPOS64_T centraldir_pos_inzip; + ZPOS64_T pos; + + if (file == NULL) + return ZIP_PARAMERROR; + + zi = (zip64_internal*)file; + + if (zi->in_opened_file_inzip == 1) + { + err = zipCloseFileInZip (file); + } + +#ifndef NO_ADDFILEINEXISTINGZIP + if (global_comment==NULL) + global_comment = zi->globalcomment; +#endif + + centraldir_pos_inzip = ZTELL64(zi->z_filefunc,zi->filestream); + + if (err==ZIP_OK) + { + linkedlist_datablock_internal* ldi = zi->central_dir.first_block; + while (ldi!=NULL) + { + if ((err==ZIP_OK) && (ldi->filled_in_this_block>0)) + { + if (ZWRITE64(zi->z_filefunc,zi->filestream, ldi->data, ldi->filled_in_this_block) != ldi->filled_in_this_block) + err = ZIP_ERRNO; + } + + size_centraldir += ldi->filled_in_this_block; + ldi = ldi->next_datablock; + } + } + free_linkedlist(&(zi->central_dir)); + + pos = centraldir_pos_inzip - zi->add_position_when_writting_offset; + if(pos >= 0xffffffff) + { + ZPOS64_T Zip64EOCDpos = ZTELL64(zi->z_filefunc,zi->filestream); + Write_Zip64EndOfCentralDirectoryRecord(zi, size_centraldir, centraldir_pos_inzip); + + Write_Zip64EndOfCentralDirectoryLocator(zi, Zip64EOCDpos); + } + + if (err==ZIP_OK) + err = Write_EndOfCentralDirectoryRecord(zi, size_centraldir, centraldir_pos_inzip); + + if(err == ZIP_OK) + err = Write_GlobalComment(zi, global_comment); + + if (ZCLOSE64(zi->z_filefunc,zi->filestream) != 0) + if (err == ZIP_OK) + err = ZIP_ERRNO; + +#ifndef NO_ADDFILEINEXISTINGZIP + TRYFREE(zi->globalcomment); +#endif + TRYFREE(zi); + + return err; +} + +extern int ZEXPORT zipRemoveExtraInfoBlock (char* pData, int* dataLen, short sHeader) +{ + char* p = pData; + int size = 0; + char* pNewHeader; + char* pTmp; + short header; + short dataSize; + + int retVal = ZIP_OK; + + if(pData == NULL || *dataLen < 4) + return ZIP_PARAMERROR; + + pNewHeader = (char*)ALLOC(*dataLen); + pTmp = pNewHeader; + + while(p < (pData + *dataLen)) + { + header = *(short*)p; + dataSize = *(((short*)p)+1); + + if( header == sHeader ) // Header found. + { + p += dataSize + 4; // skip it. do not copy to temp buffer + } + else + { + // Extra Info block should not be removed, So copy it to the temp buffer. + memcpy(pTmp, p, dataSize + 4); + p += dataSize + 4; + size += dataSize + 4; + } + + } + + if(size < *dataLen) + { + // clean old extra info block. + memset(pData,0, *dataLen); + + // copy the new extra info block over the old + if(size > 0) + memcpy(pData, pNewHeader, size); + + // set the new extra info size + *dataLen = size; + + retVal = ZIP_OK; + } + else + retVal = ZIP_ERRNO; + + TRYFREE(pNewHeader); + + return retVal; +} diff --git a/BakerShelf/lib/minizip/zip.h b/BakerShelf/lib/minizip/zip.h new file mode 100755 index 0000000..eec1082 --- /dev/null +++ b/BakerShelf/lib/minizip/zip.h @@ -0,0 +1,362 @@ +/* zip.h -- IO on .zip files using zlib + Version 1.1, February 14h, 2010 + part of the MiniZip project - ( http://www.winimage.com/zLibDll/minizip.html ) + + Copyright (C) 1998-2010 Gilles Vollant (minizip) ( http://www.winimage.com/zLibDll/minizip.html ) + + Modifications for Zip64 support + Copyright (C) 2009-2010 Mathias Svensson ( http://result42.com ) + + For more info read MiniZip_info.txt + + --------------------------------------------------------------------------- + + Condition of use and distribution are the same than zlib : + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + --------------------------------------------------------------------------- + + Changes + + See header of zip.h + +*/ + +#ifndef _zip12_H +#define _zip12_H + +#ifdef __cplusplus +extern "C" { +#endif + +//#define HAVE_BZIP2 + +#ifndef _ZLIB_H +#include "zlib.h" +#endif + +#ifndef _ZLIBIOAPI_H +#include "ioapi.h" +#endif + +#ifdef HAVE_BZIP2 +#include "bzlib.h" +#endif + +#define Z_BZIP2ED 12 + +#if defined(STRICTZIP) || defined(STRICTZIPUNZIP) +/* like the STRICT of WIN32, we define a pointer that cannot be converted + from (void*) without cast */ +typedef struct TagzipFile__ { int unused; } zipFile__; +typedef zipFile__ *zipFile; +#else +typedef voidp zipFile; +#endif + +#define ZIP_OK (0) +#define ZIP_EOF (0) +#define ZIP_ERRNO (Z_ERRNO) +#define ZIP_PARAMERROR (-102) +#define ZIP_BADZIPFILE (-103) +#define ZIP_INTERNALERROR (-104) + +#ifndef DEF_MEM_LEVEL +# if MAX_MEM_LEVEL >= 8 +# define DEF_MEM_LEVEL 8 +# else +# define DEF_MEM_LEVEL MAX_MEM_LEVEL +# endif +#endif +/* default memLevel */ + +/* tm_zip contain date/time info */ +typedef struct tm_zip_s +{ + uInt tm_sec; /* seconds after the minute - [0,59] */ + uInt tm_min; /* minutes after the hour - [0,59] */ + uInt tm_hour; /* hours since midnight - [0,23] */ + uInt tm_mday; /* day of the month - [1,31] */ + uInt tm_mon; /* months since January - [0,11] */ + uInt tm_year; /* years - [1980..2044] */ +} tm_zip; + +typedef struct +{ + tm_zip tmz_date; /* date in understandable format */ + uLong dosDate; /* if dos_date == 0, tmu_date is used */ +/* uLong flag; */ /* general purpose bit flag 2 bytes */ + + uLong internal_fa; /* internal file attributes 2 bytes */ + uLong external_fa; /* external file attributes 4 bytes */ +} zip_fileinfo; + +typedef const char* zipcharpc; + + +#define APPEND_STATUS_CREATE (0) +#define APPEND_STATUS_CREATEAFTER (1) +#define APPEND_STATUS_ADDINZIP (2) + +extern zipFile ZEXPORT zipOpen OF((const char *pathname, int append)); +extern zipFile ZEXPORT zipOpen64 OF((const void *pathname, int append)); +/* + Create a zipfile. + pathname contain on Windows XP a filename like "c:\\zlib\\zlib113.zip" or on + an Unix computer "zlib/zlib113.zip". + if the file pathname exist and append==APPEND_STATUS_CREATEAFTER, the zip + will be created at the end of the file. + (useful if the file contain a self extractor code) + if the file pathname exist and append==APPEND_STATUS_ADDINZIP, we will + add files in existing zip (be sure you don't add file that doesn't exist) + If the zipfile cannot be opened, the return value is NULL. + Else, the return value is a zipFile Handle, usable with other function + of this zip package. +*/ + +/* Note : there is no delete function into a zipfile. + If you want delete file into a zipfile, you must open a zipfile, and create another + Of couse, you can use RAW reading and writing to copy the file you did not want delte +*/ + +extern zipFile ZEXPORT zipOpen2 OF((const char *pathname, + int append, + zipcharpc* globalcomment, + zlib_filefunc_def* pzlib_filefunc_def)); + +extern zipFile ZEXPORT zipOpen2_64 OF((const void *pathname, + int append, + zipcharpc* globalcomment, + zlib_filefunc64_def* pzlib_filefunc_def)); + +extern int ZEXPORT zipOpenNewFileInZip OF((zipFile file, + const char* filename, + const zip_fileinfo* zipfi, + const void* extrafield_local, + uInt size_extrafield_local, + const void* extrafield_global, + uInt size_extrafield_global, + const char* comment, + int method, + int level)); + +extern int ZEXPORT zipOpenNewFileInZip64 OF((zipFile file, + const char* filename, + const zip_fileinfo* zipfi, + const void* extrafield_local, + uInt size_extrafield_local, + const void* extrafield_global, + uInt size_extrafield_global, + const char* comment, + int method, + int level, + int zip64)); + +/* + Open a file in the ZIP for writing. + filename : the filename in zip (if NULL, '-' without quote will be used + *zipfi contain supplemental information + if extrafield_local!=NULL and size_extrafield_local>0, extrafield_local + contains the extrafield data the the local header + if extrafield_global!=NULL and size_extrafield_global>0, extrafield_global + contains the extrafield data the the local header + if comment != NULL, comment contain the comment string + method contain the compression method (0 for store, Z_DEFLATED for deflate) + level contain the level of compression (can be Z_DEFAULT_COMPRESSION) + zip64 is set to 1 if a zip64 extended information block should be added to the local file header. + this MUST be '1' if the uncompressed size is >= 0xffffffff. + +*/ + + +extern int ZEXPORT zipOpenNewFileInZip2 OF((zipFile file, + const char* filename, + const zip_fileinfo* zipfi, + const void* extrafield_local, + uInt size_extrafield_local, + const void* extrafield_global, + uInt size_extrafield_global, + const char* comment, + int method, + int level, + int raw)); + + +extern int ZEXPORT zipOpenNewFileInZip2_64 OF((zipFile file, + const char* filename, + const zip_fileinfo* zipfi, + const void* extrafield_local, + uInt size_extrafield_local, + const void* extrafield_global, + uInt size_extrafield_global, + const char* comment, + int method, + int level, + int raw, + int zip64)); +/* + Same than zipOpenNewFileInZip, except if raw=1, we write raw file + */ + +extern int ZEXPORT zipOpenNewFileInZip3 OF((zipFile file, + const char* filename, + const zip_fileinfo* zipfi, + const void* extrafield_local, + uInt size_extrafield_local, + const void* extrafield_global, + uInt size_extrafield_global, + const char* comment, + int method, + int level, + int raw, + int windowBits, + int memLevel, + int strategy, + const char* password, + uLong crcForCrypting)); + +extern int ZEXPORT zipOpenNewFileInZip3_64 OF((zipFile file, + const char* filename, + const zip_fileinfo* zipfi, + const void* extrafield_local, + uInt size_extrafield_local, + const void* extrafield_global, + uInt size_extrafield_global, + const char* comment, + int method, + int level, + int raw, + int windowBits, + int memLevel, + int strategy, + const char* password, + uLong crcForCrypting, + int zip64 + )); + +/* + Same than zipOpenNewFileInZip2, except + windowBits,memLevel,,strategy : see parameter strategy in deflateInit2 + password : crypting password (NULL for no crypting) + crcForCrypting : crc of file to compress (needed for crypting) + */ + +extern int ZEXPORT zipOpenNewFileInZip4 OF((zipFile file, + const char* filename, + const zip_fileinfo* zipfi, + const void* extrafield_local, + uInt size_extrafield_local, + const void* extrafield_global, + uInt size_extrafield_global, + const char* comment, + int method, + int level, + int raw, + int windowBits, + int memLevel, + int strategy, + const char* password, + uLong crcForCrypting, + uLong versionMadeBy, + uLong flagBase + )); + + +extern int ZEXPORT zipOpenNewFileInZip4_64 OF((zipFile file, + const char* filename, + const zip_fileinfo* zipfi, + const void* extrafield_local, + uInt size_extrafield_local, + const void* extrafield_global, + uInt size_extrafield_global, + const char* comment, + int method, + int level, + int raw, + int windowBits, + int memLevel, + int strategy, + const char* password, + uLong crcForCrypting, + uLong versionMadeBy, + uLong flagBase, + int zip64 + )); +/* + Same than zipOpenNewFileInZip4, except + versionMadeBy : value for Version made by field + flag : value for flag field (compression level info will be added) + */ + + +extern int ZEXPORT zipWriteInFileInZip OF((zipFile file, + const void* buf, + unsigned len)); +/* + Write data in the zipfile +*/ + +extern int ZEXPORT zipCloseFileInZip OF((zipFile file)); +/* + Close the current file in the zipfile +*/ + +extern int ZEXPORT zipCloseFileInZipRaw OF((zipFile file, + uLong uncompressed_size, + uLong crc32)); + +extern int ZEXPORT zipCloseFileInZipRaw64 OF((zipFile file, + ZPOS64_T uncompressed_size, + uLong crc32)); + +/* + Close the current file in the zipfile, for file opened with + parameter raw=1 in zipOpenNewFileInZip2 + uncompressed_size and crc32 are value for the uncompressed size +*/ + +extern int ZEXPORT zipClose OF((zipFile file, + const char* global_comment)); +/* + Close the zipfile +*/ + + +extern int ZEXPORT zipRemoveExtraInfoBlock OF((char* pData, int* dataLen, short sHeader)); +/* + zipRemoveExtraInfoBlock - Added by Mathias Svensson + + Remove extra information block from a extra information data for the local file header or central directory header + + It is needed to remove ZIP64 extra information blocks when before data is written if using RAW mode. + + 0x0001 is the signature header for the ZIP64 extra information blocks + + usage. + Remove ZIP64 Extra information from a central director extra field data + zipRemoveExtraInfoBlock(pCenDirExtraFieldData, &nCenDirExtraFieldDataLen, 0x0001); + + Remove ZIP64 Extra information from a Local File Header extra field data + zipRemoveExtraInfoBlock(pLocalHeaderExtraFieldData, &nLocalHeaderExtraFieldDataLen, 0x0001); +*/ + +#ifdef __cplusplus +} +#endif + +#endif /* _zip64_H */ diff --git a/BakerView/BKRAnalyticsEvents.h b/BakerView/BKRAnalyticsEvents.h new file mode 100644 index 0000000..6e544d6 --- /dev/null +++ b/BakerView/BKRAnalyticsEvents.h @@ -0,0 +1,45 @@ +// +// BakerAnalyticsEvents.h +// Baker +// +// ========================================================================================== +// +// Copyright (c) 2010-2013, Davide Casali, Marco Colombo, Alessandro Morandi +// Copyright (c) 2014, Andrew Krowczyk, Cédric Mériau, Pieter Claerhout +// All rights reserved. +// +// 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. +// Neither the name of the Baker Framework nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written +// permission. +// 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. +// + +#import + +@interface BKRAnalyticsEvents : NSObject +//{ +// id tracker; // Can be used to reference tracking libraries (i.e. Google Analytics, ...) +//} + +#pragma mark - Singleton + ++ (BKRAnalyticsEvents*)sharedInstance; +- (id)init; + +@end diff --git a/BakerView/BKRAnalyticsEvents.m b/BakerView/BKRAnalyticsEvents.m new file mode 100644 index 0000000..e7a3674 --- /dev/null +++ b/BakerView/BKRAnalyticsEvents.m @@ -0,0 +1,136 @@ +// +// BakerAnalyticsEvents.m +// Baker +// +// ========================================================================================== +// +// Copyright (c) 2010-2013, Davide Casali, Marco Colombo, Alessandro Morandi +// Copyright (c) 2014, Andrew Krowczyk, Cédric Mériau, Pieter Claerhout +// All rights reserved. +// +// 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. +// Neither the name of the Baker Framework nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written +// permission. +// 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. +// + +#import "BKRAnalyticsEvents.h" + +@implementation BKRAnalyticsEvents + +#pragma mark - Singleton + ++ (BKRAnalyticsEvents *)sharedInstance { + static dispatch_once_t once; + static BKRAnalyticsEvents *sharedInstance; + dispatch_once(&once, ^{ + sharedInstance = [[self alloc] init]; + }); + return sharedInstance; +} + +- (id)init { + self = [super init]; + if (self) { + + // ****** Add here your analytics code + + //GAI Configuration + // Optional: automatically send uncaught exceptions to Google Analytics. + //[GAI sharedInstance].trackUncaughtExceptions = YES; + + // Optional: set Google Analytics dispatch interval to e.g. 20 seconds. + //[GAI sharedInstance].dispatchInterval = 20; + + // Optional: set Logger to VERBOSE for debug information. + //[[[GAI sharedInstance] logger] setLogLevel:kGAILogLevelVerbose]; + + // Initialize tracker. Replace with your tracking ID. + //[[GAI sharedInstance] trackerWithTrackingId:@"UA-XXXX-Y"]; + + // ****** Register to handle events + [self registerEvents]; + + } + return self; +} + +- (void) dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +#pragma mark - Events + +- (void)registerEvents { + + // Register the analytics event that are going to be tracked by Baker. + NSArray *analyticEvents = @[@"BakerApplicationStart", + @"BakerIssueDownload", + @"BakerIssueOpen", + @"BakerIssueClose", + @"BakerIssuePurchase", + @"BakerIssueArchive", + @"BakerSubscriptionPurchase", + @"BakerViewPage", + @"BakerViewIndexOpen", + @"BakerViewModalBrowser"]; + + for (NSString *eventName in analyticEvents) { + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(receiveEvent:) + name:eventName + object:nil]; + } + +} + +- (void)receiveEvent:(NSNotification*)notification { + //NSLog(@"[BakerAnalyticsEvent] Received event %@", notification.name); // Uncomment this to debug + //GAI Activation + //id tracker = [[GAI sharedInstance] defaultTracker]; + + // If you want, you can handle differently the various events + if ([notification.name isEqualToString:@"BakerApplicationStart"]) { + // Track here when the Baker app opens + } else if ([notification.name isEqualToString:@"BakerIssueDownload"]) { + // Track here when a issue download is requested + } else if ([notification.name isEqualToString:@"BakerIssueOpen"]) { + // Track here when a issue is opened to be read + } else if ([notification.name isEqualToString:@"BakerIssueClose"]) { + // Track here when a issue that was being read is closed + } else if ([notification.name isEqualToString:@"BakerIssuePurchase"]) { + // Track here when a issue purchase is requested + } else if ([notification.name isEqualToString:@"BakerIssueArchive"]) { + // Track here when a issue archival is requested + } else if ([notification.name isEqualToString:@"BakerSubscriptionPurchase"]) { + // Track here when a subscription purchased is requested + } else if ([notification.name isEqualToString:@"BakerViewPage"]) { + // Track here when a specific page is opened + // BakerViewController *bakerview = [notification object]; // Uncomment this to get the BakerViewController object and get its properties + //NSLog(@" - Tracking page %d", bakerview.currentPageNumber); // This is useful to check if it works + } else if ([notification.name isEqualToString:@"BakerViewIndexOpen"]) { + // Track here the opening of the index and status bar + } else if ([notification.name isEqualToString:@"BakerViewModalBrowser"]) { + // Track here the opening of the modal view + } else { + } + +} + +@end diff --git a/BakerView/BKRBook.h b/BakerView/BKRBook.h new file mode 100644 index 0000000..1d98383 --- /dev/null +++ b/BakerView/BKRBook.h @@ -0,0 +1,111 @@ +// +// BakerBook.h +// Baker +// +// ========================================================================================== +// +// Copyright (c) 2010-2013, Davide Casali, Marco Colombo, Alessandro Morandi +// Copyright (c) 2014, Andrew Krowczyk, Cédric Mériau, Pieter Claerhout +// All rights reserved. +// +// 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. +// Neither the name of the Baker Framework nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written +// permission. +// 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. +// + +#import + +@interface BKRBook : NSObject + +#pragma mark - HPub Parameters Properties + +@property (nonatomic, strong) NSDictionary *bookData; +@property (copy, nonatomic) NSString *parseError; + +@property (nonatomic, copy) NSNumber *hpub; +@property (nonatomic, copy) NSString *title; +@property (nonatomic, copy) NSString *date; + +@property (nonatomic, copy) NSArray *author; +@property (nonatomic, copy) NSArray *creator; +@property (nonatomic, copy) NSArray *categories; +@property (nonatomic, copy) NSString *publisher; + +@property (nonatomic, copy) NSString *url; +@property (nonatomic, copy) NSString *cover; + +@property (nonatomic, copy) NSString *orientation; +@property (nonatomic, copy) NSNumber *zoomable; + +@property (nonatomic, strong) NSMutableArray *contents; + +#pragma mark - Baker HPub Extensions Properties + +@property (nonatomic, copy) NSString *bakerBackground; +@property (nonatomic, copy) NSString *bakerBackgroundImagePortrait; +@property (nonatomic, copy) NSString *bakerBackgroundImageLandscape; +@property (nonatomic, copy) NSString *bakerPageNumbersColor; +@property (nonatomic, copy) NSNumber *bakerPageNumbersAlpha; +@property (nonatomic, copy) NSString *bakerPageScreenshots; + +@property (nonatomic, copy) NSString *bakerRendering; +@property (nonatomic, copy) NSNumber *bakerVerticalBounce; +@property (nonatomic, copy) NSNumber *bakerVerticalPagination; +@property (nonatomic, copy) NSNumber *bakerPageTurnTap; +@property (nonatomic, copy) NSNumber *bakerPageTurnSwipe; +@property (nonatomic, copy) NSNumber *bakerMediaAutoplay; + +@property (nonatomic, copy) NSNumber *bakerIndexWidth; +@property (nonatomic, copy) NSNumber *bakerIndexHeight; +@property (nonatomic, copy) NSNumber *bakerIndexBounce; +@property (nonatomic, copy) NSNumber *bakerStartAtPage; + +#pragma mark - Book Status Properties + +@property (nonatomic, copy) NSString *ID; +@property (nonatomic, copy) NSString *path; +@property (nonatomic, copy) NSNumber *isBundled; +@property (nonatomic, copy) NSString *screenshotsPath; +@property (nonatomic, copy) NSNumber *screenshotsWritable; +@property (nonatomic, copy) NSNumber *currentPage; +@property (nonatomic, copy) NSNumber *lastScrollIndex; +@property (nonatomic, copy) NSDate *lastOpenedDate; + +#pragma mark - Init + +- (id)initWithBookPath:(NSString *)bookPath bundled:(BOOL)bundled; +- (id)initWithBookJSONPath:(NSString *)bookJSONPath; +- (id)initWithBookData:(NSDictionary *)bookData; +- (BOOL)loadBookData:(NSDictionary *)bookData; + +#pragma mark - HPub validation + +- (BOOL)isValid; +- (BOOL)validateBookJSON:(NSDictionary *)bookData withRequirements:(NSArray *)requirements; +- (BOOL)validateArray:(NSArray *)array forParam:(NSString *)param withParamsArray:(NSArray*)paramsArray; +- (BOOL)validateString:(NSString *)string forParam:(NSString *)param withParamsArray:(NSArray*)paramsArray; +- (BOOL)validateNumber:(NSNumber *)number forParam:(NSString *)param withParamsArray:(NSArray*)paramsArray; +- (BOOL)matchParam:(NSString *)param againstParamsArray:(NSArray *)paramsArray; + +#pragma mark - Book status management + +- (BOOL)updateBookPath:(NSString *)bookPath bundled:(BOOL)bundled; + +@end diff --git a/BakerView/BKRBook.m b/BakerView/BKRBook.m new file mode 100644 index 0000000..c4c82cc --- /dev/null +++ b/BakerView/BKRBook.m @@ -0,0 +1,386 @@ +// +// BakerBook.m +// Baker +// +// ========================================================================================== +// +// Copyright (c) 2010-2013, Davide Casali, Marco Colombo, Alessandro Morandi +// Copyright (c) 2014, Andrew Krowczyk, Cédric Mériau, Pieter Claerhout +// All rights reserved. +// +// 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. +// Neither the name of the Baker Framework nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written +// permission. +// 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. +// + +#import "BKRCore.h" +#import "BKRBook.h" +#import "NSString+BakerExtensions.h" + +@implementation BKRBook + +#pragma mark - Initialization + +- (id)initWithBookPath:(NSString*)bookPath bundled:(BOOL)bundled { + if (![[NSFileManager defaultManager] fileExistsAtPath:bookPath]) { + return nil; + } + + self = [self initWithBookJSONPath:[bookPath stringByAppendingPathComponent:@"book.json"]]; + if (self) { + [self updateBookPath:bookPath bundled:bundled]; + } + return self; + +} + +- (id)initWithBookJSONPath:(NSString*)bookJSONPath { + + if (![[NSFileManager defaultManager] fileExistsAtPath:bookJSONPath]) { + return nil; + } + + NSError* error = nil; + NSData* bookJSON = [NSData dataWithContentsOfFile:bookJSONPath options:0 error:&error]; + if (error) { + self.parseError = [NSString stringWithFormat:@"ERROR: reading 'book.json': %@", error.localizedDescription]; + NSLog(@"[BakerBook] ERROR reading 'book.json': %@", error.localizedDescription); + return nil; + } + + NSDictionary* bookData = [NSJSONSerialization JSONObjectWithData:bookJSON + options:0 + error:&error]; + if (error) { + self.parseError = [NSString stringWithFormat:@"ERROR: reading 'book.json': %@", error.localizedDescription]; + NSLog(@"[BakerBook] ERROR parsing 'book.json': %@", error.localizedDescription); + return nil; + } + + return [self initWithBookData:bookData]; + +} + +- (id)initWithBookData:(NSDictionary*)bookData { + self = [super init]; + if (self && [self loadBookData:bookData]) { + NSString *baseID = [self.title stringByAppendingFormat:@" %@", [self.url bkrStringSHAEncoded]]; + self.ID = [self sanitizeForPath:baseID]; + NSLog(@"[BakerBook] 'book.json' parsed successfully. Book '%@' created with id '%@'.", self.title, self.ID); + return self; + } + return nil; +} + +- (NSString*)sanitizeForPath:(NSString*)string { + + NSError *error = nil; + NSString *newString; + NSRegularExpression *regex; + + // Strip everything except numbers, ASCII letters and spaces + regex = [NSRegularExpression regularExpressionWithPattern:@"[^1-9a-z ]" options:NSRegularExpressionCaseInsensitive error:&error]; + newString = [regex stringByReplacingMatchesInString:string options:0 range:NSMakeRange(0, [string length]) withTemplate:@""]; + + // Replace spaces with dashes + regex = [NSRegularExpression regularExpressionWithPattern:@" +" options:NSRegularExpressionCaseInsensitive error:&error]; + newString = [regex stringByReplacingMatchesInString:newString options:0 range:NSMakeRange(0, [newString length]) withTemplate:@"-"]; + + return [newString lowercaseString]; + +} + +- (BOOL)loadBookData:(NSDictionary*)bookData { + if (![self validateBookJSON:bookData withRequirements:@[@"title", @"author", @"url", @"contents"]]) { + return NO; + } + + self.bookData = bookData; + + self.hpub = bookData[@"hpub"]; + self.title = bookData[@"title"]; + self.date = bookData[@"date"]; + self.categories = bookData[@"categories"]; + + if ([bookData[@"author"] isKindOfClass:[NSArray class]]) { + self.author = bookData[@"author"]; + } else { + self.author = @[bookData[@"author"]]; + } + + if ([bookData[@"creator"] isKindOfClass:[NSArray class]]) { + self.creator = bookData[@"creator"]; + } else if([bookData[@"creator"] isKindOfClass:[NSString class]]) { + self.creator = @[bookData[@"creator"]]; + } + + self.publisher = bookData[@"publisher"]; + + self.url = bookData[@"url"]; + self.cover = bookData[@"cover"]; + + self.orientation = bookData[@"orientation"]; + self.zoomable = bookData[@"zoomable"]; + + // TODO: create an array of n BakerPage objects + self.contents = bookData[@"contents"]; + + self.bakerBackground = bookData[@"-baker-background"]; + self.bakerBackgroundImagePortrait = bookData[@"-baker-background-image-portrait"]; + self.bakerBackgroundImageLandscape = bookData[@"-baker-background-image-landscape"]; + self.bakerPageNumbersColor = bookData[@"-baker-page-numbers-color"]; + self.bakerPageNumbersAlpha = bookData[@"-baker-page-numbers-alpha"]; + self.bakerPageScreenshots = bookData[@"-baker-page-screenshots"]; + + self.bakerRendering = bookData[@"-baker-rendering"]; + self.bakerVerticalBounce = bookData[@"-baker-vertical-bounce"]; + self.bakerVerticalPagination = bookData[@"-baker-vertical-pagination"]; + self.bakerPageTurnTap = bookData[@"-baker-page-turn-tap"]; + self.bakerPageTurnSwipe = bookData[@"-baker-page-turn-swipe"]; + self.bakerMediaAutoplay = bookData[@"-baker-media-autoplay"]; + + self.bakerIndexWidth = bookData[@"-baker-index-width"]; + self.bakerIndexHeight = bookData[@"-baker-index-height"]; + self.bakerIndexBounce = bookData[@"-baker-index-bounce"]; + self.bakerStartAtPage = bookData[@"-baker-start-at-page"]; + + [self loadBookJSONDefault]; + + return YES; +} + +- (void)loadBookJSONDefault { + + if (self.hpub == nil) { + self.hpub = @1; + } + + if (self.bakerBackground == nil) { + self.bakerBackground = @"#000000"; + } + if (self.bakerPageNumbersColor == nil) { + self.bakerPageNumbersColor = @"#ffffff"; + } + if (self.bakerPageNumbersAlpha == nil) { + self.bakerPageNumbersAlpha = @0.3f; + } + + if (self.bakerRendering == nil) { + self.bakerRendering = @"screenshots"; + } + if (self.bakerVerticalBounce == nil) { + self.bakerVerticalBounce = @YES; + } + if (self.bakerVerticalPagination == nil) { + self.bakerVerticalPagination = @NO; + } + + if (self.bakerPageTurnTap == nil) { + self.bakerPageTurnTap = @YES; + } + + if (self.bakerPageTurnSwipe == nil) { + self.bakerPageTurnSwipe = @YES; + } + if (self.bakerMediaAutoplay == nil) { + self.bakerMediaAutoplay = @NO; + } + + if (self.bakerIndexBounce == nil) { + self.bakerIndexBounce = @NO; + } + if (self.bakerStartAtPage == nil) { + self.bakerStartAtPage = @1; + } + +} + + +#pragma mark - HPub validation + +- (BOOL)isValid { + return BKR_IsEmpty(self.parseError); +} + +- (BOOL)validateBookJSON:(NSDictionary*)bookData withRequirements:(NSArray*)requirements { + for (NSString *param in requirements) { + if (bookData[param] == nil) { + NSLog(@"[BakerBook] ERROR: param '%@' is missing. Add it to 'book.json'.", param); + return NO; + } + } + + NSArray *shouldBeArray = @[@"author", + @"creator", + @"contents"]; + + NSArray *shouldBeString = @[@"title", + @"date", + @"author", + @"creator", + @"publisher", + @"url", + @"cover", + @"orientation", + @"-baker-background", + @"-baker-background-image-portrait", + @"-baker-background-image-landscape", + @"-baker-page-numbers-color", + @"-baker-page-screenshots", + @"-baker-rendering"]; + + NSArray *shouldBeNumber = @[@"hpub", + @"zoomable", + @"-baker-page-numbers-alpha", + @"-baker-vertical-bounce", + @"-baker-vertical-pagination", + @"-baker-page-turn-tap", + @"-baker-page-turn-swipe", + @"-baker-media-autoplay", + @"-baker-index-width", + @"-baker-index-height", + @"-baker-index-bounce", + @"-baker-start-at-page"]; + + NSArray *knownParams = [[shouldBeArray arrayByAddingObjectsFromArray:shouldBeString] arrayByAddingObjectsFromArray:shouldBeNumber]; + + for (NSString *param in bookData) { + + if (![self matchParam:param againstParamsArray:knownParams]) { + continue; + } + + id obj = bookData[param]; + if ([obj isKindOfClass:[NSArray class]] && ![self validateArray:(NSArray *)obj forParam:param withParamsArray:shouldBeArray]) { + return NO; + } else if ([obj isKindOfClass:[NSString class]] && ![self validateString:(NSString *)obj forParam:param withParamsArray:shouldBeString]) { + return NO; + } else if ([obj isKindOfClass:[NSNumber class]] && ![self validateNumber:(NSNumber *)obj forParam:param withParamsArray:shouldBeNumber]) { + return NO; + } + + } + + return YES; +} + +- (BOOL)validateArray:(NSArray*)array forParam:(NSString*)param withParamsArray:(NSArray*)paramsArray { + + if (![self matchParam:param againstParamsArray:paramsArray]) { + NSLog(@"[BakerBook] ERROR: param '%@' should not be an Array. Check it in 'book.json'.", param); + return NO; + } + + if (([param isEqualToString:@"author"] || [param isEqualToString:@"contents"]) && [array count] == 0) { + NSLog(@"[BakerBook] ERROR: param '%@' is empty. Fill it in 'book.json'.", param); + return NO; + } + + for (id obj in array) { + if ([param isEqualToString:@"author"] && (![obj isKindOfClass:[NSString class]] || [(NSString *)obj isEqualToString:@""])) { + NSLog(@"[BakerBook] ERROR: param 'author' is empty. Fill it in 'book.json'."); + return NO; + } else if ([param isEqualToString:@"contents"]) { + if ([obj isKindOfClass:[NSDictionary class]] && ![self validateBookJSON:(NSDictionary *)obj withRequirements:@[@"url"]]) { + NSLog(@"[BakerBook] ERROR: param 'contents' is not validating. Check it in 'book.json'."); + return NO; + } + } else if (![obj isKindOfClass:[NSString class]]) { + NSLog(@"[BakerBook] ERROR: param '%@' type is wrong. Check it in 'book.json'.", param); + return NO; + } + } + + return YES; + +} + +- (BOOL)validateString:(NSString*)string forParam:(NSString *)param withParamsArray:(NSArray*)paramsArray { + + if (![self matchParam:param againstParamsArray:paramsArray]) { + NSLog(@"[BakerBook] ERROR: param '%@' should not be a String. Check it in 'book.json'.", param); + return NO; + } + + if (([param isEqualToString:@"title"] || [param isEqualToString:@"author"] || [param isEqualToString:@"url"]) && [string isEqualToString:@""]) { + NSLog(@"[BakerBook] ERROR: param '%@' is empty. Fill it in 'book.json'.", param); + return NO; + } + + if ([param isEqualToString:@"-baker-rendering"] && (![string isEqualToString:@"screenshots"] && ![string isEqualToString:@"three-cards"])) { + NSLog(@"Error: param \"-baker-rendering\" should be equal to \"screenshots\" or \"three-cards\" but it's not"); + NSLog(@"[BakerBook] ERROR: param '-baker-rendering' must be equal to 'screenshots' or 'three-cards'. Check it in 'book.json'."); + return NO; + } + + return YES; + +} + +- (BOOL)validateNumber:(NSNumber*)number forParam:(NSString*)param withParamsArray:(NSArray*)paramsArray { + if (![self matchParam:param againstParamsArray:paramsArray]) { + NSLog(@"[BakerBook] ERROR: param '%@' should not be a Number. Check it in 'book.json'.", param); + return NO; + } + return YES; +} + +- (BOOL)matchParam:(NSString*)param againstParamsArray:(NSArray*)paramsArray { + for (NSString *match in paramsArray) { + if ([param isEqualToString:match]) { + return YES; + } + } + return NO; +} + +#pragma mark - Book status management + +- (BOOL)updateBookPath:(NSString*)bookPath bundled:(BOOL)bundled { + + NSFileManager *fileManager = [NSFileManager defaultManager]; + if (![fileManager fileExistsAtPath:bookPath]) { + return NO; + } + + self.path = bookPath; + self.isBundled = @(bundled); + + self.screenshotsPath = [bookPath stringByAppendingPathComponent:self.bakerPageScreenshots]; + self.screenshotsWritable = @YES; + + if (bundled) { + if (![fileManager fileExistsAtPath:self.screenshotsPath]) { + // TODO: generate writableBookPath in app private documents/books/self.ID; + NSString *writableBookPath = @"writableBookPath"; + self.screenshotsPath = [writableBookPath stringByAppendingPathComponent:self.bakerPageScreenshots]; + } else { + self.screenshotsWritable = @NO; + } + } + + if (![fileManager fileExistsAtPath:self.screenshotsPath]) { + return [fileManager createDirectoryAtPath:self.screenshotsPath withIntermediateDirectories:YES attributes:nil error:nil]; + } + + return YES; +} + +@end diff --git a/BakerView/BKRBookStatus.h b/BakerView/BKRBookStatus.h new file mode 100644 index 0000000..31b973b --- /dev/null +++ b/BakerView/BKRBookStatus.h @@ -0,0 +1,44 @@ +// +// BakerBookStatus.h +// Baker +// +// ========================================================================================== +// +// Copyright (c) 2010-2013, Davide Casali, Marco Colombo, Alessandro Morandi +// Copyright (c) 2014, Andrew Krowczyk, Cédric Mériau, Pieter Claerhout +// All rights reserved. +// +// 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. +// Neither the name of the Baker Framework nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written +// permission. +// 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. +// + +#import +#import "BKRJSONStatus.h" + +@interface BKRBookStatus : BKRJSONStatus + +@property (nonatomic, copy) NSNumber *page; +@property (nonatomic, copy) NSString *scrollIndex; + +- (id)initWithBookId:(NSString*)bookId; +- (void)save; + +@end diff --git a/BakerView/BKRBookStatus.m b/BakerView/BKRBookStatus.m new file mode 100644 index 0000000..f775edd --- /dev/null +++ b/BakerView/BKRBookStatus.m @@ -0,0 +1,55 @@ +// +// BakerBookStatus.m +// Baker +// +// ========================================================================================== +// +// Copyright (c) 2010-2013, Davide Casali, Marco Colombo, Alessandro Morandi +// Copyright (c) 2014, Andrew Krowczyk, Cédric Mériau, Pieter Claerhout +// All rights reserved. +// +// 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. +// Neither the name of the Baker Framework nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written +// permission. +// 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. +// + +#import "BKRBookStatus.h" +#import "NSObject+BakerExtensions.h" + +@implementation BKRBookStatus + +- (id)initWithBookId:(NSString*)bookId { + NSString *statusPath = [[[self.bkrCachePath stringByAppendingPathComponent:@"statuses"] stringByAppendingPathComponent:bookId] stringByAppendingPathExtension:@"json"]; + return [super initWithJSONPath:statusPath]; +} + +- (NSDictionary*)load { + NSDictionary *jsonDict = [super load]; + self.page = jsonDict[@"page"]; + self.scrollIndex = jsonDict[@"scroll-index"]; + return jsonDict; +} + +- (void)save { + NSDictionary *jsonDict = @{@"page": self.page, @"scroll-index": self.scrollIndex}; + [super save:jsonDict]; +} + +@end diff --git a/BakerView/BKRBookViewController.h b/BakerView/BKRBookViewController.h new file mode 100644 index 0000000..b415916 --- /dev/null +++ b/BakerView/BKRBookViewController.h @@ -0,0 +1,208 @@ +// +// RootViewController.h +// Baker +// +// ========================================================================================== +// +// Copyright (c) 2010-2013, Davide Casali, Marco Colombo, Alessandro Morandi +// Copyright (c) 2014, Andrew Krowczyk, Cédric Mériau, Pieter Claerhout +// All rights reserved. +// +// 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. +// Neither the name of the Baker Framework nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written +// permission. +// 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. +// + + +#import +#import +#import "BKRIndexViewController.h" +#import "BKRModalWebViewController.h" +#import "BKRBook.h" +#import "BKRBookStatus.h" + +@class Downloader; + +@interface BKRBookViewController : UIViewController { + + CGRect screenBounds; + + NSArray *supportedOrientation; + + NSString *defaultScreeshotsPath; + NSString *cachedScreenshotsPath; + + NSString *renderingType; + + NSMutableArray *pages; + NSMutableArray *toLoad; + + NSMutableArray *pageDetails; + NSMutableDictionary *attachedScreenshotPortrait; + NSMutableDictionary *attachedScreenshotLandscape; + + UIImage *backgroundImageLandscape; + UIImage *backgroundImagePortrait; + + NSString *pageNameFromURL; + NSString *anchorFromURL; + + int tapNumber; + int stackedScrollingAnimations; + + BOOL currentPageFirstLoading; + BOOL currentPageIsDelayingLoading; + BOOL currentPageHasChanged; + BOOL currentPageIsLocked; + BOOL currentPageWillAppearUnderModal; + + BOOL userIsScrolling; + BOOL shouldPropagateInterceptedTouch; + BOOL shouldForceOrientationUpdate; + + BOOL adjustViewsOnAppDidBecomeActive; + + //UIScrollView *scrollView; + UIWebView *prevPage; + //UIWebView *currPage; + UIWebView *nextPage; + + UIColor *webViewBackground; + + CGRect upTapArea; + CGRect downTapArea; + CGRect leftTapArea; + CGRect rightTapArea; + + int totalPages; + int lastPageNumber; + //int currentPageNumber; + + int pageWidth; + int pageHeight; + int currentPageHeight; + + NSString *URLDownload; + Downloader *downloader; + UIAlertView *feedbackAlert; + + BKRIndexViewController *indexViewController; + BKRModalWebViewController *myModalViewController; + + BKRBookStatus *bookStatus; +} + +#pragma mark - Properties + +@property (nonatomic, strong) BKRBook *book; +@property (nonatomic, strong) UIScrollView *scrollView; +@property (nonatomic, strong) UIWebView *currPage; +@property (nonatomic, strong) UIBarButtonItem *shareButton; + +@property (nonatomic, assign) int currentPageNumber; +@property (nonatomic, assign) BOOL barsHidden; + +#pragma mark - Initialization + +- (id)initWithBook:(BKRBook*)bakerBook; +- (BOOL)loadBookWithBookPath:(NSString*)bookPath; +- (void)cleanupBookEnvironment; +- (void)resetPageSlots; +- (void)resetPageDetails; +- (void)buildPageArray; +- (void)startReading; +- (void)buildPageDetails; +- (void)setImageFor:(UIImageView*)view; +- (void)updateBookLayout; +- (void)adjustScrollViewPosition; +- (void)setPageSize:(NSString*)orientation; +- (void)setTappableAreaSizeForOrientation:(NSString*)orientation; +- (void)showPageDetails; +- (void)setFrame:(CGRect)frame forPage:(UIWebView*)page; +- (void)setupWebView:(UIWebView*)webView; +- (void)removeWebViewDoubleTapGestureRecognizer:(UIView*)view; + +#pragma mark - Loading + +- (BOOL)changePage:(int)page; +- (void)gotoPageDelayer; +- (void)gotoPage; +- (void)lockPage:(NSNumber*)lock; +- (void)addPageLoading:(int)slot; +- (void)handlePageLoading; +- (void)loadSlot:(int)slot withPage:(int)page; +- (BOOL)loadWebView:(UIWebView*)webview withPage:(int)page; + +#pragma mark - Modal Webview + +- (void)loadModalWebView:(NSURL*)url; +- (void)closeModalWebView; + +#pragma mark - Scroll View + +- (CGRect)frameForPage:(int)page; + +#pragma mark - UIWebViewDelegate + +- (void)webView:(UIWebView*)webView hidden:(BOOL)status animating:(BOOL)animating; +- (void)webViewDidAppear:(UIWebView*)webView animating:(BOOL)animating; +- (void)webView:(UIWebView*)webView setCorrectOrientation:(UIInterfaceOrientation)interfaceOrientation; + +#pragma mark - Screenshots + +- (void)removeScreenshots; +- (void)updateScreenshots; +- (BOOL)checkScreeshotForPage:(int)pageNumber andOrientation:(NSString*)interfaceOrientation; +- (void)takeScreenshotFromView:(UIWebView*)webView forPage:(int)pageNumber andOrientation:(NSString*)interfaceOrientation; +- (void)placeScreenshotForView:(UIWebView*)webView andPage:(int)pageNumber andOrientation:(NSString*)interfaceOrientation; + +#pragma mark - UIGestureRecognizer + +- (void)handleInterceptedTouch:(NSNotification*)notification; +- (void)userDidTap:(UITouch*)touch; +- (void)userDidScroll:(UITouch*)touch; + +#pragma mark - Page Scrolling + +- (void)setCurrentPageHeight; +- (int)getCurrentPageOffset; +- (void)scrollUpCurrentPage:(int)offset animating:(BOOL)animating; +- (void)scrollDownCurrentPage:(int)offset animating:(BOOL)animating; +- (void)scrollPage:(UIWebView*)webView to:(NSString*)offset animating:(BOOL)animating; +- (void)handleAnchor:(BOOL)animating; + +#pragma mark - Navigation Bars + +- (CGRect)getNewNavigationFrame:(BOOL)hidden; +- (void)toggleBars; +- (void)showBars; +- (void)showNavigationBar; +- (void)hideBars:(NSNumber*)animated; +- (void)handleBookProtocol:(NSURL*)url; + +#pragma mark - Orientation + +- (NSString*)getCurrentInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation; + +#pragma mark - Index View + +- (BOOL)isIndexView:(UIWebView*)webView; + +@end diff --git a/BakerView/BKRBookViewController.m b/BakerView/BKRBookViewController.m new file mode 100755 index 0000000..6fb85ac --- /dev/null +++ b/BakerView/BKRBookViewController.m @@ -0,0 +1,1995 @@ +// +// RootViewController.m +// Baker +// +// ========================================================================================== +// +// Copyright (c) 2010-2013, Davide Casali, Marco Colombo, Alessandro Morandi +// Copyright (c) 2014, Andrew Krowczyk, Cédric Mériau, Pieter Claerhout +// All rights reserved. +// +// 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. +// Neither the name of the Baker Framework nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written +// permission. +// 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. +// + +#import +#import +#import + +#import "BKRBookViewController.h" +#import "BKRPageTitleLabel.h" +#import "BKRUtils.h" +#import "BKRSettings.h" +#import "NSObject+BakerExtensions.h" +#import "UIScreen+BakerExtensions.h" + +#define INDEX_FILE_NAME @"index.html" + +#define URL_OPEN_MODALLY @"referrer=Baker" +#define URL_OPEN_EXTERNAL @"referrer=Safari" + +// Screenshots +#define MAX_SCREENSHOT_AFTER_CP 10 +#define MAX_SCREENSHOT_BEFORE_CP 10 + +@implementation BKRBookViewController + +#pragma mark - INIT +- (id)initWithBook:(BKRBook *)bakerBook { + + self = [super init]; + if (self) { + NSLog(@"[BakerView] Init book view..."); + + _book = bakerBook; + + if ([self respondsToSelector:@selector(automaticallyAdjustsScrollViewInsets)]) { + // Only available in iOS 7 + + self.automaticallyAdjustsScrollViewInsets = NO; + } + + // ****** DEVICE SCREEN BOUNDS + screenBounds = [[UIScreen mainScreen] bounds]; + NSLog(@"[BakerView] Device Screen (WxH): %fx%f.", screenBounds.size.width, screenBounds.size.height); + + // ****** SUPPORTED ORIENTATION FROM PLIST + supportedOrientation = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UISupportedInterfaceOrientations"]; + + if (![[NSFileManager defaultManager] fileExistsAtPath:self.bkrCachePath]) { + [[NSFileManager defaultManager] createDirectoryAtPath:self.bkrCachePath withIntermediateDirectories:YES attributes:nil error:nil]; + } + + // ****** SCREENSHOTS DIRECTORY //TODO: set in load book only if is necessary + defaultScreeshotsPath = [[self.bkrCachePath stringByAppendingPathComponent:@"screenshots"] stringByAppendingPathComponent:bakerBook.ID]; + + // ****** STATUS FILE + bookStatus = [[BKRBookStatus alloc] initWithBookId:bakerBook.ID]; + NSLog(@"[BakerView] Status: page %@ @ scrollIndex %@px.", bookStatus.page, bookStatus.scrollIndex); + + // ****** Initialize audio session for html5 audio + AVAudioSession *audioSession = [AVAudioSession sharedInstance]; + BOOL ok; + NSError *setCategoryError = nil; + ok = [audioSession setCategory:AVAudioSessionCategoryPlayback + error:&setCategoryError]; + if (!ok) { + NSLog(@"[BakerView] AudioSession - %s setCategoryError=%@", __PRETTY_FUNCTION__, setCategoryError); + } + + // ****** BOOK ENVIRONMENT + pages = [NSMutableArray array]; + toLoad = [NSMutableArray array]; + + pageDetails = [NSMutableArray array]; + + attachedScreenshotPortrait = [NSMutableDictionary dictionary]; + attachedScreenshotLandscape = [NSMutableDictionary dictionary]; + + tapNumber = 0; + stackedScrollingAnimations = 0; // TODO: CHECK IF STILL USED! + + currentPageFirstLoading = YES; + currentPageIsDelayingLoading = YES; + currentPageHasChanged = NO; + currentPageIsLocked = NO; + currentPageWillAppearUnderModal = NO; + + userIsScrolling = NO; + shouldPropagateInterceptedTouch = YES; + shouldForceOrientationUpdate = YES; + + adjustViewsOnAppDidBecomeActive = NO; + _barsHidden = NO; + + webViewBackground = nil; + + pageNameFromURL = nil; + anchorFromURL = nil; + + // TODO: LOAD BOOK METHOD IN VIEW DID LOAD + [self loadBookWithBookPath:bakerBook.path]; + } + return self; +} +- (void)viewDidLoad { + + [super viewDidLoad]; + self.navigationItem.title = self.book.title; + + + // ****** SET THE INITIAL SIZE FOR EVERYTHING + // Avoids strange animations when opening + [self setPageSize:[self getCurrentInterfaceOrientation:self.interfaceOrientation]]; + + // SOCIAL MEDIA INTEGRATION - START + if ([BKRSettings sharedSettings].showSocialShareButton) { + self.shareButton = [[UIBarButtonItem alloc] + initWithBarButtonSystemItem:UIBarButtonSystemItemAction + target:self + action:@selector(shareBtnAction:)]; + self.navigationItem.rightBarButtonItem = self.shareButton; + } + // SOCIAL MEDIA INTEGRATION - END + + // ****** SCROLLVIEW INIT + self.scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, pageWidth, pageHeight)]; + self.scrollView.showsHorizontalScrollIndicator = YES; + self.scrollView.showsVerticalScrollIndicator = NO; + self.scrollView.delaysContentTouches = NO; + self.scrollView.pagingEnabled = YES; + self.scrollView.delegate = self; + + self.scrollView.scrollEnabled = [self.book.bakerPageTurnSwipe boolValue]; + self.scrollView.backgroundColor = [BKRUtils colorWithHexString:self.book.bakerBackground]; + + [self.view addSubview:self.scrollView]; + + + // ****** BAKER BACKGROUND + backgroundImageLandscape = nil; + backgroundImagePortrait = nil; + + NSString *backgroundPathLandscape = self.book.bakerBackgroundImageLandscape; + if (backgroundPathLandscape != nil) { + backgroundPathLandscape = [self.book.path stringByAppendingPathComponent:backgroundPathLandscape]; + backgroundImageLandscape = [UIImage imageWithContentsOfFile:backgroundPathLandscape]; + } + + NSString *backgroundPathPortrait = self.book.bakerBackgroundImagePortrait; + if (backgroundPathPortrait != nil) { + backgroundPathPortrait = [self.book.path stringByAppendingPathComponent:backgroundPathPortrait]; + backgroundImagePortrait = [UIImage imageWithContentsOfFile:backgroundPathPortrait]; + } + +} + +- (void)viewWillAppear:(BOOL)animated { + + if (!currentPageWillAppearUnderModal) { + + [super viewWillAppear:animated]; + [self.navigationController.navigationBar setTranslucent:YES]; + + // Prevent duplicate observers + [[NSNotificationCenter defaultCenter] removeObserver:self name:@"notification_touch_intercepted" object:nil]; + + // ****** LISTENER FOR INTERCEPTOR WINDOW NOTIFICATION + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleInterceptedTouch:) name:@"notification_touch_intercepted" object:nil]; + + // ****** LISTENER FOR CLOSING APPLICATION + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleApplicationWillResignActive:) name:@"applicationWillResignActiveNotification" object:nil]; + + } else { + + // In case the orientation changed while being in modal view, restore the + // webview and stuff to the current orientation + [self willRotateToInterfaceOrientation:self.interfaceOrientation duration:0]; + [self didRotateFromInterfaceOrientation:self.interfaceOrientation]; + } +} +- (void)handleApplicationWillResignActive:(NSNotification *)notification { + NSLog(@"[BakerView] Resign, saving..."); + [self saveBookStatusWithScrollIndex]; + adjustViewsOnAppDidBecomeActive = YES; +} + +// SOCIAL MEDIA INTEGRATION - START + +// Reduce Image size before sharing +- (UIImage *)resizeImage:(UIImage*)image { + + int maxw=320; + CGSize newSize = CGSizeMake(maxw, image.size.height * (maxw/image.size.width)); + + CGRect newRect = CGRectIntegral(CGRectMake(0, 0, newSize.width, newSize.height)); + CGImageRef imageRef = image.CGImage; + + UIGraphicsBeginImageContextWithOptions(newSize, NO, 0); + CGContextRef context = UIGraphicsGetCurrentContext(); + + // Set the quality level to use when rescaling + CGContextSetInterpolationQuality(context, kCGInterpolationHigh); + CGAffineTransform flipVertical = CGAffineTransformMake(1, 0, 0, -1, 0, newSize.height); + + CGContextConcatCTM(context, flipVertical); + // Draw into the context; this scales the image + CGContextDrawImage(context, newRect, imageRef); + + // Get the resized image from the context and a UIImage + CGImageRef newImageRef = CGBitmapContextCreateImage(context); + UIImage *newImage = [UIImage imageWithCGImage:newImageRef]; + + CGImageRelease(newImageRef); + UIGraphicsEndImageContext(); + + return newImage; +} + + + +- (void) shareBtnAction:(UIButton *)sender { + + if (![self checkScreeshotForPage:_currentPageNumber andOrientation:[self getCurrentInterfaceOrientation:self.interfaceOrientation]]) { + [self takeScreenshotFromView:_currPage forPage:_currentPageNumber andOrientation:[self getCurrentInterfaceOrientation:self.interfaceOrientation]]; + } + + NSString *screenshotFile = [cachedScreenshotsPath stringByAppendingPathComponent:[NSString stringWithFormat:@"screenshot-%@-%i.jpg", [self getCurrentInterfaceOrientation:self.interfaceOrientation], _currentPageNumber]]; + + // items to share + + NSString *appName=[[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleDisplayName"]; + NSString *url = [ _book.url stringByReplacingOccurrencesOfString:@"book" withString:@"http"]; + + NSString *message = [NSString stringWithFormat:NSLocalizedString(@"SOCIAL_SHARE_BUTTON_MESSAGE", nil), _book.title, appName, url ]; + + + UIImage *image = [UIImage imageWithContentsOfFile: screenshotFile]; + UIImage *small = [self resizeImage:image]; + + NSArray *items = @[message, small]; + + + // create the controller + UIActivityViewController *controller = [[UIActivityViewController alloc]initWithActivityItems:items applicationActivities:nil]; + + // Exclude Irrelevant Parts + controller.excludedActivityTypes = @[UIActivityTypePostToWeibo,UIActivityTypePrint,UIActivityTypeCopyToPasteboard,UIActivityTypeAssignToContact,UIActivityTypeSaveToCameraRoll]; + if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone) { + [self presentViewController:controller animated:YES completion:nil]; + } else { + UIPopoverController *popup = [[UIPopoverController alloc] initWithContentViewController:controller]; + [popup presentPopoverFromBarButtonItem:self.shareButton permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES]; + } + +} +// SOCIAL MEDIA INTEGRATION - END + +- (void)viewDidAppear:(BOOL)animated { + + if (!currentPageWillAppearUnderModal) { + + [super viewDidAppear:animated]; + + if (![self forceOrientationUpdate]) { + [self willRotateToInterfaceOrientation:self.interfaceOrientation duration:0]; + [self performSelector:@selector(hideBars:) withObject:@YES afterDelay:0.5]; + + // Condition to make sure we only call startReading the first time this callback is invoked + // Fixes page reload on coming back from fullscreen video (#611) + if (self.currPage == nil) { + [self startReading]; + } + + [self didRotateFromInterfaceOrientation:self.interfaceOrientation]; + } + } + + currentPageWillAppearUnderModal = NO; +} + +- (void)viewDidLayoutSubviews { + // UINavigationController likes to mess with subviews when app becomes active + // viewDidLayoutSubviews is called after UINavigationController is already done, + // so we can adjust the views + if (adjustViewsOnAppDidBecomeActive) { + NSLog(@"[BakerView] Adjusting views on appDidBecomeActive"); + [self adjustScrollViewPosition]; + if (indexViewController != nil) { + [indexViewController adjustIndexView]; + } + adjustViewsOnAppDidBecomeActive = NO; + } +} + +- (BOOL)loadBookWithBookPath:(NSString *)bookPath { + NSLog(@"[BakerView] Loading book from path: %@", bookPath); + + // ****** CLEANUP PREVIOUS BOOK + [self cleanupBookEnvironment]; + + // ****** LOAD CONTENTS + [self buildPageArray]; + + // ****** SET STARTING PAGE + int lastPageViewed = [bookStatus.page intValue]; + int bakerStartAtPage = [self.book.bakerStartAtPage intValue]; + self.currentPageNumber = 1; + + if (currentPageFirstLoading && lastPageViewed != 0) { + self.currentPageNumber = lastPageViewed; + } else if (bakerStartAtPage < 0) { + self.currentPageNumber = MAX(1, totalPages + bakerStartAtPage + 1); + } else if (bakerStartAtPage > 0) { + self.currentPageNumber = MIN(totalPages, bakerStartAtPage); + } + bookStatus.page = @(self.currentPageNumber); + + // ****** SET SCREENSHOTS FOLDER + NSString *screenshotFolder = self.book.bakerPageScreenshots; + if (screenshotFolder) { + // When a screenshots folder is specified in book.json + cachedScreenshotsPath = [bookPath stringByAppendingPathComponent:screenshotFolder]; + } + + if (!screenshotFolder || ![[NSFileManager defaultManager] fileExistsAtPath:cachedScreenshotsPath]) { + // When a screenshot folder is not specified in book.json, or is specified but not actually existing + cachedScreenshotsPath = defaultScreeshotsPath; + } + NSLog(@"[BakerView] Screenshots stored at: %@", cachedScreenshotsPath); + + + return YES; +} +- (void)cleanupBookEnvironment { + + [self resetPageSlots]; + [self resetPageDetails]; + + [pages removeAllObjects]; + [toLoad removeAllObjects]; +} +- (void)resetPageSlots { + //NSLog(@"[BakerView] Reset leftover page slot"); + + if (self.currPage) { + [self.currPage setDelegate:nil]; + [self.currPage removeFromSuperview]; + } + if (nextPage) { + [nextPage setDelegate:nil]; + [nextPage removeFromSuperview]; + } + if (prevPage) { + [prevPage setDelegate:nil]; + [prevPage removeFromSuperview]; + } + + self.currPage = nil; + nextPage = nil; + prevPage = nil; +} +- (void)resetPageDetails { + //NSLog(@"[BakerView] Reset page details array and empty screenshot directory"); + + for (NSMutableDictionary *details in pageDetails) { + for (NSString *key in details) { + UIView *value = details[key]; + [value removeFromSuperview]; + } + } + + [pageDetails removeAllObjects]; +} +- (void)buildPageArray { + for (id page in self.book.contents) { + + NSString *pageFile = nil; + if ([page isKindOfClass:[NSString class]]) { + pageFile = [self.book.path stringByAppendingPathComponent:page]; + } else if ([page isKindOfClass:[NSDictionary class]]) { + pageFile = [self.book.path stringByAppendingPathComponent:page[@"url"]]; + } + + if (pageFile != nil && [[NSFileManager defaultManager] fileExistsAtPath:pageFile]) { + [pages addObject:pageFile]; + } else { + NSLog(@"[BakerView] ERROR: Page %@ does not exist in %@.", page, self.book.path); + } + } + + totalPages = (int)[pages count]; + NSLog(@"[BakerView] Pages in this book: %d", totalPages); +} +- (void)startReading { + + //[self setPageSize:[self getCurrentInterfaceOrientation:self.interfaceOrientation]]; + [self buildPageDetails]; + //[self updateBookLayout]; + + // ****** INDEX WEBVIEW INIT + // we move it here to make it more clear and clean + + if (indexViewController != nil) { + // first of all, we need to clean the indexview if it exists. + [indexViewController.view removeFromSuperview]; + } + indexViewController = [[BKRIndexViewController alloc] initWithBook:self.book fileName:INDEX_FILE_NAME webViewDelegate:self]; + [self.view addSubview:indexViewController.view]; + [indexViewController loadContent]; + + currentPageIsDelayingLoading = YES; + + [self addPageLoading:0]; + + if ([self.book.bakerRendering isEqualToString:@"three-cards"]) { + if (self.currentPageNumber != totalPages) { + [self addPageLoading:+1]; + } + + if (self.currentPageNumber != 1) { + [self addPageLoading:-1]; + } + } + + [self handlePageLoading]; +} +- (void)buildPageDetails { + //NSLog(@"[BakerView] Init page details for the book pages"); + + for (int i = 0; i < totalPages; i++) { + + UIColor *foregroundColor = [BKRUtils colorWithHexString:self.book.bakerPageNumbersColor]; + + + // ****** Background + UIImageView *backgroundView = [[UIImageView alloc] initWithFrame:CGRectMake(pageWidth * i, 0, pageWidth, pageHeight)]; + [self setImageFor:backgroundView]; + [self.scrollView addSubview:backgroundView]; + + + // ****** Spinners + UIActivityIndicatorView *spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; + spinner.backgroundColor = [UIColor clearColor]; + spinner.color = foregroundColor; + spinner.alpha = [self.book.bakerPageNumbersAlpha floatValue]; + + CGRect frame = spinner.frame; + frame.origin.x = pageWidth * i + (pageWidth - frame.size.width) / 2; + frame.origin.y = (pageHeight - frame.size.height) / 2; + spinner.frame = frame; + + [self.scrollView addSubview:spinner]; + [spinner startAnimating]; + + + // ****** Numbers + UILabel *number = [[UILabel alloc] initWithFrame:CGRectMake(pageWidth * i + (pageWidth - 115) / 2, pageHeight / 2 - 55, 115, 30)]; + number.backgroundColor = [UIColor clearColor]; + number.font = [UIFont fontWithName:@"Helvetica" size:40.0]; + number.textColor = foregroundColor; + number.textAlignment = NSTextAlignmentCenter; + number.alpha = [self.book.bakerPageNumbersAlpha floatValue]; + + number.text = [NSString stringWithFormat:@"%d", i + 1]; + if ([self.book.bakerStartAtPage intValue] < 0) { + number.text = [NSString stringWithFormat:@"%d", totalPages - i]; + } + + [self.scrollView addSubview:number]; + + + // ****** Title + BKRPageTitleLabel *title = [[BKRPageTitleLabel alloc]initWithFile:pages[i] color:foregroundColor alpha:[self.book.bakerPageNumbersAlpha floatValue]]; + [title setX:(pageWidth * i + ((pageWidth - title.frame.size.width) / 2)) Y:(pageHeight / 2 + 20)]; + [self.scrollView addSubview:title]; + + + // ****** Store instances for later use + NSMutableDictionary *details = [NSMutableDictionary dictionaryWithObjectsAndKeys:spinner, @"spinner", number, @"number", title, @"title", backgroundView, @"background", nil]; + [pageDetails insertObject:details atIndex:i]; + } +} +- (void)setImageFor:(UIImageView *)view { + if (pageWidth > pageHeight && backgroundImageLandscape != NULL) { + // Landscape + view.image = backgroundImageLandscape; + } else if (pageWidth < pageHeight && backgroundImagePortrait != NULL) { + // Portrait + view.image = backgroundImagePortrait; + } else { + view.image = NULL; + } +} +- (void)updateBookLayout { + //NSLog(@"[BakerView] Prevent page from changing until layout is updated"); + [self lockPage:@YES]; + [self showPageDetails]; + + if ([self.book.bakerRendering isEqualToString:@"screenshots"]) { + // TODO: BE SURE TO KNOW THE CORRECT CURRENT PAGE! + [self removeScreenshots]; + [self updateScreenshots]; + } + + [self adjustScrollViewPosition]; + + [self setFrame:[self frameForPage:self.currentPageNumber] forPage:self.currPage]; + [self setFrame:[self frameForPage:self.currentPageNumber + 1] forPage:nextPage]; + [self setFrame:[self frameForPage:self.currentPageNumber - 1] forPage:prevPage]; + + [self.scrollView scrollRectToVisible:[self frameForPage:self.currentPageNumber] animated:NO]; + + //NSLog(@"[BakerView] Unlock page changing"); + [self lockPage:@NO]; +} + +- (void)adjustScrollViewPosition { + int scrollViewY = 0; + [UIView animateWithDuration:UINavigationControllerHideShowBarDuration + animations:^{ self.scrollView.frame = CGRectMake(0, scrollViewY, pageWidth, pageHeight); } + completion:nil]; +} + +- (void)setPageSize:(NSString*)orientation { + NSLog(@"[BakerView] Set size for orientation: %@", orientation); + + pageWidth = [[UIScreen mainScreen] bkrWidthForOrientationName:orientation]; + pageHeight = [[UIScreen mainScreen] bkrHeightForOrientationName:orientation]; + + [self setTappableAreaSizeForOrientation:orientation]; + + self.scrollView.contentSize = CGSizeMake(pageWidth * totalPages, pageHeight); +} +- (void)setTappableAreaSizeForOrientation:(NSString*)orientation { + //NSLog(@"[BakerView] Set tappable area size"); + + CGFloat screenWidth = [[UIScreen mainScreen] bkrWidthForOrientationName:orientation]; + int tappableAreaSize; + if ([orientation isEqualToString:@"portrait"]) { + tappableAreaSize = screenWidth / 16; + } else { + tappableAreaSize = screenWidth / 7; + } + + upTapArea = CGRectMake(tappableAreaSize, 0, screenWidth - (tappableAreaSize * 2), tappableAreaSize); + downTapArea = CGRectMake(tappableAreaSize, screenWidth - tappableAreaSize, screenWidth - (tappableAreaSize * 2), tappableAreaSize); + leftTapArea = CGRectMake(0, tappableAreaSize, tappableAreaSize, screenWidth - (tappableAreaSize * 2)); + rightTapArea = CGRectMake(screenWidth - tappableAreaSize, tappableAreaSize, tappableAreaSize, screenWidth - (tappableAreaSize * 2)); +} +- (void)showPageDetails { + //NSLog(@"[BakerView] Show page details for the book pages"); + + // TODO: IS THIS NEEDED ? + for (NSMutableDictionary *details in pageDetails) { + for (NSString *key in details) { + UIView *value = details[key]; + value.hidden = YES; + } + } + + for (int i = 0; i < totalPages; i++) { + + if (pageDetails.count > i && pageDetails[i] != nil) { + + NSDictionary *details = [NSDictionary dictionaryWithDictionary:pageDetails[i]]; + + for (NSString *key in details) { + UIView *value = details[key]; + if (value != nil) { + + CGRect frame = value.frame; + if ([key isEqualToString:@"spinner"]) { + + frame.origin.x = pageWidth * i + (pageWidth - frame.size.width) / 2; + frame.origin.y = (pageHeight - frame.size.height) / 2; + value.frame = frame; + value.hidden = NO; + + } else if ([key isEqualToString:@"number"]) { + + frame.origin.x = pageWidth * i + (pageWidth - 115) / 2; + frame.origin.y = pageHeight / 2 - 55; + value.frame = frame; + value.hidden = NO; + + } else if ([key isEqualToString:@"title"]) { + + frame.origin.x = pageWidth * i + (pageWidth - frame.size.width) / 2; + frame.origin.y = pageHeight / 2 + 20; + value.frame = frame; + value.hidden = NO; + + } else if ([key isEqualToString:@"background"]) { + + [self setImageFor:(UIImageView *)value]; + + frame.origin.x = pageWidth * i; + frame.size.width = pageWidth; + frame.size.height = pageHeight; + value.frame = frame; + value.hidden = NO; + + } else { + + value.hidden = YES; + } + } + } + } + } +} +- (void)setFrame:(CGRect)frame forPage:(UIWebView *)page { + if (page && [page.superview isEqual:self.scrollView]) { + page.frame = frame; + [self.scrollView bringSubviewToFront:page]; + } +} + +- (void)setupWebView:(UIWebView *)webView { + //NSLog(@"[BakerView] Setup webView"); + + if (webViewBackground == nil) + { + webViewBackground = webView.backgroundColor; + } + + webView.backgroundColor = [UIColor clearColor]; + webView.opaque = NO; + + webView.delegate = self; + + webView.mediaPlaybackRequiresUserAction = ![self.book.bakerMediaAutoplay boolValue]; + webView.allowsInlineMediaPlayback = YES; + webView.scalesPageToFit = [self.book.zoomable boolValue]; + BOOL verticalBounce = [self.book.bakerVerticalBounce boolValue]; + + for (UIView *subview in webView.subviews) { + if ([subview isKindOfClass:[UIScrollView class]]) { + ((UIScrollView *)subview).bounces = verticalBounce; + } + } + + if (!webView.scalesPageToFit) { + [self removeWebViewDoubleTapGestureRecognizer:webView]; + } +} +- (void)removeWebViewDoubleTapGestureRecognizer:(UIView *)view +{ + for (UIGestureRecognizer *recognizer in [view gestureRecognizers]) { + if ([recognizer isKindOfClass:[UITapGestureRecognizer class]] && [(UITapGestureRecognizer *)recognizer numberOfTapsRequired] == 2) { + [view removeGestureRecognizer:recognizer]; + } + } + for (UIView *subview in view.subviews) { + [self removeWebViewDoubleTapGestureRecognizer:subview]; + } +} + +#pragma mark - LOADING +- (BOOL)changePage:(int)page { + //NSLog(@"[BakerView] Check if page has changed"); + + BOOL pageChanged = NO; + + if (page < 1) + { + self.currentPageNumber = 1; + } + else if (page > totalPages) + { + self.currentPageNumber = totalPages; + } + else if (page != self.currentPageNumber) + { + // While we are tapping, we don't want scrolling event to get in the way + self.scrollView.scrollEnabled = NO; + stackedScrollingAnimations++; + + lastPageNumber = self.currentPageNumber; + self.currentPageNumber = page; + + tapNumber = tapNumber + (lastPageNumber - self.currentPageNumber); + + [self hideBars:@YES]; + [self.scrollView scrollRectToVisible:[self frameForPage:self.currentPageNumber] animated:YES]; + + [self gotoPageDelayer]; + + pageChanged = YES; + } + bookStatus.page = @(self.currentPageNumber); + + return pageChanged; +} +- (void)gotoPageDelayer { + // This delay is required in order to avoid stuttering when the animation runs. + // The animation lasts 0.5 seconds: so we start loading after that. + + if (currentPageIsDelayingLoading) { + [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(gotoPage) object:nil]; + } + + currentPageIsDelayingLoading = YES; + [self performSelector:@selector(gotoPage) withObject:nil afterDelay:0.5]; +} +- (void)gotoPage { + + NSString *path = [NSString stringWithString:pages[self.currentPageNumber - 1]]; + if ([[NSFileManager defaultManager] fileExistsAtPath:path] && tapNumber != 0) { + + //NSLog(@"[BakerView] Goto page -> %@", [[NSFileManager defaultManager] displayNameAtPath:path]); + + if ([self.book.bakerRendering isEqualToString:@"three-cards"]) + { + // ****** THREE CARD VIEW METHOD + + // Dispatch blur event on old current page + [BKRUtils webView:self.currPage dispatchHTMLEvent:@"blur"]; + + // Calculate move direction and normalize tapNumber + int direction = 1; + if (tapNumber < 0) { + direction = -direction; + tapNumber = -tapNumber; + } + NSLog(@"[BakerView] Tap number: %d", tapNumber); + + if (tapNumber > 2) { + tapNumber = 0; + + // Moved away for more than 2 pages: RELOAD ALL pages + [toLoad removeAllObjects]; + + [self.currPage removeFromSuperview]; + [nextPage removeFromSuperview]; + [prevPage removeFromSuperview]; + + [self addPageLoading:0]; + if (self.currentPageNumber < totalPages) { + [self addPageLoading:+1]; + } + if (self.currentPageNumber > 1) { + [self addPageLoading:-1]; + } + + } else { + + int tmpSlot = 0; + if (tapNumber == 2) { + + // Moved away for 2 pages: RELOAD CURRENT page + if (direction < 0) { + // Move LEFT <<< + [prevPage removeFromSuperview]; + UIWebView *tmpView = prevPage; + prevPage = nextPage; + nextPage = tmpView; + } else { + // Move RIGHT >>> + [nextPage removeFromSuperview]; + UIWebView *tmpView = nextPage; + nextPage = prevPage; + prevPage = tmpView; + } + + // Adjust pages slot in the stack to reflect the webviews pointer change + for (int i = 0; i < [toLoad count]; i++) { + tmpSlot = -1 * [[toLoad[i] valueForKey:@"slot"] intValue]; + toLoad[i][@"slot"] = @(tmpSlot); + } + + [self.currPage removeFromSuperview]; + [self addPageLoading:0]; + + } else if (tapNumber == 1) { + + if (direction < 0) { + // ****** Move LEFT <<< + [prevPage removeFromSuperview]; + UIWebView *tmpView = prevPage; + prevPage = self.currPage; + self.currPage = nextPage; + nextPage = tmpView; + + } else { + // ****** Move RIGHT >>> + [nextPage removeFromSuperview]; + UIWebView *tmpView = nextPage; + nextPage = self.currPage; + self.currPage = prevPage; + prevPage = tmpView; + } + + // Adjust pages slot in the stack to reflect the webviews pointer change + for (int i = 0; i < [toLoad count]; i++) { + tmpSlot = [[toLoad[i] valueForKey:@"slot"] intValue]; + if (direction < 0) { + if (tmpSlot == +1) { + tmpSlot = 0; + } else if (tmpSlot == 0) { + tmpSlot = -1; + } else if (tmpSlot == -1) { + tmpSlot = +1; + } + } else { + if (tmpSlot == -1) { + tmpSlot = 0; + } else if (tmpSlot == 0) { + tmpSlot = +1; + } else if (tmpSlot == +1) { + tmpSlot = -1; + } + } + toLoad[i][@"slot"] = @(tmpSlot); + } + + // Since we are not loading anything we have to reset the delayer flag + currentPageIsDelayingLoading = NO; + + // Dispatch focus event on new current page + [BKRUtils webView:self.currPage dispatchHTMLEvent:@"focus"]; + + // Dispatch BakerViewPage analytics event + [[NSNotificationCenter defaultCenter] postNotificationName:@"BakerViewPage" object:self]; // -> Baker Analytics Event + } + + [self setCurrentPageHeight]; + + tapNumber = 0; + if (direction < 0) { + + // REMOVE OTHER NEXT page from toLoad stack + for (int i = 0; i < [toLoad count]; i++) { + if ([[toLoad[i] valueForKey:@"slot"] intValue] == +1) { + [toLoad removeObjectAtIndex:i]; + } + } + + // PRELOAD NEXT page + if (self.currentPageNumber < totalPages) { + [self addPageLoading:+1]; + } + + } else { + + // REMOVE OTHER PREV page from toLoad stack + for (int i = 0; i < [toLoad count]; i++) { + if ([[toLoad[i] valueForKey:@"slot"] intValue] == -1) { + [toLoad removeObjectAtIndex:i]; + } + } + + // PRELOAD PREV page + if (self.currentPageNumber > 1) { + [self addPageLoading:-1]; + } + } + } + + [self handlePageLoading]; + } + else + { + tapNumber = 0; + + [toLoad removeAllObjects]; + [self.currPage removeFromSuperview]; + + [self updateScreenshots]; + + if (![self checkScreeshotForPage:self.currentPageNumber andOrientation:[self getCurrentInterfaceOrientation:self.interfaceOrientation]]) { + [self lockPage:@YES]; + } + + [self addPageLoading:0]; + [self handlePageLoading]; + } + } +} +- (void)lockPage:(NSNumber *)lock { + if ([lock boolValue]) + { + if (self.scrollView.scrollEnabled) { + self.scrollView.scrollEnabled = NO; + } + currentPageIsLocked = YES; + } + else + { + if (stackedScrollingAnimations == 0) { + self.scrollView.scrollEnabled = [self.book.bakerPageTurnSwipe boolValue]; // YES by default, NO if specified + } + currentPageIsLocked = NO; + } +} +- (void)addPageLoading:(int)slot { + //NSLog(@"[BakerView] Add page to the loding queue"); + + NSArray *objs = @[@(slot), @(self.currentPageNumber + slot)]; + NSArray *keys = @[@"slot", @"page"]; + + if (slot == 0) { + [toLoad insertObject:[NSMutableDictionary dictionaryWithObjects:objs forKeys:keys] atIndex:0]; + } else { + [toLoad addObject:[NSMutableDictionary dictionaryWithObjects:objs forKeys:keys]]; + } +} +- (void)handlePageLoading { + if ([toLoad count] != 0) { + + int slot = [[toLoad[0] valueForKey:@"slot"] intValue]; + int page = [[toLoad[0] valueForKey:@"page"] intValue]; + + //NSLog(@"[BakerView] Handle loading of slot %d with page %d", slot, page); + + [toLoad removeObjectAtIndex:0]; + [self loadSlot:slot withPage:page]; + } +} +- (void)loadSlot:(int)slot withPage:(int)page { + //NSLog(@"[BakerView] Setting up slot %d with page %d.", slot, page); + + UIWebView *webView = [[UIWebView alloc] init]; + [self setupWebView:webView]; + + webView.frame = [self frameForPage:page]; + webView.hidden = YES; + + // ****** SELECT + // Since pointers can change at any time we've got to handle them directly on a slot basis. + // Save the page pointer to a temp view to avoid code redundancy make Baker go apeshit. + if (slot == 0) { + if (page == self.currentPageNumber) + [[NSNotificationCenter defaultCenter] postNotificationName:@"BakerViewPage" object:self]; // -> Baker Analytics Event + + if (self.currPage) { + self.currPage.delegate = nil; + if ([self.currPage isLoading]) { + [self.currPage stopLoading]; + } + } + self.currPage = webView; + currentPageHasChanged = YES; + + } else if (slot == +1) { + + if (nextPage) { + nextPage.delegate = nil; + if ([nextPage isLoading]) { + [nextPage stopLoading]; + } + } + nextPage = webView; + + } else if (slot == -1) { + + if (prevPage) { + prevPage.delegate = nil; + if ([prevPage isLoading]) { + [prevPage stopLoading]; + } + } + prevPage = webView; + } + + + ((UIScrollView *)[webView subviews][0]).pagingEnabled = [self.book.bakerVerticalPagination boolValue]; + + [self.scrollView addSubview:webView]; + [self loadWebView:webView withPage:page]; +} +- (BOOL)loadWebView:(UIWebView*)webView withPage:(int)page { + + NSString *path = [NSString stringWithString:pages[page - 1]]; + + if ([[NSFileManager defaultManager] fileExistsAtPath:path]) { + NSLog(@"[BakerView] Loading: %@", [[NSFileManager defaultManager] displayNameAtPath:path]); + [webView loadRequest:[NSURLRequest requestWithURL:[NSURL fileURLWithPath:path]]]; + return YES; + } + return NO; +} + +#pragma mark - MODAL WEBVIEW +- (void)loadModalWebView:(NSURL *)url { + /**************************************************************************************************** + * Initializes the modal view and opens the requested url. + * It contains a fix to avoid an overlapping status bar. + */ + + //NSLog(@"[BakerView] Loading a Modal WebView with URL: %@", url.absoluteString); + [[NSNotificationCenter defaultCenter] postNotificationName:@"BakerViewModalBrowser" object:self]; // -> Baker Analytics Event + + myModalViewController = [[BKRModalWebViewController alloc] initWithURL:url]; + myModalViewController.modalTransitionStyle = UIModalTransitionStyleCoverVertical; + myModalViewController.delegate = self; + + // Hide the IndexView before opening modal web view + [self hideBars:@YES]; + + [self presentViewController:myModalViewController animated:YES completion:nil]; + + currentPageWillAppearUnderModal = YES; +} +- (void)closeModalWebView { + /**************************************************************************************************** + * This function is called from inside the modal view to close itself (delegate). + */ + + [self dismissViewControllerAnimated:YES completion:nil]; +} + +#pragma mark - SCROLLVIEW +- (CGRect)frameForPage:(int)page { + return CGRectMake(pageWidth * (page - 1), 0, pageWidth, pageHeight); +} +- (void)scrollViewWillBeginDragging:(UIScrollView *)scroll { + //NSLog(@"[BakerView] Scrollview will begin dragging"); + [self hideBars:@YES]; +} +- (void)scrollViewDidEndDragging:(UIScrollView *)scroll willDecelerate:(BOOL)decelerate { + //NSLog(@"[BakerView] Scrollview did end dragging"); +} +- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scroll { + //NSLog(@"[BakerView] Scrollview will begin decelerating"); +} +- (void)scrollViewDidEndDecelerating:(UIScrollView *)scroll { + + int page = (int)(scroll.contentOffset.x / pageWidth) + 1; + NSLog(@"[BakerView] Swiping to page: %d", page); + + if (self.currentPageNumber != page) { + + lastPageNumber = self.currentPageNumber; + self.currentPageNumber = page; + + tapNumber = tapNumber + (lastPageNumber - self.currentPageNumber); + + currentPageIsDelayingLoading = YES; + [self gotoPage]; + } +} +- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scroll { + //NSLog(@"[BakerView] Scrollview did end scrolling animation"); + + stackedScrollingAnimations--; + if (stackedScrollingAnimations == 0) { + //NSLog(@"[BakerView] Scroll enabled"); + scroll.scrollEnabled = [self.book.bakerPageTurnSwipe boolValue]; // YES by default, NO if specified + } +} + +#pragma mark - WEBVIEW +- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType { + + // Sent before a web view begins loading content, useful to trigger actions before the WebView. + NSURL *url = [request URL]; + + if ([webView isEqual:prevPage]) + { + //NSLog(@"[BakerView] Page is prev page --> load page"); + return YES; + } + else if ([webView isEqual:nextPage]) + { + //NSLog(@"[BakerView] Page is next page --> load page"); + return YES; + } + else if (currentPageIsDelayingLoading) + { + //NSLog(@"[BakerView] Page is current page and current page IS delaying loading --> load page"); + currentPageIsDelayingLoading = NO; + return ![self isIndexView:webView]; + } + else + { + // ****** Handle URI schemes + if (url) + { + // Existing, checking if index... + if([[url relativePath] isEqualToString:[indexViewController indexPath]]) + { + //NSLog(@"[BakerView] Page is index --> load index"); + return YES; + } + else + { + //NSLog(@"[BakerView] Page is current page and current page IS NOT delaying loading --> handle clicked link: %@", [url absoluteString]); + + // Not index, checking scheme... + if ([[url scheme] isEqualToString:@"file"]) + { + // ****** Handle: file:// + NSLog(@"[BakerView] Page is a link with scheme file:// --> load internal link"); + + anchorFromURL = [url fragment]; + NSString *file = [[url relativePath] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; + + NSUInteger page = [pages indexOfObject:file]; + if (page == NSNotFound) + { + NSString *params = [url query]; + NSLog(@"[BakerView] Opening a relative URL: %@", [url absoluteString]); + + if (params != nil) + { + NSRegularExpression *referrerModalRegex = [NSRegularExpression regularExpressionWithPattern:URL_OPEN_MODALLY options:NSRegularExpressionCaseInsensitive error:NULL]; + NSUInteger matchesModal = [referrerModalRegex numberOfMatchesInString:params options:0 range:NSMakeRange(0, [params length])]; + + if (matchesModal) + { + NSLog(@"[BakerView] Link contain param '%@' --> open link modally", URL_OPEN_MODALLY); + + NSLog(@"[BakerView] Opening in Modal Panel with URL: %@", [url absoluteString]); + [self loadModalWebView:[NSURL URLWithString:[url absoluteString]]]; + + return NO; + } + } + // ****** Internal link, but not one of the book pages --> load page anyway + return YES; + } + + page = page + 1; + if (![self changePage:(int)page] && ![webView isEqual:indexViewController.view]) + { + if (anchorFromURL == nil) { + return YES; + } + + [self handleAnchor:YES]; + } + } + else if ([[url scheme] isEqualToString:@"book"]) + { + [self toggleBars]; + [self performSelector:@selector(handleBookProtocol:) withObject:url afterDelay:0.4]; + } + else if ([[url scheme] isEqualToString:@"mailto"]) + { + // Handle mailto links using MessageUI framework + NSLog(@"[BakerView] Page is a link with scheme mailto: --> handle mail link"); + + // Build temp array and dictionary + NSArray *tempArray = [[url absoluteString] componentsSeparatedByString:@"?"]; + NSMutableDictionary *queryDictionary = [[NSMutableDictionary alloc] init]; + + // Check array count to see if we have parameters to query + if ([tempArray count] == 2) + { + NSArray *keyValuePairs = [tempArray[1] componentsSeparatedByString:@"&"]; + + for (NSString *queryString in keyValuePairs) { + NSArray *keyValuePair = [queryString componentsSeparatedByString:@"="]; + if (keyValuePair.count == 2) { + queryDictionary[keyValuePair[0]] = keyValuePair[1]; + } + } + } + + NSString *email = (tempArray[0]) ? tempArray[0] : [url resourceSpecifier]; + NSString *subject = queryDictionary[@"subject"]; + NSString *body = queryDictionary[@"body"]; + + + if ([MFMailComposeViewController canSendMail]) + { + MFMailComposeViewController *mailer = [[MFMailComposeViewController alloc] init]; + + mailer.mailComposeDelegate = self; + mailer.modalPresentationStyle = UIModalPresentationPageSheet; + + [mailer setToRecipients:@[[email stringByReplacingOccurrencesOfString:@"mailto:" withString:@""]]]; + [mailer setSubject:[subject stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]; + [mailer setMessageBody:[body stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding] isHTML:NO]; + + // Show the view + [self presentViewController:mailer animated:YES completion:nil]; + + currentPageWillAppearUnderModal = YES; + + } + else + { + // Check if the system can handle a mailto link + if ([[UIApplication sharedApplication] canOpenURL:url]) + { + // Go for it and open the URL within the respective app + [[UIApplication sharedApplication] openURL: url]; + } + else + { + // Display error message + [BKRUtils showAlertWithTitle:NSLocalizedString(@"MAILTO_ALERT_TITLE", nil) + message:NSLocalizedString(@"MAILTO_ALERT_MESSAGE", nil) + buttonTitle:NSLocalizedString(@"MAILTO_ALERT_CLOSE", nil)]; + } + } + + return NO; + } + else if (![[url scheme] isEqualToString:@""] && ![[url scheme] isEqualToString:@"http"] && ![[url scheme] isEqualToString:@"https"]) + { + if ([[UIApplication sharedApplication] canOpenURL:url]) { + [[UIApplication sharedApplication] openURL:url]; + } else { + NSLog(@"[BakerView] ERROR: No installed application to open '%@'. An application to handle the '%@' URL scheme is required.", url, [url scheme]); + [BKRUtils webView:self.currPage dispatchHTMLEvent:@"urlnothandled" withParams:@{@"url": url}]; + } + + return NO; + } + else if ([[url scheme] isEqualToString:@"baker-toc://show"]) + { + [self showBars]; + return NO; + } + else if ([[url scheme] isEqualToString:@"baker-toc://hide"]) + { + [self hideBars:@YES]; + return NO; + } + else if ([[url scheme] isEqualToString:@"baker-toc://toggle"]) + { + [self toggleBars]; + return NO; + } + else + { + // **************************************************************************************************** OPEN OUTSIDE BAKER + // * This is required since the inclusion of external libraries (like Google Maps) requires + // * direct opening of external pages within Baker. So we have to handle when you want to actually + // * open a page outside of Baker. + + NSString *params = [url query]; + //NSLog(@"[BakerView] Opening absolute URL: %@", [url absoluteString]); + + if (params != nil) + { + NSRegularExpression *referrerExternalRegex = [NSRegularExpression regularExpressionWithPattern:URL_OPEN_EXTERNAL options:NSRegularExpressionCaseInsensitive error:NULL]; + NSUInteger matches = [referrerExternalRegex numberOfMatchesInString:params options:0 range:NSMakeRange(0, [params length])]; + + NSRegularExpression *referrerModalRegex = [NSRegularExpression regularExpressionWithPattern:URL_OPEN_MODALLY options:NSRegularExpressionCaseInsensitive error:NULL]; + NSUInteger matchesModal = [referrerModalRegex numberOfMatchesInString:params options:0 range:NSMakeRange(0, [params length])]; + + if (matches > 0) + { + //NSLog(@"[BakerView] Link contain param '%@' --> open link in Safari", URL_OPEN_EXTERNAL); + + // Generate new URL without + // We are regexp-ing three things: the string alone, the string first with other content, the string with other content in any other position + NSString *pattern = [[NSString alloc] initWithFormat:@"\\?%@$|(?<=\\?)%@&?|()&?%@", URL_OPEN_EXTERNAL, URL_OPEN_EXTERNAL, URL_OPEN_EXTERNAL]; + NSRegularExpression *replacerRegexp = [NSRegularExpression regularExpressionWithPattern:pattern options:NSRegularExpressionCaseInsensitive error:NULL]; + NSString *oldURL = [url absoluteString]; + //NSLog(@"[BakerView] replacement pattern: %@", [replacerRegexp pattern]); + NSString *newURL = [replacerRegexp stringByReplacingMatchesInString:oldURL options:0 range:NSMakeRange(0, [oldURL length]) withTemplate:@""]; + + NSLog(@"[BakerView] Opening in Safari with URL: %@", newURL); + [[UIApplication sharedApplication] openURL:[NSURL URLWithString:newURL]]; + + return NO; + } + else if (matchesModal) + { + //NSLog(@"[BakerView] Link contain param '%@' --> open link modally", URL_OPEN_MODALLY); + + // Generate new URL without + // We are regexp-ing three things: the string alone, the string first with other content, the string with other content in any other position + NSString *pattern = [[NSString alloc] initWithFormat:@"\\?%@$|(?<=\\?)%@&?|()&?%@", URL_OPEN_MODALLY, URL_OPEN_MODALLY, URL_OPEN_MODALLY]; + NSRegularExpression *replacerRegexp = [NSRegularExpression regularExpressionWithPattern:pattern options:NSRegularExpressionCaseInsensitive error:NULL]; + NSString *oldURL = [url absoluteString]; + //NSLog(@"[BakerView] replacement pattern: %@", [replacerRegexp pattern]); + NSString *newURL = [replacerRegexp stringByReplacingMatchesInString:oldURL options:0 range:NSMakeRange(0, [oldURL length]) withTemplate:@""]; + + NSLog(@"[BakerView] Opening in Modal Panel with URL: %@", newURL); + [self loadModalWebView:[NSURL URLWithString:newURL]]; + + return NO; + } + } + + NSLog(@"[BakerView] Opening absolute link in page (no parameter specified). URL: %@", [url absoluteString]); + + return YES; + } + } + } + + return NO; + } +} +- (void)webViewDidStartLoad:(UIWebView *)webView { + //NSLog(@"[BakerView] Page did start load"); +} +- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error { + // Sent if a web view failed to load content. + if ([webView isEqual:self.currPage]) { + NSLog(@"[BakerView] ERROR: CurrPage failed to load content with error: %@", error); + } else if ([webView isEqual:prevPage]) { + NSLog(@"[BakerView] ERROR: PrevPage failed to load content with error: %@", error); + } else if ([webView isEqual:nextPage]) { + NSLog(@"[BakerView] ERROR: NextPage failed to load content with error: %@", error); + } +} +- (void)webViewDidFinishLoad:(UIWebView *)webView { + //NSLog(@"[BakerView] Page did finish load"); + [self webView:webView setCorrectOrientation:self.interfaceOrientation]; + + if (webView.hidden == YES) + { + if ([webView isEqual:self.currPage]) { + currentPageHasChanged = NO; + [self setCurrentPageHeight]; + } + + [webView removeFromSuperview]; + webView.hidden = NO; + + if ([self.book.bakerRendering isEqualToString:@"three-cards"]) { + [self webView:webView hidden:NO animating:YES]; + } else { + [self takeScreenshotFromView:webView forPage:self.currentPageNumber andOrientation:[self getCurrentInterfaceOrientation:self.interfaceOrientation]]; + } + + [self handlePageLoading]; + } + + /** CHECK IF META TAG SAYS HTML FILE SHOULD BE PAGED **/ + [webView.scrollView setPagingEnabled:[BKRUtils webViewShouldBePaged:webView forBook:self.book]]; +} +- (void)webView:(UIWebView *)webView hidden:(BOOL)status animating:(BOOL)animating { + + //NSLog(@"[BakerView] webView hidden: %d animating: %d", status, animating); + + if (animating) { + + webView.hidden = NO; + webView.alpha = 0.0; + + [self.scrollView addSubview:webView]; + [UIView animateWithDuration:0.5 + animations:^{ webView.alpha = 1.0; } + completion:^(BOOL finished) { [self webViewDidAppear:webView animating:animating]; }]; + } else { + + [self.scrollView addSubview:webView]; + [self webViewDidAppear:webView animating:animating]; + } +} +- (void)webViewDidAppear:(UIWebView *)webView animating:(BOOL)animating { + + if ([webView isEqual:self.currPage]) + { + [BKRUtils webView:webView dispatchHTMLEvent:@"focus"]; + + // If is the first time i load something in the currPage web view... + if (currentPageFirstLoading) + { + // ... check if there is a saved starting scroll index and set it + //NSLog(@"[BakerView] Handle last scroll index if necessary"); + NSString *currPageScrollIndex = bookStatus.scrollIndex; + if (currPageScrollIndex != nil) { + [self scrollDownCurrentPage:[currPageScrollIndex intValue] animating:YES]; + } + currentPageFirstLoading = NO; + } + else + { + //NSLog(@"[BakerView] Handle saved hash reference if necessary"); + [self handleAnchor:YES]; + } + } +} + +- (void)webView:(UIWebView *)webView setCorrectOrientation:(UIInterfaceOrientation)interfaceOrientation { + + // Since the UIWebView doesn't handle orientationchange events correctly we have to set the correct value for window.orientation property ourselves + NSString *jsOrientationGetter; + switch (interfaceOrientation) { + case UIInterfaceOrientationPortrait: + jsOrientationGetter = @"window.__defineGetter__('orientation', function() { return 0; });"; + break; + case UIInterfaceOrientationLandscapeLeft: + jsOrientationGetter = @"window.__defineGetter__('orientation', function() { return 90; });"; + break; + case UIInterfaceOrientationPortraitUpsideDown: + jsOrientationGetter = @"window.__defineGetter__('orientation', function() { return 180; });"; + break; + case UIInterfaceOrientationLandscapeRight: + jsOrientationGetter = @"window.__defineGetter__('orientation', function() { return -90; });"; + break; + default: + jsOrientationGetter = nil; + break; + } + + [webView stringByEvaluatingJavaScriptFromString:jsOrientationGetter]; +} + +#pragma mark - SCREENSHOTS +- (void)removeScreenshots { + + for (NSNumber *key in attachedScreenshotLandscape) { + UIView *value = attachedScreenshotLandscape[key]; + [value removeFromSuperview]; + } + + for (NSNumber *key in attachedScreenshotPortrait) { + UIView *value = attachedScreenshotPortrait[key]; + [value removeFromSuperview]; + } + + [attachedScreenshotLandscape removeAllObjects]; + [attachedScreenshotPortrait removeAllObjects]; +} +- (void)updateScreenshots { + + NSMutableSet *completeSet = [NSMutableSet new]; + NSMutableSet *supportSet = [NSMutableSet new]; + + NSString *interfaceOrientation = nil; + NSMutableDictionary *attachedScreenshot = nil; + + if (pageWidth < pageHeight) { + interfaceOrientation = @"portrait"; + attachedScreenshot = attachedScreenshotPortrait; + } else if (pageWidth > pageHeight) { + interfaceOrientation = @"landscape"; + attachedScreenshot = attachedScreenshotLandscape; + } + + /* + DEPRECATED - Won't work if called during rotation + if (UIInterfaceOrientationIsPortrait(self.interfaceOrientation)) + { + interfaceOrientation = @"portrait"; + attachedScreenshot = attachedScreenshotPortrait; + } + else if (UIInterfaceOrientationIsLandscape(self.interfaceOrientation)) + { + interfaceOrientation = @"landscape"; + attachedScreenshot = attachedScreenshotLandscape; + } + */ + + for (NSNumber *num in attachedScreenshot) [completeSet addObject:num]; + + for (int i = MAX(1, self.currentPageNumber - MAX_SCREENSHOT_BEFORE_CP); i <= MIN(totalPages, self.currentPageNumber + MAX_SCREENSHOT_AFTER_CP); i++) + { + NSNumber *num = @(i); + [supportSet addObject:num]; + + if ([self checkScreeshotForPage:i andOrientation:interfaceOrientation] && !attachedScreenshot[num]) { + [self placeScreenshotForView:nil andPage:i andOrientation:interfaceOrientation]; + [completeSet addObject:num]; + } + } + + [completeSet minusSet:supportSet]; + + for (NSNumber *num in completeSet) { + [attachedScreenshot[num] removeFromSuperview]; + [attachedScreenshot removeObjectForKey:num]; + } + +} +- (BOOL)checkScreeshotForPage:(int)pageNumber andOrientation:(NSString *)interfaceOrientation { + + if (![[NSFileManager defaultManager] fileExistsAtPath:cachedScreenshotsPath]) { + [[NSFileManager defaultManager] createDirectoryAtPath:cachedScreenshotsPath withIntermediateDirectories:YES attributes:nil error:nil]; + } + + NSString *screenshotFile = [cachedScreenshotsPath stringByAppendingPathComponent:[NSString stringWithFormat:@"screenshot-%@-%i.jpg", interfaceOrientation, pageNumber]]; + return [[NSFileManager defaultManager] fileExistsAtPath:screenshotFile]; +} +- (void)takeScreenshotFromView:(UIWebView *)webView forPage:(int)pageNumber andOrientation:(NSString *)interfaceOrientation { + + BOOL shouldRevealWebView = YES; + BOOL animating = YES; + + if (![self checkScreeshotForPage:pageNumber andOrientation:interfaceOrientation]) + { + //NSLog(@"[BakerView] Taking screenshot of page %d", pageNumber); + + NSString *screenshotFile = [cachedScreenshotsPath stringByAppendingPathComponent:[NSString stringWithFormat:@"screenshot-%@-%i.jpg", interfaceOrientation, pageNumber]]; + UIImage *screenshot = nil; + + if ([interfaceOrientation isEqualToString:[self getCurrentInterfaceOrientation:self.interfaceOrientation]] && !currentPageHasChanged) { + + UIGraphicsBeginImageContextWithOptions(webView.frame.size, NO, [[UIScreen mainScreen] scale]); + [webView.layer renderInContext:UIGraphicsGetCurrentContext()]; + screenshot = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + if (screenshot) { + BOOL saved = [UIImageJPEGRepresentation(screenshot, 0.6) writeToFile:screenshotFile options:NSDataWritingAtomic error:nil]; + if (saved) { + NSLog(@"[BakerView] Screenshot succesfully saved to file %@", screenshotFile); + [self placeScreenshotForView:webView andPage:pageNumber andOrientation:interfaceOrientation]; + shouldRevealWebView = NO; + } + } + } + + [self performSelector:@selector(lockPage:) withObject:@NO afterDelay:0.1]; + } + + if (!currentPageHasChanged && shouldRevealWebView) { + [self webView:webView hidden:NO animating:animating]; + } +} +- (void)placeScreenshotForView:(UIWebView *)webView andPage:(int)pageNumber andOrientation:(NSString *)interfaceOrientation { + + int i = pageNumber - 1; + NSNumber *num = @(pageNumber); + + NSString *screenshotFile = [cachedScreenshotsPath stringByAppendingPathComponent:[NSString stringWithFormat:@"screenshot-%@-%i.jpg", interfaceOrientation, pageNumber]]; + UIImageView *screenshotView = [[UIImageView alloc] initWithImage:[UIImage imageWithContentsOfFile:screenshotFile]]; + + NSMutableDictionary *attachedScreenshot = attachedScreenshotPortrait; + + int pageSizeWidth = [[UIScreen mainScreen] bkrWidthForOrientationName:interfaceOrientation]; + int pageSizeHeight = [[UIScreen mainScreen] bkrHeightForOrientationName:interfaceOrientation]; + CGSize pageSize = CGSizeMake(pageSizeWidth, pageSizeHeight); + + screenshotView.frame = CGRectMake(pageSize.width * i, 0, pageSize.width, pageSize.height); + + BOOL alreadyPlaced = NO; + UIImageView *oldScreenshot = attachedScreenshot[num]; + + if (oldScreenshot) { + [self.scrollView addSubview:screenshotView]; + [attachedScreenshot removeObjectForKey:num]; + [oldScreenshot removeFromSuperview]; + + alreadyPlaced = YES; + } + + attachedScreenshot[num] = screenshotView; + + if (webView == nil) + { + screenshotView.alpha = 0.0; + + [self.scrollView addSubview:screenshotView]; + [UIView animateWithDuration:0.5 animations:^{ screenshotView.alpha = 1.0; }]; + } + else if (webView != nil) + { + if (alreadyPlaced) + { + [self webView:webView hidden:NO animating:NO]; + } + else if ([interfaceOrientation isEqualToString:[self getCurrentInterfaceOrientation:self.interfaceOrientation]] && !currentPageHasChanged) + { + screenshotView.alpha = 0.0; + + [self.scrollView addSubview:screenshotView]; + [UIView animateWithDuration:0.5 + animations:^{ screenshotView.alpha = 1.0; } + completion:^(BOOL finished) { if (!currentPageHasChanged) { [self webView:webView hidden:NO animating:NO]; }}]; + } + } + +} + +#pragma mark - GESTURES +- (void)handleInterceptedTouch:(NSNotification *)notification { + + NSDictionary *userInfo = notification.userInfo; + UITouch *touch = userInfo[@"touch"]; + BOOL shouldPropagateIndexInterceptedTouch = NO; + + if (touch.phase == UITouchPhaseBegan) { + userIsScrolling = NO; + shouldPropagateInterceptedTouch = ([touch.view isDescendantOfView:self.scrollView]); + shouldPropagateIndexInterceptedTouch = [touch.view isDescendantOfView:indexViewController.view]; + } else if (touch.phase == UITouchPhaseMoved) { + userIsScrolling = YES; + } + + if (shouldPropagateInterceptedTouch) { + if (userIsScrolling) { + [self userDidScroll:touch]; + } else if (touch.phase == UITouchPhaseEnded) { + [self userDidTap:touch]; + } + } else if (shouldPropagateIndexInterceptedTouch) { + if (touch.tapCount == 2) { + //NSLog(@"[BakerView] Index Double Tap: Bars Toggled"); + [self toggleBars]; + } + } +} +- (void)userDidTap:(UITouch *)touch { + /**************************************************************************************************** + * This function handles all the possible user navigation taps: + * up, down, left, right and double-tap. + */ + + CGPoint tapPoint = [touch locationInView:self.view]; + //NSLog(@"[BakerView] User tap at [%f, %f]", tapPoint.x, tapPoint.y); + + // Swipe or scroll the page. + if (!currentPageIsLocked) + { + if (CGRectContainsPoint(upTapArea, tapPoint)) { + NSLog(@"[BakerView] Tap UP /\\!"); + [self scrollUpCurrentPage:([self getCurrentPageOffset] - pageHeight + 50) animating:YES]; + } else if (CGRectContainsPoint(downTapArea, tapPoint)) { + NSLog(@"[BakerView] Tap DOWN \\/"); + [self scrollDownCurrentPage:([self getCurrentPageOffset] + pageHeight - 50) animating:YES]; + } else if (CGRectContainsPoint(leftTapArea, tapPoint) || CGRectContainsPoint(rightTapArea, tapPoint)) { + int page = 0; + if (CGRectContainsPoint(leftTapArea, tapPoint)) { + NSLog(@"[BakerView] Tap LEFT >>>"); + page = self.currentPageNumber - 1; + } else if (CGRectContainsPoint(rightTapArea, tapPoint)) { + NSLog(@"[BakerView] Tap RIGHT <<<"); + page = self.currentPageNumber + 1; + } + + if ([self.book.bakerPageTurnTap boolValue]) [self changePage:page]; + } + else if (touch.tapCount == 2) { + //NSLog(@"[BakerView] Index Double Tap: Bars Toggled"); + [self toggleBars]; + } + } +} +- (void)userDidScroll:(UITouch *)touch { + //NSLog(@"[BakerView] User scroll"); + [self hideBars:@YES]; + + self.currPage.backgroundColor = webViewBackground; + self.currPage.opaque = YES; +} + +#pragma mark - PAGE SCROLLING +- (void)setCurrentPageHeight { + for (UIView *subview in self.currPage.subviews) { + if ([subview isKindOfClass:[UIScrollView class]]) { + CGSize size = ((UIScrollView *)subview).contentSize; + //NSLog(@"[BakerView] Setting current page height from %d to %f", currentPageHeight, size.height); + currentPageHeight = size.height; + } + } +} +- (int)getCurrentPageOffset { + + int currentPageOffset = [[self.currPage stringByEvaluatingJavaScriptFromString:@"window.scrollY;"] intValue]; + if (currentPageOffset < 0) return 0; + + int currentPageMaxScroll = currentPageHeight - pageHeight; + if (currentPageOffset > currentPageMaxScroll) return currentPageMaxScroll; + + return currentPageOffset; +} +- (void)scrollUpCurrentPage:(int)targetOffset animating:(BOOL)animating { + + if ([self getCurrentPageOffset] > 0) + { + if (targetOffset < 0) targetOffset = 0; + + //NSLog(@"[BakerView] Scrolling page up to %d", targetOffset); + [self scrollPage:self.currPage to:[NSString stringWithFormat:@"%d", targetOffset] animating:animating]; + } +} +- (void)scrollDownCurrentPage:(int)targetOffset animating:(BOOL)animating { + + int currentPageMaxScroll = currentPageHeight - pageHeight; + if ([self getCurrentPageOffset] < currentPageMaxScroll) + { + if (targetOffset > currentPageMaxScroll) targetOffset = currentPageMaxScroll; + + //NSLog(@"[BakerView] Scrolling page down to %d", targetOffset); + [self scrollPage:self.currPage to:[NSString stringWithFormat:@"%d", targetOffset] animating:animating]; + } + +} +- (void)scrollPage:(UIWebView *)webView to:(NSString *)offset animating:(BOOL)animating { + [self hideBars:@YES]; + + NSString *jsCommand = [NSString stringWithFormat:@"window.scrollTo(0,%@);", offset]; + if (animating) { + [UIView animateWithDuration:0.35 animations:^{ [webView stringByEvaluatingJavaScriptFromString:jsCommand]; }]; + } else { + [webView stringByEvaluatingJavaScriptFromString:jsCommand]; + } +} +- (void)handleAnchor:(BOOL)animating { + if (anchorFromURL != nil) { + NSString *jsAnchorHandler = [NSString stringWithFormat:@"(function() {\ + var target = '%@';\ + var elem = document.getElementById(target);\ + if (!elem) elem = document.getElementsByName(target)[0];\ + return elem.offsetTop;\ + })();", anchorFromURL]; + + NSString *offsetString = [self.currPage stringByEvaluatingJavaScriptFromString:jsAnchorHandler]; + if (![offsetString isEqualToString:@""]) + { + int offset = [offsetString intValue]; + int currentPageOffset = [self getCurrentPageOffset]; + + if (offset > currentPageOffset) { + [self scrollDownCurrentPage:offset animating:animating]; + } else if (offset < currentPageOffset) { + [self scrollUpCurrentPage:offset animating:animating]; + } + } + + anchorFromURL = nil; + } +} + +#pragma mark - BARS VISIBILITY +- (CGRect)getNewNavigationFrame:(BOOL)hidden { + UINavigationBar *navigationBar = self.navigationController.navigationBar; + + int navX = navigationBar.frame.origin.x; + int navW = navigationBar.frame.size.width; + int navH = navigationBar.frame.size.height; + + if (hidden) { + return CGRectMake(navX, -44, navW, navH); + } else { + return CGRectMake(navX, 20, navW, navH); + } +} +- (BOOL)prefersStatusBarHidden { + return self.barsHidden; +} +- (UIStatusBarAnimation)preferredStatusBarUpdateAnimation { + return UIStatusBarAnimationSlide; +} +- (void)toggleBars { + // if modal view is up, don't toggle. + if (!self.presentedViewController) { + NSLog(@"[BakerView] Toggle bars visibility"); + + if (self.barsHidden) { + [self showBars]; + } else { + [self hideBars:@YES]; + } + } +} +- (void)showBars { + + self.barsHidden = NO; + + [UIView animateWithDuration:UINavigationControllerHideShowBarDuration animations:^{ + [self setNeedsStatusBarAppearanceUpdate]; + }]; + [self.navigationController setNavigationBarHidden:NO animated:YES]; + + if(![indexViewController isDisabled]) { + [[NSNotificationCenter defaultCenter] postNotificationName:@"BakerViewIndexOpen" object:self]; // -> Baker Analytics Event + [indexViewController setIndexViewHidden:NO withAnimation:YES]; + } +} +- (void)showNavigationBar { + + CGRect newNavigationFrame = [self getNewNavigationFrame:NO]; + UINavigationBar *navigationBar = self.navigationController.navigationBar; + + navigationBar.frame = CGRectMake(newNavigationFrame.origin.x, -24, newNavigationFrame.size.width, newNavigationFrame.size.height); + navigationBar.hidden = NO; + + [UIView animateWithDuration:0.3 + delay:0 + options:UIViewAnimationOptionCurveEaseOut + animations:^{ + navigationBar.frame = newNavigationFrame; + } + completion:nil]; +} +- (void)hideBars:(NSNumber *)animated { + + self.barsHidden = YES; + + BOOL animateHiding = [animated boolValue]; + + if (animateHiding) { + [UIView animateWithDuration:UINavigationControllerHideShowBarDuration animations:^{ + [self setNeedsStatusBarAppearanceUpdate]; + }]; + } else { + [self setNeedsStatusBarAppearanceUpdate]; + } + + [self.navigationController setNavigationBarHidden:YES animated:animateHiding]; + + if(![indexViewController isDisabled]) { + [indexViewController setIndexViewHidden:YES withAnimation:YES]; + } +} +- (void)handleBookProtocol:(NSURL *)url +{ + // ****** Handle: book:// + NSLog(@"[BakerView] Page is a link with scheme book:// --> download new book"); + if ([[url pathExtension] isEqualToString:@"html"]) { + // page --> [[url lastPathComponent] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding] + // anchor --> [url fragment]; + + url = [url URLByDeletingLastPathComponent]; + } + NSString *bookName = [[url lastPathComponent] stringByReplacingOccurrencesOfString:@".hpub" withString:@""]; + NSDictionary *userInfo = @{@"ID": bookName}; + + [[NSNotificationCenter defaultCenter] postNotificationName:@"notification_book_protocol" object:nil userInfo:userInfo]; +} + +#pragma mark - ORIENTATION +- (NSString *)getCurrentInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation { + if ([self.book.orientation isEqualToString:@"portrait"] || [self.book.orientation isEqualToString:@"landscape"]) { + return self.book.orientation; + } else { + if (UIInterfaceOrientationIsLandscape(interfaceOrientation)) { + return @"landscape"; + } else { + return @"portrait"; + } + } +} +- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation { + + BOOL appOrientation = [supportedOrientation indexOfObject:[BKRUtils stringFromInterfaceOrientation:interfaceOrientation]] != NSNotFound; + + if ([self.book.orientation isEqualToString:@"portrait"]) { + return appOrientation && UIInterfaceOrientationIsPortrait(interfaceOrientation); + } else if ([self.book.orientation isEqualToString:@"landscape"]) { + return appOrientation && UIInterfaceOrientationIsLandscape(interfaceOrientation); + } else { + return appOrientation; + } +} +- (BOOL)shouldAutorotate { + return YES; +} +- (NSUInteger)supportedInterfaceOrientations { + if ([self.book.orientation isEqualToString:@"portrait"]) { + return (UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskPortraitUpsideDown); + } else if ([self.book.orientation isEqualToString:@"landscape"]) { + return UIInterfaceOrientationMaskLandscape; + } else { + return UIInterfaceOrientationMaskAll; + } +} +- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration { + // Notify the index view + [indexViewController willRotate]; + + // Notify the current loaded views + [self webView:self.currPage setCorrectOrientation:toInterfaceOrientation]; + if (nextPage) [self webView:nextPage setCorrectOrientation:toInterfaceOrientation]; + if (prevPage) [self webView:prevPage setCorrectOrientation:toInterfaceOrientation]; + + [self setPageSize:[self getCurrentInterfaceOrientation:toInterfaceOrientation]]; + [self updateBookLayout]; +} +- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation { + [indexViewController rotateFromOrientation:fromInterfaceOrientation toOrientation:self.interfaceOrientation]; + [self setCurrentPageHeight]; +} + +- (BOOL)forceOrientationUpdate { + // We need to run this only once to prevent looping in -viewWillAppear + if (shouldForceOrientationUpdate) { + shouldForceOrientationUpdate = NO; + UIInterfaceOrientation interfaceOrientation = [[UIApplication sharedApplication] statusBarOrientation]; + + if ( (UIInterfaceOrientationIsLandscape(interfaceOrientation) && [self.book.orientation isEqualToString:@"landscape"]) + || + (UIInterfaceOrientationIsPortrait(interfaceOrientation) && [self.book.orientation isEqualToString:@"portrait"]) ) { + + //NSLog(@"[BakerView] Device and book orientations are in sync"); + return NO; + } else { + //NSLog(@"[BakerView] Device and book orientations are out of sync, force orientation update"); + + // Present and dismiss a vanilla view controller to trigger the orientation update + [self presentViewController:[UIViewController new] animated:NO completion:^{ + dispatch_after(0, dispatch_get_main_queue(), ^{ + [self dismissViewControllerAnimated:NO completion:nil]; + }); + }]; + return YES; + } + + } else { + return NO; + } +} + +#pragma mark - MEMORY +- (void)viewWillDisappear:(BOOL)animated { + [super viewWillDisappear:animated]; + NSArray *viewControllers = self.navigationController.viewControllers; + if ([viewControllers indexOfObject:self] == NSNotFound) { + // Baker book is disappearing because it was popped from the navigation stack -> Baker book is closing + [[NSNotificationCenter defaultCenter] postNotificationName:@"BakerIssueClose" object:self]; // -> Baker Analytics Event + [self saveBookStatusWithScrollIndex]; + } +} +- (void)saveBookStatusWithScrollIndex { + if (self.currPage != nil) { + bookStatus.scrollIndex = [self.currPage stringByEvaluatingJavaScriptFromString:@"window.scrollY;"]; + } + bookStatus.page = @(self.currentPageNumber); + [bookStatus save]; + //NSLog(@"[BakerView] Saved status"); +} +- (void)didReceiveMemoryWarning { + // Releases the view if it doesn't have a superview. + [super didReceiveMemoryWarning]; + // Release any cached data, images, etc that aren't in use. +} +- (void)dealloc { + + // Set web views delegates to nil, mandatory before releasing UIWebview instances + self.currPage.delegate = nil; + nextPage.delegate = nil; + prevPage.delegate = nil; + +} + +#pragma mark - MF MAIL COMPOSER +- (void)mailComposeController:(MFMailComposeViewController*)controller didFinishWithResult:(MFMailComposeResult)result error:(NSError*)error { + + // Log the result for debugging purpose + switch (result) { + case MFMailComposeResultCancelled: + NSLog(@"[BakerView] Mail cancelled."); + break; + + case MFMailComposeResultSaved: + NSLog(@"[BakerView] Mail saved."); + break; + + case MFMailComposeResultSent: + NSLog(@"[BakerView] Mail sent."); + break; + + case MFMailComposeResultFailed: + NSLog(@"[BakerView] Mail failed, check NSError."); + break; + + default: + NSLog(@"[BakerView] Mail not sent."); + break; + } + + // Remove the mail view + [self dismissViewControllerAnimated:YES completion:nil]; +} + +#pragma mark - INDEX VIEW +- (BOOL)isIndexView:(UIWebView *)webView { + if (webView == indexViewController.view) { + return YES; + } else { + return NO; + } +} + +@end diff --git a/BakerView/BKRCore.h b/BakerView/BKRCore.h new file mode 100644 index 0000000..14192bd --- /dev/null +++ b/BakerView/BKRCore.h @@ -0,0 +1,43 @@ +// +// BakerCore.h +// Baker +// +// ========================================================================================== +// +// Copyright (c) 2014, Pieter Claerhout +// All rights reserved. +// +// 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. +// Neither the name of the Baker Framework nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written +// permission. +// 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. +// + +#ifndef Baker_BKRCore_h +#define Baker_BKRCore_h + +static inline BOOL BKR_IsEmpty(id thing) { + return thing == nil || [thing isEqual:[NSNull null]] + || ([thing respondsToSelector:@selector(length)] + && [(NSData *)thing length] == 0) + || ([thing respondsToSelector:@selector(count)] + && [(NSArray *)thing count] == 0); +} + +#endif diff --git a/BakerView/BKRIndexViewController.h b/BakerView/BKRIndexViewController.h new file mode 100644 index 0000000..cd693e4 --- /dev/null +++ b/BakerView/BKRIndexViewController.h @@ -0,0 +1,76 @@ +// +// IndexViewController.h +// Baker +// +// ========================================================================================== +// +// Copyright (c) 2010-2013, Davide Casali, Marco Colombo, Alessandro Morandi +// Copyright (c) 2014, Andrew Krowczyk, Cédric Mériau, Pieter Claerhout +// All rights reserved. +// +// 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. +// Neither the name of the Baker Framework nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written +// permission. +// 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. +// + +#import +#import "BKRBook.h" + +@interface BKRIndexViewController : UIViewController { + + NSString *fileName; + UIScrollView *indexScrollView; + UIViewController *webViewDelegate; + + int pageY; + int pageWidth; + int pageHeight; + int indexWidth; + int indexHeight; + int actualIndexWidth; + int actualIndexHeight; + + BOOL disabled; + BOOL loadedFromBundle; + + CGSize cachedContentSize; +} + +@property (nonatomic, strong) BKRBook *book; + +- (id)initWithBook:(BKRBook*)bakerBook fileName:(NSString*)name webViewDelegate:(UIViewController*)delegate; +- (void)loadContent; +- (void)setBounceForWebView:(UIWebView*)webView bounces:(BOOL)bounces; +- (void)setPageSizeForOrientation:(UIInterfaceOrientation)orientation; +- (BOOL)isIndexViewHidden; +- (BOOL)isDisabled; +- (void)setIndexViewHidden:(BOOL)hidden withAnimation:(BOOL)animation; +- (void)willRotate; +- (void)rotateFromOrientation:(UIInterfaceOrientation)fromInterfaceOrientation toOrientation:(UIInterfaceOrientation)toInterfaceOrientation; +- (void)fadeOut; +- (void)fadeIn; +- (BOOL)stickToLeft; +- (CGSize)sizeFromContentOf:(UIWebView*)webView; +- (void)setActualSize; +- (void)adjustIndexView; +- (void)setViewFrame:(CGRect)frame; +- (NSString*)indexPath; + +@end diff --git a/BakerView/BKRIndexViewController.m b/BakerView/BKRIndexViewController.m new file mode 100644 index 0000000..39f75e4 --- /dev/null +++ b/BakerView/BKRIndexViewController.m @@ -0,0 +1,259 @@ +// +// IndexViewController.m +// Baker +// +// ========================================================================================== +// +// Copyright (c) 2010-2013, Davide Casali, Marco Colombo, Alessandro Morandi +// Copyright (c) 2014, Andrew Krowczyk, Cédric Mériau, Pieter Claerhout +// All rights reserved. +// +// 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. +// Neither the name of the Baker Framework nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written +// permission. +// 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. +// + +#import "BKRIndexViewController.h" +#import "BKRBookViewController.h" + +#import "UIScreen+BakerExtensions.h" + +@implementation BKRIndexViewController + +#pragma mark - Initialization + +- (id)initWithBook:(BKRBook*)bakerBook fileName:(NSString*)name webViewDelegate:(UIViewController*)delegate { + self = [super init]; + if (self) { + + _book = bakerBook; + + fileName = name; + webViewDelegate = delegate; + + disabled = NO; + indexWidth = 0; + indexHeight = 0; + + [self setPageSizeForOrientation:[UIApplication sharedApplication].statusBarOrientation]; + + } + return self; +} + +#pragma mark - View lifecycle + +- (void)loadView { + + // Initialization to 1x1px is required to get sizeThatFits to work + UIWebView *webView = [[UIWebView alloc] initWithFrame:CGRectMake(0, 1024, 1, 1)]; + webView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth; + webView.delegate = self; + webView.backgroundColor = [UIColor clearColor]; + webView.opaque = NO; + + self.view = webView; + for (UIView *subView in webView.subviews) { + if ([subView isKindOfClass:[UIScrollView class]]) { + indexScrollView = (UIScrollView *)subView; + break; + } + } + + [self loadContent]; + +} + +- (void)setBounceForWebView:(UIWebView*)webView bounces:(BOOL)bounces { + indexScrollView.bounces = bounces; +} + +- (void)setPageSizeForOrientation:(UIInterfaceOrientation)orientation { + pageWidth = [[UIScreen mainScreen] bkrWidthForOrientation:orientation]; + pageHeight = [[UIScreen mainScreen] bkrHeightForOrientation:orientation]; + NSLog(@"[IndexView] Set IndexView size to %dx%d", pageWidth, pageHeight); +} + +- (void)setActualSize { + actualIndexWidth = MIN(indexWidth, pageWidth); + actualIndexHeight = MIN(indexHeight, pageHeight); +} + +- (BOOL)isIndexViewHidden { + return ((BKRBookViewController*) webViewDelegate).barsHidden; +} + +- (BOOL)isDisabled { + return disabled; +} + +- (void)setIndexViewHidden:(BOOL)hidden withAnimation:(BOOL)animation { + CGRect frame; + if (hidden) { + if ([self stickToLeft]) { + frame = CGRectMake(-actualIndexWidth, [self trueY] + pageHeight - actualIndexHeight, actualIndexWidth, actualIndexHeight); + } else { + frame = CGRectMake(0, [self trueY] + pageHeight, actualIndexWidth, actualIndexHeight); + } + } else { + if ([self stickToLeft]) { + frame = CGRectMake(0, [self trueY] + pageHeight - actualIndexHeight, actualIndexWidth, actualIndexHeight); + } else { + frame = CGRectMake(0, [self trueY] + pageHeight - indexHeight, actualIndexWidth, actualIndexHeight); + } + } + + if (animation) { + [UIView beginAnimations:@"slideIndexView" context:nil]; { + [UIView setAnimationDuration:0.3]; + [self setViewFrame:frame]; + } + [UIView commitAnimations]; + } else { + [self setViewFrame:frame]; + } + +} + +- (int)trueY { + // Sometimes the origin (0,0) is not where it should be: this horrible hack + // compensates for it, by exploiting the fact that the superview height is + // slightly smaller then the viewport height when the origin's y needs to be adjusted. + int height = self.view.superview.frame.size.height; + if (height == 320 || height == 375 || height == 414 || height == 480 || height == 568 || height == 667 || height == 736 || height == 768 || height == 1024) { + return 0; + } else { + return -20; + } +} + +- (void)setViewFrame:(CGRect)frame { + self.view.frame = frame; + indexScrollView.contentSize = cachedContentSize; +} + +- (void)fadeOut { + [UIView beginAnimations:@"fadeOutIndexView" context:nil]; { + [UIView setAnimationDuration:0.0]; + self.view.alpha = 0.0; + } + [UIView commitAnimations]; +} + +- (void)fadeIn { + [UIView beginAnimations:@"fadeInIndexView" context:nil]; { + [UIView setAnimationDuration:0.2]; + self.view.alpha = 1.0; + } + [UIView commitAnimations]; +} + +- (void)willRotate { + [self fadeOut]; +} + +- (void)rotateFromOrientation:(UIInterfaceOrientation)fromInterfaceOrientation toOrientation:(UIInterfaceOrientation)toInterfaceOrientation { + BOOL hidden = [self isIndexViewHidden]; // cache hidden status before setting page size + + [self setPageSizeForOrientation:toInterfaceOrientation]; + [self setActualSize]; + [self setIndexViewHidden:hidden withAnimation:NO]; + [self fadeIn]; +} + +- (void)adjustIndexView { + [self setPageSizeForOrientation:[UIApplication sharedApplication].statusBarOrientation]; + [self setActualSize]; + [self setIndexViewHidden:self.isIndexViewHidden withAnimation:NO]; +} + +- (void)loadContent { + NSString* path = self.indexPath; + + UIWebView *webView = (UIWebView*)self.view; + webView.mediaPlaybackRequiresUserAction = ![self.book.bakerMediaAutoplay boolValue]; + [self setBounceForWebView:webView bounces:[self.book.bakerIndexBounce boolValue]]; + + //NSLog(@"[IndexView] Path to index view is %@", path); + if ([[NSFileManager defaultManager] fileExistsAtPath:path]) { + disabled = NO; + [(UIWebView *)self.view loadRequest:[NSURLRequest requestWithURL:[NSURL fileURLWithPath:path]]]; + } else { + NSLog(@"[IndexView] Index HTML not found at %@", path); + disabled = YES; + } +} + +- (void)webViewDidFinishLoad:(UIWebView*)webView { + id width = self.book.bakerIndexWidth; + id height = self.book.bakerIndexHeight; + + if (width != nil) { + indexWidth = (int)[width integerValue]; + } else { + indexWidth = [self sizeFromContentOf:webView].width; + } + + if (height != nil) { + indexHeight = (int)[height integerValue]; + } else { + indexHeight = [self sizeFromContentOf:webView].height; + } + + cachedContentSize = indexScrollView.contentSize; + if (cachedContentSize.width < indexWidth) { + cachedContentSize = CGSizeMake(indexWidth, indexHeight); + } + [self setActualSize]; + + NSLog(@"[IndexView] Set size for IndexView to %dx%d (constrained from %dx%d)", actualIndexWidth, actualIndexHeight, indexWidth, indexHeight); + + // After the first load, point the delegate to the main view controller + webView.delegate = webViewDelegate; + + [self setIndexViewHidden:[self isIndexViewHidden] withAnimation:NO]; +} + +- (BOOL)stickToLeft { + return (actualIndexHeight > actualIndexWidth); +} + +- (CGSize)sizeFromContentOf:(UIWebView*)webView { + /* + // Setting the frame to 1x1 is required to get meaningful results from sizeThatFits when + // the orientation of the is anything but Portrait. + // See: http://stackoverflow.com/questions/3936041/how-to-determine-the-content-size-of-a-uiwebview/3937599#3937599 + CGRect frame = view.frame; + frame.size.width = 1; + frame.size.height = 1; + view.frame = frame; + return [view sizeThatFits:CGSizeZero]; + */ + + CGFloat contentWidth = [[webView stringByEvaluatingJavaScriptFromString:@"document.body.scrollWidth"] floatValue]; + CGFloat contentHeight = [[webView stringByEvaluatingJavaScriptFromString:@"document.body.scrollHeight"] floatValue]; + return CGSizeMake(contentWidth, contentHeight); +} + +- (NSString*)indexPath { + return [self.book.path stringByAppendingPathComponent:fileName]; +} + +@end diff --git a/BakerView/BKRInterceptorWindow.h b/BakerView/BKRInterceptorWindow.h new file mode 100644 index 0000000..c3d413d --- /dev/null +++ b/BakerView/BKRInterceptorWindow.h @@ -0,0 +1,41 @@ +// +// InterceptorWindow.h +// Baker +// +// ========================================================================================== +// +// Copyright (c) 2010-2013, Davide Casali, Marco Colombo, Alessandro Morandi +// Copyright (c) 2014, Andrew Krowczyk, Cédric Mériau, Pieter Claerhout +// All rights reserved. +// +// 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. +// Neither the name of the Baker Framework nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written +// permission. +// 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. +// + +#import + +@interface BKRInterceptorWindow : UIWindow + +#pragma mark - Events management + +- (void)interceptEvent:(UIEvent*)event; + +@end diff --git a/BakerView/BKRInterceptorWindow.m b/BakerView/BKRInterceptorWindow.m new file mode 100644 index 0000000..903ec20 --- /dev/null +++ b/BakerView/BKRInterceptorWindow.m @@ -0,0 +1,56 @@ +// +// InterceptorWindow.m +// Baker +// +// ========================================================================================== +// +// Copyright (c) 2010-2013, Davide Casali, Marco Colombo, Alessandro Morandi +// Copyright (c) 2014, Andrew Krowczyk, Cédric Mériau, Pieter Claerhout +// All rights reserved. +// +// 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. +// Neither the name of the Baker Framework nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written +// permission. +// 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. +// + +#import "BKRInterceptorWindow.h" + +@implementation BKRInterceptorWindow + +#pragma mark - Events management + +- (void)sendEvent:(UIEvent*)event { + [super sendEvent:event]; + [self interceptEvent:event]; +} + +- (void)interceptEvent:(UIEvent*)event { + if (event.type == UIEventTypeTouches) { + NSSet *touches = event.allTouches; + if (touches.count == 1) { + [[NSNotificationCenter defaultCenter] postNotificationName:@"notification_touch_intercepted" + object:nil + userInfo:@{@"touch": touches.anyObject} + ]; + } + } +} + +@end diff --git a/BakerView/BKRModalWebViewController.h b/BakerView/BKRModalWebViewController.h new file mode 100755 index 0000000..1220ecf --- /dev/null +++ b/BakerView/BKRModalWebViewController.h @@ -0,0 +1,63 @@ +// +// ModalViewController.h +// Baker +// +// ========================================================================================== +// +// Copyright (c) 2010-2013, Davide Casali, Marco Colombo, Alessandro Morandi +// Copyright (c) 2014, Andrew Krowczyk, Cédric Mériau, Pieter Claerhout +// All rights reserved. +// +// 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. +// Neither the name of the Baker Framework nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written +// permission. +// 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. +// + +#import + +@protocol BKRModalWebViewControllerDelegate; + +@interface BKRModalWebViewController : UIViewController + +@property (nonatomic, strong) NSURL *initialURL; +@property (nonatomic, weak) id delegate; +@property (nonatomic, strong) UIWebView *webView; +@property (nonatomic, strong) UIToolbar *toolbar; +@property (nonatomic, strong) UIBarButtonItem *btnGoBack; +@property (nonatomic, strong) UIBarButtonItem *btnGoForward; +@property (nonatomic, strong) UIBarButtonItem *btnReload; +@property (nonatomic, strong) UIActivityIndicatorView *spinner; + +- (id)initWithURL:(NSURL*)url; +- (void)dismissAction; +- (void)goBack; +- (void)goForward; +- (void)reloadPage; +- (void)openInSafari; + +@end + +@protocol BKRModalWebViewControllerDelegate + +- (void)closeModalWebView; +- (void)webView:(UIWebView *)webView setCorrectOrientation:(UIInterfaceOrientation)interfaceOrientation; +- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation; + +@end \ No newline at end of file diff --git a/BakerView/BKRModalWebViewController.m b/BakerView/BKRModalWebViewController.m new file mode 100755 index 0000000..53131e9 --- /dev/null +++ b/BakerView/BKRModalWebViewController.m @@ -0,0 +1,228 @@ +// +// ModalViewController.m +// Baker +// +// ========================================================================================== +// +// Copyright (c) 2010-2013, Davide Casali, Marco Colombo, Alessandro Morandi +// Copyright (c) 2014, Andrew Krowczyk, Cédric Mériau, Pieter Claerhout +// All rights reserved. +// +// 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. +// Neither the name of the Baker Framework nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written +// permission. +// 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. +// +// ========================================================================================== +// + +#import "BKRModalWebViewController.h" +#import "UIColor+BakerExtensions.h" +#import "BKRUtils.h" +#import "BKRSettings.h" + +#import "UIScreen+BakerExtensions.h" + +@implementation BKRModalWebViewController + +#pragma mark - Initialization + +- (id)initWithURL:(NSURL*)url { + self = [super init]; + if (self) { + _initialURL = url; + } + return self; +} + +#pragma mark - View Lifecycle + +- (void)loadView { + + [super loadView]; + + // ****** Buttons + UIBarButtonItem *btnClose = [[UIBarButtonItem alloc] initWithTitle:NSLocalizedString(@"WEB_MODAL_CLOSE_BUTTON_TEXT", nil) + style:UIBarButtonItemStyleBordered + target:self + action:@selector(dismissAction)]; + + UIBarButtonItem *btnAction = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAction target:self action:@selector(openInSafari)]; + + self.btnGoBack = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"back"] style:UIBarButtonItemStylePlain target:self action:@selector(goBack)]; + self.btnGoBack.enabled = NO; + self.btnGoBack.width = 30; + + self.btnGoForward = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"forward"] style:UIBarButtonItemStylePlain target:self action:@selector(goForward)]; + self.btnGoForward.enabled = NO; + self.btnGoForward.width = 30; + + self.btnReload = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemRefresh target:self action:@selector(reloadPage)]; + self.btnReload.enabled = NO; + self.btnGoForward.width = 30; + + btnClose.tintColor = [UIColor bkrColorWithHexString:[BKRSettings sharedSettings].issuesActionBackgroundColor]; + btnAction.tintColor = [UIColor bkrColorWithHexString:[BKRSettings sharedSettings].issuesActionBackgroundColor]; + self.btnGoBack.tintColor = [UIColor bkrColorWithHexString:[BKRSettings sharedSettings].issuesActionBackgroundColor]; + self.btnGoForward.tintColor = [UIColor bkrColorWithHexString:[BKRSettings sharedSettings].issuesActionBackgroundColor]; + self.btnReload.tintColor = [UIColor bkrColorWithHexString:[BKRSettings sharedSettings].issuesActionBackgroundColor]; + + self.spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; + self.spinner.frame = CGRectMake(3, 3, 25, 25); + self.spinner.hidesWhenStopped = YES; + + [self.spinner startAnimating]; + + UIBarButtonItem *btnSpinner = [[UIBarButtonItem alloc] initWithCustomView:self.spinner]; + btnSpinner.width = 30; + + UIBarButtonItem *spacer = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil]; + + // ****** Add Toolbar + self.toolbar = [UIToolbar new]; + self.toolbar.barStyle = UIBarStyleDefault; + + // ****** Add items to toolbar + if ([self.initialURL.scheme isEqualToString:@"file"]) { + NSArray *items = @[btnClose, self.btnGoBack, self.btnGoForward, btnSpinner, spacer]; + [self.toolbar setItems:items animated:NO]; + } else { + NSArray *items = @[btnClose, self.btnGoBack, self.btnGoForward, self.btnReload, btnSpinner, spacer, btnAction]; + [self.toolbar setItems:items animated:NO]; + } + + // ****** Add WebView + self.webView = [[UIWebView alloc] initWithFrame:CGRectMake(0, 44, 1, 1)]; + self.webView.contentMode = UIViewContentModeScaleToFill; + self.webView.scalesPageToFit = YES; + self.webView.delegate = self; + + // ****** View + self.view = [UIView new]; + + // ****** Attach + [self.view addSubview:self.toolbar]; + [self.view addSubview:self.webView]; + + // ****** Set views starting frames according to current interface rotation + [self willRotateToInterfaceOrientation:self.interfaceOrientation duration:0]; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + self.view.backgroundColor = [UIColor whiteColor]; + [self.webView loadRequest:[NSURLRequest requestWithURL:self.initialURL]]; +} + +- (void)dealloc { + [self.webView stopLoading]; + [self.webView removeFromSuperview]; + self.webView.delegate = nil; +} + +- (BOOL)prefersStatusBarHidden { + return YES; +} + +#pragma mark - UIWebViewDelegate + +- (void)webViewDidStartLoad:(UIWebView*)webViewIn { + // NSLog(@"[Modal] Loading '%@'", [webViewIn.request.URL absoluteString]); <-- this isn't returning the URL correctly, check + [self.spinner startAnimating]; +} + +- (void)webViewDidFinishLoad:(UIWebView*)webViewIn { + + //NSLog(@"[Modal] Finish loading."); + [self.delegate webView:webViewIn setCorrectOrientation:self.interfaceOrientation]; + + [self.spinner stopAnimating]; + + self.btnGoBack.enabled = [webViewIn canGoBack]; + self.btnGoForward.enabled = [webViewIn canGoForward]; + self.btnReload.enabled = YES; + +} + +- (void)webView:(UIWebView*)webViewIn didFailLoadWithError:(NSError*)error { + NSLog(@"[Modal] Failed to load '%@', error code %li", [webViewIn.request.URL absoluteString], (long)error.code); + if (error.code == -1009) { + + UILabel *errorLabel = [[UILabel alloc] initWithFrame:self.webView.frame]; + errorLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + errorLabel.textAlignment = NSTextAlignmentCenter; + errorLabel.textColor = [UIColor grayColor]; + errorLabel.text = NSLocalizedString(@"WEB_MODAL_FAILURE_MESSAGE", nil); + errorLabel.numberOfLines = 1; + + CGRect screenBounds = [[UIScreen mainScreen] bounds]; + if (MIN(screenBounds.size.width, screenBounds.size.height) < 768) { + errorLabel.font = [UIFont fontWithName:@"Helvetica" size:14.0]; + } else { + errorLabel.font = [UIFont fontWithName:@"Helvetica" size:18.0]; + } + + [self.view addSubview:errorLabel]; + } + + [self.spinner stopAnimating]; +} + +#pragma mark - Actions + +- (void)dismissAction { + [[self delegate] closeModalWebView]; +} + +- (void)goBack { + [self.webView goBack]; +} + +- (void)goForward { + [self.webView goForward]; +} + +- (void)reloadPage { + [self.webView reload]; +} + +- (void)openInSafari { + [[UIApplication sharedApplication] openURL:self.webView.request.URL]; +} + +#pragma mark - Orientation + +- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)orientation { + return [self.delegate shouldAutorotateToInterfaceOrientation:orientation]; +} + +- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration { + + CGFloat screenWidth = [[UIScreen mainScreen] bkrWidthForOrientation:toInterfaceOrientation]; + CGFloat screenHeight = [[UIScreen mainScreen] bkrHeightForOrientation:toInterfaceOrientation]; + + self.view.frame = CGRectMake(0, 0, screenWidth, screenHeight); + self.toolbar.frame = CGRectMake(0, 0, screenWidth, 44); + self.webView.frame = CGRectMake(0, 44, screenWidth, screenHeight - 44); + + [self.delegate webView:self.webView setCorrectOrientation:toInterfaceOrientation]; + +} + +@end diff --git a/BakerView/BKRSettings.h b/BakerView/BKRSettings.h new file mode 100644 index 0000000..fe9a86b --- /dev/null +++ b/BakerView/BKRSettings.h @@ -0,0 +1,123 @@ +// +// BKRSettings.h +// Baker +// +// ========================================================================================== +// +// Copyright (c) 2014, Pieter Claerhout +// All rights reserved. +// +// 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. +// Neither the name of the Baker Framework nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written +// permission. +// 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. +// + +#import + +@interface BKRSettings : NSObject + +// Timeout for most network requests (in seconds) +@property (nonatomic, readonly) NSTimeInterval requestTimeout; + +@property (nonatomic, readonly) BOOL isNewsstand; + +// ---------------------------------------------------------------------------------------------------- +// Mandatory - This constant defines where the JSON file containing all the publications is located. +// For more information on this file, see: https://github.com/Simbul/baker/wiki/Newsstand-shelf-JSON +// E.g. @"http://example.com/shelf.json" +@property (nonatomic, readonly) NSString *newsstandManifestUrl; + +@property (nonatomic, readonly) BOOL newsstandLatestIssueCover; + +// ---------------------------------------------------------------------------------------------------- +// Optional - This constant specifies the URL to ping back when a user purchases an issue or a subscription. +// For more information, see: https://github.com/Simbul/baker/wiki/Baker-Server-API +// E.g. @"http://example.com/purchased" +@property (nonatomic, readonly) NSString *purchaseConfirmationUrl; + +// ---------------------------------------------------------------------------------------------------- +// Optional - This constant specifies a URL that will be used to retrieve the list of purchased issues. +// For more information, see: https://github.com/Simbul/baker/wiki/Baker-Server-API +// E.g. @"http://example.com/purchases" +@property (nonatomic, readonly) NSString *purchasesUrl; + +// ---------------------------------------------------------------------------------------------------- +// Optional - This constant specifies the URL to ping back when a user enables push notifications. +// For more information, see: https://github.com/Simbul/baker/wiki/Baker-Server-API +// E.g. @"http://example.com/post_apns_token" +@property (nonatomic, readonly) NSString *postApnsTokenUrl; + +// ---------------------------------------------------------------------------------------------------- +// Mandatory - The following two constants identify the subscriptions you set up in iTunesConnect. +// See: iTunes Connect -> Manage Your Application -> (Your application) -> Manage In App Purchases + +// This constant identifies a free subscription. +// E.g. @"com.example.MyBook.subscription.free" +@property (nonatomic, readonly) NSString *freeSubscriptionProductId; + +// This constant identifies one or more auto-renewable subscriptions. +@property (nonatomic, readonly) NSArray *autoRenewableSubscriptionProductIds; + +// Pull subscriptions localisations from iTunes or localizedString +@property (nonatomic, readonly) BOOL useiTunesConnectLocalizations; + +// Background color for issues cover (before downloading the actual cover) +@property (nonatomic, readonly) NSString *issuesCoverBackgroundColor; + +// Title for issues in the shelf +@property (nonatomic, readonly) NSString *issuesTitleFont; +@property (nonatomic, readonly) int issuesTitleFontSize; +@property (nonatomic, readonly) NSString *issuesTitleColor; + +// Info text for issues in the shelf +@property (nonatomic, readonly) NSString *issuesInfoFont; +@property (nonatomic, readonly) int issuesInfoFontSize; +@property (nonatomic, readonly) NSString *issuesInfoColor; + +@property (nonatomic, readonly) NSString *issuesPriceColor; + +// Download/read button for issues in the shelf +@property (nonatomic, readonly) NSString *issuesActionFont; +@property (nonatomic, readonly) int issuesActionFontSize; +@property (nonatomic, readonly) NSString *issuesActionBackgroundColor; +@property (nonatomic, readonly) NSString *issuesActionButtonColor; + +// Archive button for issues in the shelf +@property (nonatomic, readonly) NSString *issuesArchiveFont; +@property (nonatomic, readonly) int issuesArchiveFontSize; +@property (nonatomic, readonly) NSString *issuesArchiveBackgroundColor; +@property (nonatomic, readonly) NSString *issuesArchiveButtonColor; + +// Text and spinner for issues that are being loaded in the shelf +@property (nonatomic, readonly) NSString *issuesLoadingLabelColor; +@property (nonatomic, readonly) NSString *issuesLoadingSpinnerColor; + +// Progress bar for issues that are being downloaded in the shelf +@property (nonatomic, readonly) NSString *issuesProgressbarTintColor; + +// Shelf background customization +@property (nonatomic, readonly) NSDictionary *issuesShelfOptions; + +// Social Share Button +@property (nonatomic, readonly) BOOL showSocialShareButton; + ++ (BKRSettings*)sharedSettings; + +@end diff --git a/BakerView/BKRSettings.m b/BakerView/BKRSettings.m new file mode 100644 index 0000000..3898657 --- /dev/null +++ b/BakerView/BKRSettings.m @@ -0,0 +1,189 @@ +// +// BKRSettings.h +// Baker +// +// ========================================================================================== +// +// Copyright (c) 2014, Pieter Claerhout +// All rights reserved. +// +// 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. +// Neither the name of the Baker Framework nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written +// permission. +// 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. +// + +// +// !! IMPORTANT !! +// DO NOT ALTER THIS FILE. SETTINGS ARE DEFINED IN SETTINGS.PLIST INSTEAD! +// !! IMPORTANT !! +// + +#import "BKRSettings.h" + +#pragma mark - Private + +@interface BKRSettings () + +@property (nonatomic, readonly) NSDictionary *settings; + +@end + +@implementation BKRSettings + +#pragma mark - Shared Instance + ++ (BKRSettings*)sharedSettings { + static BKRSettings *_sharedInstance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + _sharedInstance = [[self alloc] init]; + }); + return _sharedInstance; +} + +#pragma mark - Instance Methods + +- (id)init { + self = [super init]; + if (self) { + + NSString *settingsPath = [[NSBundle mainBundle] pathForResource:@"settings" ofType:@"plist"]; + if ([[NSFileManager defaultManager] fileExistsAtPath:settingsPath]) { + _settings = [NSDictionary dictionaryWithContentsOfFile:settingsPath]; + } else { + _settings = @{}; + } + + NSLog(@"Settings: %@", _settings); + + _requestTimeout = [self doubleSettingForKey:@"requestTimeout" withDefault:15]; + _isNewsstand = [self boolSettingForKey:@"isNewsstand" withDefault:YES]; + _newsstandLatestIssueCover = [self boolSettingForKey:@"newsstandLatestIssueCover" withDefault:YES]; + + _newsstandManifestUrl = [self stringSettingForKey:@"newsstandManifestUrl" withDefault:@"http://bakerframework.com/demo/shelf.json"]; + _purchaseConfirmationUrl = [self stringSettingForKey:@"purchaseConfirmationUrl" withDefault:@""]; + _purchasesUrl = [self stringSettingForKey:@"purchasesUrl" withDefault:@""]; + _postApnsTokenUrl = [self stringSettingForKey:@"postApnsTokenUrl" withDefault:@""]; + _freeSubscriptionProductId = [self stringSettingForKey:@"freeSubscriptionProductId" withDefault:@""]; + _autoRenewableSubscriptionProductIds = [self arraySettingForKey:@"autoRenewableSubscriptionProductIds" withDefault:@[]]; + + _useiTunesConnectLocalizations = [self boolSettingForKey:@"useiTunesConnectLocalizations" withDefault:YES]; + + _issuesCoverBackgroundColor = [self stringSettingForKey:@"issuesCoverBackgroundColor" withDefault:@"#ffffff"]; + + _issuesTitleFont = [self stringSettingForKey:@"issuesTitleFont" withDefault:@"Helvetica"]; + _issuesTitleFontSize = [self intSettingForKey:@"issuesTitleFontSize" withDefault:15]; + _issuesTitleColor = [self stringSettingForKey:@"issuesTitleColor" withDefault:@"#000000"]; + + _issuesInfoFont = [self stringSettingForKey:@"issuesInfoFont" withDefault:@"Helvetica"]; + _issuesInfoFontSize = [self intSettingForKey:@"issuesInfoFontSize" withDefault:15]; + _issuesInfoColor = [self stringSettingForKey:@"issuesInfoColor" withDefault:@"#929292"]; + + _issuesPriceColor = [self stringSettingForKey:@"issuesPriceColor" withDefault:@"#bc242a"]; + + _issuesActionFont = [self stringSettingForKey:@"issuesActionFont" withDefault:@"Helvetica-Bold"]; + _issuesActionFontSize = [self intSettingForKey:@"issuesActionFontSize" withDefault:11]; + _issuesActionBackgroundColor = [self stringSettingForKey:@"issuesActionBackgroundColor" withDefault:@"#bc242a"]; + _issuesActionButtonColor = [self stringSettingForKey:@"issuesActionButtonColor" withDefault:@"#ffffff"]; + + _issuesArchiveFont = [self stringSettingForKey:@"issuesArchiveFont" withDefault:@"Helvetica-Bold"]; + _issuesArchiveFontSize = [self intSettingForKey:@"issuesArchiveFontSize" withDefault:11]; + _issuesArchiveBackgroundColor = [self stringSettingForKey:@"issuesArchiveBackgroundColor" withDefault:@"#bc242a"]; + _issuesArchiveButtonColor = [self stringSettingForKey:@"issuesArchiveButtonColor" withDefault:@"#ffffff"]; + + _issuesLoadingLabelColor = [self stringSettingForKey:@"issuesLoadingLabelColor" withDefault:@"#bc242a"]; + _issuesLoadingSpinnerColor = [self stringSettingForKey:@"issuesLoadingSpinnerColor" withDefault:@"#929292"]; + + _issuesProgressbarTintColor = [self stringSettingForKey:@"issuesProgressbarTintColor" withDefault:@"#bc242a"]; + + _issuesShelfOptions = [self dictionarySettingForKey:@"issuesShelfOptions" withDefault:@{}]; + _showSocialShareButton = [self boolSettingForKey:@"showSocialShareButton" withDefault:NO]; + + } + return self; +} + +#pragma mark - Helpers + +- (NSString*)stringSettingForKey:(NSString*)setting withDefault:(NSString*)defaultValue { + if (self.settings && [self.settings objectForKey:setting]) { + return [self.settings objectForKey:setting]; + } else { + return defaultValue; + } +} + +- (NSDictionary*)dictionarySettingForKey:(NSString*)setting withDefault:(NSDictionary*)defaultValue { + if (self.settings && [self.settings objectForKey:setting]) { + return [self.settings objectForKey:setting]; + } else { + return defaultValue; + } +} + +- (NSArray*)arraySettingForKey:(NSString*)setting withDefault:(NSArray*)defaultValue { + if (self.settings && [self.settings objectForKey:setting]) { + return [self.settings objectForKey:setting]; + } else { + return defaultValue; + } +} + +- (NSDate*)dateSettingForKey:(NSString*)setting withDefault:(NSDate*)defaultValue { + if (self.settings && [self.settings objectForKey:setting]) { + return [self.settings objectForKey:setting]; + } else { + return defaultValue; + } +} + +- (NSNumber*)numberSettingForKey:(NSString*)setting withDefault:(NSNumber*)defaultValue { + if (self.settings && [self.settings objectForKey:setting]) { + return [self.settings objectForKey:setting]; + } else { + return defaultValue; + } +} + +- (BOOL)boolSettingForKey:(NSString*)setting withDefault:(BOOL)defaultValue { + if (self.settings && [self.settings objectForKey:setting]) { + return [[self.settings objectForKey:setting] boolValue]; + } else { + return defaultValue; + } +} + +- (int)intSettingForKey:(NSString*)setting withDefault:(int)defaultValue { + if (self.settings && [self.settings objectForKey:setting]) { + return [[self.settings objectForKey:setting] intValue]; + } else { + return defaultValue; + } +} + +- (double)doubleSettingForKey:(NSString*)setting withDefault:(double)defaultValue { + if (self.settings && [self.settings objectForKey:setting]) { + return [[self.settings objectForKey:setting] doubleValue]; + } else { + return defaultValue; + } +} + +@end diff --git a/BakerView/BKRShelfViewLayout.h b/BakerView/BKRShelfViewLayout.h new file mode 100644 index 0000000..a1c3683 --- /dev/null +++ b/BakerView/BKRShelfViewLayout.h @@ -0,0 +1,18 @@ +// +// BKRShelfViewLayout.h +// Baker +// +// Created by Tobias Strebitzer on 10/11/14. +// +// + +#import + +@interface BKRShelfViewLayout : UICollectionViewFlowLayout + +@property (nonatomic, readonly) BOOL isSticky; +@property (nonatomic, readonly) BOOL isStretch; + +- (id)initWithSticky:(BOOL)sticky stretch:(BOOL)stretch; + +@end diff --git a/BakerView/BKRShelfViewLayout.m b/BakerView/BKRShelfViewLayout.m new file mode 100644 index 0000000..eab78e3 --- /dev/null +++ b/BakerView/BKRShelfViewLayout.m @@ -0,0 +1,132 @@ +// +// BKRShelfViewLayout.m +// Baker +// +// Created by Tobias Strebitzer on 10/11/14. +// +// + +#import "BKRShelfViewLayout.h" + +@implementation BKRShelfViewLayout + +- (id)initWithSticky:(BOOL)sticky stretch:(BOOL)stretch { + + self = [super init]; + if (self) { + _isSticky = sticky; + _isStretch = stretch; + } + + return self; +} + +- (NSArray *) layoutAttributesForElementsInRect:(CGRect)rect { + + // Get attributes + NSMutableArray *attributes = [[super layoutAttributesForElementsInRect:rect] mutableCopy]; + + // Sticky header + if(_isSticky) { + NSMutableIndexSet *missingSections = [NSMutableIndexSet indexSet]; + for (NSUInteger idx=0; idx<[attributes count]; idx++) { + UICollectionViewLayoutAttributes *layoutAttributes = attributes[idx]; + + if (layoutAttributes.representedElementCategory == UICollectionElementCategoryCell) { + [missingSections addIndex:layoutAttributes.indexPath.section]; + } + if ([layoutAttributes.representedElementKind isEqualToString:UICollectionElementKindSectionHeader]) { + [attributes removeObjectAtIndex:idx]; + idx--; + } + } + + // layout all headers needed for the rect + [missingSections enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) { + NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:idx]; + UICollectionViewLayoutAttributes *layoutAttributes = [self layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionHeader atIndexPath:indexPath]; + if (layoutAttributes != nil) { + [attributes addObject:layoutAttributes]; + } + }]; + } + + // Stretch header + if(_isStretch) { + // Calculate offset + UICollectionView *collectionView = [self collectionView]; + UIEdgeInsets insets = [collectionView contentInset]; + CGPoint offset = [collectionView contentOffset]; + CGFloat minY = -insets.top; + + // Check if we've pulled below past the lowest position + if (offset.y < minY) { + + // Figure out how much we've pulled down + CGFloat deltaY = fabs(offset.y - minY); + + for (UICollectionViewLayoutAttributes *attrs in attributes) { + + // Locate the header attributes + NSString *kind = [attrs representedElementKind]; + if (kind == UICollectionElementKindSectionHeader) { + + // Adjust the header's height and y based on how much the user + // has pulled down. + CGSize headerSize = [self headerReferenceSize]; + CGRect headerRect = [attrs frame]; + headerRect.size.height = MAX(minY, headerSize.height + deltaY); + headerRect.origin.y = headerRect.origin.y - deltaY; + [attrs setFrame:headerRect]; + break; + } + } + } + } + + return attributes; +} + +- (UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryViewOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath { + UICollectionViewLayoutAttributes *attributes = [super layoutAttributesForSupplementaryViewOfKind:kind atIndexPath:indexPath]; + if ([kind isEqualToString:UICollectionElementKindSectionHeader]) { + UICollectionView * const cv = self.collectionView; + CGPoint const contentOffset = cv.contentOffset; + CGPoint nextHeaderOrigin = CGPointMake(INFINITY, INFINITY); + + if (indexPath.section+1 < [cv numberOfSections]) { + UICollectionViewLayoutAttributes *nextHeaderAttributes = [super layoutAttributesForSupplementaryViewOfKind:kind atIndexPath:[NSIndexPath indexPathForItem:0 inSection:indexPath.section+1]]; + nextHeaderOrigin = nextHeaderAttributes.frame.origin; + } + + CGRect frame = attributes.frame; + if (self.scrollDirection == UICollectionViewScrollDirectionVertical) { + frame.origin.y = MIN(MAX(contentOffset.y, frame.origin.y), nextHeaderOrigin.y - CGRectGetHeight(frame)); + } + else { // UICollectionViewScrollDirectionHorizontal + frame.origin.x = MIN(MAX(contentOffset.x, frame.origin.x), nextHeaderOrigin.x - CGRectGetWidth(frame)); + } + attributes.zIndex = 1024; + attributes.frame = frame; + } + return attributes; +} + +- (UICollectionViewLayoutAttributes *)initialLayoutAttributesForAppearingSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath { + UICollectionViewLayoutAttributes *attributes = [self layoutAttributesForSupplementaryViewOfKind:kind atIndexPath:indexPath]; + return attributes; +} +- (UICollectionViewLayoutAttributes *)finalLayoutAttributesForDisappearingSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath { + UICollectionViewLayoutAttributes *attributes = [self layoutAttributesForSupplementaryViewOfKind:kind atIndexPath:indexPath]; + return attributes; +} + +- (BOOL) shouldInvalidateLayoutForBoundsChange:(CGRect)newBound { + return YES; +} + +- (UICollectionViewScrollDirection)scrollDirection { + return UICollectionViewScrollDirectionVertical; +} + +@end diff --git a/BakerView/lib/BKRJSONStatus.h b/BakerView/lib/BKRJSONStatus.h new file mode 100644 index 0000000..7115cea --- /dev/null +++ b/BakerView/lib/BKRJSONStatus.h @@ -0,0 +1,43 @@ +// +// NSString+Extensions.h +// Baker +// +// ========================================================================================== +// +// Copyright (c) 2010-2013, Davide Casali, Marco Colombo, Alessandro Morandi +// Copyright (c) 2014, Andrew Krowczyk, Cédric Mériau, Pieter Claerhout +// All rights reserved. +// +// 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. +// Neither the name of the Baker Framework nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written +// permission. +// 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. +// + +#import + +@interface BKRJSONStatus : NSObject + +@property (nonatomic, copy) NSString *path; + +- (id)initWithJSONPath:(NSString*)JSONPath; +- (void)save:(NSDictionary*)status; +- (NSDictionary*)load; + +@end diff --git a/BakerView/lib/BKRJSONStatus.m b/BakerView/lib/BKRJSONStatus.m new file mode 100644 index 0000000..85da2a7 --- /dev/null +++ b/BakerView/lib/BKRJSONStatus.m @@ -0,0 +1,100 @@ +// +// NSString+Extensions.h +// Baker +// +// ========================================================================================== +// +// Copyright (c) 2010-2013, Davide Casali, Marco Colombo, Alessandro Morandi +// Copyright (c) 2014, Andrew Krowczyk, Cédric Mériau, Pieter Claerhout +// All rights reserved. +// +// 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. +// Neither the name of the Baker Framework nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written +// permission. +// 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. +// + +#import "BKRJSONStatus.h" +#import "BKRUtils.h" + +@implementation BKRJSONStatus + +@synthesize path; + +- (id)initWithJSONPath:(NSString*)JSONPath +{ + self = [super init]; + + if (self) { + path = JSONPath; + [self createFileIfMissing]; + [self load]; + } + + return self; +} + +- (NSDictionary*)load { + NSError *error = nil; + NSData* json = [NSData dataWithContentsOfFile:self.path options:0 error:&error]; + if (error) { + NSLog(@"[JSONStatus] Error when loading JSON status: %@", error); + } + NSDictionary* retv = [NSJSONSerialization JSONObjectWithData:json + options:0 + error:&error]; + // TODO: deal with error + return retv; +} + +- (void)save:(NSDictionary*)status { + NSError* error = nil; + NSData* json = [NSJSONSerialization dataWithJSONObject:status + options:0 + error:&error]; + // TODO: deal with error + + [json writeToFile:path options:NSDataWritingAtomic error:&error]; + + if (error) { + NSLog(@"[JSONStatus] Error when saving JSON status: %@", error); + } +} + +- (void)createFileIfMissing { + NSError *error = nil; + + NSString *dirPath = [path stringByDeletingLastPathComponent]; + if (![[NSFileManager defaultManager] fileExistsAtPath:dirPath]) { + [[NSFileManager defaultManager] createDirectoryAtPath:dirPath withIntermediateDirectories:YES attributes:nil error:&error]; + if (error) { + NSLog(@"[JSONStatus] Error when creating JSON status folder: %@", error); + } + } + + if (![[NSFileManager defaultManager] fileExistsAtPath:path]) { + if (![[NSFileManager defaultManager] createFileAtPath:path contents:nil attributes:nil]) { + NSLog(@"[JSONStatus] JSON status file could not be created at %@", path); + } + + } +} + + +@end diff --git a/BakerView/lib/BKRUtils.h b/BakerView/lib/BKRUtils.h new file mode 100644 index 0000000..5dbef73 --- /dev/null +++ b/BakerView/lib/BKRUtils.h @@ -0,0 +1,56 @@ +// +// NSString+Extensions.h +// Baker +// +// ========================================================================================== +// +// Copyright (c) 2010-2013, Davide Casali, Marco Colombo, Alessandro Morandi +// Copyright (c) 2014, Andrew Krowczyk, Cédric Mériau, Pieter Claerhout +// All rights reserved. +// +// 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. +// Neither the name of the Baker Framework nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written +// permission. +// 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. +// + +#import +#import "BKRBook.h" + +// IOS VERSION COMPARISON MACROS +#define SYSTEM_VERSION_EQUAL_TO(version) ([[[UIDevice currentDevice] systemVersion] compare:version options:NSNumericSearch] == NSOrderedSame) +#define SYSTEM_VERSION_GREATER_THAN(version) ([[[UIDevice currentDevice] systemVersion] compare:version options:NSNumericSearch] == NSOrderedDescending) +#define SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(version) ([[[UIDevice currentDevice] systemVersion] compare:version options:NSNumericSearch] != NSOrderedAscending) +#define SYSTEM_VERSION_LESS_THAN(version) ([[[UIDevice currentDevice] systemVersion] compare:version options:NSNumericSearch] == NSOrderedAscending) +#define SYSTEM_VERSION_LESS_THAN_OR_EQUAL_TO(version) ([[[UIDevice currentDevice] systemVersion] compare:version options:NSNumericSearch] != NSOrderedDescending) + +@interface BKRUtils : NSObject + ++ (UIColor*)colorWithRGBHex:(UInt32)hex; ++ (UIColor*)colorWithHexString:(NSString*)stringToConvert; ++ (CAGradientLayer *)gradientLayerFromHexString:(NSString *)startString toHexString:(NSString *)stopString; ++ (NSString*)stringFromInterfaceOrientation:(UIInterfaceOrientation)orientation; ++ (BOOL)webViewShouldBePaged:(UIWebView*)webView forBook:(BKRBook*)book; ++ (NSString*)appID; ++ (NSDate*)dateWithFormattedString:(NSString*)string; ++ (void)showAlertWithTitle:(NSString*)title message:(NSString*)message buttonTitle:(NSString*)buttonTitle; ++ (void)webView:(UIWebView*)webView dispatchHTMLEvent:(NSString*)event; ++ (void)webView:(UIWebView*)webView dispatchHTMLEvent:(NSString*)event withParams:(NSDictionary*)params; + +@end diff --git a/BakerView/lib/BKRUtils.m b/BakerView/lib/BKRUtils.m new file mode 100644 index 0000000..f80556a --- /dev/null +++ b/BakerView/lib/BKRUtils.m @@ -0,0 +1,154 @@ +// +// NSString+Extensions.h +// Baker +// +// ========================================================================================== +// +// Copyright (c) 2010-2013, Davide Casali, Marco Colombo, Alessandro Morandi +// Copyright (c) 2014, Andrew Krowczyk, Cédric Mériau, Pieter Claerhout +// All rights reserved. +// +// 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. +// Neither the name of the Baker Framework nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written +// permission. +// 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. +// + +#define ISPAGED_JS_SNIPPET @"\ + var elem = document.getElementsByName('paged')[0];\ + if (elem) {\ + elem.getAttribute('content');\ + }" + +#import "BKRUtils.h" +#import + +@implementation BKRUtils + ++ (UIColor*)colorWithRGBHex:(UInt32)hex { + int r = (hex >> 16) & 0xFF; + int g = (hex >> 8) & 0xFF; + int b = (hex) & 0xFF; + + return [UIColor colorWithRed:r / 255.0f + green:g / 255.0f + blue:b / 255.0f + alpha:1.0f]; +} + ++ (UIColor*)colorWithHexString:(NSString*)stringToConvert { + // Returns a UIColor by scanning the string for a hex number and passing that to (UIColor*)colorWithRGBHex:(UInt32)hex + // Skips any leading whitespace and ignores any trailing characters + + if([stringToConvert isEqualToString:@"transparent"]) { + return [UIColor clearColor]; + } + + NSString *hexString = [stringToConvert stringByReplacingOccurrencesOfString:@"#" withString:@""]; + NSScanner *scanner = [NSScanner scannerWithString:hexString]; + + unsigned hexNum; + if (![scanner scanHexInt:&hexNum]) { + return nil; + } + return [BKRUtils colorWithRGBHex:hexNum]; +} + ++ (CAGradientLayer *)gradientLayerFromHexString:(NSString *)startString toHexString:(NSString *)stopString { + UIColor *startColor = [self colorWithHexString:startString]; + UIColor *stopColor = [self colorWithHexString:stopString]; + + NSArray *gradientColors = [NSArray arrayWithObjects:(id)startColor.CGColor, (id)stopColor.CGColor, nil]; + NSArray *gradientLocations = [NSArray arrayWithObjects:[NSNumber numberWithInt:0.0],[NSNumber numberWithInt:1.0], nil]; + + CAGradientLayer *gradientLayer = [CAGradientLayer layer]; + gradientLayer.colors = gradientColors; + gradientLayer.locations = gradientLocations; + + return gradientLayer; +} + ++ (NSString*)stringFromInterfaceOrientation:(UIInterfaceOrientation)orientation { + switch (orientation) { + case UIInterfaceOrientationPortrait: return @"UIInterfaceOrientationPortrait"; + case UIInterfaceOrientationPortraitUpsideDown: return @"UIInterfaceOrientationPortraitUpsideDown"; + case UIInterfaceOrientationLandscapeLeft: return @"UIInterfaceOrientationLandscapeLeft"; + case UIInterfaceOrientationLandscapeRight: return @"UIInterfaceOrientationLandscapeRight"; + case UIInterfaceOrientationUnknown: return @"UIInterfaceOrientationUnknown"; + } + return nil; +} + ++ (BOOL)webViewShouldBePaged:(UIWebView*)webView forBook:(BKRBook*)book { + BOOL shouldBePaged = NO; + + NSString *pagePagination = [webView stringByEvaluatingJavaScriptFromString:ISPAGED_JS_SNIPPET]; + if ([pagePagination length] > 0) { + shouldBePaged = [pagePagination boolValue]; + } else { + shouldBePaged = [book.bakerVerticalPagination boolValue]; + } + //NSLog(@"[Utils] Current page Pagination Mode status = %d", shouldBePaged); + + return shouldBePaged; +} + ++ (NSString*)appID { + return [[NSBundle mainBundle] bundleIdentifier]; +} + ++ (NSDate*)dateWithFormattedString:(NSString*)string { + static NSDateFormatter *dateFormat = nil; + if (dateFormat == nil) { + dateFormat = [[NSDateFormatter alloc] init]; + NSLocale *enUSPOSIXLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]; + [dateFormat setLocale:enUSPOSIXLocale]; + [dateFormat setDateFormat:@"yyyy-MM-dd HH:mm:ss"]; + [dateFormat setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]]; + } + return [dateFormat dateFromString:string]; +} + ++ (void)showAlertWithTitle:(NSString*)title message:(NSString*)message buttonTitle:(NSString*)buttonTitle { + UIAlertView *alert = [[UIAlertView alloc] initWithTitle:title + message:message + delegate:nil + cancelButtonTitle:buttonTitle + otherButtonTitles:nil]; + [alert show]; +} + ++ (void)webView:(UIWebView*)webView dispatchHTMLEvent:(NSString*)event { + [BKRUtils webView:webView dispatchHTMLEvent:event withParams:[NSDictionary dictionary]]; +} + ++ (void)webView:(UIWebView*)webView dispatchHTMLEvent:(NSString*)event withParams:(NSDictionary*)params { + NSMutableString *jsDispatchEvent = [NSMutableString stringWithFormat: + @"var bakerDispatchedEvent = document.createEvent('Events');\ + bakerDispatchedEvent.initEvent('%@', false, false);", event]; + [params enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + NSString *jsParamSet = [NSString stringWithFormat:@"bakerDispatchedEvent.%@='%@';\n", key, obj]; + [jsDispatchEvent appendString:jsParamSet]; + }]; + [jsDispatchEvent appendString:@"window.dispatchEvent(bakerDispatchedEvent);"]; + + [webView stringByEvaluatingJavaScriptFromString:jsDispatchEvent]; +} + +@end diff --git a/BakerView/lib/GTMDefines.h b/BakerView/lib/GTMDefines.h new file mode 100644 index 0000000..9ef6fcc --- /dev/null +++ b/BakerView/lib/GTMDefines.h @@ -0,0 +1,412 @@ +// +// GTMDefines.h +// +// Copyright 2008 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy +// of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. +// + +// ============================================================================ + +#include +#include + +#if TARGET_OS_IPHONE +#include +#endif // TARGET_OS_IPHONE + +// Not all MAC_OS_X_VERSION_10_X macros defined in past SDKs +#ifndef MAC_OS_X_VERSION_10_5 + #define MAC_OS_X_VERSION_10_5 1050 +#endif +#ifndef MAC_OS_X_VERSION_10_6 + #define MAC_OS_X_VERSION_10_6 1060 +#endif + +// Not all __IPHONE_X macros defined in past SDKs +#ifndef __IPHONE_2_1 + #define __IPHONE_2_1 20100 +#endif +#ifndef __IPHONE_2_2 + #define __IPHONE_2_2 20200 +#endif +#ifndef __IPHONE_3_0 + #define __IPHONE_3_0 30000 +#endif +#ifndef __IPHONE_3_1 + #define __IPHONE_3_1 30100 +#endif +#ifndef __IPHONE_3_2 + #define __IPHONE_3_2 30200 +#endif +#ifndef __IPHONE_4_0 + #define __IPHONE_4_0 40000 +#endif + +// ---------------------------------------------------------------------------- +// CPP symbols that can be overridden in a prefix to control how the toolbox +// is compiled. +// ---------------------------------------------------------------------------- + + +// By setting the GTM_CONTAINERS_VALIDATION_FAILED_LOG and +// GTM_CONTAINERS_VALIDATION_FAILED_ASSERT macros you can control what happens +// when a validation fails. If you implement your own validators, you may want +// to control their internals using the same macros for consistency. +#ifndef GTM_CONTAINERS_VALIDATION_FAILED_ASSERT + #define GTM_CONTAINERS_VALIDATION_FAILED_ASSERT 0 +#endif + +// Give ourselves a consistent way to do inlines. Apple's macros even use +// a few different actual definitions, so we're based off of the foundation +// one. +#if !defined(GTM_INLINE) + #if (defined (__GNUC__) && (__GNUC__ == 4)) || defined (__clang__) + #define GTM_INLINE static __inline__ __attribute__((always_inline)) + #else + #define GTM_INLINE static __inline__ + #endif +#endif + +// Give ourselves a consistent way of doing externs that links up nicely +// when mixing objc and objc++ +#if !defined (GTM_EXTERN) + #if defined __cplusplus + #define GTM_EXTERN extern "C" + #define GTM_EXTERN_C_BEGIN extern "C" { + #define GTM_EXTERN_C_END } + #else + #define GTM_EXTERN extern + #define GTM_EXTERN_C_BEGIN + #define GTM_EXTERN_C_END + #endif +#endif + +// Give ourselves a consistent way of exporting things if we have visibility +// set to hidden. +#if !defined (GTM_EXPORT) + #define GTM_EXPORT __attribute__((visibility("default"))) +#endif + +// Give ourselves a consistent way of declaring something as unused. This +// doesn't use __unused because that is only supported in gcc 4.2 and greater. +#if !defined (GTM_UNUSED) +#define GTM_UNUSED(x) ((void)(x)) +#endif + +// _GTMDevLog & _GTMDevAssert +// +// _GTMDevLog & _GTMDevAssert are meant to be a very lightweight shell for +// developer level errors. This implementation simply macros to NSLog/NSAssert. +// It is not intended to be a general logging/reporting system. +// +// Please see http://code.google.com/p/google-toolbox-for-mac/wiki/DevLogNAssert +// for a little more background on the usage of these macros. +// +// _GTMDevLog log some error/problem in debug builds +// _GTMDevAssert assert if conditon isn't met w/in a method/function +// in all builds. +// +// To replace this system, just provide different macro definitions in your +// prefix header. Remember, any implementation you provide *must* be thread +// safe since this could be called by anything in what ever situtation it has +// been placed in. +// + +// We only define the simple macros if nothing else has defined this. +#ifndef _GTMDevLog + +#ifdef DEBUG + #define _GTMDevLog(...) NSLog(__VA_ARGS__) +#else + #define _GTMDevLog(...) do { } while (0) +#endif + +#endif // _GTMDevLog + +#ifndef _GTMDevAssert +// we directly invoke the NSAssert handler so we can pass on the varargs +// (NSAssert doesn't have a macro we can use that takes varargs) +#if !defined(NS_BLOCK_ASSERTIONS) + #define _GTMDevAssert(condition, ...) \ + do { \ + if (!(condition)) { \ + [[NSAssertionHandler currentHandler] \ + handleFailureInFunction:[NSString stringWithUTF8String:__PRETTY_FUNCTION__] \ + file:[NSString stringWithUTF8String:__FILE__] \ + lineNumber:__LINE__ \ + description:__VA_ARGS__]; \ + } \ + } while(0) +#else // !defined(NS_BLOCK_ASSERTIONS) + #define _GTMDevAssert(condition, ...) do { } while (0) +#endif // !defined(NS_BLOCK_ASSERTIONS) + +#endif // _GTMDevAssert + +// _GTMCompileAssert +// _GTMCompileAssert is an assert that is meant to fire at compile time if you +// want to check things at compile instead of runtime. For example if you +// want to check that a wchar is 4 bytes instead of 2 you would use +// _GTMCompileAssert(sizeof(wchar_t) == 4, wchar_t_is_4_bytes_on_OS_X) +// Note that the second "arg" is not in quotes, and must be a valid processor +// symbol in it's own right (no spaces, punctuation etc). + +// Wrapping this in an #ifndef allows external groups to define their own +// compile time assert scheme. +#ifndef _GTMCompileAssert + // We got this technique from here: + // http://unixjunkie.blogspot.com/2007/10/better-compile-time-asserts_29.html + + #define _GTMCompileAssertSymbolInner(line, msg) _GTMCOMPILEASSERT ## line ## __ ## msg + #define _GTMCompileAssertSymbol(line, msg) _GTMCompileAssertSymbolInner(line, msg) + #define _GTMCompileAssert(test, msg) \ + typedef char _GTMCompileAssertSymbol(__LINE__, msg) [ ((test) ? 1 : -1) ] +#endif // _GTMCompileAssert + +// ---------------------------------------------------------------------------- +// CPP symbols defined based on the project settings so the GTM code has +// simple things to test against w/o scattering the knowledge of project +// setting through all the code. +// ---------------------------------------------------------------------------- + +// Provide a single constant CPP symbol that all of GTM uses for ifdefing +// iPhone code. +#if TARGET_OS_IPHONE // iPhone SDK + // For iPhone specific stuff + #define GTM_IPHONE_SDK 1 + #if TARGET_IPHONE_SIMULATOR + #define GTM_IPHONE_SIMULATOR 1 + #else + #define GTM_IPHONE_DEVICE 1 + #endif // TARGET_IPHONE_SIMULATOR +#else + // For MacOS specific stuff + #define GTM_MACOS_SDK 1 +#endif + +// Some of our own availability macros +#if GTM_MACOS_SDK +#define GTM_AVAILABLE_ONLY_ON_IPHONE UNAVAILABLE_ATTRIBUTE +#define GTM_AVAILABLE_ONLY_ON_MACOS +#else +#define GTM_AVAILABLE_ONLY_ON_IPHONE +#define GTM_AVAILABLE_ONLY_ON_MACOS UNAVAILABLE_ATTRIBUTE +#endif + +// Provide a symbol to include/exclude extra code for GC support. (This mainly +// just controls the inclusion of finalize methods). +#ifndef GTM_SUPPORT_GC + #if GTM_IPHONE_SDK + // iPhone never needs GC + #define GTM_SUPPORT_GC 0 + #else + // We can't find a symbol to tell if GC is supported/required, so best we + // do on Mac targets is include it if we're on 10.5 or later. + #if MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_5 + #define GTM_SUPPORT_GC 0 + #else + #define GTM_SUPPORT_GC 1 + #endif + #endif +#endif + +// To simplify support for 64bit (and Leopard in general), we provide the type +// defines for non Leopard SDKs +#if !(MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5) + // NSInteger/NSUInteger and Max/Mins + #ifndef NSINTEGER_DEFINED + #if __LP64__ || NS_BUILD_32_LIKE_64 + typedef long NSInteger; + typedef unsigned long NSUInteger; + #else + typedef int NSInteger; + typedef unsigned int NSUInteger; + #endif + #define NSIntegerMax LONG_MAX + #define NSIntegerMin LONG_MIN + #define NSUIntegerMax ULONG_MAX + #define NSINTEGER_DEFINED 1 + #endif // NSINTEGER_DEFINED + // CGFloat + #ifndef CGFLOAT_DEFINED + #if defined(__LP64__) && __LP64__ + // This really is an untested path (64bit on Tiger?) + typedef double CGFloat; + #define CGFLOAT_MIN DBL_MIN + #define CGFLOAT_MAX DBL_MAX + #define CGFLOAT_IS_DOUBLE 1 + #else /* !defined(__LP64__) || !__LP64__ */ + typedef float CGFloat; + #define CGFLOAT_MIN FLT_MIN + #define CGFLOAT_MAX FLT_MAX + #define CGFLOAT_IS_DOUBLE 0 + #endif /* !defined(__LP64__) || !__LP64__ */ + #define CGFLOAT_DEFINED 1 + #endif // CGFLOAT_DEFINED +#endif // MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_5 + +// Some support for advanced clang static analysis functionality +// See http://clang-analyzer.llvm.org/annotations.html +#ifndef __has_feature // Optional. + #define __has_feature(x) 0 // Compatibility with non-clang compilers. +#endif + +#ifndef NS_RETURNS_RETAINED + #if __has_feature(attribute_ns_returns_retained) + #define NS_RETURNS_RETAINED __attribute__((ns_returns_retained)) + #else + #define NS_RETURNS_RETAINED + #endif +#endif + +#ifndef NS_RETURNS_NOT_RETAINED + #if __has_feature(attribute_ns_returns_not_retained) + #define NS_RETURNS_NOT_RETAINED __attribute__((ns_returns_not_retained)) + #else + #define NS_RETURNS_NOT_RETAINED + #endif +#endif + +#ifndef CF_RETURNS_RETAINED + #if __has_feature(attribute_cf_returns_retained) + #define CF_RETURNS_RETAINED __attribute__((cf_returns_retained)) + #else + #define CF_RETURNS_RETAINED + #endif +#endif + +#ifndef CF_RETURNS_NOT_RETAINED + #if __has_feature(attribute_cf_returns_not_retained) + #define CF_RETURNS_NOT_RETAINED __attribute__((cf_returns_not_retained)) + #else + #define CF_RETURNS_NOT_RETAINED + #endif +#endif + +#ifndef NS_CONSUMED + #if __has_feature(attribute_ns_consumed) + #define NS_CONSUMED __attribute__((ns_consumed)) + #else + #define NS_CONSUMED + #endif +#endif + +#ifndef CF_CONSUMED + #if __has_feature(attribute_cf_consumed) + #define CF_CONSUMED __attribute__((cf_consumed)) + #else + #define CF_CONSUMED + #endif +#endif + +#ifndef NS_CONSUMES_SELF + #if __has_feature(attribute_ns_consumes_self) + #define NS_CONSUMES_SELF __attribute__((ns_consumes_self)) + #else + #define NS_CONSUMES_SELF + #endif +#endif + +// Defined on 10.6 and above. +#ifndef NS_FORMAT_ARGUMENT + #define NS_FORMAT_ARGUMENT(A) +#endif + +// Defined on 10.6 and above. +#ifndef NS_FORMAT_FUNCTION + #define NS_FORMAT_FUNCTION(F,A) +#endif + +// Defined on 10.6 and above. +#ifndef CF_FORMAT_ARGUMENT + #define CF_FORMAT_ARGUMENT(A) +#endif + +// Defined on 10.6 and above. +#ifndef CF_FORMAT_FUNCTION + #define CF_FORMAT_FUNCTION(F,A) +#endif + +#ifndef GTM_NONNULL + #define GTM_NONNULL(x) __attribute__((nonnull(x))) +#endif + +#ifdef __OBJC__ + +// Declared here so that it can easily be used for logging tracking if +// necessary. See GTMUnitTestDevLog.h for details. +@class NSString; +GTM_EXTERN void _GTMUnitTestDevLog(NSString *format, ...); + +// Macro to allow you to create NSStrings out of other macros. +// #define FOO foo +// NSString *fooString = GTM_NSSTRINGIFY(FOO); +#if !defined (GTM_NSSTRINGIFY) + #define GTM_NSSTRINGIFY_INNER(x) @#x + #define GTM_NSSTRINGIFY(x) GTM_NSSTRINGIFY_INNER(x) +#endif + +// Macro to allow fast enumeration when building for 10.5 or later, and +// reliance on NSEnumerator for 10.4. Remember, NSDictionary w/ FastEnumeration +// does keys, so pick the right thing, nothing is done on the FastEnumeration +// side to be sure you're getting what you wanted. +#ifndef GTM_FOREACH_OBJECT + #if TARGET_OS_IPHONE || !(MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_5) + #define GTM_FOREACH_ENUMEREE(element, enumeration) \ + for (element in enumeration) + #define GTM_FOREACH_OBJECT(element, collection) \ + for (element in collection) + #define GTM_FOREACH_KEY(element, collection) \ + for (element in collection) + #else + #define GTM_FOREACH_ENUMEREE(element, enumeration) \ + for (NSEnumerator *_ ## element ## _enum = enumeration; \ + (element = [_ ## element ## _enum nextObject]) != nil; ) + #define GTM_FOREACH_OBJECT(element, collection) \ + GTM_FOREACH_ENUMEREE(element, [collection objectEnumerator]) + #define GTM_FOREACH_KEY(element, collection) \ + GTM_FOREACH_ENUMEREE(element, [collection keyEnumerator]) + #endif +#endif + +// ============================================================================ + +// To simplify support for both Leopard and Snow Leopard we declare +// the Snow Leopard protocols that we need here. +#if !defined(GTM_10_6_PROTOCOLS_DEFINED) && !(MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_6) +#define GTM_10_6_PROTOCOLS_DEFINED 1 +@protocol NSConnectionDelegate +@end +@protocol NSAnimationDelegate +@end +@protocol NSImageDelegate +@end +@protocol NSTabViewDelegate +@end +#endif // !defined(GTM_10_6_PROTOCOLS_DEFINED) && !(MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_6) + +// GTM_SEL_STRING is for specifying selector (usually property) names to KVC +// or KVO methods. +// In debug it will generate warnings for undeclared selectors if +// -Wunknown-selector is turned on. +// In release it will have no runtime overhead. +#ifndef GTM_SEL_STRING + #ifdef DEBUG + #define GTM_SEL_STRING(selName) NSStringFromSelector(@selector(selName)) + #else + #define GTM_SEL_STRING(selName) @#selName + #endif // DEBUG +#endif // GTM_SEL_STRING + +#endif // __OBJC__ diff --git a/BakerView/lib/GTMNSString+HTML.h b/BakerView/lib/GTMNSString+HTML.h new file mode 100644 index 0000000..1273cc3 --- /dev/null +++ b/BakerView/lib/GTMNSString+HTML.h @@ -0,0 +1,66 @@ +// +// GTMNSString+HTML.h +// Dealing with NSStrings that contain HTML +// +// Copyright 2006-2008 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy +// of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. +// + +#import + +/// Utilities for NSStrings containing HTML +@interface NSString (GTMNSStringHTMLAdditions) + +/// Get a string where internal characters that need escaping for HTML are escaped +// +/// For example, '&' become '&'. This will only cover characters from table +/// A.2.2 of http://www.w3.org/TR/xhtml1/dtds.html#a_dtd_Special_characters +/// which is what you want for a unicode encoded webpage. If you have a ascii +/// or non-encoded webpage, please use stringByEscapingAsciiHTML which will +/// encode all characters. +/// +/// For obvious reasons this call is only safe once. +// +// Returns: +// Autoreleased NSString +// +- (NSString *)gtm_stringByEscapingForHTML; + +/// Get a string where internal characters that need escaping for HTML are escaped +// +/// For example, '&' become '&' +/// All non-mapped characters (unicode that don't have a &keyword; mapping) +/// will be converted to the appropriate &#xxx; value. If your webpage is +/// unicode encoded (UTF16 or UTF8) use stringByEscapingHTML instead as it is +/// faster, and produces less bloated and more readable HTML (as long as you +/// are using a unicode compliant HTML reader). +/// +/// For obvious reasons this call is only safe once. +// +// Returns: +// Autoreleased NSString +// +- (NSString *)gtm_stringByEscapingForAsciiHTML; + +/// Get a string where internal characters that are escaped for HTML are unescaped +// +/// For example, '&' becomes '&' +/// Handles and 2 cases as well +/// +// Returns: +// Autoreleased NSString +// +- (NSString *)gtm_stringByUnescapingFromHTML; + +@end diff --git a/BakerView/lib/GTMNSString+HTML.m b/BakerView/lib/GTMNSString+HTML.m new file mode 100644 index 0000000..708c88d --- /dev/null +++ b/BakerView/lib/GTMNSString+HTML.m @@ -0,0 +1,522 @@ +// +// GTMNSString+HTML.m +// Dealing with NSStrings that contain HTML +// +// Copyright 2006-2008 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy +// of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. +// + +#import "GTMDefines.h" +#import "GTMNSString+HTML.h" + +typedef struct { + __unsafe_unretained NSString *escapeSequence; + unichar uchar; +} HTMLEscapeMap; + +// Taken from http://www.w3.org/TR/xhtml1/dtds.html#a_dtd_Special_characters +// Ordered by uchar lowest to highest for bsearching +static HTMLEscapeMap gAsciiHTMLEscapeMap[] = { + // A.2.2. Special characters + { @""", 34 }, + { @"&", 38 }, + { @"'", 39 }, + { @"<", 60 }, + { @">", 62 }, + + // A.2.1. Latin-1 characters + { @" ", 160 }, + { @"¡", 161 }, + { @"¢", 162 }, + { @"£", 163 }, + { @"¤", 164 }, + { @"¥", 165 }, + { @"¦", 166 }, + { @"§", 167 }, + { @"¨", 168 }, + { @"©", 169 }, + { @"ª", 170 }, + { @"«", 171 }, + { @"¬", 172 }, + { @"­", 173 }, + { @"®", 174 }, + { @"¯", 175 }, + { @"°", 176 }, + { @"±", 177 }, + { @"²", 178 }, + { @"³", 179 }, + { @"´", 180 }, + { @"µ", 181 }, + { @"¶", 182 }, + { @"·", 183 }, + { @"¸", 184 }, + { @"¹", 185 }, + { @"º", 186 }, + { @"»", 187 }, + { @"¼", 188 }, + { @"½", 189 }, + { @"¾", 190 }, + { @"¿", 191 }, + { @"À", 192 }, + { @"Á", 193 }, + { @"Â", 194 }, + { @"Ã", 195 }, + { @"Ä", 196 }, + { @"Å", 197 }, + { @"Æ", 198 }, + { @"Ç", 199 }, + { @"È", 200 }, + { @"É", 201 }, + { @"Ê", 202 }, + { @"Ë", 203 }, + { @"Ì", 204 }, + { @"Í", 205 }, + { @"Î", 206 }, + { @"Ï", 207 }, + { @"Ð", 208 }, + { @"Ñ", 209 }, + { @"Ò", 210 }, + { @"Ó", 211 }, + { @"Ô", 212 }, + { @"Õ", 213 }, + { @"Ö", 214 }, + { @"×", 215 }, + { @"Ø", 216 }, + { @"Ù", 217 }, + { @"Ú", 218 }, + { @"Û", 219 }, + { @"Ü", 220 }, + { @"Ý", 221 }, + { @"Þ", 222 }, + { @"ß", 223 }, + { @"à", 224 }, + { @"á", 225 }, + { @"â", 226 }, + { @"ã", 227 }, + { @"ä", 228 }, + { @"å", 229 }, + { @"æ", 230 }, + { @"ç", 231 }, + { @"è", 232 }, + { @"é", 233 }, + { @"ê", 234 }, + { @"ë", 235 }, + { @"ì", 236 }, + { @"í", 237 }, + { @"î", 238 }, + { @"ï", 239 }, + { @"ð", 240 }, + { @"ñ", 241 }, + { @"ò", 242 }, + { @"ó", 243 }, + { @"ô", 244 }, + { @"õ", 245 }, + { @"ö", 246 }, + { @"÷", 247 }, + { @"ø", 248 }, + { @"ù", 249 }, + { @"ú", 250 }, + { @"û", 251 }, + { @"ü", 252 }, + { @"ý", 253 }, + { @"þ", 254 }, + { @"ÿ", 255 }, + + // A.2.2. Special characters cont'd + { @"Œ", 338 }, + { @"œ", 339 }, + { @"Š", 352 }, + { @"š", 353 }, + { @"Ÿ", 376 }, + + // A.2.3. Symbols + { @"ƒ", 402 }, + + // A.2.2. Special characters cont'd + { @"ˆ", 710 }, + { @"˜", 732 }, + + // A.2.3. Symbols cont'd + { @"Α", 913 }, + { @"Β", 914 }, + { @"Γ", 915 }, + { @"Δ", 916 }, + { @"Ε", 917 }, + { @"Ζ", 918 }, + { @"Η", 919 }, + { @"Θ", 920 }, + { @"Ι", 921 }, + { @"Κ", 922 }, + { @"Λ", 923 }, + { @"Μ", 924 }, + { @"Ν", 925 }, + { @"Ξ", 926 }, + { @"Ο", 927 }, + { @"Π", 928 }, + { @"Ρ", 929 }, + { @"Σ", 931 }, + { @"Τ", 932 }, + { @"Υ", 933 }, + { @"Φ", 934 }, + { @"Χ", 935 }, + { @"Ψ", 936 }, + { @"Ω", 937 }, + { @"α", 945 }, + { @"β", 946 }, + { @"γ", 947 }, + { @"δ", 948 }, + { @"ε", 949 }, + { @"ζ", 950 }, + { @"η", 951 }, + { @"θ", 952 }, + { @"ι", 953 }, + { @"κ", 954 }, + { @"λ", 955 }, + { @"μ", 956 }, + { @"ν", 957 }, + { @"ξ", 958 }, + { @"ο", 959 }, + { @"π", 960 }, + { @"ρ", 961 }, + { @"ς", 962 }, + { @"σ", 963 }, + { @"τ", 964 }, + { @"υ", 965 }, + { @"φ", 966 }, + { @"χ", 967 }, + { @"ψ", 968 }, + { @"ω", 969 }, + { @"ϑ", 977 }, + { @"ϒ", 978 }, + { @"ϖ", 982 }, + + // A.2.2. Special characters cont'd + { @" ", 8194 }, + { @" ", 8195 }, + { @" ", 8201 }, + { @"‌", 8204 }, + { @"‍", 8205 }, + { @"‎", 8206 }, + { @"‏", 8207 }, + { @"–", 8211 }, + { @"—", 8212 }, + { @"‘", 8216 }, + { @"’", 8217 }, + { @"‚", 8218 }, + { @"“", 8220 }, + { @"”", 8221 }, + { @"„", 8222 }, + { @"†", 8224 }, + { @"‡", 8225 }, + // A.2.3. Symbols cont'd + { @"•", 8226 }, + { @"…", 8230 }, + + // A.2.2. Special characters cont'd + { @"‰", 8240 }, + + // A.2.3. Symbols cont'd + { @"′", 8242 }, + { @"″", 8243 }, + + // A.2.2. Special characters cont'd + { @"‹", 8249 }, + { @"›", 8250 }, + + // A.2.3. Symbols cont'd + { @"‾", 8254 }, + { @"⁄", 8260 }, + + // A.2.2. Special characters cont'd + { @"€", 8364 }, + + // A.2.3. Symbols cont'd + { @"ℑ", 8465 }, + { @"℘", 8472 }, + { @"ℜ", 8476 }, + { @"™", 8482 }, + { @"ℵ", 8501 }, + { @"←", 8592 }, + { @"↑", 8593 }, + { @"→", 8594 }, + { @"↓", 8595 }, + { @"↔", 8596 }, + { @"↵", 8629 }, + { @"⇐", 8656 }, + { @"⇑", 8657 }, + { @"⇒", 8658 }, + { @"⇓", 8659 }, + { @"⇔", 8660 }, + { @"∀", 8704 }, + { @"∂", 8706 }, + { @"∃", 8707 }, + { @"∅", 8709 }, + { @"∇", 8711 }, + { @"∈", 8712 }, + { @"∉", 8713 }, + { @"∋", 8715 }, + { @"∏", 8719 }, + { @"∑", 8721 }, + { @"−", 8722 }, + { @"∗", 8727 }, + { @"√", 8730 }, + { @"∝", 8733 }, + { @"∞", 8734 }, + { @"∠", 8736 }, + { @"∧", 8743 }, + { @"∨", 8744 }, + { @"∩", 8745 }, + { @"∪", 8746 }, + { @"∫", 8747 }, + { @"∴", 8756 }, + { @"∼", 8764 }, + { @"≅", 8773 }, + { @"≈", 8776 }, + { @"≠", 8800 }, + { @"≡", 8801 }, + { @"≤", 8804 }, + { @"≥", 8805 }, + { @"⊂", 8834 }, + { @"⊃", 8835 }, + { @"⊄", 8836 }, + { @"⊆", 8838 }, + { @"⊇", 8839 }, + { @"⊕", 8853 }, + { @"⊗", 8855 }, + { @"⊥", 8869 }, + { @"⋅", 8901 }, + { @"⌈", 8968 }, + { @"⌉", 8969 }, + { @"⌊", 8970 }, + { @"⌋", 8971 }, + { @"⟨", 9001 }, + { @"⟩", 9002 }, + { @"◊", 9674 }, + { @"♠", 9824 }, + { @"♣", 9827 }, + { @"♥", 9829 }, + { @"♦", 9830 } +}; + +// Taken from http://www.w3.org/TR/xhtml1/dtds.html#a_dtd_Special_characters +// This is table A.2.2 Special Characters +static HTMLEscapeMap gUnicodeHTMLEscapeMap[] = { + // C0 Controls and Basic Latin + { @""", 34 }, + { @"&", 38 }, + { @"'", 39 }, + { @"<", 60 }, + { @">", 62 }, + + // Latin Extended-A + { @"Œ", 338 }, + { @"œ", 339 }, + { @"Š", 352 }, + { @"š", 353 }, + { @"Ÿ", 376 }, + + // Spacing Modifier Letters + { @"ˆ", 710 }, + { @"˜", 732 }, + + // General Punctuation + { @" ", 8194 }, + { @" ", 8195 }, + { @" ", 8201 }, + { @"‌", 8204 }, + { @"‍", 8205 }, + { @"‎", 8206 }, + { @"‏", 8207 }, + { @"–", 8211 }, + { @"—", 8212 }, + { @"‘", 8216 }, + { @"’", 8217 }, + { @"‚", 8218 }, + { @"“", 8220 }, + { @"”", 8221 }, + { @"„", 8222 }, + { @"†", 8224 }, + { @"‡", 8225 }, + { @"‰", 8240 }, + { @"‹", 8249 }, + { @"›", 8250 }, + { @"€", 8364 }, +}; + + +// Utility function for Bsearching table above +static int EscapeMapCompare(const void *ucharVoid, const void *mapVoid) { + const unichar *uchar = (const unichar*)ucharVoid; + const HTMLEscapeMap *map = (const HTMLEscapeMap*)mapVoid; + int val; + if (*uchar > map->uchar) { + val = 1; + } else if (*uchar < map->uchar) { + val = -1; + } else { + val = 0; + } + return val; +} + +@implementation NSString (GTMNSStringHTMLAdditions) + +- (NSString *)gtm_stringByEscapingHTMLUsingTable:(HTMLEscapeMap*)table + ofSize:(NSUInteger)size + escapingUnicode:(BOOL)escapeUnicode { + NSUInteger length = [self length]; + if (!length) { + return self; + } + + NSMutableString *finalString = [NSMutableString string]; + NSMutableData *data2 = [NSMutableData dataWithCapacity:sizeof(unichar) * length]; + + // this block is common between GTMNSString+HTML and GTMNSString+XML but + // it's so short that it isn't really worth trying to share. + const unichar *buffer = CFStringGetCharactersPtr((CFStringRef)self); + if (!buffer) { + // We want this buffer to be autoreleased. + NSMutableData *data = [NSMutableData dataWithLength:length * sizeof(UniChar)]; + if (!data) { + // COV_NF_START - Memory fail case + _GTMDevLog(@"couldn't alloc buffer"); + return nil; + // COV_NF_END + } + [self getCharacters:[data mutableBytes]]; + buffer = [data bytes]; + } + + if (!buffer || !data2) { + // COV_NF_START + _GTMDevLog(@"Unable to allocate buffer or data2"); + return nil; + // COV_NF_END + } + + unichar *buffer2 = (unichar *)[data2 mutableBytes]; + + NSUInteger buffer2Length = 0; + + for (NSUInteger i = 0; i < length; ++i) { + HTMLEscapeMap *val = bsearch(&buffer[i], table, + size / sizeof(HTMLEscapeMap), + sizeof(HTMLEscapeMap), EscapeMapCompare); + if (val || (escapeUnicode && buffer[i] > 127)) { + if (buffer2Length) { + CFStringAppendCharacters((CFMutableStringRef)finalString, + buffer2, + buffer2Length); + buffer2Length = 0; + } + if (val) { + [finalString appendString:val->escapeSequence]; + } + else { + _GTMDevAssert(escapeUnicode && buffer[i] > 127, @"Illegal Character"); + [finalString appendFormat:@"&#%d;", buffer[i]]; + } + } else { + buffer2[buffer2Length] = buffer[i]; + buffer2Length += 1; + } + } + if (buffer2Length) { + CFStringAppendCharacters((CFMutableStringRef)finalString, + buffer2, + buffer2Length); + } + return finalString; +} + +- (NSString *)gtm_stringByEscapingForHTML { + return [self gtm_stringByEscapingHTMLUsingTable:gUnicodeHTMLEscapeMap + ofSize:sizeof(gUnicodeHTMLEscapeMap) + escapingUnicode:NO]; +} // gtm_stringByEscapingHTML + +- (NSString *)gtm_stringByEscapingForAsciiHTML { + return [self gtm_stringByEscapingHTMLUsingTable:gAsciiHTMLEscapeMap + ofSize:sizeof(gAsciiHTMLEscapeMap) + escapingUnicode:YES]; +} // gtm_stringByEscapingAsciiHTML + +- (NSString *)gtm_stringByUnescapingFromHTML { + NSRange range = NSMakeRange(0, [self length]); + NSRange subrange = [self rangeOfString:@"&" options:NSBackwardsSearch range:range]; + + // if no ampersands, we've got a quick way out + if (subrange.length == 0) return self; + NSMutableString *finalString = [NSMutableString stringWithString:self]; + do { + NSRange semiColonRange = NSMakeRange(subrange.location, NSMaxRange(range) - subrange.location); + semiColonRange = [self rangeOfString:@";" options:0 range:semiColonRange]; + range = NSMakeRange(0, subrange.location); + // if we don't find a semicolon in the range, we don't have a sequence + if (semiColonRange.location == NSNotFound) { + continue; + } + NSRange escapeRange = NSMakeRange(subrange.location, semiColonRange.location - subrange.location + 1); + NSString *escapeString = [self substringWithRange:escapeRange]; + NSUInteger length = [escapeString length]; + // a squence must be longer than 3 (<) and less than 11 (ϑ) + if (length > 3 && length < 11) { + if ([escapeString characterAtIndex:1] == '#') { + unichar char2 = [escapeString characterAtIndex:2]; + if (char2 == 'x' || char2 == 'X') { + // Hex escape squences £ + NSString *hexSequence = [escapeString substringWithRange:NSMakeRange(3, length - 4)]; + NSScanner *scanner = [NSScanner scannerWithString:hexSequence]; + unsigned value; + if ([scanner scanHexInt:&value] && + value < USHRT_MAX && + value > 0 + && [scanner scanLocation] == length - 4) { + unichar uchar = value; + NSString *charString = [NSString stringWithCharacters:&uchar length:1]; + [finalString replaceCharactersInRange:escapeRange withString:charString]; + } + + } else { + // Decimal Sequences { + NSString *numberSequence = [escapeString substringWithRange:NSMakeRange(2, length - 3)]; + NSScanner *scanner = [NSScanner scannerWithString:numberSequence]; + int value; + if ([scanner scanInt:&value] && + value < USHRT_MAX && + value > 0 + && [scanner scanLocation] == length - 3) { + unichar uchar = value; + NSString *charString = [NSString stringWithCharacters:&uchar length:1]; + [finalString replaceCharactersInRange:escapeRange withString:charString]; + } + } + } else { + // "standard" sequences + for (unsigned i = 0; i < sizeof(gAsciiHTMLEscapeMap) / sizeof(HTMLEscapeMap); ++i) { + if ([escapeString isEqualToString:gAsciiHTMLEscapeMap[i].escapeSequence]) { + [finalString replaceCharactersInRange:escapeRange withString:[NSString stringWithCharacters:&gAsciiHTMLEscapeMap[i].uchar length:1]]; + break; + } + } + } + } + } while ((subrange = [self rangeOfString:@"&" options:NSBackwardsSearch range:range]).length != 0); + return finalString; +} // gtm_stringByUnescapingHTML + + + +@end diff --git a/BakerView/lib/NSObject+BakerExtensions.h b/BakerView/lib/NSObject+BakerExtensions.h new file mode 100644 index 0000000..d879d7b --- /dev/null +++ b/BakerView/lib/NSObject+BakerExtensions.h @@ -0,0 +1,39 @@ +// +// NSString+Extensions.h +// Baker +// +// ========================================================================================== +// +// Copyright (c) 2010-2013, Davide Casali, Marco Colombo, Alessandro Morandi +// Copyright (c) 2014, Andrew Krowczyk, Cédric Mériau, Pieter Claerhout +// All rights reserved. +// +// 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. +// Neither the name of the Baker Framework nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written +// permission. +// 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. +// + +#import + +@interface NSObject (BakerExtensions) + +- (NSString*)bkrCachePath; + +@end diff --git a/BakerView/lib/NSObject+BakerExtensions.m b/BakerView/lib/NSObject+BakerExtensions.m new file mode 100644 index 0000000..3d1a91f --- /dev/null +++ b/BakerView/lib/NSObject+BakerExtensions.m @@ -0,0 +1,17 @@ +// +// NSObject+Extensions.m +// Baker +// +// Created by Pieter Claerhout on 03/11/14. +// +// + +#import "NSObject+BakerExtensions.h" + +@implementation NSObject (BakerExtensions) + +- (NSString*)bkrCachePath { + return NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES)[0]; +} + +@end diff --git a/BakerView/lib/NSString+BakerExtensions.h b/BakerView/lib/NSString+BakerExtensions.h new file mode 100644 index 0000000..b57d623 --- /dev/null +++ b/BakerView/lib/NSString+BakerExtensions.h @@ -0,0 +1,47 @@ +// +// NSString+Extensions.h +// Baker +// +// ========================================================================================== +// +// Copyright (c) 2010-2013, Davide Casali, Marco Colombo, Alessandro Morandi +// Copyright (c) 2014, Andrew Krowczyk, Cédric Mériau, Pieter Claerhout +// All rights reserved. +// +// 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. +// Neither the name of the Baker Framework nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written +// permission. +// 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. +// + +#import + +@interface NSString (BakerExtensions) + +#pragma mark - SHA management + +- (NSString*)bkrStringSHAEncoded; ++ (NSString*)bkrEncodeSHAString:(NSString*)str; ++ (NSString*)bkrStringFromInterfaceOrientation:(UIInterfaceOrientation)orientation; + +#pragma mark - UUID + ++ (NSString*)bkrUUID; + +@end diff --git a/BakerView/lib/NSString+BakerExtensions.m b/BakerView/lib/NSString+BakerExtensions.m new file mode 100644 index 0000000..0ce6a5a --- /dev/null +++ b/BakerView/lib/NSString+BakerExtensions.m @@ -0,0 +1,81 @@ +// +// NSString+Extensions.h +// Baker +// +// ========================================================================================== +// +// Copyright (c) 2010-2013, Davide Casali, Marco Colombo, Alessandro Morandi +// Copyright (c) 2014, Andrew Krowczyk, Cédric Mériau, Pieter Claerhout +// All rights reserved. +// +// 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. +// Neither the name of the Baker Framework nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written +// permission. +// 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. +// + +#import +#import "NSString+BakerExtensions.h" + +@implementation NSString (BakerExtensions) + +#pragma mark - SHA management + +- (NSString*)bkrStringSHAEncoded { + const char *src = [self UTF8String]; + unsigned char result[CC_SHA1_DIGEST_LENGTH]; + + CC_SHA1(src, (int)strlen(src), result); + NSMutableString *sha = [NSMutableString stringWithCapacity:CC_SHA1_DIGEST_LENGTH * 2]; + + for (int i = 0; i < 8; i++) { + if (result[i]) [sha appendFormat:@"%02X", result[i]]; + } + + return sha; +} + ++ (NSString*)bkrEncodeSHAString:(NSString*)str { + return [str bkrStringSHAEncoded]; +} + ++ (NSString*)bkrStringFromInterfaceOrientation:(UIInterfaceOrientation)orientation { + switch (orientation) { + case UIInterfaceOrientationPortrait: return @"UIInterfaceOrientationPortrait"; + case UIInterfaceOrientationPortraitUpsideDown: return @"UIInterfaceOrientationPortraitUpsideDown"; + case UIInterfaceOrientationLandscapeLeft: return @"UIInterfaceOrientationLandscapeLeft"; + case UIInterfaceOrientationLandscapeRight: return @"UIInterfaceOrientationLandscapeRight"; + case UIInterfaceOrientationUnknown: return @"UIInterfaceOrientationUnknown"; + } + return nil; +} + +#pragma mark - UUID + ++ (NSString*)bkrUUID { + NSString *uuidString = nil; + CFUUIDRef uuid = CFUUIDCreate(NULL); + if (uuid) { + uuidString = (NSString *)CFBridgingRelease(CFUUIDCreateString(NULL, uuid)); + CFRelease(uuid); + } + return uuidString; +} + +@end diff --git a/BakerView/lib/UIColor+BakerExtensions.h b/BakerView/lib/UIColor+BakerExtensions.h new file mode 100644 index 0000000..0ad5e42 --- /dev/null +++ b/BakerView/lib/UIColor+BakerExtensions.h @@ -0,0 +1,18 @@ +// +// UIColor+Extensions.h +// Pirelli +// +// Created by Marco Colombo on 27/09/11. +// Copyright 2011 Marco Natale Colombo. All rights reserved. +// + +#import + +@interface UIColor (BakerExtensions) + +#pragma mark - Hex color management + ++ (UIColor*)bkrColorWithRGBHex:(UInt32)hex; ++ (UIColor*)bkrColorWithHexString:(NSString*)stringToConvert; + +@end diff --git a/BakerView/lib/UIColor+BakerExtensions.m b/BakerView/lib/UIColor+BakerExtensions.m new file mode 100644 index 0000000..6de5659 --- /dev/null +++ b/BakerView/lib/UIColor+BakerExtensions.m @@ -0,0 +1,40 @@ +// +// UIColor+Extensions.m +// Pirelli +// +// Created by Marco Colombo on 27/09/11. +// Copyright 2011 Marco Natale Colombo. All rights reserved. +// + +#import "UIColor+BakerExtensions.h" + +@implementation UIColor (BakerExtensions) + +#pragma mark - Hex color management + ++ (UIColor*)bkrColorWithRGBHex:(UInt32)hex { + int r = (hex >> 16) & 0xFF; + int g = (hex >> 8) & 0xFF; + int b = (hex) & 0xFF; + + return [UIColor colorWithRed:r / 255.0f + green:g / 255.0f + blue:b / 255.0f + alpha:1.0f]; +} + ++ (UIColor*)bkrColorWithHexString:(NSString*)stringToConvert { + // Returns a UIColor by scanning the string for a hex number and passing that to (UIColor*)colorWithRGBHex:(UInt32)hex + // Skips any leading whitespace and ignores any trailing characters + + NSString *hexString = [stringToConvert stringByReplacingOccurrencesOfString:@"#" withString:@""]; + NSScanner *scanner = [NSScanner scannerWithString:hexString]; + + unsigned hexNum; + if (![scanner scanHexInt:&hexNum]) { + return nil; + } + return [UIColor bkrColorWithRGBHex:hexNum]; +} + +@end diff --git a/BakerView/lib/UIScreen+BakerExtensions.h b/BakerView/lib/UIScreen+BakerExtensions.h new file mode 100644 index 0000000..d307e04 --- /dev/null +++ b/BakerView/lib/UIScreen+BakerExtensions.h @@ -0,0 +1,50 @@ +// +// NSString+Extensions.h +// Baker +// +// ========================================================================================== +// +// Copyright (c) 2014, Pieter Claerhout +// All rights reserved. +// +// 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. +// Neither the name of the Baker Framework nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written +// permission. +// 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. +// + +#import + +@interface UIScreen (BakerExtensions) + +- (CGFloat)bkrScreenWidthPortrait; +- (CGFloat)bkrScreenHeightPortrait; + +- (CGFloat)bkrScreenWidth; +- (CGFloat)bkrScreenHeight; + +- (NSString*)bkrLayoutName; + +- (CGFloat)bkrWidthForOrientationName:(NSString*)orientationName; +- (CGFloat)bkrHeightForOrientationName:(NSString*)orientationName; + +- (CGFloat)bkrWidthForOrientation:(UIInterfaceOrientation)orientation; +- (CGFloat)bkrHeightForOrientation:(UIInterfaceOrientation)orientation; + +@end diff --git a/BakerView/lib/UIScreen+BakerExtensions.m b/BakerView/lib/UIScreen+BakerExtensions.m new file mode 100644 index 0000000..670f7ea --- /dev/null +++ b/BakerView/lib/UIScreen+BakerExtensions.m @@ -0,0 +1,91 @@ +// +// NSString+Extensions.h +// Baker +// +// ========================================================================================== +// +// Copyright (c) 2014, Pieter Claerhout +// All rights reserved. +// +// 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. +// Neither the name of the Baker Framework nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written +// permission. +// 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. +// + +#import "UIScreen+BakerExtensions.h" + +@implementation UIScreen (BakerExtensions) + +- (CGFloat)bkrScreenWidthPortrait { + CGFloat screenSize1 = self.bounds.size.width; + CGFloat screenSize2 = self.bounds.size.height; + return MIN(screenSize1, screenSize2); +} + +- (CGFloat)bkrScreenHeightPortrait { + CGFloat screenSize1 = self.bounds.size.width; + CGFloat screenSize2 = self.bounds.size.height; + return MAX(screenSize1, screenSize2); +} + +- (CGFloat)bkrScreenWidth { + return self.bounds.size.width; +} + +- (CGFloat)bkrScreenHeight { + return self.bounds.size.height; +} + +- (NSString*)bkrLayoutName { + CGFloat screenWidth = [self bkrScreenWidthPortrait]; + CGFloat screenHeight = [self bkrScreenHeightPortrait]; + if (screenWidth == 320 && screenHeight == 480) { + screenHeight = 568; // Special case for iPhone 4s + } + return [NSString stringWithFormat:@"%.0fx%.0f", screenHeight, screenWidth]; +} + +- (CGFloat)bkrWidthForOrientationName:(NSString*)orientationName { + if ([orientationName isEqualToString:@"portrait"]) { + return [self bkrScreenWidthPortrait]; + } else { + return [self bkrScreenHeightPortrait]; + } +} + +- (CGFloat)bkrHeightForOrientationName:(NSString*)orientationName { + if ([orientationName isEqualToString:@"portrait"]) { + return [self bkrScreenHeightPortrait]; + } else { + return [self bkrScreenWidthPortrait]; + } +} + +- (CGFloat)bkrWidthForOrientation:(UIInterfaceOrientation)orientation { + NSString *orientationName = UIInterfaceOrientationIsPortrait(orientation) ? @"portrait" : @"landscape"; + return [self bkrWidthForOrientationName:orientationName]; +} + +- (CGFloat)bkrHeightForOrientation:(UIInterfaceOrientation)orientation { + NSString *orientationName = UIInterfaceOrientationIsPortrait(orientation) ? @"portrait" : @"landscape"; + return [self bkrHeightForOrientationName:orientationName]; +} + +@end diff --git a/BakerView/ui/BKRPageTitleLabel.h b/BakerView/ui/BKRPageTitleLabel.h new file mode 100644 index 0000000..29416f3 --- /dev/null +++ b/BakerView/ui/BKRPageTitleLabel.h @@ -0,0 +1,41 @@ +// +// PageTitleLabel.h +// Baker +// +// ========================================================================================== +// +// Copyright (c) 2010-2013, Davide Casali, Marco Colombo, Alessandro Morandi +// Copyright (c) 2014, Andrew Krowczyk, Cédric Mériau, Pieter Claerhout +// All rights reserved. +// +// 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. +// Neither the name of the Baker Framework nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written +// permission. +// 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. +// + +#import + +@interface BKRPageTitleLabel : UILabel + +- (id)initWithFile:(NSString*)path color:(UIColor*)color alpha:(float)alpha; +- (id)initWithFileContent:(NSString*)fileContent color:(UIColor*)color alpha:(float)alpha; +- (void)setX:(CGFloat)x Y:(CGFloat)y; + +@end diff --git a/BakerView/ui/BKRPageTitleLabel.m b/BakerView/ui/BKRPageTitleLabel.m new file mode 100644 index 0000000..64b6cdd --- /dev/null +++ b/BakerView/ui/BKRPageTitleLabel.m @@ -0,0 +1,96 @@ +// +// PageTitleLabel.m +// Baker +// +// ========================================================================================== +// +// Copyright (c) 2010-2013, Davide Casali, Marco Colombo, Alessandro Morandi +// Copyright (c) 2014, Andrew Krowczyk, Cédric Mériau, Pieter Claerhout +// All rights reserved. +// +// 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. +// Neither the name of the Baker Framework nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written +// permission. +// 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. +// + +#import "BKRPageTitleLabel.h" +#import "BKRUtils.h" +#import "GTMNSString+HTML.h" + +@implementation BKRPageTitleLabel + +#pragma mark - Initialization + +- (id)initWithFile:(NSString*)path color:(UIColor*)color alpha:(float)alpha { + NSError *error = nil; + NSString *fileContent = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:&error]; + if (error == nil) { + return [self initWithFileContent:fileContent color:(UIColor*)color alpha:(float)alpha]; + } else { + NSLog(@"Error while loading %@ : %@ : Check that encoding is UTF8 for the file.", path, [error localizedDescription]); + return [super init]; + } +} + +- (id)initWithFileContent:(NSString*)fileContent color:(UIColor*)color alpha:(float)alpha { + + self = [super init]; + if (self) { + NSRegularExpression *titleRegex = [NSRegularExpression regularExpressionWithPattern:@"(.*)" options:NSRegularExpressionCaseInsensitive error:NULL]; + NSRange matchRange = [[titleRegex firstMatchInString:fileContent options:0 range:NSMakeRange(0, [fileContent length])] rangeAtIndex:1]; + if (!NSEqualRanges(matchRange, NSMakeRange(NSNotFound, 0))) { + NSString *titleText = [[fileContent substringWithRange:matchRange] gtm_stringByUnescapingFromHTML]; + + CGSize titleDimension = CGSizeMake(672, 330); + UIFont *titleFont = [UIFont fontWithName:@"Helvetica" size:24.0]; + + CGRect screenBounds = [[UIScreen mainScreen] bounds]; + if (MIN(screenBounds.size.width, screenBounds.size.height) < 768) { + titleDimension = CGSizeMake(280, 134); + titleFont = [UIFont fontWithName:@"Helvetica" size:15.0]; + } + + CGSize titleTextSize = [titleText boundingRectWithSize:titleDimension + options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingTruncatesLastVisibleLine + attributes:@{NSFontAttributeName: titleFont} + context:nil].size; + + self.frame = CGRectMake(0, 0, titleTextSize.width, titleTextSize.height); + self.backgroundColor = [UIColor clearColor]; + self.textAlignment = NSTextAlignmentCenter; + self.lineBreakMode = NSLineBreakByTruncatingTail; + self.numberOfLines = 0; + self.textColor = color; + self.alpha = alpha; + self.font = titleFont; + self.text = titleText; + } + } + return self; +} + +- (void)setX:(CGFloat)x Y:(CGFloat)y { + CGRect titleFrame = self.frame; + titleFrame.origin.x = x; + titleFrame.origin.y = y; + self.frame = titleFrame; +} + +@end diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..a1f19c4 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,18 @@ +# Creating a new issue + +If you are having trouble with Baker, you can ask for help on the [issue board](https://github.com/Simbul/baker/issues) on Github. However, before you open a new issue, make sure you have followed these steps: + +* Follow the relevant tutorial from the [wiki](https://github.com/Simbul/baker/wiki) +* Make sure your question has not been answered in one of the [FAQs](https://github.com/Simbul/baker/wiki/Baker-Walkthrough-FAQ-for-Newbies) +* If you've downloaded Baker from `bakerframework.com`, try updating to the latest [master branch](https://github.com/Simbul/baker/archive/master.zip) from Github + +If you still need to open an issue, please make sure you follow the guidelines on the [Problems and Debugging](https://github.com/Simbul/baker/wiki/Problems-and-Debugging) page. + +# Contributing code + +If you want to contribute code to Baker, you can create a [pull request](https://help.github.com/articles/using-pull-requests) on Github. Please make sure that: + +* Your pull request is based on the latest possible version of the Baker `master` branch +* You have provided a meaningful title for your pull request +* You have briefly explained in the pull request what it does and why it should be merged +* You have referenced any relevant issue diff --git a/README.md b/README.md new file mode 100644 index 0000000..8037ab6 --- /dev/null +++ b/README.md @@ -0,0 +1,268 @@ +Project Baker +============= + +**The HTML5 ebook framework to publish interactive books & magazines on iPad & iPhone using simply open web standards** + + + + +WHAT IS BAKER +------------- + +To this day, ebooks have been limited by an old standard created in the pre-Kindle and pre-iPad era. +Baker is the first stepping stone to show that we could already be using the powerful HTML5 language +to create books with real typography, real layouts and high-quality design. + + + +HOW TO USE BAKER +---------------- + +Creating an ebook in HTML5 to be used with Baker is a three-steps operation. +It's easier done than said! ;) + +1. DESIGN + * Create you publication as one or more **HTML5 files** like a standalone website + * Design your publication to fit multiple screen (using CSS media-queries) + * You can use **any** feature in WebKit: HTML5, CSS3, JavaScript (video, audio, fonts, jQuery, Google Maps, etc) + * On iPad/iPhone you can constrain the height to match the page size or make it scrollable if you need longer pages + * For best results, consider having 1 HTML for each chapter/section/article and use the native vertical scrolling. + * ...remember: it's a publication. If you want to build an app, check [PhoneGap](http://www.phonegap.com/). ;) + +2. PACKAGE + * The publication must be contained in a single Hpub file + * Each chapter/section/article (horizontally swiped on the iPad) should be a single HTML file inside the Hpub + * Create a Hpub book.json file: title, author, pages, etc. (see below) + * All the assets must be contained within the publication folder or its subfolders + * Check the example book from the website for an hands-on example + * See either [Standalone tutorial](https://github.com/bakerframework/baker/wiki/Tutorial-for-Standalone-App) or [Newsstand tutorial for free subscriptions](https://github.com/bakerframework/baker/wiki/Tutorial-for-Newsstand-with-Free-issues) or [Newsstand tutorial for paid subscriptions](https://github.com/bakerframework/baker/wiki/Tutorial-for-Newsstand-with-In-App-Purchase) for more information + +3. PUBLISH + * Download the Baker Framework Xcode project from http://bakerframework.com (or GitHub). + * Download Xcode from the Mac App Store or from the Apple Developer website. + * Decide if you want to release using the [Standalone tutorial](https://github.com/bakerframework/baker/wiki/Tutorial-for-Standalone-App) or [Newsstand tutorial](https://github.com/bakerframework/baker/wiki/Tutorial-for-Newsstand-with-Free-issues) mode and follow the tutorial accordingly. + * Select the Baker Scheme (Simulator or Device) from the toolbar dropdown. + * Run and check that everything works correctly _both_ on simulator and device. + * Check [this page](https://github.com/bakerframework/baker/wiki/Problems-and-Debugging) if you encounter any problem. + * Create an Apple iPhone Developer account to publish on the App Store. + * If you are using Newsstand, follow the instructions on the Apple iPhone Developer website to create either your free subscription or paid subscription / issue In App Purchases + * Follow the instructions on the Apple iPhone Developer website to submit your book to the app store. + + +BOOK.JSON +--------- + +This is an example of a minimal book.json file: + +```json +{ + "title": "The Study in Scarlet", + "author": "Arthur Conan Doyle", + "url": "book://bakerframework.com/books/arthurconandoyle-thestudyinscarlet", + + "contents": [ + "Article-Lorem.html", + "Article-Ipsum.html", + "Article-Gaium.html", + "Article-Sit.html", + "Article-Amet.html" + ] +} +``` + +For all the details and the advanced options, check the [Hpub specification on the wiki](https://github.com/bakerframework/baker/wiki/hpub-specification). + + +SHELF.JSON +---------- + +This is an example of the shelf.json file that is downloaded by Baker in Newsstand mode to check the available publications: + +```json +[ + { + "name": "a-study-in-scarlet", + "title": "A Study in Scarlet", + "info": "The original masterpiece by Sir Arthur Conan Doyle", + "date": "1887-10-10 10:10:10", + "cover": "http://bakerframework.com/newsstand-books/a-study-in-scarlet.png", + "url": "http://bakerframework.com/newsstand-books/a-study-in-scarlet.hpub", + "product_id": "com.bakerframework.Baker.issues.a_study_in_scarlet" + } +] +``` + +For all the details on how to create and use it, check the [Newsstand publications](https://github.com/bakerframework/baker/wiki/4.0-tutorial-for-Newsstand). + + +CREATE A BOOK FOR BOTH IPAD AND IPHONE +-------------------------------------- + +With Baker Framework you can create books and magazines for the iPhone too. + +To compile your application for iPhone follow these steps: + +* Open the Baker Framework Xcode project with Xcode. +* Click on the "Baker" project in the leftmost column of the project navigator. +* On the column just left, click under Projects on "Baker" +* In the "Build Settings" tab locate the section Deployment for the configuration you would like to use for compiling. +* Under the Deployment section change the Target Device Family entry to match the devices you would like to target (only iPhone, only iPad or iPhone/iPad). +* Update your publications to manage multiple orientations (using CSS media-queries). +* Compile and test the application. + + + +BUGS AND FEEDBACK +----------------- + +* Submit your bugs here: +* Give us your feedback at: +* Follow us on Twitter: + + + +CHANGELOG +--------- + +* **4.3** (11/12/2014) + * Added support for iOS 8, iOS 8.1, iPhone 6 and iPhone 6 Plus + * The Baker app is now a universal app + * Support for iOS 6 is dropped, Baker now requires iOS 7 or newer + * The shelf header is now a separate view that can be customized + * The embedded book "A Study Of Scarlet" is now responsive + * The shelf view now supports categories + * Newsstand icon is now updated dynamically + * Autoplay on UIWebView is now allowed + * Settings are now defined in the settings.plist file instead of code + * Many new config settings to customize the shelf + * Full cleanup of the code, move to ARC and move to modern Objective-C syntax + * Minor bug fixes and other resolved GitHub reported issues + +* **4.2.1** (26/01/2014) + * Branding changes. New Baker Framework logo assets + * Open local files in modal view. Action button not shown. Useful if you have local documents (html, pdf, etc...) related to the content of your Baker HPub + * Bugfixes related to iOS 7 app validation (Asset Catalog Errors) + +* **4.2** (28/10/2013) + * iOS 7 support + * Text Kit support + * Support for analytics/tracking frameworks + * Info box in shelf + * `init` JS event for Info box with `user_id` and `app_id` + * Asset Catalog support + * Added `environment` parameter to Baker API calls + * Made most remote calls asynchronous + * Support for removing issues from `shelf.json` + * Replaced AQGridView with UICollectionView + * Minor improvements and bugfixes + +* **4.1** (06/05/2013) + * In-App Purchases for your magazine issues (server required) + * Paid Subscriptions support (server required) + * Push notifications capabilit (server required) + * Baker API defined to allow third-party servers + * Vertical pagination flexibility with `` + * Issue data can now be refreshed + * book: protocol now works within the issues on the shelf + * Index handling improvements + * Removed JSONKit, using iOS5+ parser + * Memory enhancements + * Cleaned up the debug console log and error messages + * Bugs and fixes + +* **4.0** (22/12/2012) + * Full Newsstand support + * Shelf to manage multiple publications + * Free subscriptions support + * Orientation handling improvement + +* **3.2.3** (25/10/2012) + * Added more complete user-agent to work with JS that do user-agent detection for features + * Fix: HTML5 Video playback now uses the Audio session + * Fix: long touch doesn't trigger the index bar anymore + +* **3.2.2** (10/10/2012) + * iOS 6 and iPhone 5 display support + * Improved modal web view offline handling + * Fixed orientation bug and javascript property + * Fixed modal web view crash when interrupted and other minor fixes + * For developers: now Baker view is a separate class + * User agent and status bar tweaks (thanks to @jcampbell05) + +* **3.2.1** (20/08/2012) + * Internal release + +* **3.2** (20/03/2012) + * iOS 5.1 and Retina display support + * External links now open in internal browser, see referrer=Baker (thanks to @steiny2k) + * Custom events fired on window blur and focus + * Book.json parameters to disable tap and swipe: -baker-page-turn-tap/swipe + * Index bar dynamically sized from index.html size. Use viewport meta property to configure + * Change: referrer=Baker variable now not passed to destination website + * Fix: "white flash" workaround found + * Fix: solved issue with pre-compiled screenshots and books with more than 50 pages + * Fix: rare bug of content loaded into index view instead of page + +* **3.1** (20/12/2011) + * Newsstand basic support + * iOS5/iCloud data storage guidelines support + * Pre-build screenshots in screenshot mode using -baker-page-screenshots in book.json + * Retina display support in screenshot mode (thanks to @zenz) + * Manga support: start from the last page, or any arbitrary page using -baker-start-at-page in book.json + * Email protocol support + * Change JSON library to JSONKit (thanks to @zenz) + * Fix: block idle when downloading + * Fix: spinner color and iOS4.x support + * Change: -baker-background-image-* properties are now relative to ./book/ folder, see Issue #247 + +* **3.0.2** (29/10/2011) + * Fix: iOS 4 support for the spinner feature available only in iOS 5 + +* **3.0** (18/10/2011) + * Two rendering modes to improve performances: screenshots (thanks to @pugpig) and three-cards + * index.html view on double-tap to manage navigation and menu + * Full Hpub 1.0 support + * Improved rendering speed and responsiveness + * Improved handling of internal and external links + * Memory optimization + * iOS 5 and Xcode 4.2 compatibility + * Minimum supported version: iOS 4.0 + * Minor fixes and improvements + * Thanks to @francesctovar @therabidbanana @eaglejohn @ffranke for the great support + +* **2.0** (28/04/2011) + * Multi-orientation books support: portrait, landscape, both (thanks to @svdgraaf) + * iPhone support + * Xcode 4 compatibility + * Added support to open a specific page of a downloaded book + * Added support to remove vertical bounce (for non-scrolling books) + * Added support to enable automatic media playback + * Changed the gesture to open the status bar to the more reliable doubletap + * Fix: page anchors now handled in internal links + * Fix: orientationchange event now fires + * Minimum supported version: iOS 3.2 + * Minor fixes + +* **1.1** (19/01/2011) + * Added book:// protocol to allow downloadable HPub books + * Support for zipped HPub books (to allow downloading) + * Link support (internal/external) + * Multitap page navigation + * Alphabetical ordering (WARNING: breaks previous books, check before upgrading) + * Statusbar on tap + * Full screen swipes + * Fix: now the previous page doesn't flash anymore when you change page + * Minor fixes + +* **1.0** (03/11/2010) + * First release + + +LICENSE +------- + + _Copyright (C) 2010-2013, Davide Casali, Marco Colombo, Alessandro Morandi_ + _Licensed under **BSD Opensource License** (free for personal and commercial use)_ + + +> _Elementary, my dear Watson._ diff --git a/books/a-study-in-scarlet/Book Cover.html b/books/a-study-in-scarlet/Book Cover.html new file mode 100644 index 0000000..b82a38f --- /dev/null +++ b/books/a-study-in-scarlet/Book Cover.html @@ -0,0 +1,29 @@ + + + + A Study In Scarlet + + + + + + +
+

A Study in Scarlet

+

by A. Conan Doyle

+ + +
+ + +
+ + diff --git a/books/a-study-in-scarlet/Book Index.html b/books/a-study-in-scarlet/Book Index.html new file mode 100644 index 0000000..c62b015 --- /dev/null +++ b/books/a-study-in-scarlet/Book Index.html @@ -0,0 +1,42 @@ + + + + A Study In Scarlet + + + + + + + diff --git a/books/a-study-in-scarlet/Part1-01.html b/books/a-study-in-scarlet/Part1-01.html new file mode 100644 index 0000000..22e4a24 --- /dev/null +++ b/books/a-study-in-scarlet/Part1-01.html @@ -0,0 +1,90 @@ + + + + 1.1 • Mr. Sherlock Holmes + + + + +
+

Mr. Sherlock Holmes

+ +
+

In the year 1878 I took my degree of Doctor of Medicine of the University of London, and proceeded to Netley to go through the course prescribed for surgeons in the army. Having completed my studies there, I was duly attached to the Fifth Northumberland Fusiliers as Assistant Surgeon. The regiment was stationed in India at the time, and before I could join it, the second Afghan war had broken out. On landing at Bombay, I learned that my corps had advanced through the passes, and was already deep in the enemy's country. I followed, however, with many other officers who were in the same situation as myself, and succeeded in reaching Candahar in safety, where I found my regiment, and at once entered upon my new duties.

+

The campaign brought honours and promotion to many, but for me it had nothing but misfortune and disaster. I was removed from my brigade and attached to the Berkshires, with whom I served at the fatal battle of Maiwand. There I was struck on the shoulder by a Jezail bullet, which shattered the bone and grazed the subclavian artery. I should have fallen into the hands of the murderous Ghazis had it not been for the devotion and courage shown by Murray, my orderly, who threw me across a pack-horse, and succeeded in bringing me safely to the British lines.

+ +

Worn with pain, and weak from the prolonged hardships which I had undergone, I was removed, with a great train of wounded sufferers, to the base hospital at Peshawar. Here I rallied, and had already improved so far as to be able to walk about the wards, and even to bask a little upon the verandah, when I was struck down by enteric fever, that curse of our Indian possessions. For months my life was despaired of, and when at last I came to myself and became convalescent, I was so weak and emaciated that a medical board determined that not a day should be lost in sending me back to England. I was dispatched, accordingly, in the troopship "Orontes," and landed a month later on Portsmouth jetty, with my health irretrievably ruined, but with permission from a paternal government to spend the next nine months in attempting to improve it.

+

I had neither kith nor kin in England, and was therefore as free as air -- or as free as an income of eleven shillings and sixpence a day will permit a man to be. Under such circumstances, I naturally gravitated to London, that great cesspool into which all the loungers and idlers of the Empire are irresistibly drained. There I stayed for some time at a private hotel in the Strand, leading a comfortless, meaningless existence, and spending such money as I had, considerably more freely than I ought. So alarming did the state of my finances become, that I soon realized that I must either leave the metropolis and rusticate somewhere in the country, or that I must make a complete alteration in my style of living. Choosing the latter alternative, I began by making up my mind to leave the hotel, and to take up my quarters in some less pretentious and less expensive domicile.

+

On the very day that I had come to this conclusion, I was standing at the Criterion Bar, when some one tapped me on the shoulder, and turning round I recognized young Stamford, who had been a dresser under me at Barts. The sight of a friendly face in the great wilderness of London is a pleasant thing indeed to a lonely man. In old days Stamford had never been a particular crony of mine, but now I hailed him with enthusiasm, and he, in his turn, appeared to be delighted to see me. In the exuberance of my joy, I asked him to lunch with me at the Holborn, and we started off together in a hansom.

+

"Whatever have you been doing with yourself, Watson?" he asked in undisguised wonder, as we rattled through the crowded London streets. "You are as thin as a lath and as brown as a nut."

+

I gave him a short sketch of my adventures, and had hardly concluded it by the time that we reached our destination.

+

"Poor devil!" he said, commiseratingly, after he had listened to my misfortunes. "What are you up to now?"

+

"Looking for lodgings," I answered. "Trying to solve the problem as to whether it is possible to get comfortable rooms at a reasonable price."

+

"That's a strange thing," remarked my companion; "you are the second man to-day that has used that expression to me."

+

"And who was the first?" I asked.

+

"A fellow who is working at the chemical laboratory up at the hospital. He was bemoaning himself this morning because he could not get someone to go halves with him in some nice rooms which he had found, and which were too much for his purse."

+

"By Jove!" I cried, "if he really wants someone to share the rooms and the expense, I am the very man for him. I should prefer having a partner to being alone."

+

Young Stamford looked rather strangely at me over his wine-glass. "You don't know Sherlock Holmes yet," he said; "perhaps you would not care for him as a constant companion."

+

"Why, what is there against him?"

+

"Oh, I didn't say there was anything against him. He is a little queer in his ideas -- an enthusiast in some branches of science. As far as I know he is a decent fellow enough."

+

"A medical student, I suppose?" said I.

+

"No -- I have no idea what he intends to go in for. I believe he is well up in anatomy, and he is a first-class chemist; but, as far as I know, he has never taken out any systematic medical classes. His studies are very desultory and eccentric, but he has amassed a lot of out-of-the way knowledge which would astonish his professors."

+

"Did you never ask him what he was going in for?" I asked.

+

"No; he is not a man that it is easy to draw out, though he can be communicative enough when the fancy seizes him."

+

"I should like to meet him," I said. "If I am to lodge with anyone, I should prefer a man of studious and quiet habits. I am not strong enough yet to stand much noise or excitement. I had enough of both in Afghanistan to last me for the remainder of my natural existence. How could I meet this friend of yours?"

+

"He is sure to be at the laboratory," returned my companion. "He either avoids the place for weeks, or else he works there from morning to night. If you like, we shall drive round together after luncheon."

+

"Certainly," I answered, and the conversation drifted away into other channels.

+

As we made our way to the hospital after leaving the Holborn, Stamford gave me a few more particulars about the gentleman whom I proposed to take as a fellow-lodger.

+

"You mustn't blame me if you don't get on with him," he said; "I know nothing more of him than I have learned from meeting him occasionally in the laboratory. You proposed this arrangement, so you must not hold me responsible."

+

"If we don't get on it will be easy to part company," I answered. "It seems to me, Stamford," I added, looking hard at my companion, "that you have some reason for washing your hands of the matter. Is this fellow's temper so formidable, or what is it? Don't be mealy-mouthed about it."

+

"It is not easy to express the inexpressible," he answered with a laugh. "Holmes is a little too scientific for my tastes -- it approaches to cold-bloodedness. I could imagine his giving a friend a little pinch of the latest vegetable alkaloid, not out of malevolence, you understand, but simply out of a spirit of inquiry in order to have an accurate idea of the effects. To do him justice, I think that he would take it himself with the same readiness. He appears to have a passion for definite and exact knowledge."

+

"Very right too."

+

"Yes, but it may be pushed to excess. When it comes to beating the subjects in the dissecting-rooms with a stick, it is certainly taking rather a bizarre shape."

+

"Beating the subjects!"

+

"Yes, to verify how far bruises may be produced after death. I saw him at it with my own eyes."

+

"And yet you say he is not a medical student?"

+

"No. Heaven knows what the objects of his studies are. But here we are, and you must form your own impressions about him." As he spoke, we turned down a narrow lane and passed through a small side-door, which opened into a wing of the great hospital. It was familiar ground to me, and I needed no guiding as we ascended the bleak stone staircase and made our way down the long corridor with its vista of whitewashed wall and dun-coloured doors. Near the further end a low arched passage branched away from it and led to the chemical laboratory.

+

This was a lofty chamber, lined and littered with countless bottles. Broad, low tables were scattered about, which bristled with retorts, test-tubes, and little Bunsen lamps, with their blue flickering flames. There was only one student in the room, who was bending over a distant table absorbed in his work.

+

At the sound of our steps he glanced round and sprang to his feet with a cry of pleasure.

+

"I've found it! I've found it," he shouted to my companion, running towards us with a test-tube in his hand. "I have found a re-agent which is precipitated by hoemoglobin, and by nothing else."

+

Had he discovered a gold mine, greater delight could not have shone upon his features.

+ +

"Dr. Watson, Mr. Sherlock Holmes," said Stamford, introducing us.

+

"How are you?" he said cordially, gripping my hand with a strength for which I should hardly have given him credit. "You have been in Afghanistan, I perceive."

+

"How on earth did you know that?" I asked in astonishment.

+

"Never mind," said he, chuckling to himself. "The question now is about hoemoglobin. No doubt you see the significance of this discovery of mine?"

+

"It is interesting, chemically, no doubt," I answered, "but practically ----"

+

"Why, man, it is the most practical medico-legal discovery for years. Don't you see that it gives us an infallible test for blood stains. Come over here now!" He seized me by the coat-sleeve in his eagerness, and drew me over to the table at which he had been working. "Let us have some fresh blood," he said, digging a long bodkin into his finger, and drawing off the resulting drop of blood in a chemical pipette. "Now, I add this small quantity of blood to a litre of water. You perceive that the resulting mixture has the appearance of pure water. The proportion of blood cannot be more than one in a million. I have no doubt, however, that we shall be able to obtain the characteristic reaction." As he spoke, he threw into the vessel a few white crystals, and then added some drops of a transparent fluid. In an instant the contents assumed a dull mahogany colour, and a brownish dust was precipitated to the bottom of the glass jar.

+

"Ha! ha!" he cried, clapping his hands, and looking as delighted as a child with a new toy. "What do you think of that?"

+

"It seems to be a very delicate test," I remarked.

+

"Beautiful! beautiful! The old Guiacum test was very clumsy and uncertain. So is the microscopic examination for blood corpuscles. The latter is valueless if the stains are a few hours old. Now, this appears to act as well whether the blood is old or new. Had this test been invented, there are hundreds of men now walking the earth who would long ago have paid the penalty of their crimes."

+

"Indeed!" I murmured.

+

"Criminal cases are continually hinging upon that one point. A man is suspected of a crime months perhaps after it has been committed. His linen or clothes are examined, and brownish stains discovered upon them. Are they blood stains, or mud stains, or rust stains, or fruit stains, or what are they? That is a question which has puzzled many an expert, and why? Because there was no reliable test. Now we have the Sherlock Holmes' test, and there will no longer be any difficulty."

+

His eyes fairly glittered as he spoke, and he put his hand over his heart and bowed as if to some applauding crowd conjured up by his imagination.

+

"You are to be congratulated," I remarked, considerably surprised at his enthusiasm.

+

"There was the case of Von Bischoff at Frankfort last year. He would certainly have been hung had this test been in existence. Then there was Mason of Bradford, and the notorious Muller, and Lefevre of Montpellier, and Samson of new Orleans. I could name a score of cases in which it would have been decisive."

+

"You seem to be a walking calendar of crime," said Stamford with a laugh. "You might start a paper on those lines. Call it the `Police News of the Past.'"

+

"Very interesting reading it might be made, too," remarked Sherlock Holmes, sticking a small piece of plaster over the prick on his finger. "I have to be careful," he continued, turning to me with a smile, "for I dabble with poisons a good deal." He held out his hand as he spoke, and I noticed that it was all mottled over with similar pieces of plaster, and discoloured with strong acids.

+

"We came here on business," said Stamford, sitting down on a high three-legged stool, and pushing another one in my direction with his foot. "My friend here wants to take diggings, and as you were complaining that you could get no one to go halves with you, I thought that I had better bring you together."

+

Sherlock Holmes seemed delighted at the idea of sharing his rooms with me. "I have my eye on a suite in Baker Street," he said, "which would suit us down to the ground. You don't mind the smell of strong tobacco, I hope?"

+

"I always smoke `ship's' myself," I answered.

+

"That's good enough. I generally have chemicals about, and occasionally do experiments. Would that annoy you?"

+

"By no means."

+

"Let me see -- what are my other shortcomings. I get in the dumps at times, and don't open my mouth for days on end. You must not think I am sulky when I do that. Just let me alone, and I'll soon be right. What have you to confess now? It's just as well for two fellows to know the worst of one another before they begin to live together."

+

I laughed at this cross-examination. "I keep a bull pup," I said, "and I object to rows because my nerves are shaken, and I get up at all sorts of ungodly hours, and I am extremely lazy. I have another set of vices when I'm well, but those are the principal ones at present."

+

"Do you include violin-playing in your category of rows?" he asked, anxiously.

+

"It depends on the player," I answered. "A well-played violin is a treat for the gods -- a badly-played one --"

+

"Oh, that's all right," he cried, with a merry laugh. "I think we may consider the thing as settled -- that is, if the rooms are agreeable to you."

+

"When shall we see them?"

+

"Call for me here at noon to-morrow, and we'll go together and settle everything," he answered.

+

"All right -- noon exactly," said I, shaking his hand.

+

We left him working among his chemicals, and we walked together towards my hotel.

+

"By the way," I asked suddenly, stopping and turning upon Stamford, "how the deuce did he know that I had come from Afghanistan?"

+

My companion smiled an enigmatical smile. "That's just his little peculiarity," he said. "A good many people have wanted to know how he finds things out."

+

"Oh! a mystery is it?" I cried, rubbing my hands. "This is very piquant. I am much obliged to you for bringing us together. `The proper study of mankind is man,' you know."

+

"You must study him, then," Stamford said, as he bade me good-bye. "You'll find him a knotty problem, though. I'll wager he learns more about you than you about him. Good-bye."

+

"Good-bye," I answered, and strolled on to my hotel, considerably interested in my new acquaintance.

+
+
+ + diff --git a/books/a-study-in-scarlet/Part1-02.html b/books/a-study-in-scarlet/Part1-02.html new file mode 100644 index 0000000..169e9c3 --- /dev/null +++ b/books/a-study-in-scarlet/Part1-02.html @@ -0,0 +1,83 @@ + + + + 1.2 • The Science of Deduction + + + + +
+

The Science of Deduction

+ +
+

We met next day as he had arranged and inspected the rooms at No. 221B, Baker Street, of which he had spoken at our meeting. They consisted of a couple of comfortable bedrooms and a single large airy sitting-room, cheerfully furnished, and illuminated by two broad windows. So desirable in every way were the apartments, and so moderate did the terms seem when divided between us, that the bargain was concluded upon the spot, and we at once entered into possession. That very evening I moved my things round from the hotel, and on the following morning Sherlock Holmes followed me with several boxes and portmanteaus. For a day or two we were busily employed in unpacking and laying out our property to the best advantage. That done, we gradually began to settle down and to accommodate ourselves to our new surroundings.

+

Holmes was certainly not a difficult man to live with. He was quiet in his ways, and his habits were regular. It was rare for him to be up after ten at night, and he had invariably breakfasted and gone out before I rose in the morning. Sometimes he spent his day at the chemical laboratory, sometimes in the dissecting rooms, and occasionally in long walks, which appeared to take him into the lowest portions of the city. Nothing could exceed his energy when the working fit was upon him; but now and again a reaction would seize him, and for days on end he would lie upon the sofa in the sitting-room, hardly uttering a word or moving a muscle from morning to night. On these occasions I have noticed such a dreamy, vacant expression in his eyes, that I might have suspected him of being addicted to the use of some narcotic, had not the temperance and cleanliness of his whole life forbidden such a notion.

+

As the weeks went by, my interest in him and my curiosity as to his aims in life gradually deepened and increased. His very person and appearance were such as to strike the attention of the most casual observer. In height he was rather over six feet, and so excessively lean that he seemed to be considerably taller. His eyes were sharp and piercing, save during those intervals of torpor to which I have alluded; and his thin, hawk-like nose gave his whole expression an air of alertness and decision. His chin, too, had the prominence and squareness which mark the man of determination. His hands were invariably blotted with ink and stained with chemicals, yet he was possessed of extraordinary delicacy of touch, as I frequently had occasion to observe when I watched him manipulating his fragile philosophical instruments.

+

The reader may set me down as a hopeless busybody, when I confess how much this man stimulated my curiosity, and how often I endeavoured to break through the reticence which he showed on all that concerned himself. Before pronouncing judgment, however, be it remembered, how objectless was my life, and how little there was to engage my attention. My health forbade me from venturing out unless the weather was exceptionally genial, and I had no friends who would call upon me and break the monotony of my daily existence. Under these circumstances, I eagerly hailed the little mystery which hung around my companion, and spent much of my time in endeavouring to unravel it.

+

He was not studying medicine. He had himself, in reply to a question, confirmed Stamford's opinion upon that point. Neither did he appear to have pursued any course of reading which might fit him for a degree in science or any other recognized portal which would give him an entrance into the learned world. Yet his zeal for certain studies was remarkable, and within eccentric limits his knowledge was so extraordinarily ample and minute that his observations have fairly astounded me. Surely no man would work so hard or attain such precise information unless he had some definite end in view. Desultory readers are seldom remarkable for the exactness of their learning. No man burdens his mind with small matters unless he has some very good reason for doing so.

+

His ignorance was as remarkable as his knowledge. Of contemporary literature, philosophy and politics he appeared to know next to nothing. Upon my quoting Thomas Carlyle, he enquired in the naivest way who he might be and what he had done. My surprise reached a climax, however, when I found incidentally that he was ignorant of the Copernican Theory and of the composition of the Solar System. That any civilized human being in this nineteenth century should not be aware that the earth travelled round the sun appeared to be to me such an extraordinary fact that I could hardly realize it.

+

"You appear to be astonished," he said, smiling at my expression of surprise. "Now that I do know it I shall do my best to forget it." +

"To forget it!"

+

"You see," he explained, "I consider that a man's brain originally is like a little empty attic, and you have to stock it with such furniture as you choose. A fool takes in all the lumber of every sort that he comes across, so that the knowledge which might be useful to him gets crowded out, or at best is jumbled up with a lot of other things, so that he has a difficulty in laying his hands upon it. Now the skilful workman is very careful indeed as to what he takes into his brain attic. He will have nothing but the tools which may help him in doing his work, but of these he has a large assortment, and all in the most perfect order. It is a mistake to think that that little room has elastic walls and can distend to any extent. Depend upon it there comes a time when for every addition of knowledge you forget something that you knew before. It is of the highest importance, therefore, not to have useless facts elbowing out the useful ones."

+

"But the Solar System!" I protested.

+

"What the deuce is it to me?" he interrupted impatiently; "you say that we go round the sun. If we went round the moon it would not make a pennyworth of difference to me or to my work." +

I was on the point of asking him what that work might be, but something in his manner showed me that the question would be an unwelcome one. I pondered over our short conversation, however, and endeavoured to draw my deductions from it. He said that he would acquire no knowledge which did not bear upon his object. Therefore all the knowledge which he possessed was such as would be useful to him; I enumerated in my own mind all the various points upon which he had shown me that he was exceptionally well-informed. I even took a pencil and jotted them down. I could not help smiling at the document when I had completed it. It ran in this way:— +

Sherlock Holmes — his limits.

+
    +
  1. Knowledge of literature.—Nil.
  2. +
  3. Knowledge of Philosophy.—Nil.
  4. +
  5. Knowledge of Astronomy.—Nil.
  6. +
  7. Knowledge of Politics.—Feeble.
  8. +
  9. Knowledge of Botany.—Variable. Well up in belladonna, opium, and poisons generally. Knows nothing of practical gardening.
  10. +
  11. Knowledge of Geology.—Practical, but limited. Tells at a glance different soils from each other. After walks has shown me spashes upon his trousers, and told me by their colour and consistence in what part of London he had received them.
  12. +
  13. Knowledge of Chemistry.—Profound.
  14. +
  15. Knowledge of Anatomy.—Accurate, but unsystematic.
  16. +
  17. Knowledge of Sensational Literature.—Immense. He appears to know every detail of every horror perpetrated in the century.
  18. +
  19. Plays the violin well.
  20. +
  21. Is an expert singlestick player, boxer, and swordsman.
  22. +
  23. Has a good practical knowledge of British law.
  24. +
+

When I got so far in my list I threw it into the fire in despair. "If I could only find what the fellow is driving at by reconciling all these accomplishments, and discovering a calling which needs them all," I said to myself, "I may as well give up the attempt at once." +

I see that I have alluded above to his powers upon the violin. These were very remarkable, but as eccentric as all his other accomplishments. That he could play pieces, and difficult pieces, I knew well, because at my request he has played me some of Mendelssohn's Lieder, and other favourites. When left to himself, however, he would seldom produce any music or attempt any recognised air. Leaning back in his arm-chair of an evening, he would close his eyes and scrape carelessly at the fiddle which was thrown across his knee. Sometimes the chords were sonorous and melancholy. Occasionally they were fantastic and cheerful. Clearly they reflected the thoughts which possessed him, but whether the music aided those thoughts, or whether the playing was simply the result of a whim or fancy, was more than I could determine. I might have rebelled against these exasperating solos had it not been that he usually terminated them by playing in quick succession a whole series of my favourite airs as a slight compensation for the trial upon my patience.

+ +

During the first week or so we had no callers, and I had begun to think that my companion was as friendless a man as I was myself. Presently, however, I found that he had many acquaintances, and those in the most different classes of society. There was one little sallow, rat-faced, dark-eyed fellow, who was introduced to me as Mr. Lestrade, and who came three or four times in a single week. One morning a young girl called, fashionably dressed, and stayed for half an hour or more. The same afternoon brought a grey-headed, seedy visitor, looking like a Jew pedlar, who appeared to me to be much excited, and who was closely followed by a slip-shod elderly woman. On another occasion an old white-haired gentleman has an interview with my companion; and on another, a railway porter in his velveteen uniform. When any of these non-descript individuals put in an appearance, Sherlock Holmes used to beg for the use of the sitting-room, and I would retire to my bedroom. He always apologized to me for putting me to this inconvenience. "I have to use this room as a place of business," he said, "and these people are my clients." Again I had an opportunity of asking him a point-blank question, and again my delicacy prevented me from forcing another man to confide in me. I imagined at the time that he had some strong reason for not alluding to it, but he soon dispelled the idea by coming round to the subject of his own accord.

+

It was upon the 4th of March, as I have good reason to remember, that I rose somewhat earlier than usual, and found that Sherlock Holmes had not yet finished his breakfast. The landlady had become so accustomed to my late habits that my place had not been laid nor my coffee prepared. With the unreasonable petulance of mankind I rang the bell and gave a curt intimation that I was ready. Then I picked up a magazine from the table and attempted to while away the time with it, while my companion munched silently at his toast. One of the articles had a pencil mark at the heading, and I naturally began to run my eye through it.

+

Its somewhat ambitious title was "The Book of Life," and it attempted to show how much an observant man might learn by an accurate and systematic examination of all that came in his way. It struck me as being a remarkable mixture of shrewdness and of absurdity. The reasoning was close and intense, but the deductions appeared to me to be far-fetched and exaggerated. The writer claimed by a momentary expression, a twitch of a muscle or a glance of an eye, to fathom a man's inmost thoughts. Deceit, according to him, was an impossibility in the case of one trained to observation and analysis. His conclusions were as infallible as so many propositions of Euclid. So startling would his results appear to the uninitiated that until they learned the processes by which he had arrived at them they might well consider him as a necromancer.

+

'"From a drop of water," said the writer, "a logician could infer the possibility of an Atlantic or a Niagara without having seen or heard of one of the other. So all life is a great chain, the nature of which is known whenever we are shown a single link of it. Like all other arts, the Science of Deduction and Analysis is one which can only be acquired by long and patient study, nor is life long enough to allow any mortal to attain the highest possible perfection in it. Before turning to those moral and mental aspects of the matter which present the greatest difficulties, let the inquirer begin by mastering more elementary problems. Let him on meeting a fellow-mortal, learn at a glance to distinguish the history of the man, and the trade or profession to which he belongs. Puerile as such an exercise may seem, it sharpens the faculties of observation, and teaches one where to look and what to look for. By a man's finger-nails, by his coat-sleeve, by his boot, by his trouser-knees, by the callosities of his forefinger and thumb, by his expression, by his shirt-cuffs—by each of these things a man's calling is plainly revealed. That all united should fail to enlighten the competent inquirer in any case is almost inconceivable." +

"What ineffable twaddle!" I cried, slapping the magazine down on the table; "I never read such rubbish in my life." +

"What is it?" asked Sherlock Holmes.

+

"Why this article," I said, pointing at it with my egg-spoon as I sat down to my breakfast.

+

"I see that you have read it since you have marked it. I don't deny that it is smartly written. It irritates me though. It is evidently the theory of some arm-chair lounger who evolves all these neat little paradoxes in the seclusion of his own study. It is not practical. I should like to see him clapped down in a third-class carriage on the Underground, and asked to give the trades of all his fellow-travellers. I would lay a thousand to one against him." +

"You would lose your money," Holmes remarked calmly. "As for the article, I wrote it myself." +

"You!" +

"Yes; I have a turn both for observation and for deduction. The theories which I have expressed there, and which appear to you to be so chimerical, are really extremely practical—so practical that I depend upon them for my bread and cheese." +

"And how?" I asked involuntarily.

+

"Well, I have a trade of my own. I suppose I am the only one in the world. I'm a consulting detective, if you can understand what that is. Here in London we have lots of Government detectives and lots of private ones. When these fellows are at fault, they come to me, and I manage to put them on the right scent. They lay all the evidence before me, and I am generally able, by the hold of my knowledge of the history of crime, to set them straight. There is a strong family resemblance about misdeeds, and if you have all the details of a thousand at your finger ends, it is odd if you can't unravel the thousand and first. Lestrade is a well-known detective. He got himself into a fog recently over a forgery case, and that was what brought him here." +

"And these other people?" +

"They are mostly sent on by private inquiry agencies. They are all people who are in trouble about something, and want a little enlightening. I listen to their story, they listen to my comments, and then I pocket my fee." +

"But do you mean to say," I said, "that without leaving your room you can unravel some know which other men can make nothing of, although they have seen every detail for themselves?" +

"Quite so. I have a kind of intuition that way. Now and again a case turns up which is a little more complex. Then I have to bustle about and see things with my own eyes. You see I have a lot of special knowledge which I apply to the problem, and which facilitates matters wonderfully. Those rules of deduction laid down in that article which aroused your scorn are invaluable to me in practical work. Observation with me is second nature. You appeared to be surprised when I told you, on our first meeting, that you had come from Afghanistan." +

"You were told, no doubt." +

"Nothing of the sort. I knew you came from Afghanistan. From long habit the train of thoughts ran so swiftly through my mind that I arrived at the conclusion without being conscious of intermediate steps. There were such steps, however. The train of reasoning ran, 'Here is a gentleman of the medical type, but with the air of a military man. Clearly an army doctor, then. He has just come from the tropics, for his face is dark, and that is not the natural tint of his skin, for his wrists are fair. He has undergone hardship and sickness, as his haggard face says clearly. His left arm has been injured: He holds it in a stiff and unnatural manner. Where in the tropics could an English army doctor have seen much hardship and got his arm wounded? Clearly in Afghanistan.' The whole train of thought did not occupy a second. I then remarked that you came from Afghanistan, and you were astonished." +

"It is simple enough as you explain it," I said smiling. "You remind me of Edgar Allen Poe's Dupin. I had no idea that such individuals did exist outside of stories." +

Sherlock Holmes rose and lit his pipe. "No doubt you think that you are complimenting me in comparing me to Dupin," he observed. "Now, in my opinion, Dupin was a very inferior fellow. That trick of his of breaking in on his friends' thoughts with an apropos remark after a quarter of an hour's silence is really very showy and superficial. He had some analytical genius, no doubt; but he was by no means such a phenomenon as Poe appeared to imagine." +

"Have you read Gaboriau's works?" I asked. "Does Lecoq come up to your idea of a detective?" +

Sherlock Holmes sniffed sardonically. "Lecoq was a miserable bungler," he said, in an angry voice; "he had only one thing to recommend him, and that was his energy. That book made me positively ill. The question was how to identify an unknown prisoner. I could have done it in twenty-four hours. Lecoq took six months or so. It might be made a text-book for detectives to teach them what to avoid." +

I felt rather indignant at having two characters whom I had admired treated in this cavalier style. I walked over to the window, and stood looking out into the busy street. "This fellow may be very clever," I said to myself, "but he is certainly very conceited." +

"There are no crimes and no criminals in these days," he said, querulously. "What is the use of having brains in our profession. I know well that I have it in me to make my name famous. No man lives or has ever lived who has brought the same amount of study and of natural talent to the detection of crime which I have done. And what is the result? There is no crime to detect, or, at most, some bungling villainy with a motive so transparent that even a Scotland Yard official can see through it." +

I was still annoyed at his bumptious style of conversation; I thought it best to change the topic.

+

"I wonder what that fellow is looking for?" I asked, pointing to a stalwart, plainly-dressed individual who was walking slowly down the other side of the street, looking anxiously at the numbers. He had a large blue envelope in his hand, and was evidently the bearer of a message.

+

"You mean the retired sergeant of Marines," said Sherlock Holmes.

+

"Brag and bounce!" thought I to myself. "He knows that I cannot verify his guess." +

The thought had hardly passed through my mind when the man whom we were watching caught sight of the number on our door, and ran rapidly across the roadway. We heard a loud knock, a deep voice below, and heavy steps ascending the stairs.

+ +

"For Mr. Sherlock Holmes," he said, stepping into the room and handing my friend the letter.

+

Here was an opportunity of taking the conceit out of him. He little thought of this when he made that random shot. "May I ask, my lad," I said, in the blandest voice, "what your trade may be?" +

"Commissionaire, sir," he said, gruffly. "Uniform away for repairs." +

"And you were?" I asked, with a slightly malicious glance at my companion.

+

"A sergeant, sir, Royal Marine Light Infantry, sir. No answer? Right, sir." +

He clicked his heels together, raised his hand in a salute, and was gone.

+
+
+ + diff --git a/books/a-study-in-scarlet/Part1-03.html b/books/a-study-in-scarlet/Part1-03.html new file mode 100644 index 0000000..331ef67 --- /dev/null +++ b/books/a-study-in-scarlet/Part1-03.html @@ -0,0 +1,123 @@ + + + + 1.3 • The Lauriston Garden Mystery + + + + +
+

The Lauriston Garden Mystery

+ +
+

I confess that I was considerably startled by this fresh proof of the practical nature of my companion's theories. My respect for his power of analysis increased wondrously. There still remained some lurking suspicion in my mind, however, that the whole thing was a prearranged episode, intended to dazzle me, though what earthly object he could have in taking me in was past my comprehension. When I looked at him, he had finished reading the note, and his eyes had assumed the vacant, lack-lustre expression which showed mental abstraction.

+

"How in the world did you deduce that?" I asked.

+

"Deduce what?" said he, petulantly.

+

"Why, that he was a retired sergeant of Marines."

+

"I have no time for trifles," he answered, brusquely; then with a smile, "Excuse my rudeness. You broke the thread of my thoughts; but perhaps it is as well. So you actually were not able to see that that man was a sergeant of Marines?"

+

"No, indeed."

+

"It was easier to know it than to explain why I know it. If you were asked to prove that two and two made four, you might find some difficulty, and yet you are quite sure of the fact. Even across the street I could see a great blue anchor tattooed on the back of the fellow's hand. That smacked of the sea. He had a military carriage, however, and regulation side whiskers. There we have the marine. He was a man with some amount of self-importance and a certain air of command. You must have observed the way in which he held his head and swung his cane. A steady, respectable, middle-aged man, too, on the face of him—all facts which led me to believe that he had been a sergeant."

+

"Wonderful!" I ejaculated.

+

"Commonplace," said Holmes, though I thought from his expression that he was pleased at my evident surprise and admiration. "I said just now that there were no criminals. It appears that I am wrong—look at this!" He threw me over the note which the commissionaire had brought.

+

"Why," I cried, as I cast my eye over it, "this is terrible!"

+

"It does seem to be a little out of the common," he remarked, calmly. "Would you mind reading it to me aloud?"

+

This is the letter which I read to him, —

+

"My dear Mr. Sherlock Holmes, —

+

"There has been a bad business during the night at 3, Lauriston Gardens, off the Brixton Road. Our man on the beat saw a light there about two in the morning, and as the house was an empty one, suspected that something was amiss. He found the door open, and in the front room, which is bare of furniture, discovered the body of a gentleman, well dressed, and having cards in his pocket bearing the name of 'Enoch J. Drebber, Cleveland, Ohio, U.S.A.' There had been no robbery, nor is there any evidence as to how the man met his death. There are marks of blood in the room, but there is no wound upon his person. We are at a loss as to how he came into the empty house; indeed, the whole affair is a puzzler. If you can come round to the house any time before twelve, you will find me there. I have left everything in statu quo until I hear from you. If you are unable to come, I shall give you fuller details, and would esteem it a great kindness if you would favour me with your opinion.

+
+

          "Yours faithfully,
+                   " Tobias Gregson."

+
+

"Gregson is the smartest of the Scotland Yarders," my friend remarked; "he and Lestrade are the pick of a bad lot. They are both quick and energetic, but conventional—shockingly so. They have their knives into one another, too. They are as jealous as a pair of professional beauties. There will be some fun over this case if they are both put upon the scent."

+

I was amazed at the calm way in which he rippled on. "Surely there is not a moment to be lost," I cried; "shall I go and order you a cab?"

+

"I'm not sure about whether I shall go. I am the most incurably lazy devil that ever stood in shoe leather—that is, when the fit is on me, for I can be spry enough at times."

+

"Why, it is just such a chance as you have been longing for."

+

"My dear fellow, what does it matter to me? Supposing I unravel the whole matter, you may be sure that Gregson, Lestrade, and Co. will pocket all the credit. That comes of being an unofficial personage."

+

"But he begs you to help him."

+

"Yes. He knows that I am his superior, and acknowledges it to me; but he would cut his tongue out before he would own it to any third person. However, we may as well go and have a look. I shall work it out on my own hook. I may have a laugh at them, if I have nothing else. Come on!"

+

He hustled on his overcoat, and bustled about in a way that showed that an energetic fit had superseded the apathetic one.

+

"Get your hat," he said.

+

"You wish me to come?"

+

"Yes, if you have nothing better to do." A minute later we were both in a hansom, driving furiously for the Brixton Road.

+

It was a foggy, cloudy morning, and a dun-coloured veil hung over the house-tops, looking like the reflection of the mud-coloured streets beneath. My companion was in the best of spirits, and prattled away about Cremona fiddles, and the difference between a Stradivarius and an Amati. As for myself, I was silent, for the dull weather and the melancholy business upon which we were engaged, depressed my spirits.

+

"You don't seem to give much thought to the matter in hand," I said at last, interrupting Holmes' musical disquisition.

+

"No data yet," he answered. "It is a capital mistake to theorize before you have all the evidence. It biases the judgment."

+

"You will have your data soon," I remarked, pointing with my finger; "this is the Brixton Road, and that is the house, if I am not very much mistaken."

+

"So it is. Stop, driver, stop!" We were still a hundred yards or so from it, but he insisted upon our alighting, and we finished our journey upon foot.

+

Number 3, Lauriston Gardens wore an ill-omened and minatory look. It was one of four which stood back some little way from the street, two being occupied and two empty. The latter looked out with three tiers of vacant melancholy windows, which were blank and dreary, save that here and there a "To Let" card had developed like a cataract upon the bleared panes. A small garden sprinkled over with a scattered eruption of sickly plants separated each of these houses from the street, and was traversed by a narrow pathway, yellowish in colour, and consisting apparently of a mixture of clay and of gravel. The whole place was very sloppy from the rain which had fallen through the night. The garden was bounded by a three-foot brick wall with a fringe of wood rails upon the top, and against this wall was leaning a stalwart police constable, surrounded by a small knot of loafers, who craned their necks and strained their eyes in the vain hope of catching some glimpse of the proceedings within.

+

I had imagined that Sherlock Holmes would at once have hurried into the house and plunged into a study of the mystery. Nothing appeared to be further from his intention. With an air of nonchalance which, under the circumstances, seemed to me to border upon affectation, he lounged up and down the pavement, and gazed vacantly at the ground, the sky, the opposite houses and the line of railings. Having finished his scrutiny, he proceeded slowly down the path, or rather down the fringe of grass which flanked the path, keeping his eyes riveted upon the ground. Twice he stopped, and once I saw him smile, and heard him utter an exclamation of satisfaction. There were many marks of footsteps upon the wet clayey soil; but since the police had been coming and going over it, I was unable to see how my companion could hope to learn anything from it. Still I had had such extraordinary evidence of the quickness of his perceptive faculties, that I had no doubt that he could see a great deal which was hidden from me.

+

At the door of the house we were met by a tall, white-faced, flaxen-haired man, with a notebook in his hand, who rushed forward and wrung my companion's hand with effusion. "It is indeed kind of you to come," he said, "I have had everything left untouched."

+

"Except that!" my friend answered, pointing at the pathway. "If a herd of buffaloes had passed along there could not be a greater mess. No doubt, however, you had drawn your own conclusions, Gregson, before you permitted this."

+

"I have had so much to do inside the house," the detective said evasively. "My colleague, Mr. Lestrade, is here. I had relied upon him to look after this."

+

Holmes glanced at me and raised his eyebrows sardonically. "With two such men as yourself and Lestrade upon the ground, there will not be much for a third party to find out," he said.

+

Gregson rubbed his hands in a self-satisfied way. "I think we have done all that can be done," he answered; "it's a queer case though, and I knew your taste for such things."

+

"You did not come here in a cab?" asked Sherlock Holmes.

+

"No, sir."

+

"Nor Lestrade?"

+

"No, sir."

+

"Then let us go and look at the room." With, which inconsequent remark he strode on into the house, followed by Gregson, whose features expressed his astonishment.

+

A short passage, bare-planked and dusty, led to the kitchen and offices. Two doors opened out of it to the left and to the right. One of these had obviously been closed for many weeks. The other belonged to the dining-room, which was the apartment in which the mysterious affair had occurred. Holmes walked in, and I followed him with that subdued feeling at my heart which the presence of death inspires.

+

It was a large square room, looking all the larger from the absence of all furniture. A vulgar flaring paper adorned the walls, but it was blotched in places with mildew; and here and there great strips had become detached and hung down, exposing the yellow plaster beneath. Opposite the door was a showy fireplace, surmounted by a mantelpiece of imitation white marble. On one corner of this was stuck the stump of a red wax candle. The solitary window was so dirty that the light was hazy and uncertain, giving a dull grey tinge to everything, which was intensified by the thick layer of dust which coated the whole apartment.

+ +

All these details I observed afterwards. At present my attention was centred upon the single, grim, motionless figure which lay stretched upon the boards, with vacant, sightless eyes staring up at the discoloured ceiling. It was that of a man about forty-three or forty-four years of age, middle-sized, broad-shouldered, with crisp curling black hair, and a short, stubbly beard. He was dressed in a heavy broadcloth frock coat and waistcoat, with light-coloured trousers, and immaculate collar and cuffs. A top hat, well brushed and trim, was placed upon the floor beside him. His hands were clenched and his arms thrown abroad, while his lower limbs were interlocked, as though his death struggle had been a grievous one. On his rigid face there stood an expression of horror, and as it seemed to me, of hatred, such as I have never seen upon human features. This malignant and terrible contortion, combined with the low forehead, blunt nose, and prognathous jaw, gave the dead man a singularly simious and ape-like appearance, which was increased by his writhing, unnatural posture. I have seen death in many forms, but never has it appeared to me in a more fearsome aspect than in that dark, grimy apartment, which looked out upon one of the main arteries of suburban London.

+

Lestrade, lean and ferret-like as ever, was standing by the door-way, and greeted my companion and myself.

+

"This case will make a stir, sir," he remarked. "It beats anything I have seen, and I am no chicken."

+

"There is no clue?" said Gregson.

+

"None at all," chimed in Lestrade.

+

Sherlock Holmes approached the body, and kneeling down, examined it intently. "You are sure that there is no wound?" he asked, pointing to numerous gouts and splashes of blood which lay all round.

+

"Positive!" cried both detectives.

+

"Then, of course, this blood belongs to a second individual—presumably the murderer, if murder has been committed. It reminds me of the circumstances attendant on the death of Van Jansen, in Utrecht, in the year '34. Do you remember the case, Gregson?"

+

"No, sir."

+

"Read it up—you really should. There is nothing new under the sun. It has all been done before."

+

As he spoke, his nimble fingers were flying here, there, and everywhere, feeling, pressing, unbuttoning, examining, while his eyes wore the same far-away expression which I have already remarked upon. So swiftly was the examination made, that one would hardly have guessed the minuteness with which it was conducted. Finally, he sniffed the dead man's lips, and then glanced at the soles of his patent leather boots.

+

"He has not been moved at all?" he asked.

+

"No more than was necessary for the purposes of our examination."

+

"You can take him to the mortuary now," he said. "There is nothing more to be learned."

+

Gregson had a stretcher and four men at hand. At his call they entered the room, and the stranger was lifted and carried out. As they raised him, a ring tinkled down and rolled across the floor. Lestrade grabbed it up and stared at it with mystified eyes.

+

"There's been a woman here," he cried. "It's a woman's wedding-ring."

+

He held it out as he spoke, upon the palm of his hand. We all gathered round him and gazed at it. There could be no doubt that that circlet of plain gold had once adorned the finger of a bride.

+

"This complicates matters," said Gregson. "Heaven knows, they were complicated enough before."

+

"You're sure it doesn't simplify them?" observed Holmes. "There's nothing to be learned by staring at it. What did you find in his pockets?"

+

"We have it all here," said Gregson, pointing to a litter of objects upon one of the bottom steps of the stairs. "A gold watch, No. 97163, by Barraud, of London. Gold Albert chain, very heavy and solid. Gold ring, with masonic device. Gold pin—bull-dog's head, with rubies as eyes. Russian leather card-case, with cards of Enoch J. Drebber of Cleveland, corresponding with the E. J. D. upon the linen. No purse, but loose money to the extent of seven pounds thirteen. Pocket edition of Boccaccio's Decameron, with name of Joseph Stangerson upon the fly-leaf. Two letters—one addressed to E. J. Drebber and one to Joseph Stangerson."

+

"At what address?"

+

"American Exchange, Strand—to be left till called for. They are both from the Guion Steamship Company, and refer to the sailing of their boats from Liverpool. It is clear that this unfortunate man was about to return to New York."

+

"Have you made any inquiries as to this man Stangerson?"

+

"I did it at once, sir," said Gregson. "I have had advertisements sent to all the newspapers, and one of my men has gone to the American Exchange, but he has not returned yet."

+

"Have you sent to Cleveland?"

+

"We telegraphed this morning."

+

"How did you word your inquiries?"

+

"We simply detailed the circumstances, and said that we should be glad of any information which could help us."

+

"You did not ask for particulars on any point which appeared to you to be crucial?"

+

"I asked about Stangerson."

+

"Nothing else? Is there no circumstance on which this whole case appears to hinge? Will you not telegraph again?"

+

"I have said all I have to say," said Gregson, in an offended voice.

+

Sherlock Holmes chuckled to himself, and appeared to be about to make some remark, when Lestrade, who had been in the front room while we were holding this conversation in the hall, reappeared upon the scene, rubbing his hands in a pompous and self-satisfied manner.

+

"Mr. Gregson," he said, "I have just made a discovery of the highest importance, and one which would have been overlooked had I not made a careful examination of the walls."

+

The little man's eyes sparkled as he spoke, and he was evidently in a state of suppressed exultation at having scored a point against his colleague.

+

"Come here," he said, bustling back into the room, the atmosphere of which felt clearer since the removal of its ghastly inmate.

+

"Now stand there!"

+

He struck a match on his boot and held it up against the wall.

+ +

"Look at that!" he said, triumphantly.

+

I have remarked that the paper had fallen away in parts. In this particular corner of the room a large piece had peeled off, leaving a yellow square of coarse plastering. Across this bare space there was scrawled in blood-red letters a single word —

+

RACHE.

+

"What do you think of that?" cried the detective, with the air of a showman exhibiting his show. "This was overlooked because it was in the darkest corner of the room, and no one thought of looking there. The murderer has written it with his or her own blood. See this smear where it has trickled down the wall! That disposes of the idea of suicide anyhow. Why was that corner chosen to write it on? I will tell you. See that candle on the mantelpiece. It was lit at the time, and if it was lit this corner would be the brightest instead of the darkest portion of the wall."

+

"And what does it mean now that you have found it?" asked Gregson in a depreciatory voice.

+

"Mean? Why, it means that the writer was going to put the female name Rachel, but was disturbed before he or she had time to finish. You mark my words, when this case comes to be cleared up, you will find that a woman named Rachel has something to do with it. It's all very well for you to laugh, Mr. Sherlock Holmes. You may be very smart and clever, but the old hound is the best, when all is said and done."

+

"I really beg your pardon!" said my companion, who had ruffled the little man's temper by bursting into an explosion of laughter. "You certainly have the credit of being the first of us to find this out and, as you say, it bears every mark of having been written by the other participant in last night's mystery. I have not had time to examine this room yet, but with your permission I shall do so now."

+

As he spoke he whipped a tape measure and a large round magnifying glass from his pocket. With these two implements he trotted noiselessly about the room, sometimes stopping, occasionally kneeling, and once lying flat upon his face. So engrossed was he with his occupation that he appeared to have forgotten our presence, for he chattered away to himself under his breath the whole time, keeping up a running fire of exclamations, groans, whistles, and little cries suggestive of encouragement and of hope. As I watched him I was irresistibly reminded of a pure-blooded, well-trained foxhound as it dashes backwards and forwards through the covert, whining in its eagerness, until it comes across the lost scent. For twenty minutes or more he continued his researches, measuring with the most exact care the distance between marks which were entirely invisible to me, and occasionally applying his tape to the walls in an equally incomprehensible manner. In one place he gathered up very carefully a little pile of grey dust from the floor, and packed it away in an envelope. Finally he examined with his glass the word upon the wall, going over every letter of it with the most minute exactness. This done, he appeared to be satisfied, for he replaced his tape and his glass in his pocket.

+

"They say that genius is an infinite capacity for taking pains," he remarked with a smile. "It's a very bad definition, but it does apply to detective work."

+

Gregson and Lestrade had watched the manoeuvres of their amateur companion with considerable curiosity and some contempt. They evidently failed to appreciate the fact, which I had begun to realize, that Sherlock Holmes' smallest actions were all directed towards some definite and practical end.

+

"What do you think of it, sir?" they both asked.

+

"It would be robbing you of the credit of the case if I was to presume to help you," remarked my friend. " You are doing so well now that it would be a pity for any one to interfere." There was a world of sarcasm in his voice as he spoke. "If you will let me know how your investigations go," he continued, "I shall be happy to give you any help I can. In the meantime I should like to speak to the constable who found the body. Can you give me his name and address?"

+

Lestrade glanced at his notebook; "John Rance," he said. "He is off duty now. You will find him at 46, Audley Court, Kennington Park Gate."

+

Holmes took a note of the address.

+

"Come along, Doctor," he said "we shall go and look him up. I'll tell you one thing which may help you in the case," he continued turning to the two detectives. " There has been murder done, and the murderer was a man. He was more than six feet high, was in the prime of life, had small feet for his height, wore coarse, square-toed boots and smoked a Trichinopoly cigar. He came here with his victim in a four-wheeled cab, which was drawn by a horse with three old shoes and one new one on his off fore-leg. In all probability the murderer had a florid face, and the finger-nails of his right hand were remarkably long. These are only a few of the indications, but they may assist you."

+

Lestrade and Gregson glanced at each other with an incredulous smile.

+

"If this man was murdered, how was it done?" asked the former.

+

"Poison," said Sherlock Holmes curtly, and strode off. "One other thing, Lestrade," he added, turning round at the door: "'Rache,' is the German for 'revenge'; so don't lose your time looking for Miss Rachel."

+

With which Parthian shot he walked away, leaving the two rivals open-mouthed behind him.

+
+
+ + diff --git a/books/a-study-in-scarlet/Part1-04.html b/books/a-study-in-scarlet/Part1-04.html new file mode 100644 index 0000000..fe087ed --- /dev/null +++ b/books/a-study-in-scarlet/Part1-04.html @@ -0,0 +1,75 @@ + + + + 1.4 • What John Rance Had to Tell + + + + +
+

What John Rance Had to Tell

+ +
+

It was one o'clock when we left No. 3, Lauriston Gardens. Sherlock Holmes led me to the nearest telegraph office, whence he dispatched a long telegram. He then hailed a cab, and ordered the driver to take us to the address given us by Lestrade.

+

"There is nothing like first-hand evidence," he remarked; "as a matter of fact, my mind is entirely made up upon the case, but still we may as well learn all that is to be learned."

+

"You amaze me, Holmes," said I. "Surely you are not as sure as you pretend to be of all those particulars which you gave."

+

"There's no room for a mistake," he answered. "The very first thing which I observed on arriving there was that a cab had made two ruts with its wheels close to the curb. Now, up to last night, we have had no rain for a week, so that those wheels which left such a deep impression must have been there during the night. There were the marks of the horse's hoofs, too, the outline of one of which was far more clearly cut than that of the other three, showing that that was a new shoe. Since the cab was there after the rain began, and was not there at any time during the morning—I have Gregson's word for that—it follows that it must have been there during the night, and, therefore, that it brought those two individuals to the house."

+

"That seems simple enough," said I; "but how about the other man's height?"

+

"Why, the height of a man, in nine cases out of ten, can be told from the length of his stride. It is a simple calculation enough, though there is no use my boring you with figures. I had this fellow's stride both on the clay outside and on the dust within. Then I had a way of checking my calculation. When a man writes on the wall, his instinct leads him to write about the level of his own eyes. Now that writing was just over six feet from the ground. It was child's play."

+

"And his age?" I asked.

+

"Well, if a man can stride four and a-half feet without the smallest effort, he can't be quite in the sere and yellow. That was the breadth of a puddle on the garden walk which he had evidently walked across. Patent-leather boots had gone round, and Square-toes had hopped over. There is no mystery about it at all. I am simply applying to ordinary life a few of those precepts of observation and deduction which I advocated in that article. Is there anything else that puzzles you?"

+

"The finger-nails and the Trichinopoly," I suggested.

+

"The writing on the wall was done with a man's fore-finger dipped in blood. My glass allowed me to observe that the plaster was slightly scratched in doing it, which would not have been the case if the man's nail had been trimmed. I gathered up some scattered ash from the floor. It was dark in colour and flakey—such an ash as is only made by a Trichinopoly. I have made a special study of cigar ashes—in fact, I have written a monograph upon the subject. I flatter myself that I can distinguish at a glance the ash of any known brand either of cigar or of tobacco. It is just in such details that the skilled detective differs from the Gregson and Lestrade type."

+

"And the florid face?" I asked.

+

"Ah, that was a more daring shot, though I have no doubt that I was right. You must not ask me that at the present state of the affair."

+

I passed my hand over my brow. "My head is in a whirl," I remarked; "the more one thinks of it the more mysterious it grows. How came these two men—if there were two men—into an empty house? What has become of the cabman who drove them? How could one man compel another to take poison? Where did the blood come from? What was the object of the murderer, since robbery had no part in it? How came the woman's ring there? Above all, why should the second man write up the German word RACHE before decamping? I confess that I cannot see any possible way of reconciling all these facts."

+

My companion smiled approvingly.

+

"You sum up the difficulties of the situation succinctly and well," he said. "There is much that is still obscure, though I have quite made up my mind on the main facts. As to poor Lestrade's discovery, it was simply a blind intended to put the police upon a wrong track, by suggesting Socialism and secret societies. It was not done by a German. The A, if you noticed, was printed somewhat after the German fashion. Now a real German invariably prints in the Latin character, so that we may safely say that this was not written by one, but by a clumsy imitator who overdid his part. It was simply a ruse to divert inquiry into a wrong channel. I'm not going to tell you much more of the case, Doctor. You know a conjuror gets no credit when once he has explained his trick; and if I show you too much of my method of working, you will come to the conclusion that I am a very ordinary individual after all."

+

"I shall never do that," I answered ; "you have brought detection as near an exact science as it ever will be brought in this world."

+

My companion flushed up with pleasure at my words, and the earnest way in which I uttered them. I had already observed that he was as sensitive to flattery on the score of his art as any girl could be of her beauty.

+

"I'll tell you one other thing," he said. "Patent-leathers and Square-toes came in the same cab, and they walked down the pathway together as friendly as possible—arm-in-arm, in all probability. When they got inside, they walked up and down the room—or rather, Patent-leathers stood still while Square-toes walked up and down. I could read all that in the dust; and I could read that as he walked he grew more and more excited. That is shown by the increased length of his strides. He was talking all the while, and working himself up, no doubt, into a fury. Then the tragedy occurred. I've told you all I know myself now, for the rest is mere surmise and conjecture. We have a good working basis, however, on which to start. We must hurry up, for I want to go to Halle's concert to hear Norman Neruda this afternoon."

+

This conversation had occurred while our cab had been threading its way through a long succession of dingy streets and dreary by-ways. In the dingiest and dreariest of them our driver suddenly came to a stand. "That's Audley Court in there," he said, pointing to a narrow slit in the line of dead-coloured brick. "You'll find me here when you come back."

+

Audley Court was not an attractive locality. The narrow passage led us into a quadrangle paved with flags and lined by sordid dwellings. We picked our way among groups of dirty children, and through lines of discoloured linen, until we came to Number 46, the door of which was decorated with a small slip of brass on which the name Rance was engraved. On inquiry we found that the constable was in bed, and we were shown into a little front parlour to await his coming.

+ +

He appeared presently, looking a little irritable at being disturbed in his slumbers. "I made my report at the office," he said.

+

Holmes took a half-sovereign from his pocket and played with it pensively. "We thought that we should like to hear it all from your own lips," he said.

+

"I shall be most happy to tell you anything I can," the constable answered, with his eyes upon the little golden disk.

+

"Just let us hear it all in your own way as it occurred."

+

Rance sat down on the horse-hair sofa, and knitted his brows, as though determined not to omit anything in his narrative.

+

"I'll tell it ye from the beginning," he said. "My time is from ten at night to six in the morning. At eleven there was a fight at the 'White Hart'; but bar that all was quiet enough on the beat. At one o'clock it began to rain, and I met Harry Murcher—him who has the Holland Grove beat—and we stood together at the corner of Henrietta Street a-talkin'. Presently—maybe about two or a little after—I thought I would take a look round and see that all was right down the Brixton Road. It was precious dirty and lonely. Not a soul did I meet all the way down, though a cab or two went past me. I was a-strollin' down, thinkin' between ourselves how uncommon handy a four of gin hot would be, when suddenly the glint of a light caught my eye in the window of that same house. Now, I knew that them two houses in Lauriston Gardens was empty on account of him that owns them who won't have the drains seed too, though the very last tenant what lived in one of them died o' typhoid fever. I was knocked all in a heap, therefore, at seeing a light in the window, and I suspected as something was wrong. When I got to the door——"

+

"You stopped, and then walked back to the garden gate," my companion interrupted. "What did you do that for?"

+

Rance gave a violent jump, and stared at Sherlock Holmes with the utmost amazement upon his features.

+

"Why, that's true sir," he said; "though how you come to know it, Heaven only knows. Ye see when I got up to the door, it was so still and so lonesome, that I thought I'd be none the worse for some one with me. I ain't afeard of anything on this side o' the grave; but I thought that maybe it was him that died o' the typhoid inspecting the drains what killed him. The thought gave me a kind o' turn, and I walked back to the gate to see if I could see Murcher's lantern, but there wasn't no sign of him nor of any one else."

+

"There was no one in the street?"

+

"Not a livin' soul, sir, nor as much as a dog. Then I pulled myself together and went back and pushed the door open. All was quiet inside, so I went into the room where the light was a-burnin'. There was a candle flickering on the mantelpiece—a red wax one—and by its light I saw——"

+

"Yes, I know all that you saw. You walked round the room several times, and you knelt down by the body, and then you walked through and tried the kitchen door, and then——"

+

John Rance sprang to his feet with a frightened face and suspicion in his eyes. "Where was you hid to see all that?" he cried. "It seems to me that you knows a deal more than you should."

+

Holmes laughed and threw his card across the table to the constable. "Don't get arresting me for the murder," he said. "I am one of the hounds and not the wolf; Mr. Gregson or Mr. Lestrade will answer for that. Go on, though. What did you do next?"

+

Rance resumed his seat, without, however, losing his mystified expression. "I went back to the gate and sounded my whistle. That brough Murcher and two more to the spot."

+

"Was the street empty then?"

+

"Well, it was, as far as anybody that could be of any good goes."

+

"What do you mean?"

+

The constable's features broadened into a grin. "I've seen many a drunk chap in my time," he said, "by never any one so cryin' drunk as that cove. He was at the gate when I came out, a-leaning up ag'in the railings, and singin' at the pitch o' his lungs about Columbine's New-fangled Banner, or some such stuff. He couldn't stand, far less help."

+ +

"What sort of a man was he?" asked Sherlock Holmes.

+

John Rance appeared to be somewhat irritated at this digression: "He was an uncommon drunk sort o' man," he said. "He'd ha' found hisself in the station if we hadn't been so took up."

+

"His face—his dress—didn't you notice them?" Holmes broke in impatiently.

+

"I should think I did notice them, seeing that I had to prop him up—me and Murcher between us. He was a long chap, with a red face, the lower part muffled round——"

+

"That will do," cried Holmes. "What became of him?"

+

"We'd enough to do without lookin' after him," the policeman said, in an aggrieved voice. "I'll wager he found his way home all right."

+

"How was he dressed?"

+

"A brown overcoat."

+

"Had he a whip in his hand?"

+

"A whip—no."

+

"He must have left it behind," muttered my companion. "You didn't happen to see or hear a cab after that?"

+

"No."

+

"There's a half-sovereign for you," my companion said, standing up and taking his hat. "I am afraid, Rance, that you will never rise in the force. That head of yours should be for use as well as ornament. You might have gained your sergeant's stripes last night. The man whom you held in your hands is the man who holds the clue of this mystery, and whom we are seeking. There is no use of arguing about it now; I tell you that it is so. Come along, Doctor."

+

We started off for the cab together, leaving our informant incredulous, but obviously uncomfortable.

+

"The blundering fool!" Holmes said, bitterly, as we drove back to our lodgings. "Just to think of his having such an incomparable bit of good luck, and not taking advantage of it."

+

"I am rather in the dark still. It is true that the description of this man tallies with your idea of the second party in this mystery. But why should he come back to the house after leaving it? That is not the way of criminals."

+

"The ring, man, the ring: that was what he came back for. If we have no other way of catching him, we can always bait our line with the ring. I shall have him, Doctor—I'll lay you two to one that I have him. I must thank you for it all. I might not have gone but for you, and so have missed the finest study I ever came across: a study in scarlet, eh? Why shouldn't we use a little art jargon. There's the scarlet thread of murder running through the colourless skein of life, and our duty is to unravel it, and isolate it, and expose every inch of it. And now for lunch, and then for Norman Neruda. Her attack and her bowing are splendid. What's that little thing of Chopin's she plays so magnificently: Tra-la-la-lira-lira-lay."

+

Leaning back in the cab, this amateur blood-hound carolled away like a lark while I meditated upon the manysidedness of the human mind.

+
+
+ + diff --git a/books/a-study-in-scarlet/Part1-05.html b/books/a-study-in-scarlet/Part1-05.html new file mode 100644 index 0000000..d1c1bc4 --- /dev/null +++ b/books/a-study-in-scarlet/Part1-05.html @@ -0,0 +1,71 @@ + + + + 1.5 • Our Advertisement Brings a Visitor + + + + +
+

Our Advertisement Brings a Visitor

+ +
+

Our morning's exertions had been too much for my weak health, and I was tired out in the afternoon. After Holmes' departure for the concert, I lay down upon the sofa and endeavoured to get a couple of hours' sleep. It was a useless attempt. My mind had been too much excited by all that had occurred, and the strangest fancies and surmises crowded into it. Every time that I closed my eyes I saw before me the distorted baboon-like countenance of the murdered man. So sinister was the impression which that face had produced upon me that I found it difficult to feel anything but gratitude for him who had removed its owner from the world. If ever human features bespoke vice of the most malignant type, they were certainly those of Enoch J. Drebber, of Cleveland. Still I recognized that justice must be done, and that the depravity of the victim was no condonement in the eyes of the law.

+

The more I thought of it the more extraordinary did my companion's hypothesis, that the man had been poisoned, appear. I remembered how he had sniffed his lips, and had no doubt that he had detected something which had given rise to the idea. Then, again, if not poison, what had caused the man's death, since there was neither wound nor marks of strangulation? But on the other hand, whose blood was that which lay so thickly upon the floor? There were no signs of a struggle, nor had the victim any weapon with which he might have wounded an antagonist. As long as all these questions were unsolved, I felt that sleep would be no easy matter, either for Holmes or myself. His quiet, self-confident manner convinced me that he had already formed a theory which explained all the facts, though what it was I could not for an instant conjecture.

+

He was very late in returning—so late that I knew that the concert could not have detained him all the time. Dinner was on the table before he appeared.

+

"It was magnificent," he said, as he took his seat. "Do you remember what Darwin says about music? He claims that the power of producing and appreciating it existed among the human race long before the power of speech was arrived at. Perhaps that is why we are so subtly influenced by it. There are vague memories in our souls of those misty centuries when the world was in its childhood."

+

"That's rather a broad idea," I remarked.

+

"One's ideas must be as broad as Nature if they are to interpret Nature," he answered. "What's the matter? You're not looking quite yourself. This Brixton Road affair has upset you."

+

"To tell the truth, it has," I said. "I ought to be more case-hardened after my Afghan experiences. I saw my own comrades hacked to pieces at Maiwand without losing my nerve."

+

"I can understand. There is a mystery about this which stimulates the imagination; where there is no imagination there is no horror. Have you seen the evening paper?"

+

"No."

+

"It gives a fairly good account of the affair. It does not mention the fact that when the man was raised up a woman's wedding ring fell upon the floor. It is just as well it does not."

+

"Why?"

+

"Look at this advertisement," he answered. "I had one sent to every paper this morning immediately after the affair."

+

He threw the paper across to me and I glanced at the place indicated. It was the first announcement in the "Found" column. "In Brixton Road, this morning," it ran, "a plain gold wedding ring, found in the roadway between the White Hart Tavern and Holland Grove. Apply Dr. Watson, 221B, Baker Street, between eight and nine this evening."

+

"Excuse my using your name," he said. "If I used my own, some of those dunderheads would recognize it, and want to meddle in the affair."

+

"That is all right," I answered. "But supposing any one applies, I have no ring."

+

"Oh yes, you have," said he, handing me one. "This will do very well. It is almost a facsimile."

+

"And who do you expect will answer this advertisement?"

+

"Why, the man in the brown coat—our florid friend with the square toes. If he does not come himself, he will send an accomplice."

+

"Would he not consider it as too dangerous?"

+

"Not at all. If my view of the case is correct, and I have every reason to believe that it is, this man would rather risk anything than lose the ring. According to my notion he dropped it while stooping over Drebber's body, and did not miss it at the time. After leaving the house he discovered his loss and hurried back, but found the police already in possession, owing to his own folly in leaving the candle burning. He had to pretend to be drunk in order to allay the suspicions which might have been aroused by his appearance at the gate. Now put yourself in that man's place. On thinking the matter over, it must have occurred to him that it was possible that he had lost the ring in the road after leaving the house. What would he do then? He would eagerly look out for the evening papers in the hope of seeing it among the articles found. His eye, of course, would light upon this. He would be overjoyed. Why should he fear a trap? There would be no reason in his eyes why the finding of the ring should be connected with the murder. He would come. He will come. You shall see him within an hour?"

+

"And then?" I asked.

+

"Oh, you can leave me to deal with him then. Have you any arms?"

+

"I have my old service revolver and a few cartridges."

+

"You had better clean it and load it. He will be a desperate man; and though I shall take him unawares, it is as well to be ready for anything."

+

I went to my bedroom and followed his advice. When I returned with the pistol, the table had been cleared, and Holmes was engaged in his favourite occupation of scraping upon his violin.

+

"The plot thickens," he said, as I entered; "I have just had an answer to my American telegram. My view of the case is the correct one."

+

"And that is?" I asked eagerly.

+

"My fiddle would be the better for new strings," he remarked. "Put your pistol in your pocket. When the fellow comes, speak to him in an ordinary way. Leave the rest to me. Don't frighten him by looking at him too hard."

+

"It is eight o'clock now," I said, glancing at my watch.

+

"Yes. He will probably be here in a few minutes. Open the door slightly. That will do. Now put the key on the inside. Thank you! This is a queer old book I picked up at a stall yesterday—De Jure inter Gentes—published in Latin at Liege in the Lowlands, in 1642. Charles's head was still firm on his shoulders when this little brown-backed volume was struck off."

+

"Who is the printer?"

+

"Philippe de Croy, whoever he may have been. On the fly-leaf, in very faded ink, is written 'Ex-libris Guliolmi Whyte.' I wonder who William Whyte was. Some pragmatical seventeenth century lawyer, I suppose. His writing has a legal twist about it. Here comes our man, I think."

+

As he spoke there was a sharp ring at the bell. Sherlock Holmes rose softly and moved his chair in the direction of the door. We heard the servant pass along the hall, and the sharp click of the latch as she opened it.

+

"Does Dr. Watson live here?" asked a clear but rather harsh voice. We could not hear the servant's reply, but the door closed, and some one began to ascend the stairs. The footfall was an uncertain and shuffling one. A look of surprise passed over the face of my companion as he listened to it. It came slowly along the passage, and there was a feeble tap at the door:

+

"Come in," I cried.

+ +

At my summons, instead of the man of violence whom we expected, a very old and wrinkled woman hobbled into the apartment. She appeared to be dazzled by the sudden blaze of light, and after dropping a curtsey, she stood blinking at us with her bleared eyes and fumbling in her pocket with nervous, shaky fingers. I glanced at my companion, and his face had assumed such a disconsolate expression that it was all I could do to keep my countenance.

+

The old crone drew out an evening paper, and pointed at our advertisement. "It's this as has brought me, good gentlemen," she said, dropping another curtsey; "a gold wedding ring in the Brixton Road. It belongs to my girl Sally, as was married only this time twelvemonth, which her husband is steward aboard a Union boat, and what he'd say if he come 'ome and found her without her ring is more than I can think, he being short enough at the best of times, but more especially when he has the drink. If it pleases you, she went to the circus last night along with——"

+

"Is that her ring?" I asked.

+

"The Lord be thanked!" cried the old woman; "Sally will be a glad woman this night: That's the ring."

+

"And what may your address be?" I inquired; taking up a pencil.

+

"13, Duncan Street, Houndsditch. A weary way from here."

+

"The Brixton Road does not lie between any circus and Houndsditch," said Sherlock Holmes sharply.

+

The old woman faced round and looked keenly at him from her little red-rimmed eyes. "The gentleman asked me for my address," she said. "Sally lives in lodgings at 3, Mayfield Place, Peckham."

+

"And your name is——?"

+

"My name is Sawyer—hers is Dennis, which Tom Dennis married her—and a smart, clean lad, too, as long as he's at sea, and no steward in the company more thought of; but when on shore, what with the women and what with liquor shops——"

+

"Here is your ring, Mrs. Sawyer," I interrupted, in obedience to a sign from my companion; "it clearly belongs to your daughter, and I am glad to be able to restore it to the rightful owner."

+

With many mumbled blessings and protestations of gratitude the old crone packed it away in her pocket, and shuffled off down the stairs. Sherlock Holmes sprang to his feet the moment that she was gone and rushed into his room. He returned in a few seconds enveloped in an ulster and a cravat. "I'll follow her," he said, hurriedly; "she must be an accomplice, and will lead me to him. Wait up for me." The hall door had hardly slammed behind our visitor before Holmes had descended the stair. Looking through the window I could see her walking feebly along the other side while her pursuer dogged her some little distance behind. "Either his whole theory is incorrect," I thought to myself," or else he will be led now to the heart of the mystery." There was no need for him to ask me to wait up for him, for I felt that sleep was impossible until I heard the result of his adventure.

+

It was close upon nine when he set out. I had no idea how long he might be, but I sat stolidly puffing at my pipe and skipping over the pages of Henri Murger's Vie de Bohème. Ten o'clock passed, and I heard the footsteps of the maid as they pattered off to bed. Eleven, and the more stately tread of the landlady passed my door, bound for the same destination. It was close upon twelve before I heard the sharp sound of his latch-key. The instant he entered I saw by his face that he had not been successful. Amusement and chagrin seemed to be struggling for the mastery, until the former suddenly carried the day, and he burst into a hearty laugh.

+

"I wouldn't have the Scotland Yarders know it for the world," he cried dropping into his chair; "I have chaffed them so much that they would never have let me hear the end of it. I can afford to laugh, because I know that I will be even with them in the long run."

+

"What is it then?" I asked.

+

"Oh, I don't mind telling a story against myself. That creature had gone a little way when she began to limp and show every sign of being foot-sore. Presently she came to a halt, and hailed a four-wheeler which was passing. I managed to be close to her so as to hear the address, but I need not have been so anxious, for she sang it out loud enough to be heard at the other side of the street, 'Drive to 13, Duncan Street, Houndsditch,' she cried. This begins to look genuine, I thought, and having seen her safely inside, I perched myself behind. That's an art which every detective should be an expert at. Well, away we rattled, and never drew rein until we reached the street in question. I hopped off before we came to the door, and strolled down the street in an easy lounging way. I saw the cab pull up. The driver jumped down, and I saw him open the door and stand expectantly. Nothing came out though. When I reached him, he was groping about frantically in the empty cab, and giving vent to the finest assorted collection of oaths that ever I listened to. There was no sign or trace of his passenger, and I fear it will be some time before he gets his fare. On inquiring at Number 13 we found that the house belonged to a respectable paperhanger, named Keswick, and that no one of the name either of Sawyer or Dennis had ever been heard of there."

+

"You don't mean to say," I cried, in amazement, "that that tottering, feeble old woman was able to get out of the cab while it was in motion, without either you or the driver seeing her?"

+

"Old woman be damned!" said Sherlock Holmes, sharply. "We were the old women to be so taken in. It must have been a young man, and an active one, too, besides being an incomparable actor. The get-up was inimitable. He saw that he was followed, no doubt, and used this means of giving me the slip. It shows that the man we are after is not as lonely as I imagined he was, but has friends who are ready to risk something for him. Now, Doctor, you are looking done-up. Take my advice and turn in."

+

I was certainly feeling very weary, so I obeyed his injunction. I left Holmes seated in front of the smouldering fire, and long into the watches of the night I heard the low, melancholy wailings of his violin, and knew that he was still pondering over the strange problem which he had set himself to unravel.

+
+
+ + diff --git a/books/a-study-in-scarlet/Part1-06.html b/books/a-study-in-scarlet/Part1-06.html new file mode 100644 index 0000000..cabe771 --- /dev/null +++ b/books/a-study-in-scarlet/Part1-06.html @@ -0,0 +1,105 @@ + + + + 1.6 • Tobias Gregson Shows What He Can Do + + + + +
+

Tobias Gregson Shows What He Can Do

+ +
+

The papers next day were full of the "Brixton Mystery," as they termed it. Each had a long account of the affair, and some had leaders upon it in addition. There was some information in them which was new to me. I still retain in my scrap-book numerous clippings and extracts bearing upon the case. Here is a condensation of a few of them:—

+

The Daily Telegraph remarked that in the history of crime there had seldom been a tragedy which presented stranger features. The German name of the victim, the absence of all other motive, and the sinister inscription on the wall, all pointed to its perpetration by political refugees and revolutionists. The Socialists had many branches in America, and the deceased had, no doubt, infringed their unwritten laws and been tracked down by them. After alluding airily to the Vehmgericht, aqua tofana, Carbonari, the Marchioness de Brinvilliers, the Darwinian theory, the principles of Malthus, and the Ratcliff Highway murders, the article concluded by admonishing the Government and advocating a closer watch over foreigners in England.

+

The Standard commented upon the fact that lawless outrages of the sort usually occurred under a Liberal Administration. They arose from the unsettling of the minds of the masses, and the consequent weakening of all authority. The deceased was an American gentleman who had been residing for some weeks in the Metropolis. He had stayed at the boarding-house of Madame Charpentier, in Torquay Terrace, Camberwell. He was accompanied in his travels by his private secretary, Mr. Joseph Stangerson. The two bade adieu to their landlady upon Tuesday, the 4th inst., and departed to Euston Station with the avowed intention of catching the Liverpool express. They were afterwards seen together upon the platform. Nothing more is known of them until Mr. Drebber's body was, as recorded, discovered in an empty house in the Brixton Road, many miles from Euston. How he came there, or how he met his fate, are questions which are still involved in mystery. Nothing is known of the whereabouts of Stangerson. We are glad to learn that Mr. Lestrade and Mr. Gregson, of Scotland Yard, are both engaged upon the case, and it is confidently anticipated that these well-known officers will speedily throw light upon the matter.

+

The Daily News observed that there was no doubt as to the crime being a political one. The despotism and hatred of Liberalism which animated the Continental Governments had had the effect of driving to our shores a number of men who might have made excellent citizens were they not soured by the recollection of all that they had undergone. Among these men there was a stringent code of honour, any infringement of which was punished by death. Every effort should be made to find the secretary, Stangerson, and to ascertain some particulars of the habits of the deceased. A great step had been gained by the discovery of the address of the house at which he had boarded—a result which was entirely due to the acuteness and energy of Mr. Gregson of Scotland Yard.

+

Sherlock Holmes and I read these notices over together at breakfast, and they appeared to afford him considerable amusement.

+

"I told you that, whatever happened, Lestrade and Gregson would be sure to score."

+

"That depends on how it turns out."

+

"Oh, bless you, it doesn't matter in the least. If the man is caught, it will be on account of their exertions; if he escapes, it will be in spite of their exertions. It's heads I win and tails you lose. Whatever they do, they will have followers. 'Un sot trouve toujours un plus sot qui l'admire.'"

+

"What on earth is this?" I cried, for at this moment there came the pattering of many steps in the hall and on the stairs, accompanied by audible expressions of disgust upon the part of our landlady.

+

"It's the Baker Street division of the detective police force," said my companion gravely; and as he spoke there rushed into the room half a dozen of the dirtiest and most ragged street Arabs that ever I clapped eyes on.

+

"'Tention!" cried Holmes, in a sharp tone, and the six dirty little scoundrels stood in a line like so many disreputable statuettes. "In future you shall send up Wiggins alone to report, and the rest of you must wait in the street. Have you found it, Wiggins?"

+ +

"No, sir, we hain't," said one of the youths.

+

"I hardly expected you would. You must keep on until you do. Here are your wages." He handed each of them a shilling. "Now, off you go, and come back with a better report next time."

+

He waved his hand, and they scampered away downstairs like so many rats, and we heard their shrill voices next moment in the street.

+

"There's more work to be got out of one of those little beggars than out of a dozen of the force," Holmes remarked. "The mere sight of an official-looking person seals men's lips. These youngsters, however, go everywhere and hear everything. They are as sharp as needles, too; all they want is organization."

+

"Is it on this Brixton case that you are employing them?" I asked.

+

"Yes; there is a point which I wish to ascertain. It is merely a matter of time. Hullo! we are going to hear some news now with a vengeance ! Here is Gregson coming down the road with beatitude written upon every feature of his face. Bound for us, I know. Yes, he is stopping. There he is!"

+

There was a violent peal at the bell, and in a few seconds the fair-headed detective came up the stairs, three steps at a time, and burst into our sitting-room.

+

"My dear fellow," he cried wringing Holmes' unresponsive hand, "congratulate me! I have made the whole thing as clear as day."

+

A shade of anxiety seemed to me to cross my companion's expressive face.

+

"Do you mean that you are on the right track?" he asked.

+

"The right track! Why, sir, we have the man under lock and key."

+

"And his name is?"

+

"Arthur Charpentier, sub-lieutenant in Her Majesty's navy." cried Gregson pompously rubbing his fat hands and inflating his chest.

+

Sherlock Holmes gave a sigh of relief and relaxed into a smile.

+

"Take a seat, and try one of these cigars," he said. "We are anxious to know how you managed it. Will you have some whiskey and water?"

+

"I don't mind if I do," the detective answered. "The tremendous exertions which I have gone through during the last day or two have worn me out. Not so much bodily exertion, you understand, as the strain upon the mind. You will appreciate that, Mr. Sherlock Holmes, for we are both brain-workers."

+

"You do me too much honour," said Holmes, gravely. "Let us hear how you arrived at this most gratifying result."

+

The detective seated himself in the arm-chair, and puffed complacently at his cigar. Then suddenly he slapped his thigh in a paroxysm of amusement.

+

"The fun of it is," he cried, "that that fool Lestrade, who thinks himself so smart, has gone off upon the wrong track altogether. He is after the secretary Stangerson, who had no more to do with the crime than the babe unborn. I have no doubt that he has caught him by this time."

+

The idea tickled Gregson so much that he laughed until he choked.

+

"And how did you get your clue?"

+

"Ah, I'll tell you all about it. Of course, Doctor Watson, this is strictly between ourselves. The first difficulty which we had to contend with was the finding of this American's antecedents. Some people would have waited until their advertisements were answered, or until parties came forward and volunteered information. That is not Tobias Gregson's way of going to work. You remember the hat beside the dead man?"

+

"Yes," said Holmes; "by John Underwood and Sons, 129, Camberwell Road."

+

Gregson looked quite crest-fallen.

+

"I had no idea that you noticed that," he said. "Have you been there?"

+

"No."

+

"Ha!" cried Gregson, in a relieved voice; "you should never neglect a chance, however small it may seem."

+

"To a great mind, nothing is little," remarked Holmes, sententiously.

+

"Well, I went to Underwood, and asked him if he had sold a hat of that size and description. He looked over his books, and came on it at once. He had sent the hat to a Mr. Drebber, residing at Charpentier's Boarding Establishment, Torquay Terrace. Thus I got at his address."

+

"Smart—very smart!" murmured Sherlock Holmes.

+

"I next called upon Madame Charpentier," continued the detective: "I found her very pale and distressed. Her daughter was in the room too—an uncommonly fine girl she is too; she was looking red about the eyes and her lips trembled as I spoke to her. That didn't escape my notice. I began to smell a rat. You know the feeling, Mr. Sherlock Holmes, when you come upon the right scent—a kind of thrill in your nerves. 'Have you heard of the mysterious death of your late boarder Mr. Enoch J. Drebber, of Cleveland?' I asked.

+

"The mother nodded. She didn't seem able to get out a word. The daughter burst into tears. I felt more than ever that these people knew something of the matter.

+

"'At what o'clock did Mr. Drebber leave your house for the train?' I asked.

+

"'At eight o'clock,' she said, gulping in her throat to keep down her agitation. 'His secretary, Mr. Stangerson, said that there were two trains—one at 9.15 and one at 11. He was to catch the first.'

+

" 'And was that the last which you saw of him?'

+

"A terrible change came over the woman's face as I asked the question. Her features turned perfectly livid. It was some seconds before she could get out the single word 'Yes'—and when it did come it was in a husky, unnatural tone.

+

"There was silence for a moment, and then the daughter spoke in a calm, clear voice.

+

"'No good can ever come of falsehood, mother,' she said. 'Let us be frank with this gentleman. We did see Mr. Drebber again.'

+

"'God forgive you!' cried Madame Charpentier, throwing up her hands and sinking back in her chair. 'You have murdered your brother.'

+

"'Arthur would rather that we spoke the truth,' the girl answered firmly.

+

"'You had best tell me all about it now,' I said. 'Half confidences are worse than none. Besides, you do not know how much we know of it.'

+

"'On your head be it, Alice!' cried her mother; and then, turning to me, 'I will tell you all, sir. Do not imagine that my agitation on behalf of my son arises from any fear lest he should have had a hand in this terrible affair. He is utterly innocent of it. My dread is, however, that in your eyes and in the eyes of others he may appear to be compromised. That, however, is surely impossible. His high character, his profession, his antecedents would all forbid it.'

+

"'Your best way is to make a clean breast of the facts,' I answered. 'Depend upon it, if your son is innocent he will be none the worse.'

+

"'Perhaps, Alice, you had better leave us together,' she said, and her daughter withdrew. 'Now, sir,' she continued, 'I had no intention of telling you all this, but since my poor daughter has disclosed it I have no alternative. Having once decided to speak, I will tell you all without omitting any particular.'

+

"'It is your wisest course,' said I.

+

"'Mr. Drebber has been with us nearly three weeks. He and his secretary, Mr. Stangerson, had been travelling on the Continent. I noticed a "Copenhagen" label upon each of their trunks, showing that that had been their last stopping place. Stangerson was a quiet, reserved man, but his employer, I am sorry to say, was far otherwise. He was coarse in his habits and brutish in his ways. The very night of his arrival he became very much the worse for drink, and, indeed, after twelve o'clock in the day he could hardly ever be said to be sober. His manners towards the maid-servants were disgustingly free and familiar. Worst of all, he speedily assumed the same attitude towards my daughter, Alice, and spoke to her more than once in a way which, fortunately, she is too innocent to understand. On one occasion he actually seized her in his arms and embraced her—an outrage which caused his own secretary to reproach him for his unmanly conduct.'

+

"'But why did you stand all this?' I asked. 'I suppose that you can get rid of your boarders when you wish.'

+

"Mrs. Charpentier blushed at my pertinent question. 'Would to God that I had given him notice on the very day that he came,' she said. 'But it was a sore temptation. They were paying a pound a day each—fourteen pounds a week, and this is the slack season. I am a widow, and my boy in the Navy has cost me much. I grudged to lose the money. I acted for the best. This last was too much, however, and I gave him notice to leave on account of it. That was the reason of his going.'

+

"'Well?'

+

"'My heart grew light when I saw him drive away. My son is on leave just now, but I did not tell him anything of all this, for his temper is violent, and he is passionately fond of his sister. When I closed the door behind them a load seemed to be lifted from my mind. Alas, in less than an hour there was a ring at the bell, and I learned that Mr. Drebber had returned. He was much excited, and evidently the worse for drink. He forced his way into the room, where I was sitting with my daughter, and made some incoherent remark about having missed his train. He then turned to Alice, and before my very face, proposed to her that she should fly with him. "You are of age," he said, "and there is no law to stop you. I have money enough and to spare. Never mind the old girl there, but come along with me now straight away. You shall live like a princess." Poor Alice was so frightened that she shrunk away from him, but he caught her by the wrist and endeavoured to draw her towards the door. I screamed, and at that moment my son Arthur came into the room. What happened then I do not know. I heard oaths and the confused sounds of a scuffle. I was too terrified to raise my head. When I did look up, I saw Arthur standing in the doorway laughing, with a stick in his hand. "I don't think that fine fellow will trouble us again," he said. "I will just go after him and see what he does with himself." With those words he took his hat and started off down the street. The next morning we heard of Mr. Drebber's mysterious death.'

+

"This statement came from Mrs. Charpentier's lips with many gasps and pauses. At times she spoke so low that I could hardly catch the words. I made short-hand notes of all that she said, however, so that there should be no possibility of a mistake."

+

"It's quite exciting," said Sherlock Holmes, with a yawn. "What happened next?"

+

"When Mrs. Charpentier paused," the detective continued, "I saw that the whole case hung upon one point. Fixing her with my eye in a way which I always found effective with women, I asked her at what hour her son returned.

+

"'I do not know,' she answered.

+

"'Not know?"

+

"'No; he has a latch-key, and he lets himself in.'

+

"'After you went to bed?'

+

"'Yes.'

+

"'When did you go to bed?'

+

"'About eleven.'

+

"'So your son was gone at least two hours?'

+

"'Yes.'

+

"'Possibly four or five?'

+

"'Yes.'

+

"'What was he doing during that time?'

+

"'I do not know?' she answered, turning white to her very lips.

+

"Of course after that there was nothing more to be done. I found out where Lieutenant Charpentier was, took two officers with me, and arrested him. When I touched him on the shoulder and warned him to come quietly with us, he answered us as bold as brass, 'I suppose you are arresting me for being concerned in the death of that scoundrel Drebber,' he said. We had said nothing to him about it, so that his alluding to it had a most suspicious aspect."

+

"Very," said Holmes.

+

"He still carried the heavy stick which the mother described him as having with him when he followed Drebber. It was a stout oak cudgel."

+

"What is your theory, then?"

+

"Well, my theory is that he followed Drebber as far as the Brixton Road. When there, a fresh altercation arose between them, in the course of which Drebber received a blow from the stick, in the pit of the stomach perhaps, which killed him without leaving any mark. The night was so wet that no one was about, so Charpentier dragged the body of his victim into the empty house. As to the candle, and the blood, and the writing on the wall, and the ring, they may all be so many tricks to throw the police on to the wrong scent."

+

"Well done!" said Holmes in an encouraging voice. "Really, Gregson, you are getting along. We shall make something of you yet."

+

"I flatter myself that I have managed it rather neatly," the detective answered proudly. "The young man volunteered a statement, in which he said that after following Drebber some time, the latter perceived him, and took a cab in order to get away from him. On his way home he met an old shipmate, and took a long walk with him. On being asked where this old shipmate lived, he was unable to give any satisfactory reply. I think the whole case fits together uncommonly well. What amuses me is to think of Lestrade, who had started off upon the wrong scent. I am afraid he won't make much of it. Why, by Jove, here's the very man himself!"

+

It was indeed Lestrade, who had ascended the stairs while we were talking, and who now entered the room. The assurance and jauntiness which generally marked his demeanour and dress were however, wanting. His face was disturbed and troubled, while his clothes were disarranged and untidy. He had evidently come with the intention of consulting with Sherlock Holmes, for on perceiving his colleague he appeared to be embarrassed and put out. He stood in the centre of the room fumbling nervously with his hat and uncertain what to do. "This is a most extraordinary case," he said at last—"a most incomprehensible affair."

+ +

"Ah, you find it so, Mr. Lestrade!" cried Gregson, triumphantly. "I thought you would come to that conclusion. Have you managed to find the secretary, Mr. Joseph Stangerson?"

+

"The secretary, Mr. Joseph Stangerson," said Lestrade gravely, "was murdered at Halliday's Private Hotel about six o'clock this morning."

+
+
+ + diff --git a/books/a-study-in-scarlet/Part1-07.html b/books/a-study-in-scarlet/Part1-07.html new file mode 100644 index 0000000..902aee0 --- /dev/null +++ b/books/a-study-in-scarlet/Part1-07.html @@ -0,0 +1,76 @@ + + + + 1.7 • Light in the Darkness + + + + +
+

Light in the Darkness

+ +
+

The intelligence with which Lestrade greeted us was so momentous and so unexpected that we were all three fairly dumbfoundered. Gregson sprang out of his chair and upset the remainder of his whiskey and water. I stared in silence at Sherlock Holmes, whose lips were compressed and his brows drawn down over his eyes.

+

"Stangerson too!" he muttered. "The plot thickens."

+

"It was quite thick enough before," grumbled Lestrade, taking a chair. "I seem to have dropped into a sort of council of war."

+

"Are you—are you sure of this piece of intelligence?" stammered Gregson.

+

"I have just come from his room," said Lestrade; "I was the first to discover what had occurred."

+

"We have been hearing Gregson's view of the matter," Holmes observed. "Would you mind letting us know what you have seen and done?"

+

"I have no objection," Lestrade answered, seating himself. "I freely confess that I was of the opinion that Stangerson was concerned in the death of Drebber. This fresh development has shown me that I was completely mistaken. Full of the one idea, I set myself to find out what had become of the secretary. They had been seen together at Euston Station about half-past eight on the evening of the third. At two in the morning Drebber had been found in the Brixton Road. The question which confronted me was to find out how Stangerson had been employed between 8.30 and the time of the crime, and what had become of him afterwards. I telegraphed to Liverpool, giving a description of the man, and warning them to keep a watch upon the American boats. I then set to work calling upon all the hotels and lodging-houses in the vicinity of Euston. You see, I argued that if Drebber and his companion had become separated, the natural course for the latter would be to put up somewhere in the vicinity for the night, and then to hang about the station again next morning."

+

"They would be likely to agree on some meeting-place before-hand," remarked Holmes.

+

"So it proved. I spent the whole of yesterday evening in making inquiries entirely without avail. This morning I began very early, and at eight o'clock I reached Halliday's Private Hotel, in Little George Street. On my inquiry as to whether a Mr. Stangerson was living there, they at once answered me in the affirmative.

+

"'No doubt you are the gentleman whom he was expecting,' they said. 'He has been waiting for a gentleman for two days.'

+

"'Where is he now?' I asked.

+

"'He is upstairs in bed. He wished to be called at nine.'

+

"'I will go up and see him at once,' I said.

+

"It seemed to me that my sudden appearance might shake his nerves and lead him to say something unguarded. The Boots volunteered to show me the room; it was on the second floor, and there was a small corridor leading up to it. The Boots pointed out the door to me, and was about to go downstairs again when I saw something that made me feel sickish, in spite of my twenty years' experience. From under the door there curled a little red ribbon of blood, which had meandered across the passage and formed a little pool along the skirting at the other side. I gave a cry, which brought the Boots back. He nearly fainted when he saw it. The door was locked on the inside, but we put our shoulders to it, and knocked it in. The window of the room was open, and beside the window, all huddled up, lay the body of a man in his nightdress. He was quite dead, and had been for some time, for his limbs were rigid and cold. When we turned him over, the Boots recognized him at once as being the same gentleman who had engaged the room under the name of Joseph Stangerson. The cause of death was a deep stab in the left side, which must have penetrated the heart. And now comes the strangest part of the affair. What do you suppose was above the murdered man?"

+

I felt a creeping of the flesh, and a presentiment of coming horror, even before Sherlock Holmes answered.

+

"The word RACHE, written in letters of blood," he said.

+

"That was it," said Lestrade, in an awe-struck voice; and we were all silent for a while.

+

There was something so methodical and so incomprehensible about the deeds of this unknown assassin, that it imparted a fresh ghastliness to his crimes. My nerves, which were steady enough on the field of battle, tingled as I thought of it.

+

"The man was seen," continued Lestrade: "A milk boy, passing on his way to the dairy, happened to walk down the lane which leads from the mews at the back of the hotel. He noticed that a ladder, which usually lay there, was raised against one of the windows of the second floor, which was wide open. After passing, he looked back and saw a man descend the ladder. He came down so quietly and openly that the boy imagined him to be some carpenter or joiner at work in the hotel. He took no particular notice of him, beyond thinking in his own mind that it was early for him to be at work. He had an impression that the man was tall, had a reddish face, and was dressed in a long, brownish coat. He must have stayed in the room some little time after the murder, for we found blood-stained water in the basin, where he had washed his hands, and marks on the sheets where he had deliberately wiped his knife."\

+

I glanced at Holmes on hearing the description of the murderer which tallied so exactly with his own. There was, however, no trace of exultation or satisfaction upon his face.

+

"Did you find nothing in the room which could furnish a clue to the murderer?" he asked.

+

"Nothing. Stangerson had Drebber's purse in his pocket, but it seems that this was usual, as he did all the paying. There was eighty odd pounds in it, but nothing had been taken. Whatever the motive of these extraordinary crimes, robbery is certainly not one of them. There were no papers or memoranda in the murdered man's pocket, except a single telegram, dated from Cleveland about a month ago, and containing the words, 'J. H. is in Europe.' There was no name appended to this message."

+

"And there was nothing else?" Holmes asked.

+

"Nothing of any importance. The man's novel, with which he had read himself to sleep, was lying upon the bed, and his pipe was on a chair beside him. There was a glass of water on the table, and on the window-sill a small chip ointment box containing a couple of pills."

+

Sherlock Holmes sprang from his chair with an exclamation of delight.

+

"The last link," he cried, exultantly. " My case is complete."

+

The two detectives stared at him in amazement.

+

"I have now in my hands," my companion said, confidently, "all the threads which have formed such a tangle. There are, of course details to be filled in, but I am as certain of all the main facts, from the time that Drebber parted from Stangerson at the station, up to the discovery of the body of the latter, as if I had seen them with my own eyes. I will give you a proof of my knowledge. Could you lay your hand upon those pills?"

+

"I have them," said Lestrade, producing a small white box; "I took them and the purse and the telegram, intending to have them put in a place of safety at the Police Station. It was the merest chance my taking these pills, for I am bound to say that I do not attach any importance to them."

+

"Give them here," said Holmes. " Now, Doctor," turning to me, "are those ordinary pills?"

+

They certainly were not: They were of a pearly grey colour, small, round, and almost transparent against the light. "From their lightness and transparency, I should imagine that they are soluble in water," I remarked.

+

"Precisely so," answered Holmes. "Now would you mind going down and fetching that poor little devil of a terrier which has been bad so long, and which the landlady wanted you to put out of its pain yesterday."

+

I went downstairs and carried the dog upstairs in my arms. Its laboured breathing and glazing eye showed that it was not far from its end. Indeed, its snow-white muzzle proclaimed that it had already exceeded the usual term of canine existence. I placed it upon a cushion on the rug.

+

"I will now cut one of these pills in two," said Holmes, and drawing his penknife he suited the action to the word. "One half we return into the box for future purposes. The other half I will place in this wine glass, in which is a teaspoonful of water. You perceive that our friend, the Doctor, is right, and that it readily dissolves."

+

"This may be very interesting," said Lestrade, in the injured tone of one who suspects that he is being laughed at; "I cannot see, however, what it has to do with the death of Mr. Joseph Stangerson."

+

"Patience, my friend, patience! You will find in time that it has everything to do with it. I shall now add a little milk to make the mixture palatable, and on presenting it to the dog we find that he laps it up readily enough."

+ +

As he spoke he turned the contents of the wine glass into a saucer and placed it in front of the terrier, who speedily licked it dry. Sherlock Holmes' earnest demeanour had so far convinced us that we all sat in silence, watching the animal intently, and expecting some startling effect. None such appeared, however. The dog continued to lie stretched upon the cushion, breathing in a laboured way, but apparently neither the better nor the worse for its draught.

+

Holmes had taken out his watch, and as minute followed minute without result, an expression of the utmost chagrin and disappointment appeared upon his features. He gnawed his lip, drummed his fingers upon the table, and showed every other symptom of acute impatience. So great was his emotion that I felt sincerely sorry for him, while the two detectives smiled derisively, by no means displeased at this check which he had met.

+

"It can't be a coincidence," he cried, at last springing from his chair and pacing wildly up and down the room; "it is impossible that it should be a mere coincidence. The very pills which I suspected in the case of Drebber are actually found after the death of Stangerson. And yet they are inert. What can it mean? Surely my whole chain of reasoning cannot have been false. It is impossible! And yet this wretched dog is none the worse. Ah, I have it! I have it! "With a perfect shriek of delight he rushed to the box, cut the other pill in two, dissolved it, added milk, and presented it to the terrier. The unfortunate creature's tongue seemed hardly to have been moistened in it before it gave a convulsive shiver in every limb, and lay as rigid and lifeless as if it had been struck by lightning.

+

Sherlock Holmes drew a long breath, and wiped the perspiration from his forehead. "I should have more faith," he said; "I ought to know by this time that when a fact appears to be opposed to a long train of deductions, it invariably proves to be capable of bearing some other interpretation. Of the two pills in that box, one was of the most deadly poison, and the other was entirely harmless. I ought to have known that before ever I saw the box at all."

+

This last statement appeared to me to be so startling that I could hardly believe that he was in his sober senses. There was the dead dog, however, to prove that his conjecture had been correct. It seemed to me that the mists in my own mind were gradually clearing away, and I began to have a dim, vague perception of the truth.

+

"All this seems strange to you," continued Holmes, "because you failed at the beginning of the inquiry to grasp the importance of the single real clue which was presented to you. I had the good fortune to seize upon that, and everything which has occurred since then has served to confirm my original supposition, and, indeed, was the logical sequence of it. Hence things which have perplexed you and made the case more obscure have served to enlighten me and strengthen my conclusions. It is a mistake to confound strangeness with mystery. The most commonplace crime is often the most mysterious, because it presents no new or special features from which deductions may be drawn. This murder would have been infinitely more difficult to unravel had the body of the victim been simply found lying in the roadway without any of those outré and sensational accompaniments which have rendered it remarkable. These strange details, far from making the case more difficult, have really had the effect of making it less so."

+

Mr. Gregson, who had listened to this address with considerable impatience, could contain himself no longer. "Look here, Mr. Sherlock Holmes," he said, "we are all ready to acknowledge that you are a smart man, and that you have your own methods of working. We want something more than mere theory and preaching now, though. It is a case of taking the man. I have made my case out, and it seems I was wrong. Young Charpentier could not have been engaged in this second affair. Lestrade went after his man, Stangerson, and it appears that he was wrong too. You have thrown out hints here, and hints there, and seem to know more than we do, but the time has come when we feel that we have a right to ask you straight how much you do know of the business. Can you name the man who did it?"

+

"I cannot help feeling that Gregson is right, sir," remarked Lestrade. "We have both tried, and we have both failed. You have remarked more than once since I have been in the room that you had all the evidence which you require. Surely you will not withhold it any longer."

+

"Any delay in arresting the assassin," I observed, "might give him time to perpetrate some fresh atrocity."

+

Thus pressed by us all, Holmes showed signs of irresolution. He continued to walk up and down the room with his head sunk on his chest and his brows drawn down, as was his habit when lost in thought.

+

"There will be no more murders," he said at last, stopping abruptly and facing us. "You can put that consideration out of the question. You have asked me if I know the name of the assassin. I do. The mere knowing of his name is a small thing, however, compared with the power of laying our hands upon him. This I expect very shortly to do. I have good hopes of managing it through my own arrangements; but it is a thing which needs delicate handling, for we have a shrewd and desperate man to deal with, who is supported, as I have had occasion to prove, by another who is as clever as himself. As long as this man has no idea that any one can have a clue there is some chance of securing him; but if he had the slightest suspicion, he would change his name, and vanish in an instant among the four million inhabitants of this great city. Without meaning to hurt either of your feelings, I am bound to say that I consider these men to be more than a match for the official force, and that is why I have not asked your assistance. If I fail, I shall, of course, incur all the blame due to this omission; but that I am prepared for. At present I am ready to promise that the instant that I can communicate with you without endangering my own combinations, I shall do so."

+

Gregson and Lestrade seemed to be far from satisfied by this assurance, or by the depreciating allusion to the detective police. The former had flushed up to the roots of his flaxen hair, while the other's beady eyes glistened with curiosity and resentment. Neither of them had time to speak, however, before there was a tap at the door, and the spokesman of the street Arabs, young Wiggins, introduced his insignificant and unsavory person.

+

"Please, sir," he said, touching his forelock, "I have the cab downstairs."

+

"Good boy," said Holmes, blandly. "Why don't you introduce this pattern at Scotland Yard?" he continued, taking a pair of steel handcuffs from a drawer. "See how beautifully the spring works. They fasten in an instant."

+

"The old pattern is good enough," remarked Lestrade, "if we can only find the man to put them on."

+

"Very good, very good," said Holmes, smiling. "The cabman may as well help me with my boxes. Just ask him to step up, Wiggins."

+

I was surprised to find my companion speaking as though he were about to set out on a journey, since he had not said anything to me about it. There was a small portmanteau in the room, and this he pulled out and began to strap. He was busily engaged at it when the cabman entered the room.

+

"Just give me a help with this buckle, cabman," he said, kneeling over his task, and never turning his head.

+

The fellow came forward with a somewhat sullen, defiant air, and put down his hands to assist. At that instant there was a sharp click, the jangling of metal, and Sherlock Holmes sprang to his feet again.

+

"Gentlemen," he cried, with flashing eyes, "let me introduce you to Mr. Jefferson Hope, the murderer of Enoch Drebber and of Joseph Stangerson."

+ +

The whole thing occurred in a moment—so quickly that I had no time to realize it. I have a vivid recollection of that instant, of Holmes' triumphant expression and the ring of his voice, of the cabman's dazed, savage face, as he glared at the glittering handcuffs, which had appeared as if by magic upon his wrists. For a second or two we might have been a group of statues. Then with an inarticulate roar of fury, the prisoner wrenched himself free from Holmes' grasp, and hurled himself through the window. Woodwork and glass gave way before him; but before he got quite through, Gregson, Lestrade, and Holmes sprang upon him like so many staghounds. He was dragged back into the room, and then commenced a terrific conflict. So powerful and so fierce was he that the four of us were shaken off again and again. He appeared to have the convulsive strength of a man in an epileptic fit. His face and hands were terribly mangled by his passage through the glass, but loss of blood had no effect in diminishing his resistance. It was not until Lestrade succeeded in getting his hand inside his neck-cloth and half-strangling him that we made him realize that his struggles were of no avail; and even then we felt no security until we had pinioned his feet as well as his hands. That done, we rose to our feet breathless and panting.

+

"We have his cab," said Sherlock Holmes. "It will serve to take him to Scotland Yard. And now, gentlemen," he continued, with a pleasant smile, "we have reached the end of our little mystery. You are very welcome to put any questions that you like to me now, and there is no danger that I will refuse to answer them."

+
+
+ + diff --git a/books/a-study-in-scarlet/Part2-01.html b/books/a-study-in-scarlet/Part2-01.html new file mode 100644 index 0000000..9631c23 --- /dev/null +++ b/books/a-study-in-scarlet/Part2-01.html @@ -0,0 +1,85 @@ + + + + 2.1 • On the Great Alkali Plain + + + + +
+

On the Great Alkali Plain

+ +
+

In the central portion of the great North American Continent there lies an arid and repulsive desert, which for many a long year served as a barrier against the advance of civilization. From the Sierra Nevada to Nebraska, and from the Yellowstone River in the north to the Colorado upon the south, is a region of desolation and silence. Nor is Nature always in one mood throughout this grim district. It comprises snow-capped and lofty mountains, and dark and gloomy valleys. There are swift-flowing rivers which dash through jagged cañons; and there are enormous plains, which in winter are white with snow, and in summer are grey with the saline alkali dust; They all present however, the common characteristics of barenness, inhospitality, and misery.

+

There are no inhabitants of this land of despair. A band of Pawnees or of Blackfeet may occasionally traverse it in order to reach other hunting-grounds, but the hardiest of the braves are glad to lose sight of those awsome plains, and to find themselves once more upon the prairies. The coyote skulks among the scrub, the buzzard flaps heavily through the air, and the clumsy grizzly bear lumbers through the dark ravines, and picks up such sustenance as it can amongst the rocks. These are the sole dwellers in the wilderness.

+

In the whole world there can be no more dreary view than that from the northern slope of the Sierra Blanco. As far as the eye can reach stretches the great flat plainland, all dusted over with patches of alkali, and intersected by clumps of the dwarfish chapparal bushes. On the extreme verge of the horizon lie a long chain of mountain peaks, with their rugged summits flecked with snow. In this great stretch of country there is no sign of life, nor of anything appertaining to life. There is no bird in the steel-blue heaven, no movement upon the dull, grey earth—above all, there is absolute silence. Listen as one may, there is no shadow of a sound in all that mighty wilderness; nothing but silence—complete and heart-subduing silence.

+

It has been said there is nothing appertaining to life upon the broad plain. That is hardly true. Looking down from the Sierra Blanco, one sees a pathway traced out across the desert, which winds away and is lost in the extreme distance. It is rutted with wheels and trodden down by the feet of many adventurers. Here and there there are scattered white objects which glisten in the sun, and stand out against the dull deposit of alkali. Approach and examine them! They are bones: some large and coarse, others smaller and more delicate. The former have belonged to oxen, and the latter to men. For fifteen hundred miles one may trace this ghastly caravan route by these scattered remains of those who had fallen by the wayside.

+

Looking down on this very scene, there stood upon the fourth of May, eighteen hundred and forty-seven, a solitary traveller. His appearance was such that he might have been the very genius or demon of the region. An observer would have found it difficult to say whether he was nearer to forty or to sixty. His face was lean and haggard, and the brown parchment-like skin was drawn tightly over the projecting bones; his long, brown hair and beard were all flecked and dashed with white: his eyes were sunken in his head, and burned with an unnatural lustre; while the hand which grasped his rifle was hardly more fleshy than that of a skeleton. As he stood, he leaned upon his weapon for support, and yet his tall figure and the massive framework of his bones suggested a wiry and vigorous constitution. His gaunt face, however, and his clothes, which hung so baggily over his shrivelled limbs, proclaimed what it was that gave him that senile and decrepit appearance. The man was dying—dying from hunger and from thirst.

+

He had toiled painfully down the ravine, and on to this little elevation, in the vain hope of seeing some signs of water. Now the great salt plain stretched before his eyes, and the distant belt of savage mountains, without a sign anywhere of plant or tree, which might indicate the presence of moisture. In all that broad landscape there was no gleam of hope. North, and east, and west he looked with wild, questioning eyes, and then he realized that his wanderings had come to an end, and that there, on that barren crag, he was about to die. "Why not here, as well as in a feather bed, twenty years hence," he muttered, as he seated himself in the shelter of a boulder.

+

Before sitting down, he had deposited upon the ground his useless rifle, and also a large bundle tied up in a grey shawl, which he had carried slung over his right shoulder. It appeared to be somewhat too heavy for his strength, for in lowering it, it came down on the ground with some little violence. Instantly there broke from the grey parcel a little moaning cry, and from it there protruded a small, scared face, with very bright brown eyes, and two little speckled dimpled fists.

+

"You've hurt me!" said a childish voice reproachfully.

+

"Have I though," the man answered penitently; "I didn't go for to do it." As he spoke he unwrapped the grey shawl and extricated a pretty little girl of about five years of age, whose dainty shoes and smart pink frock with its little linen apron, all bespoke a mother's care. The child was pale and wan, but her healthy arms and legs showed that she had suffered less than her companion.

+

"How is it now?" he answered anxiously, for she was still rubbing the towsy golden curls which covered the back of her head.

+

"Kiss it and make it well," she said, with perfect gravity, showing the injured part up to him.

+

"That's what mother used to do. Where's mother?"

+

"Mother's gone; I guess you'll see her before long."

+

"Gone, eh!" said the little girl, "Funny, she didn't say good-bye; she 'most always did if she was just goin' over to auntie's for tea, and now she's been away three days. Say, it's awful dry, ain't it? Ain't there no water nor nothing to eat?"

+

"No, there ain't nothing, dearie. You'll just need to be patient awhile, and then you'll be all right. Put your head up ag'in me like that, and then you'll feel bullier. It ain't easy to talk when your lips is like leather, but I guess I'd best let you know how the cards lie. What's that you've got?"

+

"Pretty things! fine things!" cried the little girl enthusiastically, holding up two glittering fragments of mica. "When we goes back to home I'll give them to brother Bob."

+

"You'll see prettier things than them soon," said the man confidently. "You just wait a bit. I was going to tell you though—you remember when we left the river?"

+

"Oh, yes."

+

"Well we reckoned we'd strike another river soon, d'ye see. But there was somethin' wrong; compasses, or map, or somethin' and it didn't turn up. Water ran out. Just except a little drop for the likes of you, and—and——"

+

"And you couldn't wash yourself," interrupted his companion gravely, staring up at his grimy visage.

+

"No, nor drink. And Mr. Bender, he was the fust to go, and then Indian Pete, and then Mrs. McGregor, and then Johnny Hones, and then, dearie, your mother."

+

"Then mother's a deader too," cried the little girl, dropping her face in her pinafore and sobbing bitterly.

+

"Yes, they all went except you and me. Then I thought there was some chance of water in this direction, so I heaved you over my shoulder and we tramped it together. It don't seem as though we've improved matters. There's an almighty small chance for us now!"

+

"Do you mean that we are going to die too?" asked the child, checking her sobs, and raising her tear-stained face.

+

"I guess that's about the size of it."

+

"Why didn't you say so before?" she said laughing gleefully. "You gave me such a fright. Why, of course, now as long as we die we'll be with mother again."

+

"Yes, you will, dearie."

+

"And you too. I'll tell her how awful good you've been. I'll bet she meets us at the door of heaven with a big pitcher of water, and a lot of buckwheat cakes, hot, and toasted on both sides, like Bob and me was fond of. How long will it be first?"

+

"I don't know—not very long." The man's eyes were fixed upon the northern horizon. In the blue vault of the heaven there had appeared three little specks which increased in size every moment, so rapidly did they approach. They speedily resolved themselves into three large brown birds, which circled over the heads of the two wanderers, and then settled upon some rocks which overlooked them. They were buzzards, the vultures of the west, whose coming is the forerunner of death.

+

"Cocks and hens," cried the little girl gleefully, pointing at their ill-omened forms, and clapping her hands to make them rise. "Say, did God make this country?"

+

"In course He did," said her companion, rather startled by this unexpected question.

+

"He made the country down in Illinois, and He made the Missouri," the little girl continued. "I guess somebody else made the country in these parts. It's not nearly so well done. They forgot the water and the trees."

+

"What would ye think of offering up prayer?" the man asked diffidently.

+

"It ain't night yet," she answered.

+

"It don't matter. It ain't quite regular, but He won't mind that, you bet. You say over them ones that you used to say every night in the waggon when we was on the Plains."

+

"Why don't you say some yourself?" the child asked, with wondering eyes.

+

"I disremember them," he answered. "I hain't said none since I was half the height o' that gun. I guess it's never too late. You say them out, and I'll stand by and come in on the choruses."

+ +

"Then you'll need to kneel down, and me too," she said, laying the shawl out for that purpose. "You've got to put your hands up like this. It makes you feel kind of good."

+

It was a strange sight, had there been anything but the buzzards to see it. Side by side on the narrow shawl knelt the two wanderers, the little prattling child and the reckless, hardened adventurer. Her chubby face and his haggard, angular visage were both turned up to the cloudless heaven in heartfelt entreaty to that dread Being with whom they were face to face, while the two voices—the one thin and clear, the other deep and harsh—united in the entreaty for mercy and forgiveness. The prayer finished, they resumed their seat in the shadow of the boulder until the child fell asleep, nestling upon the broad breast of her protector. He watched over her slumber for some time, but Nature proved to be too strong for him. For three days and three nights he had allowed himself neither rest nor repose. Slowly the eyelids drooped over the tired eyes, and the head sunk lower and lower upon the breast, until the man's grizzled beard was mixed with the gold tresses of his companion, and both slept the same deep and dreamless slumber.

+

Had the wanderer remained awake for another half-hour a strange sight would have met his eyes. Far away on the extreme verge of the alkali plain there rose up a little spray of dust, very slight at first, and hardly to be distinguished from the mists of the distance, but gradually growing higher and broader until it formed a solid, well defined cloud. This cloud continued to increase in size until it became evident that it could only be raised by a great multitude of moving creatures. In more fertile spots the observer would have come to the conclusion that one of those great herds of bisons which graze upon the prairie land was approaching him. This was obviously impossible in these arid wilds. As the whirl of dust drew nearer to the solitary bluff upon which the two castaways were reposing, the canvas-covered tilts of waggons and the figures of armed horsemen began to show up through the haze, and the apparition revealed itself as being a great caravan upon its journey for the West. But what a caravan! When the head of it had reached the base of the mountains, the rear was not yet visible on the horizon. Right across the enormous plain stretched the straggling array, waggons and carts, men on horseback, and men on foot. Innumerable women who staggered along under burdens, and children who toddled beside the waggons or peeped out from under the white coverings. This was evidently no ordinary party of immigrants, but rather some nomad people who had been compelled from stress of circumstances to seek themselves a new country. There rose through the clear air a confused clattering and rumbling from this great mass of humanity, with the creaking of wheels and the neighing of horses. Loud as it was, it was not sufficient to rouse the two tired wayfarers above them.

+

At the head of the column there rode a score or more of grave, iron-faced men, clad in sombre home-spun garments and armed with rifles. On reaching the base of the bluff they halted, and held a short council among themselves. "The wells are to the right, my brothers," said one, a hard-lipped clean-shaven man with grizzly hair.

+

"To the right of the Sierra Blanco—so we shall reach the Rio Grande," said another.

+

"Fear not for water," cried a third. "He who could draw it from the rocks will not now abandon His own chosen people."

+

"Amen! amen!" responded the whole party.

+

They were about to resume their journey when one of the youngest and keenest-eyed uttered an exclamation and pointed up at the rugged crag above them. From its summit there fluttered a little wisp of pink, showing up hard and bright against the grey rocks behind. At the sight there was a general reining up of horses and unslinging of guns, while fresh horsemen came galloping up to reinforce the vanguard. The word "Redskins" was on every lip.

+

"There can't be any number of Injuns here," said the elderly man who appeared to be in command. "We have passed the Pawnees, and there are no other tribes until we cross the great mountains."

+

"Shall I go forward and see, Brother Stangerson," asked one of the band.

+

"And I," "and I," cried a dozen voices.

+

"Leave your horses below and we will await you here," the elder answered. In a moment the young fellows had dismounted, fastened their horses, and were ascending the precipitous slope which led up to the object which had excited their curiosity. They advanced rapidly and noiselessly, with the confidence and dexterity of practised scouts. The watchers from the plain below could see them flit from rock to rock until their figures stood out against the skyline. The young man who had first given the alarm was leading them. Suddenly his followers saw him throw up his hands, as though overcome with astonishment, and on joining him they were affected in the same way by the sight which met their eyes.

+

On the little plateau which crowned the barren hill there stood a single giant boulder, and against this boulder there lay a tall man, long-bearded and hard-featured, but of an excessive thinness. His placid face and regular breathing showed that he was fast sleep. Beside him lay a little child, with her round white arms encircling his brown sinewy neck; and her golden-haired head resting upon the breast of his velveteen tunic. Her rosy lips were parted, showing the regular line of snow-white teeth within, and a playful smile played over her infantile features. Her plump little white legs, terminating in white socks and neat shoes with shining buckles, offered a strange contrast to the long shrivelled members of her companion. On the ledge of rock above this strange couple there stood three solemn buzzards, who, at the sight of the new comers, uttered raucous screams of disappointment and flapped sullenly away.

+

The cries of the foul birds awoke the two sleepers, who stared about them in bewilderment. The man staggered to his feet and looked down upon the plain which had been so desolate when sleep had overtaken him, and which was now traversed by this enormous body of men and of beasts. His face assumed an expression of incredulity as he gazed, and he passed his bony hand over his eyes. "This is what they call delirium, I guess," he muttered. The child stood beside him, holding on to the skirt of his coat, and said nothing, but looked all round her with the wondering, questioning gaze of childhood.

+

The rescuing party were speedily able to convince the two castaways that their appearance was no delusion. One of them seized the little girl and hoisted her upon his shoulder, while the others supported her gaunt companion, and assisted him towards the waggons.

+ +

"My name is John Ferrier," the wanderer explained; "me and that little un are all that's left o' twenty-one people. The rest is all dead o' thirst and hunger away down in the south."

+

"Is she your child?" asked some one.

+

"I guess she is now," the other cried, defiantly; "she's mine 'cause I saved her. No man will take her from me. She's Lucy Ferrier from this day on. Who are you though?" he continued, glancing with curiosity at his stalwart, sunburned rescuers; "there seems to be a powerful lot of ye."

+

"Nigh upon ten thousand," said one of the young men; "we are the persecuted children of God—the chosen of the Angel Merona."

+

"I never heard tell on him," said the wanderer. "He appears to have chosen a fair crowd of ye." "Do not jest at that which is sacred," said the other sternly. "We are of those who believe in those sacred writings, drawn in Egyptian letters on plates of beaten gold, which were handed unto the holy Joseph Smith at Palmyra. We have come from Nauvoo, in the State of Illinois, where we had founded our temple. We have come to seek a refuge from the violent man and from the godless, even though it be the heart of the desert."

+

The name of Nauvoo evidently recalled recollections to John Ferrier. "I see," he said; "you are the Mormons."

+

"We are the Mormons," answered his companions with one voice.

+

"And where are you going?"

+

"We do not know. The hand of God is leading us under the person of our Prophet. You must come before him. He shall say what is to be done with you."

+

They had reached the base of the hill by this time, and were surrounded by crowds of the pilgrims—pale-faced, meek-looking women; strong, laughing children; and anxious, earnest-eyed men. Many were the cries of astonishment and of commiseration which arose from them when they perceived the youth of one of the strangers and the destitution of the other. Their escort did not halt, however, but pushed on, followed by a great crowd of Mormons, until they reached a waggon, which was conspicuous for its great size and for the gaudiness and smartness of its appearance. Six horses were yoked to it, whereas the others were furnished with two, or, at most, four a-piece. Beside the driver there sat a man who could not have been more than thirty years of age, but whose massive head and resolute expression marked him as a leader. He was reading a brown-backed volume, but as the crowd approached he laid it aside, and listened attentively to an account of the episode. Then he turned to the two castaways.

+

"If we take you with us," he said, in solemn words, "it can only be as believers in our own creed. We shall have no wolves in our fold. Better far that your bones should bleach in this wilderness than that you should prove to be that little speck of decay which in time corrupts the whole fruit. Will you come with us on these terms?"

+

"Guess I'll come with you on any terms," said Ferrier, with such emphasis that the grave Elders could not restrain a smile. The leader alone retained his stern, impressive expression.

+

"Take him, Brother Stangerson," he said, "give him food and drink, and the child likewise. Let it be your task also to teach him our holy creed. We have delayed long enough. Forward! On, on to Zion!"

+

"On, on to Zion!" cried the crowd of Mormons, and the words rippled down the long caravan, passing from mouth to mouth until they died away in a dull murmur in the far distance. With a cracking of whips and a creaking of wheels the great waggons got into motion and soon the whole caravan was winding along once more. The Elder to whose care the two waifs had been committed led them to his waggon, where a meal was already awaiting them.

+

"You shall remain here," he said. "In a few days you will have recovered from your fatigues. In the meantime, remember that now and for ever you are of our religion. Brigham Young has said it, and he has spoken with the voice of Joseph Smith, which is the voice of God."

+
+
+ + diff --git a/books/a-study-in-scarlet/Part2-02.html b/books/a-study-in-scarlet/Part2-02.html new file mode 100644 index 0000000..88e315c --- /dev/null +++ b/books/a-study-in-scarlet/Part2-02.html @@ -0,0 +1,49 @@ + + + + 2.2 • The Flower of Utah + + + + +
+

The Flower of Utah

+ +
+

This is not the place to commemorate the trials and privations endured by the immigrant Mormons before they came to their final haven. From the shores of the Mississippi to the western slopes of the Rocky Mountains they had struggled on with a constancy almost unparalleled in history. The savage man, and the savage beast, hunger, thirst, fatigue, and disease—every impediment which Nature could place in the way—had all been overcome with Anglo-Saxon tenacity. Yet the long journey and the accumulated terrors had shaken the hearts of the stoutest among them. There was not one who did not sink upon his knees in heartfelt prayer when they saw the broad valley of Utah bathed in the sunlight beneath them, and learned from the lips of their leader that this was the promised land, and that these virgin acres were to be theirs for evermore.

+

Young speedily proved himself to be a skilful administrator as well as a resolute chief. Maps were drawn and charts prepared, in which the future city was sketched out. All around farms were apportioned and allotted in proportion to the standing of each individual. The tradesman was put to his trade and the artisan to his calling. In the town streets and squares sprang up as if by magic. In the country there was draining and hedging, planting and clearing, until the next summer saw the whole country golden with the wheat crop. Everything prospered in the strange settlement. Above all, the great temple which they had erected in the centre of the city grew ever taller and larger. From the first blush of dawn until the closing of the twilight, the clatter of the hammer and the rasp of the saw were never absent from the monument which the immigrants erected to Him who had led them safe through many dangers.

+

The two castaways, John Ferrier and the little girl, who had shared his fortunes and had been adopted as his daughter, accompanied the Mormons to the end of their great pilgrimage. Little Lucy Ferrier was borne along pleasantly enough in Elder Stangerson's waggon, a retreat which she shared with the Mormon's three wives and with his son, a headstrong, forward boy of twelve. Having rallied, with the elasticity of childhood, from the shock caused by her mother's death, she soon became a pet with the women, and reconciled herself to this new life in her moving canvas-covered home. In the meantime Ferrier having recovered from his privations, distinguished himself as a useful guide and an indefatigable hunter. So rapidly did he gain the esteem of his new companions, that when they reached the end of their wanderings, it was unanimously agreed that he should be provided with as large and as fertile a tract of land as any of the settlers, with the exception of Young himself, and of Stangerson, Kemball, Johnston, and Drebber, who were the four principal Elders.

+

On the farm thus acquired John Ferrier built himself a substantial log-house, which received so many additions in succeeding years that it grew into a roomy villa. He was a man of a practical turn of mind, keen in his dealing and skilful with his hands. His iron constitution enabled him to work morning and evening at improving and tilling his lands. Hence it came about that his farm and all that belonged to him prospered exceedingly. In three years he was better off than his neighbours, in six he was well-to-do, in nine he was rich, and in twelve there were no half a dozen men in the whole of Salt Lake City who could compare with him. From the great inland sea to the distant Wahsatch Mountains there was no name better known than that of John Ferrier.

+

There was one way and only one in which he offended the susceptibilities of his co-religionists. No argument or persuasion could ever induce him to set up a female establishment after the manner of his companions. He never gave reasons for this persistent refusal, but contented himself by resolutely and inflexibly adhering to his determination. There were some who accused him of luke-warmness in his adopted religion, and others who put it down to greed of wealth and reluctance to incur expense. Others, again, spoke of some early love affair, and of a fair-haired girl who had pined away on the shores of the Atlantic. Whatever the reason, Ferrier remained strictly celibate. In every other respect he conformed to the religion of the young settlement, and gained the name of being an orthodox and straight-walking man.

+

Lucy Ferrier grew up within the log-house, and assisted her adopted father in all his undertakings. The keen air of the mountains and the balsamic odour of the pine trees took the place of nurse and mother to the young girl. As year succeeded to year she grew taller and stronger, her cheek more ruddy and her step more elastic. Many a wayfarer upon the high road which ran by Ferrier's farm felt long-forgotten thoughts revive in their minds as they watched her lithe, girlish figure tripping through the wheat-fields, or met her mounted upon her father's mustang, and managing it with all the east and grace of a true child of the West. So the bud blossomed into a flower, and the year which saw her father the richest of the farmers left her as fair a specimen of American girlhood as could be found in the whole Pacific slope.

+

It was not the father, however, who first discovered that the child had developed into a woman. It seldom is in such cases. That mysterious change is too subtle and too gradual to be measured by dates. Least of all does the maiden herself know it until the tone of a voice of the touch of a hand sets her heart thrilling within her, and she learns with a mixture of pride and of fear, that a new and a larger nature has awoke within her. There are few who cannot recall that day and remember the one little incident which heralded the dawn of a new life. In the case of Lucy Ferrier the occasion was serious enough in itself, apart from its future influence on her destiny and that of many besides.

+

It was a warm June morning, her, and the Latter Day Saints were as busy as the bees whose hives they have chosen for their emblem. In the fields and in the streets rose the same hum of human industry. Down the dusty high roads defiled long streams of heavily-laden mules, all heading to the west, for the gold fever had broken out in California, and the overland route lay through the city of the Elect. There, too, were droves of sheep and bullocks coming in from the outlying pasture lands, and trains of tired immigrants, men and horses equally weary of their interminable journey. Through all this motley assemblage, threading her way with the skill of an accomplished rider, there galloped Lucy Ferrier, her fair face flushed with the exercise and her long chestnut hair floating out behind her. She had a commission from her father in the city, and was dashing in as she had done many a time before, with all the fearlessness of youth, thinking only of her task and how it was to be performed. The travel-stained adventurers gazed after her in astonishment, and even the unemotional Indians, journeying in with their pelties, relaxed their accustomed stoicism as they marvelled at the beauty of the pale-faced maiden.

+

She had reached the outskirts of the city when she found the road blocked by a great drove of cattle, driven by a half-dozen wild-looking herdsmen from the plains. In her impatience she endeavoured to pass this obstacle by pushing her horse into what appeared to be a gap. Scarcely had she got fairly into it, however, before the beasts closed in behind her, and she found herself completely imbedded in the moving stream of fierce-eyed, long-horned bullocks. Accustomed as she was to deal with cattle, she was not alarmed at her situation, but took advantage of every opportunity to urge her horse on, in the hopes of pushing her way through the cavalcade. Unfortunately the horns of one of the creatures, either by accident or design, came in violent contact with the flank of the mustang, and excited it to madness. In an instant it reared upon its hind legs with a snort of rage, and pranced and tossed in a way that would have unseated any but a skilful rider. The situation was full of peril. Every plunge of the excited horse brought it against the horns again, and goaded it to fresh madness. It was all that the girl could do to keep herself in the saddle, yet a slip would mean a terrible death under the hoofs of the unwieldy and terrified animals. Unaccustomed to sudden emergencies, her head began to swim, and her grip upon the bridle to relax. Choked by the rising cloud of dust and by the steam of the struggling creatures, she might have abandoned her efforts in despair, but for a kindly voice at her elbow which assured her of assistance. At the same moment a sinewy brown hand caught the frightened horse by the curb, and forcing a way through the drove, soon brought her to the outskirts.

+ +

"You're not hurt, I hope, miss," said her preserver, respectfully.

+

She looked up at his dark, fierce face, and laughed saucily. "I'm awful frightened," she said, naïvely; "whoever would have thought that Poncho would have been so scared by a lot of cows?"

+

"Thank God you kept your seat," the other said earnestly. He was a tall, savage-looking young fellow, mounted on a powerful roan horse, and clad in the rough dress of a hunter, with a long rifle slung over his shoulders. "I guess you are the daughter of John Ferrier," he remarked; "I saw you ride down from his house. When you see him, ask him if he remembers the Jefferson Hopes of St. Louis. If he's the same Ferrier, my father and he were pretty thick."

+

"Hadn't you better come and ask yourself?" she asked, demurely.

+

The young fellow seemed pleased at the suggestion, and his dark eyes sparkled with pleasure. "I'll do so," he said; "we've been in the mountains for two months, and are not over and above in visiting condition. He must take us as he finds us."

+

"He has a good deal to thank you for, and so have I," she answered, "he's awful fond of me. If those cows had jumped on me he'd have never got over it."

+

"Neither would I," said her companion.

+

"You! Well, I don't see that it would make much matter to you, anyhow. You ain't even a friend of ours."

+

The young hunter's dark face grew so gloomy over this remark that Lucy Ferrier laughed aloud.

+

"There, I didn't mean that," she said; "of course, you are a friend now. You must come and see us. Now I must push along, or father won't trust me with his business any more. Good-bye!"

+

"Good-bye," he answered, raising his broad sombrero, and bending over her little hand. She wheeled her mustang round, gave it a cut with her riding-whip, and darted away down the broad road in a rolling cloud of dust.

+

Young Jefferson Hope rode on with his companions, gloomy and taciturn. He and they had been among the Nevada Mountains prospecting for silver, and were returning to Salt Lake City in the hope of raising capital enough to work some lodes which they had discovered. He had been as keen as any of them upon the business until this sudden incident had drawn his thoughts into another channel. The sight of the fair young girl, as frank and wholesome as the Sierra breezes, had stirred his volcanic, untamed heart to its very depths. When she had vanished from his sight, he realized that a crisis had come in his life, and that neither silver speculations nor any other questions could ever be of such importance to him as this new and all-absorbing one. The love which had sprung up in his heart was not the sudden, changeable fancy of a boy, but rather the wild, fierce passion of a man of strong will and imperious temper. He had been accustomed to succeed in all that he undertook. He swore in his heart that he would not fail in this if human effort and human perseverance could render him successful.

+

He called on John Ferrier that night, and many times again, until his face was a familiar one at the farmhouse. John, cooped up in the valley, and absorbed in his work, had had little chance of learning the news of the outside world during the last twelve years. All this Jefferson Hope was able to tell him, and in a style which interested Lucy as well as her father. He had been a pioneer in California, and could narrate many a strange tale of fortunes made and fortunes lost in those wild, halcyon days. He had been a scout too, and a trapper, a silver explorer, and a ranchman. Wherever stirring adventures were to be had, Jefferson Hope had been there in search of them. He soon became a favourite with the old farmer, who spoke eloquently of his virtues. On such occasions, Lucy was silent, but her blushing cheek and her bright, happy eyes showed only too clearly that her young heart was no longer her own. Her honest father may not have observed these symptoms, but they were assuredly not thrown away upon the man who had won her affections.

+

One summer evening he came galloping down the road and pulled up at the gate. She was at the doorway, and came down to meet him. He threw the bridle over the fence and strode up the pathway.

+

"I am off, Lucy," he said, taking her two hands in his, and gazing tenderly down into her face; "I won't ask you to come with me now, but will you be ready to come when I am here again?"

+

"And when will that be?" she asked, blushing and laughing.

+

"A couple of months at the outside. I will come and claim you then, my darling. There's no one who can stand between us."

+

"And how about father?" she asked.

+

"He has given his consent, provided we get these mines working all right. I have no fear on that head."

+

"Oh, well; of course, if you and father have arranged it all, there's no more to be said," she whispered, with her cheek against his broad breast.

+ +

"Thank God!" he said, hoarsely, stooping and kissing her. "It is settled then. The longer I stay, the harder it will be to go. They are waiting for me at the cañon. Good-bye, my own darling—good-bye. In two months you shall see me."

+

He tore himself from her as he spoke, and, flinging himself upon his horse, galloped furiously away, never even looking round, as though afraid that his resolution might fail him if he took one glance at what he was leaving. She stood at the gate, gazing after him until he vanished from her sight. Then she walked back into the house the happiest girl in all Utah.

+
+
+ + diff --git a/books/a-study-in-scarlet/Part2-03.html b/books/a-study-in-scarlet/Part2-03.html new file mode 100644 index 0000000..3ddc8a2 --- /dev/null +++ b/books/a-study-in-scarlet/Part2-03.html @@ -0,0 +1,54 @@ + + + + 2.3 • John Ferrier Talks with the Prophet + + + + +
+

John Ferrier Talks with the Prophet

+ +
+

Three weeks had passed since Jefferson Hope and his comrades had departed from Salt Lake City. John Ferrier's heart was sore within him when he thought of the young man's return, and of the impending loss of his adopted child. Yet her bright and happy face reconciled him to the arrangement more than any argument could have done. He had always determined, deep down in his resolute heart, that nothing would ever induce him to allow his daughter to wed a Mormon. Such a marriage he regarded as no marriage at all, but as a shame and a disgrace. Whatever he might think of the Mormon doctrines, upon that one point he was inflexible. He had to seal his mouth on the subject, however, for to express an unorthodox opinion was a dangerous matter in those days in the Land of the Saints.

+

Yes, a dangerous matter—so dangerous that even the most saintly dared only whisper their religious opinions with bated breath, lest something which fell from their lips might be misconstrued, and bring down a swift retribution upon them. The victims of persecution had now turned persecutors on their own account, and persecutors of the most terrible description. Not the Inquisition of Seville, nor the German Vehmgericht, nor the Secret Societies of Italy, were ever able to put a more formidable machinery in motion than that which cast a cloud over the State of Utah.

+

Its invisibility, and the mystery which was attached to it, made this organization doubly terrible. It appeared to be omniscient and omnipotent, and yet was neither seen nor heard. The man who held out against the Church vanished away, and none knew whither he had gone or what had befallen him. His wife and his children awaited him at home, but no father ever returned to tell them how he had fared at the hands of his secret judges. A rash word or a hasty act was followed by annihilation, and yet none knew what the nature might be of this terrible power which was suspended over them. No wonder that men went about in fear and trembling, and that even in the heart of the wilderness they dared not whisper the doubts which oppressed them.

+

At first this vague and terrible power was exercised only upon the recalcitrants who, having embraced the Mormon faith, wished afterwards to pervert or to abandon it. Soon, however, it took a wider range. The supply of adult women was running short, and polygamy without a female population on which to draw was a barren doctrine indeed. Strange rumours began to be bandied about—rumours of murdered immigrants and rifled camps in regions where Indians had never been seen. Fresh women appeared in the harems of the Elders—women who pined and wept, and bore upon their faces the traces of an unextinguishable horror. Belated wanderers upon the mountains spoke of gangs of armed men, masked, stealthy, and noiseless, who flitted by them in the darkness. These tales and rumours took substance and shape and were corroborated and re-corroborated, until they resolved themselves into a definite name. To this day, in the lonely ranches of the West, the name of the Danite Band, or the Avenging Angels, is a sinister and an ill-omened one.

+

Fuller knowledge of the organization which produced such terrible results served to increase rather than to lessen the horror which it inspired in the minds of men. None knew who belonged to this ruthless society. The names of the participators in the deeds of blood and violence done under the name of religion were kept profoundly secret. The very friend to whom you communicated your misgivings as to the Prophet and his mission might be one of those who would come forth at night with fire and sword to exact a terrible reparation. Hence every man feared his neighbour, and none spoke of the things which were nearest his heart.

+

One fine morning John Ferrier was about to set out to his wheat-fields, when he heard the click of the latch, and, looking through the window, saw a stout, sandy-haired, middle-aged man coming up the pathway. His heart leapt to his mouth, for this was none other than the great Brigham Young himself. Full of trepidation—for he knew that such a visit boded him little good—Ferrier ran to the door to greet the Mormon chief. The latter, however, received his salutations coldly, and followed him with a stern face into the sitting-room.

+

"Brother Ferrier," he said, taking a seat, and eyeing the farmer keenly from under his light-coloured eyelashes, "the true believers have been good friends to you. We picked you up when you were starving in the desert, we shared our food with you, led you safe to the Chosen Valley, gave you a goodly share of land, and allowed you to wax rich under our protection. Is not this so?"

+

"It is so," answered John Ferrier.

+

"In return for all this we asked but one condition: that was, that you should embrace the true faith, and conform in every way to its usages. This you promised to do, and this, if common report says truly, you have neglected."

+

"And how have I neglected it?" asked Ferrier, throwing out his hands in expostulation. "Have I not given to the common fund? Have I not attended at the Temple? Have I not——?"

+

"Where are your wives?" asked Young, looking round him. "Call them in that I may greet them."

+

"It is true that I have not married," Ferrier answered. "But women were few, and there were many who had better claims than I. I was not a lonely man; I had my daughter to attend to my wants."

+

"It is of that daughter that I would speak to you," said the leader of the Mormons. "She has grown to be the flower of Utah, and has found favour in the eyes of many who are high in the land."

+

John Ferrier groaned internally.

+

"There are stories of her which I would fain disbelieve—stories that she is sealed to some Gentile. This must be the gossip of idle tongues. What is the thirteenth rule in the code of the sainted Joseph Smith? 'Let every maiden of the true faith marry one of the elect; for if she wed a Gentile, she commits a grievous sin.' This being so, it is impossible that you, who profess the holy creed, should suffer your daughter to violate it."

+

John Ferrier made no answer, but he played nervously with his riding-whip.

+

"Upon this one point your whole faith shall be tested—so it has been decided in the Sacred Council of Four. The girl is young, and we would not have her wed grey hairs, neither would we deprive her of all choice. We Elders have many heifers, but our children must also be provided. Stangerson has a son, and Drebber has a son, and either of them would gladly welcome your daughter to their house. Let her choose between them. They are young and rich, and of the true faith. What say you to that?"

+

Ferrier remained silent for some little time with his brows knitted.

+

"You will give us time," he said at last. "My daughter is very young—she is scarce of an age to marry."

+

"She shall have a month to choose," said Young, rising from his seat. "At the end of that time she shall give her answer."

+ +

He was passing through the door, when he turned, with flushed face and flashing eyes. "It were better for you, John Ferrier," he thundered, "that you and she were now lying blanched skeletons upon the Sierra Blanco, than that you should put your weak wills against the orders of the Holy Four!"

+

With a threatening gesture of his hand, he turned from the door, and Ferrier heard his heavy steps scrunching along the shingly path.

+

He was still sitting with his elbow upon his knee, considering how he should broach the matter to his daughter, when a soft hand was laid upon his, and looking up, he saw her standing beside him. One glance at her pale, frightened face showed him that she had heard what had passed.

+

"I could not help it," she said, in answer to his look. "His voice rang through the house. Oh, father, father, what shall we do?"

+

"Don't you scare yourself," he answered, drawing her to him, and passing his broad, rough hand caressingly over her chestnut hair. "We'll fix it up somehow or another. You don't find your fancy kind o' lessening for this chap, do you?"

+

A sob and a squeeze of his hand was her only answer.

+

"No; of course not. I shouldn't care to hear you say you did. He's a likely lad, and he's a Christian, which is more than these folk here, in spite o' all their praying and preaching. There's a party starting for Nevada to-morrow, and I'll manage to send him a message letting him know the hole we are in. If I know anything o' that young man, he'll be back here with a speed that would whip electro-telegraphs."

+

Lucy laughed through her tears at her father's description.

+

"When he comes, he will advise us for the best. But it is for you that I am frightened, dear. One hears—one hears such dreadful stories about those who oppose the Prophet: something terrible always happens to them."

+

"But we haven't opposed him yet," her father answered. "It will be time to look out for squalls when we do. We have a clear month before us; at the end of that, I guess we had best shin out of Utah."

+

"Leave Utah!"

+

"That's about the size of it."

+

"But the farm?"

+

"We will raise as much as we can in money, and let the rest go. To tell the truth, Lucy, it isn't the first time I have thought of doing it. I don't care about knuckling under to any man, as these folk do to their darned Prophet. I'm a free-born American, and it's all new to me. Guess I'm too old to learn. If he comes browsing about this farm, he might chance to run up against a charge of buck-shot travelling in the opposite direction."

+

"But they won't let us leave," his daughter objected.

+

"Wait till Jefferson comes, and we'll soon manage that. In the meantime, don't you fret yourself, my dearie, and don't get your eyes swelled up, else he'll be walking into me when he sees you. There's nothing to be afeard about and there's no danger at all."

+

John Ferrier uttered these consoling remarks in a very confident tone, but she could not help observing that he paid unusual care to the fastening of the doors that night, and that he carefully cleaned and loaded the rusty old shot-gun which hung upon the wall of his bedroom.

+
+
+ + diff --git a/books/a-study-in-scarlet/Part2-04.html b/books/a-study-in-scarlet/Part2-04.html new file mode 100644 index 0000000..c5685d8 --- /dev/null +++ b/books/a-study-in-scarlet/Part2-04.html @@ -0,0 +1,83 @@ + + + + 2.4 • A Fight for Life + + + + +
+

A Fight for Life

+ +
+

On the morning which followed his interview with the Mormon Prophet, John Ferrier went in to Salt Lake City, and having found his acquaintance, who was bound for the Nevada Mountains, he entrusted him with his message to Jefferson Hope. In it he told the young man of the imminent danger which threatened them, and how necessary it was that he should return. Having done thus he felt easier in his mind, and returned home with a lighter heart.

+

As he approached his farm, he was surprised to see a horse hitched to each of the posts of the gate. Still more surprised was he on entering to find two young men in possession of his sitting-room. One, with a long pale face, was leaning back in the rocking-chair, with his feet cocked up upon the stove. The other, a bull-necked youth with coarse, bloated features, was standing in front of the window with his hands in his pockets whistling a popular hymn. Both of them nodded to Ferrier as he entered, and the one in the rocking-chair commenced the conversation.

+

"Maybe you don't know us," he said. "This here is the son of Elder Drebber, and I'm Joseph Stangerson, who travelled with you in the desert when the Lord stretched out His hand and gathered you into the true fold."

+

"As He will all the nations in His own good time," said the other in a nasal voice; "He grindeth slowly but exceeding small."

+

John Ferrier bowed coldly. He had guessed who his vistiors were.

+

"We have come," continued Stangerson, "at the advice of our fathers to solicit the hand of your daughter for whichever of us may seem good to you and to her. As I have but four wives and Brother Drebber here has seven, it appears to me that my claim is the stronger one."

+

"Nay, nay, Brother Stangerson," cried the other; "the question is not how many wives we have, but how many we can keep. My father has now given over his mills to me, and I am the richer man."

+

"But my prospects are better," said the other, warmly. "When the Lord removes my father, I shall have his tanning yard and his leather factory. Then I am your elder, and am higher in the Church."

+

"It will be for the maiden to decide," rejoined young Drebber, smirking at his own reflection in the glass. "We will leave it all to her decision."

+

During this dialogue John Ferrier had stood fuming in the door-way, hardly able to keep his riding-whip from the backs of his two visitors.

+

"Look here," he said at last, striding up to them, "when my daughter summons you, you can come, but until then I don't want to see your faces again."

+

The two young Mormons stared at him in amazement. In their eyes this competition between them for the maiden's hand was the highest of honours both to her and her father.

+

"There are two ways out of the room," cried Ferrier; "there is the door, and there is the window. Which do you care to use?"

+

His brown face looked so savage and his gaunt hands so threatening, that his visitors sprang to their feet and beat a hurried retreat. The old farmer followed them to the door.

+

"Let me know when you have settled which it is to be," he said, sardonically.

+

"You shall smart for this?" Stangerson cried, white with rage.

+

"You have defied the Prophet and the Council of Four. You shall rue it to the end of your days."

+

"The hand of the Lord shall be heavy upon you," cried young Drebber; "He will arise and smite you!"

+

"Then I'll start the smiting," exclaimed Ferrier, furiously, and would have rushed upstairs for his gun had not Lucy seized him by the arm and restrained him. Before he could escape from her, the clatter of horses' hoofs told him that they were beyond his reach.

+

"The young canting rascals!" he exclaimed, wiping the perspiration from his forehead; "I would sooner see you in your grave my girl, than the wife of either of them."

+

"And so should I, father," she answered, with spirit; "but Jefferson will soon be here."

+

"Yes. It will not be long before he comes. The sooner the better, for we do not know what their next move may be."

+

It was, indeed, high time that some one capable of giving advice and help should come to the aid of the sturdy old farmer and his adopted daughter. In the whole history of the settlement there had never been such a case of rank disobedience to the authority of the Elders. If minor errors were punished so sternly, what would be the fate of this arch rebel. Ferrier knew that his wealth and position would be of no avail to him. Others as well known and as rich as himself had been spirited away before now, and their goods given over to the Church. He was a brave man, but he trembled at the vague, shadowy terrors which hung over him. Any known danger he could face with a firm lip, but this suspense was unnerving. He concealed his fears from his daughter, however, and affected to make light of the whole matter, though she, with the keen eye of love, saw plainly that he was ill at ease.

+

He expected that he would receive some message or remonstrance from Young as to his conduct, and he was not mistaken, though it came in an unlooked-for manner. Upon rising next morning he found, to his surprise, a small square of paper pinned on to the coverlet of his bed just over his chest. On it was printed, in bold, straggling letters:—

+

"Twenty-nine days are given you for amendment; and then——"

+

The dash was more fear-inspiring than any threat could have been. How this warning came into his room puzzled John Ferrier sorely, for his servants slept in an outhouse, and the doors and windows had all been secured. He crumpled the paper up and said nothing to his daughter, but the incident struck a chill into his heart. The twenty-nine days were evidently the balance of the month which Young had promised. What strength or courage could avail against an enemy armed with such mysterious powers? The hand which fastened that pin might have struck him to the heart, and he could never have known who had slain him.

+

Still more shaken was he next morning. They had sat down to their breakfast, when Lucy with a cry of surprise pointed upwards. In the centre of the ceiling was scrawled with a burned stick apparently, the number 28. To his daughter it was unintelligible,and he did not enlighten her. That night he sat up with his gun and kept watch and ward. He saw and he heard nothing, and yet in the morning a great 27 had been painted upon the outside of his door.

+

Thus day followed day; and as sure as morning came he found that his unseen enemies had kept their register, and had marked up in some conspicuous position how many days were still left to him out of the month of grace. Sometimes the fatal numbers appeared upon the walls, sometimes upon the floors, occasionally they were on small placards stuck upon the garden gate or the railings. With all his vigilance John Ferrier could not discover whence these daily warnings proceeded. A horror which was almost superstitious came upon him at the sight of them. He became haggard and restless, and his eyes had the troubled look of some hunted creature. He had but one hope in life now, and that was for the arrival of the young hunter from Nevada.

+

Twenty had changed to fifteen, and fifteen to ten, but there was no news of the absentee. One by one the numbers dwindled down, and still there came no sign of him. Whenever a horseman clattered down the road, or a driver shouted at his team, the old farmer hurried to the gate, thinking that help had arrived at last. At last, when he saw five give way to four and that again to three, he lost heart, and abandoned all hope of escape. Single-handed, and with his limited knowledge of the mountains which surrounded the settlement, he knew that he was powerless. The more-frequented roads were strictly watched and guarded, and none could pass along them without on order from the Council. Turn which way he would, there appeared to be no avoiding the blow which hung over him. Yet the old man never wavered in his resolution to part with life itself before he consented to what he regarded as his daughter's dishonour.

+

He was sitting alone one evening pondering deeply over his troubles, and searching vainly for some way out of them. That morning had shown the figure 2 upon the wall of the house, and the next day would be the last of the allotted time. What was to happen then? All manner of vague and terrible fancies filled his imagination. And his daughter—what was to become of her after he was gone? Was there no escape from the invisible network which was drawn all round them. He sank his head upon the table and sobbed at the thought of his own impotence.

+

What was that? In the silence he heard a gentle scratching sound—low, but very distinct in the quiet of the night. It came from the door of the house. Ferrier crept into the hall and listened intently. There was a pause for a few moments, and then the low insidious sound was repeated. Some one was evidently tapping very gently upon one of the panels of the door. Was it some midnight assassin who had come to carry out the murderous orders of the secret tribunal? Or was it some agent who was marking up that the last day of grace had arrived. John Ferrier felt that instant death would be better than the suspense which shook his nerves and chilled his heart. Springing forward, he drew the bolt and threw the door open.

+ +

Outside all was calm and quiet. The night was fine, and the stars were twinkling brightly overhead. The little front garden lay before the farmer's eyes bounded by the fence and gate, but neither there nor on the road was any human being to be seen. With a sigh of relief, Ferrier looked to right and to left, until, happening to glance straight down at his own feet, he saw to his astonishment a man lying flat upon his face upon the ground, with arms and legs all asprawl.

+

So unnerved was he at the sight that he leaned up against the wall with his hand to his throat to stifle his inclination to call out. His first thought was that the prostrate figure was that of some wounded or dying man, but as he watched it he saw it writhe along the ground and into the hall with the rapidity and noiselessness of a serpent. Once within the house the man sprang to his feet, closed the door, and revealed to the astonished farmer the fierce face and resolute expression of Jefferson Hope.

+

"Good God!" gasped John Ferrier. "How you scared me! Whatever made you come in like that?"

+

"Give me food," the other said, hoarsely. "I have had no time for bit or sup for eight-and-forty hours." He flung himself upon the cold meat and bread which were still lying upon the table from his host's supper, and devoured it voraciously. "Does Lucy bear up well?" he asked, when he had satisfied his hunger.

+

"Yes. She does not know the danger," her father answered.

+

"That is well. The house is watched on every side. That is why I crawled my way up to it. They may be darned sharp, but they're not quite sharp enough to catch a Washoe hunter."

+

John Ferrier felt a different man now that he realized that he had a devoted ally. He seized the young man's leathery hand and wrung it cordially. "You're a man to be proud of," he said. "There are not many who would come to share our danger and our troubles."

+

"You've hit it there, pard," the young hunter answered. "I have a respect for you, but if you were alone in this business I'd think twice before I put my head into such a hornet's nest. It's Lucy that brings me here, and before harm comes on her I guess there will be one less o' the Hope family in Utah."

+

"What are we to do?"

+

"To-morrow is your last day, and unless you act to-night you are lost. I have a mule and two horses waiting in the Eagle Ravine. How much money have you?"

+

"Two thousand dollars in gold, and five in notes."

+

"That will do. I have as much more to add to it. We must push for Carson City through the mountains. You had best wake Lucy. It is as well that the servants do not sleep in the house."

+

While Ferrier was absent, preparing his daughter for the approaching journey, Jefferson Hope packed all the eatables that he could find into a small parcel, and filled a stoneware jar with water, for he knew by experience that the mountain wells were few and far between. He had hardly completed his arrangements before the farmer returned with his daughter all dressed and ready for a start. The greeting between the lovers was warm, but brief, for minutes were precious, and there was much to be done.

+

"We must make our start at once," said Jefferson Hope, speaking in a low but resolute voice, like one who realizes the greatness of the peril, but has steeled his heart to meet it. "The front and back entrances are watched, but with caution we may get away through the side window and across the fields. Once on the road we are only two miles from the Ravine where the horses are waiting. By daybreak we should be halfway through the mountains."

+

"What if we are stopped?" asked Ferrier.

+

Hope slapped the revolver butt which protruded from the front of his tunic. "If they are too many for us, we shall take two or three of them with us," he said with a sinister smile.

+

The lights inside the house had all been extinguished, and from the darkened window Ferrier peered over the fields which had been his own, and which he was now about to abandon for ever. He had long nerved himself to the sacrifice, however, and the thought of the honour and happiness of his daughter outweighed any regret at his ruined fortunes. All looked so peaceful and happy, the rustling trees and the broad silent stretch of grainland, that it was difficult to realize that the spirit of murder lurked through it all. Yet the white face and set expression of the young hunter showed that in his approach to the house he had seen enough to satisfy him upon that head.

+

Ferrier carried the bag of gold and notes, Jefferson Hope had the scanty provisions and water, while Lucy had a small bundle containing a few of her more valued possessions. Opening the window very slowly and carefully, they waited until a dark cloud had somewhat obscured the night, and then one by one passed through into the little garden. With bated breath and crouching figures they stumbled across it, and gained the shelter of the hedge, which they skirted until they came to the gap which opened into the cornfield. They had just reached this point when the young man seized his two companions and dragged them down into the shadow, where they lay silent and trembling.

+

It was as well that his prairie training had given Jefferson Hope the ears of a lynx. He and his friends had hardly crouched down before the melancholy hooting of a mountain owl was heard within a few yards of them, which was immediately answered by another hoot at a small distance. At the same moment a vague, shadowy figure emerged from the gap for which they had been making, and uttered the plaintive signal cry again, on which a second man appeared out of the obscurity.

+

"To-morrow at midnight," said the first, who appeared to be in authority. "When the Whip-poor-Will calls three times."

+

"It is well," returned the other. "Shall I tell Brother Drebber?"

+

"Pass it on to him, and from him to the others. Nine to seven!"

+

"Seven to five!" repeated the other; and the two figures flitted away in different directions. Their concluding words had evidently been some form of sign and countersign. The instant that their footsteps had died away in the distance, Jefferson Hope sprang to his feet, and helping his companions through the gap, led the way across the fields at the top of his speed, supporting and half-carrying the girl when her strength appeared to fail her.

+

"Hurry on! hurry on!" he gasped from time to time. "We are through the line of sentinels. Everything depends on speed. Hurry on!"

+

Once on the high road, they made rapid progress. Only once did they meet any one, and then they managed to slip into a field, and so avoid recognition. Before reaching the town the hunter branched away into a rugged and narrow footpath which led to the mountains. Two dark, jagged peaks loomed above them through the darkness, and the defile which led between them was the Eagle Cañon in which the horses were awaiting them. With unerring instinct Jefferson Hope picked his way among the great boulders and along the bed of a dried-up watercourse, until he came to the retired corner screened with rocks, where the faithful animals had been picketed. The girl was placed upon the mule, and old Ferrier upon one of the horses, with his money-bag, while Jefferson Hope led the other along the precipitous and dangerous path.

+

It was a bewildering route for any one who was not accustomed to face Nature in her wildest moods. On the one side a great crag towered up a thousand feet or more, black, stern, and menacing, with long basaltic columns upon his rugged surface like the ribs of some petrified monster. On the other hand a wild chaos of boulders and débris made all advance impossible. Between the two ran the irregular track, so narrow in places that they had to travel in Indian file, and so rough that only practised riders could have traversed it at all. Yet, in spite of all dangers and difficulties, the hearts of the fugitives were light within them, for every step increased the distance between them and the terrible despotism from which they were flying.

+

They soon had a proof, however, that they were still within the jurisdiction of the Saints. They had reached the very wildest and most desolate portion of the pass when the girl gave a startled cry, and pointed upwards. On a rock which overlooked the track, showing out dark and plain against the sky, there stood a solitary sentinel. He saw them as soon as they perceived him, and his military challenge of "Who goes there?" rang through the silent ravine.

+

"Travellers for Nevada," said Jefferson Hope, with his hand upon the rifle which hung by his saddle.

+

They could see the lonely watcher fingering his gun, and peering down at them as if dissatisfied at their reply.

+

"By whose permission?" he asked.

+

"The Holy Four," answered Ferrier. His Mormon experiences had taught him that that was the highest authority to which he could refer.

+ +

"Nine to seven," cried the sentinel.

+

"Seven to five," returned Jefferson Hope promptly, remembering the countersign which he had heard in the garden.

+

"Pass, and the Lord go with you," said the voice from above. Beyond his post the path broadened out and the horses were able to break into a trot. Looking back, they could see the solitary watcher leaning upon his gun, and knew that they had passed the outlying post of the chosen people, and that freedom lay before them.

+
+
+ + diff --git a/books/a-study-in-scarlet/Part2-05.html b/books/a-study-in-scarlet/Part2-05.html new file mode 100644 index 0000000..96b0a45 --- /dev/null +++ b/books/a-study-in-scarlet/Part2-05.html @@ -0,0 +1,51 @@ + + + + 2.5 • The Avenging Angels + + + + +
+

The Avenging Angels

+ +
+

All night their course lay through intricate defiles and over irregular and rock-strewn paths. More than once they lost their way, but Hope's intimate knowledge of the mountains enabled them to regain the track once more. When morning broke, a scene of marvellous though savage beauty lay before them. In every direction the great snow-capped peaks hemmed them in, peeping over each other's shoulders to the far horizon. So steep were the rocky banks on either side of them that the larch and the pine seemed to be suspended over their heads, and to need only a gust of wind to come hurtling down upon them. Nor was the fear entirely an illusion, for the barren valley was thickly strewn with trees and boulders which had fallen in a similar manner. Even as they passed, a great rock came thundering down with a hoarse rattle which woke the echoes in the silent gorges, and startled the weary horses into a gallop.

+

As the sun rose slowly above the eastern horizon, the caps of the great mountains lit up one after the other, like lamps at a festival, until they were all ruddy and glowing. The magnificent spectacle cheered the hearts of the three fugitives and gave them fresh energy. At a wild torrent which swept out of a ravine they called a halt and watered their horses, while they partook of a hasty breakfast. Lucy and her father would fain have rested longer, but Jefferson Hope was inexorable. "They will be upon our track by this time," he said. "Everything depends upon our speed. Once safe in Carson, we may rest for the remainder of our lives."

+

During the whole of that day they struggled on through the defiles, and by evening they calculated that they were more than thirty miles from their enemies. At night-time they chose the base of a beetling crag, where the rocks offered some protection from the chill wind, and there, huddled together for warmth, they enjoyed a few hours' sleep. Before daybreak, however, they were up and on their way once more. They had seen no sign of any pursuers, and Jefferson Hope began to think that they were fairly out of the reach of the terrible organization whose enmity they had incurred. He little knew how far that iron grasp could reach, or how soon it was to close upon them and crush them.

+

About the middle of the second day of their flight their scanty store of provisions began to run out. This gave the hunter little uneasiness, however, for there was game to be had among the mountains, and he had frequently before had to depend upon his rifle for the needs of life. Choosing a sheltered nook, he piled together a few dried branches and made a blazing fire, at which his companions might warm themselves, for they were now nearly five thousand feet above the sea level, and the air was bitter and keen. Having tethered the horses, and bade Lucy adieu, he threw his gun over his shoulder, and set out in search of whatever chance might throw in his way. Looking back, he saw the old man and the young girl crouching over the blazing fire, while the three animals stood motionless in the background. Then the intervening rocks hid them from his view.

+

He walked for a couple of miles through one ravine after another without success, though, from the marks upon the bark of the trees, and other indications, he judged that there were numerous bears in the vicinity. At last, after two or three hours' fruitless search, he was thinking of turning back in despair, when casting his eyes upwards he saw a sight which sent a thrill of pleasure through his heart. On the edge of a jutting pinnacle, three or four hundred feet above him, there stood a creature somewhat resembling a sheep in appearance, but armed with a pair of gigantic horns. The big-horn—for so it is called—was acting, probably, as a guardian over a flock which were invisible to the hunter; but fortunately it was heading in the opposite direction, and had not perceived him. Lying on his face, he rested his rifle upon a rock, and took a long and steady aim before drawing the trigger. The animal sprang into the air, tottered for a moment upon the edge of the precipice, and then came crashing down into the valley beneath.

+

The creature was too unwieldy to lift, so the hunter contented himself with cutting away one haunch and part of the flank. With this trophy over his shoulder he hastened to retrace his steps, for the evening was already drawing in. He had hardly started, however, before he realized the difficulty which faced him. In his eagerness he had wandered far past ravines which were known to him, and it was no easy matter to pick out the path which he had taken. The valley in which he found himself divided and sub-divided into many gorges, which were so like each other that it was impossible to distinguish one from the other. He followed one for a mile or more until he came to a mountain torrent which he was sure he had never seen before. Convinced that he had taken the wrong turn, he tried another, but with the same result. Night was coming on rapidly, and it was almost dark before he at last found himself in a defile which was familiar to him. Even then it was no easy matter to keep to the right track, for the moon had not yet risen, and the high cliffs on either side made the obscurity more profound. Weighed down with his burden, and weary from his exertions, he stumbled along, keeping up his heart by the reflection that every step brought him nearer to Lucy, and that he carried with him enough to ensure them food for the remainder of their journey.

+

He had now come to the mouth of the very defile in which he had left them. Even in the darkness he could recognize the outline of the cliffs which bounded it. They must, he reflected, be awaiting him anxiously, for he had been absent nearly five hours. In the gladness of his heart he put his hands to his mouth and made the glen re-echo to a loud halloo as a signal that he was coming. He paused and listened for an answer. None came save his own cry, which clattered up the dreary silent ravines, and was borne back to his ears in countless repetitions. Again he shouted, even louder than before, and again no whisper came back from his friends whom he had left such a short time ago. A vague, nameless dread came over him, and he hurried onwards frantically, dropping the precious food in his agitation.

+

When he turned the corner, he came full in sight of the spot where the fire had been lit. There was still a glowing pile of wood ashes there, but it had evidently not been tended since his departure. The same dead silence still reigned all round. With his fears all changed to convictions, he hurried on. There was no living creature near the remains of the fire: animals, man, maiden, all were gone. It was only too clear that some sudden and terrible disaster had occurred during his absence—a disaster which had embraced them all, and yet had left no traces behind it.

+ +

Bewildered and stunned by this blow, Jefferson Hope felt his head spin round, and had to lean upon his rifle to save himself from falling. He was essentially a man of action, however, and speedily recovered from his temporary impotence. Seizing a half-consumed piece of wood from the smouldering fire, he blew it into a flame, and proceeded with its help to examine the little camp. The ground was all stamped down by the feet of horses, showing that a large party of mounted men had overtaken the fugitives, and the direction of their tracks proved that they had afterwards turned back to Salt Lake City. Had they carried back both of his companions with them? Jefferson Hope had almost persuaded himself that they must have done so, when his eye fell upon an object which made every nerve of his body tingle within him. A little way on one side of the camp was a low-lying heap of reddish soil, which had assuredly not been there before. There was no mistaking it for anything but a newly-dug grave. As the young hunter approached it, he perceived that a stick had been planted on it, with a sheet of paper stuck in the cleft fork of it. The inscription upon the paper was brief, but to the point:—

+

JOHN FERRIER,
+ Formerly of Salt Lake City,
+ Died August 4th, 1860.

+

 

+

The sturdy old man whom he had left so short a time before, was gone, then, and this was all his epitaph. Jefferson Hope looked wildly round to see if there was a second grave, but there was no sign of one. Lucy had been carried back by their terrible pursuers to fulfil her original destiny, by becoming one of the harem of the Elder's son. As the young fellow realized the certainty of her fate, and his own powerlessness to prevent it, he wished that he, too, was lying with the old farmer in his last silent resting-place.

+

Again, however, his active spirit shook off the lethargy which springs from despair. If there was nothing else left to him, he could at least devote his life to revenge. With indomitable patience and perseverance, Jefferson Hope possessed also a power of sustained vindictiveness, which he may have learned from the Indians amongst whom he had lived. As he stood by the desolate fire, he felt that the only one thing which could assuage his grief would be thorough and complete retribution, brought by his own hand upon his enemies. His strong will and untiring energy should, he determined, be devoted to that one end. With a grim, white face, he retraced his steps to where he had dropped the food, and having stirred up the smouldering fire, he cooked enough to last him for a few days. This he made up into a bundle, and, tired as he was, he set himself to walk back through the mountains upon the track of the avenging angels.

+

For five days he toiled footsore and weary through the defiles which he had already traversed on horseback. At night he flung himself down among the rocks, and snatched a few hours of sleep; but before daybreak he was always well on his way. On the sixth day, he reached the Eagle Cañon, from which they had commenced their ill-fated flight. Thence he could look down upon the home of the saints. Worn and exhausted, he leaned upon his rifle and shook his gaunt hand fiercely at the silent widespread city beneath him. As he looked at it, he observed that there were flags in some of the principal streets, and other signs of festivity. He was still speculating as to what this might mean when he heard the clatter of horse's hoofs, and saw a mounted man riding towards him. As he approached, he recognized him as a Mormon named Cowper, to whom he had rendered services at different times. He therefore accosted him when he got up to him, with the object of finding out what Lucy Ferrier's fate had been.

+

"I am Jefferson Hope," he said. "You remember me."

+

The Mormon looked at him with undisguised astonishment—indeed, it was difficult to recognize in this tattered, unkempt wanderer, with ghastly white face and fierce, wild eyes, the spruce young hunter of former days. Having, however, at last satisfied himself as to his identity, the man's surprise changed to consternation.

+

"You are mad to come here," he cried. "It is as much as my own life is worth to be seen talking with you. There is a warrant against you from the Holy Four for assisting the Ferriers away."

+

"I don't fear them, or their warrant," Hope said earnestly. "You must know something of this matter, Cowper. I conjure you by everything you hold dear to answer a few questions. We have always been friends. For God's sake, don't refuse to answer me."

+

"What is it?" the Mormon asked uneasily. "Be quick. The very rocks have ears and the trees eyes."

+

"What has become of Lucy Ferrier?"

+

"She was married yesterday to young Drebber. Hold up man, hold up; you have no life left in you."

+

"Dont mind me," said Hope faintly. He was white to the very lips, and had sunk down on the stone against which he had been leaning. "Married, you say?"

+

"Married yesterday—that's what those flags are for on the Endowment House. There was some words between young Drebber and young Stangerson as to which was to have her. They'd both been in the party that followed them, and Stangerson had shot her father, which seemed to give him the best claim; but when they argued it out in council, Drebber's party was the stronger, so the Prophet gave her over to him. No one won't have her very long though, for I saw death in her face yesterday. She is more like a ghost than a woman. Are you off, then?"

+

"Yes, I am off," said Jefferson Hope, who had risen from his seat. His face might have been chiselled out of marble, so hard and set was its expression, while its eyes glowed with a baleful light.

+

"Where are you going?"

+

"Never mind," he answered; and, slinging his weapon over his shoulder, strode off down the gorge and so away into the heart of the mountains to the haunts of the wild beasts. Amongst them all there was none so fierce and so dangerous as himself.

+

The prediction of the Mormon was only too well fulfilled. Whether it was the terrible death of her father or the effects of the hateful marriage into which she had been forced, poor Lucy never held up her head again, but pined away and died within a month. Her sottish husband, who had married her principally for the sake of John Ferrier's property, did not affect any great grief at his bereavement; but his other wives mourned over her, and sat up with her the night before the burial, as is the Mormon custom. They were grouped round the bier in the early hours of the morning, when, to their inexpressible fear and astonishment, the door was flung open, and a savage-looking weather-beaten man in tattered garments strode into the room. Without a glance or a word to the cowering women, he walked up to the white silent figure which had once contained the pure soul of Lucy Ferrier. Stooping over her, he pressed his lips reverently to her cold forehead, and then, snatching up her hand, he took the wedding-ring from her finger. "She shall not be buried in that," he cried with a fierce snarl, and before an alarm could be raised sprang down the stairs and was gone. So strange and so brief was the episode that the watchers might have found it hard to believe it themselves or persuade other people of it, had it not been for the undeniable fact that the circlet of gold which marked her as having been a bride had disappeared.

+

For some months Jefferson Hope lingered among the mountains, leading a strange, wild life, and nursing in his heart the fierce desire for vengeance which possessed him. Tales were told in the city of the wierd figure which was seen prowling about the suburbs, and which haunted the lonely mountain gorges. Once a bullet whistled through Stangerson's window and flattened itself upon the wall within a foot of him. On another occasion, as Drebber passed under a cliff a great boulder crashed down on him, and he only escaped a terrible death by throwing himself upon his face. The two young Mormons were not long in discovering the reason of these attempts upon their lives, and led repeated expeditions into the mountains in the hope of capturing or killing their enemy, but always without success. Then they adopted the precaution of never going out alone or after night-fall, and of having their houses guarded. After a time they were able to relax these measures, for nothing was either heard or seen of their opponent, and they hoped that time had cooled his vindictiveness.

+

Far from doing so, it had, if anything, augmented it. The hunter's mind was of a hard, unyielding nature, and the predominant idea of revenge had taken such complete possession of it that there was no room for any other emotion. He was, however, above all things, practical. He soon realized that even his iron constitution could not stand the incessant strain which he was putting upon it. Exposure and want of wholesome food were wearing him out. If he died like a dog among the mountains, what was to become of his revenge then? And yet such a death was sure to overtake him if he persisted. He felt that that was to play his enemy's game, so he reluctantly returned to the old Nevada mines, there to recruit his health and to amass money enough to allow him to pursue his object without privation.

+

His intention had been to be absent a year at the most, but a combination of unforeseen circumstances prevented his leaving the mines for nearly five. At the end of that time, however, his memory of his wrongs and his craving for revenge were quite as keen as on that memorable night when he had stood by John Ferrier's grave. Disguised, and under an assumed name, he returned to Salt Lake City, careless what became of his own life, as long as he obtained what he knew to be justice. There he found evil tidings awaiting him. There had been a schism among the Chosen People a few months before, some of the younger members of the Church having rebelled against the authority of the Elders, and the result had been the secession of a certain number of the malcontents, who had left Utah and become Gentiles. Among these had been Drebber and Stangerson; and no one knew whither they had gone. Rumour reported that Drebber had managed to convert a large part of his property into money, and that he had departed a wealthy man, while his companion Stangerson, was comparitively poor. There was no clue at all, however, as to their whereabouts.

+

Many a man, however vindictive, would have abandoned all thought of revenge in the face of such a difficulty, but Jefferson Hope never faltered for a moment. With the small competence he possessed, eked out by such employment as he could pick up, he travelled from town to town through the United States in quest of his enemies. Year passed into year, his black hair turned grizzled, but still he wandered on, a human bloodhound, with his mind wholly set upon the one object to which he had devoted his life. At last his perseverance was rewarded. It was but a glance of a face in a window, but that one glance told him that Cleveland in Ohio possessed the men whom he was in pursuit of. He returned to his miserable lodgings with his plan of vengeance all arranged. It chanced, however, that Drebber, looking from his window, had recognized the vagrant in the street, and had read murder in his eyes. He hurried before a justice of the peace, accompanied by Stangerson, who had become his private secretary, and represented to him that they were in danger of their lives from the jealousy and hatred of an old rival. That evening Jefferson Hope was taken into custody, and not being able to find sureties, was detained for some weeks. When at last he was liberated it was only to find that Drebber's house was deserted, and that he and his secretary had departed for Europe.

+

Again the avenger had been foiled, and again his concentrated hatred urged him to continue the pursuit. Funds were wanting, however, and for some time he had to return to work, saving every dollar for his approaching journey. At last, having collected enough to keep life in him he departed for Europe, and tracked his enemies from city to city, working his way in any menial capacity, but never over-taking the fugitives. When he reached St. Petersburg, they had departed for Paris; and when he followed them there, he learned that they had just set off for Copenhagen. At the Danish capital he was again a few days late, for they had journeyed on to London, where he at last succeeded in running them to earth. As to what occurred there, we cannot do better than quote the old hunter's own account, as duly recorded in Dr. Watson's Journal, to which we are already under such obligations.

+
+
+ + diff --git a/books/a-study-in-scarlet/Part2-06.html b/books/a-study-in-scarlet/Part2-06.html new file mode 100644 index 0000000..28d8297 --- /dev/null +++ b/books/a-study-in-scarlet/Part2-06.html @@ -0,0 +1,74 @@ + + + + 2.6 • A Continuation of the Reminiscences of John Watson M.D. + + + + +
+

A Continuation of the Reminiscences
of John Watson M.D.

+ +
+

Our prisoner's furious resistance did not apparently indicate any ferocity in his disposition towards ourselves, for on finding himself powerless, he smiled in an affable manner, and expressed his hopes that he had not hurt any of us in the scuffle. "I guess you're going to take me to the police-station," he remarked to Sherlock Holmes. "My cab's at the door. If you'll loose my legs I'll walk down to it. I'm not so light to lift as I used to be."

+

Gregson and Lestrade exchanged glances, as if they thought this proposition rather a bold one; but Holmes at once took the prisoner at his word, and loosened the towel which we had bound round his ankles. He rose and stretched his legs, as though to assure himself that they were free once more. I remember that I thought to myself, as I eyed him, that I had seldom seen a more powerfully built man; and his dark, sunburnt face bore an expression of determination and energy which was as formidable as his personal strength.

+

"If there's a vacant place for a chief of the police, I reckon you are the man for it," he said, gazing with undisguised admiration at my fellow-lodger. "The way you kept on my trail was a caution."

+

"You had better come with me," said Holmes to the two detectives.

+

"I can drive you," said Lestrade.

+

"Good! and Gregson can come inside with me. You too, Doctor. You have taken an interest in the case, and may as well stick to us."

+

I assented gladly, and we all descended together. Our prisoner made no attempt at escape, but stepped calmly into the cab which had been his, and we followed him. Lestrade mounted the box, whipped up the horse, and brought us in a very short time to our destination. We were ushered into a small chamber, where a police inspector noted down our prisoner's name and the names of the men with whose murder he had been charged. The official was a white-faced, unemotional man, who went through his duties in a dull, mechanical way. "The prisoner will be put before the magistrates in the course of the week," he said; "in the meantime, Mr. Jefferson Hope, have you anything that you wish to say? I must warn you that your words will be taken down, and may be used against you."

+ +

"I've got a good deal to say," our prisoner said slowly. "I want to tell you gentlemen all about it."

+

"Hadn't you better reserve that for your trial?" asked the inspector.

+

"I may never be tried," he answered. "You needn't look startled. It isn't suicide I am thinking of. Are you a doctor?" He turned his fierce dark eyes upon me as he asked this last question.

+

"Yes, I am," I answered.

+

"Then put your hand here," he said, with a smile, motioning with his manacled wrist towards his chest.

+

I did so; and became at once conscious of an extraordinary throbbing and commotion which was going on inside. The walls of his chest seemed to thrill and quiver as a frail building would do inside when some powerful engine was at work. In the silence of the room I could hear a dull humming and buzzing noise which proceeded from the same source.

+

"Why," I cried, "you have an aortic aneurism!"

+

"That's what they call it," he said, placidly. "I went to a doctor last week about it, and he told me that it is bound to burst before many days passed. It has been getting worse for years. I got it from over-exposure and under-feeding among the Salt Lake Mountains. I've done my work now, and I don't care how soon I go, but I should like to leave some account of the business behind me. I don't want to be remembered as a common cut-throat."

+

The inspector and the two detectives had an hurried discussion as to the advisability of allowing him to tell his story.

+

"Do you consider, Doctor, that there is immediate danger?" the former asked.

+

"Most certainly there is," I answered.

+

"In that case it is clearly our duty, in the interests of justice, to take his statement," said the inspector. "You are at liberty, sir, to give your account, which I again warn you will be taken down."

+

"I'll sit down, with your leave," the prisoner said, suiting the action to the word. "This aneurism of mine makes me easily tired, and the tussle we had half an hour ago has not mended matters. I'm on the brink of the grave, and I am not likely to lie to you. Every word I say is the absolute truth, and how you use it is a matter of no consequence to me."

+

With these words, Jefferson Hope leaned back in his chair and began the following remarkable statement. He spoke in a calm and methodical manner, as though the events which he narrated were commonplace enough. I can vouch for the accuracy of the subjoined account, for I have had access to Lestrade's note-book, in which the prisoner's words were taken down exactly as they were uttered.

+

"It don't much matter to you why I hated these men," he said; "it's enough that they were guilty of the death of two human beings—a father and a daughter—and that they had, therefore, forfeited their own lives. After the lapse of time that has passed since their crime, it was impossible for me to secure a conviction against them in any court. I knew of their guilt though, and I determined that I should be judge, jury, and executioner all rolled into one. You'd have done the same, if you have any manhood in you, if you had been in my place.

+

"That girl that I spoke of was to have married me twenty years ago. She was forced into marrying that same Drebber, and broke her heart over it. I took the marriage ring from her dead finger, and I vowed that his dying eyes should rest upon that very ring, and that his last thoughts should be of the crime for which he was punished. I have carried it about with me, and have followed him and his accomplice over two continents until I caught them. They thought to tire me out, but they could not do it. If I die to-morrow, as is likely enough, I die knowing that my work in this world is done, and well done. They have perished, and by my hand. There is nothing left for me to hope for, or to desire.

+

"They were rich and I was poor, so that it was no easy matter for me to follow them. When I got to London my pocket was about empty, and I found that I must turn my hand to something for my living. Driving and riding are as natural to me as walking, so I applied at a cab-owner's office, and soon got employment. I was to bring a certain sum a week to the owner, and whatever was over that I might keep for myself. There was seldom much over, but I managed to scrape along somehow. The hardest job was to learn my way about, for I reckon that of all the mazes that ever were contrived, this city is the most confusing. I had a map beside me though, and when once I had spotted the principal hotels and stations. I got on pretty well.

+

"It was some time before I found out where my two gentlemen were living; but I inquired and inquired until at last I dropped across them. They were at a boarding-house at Camberwell, over on the other side of the river. When once I found them out, I knew that I had them at my mercy. I had grown my beard, and there was no chance of their recognizing me. I would dog them and follow them until I saw my opportunity. I was determined that they should not escape me again.

+

"They were very near doing it for all that. Go where they would about London, I was always at their heels. Sometimes I followed them on my cab, and sometimes on foot, but the former was the best, for then they could not get away from me. It was only early in the morning or late at night that I could earn anything, so that I began to get behindhand with my employer. I did not mind that, however, as long as I could lay my hand upon the men I wanted.

+

"They were very cunning, though. They must have thought that there was some chance of their being followed, for they would never go out alone, and never after nightfall. During two weeks I drove behind them every day, and never once saw them separate. Drebber himself was drunk half the time, but Stangerson was not to be caught napping. I watched them late and early, but never saw the ghost of a chance; but I was not discouraged, for something told me that the hour had almost come. My only fear was that this thing in my chest might burst a little too soon and leave my work undone.

+

"At last, one evening I was driving up and down Torquay Terrace, as the street was called in which they boarded, when I saw a cab drive up to their door. Presently some luggage was brought out and after a time Drebber and Stangerson followed it, and drove off. I whipped up my horse and kept within sight of them, feeling very ill at ease, for I feared that they were going to shift their quarters. At Euston Station they got out and I left a boy to hold my horse and followed them on to the platform. I heard them ask for the Liverpool train, and the guard answer that one had just gone, and there would not be another for some hours. Stangerson seemed to be put out at that, but Drebber was rather pleased than otherwise. I got so close to them in the bustle that I could hear every word that passed between them. Drebber said that he had a little business of his own to do, and that if the other would wait for him he would soon rejoin him. His companion remonstrated with him, and reminded him that they had resolved to stick together. Drebber answered that the matter was a delicate one, and that he must go alone. I could not catch what Stangerson said to that, but the other burst out swearing, and reminded him that he was nothing more than his paid servant, and that he must not presume to dictate to him. On that the secretary gave it up as a bad job, and simply bargained with him that if he missed the last train he should rejoin him at Halliday's Private Hotel; to which Drebber answered that he would be back on the platform before eleven, and made his way out of the station.

+

"The moment for which I had waited so long had at last come. I had my enemies within my power. Together they could protect each other, but singly they were at my mercy. I did not act, however, with undue precipitation. My plans were already formed. There is no satisfaction in vengeance unless the offender has time to realize who it is that strikes him, and why retribution has come upon him. I had my plans arranged by which I should have the opportunity of making the man who had wronged me understand that his old sin had found him out. It chanced that some days before a gentleman who had been engaged in looking over some houses in the Brixton Road had dropped the key of one of them in my carriage. It was claimed that same evening, and returned; but in the interval I had taken a moulding of it, and had a duplicate constructed. By means of this I had access to at least one spot in this great city where I could rely upon being free from interruption. How to get Drebber to that house was the difficult problem which I had now to solve.

+

"He walked down the road and went into one or two liquor shops, staying for nearly half an hour in the last of them. When he came out, he staggered in his walk, and was evidently pretty well on. There was a hansom just in front of me, and he hailed it. I followed it so close that the nose of my horse was within a yard of his driver the whole way. We rattled across Waterloo Bridge and through miles of streets, until to my astonishment, we found ourselves back in the terrace in which he had boarded. I could not imagine what his intention was in returning there; but I went on and pulled up my cab a hundred yards or so from the house. He entered it, and his hansom drove away. Give me a glass of water, if you please. My mouth gets dry with the talking."

+

I handed him the glass, and he drank it down.

+

"That's better," he said. "Well, I waited for a quarter of an hour, or more, when suddenly there came a noise like people struggling inside the house. Next moment the door was flung open and two men appeared, one of whom was Drebber, and the other was a young chap whom I had never seen before. This fellow had Drebber by the collar, and when they came to the head of the steps he gave him a shove and a kick which sent him half across the road. 'You hound!' he cried, shaking his stick at him; 'I'll teach you to insult an honest girl!' He was so hot that I think he would have thrashed Drebber with his cudgel, only that the cur staggered away down the road as fast as his legs would carry him. He ran as far as the corner, and then seeing my cab, he hailed me and jumped in. 'Drive me to Halliday's Private Hotel,' said he.

+

"When I had him fairly inside my cab, my heart jumped so with joy that I feared lest at this last moment my aneurism might go wrong. I drove along slowly, weighing in my own mind what it was best to do. I might take him right out into the country, and there in some deserted lane have my last interview with him. I had almost decided upon this when he solved the problem for me. The craze for drink had seized him again, and he ordered me to pull up outside a gin palace. He went in, leaving word that I should wait for him. There he remained until closing time, and when he came out he was so far gone that I knew the game was in my own hands.

+

"Don't imagine that I intended to kill him in cold blood. It would only have been rigid justice if I had done so, but I could not bring myself to do it. I had long determined that he should have a show for his life if he chose to take advantage of it. Among the many billets which I have filled in America during my wandering life, I was once janitor and sweeper-out of the laboratory at York College. One day the professor was lecturing on poisons, and he showed his students some alkaloid, as he called it, which he had extracted from some South American arrow poison, and which was so powerful that the least grain meant instant death. I spotted the bottle in which this preparation was kept, and when they were all gone, I helped myself to a little of it. I was a fairly good dispenser, so I worked this alkaloid into small, soluble pills, and each pill I put in a box with a similar pill made without the poison. I determined at the time that when I had my chance my gentlemen should each have a draw out of one of these boxes, while I ate the pill that remained. It would be quite as deadly and a good deal less noisy than firing across a handkerchief. From that day I had always my pill boxes about with me, and the time had now come when I was to use them.

+

"It was nearer one than twelve, and a wild, bleak night, blowing hard and raining in torrents. Dismal as it was outside, I was glad within—so glad that I could have shouted out from pure exultation. If any of you gentlemen have ever pined for a thing, and longed for it during twenty long years, and then suddenly found it within your reach, you would understand my feelings. I lit a cigar, and puffed at it to steady my nerves, but my hands were trembling and my temples throbbing with excitement. As I drove, I could see old John Ferrier and sweet Lucy looking at me out of the darkness and smiling at me, just as plain as I see you all in this room. All the way they were ahead of me, one on each side of the horse until I pulled up at the house in the Brixton Road.

+

"There was not a soul to be seen, nor a sound to be heard, except the dripping of the rain. When I looked in at the window, I found Drebber all huddled together in a drunken sleep. I shook him by the arm, 'It's time to get out,' I said.

+

"'All right, cabby,' said he.

+

"I suppose he thought we had come to the hotel that he had mentioned, for he got out without another word, and followed me down the garden. I had to walk beside him to keep him steady, for he was still a little top-heavy. When we came to the door, I opened it, and led him into the front room. I give you my word that all the way, the father and the daughter were walking in front of us.

+

"'It's infernally dark,' said he, stamping about.

+

"'We'll soon have a light,' I said, striking a match and putting it to a wax candle which I had brought with me. 'Now, Enoch Drebber,' I continued, turning to him, and holding the light to my own face, 'who am I?'

+ +

"He gazed at me with bleared, drunken eyes for a moment, and then I saw a horror spring up in them, and convulse his whole features, which showed me that he knew me. He staggered back with a livid face, and I saw the perspiration break out upon his brow, while his teeth chattered in his head. At the sight I leaned my back against the door and laughed loud and long. I had always known that vengeance would be sweet, but I had never hoped for the contentment of soul which now possessed me.

+

"'You dog!' I said; 'I have hunted you from Salt Lake City to St. Petersburg, and you have always escaped me. Now, at last your wanderings have come to an end, for either you or I shall never see to-morrow's sunrise.' He shrunk still farther away as I spoke, and I could see on his face that he thought I was mad. So I was for the time. The pulses in my temples beat like sledge-hammers, and I believe I would have had a fit of some sort if the blood had not gushed from my nose and relieved me.

+

"'What do you think of Lucy Ferrier now?' I cried, locking the door, and shaking the key in his face. 'Punishment has been slow in coming, but it has overtaken you at last.' I saw his coward lips tremble as I spoke. He would have begged for his life, but he knew well that it was useless.

+

"'Would you murder me?' he stammered.

+

"'There is no murder,' I answered. 'Who talks of murdering a mad dog? What mercy had you upon my poor darling, when you dragged her from her slaughtered father, and bore her away to your accursed and shameless harem?'

+

"'It was not I who killed her father,' he cried.

+

"'But it was you who broke her innocent heart,' I shrieked, thrusting the box before him. 'Let the high God judge between us. Choose and eat. There is death in one and life in the other. I shall take what you leave. Let us see if there is justice upon the earth, or if we are ruled by chance.'

+

"He cowered away with wild cries and prayers for mercy, but I drew my knife and held it to his throat until he had obeyed me. Then I swallowed the other, and we stood facing one another in silence for a minute or more, waiting to see which was to live and which was to die. Shall I ever forget the look which came over his face when the first warning pangs told him that the poison was in his system? I laughed as I saw it, and held Lucy's marriage ring in front of his eyes. It was but for a moment, for the action of the alkaloid is rapid. A spasm of pain contorted his features; he threw his hands out in front of him, staggered, and then, with a hoarse cry, fell heavily upon the floor. I turned him over with my foot, and placed my hand upon his heart. There was no movement. He was dead!

+

"The blood had been streaming from my nose, but I had taken no notice of it. I don't know what it was that put it into my head to write upon the wall with it. Perhaps it was some mischievous idea of setting the police upon a wrong track, for I felt light-hearted and cheerful. I remembered a German being found in New York with RACHE written up above him, and it was argued at the time in the newspapers that the secret societies must have done it. I guessed that what puzzled the New Yorkers would puzzle the Londoners, so I dipped my finger in my own blood and printed it on a place on the wall. Then I walked down to my cab and found that there was nobody about, and that the night was still very wild. I had driven some distance, when I put my hand into the pocket in which I usually kept Lucy's ring, and found that it was not there. I was thunderstruck at this, for it was the only memento that I had of her. Thinking that I might have dropped it when I stooped over Drebber's body, I drove back, and leaving my cab in a side street, I went boldly up to the house—for I was ready to dare anything rather than lose the ring. When I arrived there, I walked right into the arms of a police-officer who was coming out, and only managed to disarm his suspicions by pretending to be hopelessly drunk.

+

"That was how Enoch Drebber came to his end. All I had to do then was to do as much for Stangerson, and so pay off John Ferrier's debt. I knew that he was staying at Halliday's Private Hotel, and I hung about all day, but he never came out. I fancy that he suspected something when Drebber failed to put in an appearance. He was cunning, was Stangerson, and always on his guard. If he thought he could keep me off by staying indoors he was very much mistaken. I soon found out which was the window of his bedroom, and early next morning I took advantage of some ladders which were lying in the lane behind the hotel, and so made my way into his room in the grey of the dawn. I woke him up and told him that the hour had come when he was to answer for the life he had taken so long before. I described Drebber's death to him, and I gave him the same choice of the poisoned pills. Instead of grasping at the chance of safety which that offered him, he sprang from his bed and flew at my throat. In self-defence I stabbed him to the heart. It would have been the same in any case, for Providence would never have allowed his guilty hand to pick out anything but the poison.

+

"I have little more to say, and it's as well, for I am about done up. I went on cabbing it for a day or so, intending to keep at it until I could save enough to take me back to America. I was standing in the yard when a ragged youngster asked if there was a cabby there called Jefferson Hope, and said that his cab was wanted by a gentleman at 221B, Baker Street. I went round suspecting no harm, and the next thing I knew, this young man here had the bracelets on my wrists, and as neatly snackled as ever I saw in my life. That's the whole of my story, gentlemen. You may consider me to be a murderer; but I hold that I am just as much an officer of justice as you are."

+

So thrilling had the man's narrative been and his manner was so impressive that we had sat silent and absorbed. Even the professional detectives, blasé as they were in every detail of crime, appeared to be keenly interested in the man's story. When he finished, we sat for some minutes in a stillness which was only broken by the scratching of Lestrade's pencil as he gave the finishing touches to his shorthand account.

+

"There is only one point on which I should like a little more information," Sherlock Holmes said at last. "Who was your accomplice who came for the ring which I advertised?"

+

The prisoner winked at my friend jocosely. "I can tell my own secrets," he said, "but I don't get other people into trouble. I saw your advertisement, and I thought it might be a plant, or it might be the ring which I wanted. My friend volunteered to go and see. I think you'll own he did it smartly."

+

"Not a doubt of that," said Holmes heartily.

+

"Now, gentlemen," the inspector remarked, gravely, "the forms of the law must be complied with. On Thursday the prisoner will be brought before the magistrates, and your attendance will be required. Until then I will be responsible for him." He rang the bell as he spoke, and Jefferson Hope was led off by a couple of warders, while my friend and I made our way out of the station and took a cab back to Baker Street.

+
+
+ + diff --git a/books/a-study-in-scarlet/Part2-07.html b/books/a-study-in-scarlet/Part2-07.html new file mode 100644 index 0000000..603ea1c --- /dev/null +++ b/books/a-study-in-scarlet/Part2-07.html @@ -0,0 +1,45 @@ + + + + 2.7 • The Conclusion + + + + +
+

The Conclusion

+ +
+

We had all been warned to appear before the magistrates upon the Thursday; but when the Thursday came there was no occasion for our testimony. A higher Judge had taken the matter in hand, and Jefferson Hope had been summoned before a tribunal where strict justice would be meted out to him. On the very night after his capture the aneurism burst, and he was found in the morning stretched upon the floor of the cell, with a placid smile upon his face, as though he had been able in his dying moments to look back upon a useful life, and on work well done.

+

"Gregson and Lestrade will be wild about his death," Holmes remarked, as we chatted it over next evening. "Where will their grand advertisement be now?"

+

"I don't see that they had very much to do with his capture," I answered.

+

"What you do in this world is a matter of no consequence," returned my companion bitterly. "The question is, what can you make people believe that you have done? Never mind," he continued, more brightly, after a pause. "I would not have missed the investigation for anything. There has been no better case within my recollection. Simple as it was, there were several most instructive points about it."

+

"Simple!" I ejaculated.

+

"Well, really, it can hardly be described as otherwise," said Sherlock Holmes, smiling at my surprise. "The proof of its intrinsic simplicity is, that without any help save a few very ordinary deductions I was able to lay my hand upon the criminal within three days."

+

"That is true," said I.

+

"I have already explained to you that what is out of the common is usually a guide rather than a hindrance. In solving a problem of this sort, the grand thing is to be able to reason backwards. That is a very useful accomplishment, and a very easy one, but people do not practice it much. In the every-day affairs of life it is more useful to reason forwards, and so the other comes to be neglected. There are fifty who can reason synthetically for one who can reason analytically."

+

"I confess," said I, "that I do not quite follow you."

+

"I hardly expected that you would. Let me see if I can make it clearer. Most people, if you describe a train of events to them, will tell you what the result would be. They can put those events together in their minds, and argue from them that something will come to pass. There are few people, however, who, if you told them a result, would be able to evolve from their own inner consciousness what the steps were which led up to that result. This power is what I mean when I talk of reasoning backwards, or analytically."

+

"I understand," said I.

+

"Now this was a case in which you were given the result and had to find everything else for yourself. Now let me endeavour to show you the different steps in my reasoning. To begin at the beginning. I approached the house, as you know, on foot, and with my mind entirely free from all impressions. I naturally began by examining the roadway, and there, as I have already explained to you, I saw clearly the marks of a cab, which, I ascertained by inquiry, must have been there during the night. I satisfied myself that it was a cab and not a private carriage by the narrow gauge of the wheels. The ordinary London growler is considerable less wide than a gentleman's brougham.

+

"This was the first point gained. I then walked slowly down the garden path, which happened to be composed of a clay soil, peculiarly suitable for taking impressions. No doubt it appeared to you to be a mere trampled line of slush, but to my trained eyes every mark upon its surface had a meaning. There is no branch of detective science which is so important and so much neglected as the art of tracing footsteps. Happily, I have always laid great stress upon it, and much practice has made it second nature to me. I saw the heavy footmarks of the constables, but I saw also the track of the two men who had first passed through the garden. It was easy to tell that they had been before the others, because in places their marks had been entirely obliterated by the others coming upon the top of them. In this way my second link was formed, which told me that the nocturnal visitors were two in number, one remarkable for his height (as I calculated from the length of his stride), and the other fashionably dressed, to judge from the small and elegant impression left by his boots.

+

"On entering the house this last inference was confirmed. My well-booted man lay before me. The tall one, then, had done the murder, if murder there was. There was no wound upon the dead man's person, but the agitated expression upon his face assured me that he had foreseen his fate before it came upon him. Men who die from heart disease, or any sudden natural cause, never by any chance exhibit agitation upon their features. Having sniffed the dead man's lips, I detected a slightly sour smell, and I came to the conclusion that he had had poison forced upon him. Again, I argued that it had been forced upon him from the hatred and fear expressed upon his face. By the method of exclusion, I had arrived at this result, for no other hypothesis would meet the facts. Do not imagine that it was a very unheard-of idea. The forcible administration of poison is by no means a new thing in criminal annals. The cases of Dolsky in Odessa, and of Leturier in Montpellier, will occur at once to any toxicologist.

+

"And now came the great question as to the reason why. Robbery had not been the object of the murder, for nothing was taken. Was it politics, then, or was it a woman? That was the question which confronted me. I was inclined from the first to the latter supposition. Political assassins are only too glad to do their work and to fly. This murder had, on the contrary, been done most deliberately, and the perpetrator had left his tracks all over the room, showing that he had been there all the time. It must have been a private wrong, and not a political one, which called for such a methodical revenge. When the inscription was discovered upon the wall, I was more inclined than ever to my opinion. The thing was too evidently a blind. When the ring was found, however, it settled the question. Clearly the murderer had used it to remind his victim of some dead or absent woman. It was at this point that I asked Gregson whether he had inquired in his telegram to Cleveland as to particular point in Mr. Drebber's former career. He answered, you remember, in the negative.

+

"I then proceeded to make a careful examination of the room, which confirmed me in my opinion as to the murderer's height, and furnished me with the additional details as to the Trichinopoly cigar and the length of his nails. I had already come to the conclusion, since there were no signs of a struggle, that the blood which covered the floor had burst from the murderer's nose in his excitement. I could perceive that the track of blood coincided with the track of his feet. It is seldom that any man, unless he is very full-blooded, breaks out in this way through emotion, so I hazarded the opinion that the criminal was probably a robust and ruddy-faced man. Events proved that I had judged correctly.

+

"Having left the house, I proceeded to do what Gregson had neglected. I telegraphed to the head of the police at Cleveland, limiting my inquiry to the circumstances connected with the marriage of Enoch Drebber. The answer was conclusive. It told me that Drebber had already applied for the protection of the law against an old rival in love, named Jefferson Hope, and that this same Hope was at present in Europe. I knew now that I held the clue to the mystery in my hand, and all that remained was to secure the murderer.

+

"I had already determined in my own mind that the man who had walked into the house with Drebber was none other than the man who had driven the cab. The marks in the road showed me that the horse had wandered on in a way which would have been impossible had there been any one in charge of it. Where, then, could the driver be, unless he were inside the house? Again, it is absurd to suppose that any sane man would carry out a deliberate crime under the very eyes as it were, of a third person, who was sure to betray him. Lastly, supposing one man wished to dog another through London, what better means could he adopt than to turn cabdriver. All these considerations led me to the irresistible conclusion that Jefferson Hope was to be found among the jarveys of the Metropolis.

+

"If he had been one, there was no reason to believe that he had ceased to be. On the contrary, from his point of view, any sudden change would be likely to draw attention to himself. He would probably, for a time at least, continue to perform his duties. There was no reason to suppose that he was going under an assumed name. Why should he change his name in a country where no one knew his original one? I therefore organized my Street Arab detective corps, and sent them systematically to every cab proprietor in London until they ferreted out the man that I wanted. How well they succeeded, and how quickly I took advantage of it, are still fresh in your reflection. The murder of Stangerson was an incident which was entirely unexpected, but which could hardly in any case have been prevented. Through it, as you know, I came into possession of the pills, the existence of which I had already surmised. You see, the whole thing is a chain of logical sequence without a break or flaw."

+

"It is wonderful!" I cried. "Your merits should be publicly recognized. You should publish an account of the case. If you won't, I will for you."

+

"You may do what you like, Doctor," he answered. "See here!" he continued, handing a paper over to me, "look at this!"

+

It was the Echo for the day, and the paragraph to which he pointed was devoted to the case in question.

+

"The public," it said, "have lost a sensational treat through the sudden death of the man Hope, who was suspected of the murder of Mr. Enoch Drebber and of Mr. Joseph Stangerson. The details of the case will probably be never known now, though we are informed upon good authority that the crime was the result of an old-standing and romantic feud, in which love and Mormonism bore a part. It seems that both the victims belonged, in their younger days, to the Latter Day Saints, and Hope, the deceased prisoner, hails also from Salt Lake City. If the case has had no other effect, it, at least, brings out in the most striking manner the efficiency of our detective police force, and will serve as a lesson to all foreigners that they will do wisely to settle their feuds at home, and not to carry them on to British soil. It is an open secret that the credit of this smart capture belongs entirely to the well-known Scotland Yard officials, Messrs. Lestrade and Gregson. The man was apprehended, it appears, in the rooms of a certain Mr. Sherlock Holmes, who has himself, as an amateur, shown some talent in the detective line and who, with such instructors, may hope in time to attain to some degree of their skill. It is expected that a testimonial of some sort will be presented to the two officers as a fitting recognition of their services."

+ +

"Didn't I tell you so when we started?" cried Sherlock Holmes with a laugh. "That's the result of all our Study in Scarlet: to get them a testimonial!"

+

"Never mind," I answered; "I have all the facts in my journal, and the public shall know them. In the meantime you must make yourself contented by the consciousness of success, like the Roman miser—

+

 

+

"'Populus me sibilat, at mihi plaudo
+ Ipse domi simul ac nummos contemplar in arca.'"

+
+
+ + diff --git a/books/a-study-in-scarlet/Tail.html b/books/a-study-in-scarlet/Tail.html new file mode 100644 index 0000000..a908dfd --- /dev/null +++ b/books/a-study-in-scarlet/Tail.html @@ -0,0 +1,39 @@ + + + + A Study In Scarlet + + + + + +
+ + + Built with Baker Framework + + +

+ Originally published in 1887
+ Built with Baker Framework +

+
+ + diff --git a/books/a-study-in-scarlet/a-study-in-scarlet.png b/books/a-study-in-scarlet/a-study-in-scarlet.png new file mode 100644 index 0000000..f7480d6 Binary files /dev/null and b/books/a-study-in-scarlet/a-study-in-scarlet.png differ diff --git a/books/a-study-in-scarlet/book.json b/books/a-study-in-scarlet/book.json new file mode 100644 index 0000000..2beec3e --- /dev/null +++ b/books/a-study-in-scarlet/book.json @@ -0,0 +1,39 @@ +{ + "hpub": 1, + "title": "A Study in Scarlet", + "author": ["Arthur Conan Doyle"], + "creator": ["D. Casali", "M. Colombo", "A. Morandi"], + "date": "1887-10-10", + "url": "book://bakerframework.com/newsstand-books/a-study-in-scarlet", + "cover": "a-study-in-scarlet.png", + + "orientation": "both", + "zoomable": false, + + "-baker-background": "#ffffff", + "-baker-vertical-bounce": true, + "-baker-media-autoplay": true, + "-baker-background-image-portrait": "gfx/background-portrait.png", + "-baker-background-image-landscape": "gfx/background-landscape.png", + "-baker-page-numbers-color": "#000000", + + "contents": [ + "Book Cover.html", + "Book Index.html", + "Part1-01.html", + "Part1-02.html", + "Part1-03.html", + "Part1-04.html", + "Part1-05.html", + "Part1-06.html", + "Part1-07.html", + "Part2-01.html", + "Part2-02.html", + "Part2-03.html", + "Part2-04.html", + "Part2-05.html", + "Part2-06.html", + "Part2-07.html", + "Tail.html" + ] +} \ No newline at end of file diff --git a/books/a-study-in-scarlet/css/book.css b/books/a-study-in-scarlet/css/book.css new file mode 100644 index 0000000..2e3165b --- /dev/null +++ b/books/a-study-in-scarlet/css/book.css @@ -0,0 +1,260 @@ +/* + * Baker Ebook Framework - Basic Book + * last update: 2011-10-10 + * + * Copyright (C) 2011 by Davide S. Casali + * + */ + +/**************************************************************************************************** + * General + */ +body { + background: #ffffff url(../gfx/background.jpg); + font-family: "BodoniSvtyTwoOSITCTT-Book", Georgia, Helvetica, Arial, sans-serif; + font-size: 16px; + margin: 0; + padding: 0; +} + +a { + color: #b50303; +} + +/**************************************************************************************************** + * Typography + */ +p, ul, ol { + font-size: 18px; + line-height: 40px; +} + +h1, h2, h3, h4, h5, h6 { + font-family: "BodoniSvtyTwoITCTT-Book", Times, serif; + font-weight: normal; + text-align: center; + + border: 0; + margin: 0; + padding: 0; +} + +h1 { + border-bottom: 1px solid #cf0000; + color: #cf0000; + font-size: 70px; + + height: 264px; + position: relative; +} +h1 div { + width: 100%; + position: absolute; + bottom: 0; + margin: 0 0 20px; +} + +h2 { + font-size: 50px; + letter-spacing: -2px; + margin: 8px 0 2px; + padding: 5px 0 2px; +} + +ul, ol { + line-height: 1.8em; +} + +img.right { + float: right; + margin: 8px -100px 8px 25px; + padding: 15px 0 15px 0; +} + +/**************************************************************************************************** + * Pages + */ +#page { + background: #ffffff url(../gfx/background.jpg); + min-height: 1024px; + overflow: hidden; + margin: 0 auto; +} + +/****** Blood */ +#blood { + background: transparent url(../gfx/blood.png) no-repeat; + width: 17px; + height: 55px; + + position: absolute; + top: 0; + right: 20px; + + -webkit-tap-highlight-color: rgba(0,0,0,0); +} + +.blood-drop { + background: transparent url(../gfx/blood-drop.png) no-repeat; + display: block; + width: 10px; + height: 26px; +} + + +/**************************************************************************************************** + * Book Cover + */ +#page.cover img.central { + display: block; + margin: 70px auto 50px; +} + +#page.cover .footer { + text-align: center; +} + +#page.cover .footer p { + font-size: 14px; + padding: 15px 0 5px; +} + + +/**************************************************************************************************** + * Book Index + */ +#page.index .menu { + margin: 30px auto; + max-width: 670px; + width: 90%; +} + +#page.index .menu small { + display: block; + text-align: center; + width: 400px; + margin: 0 auto; + font-size: 14px; +} + +#page.index .menu ol { + list-style: none; + padding: 0; +} + +#page.index .menu li { + position: relative; + background: transparent url(../gfx/dots-menu.png) repeat-x -1px; +} + +#page.index .menu li a .count { + background: #ffffff url(../gfx/background.jpg); + padding-left: 3px; + position: absolute; + right: 0; +} + +#page.index .menu li a { + background: #ffffff url(../gfx/background.jpg); + color: #000000; + text-decoration: none; + + -webkit-transition: color 0.4s ease; + -moz-transition: color 0.4s ease; + -o-transition: color 0.4s ease; + transition: color 0.4s ease; +} +#page.index .menu li a:hover { + color: #b50303; +} + +/**************************************************************************************************** + * Chapter + */ +#page.chapter h1 { + font-size: 42px; +} + +#page.chapter #content { + min-height: 1024px; + max-width: 530px; + width: 90%; + + margin: 120px auto; +} + +#page.chapter p, +#page.chapter ol, +#page.chapter ul { + font-size: 22px; + text-align: justify; + text-indent: 1em; + line-height: 1.8em; +} + + +/**************************************************************************************************** + * Media queries + */ +@media only screen and (max-device-width : 768px) and (orientation : portrait) { +} + +@media only screen and (max-device-width : 1024px) and (orientation : landscape) { + + #page.cover img.central { + display: none; + } + + #page { + min-height: 768px; + } + + #page.cover .footer { + margin-top: 240px; + } + +} + +@media all and (max-width: 768px) { + + img.right { + display: block; + float: none; + margin: 8px auto; + padding: 15px 0 15px 0; + } + + #page.chapter h1 { + font-size: 36px; + } + + #page.chapter p, + #page.chapter ol, + #page.chapter ul { + font-size: 18px; + text-align: justify; + text-indent: 1em; + line-height: 1.8em; + } + + #page.chapter #content { + margin-top: 40px; + } + + h1 { + border-bottom: 1px solid #cf0000; + color: #cf0000; + font-size: 52px; + + height: 120px; + position: relative; + } + + h2 { + font-size: 36px; + letter-spacing: -2px; + margin: 8px 0 2px; + padding: 5px 0 2px; + } + +} diff --git a/books/a-study-in-scarlet/css/index.css b/books/a-study-in-scarlet/css/index.css new file mode 100644 index 0000000..2bb91b9 --- /dev/null +++ b/books/a-study-in-scarlet/css/index.css @@ -0,0 +1,129 @@ +/* + * Baker Ebook Framework - Basic Book + * last update: 2011-10-10 + * + * Copyright (C) 2011 by Davide S. Casali + * + */ + + + +/**************************************************************************************************** + * General + */ +body { + font-family: Helvetica, Arial, sans-serif; + font-size: 16px; + margin: 0; + padding: 25px; + background: rgba(240, 240, 240, 0.9); + + width: 2800px; +} + +a { + color: #b50303; +} + + +/**************************************************************************************************** + * Typography + */ +p, ul, ol { + font-size: 18px; + line-height: 40px; +} + +h1, h2, h3, h4, h5, h6 { + font-family: "BodoniSvtyTwoITCTT-Book", Times, serif; + font-weight: normal; + text-align: center; + + border: 0; + margin: 0; + padding: 0; +} + +/**************************************************************************************************** + * Index + */ +#index .navigation h6, +#index .navigation ol li a small { + display: none; +} + +#index .navigation ol { + display: inline; + padding: 0; + margin-right: 60px; +} + +#index .navigation h5 { + background: #cf0000; + color: #ffffff; + display: inline-block; + font-size: 20px; + + padding: 5px; + width: 55px; + height: 140px; +} + +#index .navigation li { + background: #ffffff url(../images/1-01-a.gif) -30px 0; + display: inline-block; + + margin-left: 30px; + overflow: hidden; + position: relative; + + padding: 0; + width: 150px; + height: 150px; + + vertical-align: top; +} + +#index .navigation li a { + font-family: "BodoniSvtyTwoITCTT-Book", Times, serif; + font-weight: normal; + text-align: center; + text-decoration: none; + display: block; + width: 150px; + height: 150px; +} + +#index .navigation li a h7 { + background: rgba(255, 255, 255, 0.9); + font-size: 30px; + padding-top: 8px; + + position: absolute; + bottom: 0; + + width: 100%; + height: 47px; +} + + +/****** Images */ +#index .navigation li.p1-01 { background: #ffffff url(../images/1-01-b.gif) -30px -72px; } +#index .navigation li.p1-02 { background: #ffffff url(../images/1-02-a.gif) -30px -30px; } +#index .navigation li.p1-03 { background: #ffffff url(../images/1-03-a.gif) -60px -180px; } +#index .navigation li.p1-04 { background: #ffffff url(../images/1-04-a.gif) -20px -80px; } +#index .navigation li.p1-05 { background: #ffffff url(../images/1-05-a.gif) -30px -60px; } +#index .navigation li.p1-06 { background: #ffffff url(../images/1-06-a.gif) -60px -45px; } +#index .navigation li.p1-07 { background: #ffffff url(../images/1-07-b.gif) -75px -40px; } + +#index .navigation li.p2-01 { background: #ffffff url(../images/2-01-a.gif) -30px 0; } +#index .navigation li.p2-02 { background: #ffffff url(../images/2-02-b.gif) -30px -80px; } +#index .navigation li.p2-03 { background: #ffffff url(../images/2-03-a.gif) -10px -60px; } +#index .navigation li.p2-04 { background: #ffffff url(../images/2-04-b.gif) -40px -130px; } +#index .navigation li.p2-05 { background: #ffffff url(../images/2-05-a.gif) -60px -80px; } +#index .navigation li.p2-06 { background: #ffffff url(../images/2-06-a.gif) -70px -60px; } +#index .navigation li.p2-07 { background: #ffffff url(../images/2-07-a.gif) -60px -80px; } + + + + diff --git a/books/a-study-in-scarlet/gfx/background-landscape.png b/books/a-study-in-scarlet/gfx/background-landscape.png new file mode 100644 index 0000000..4417eca Binary files /dev/null and b/books/a-study-in-scarlet/gfx/background-landscape.png differ diff --git a/books/a-study-in-scarlet/gfx/background-portrait.png b/books/a-study-in-scarlet/gfx/background-portrait.png new file mode 100644 index 0000000..032e0d8 Binary files /dev/null and b/books/a-study-in-scarlet/gfx/background-portrait.png differ diff --git a/books/a-study-in-scarlet/gfx/background.jpg b/books/a-study-in-scarlet/gfx/background.jpg new file mode 100644 index 0000000..a757112 Binary files /dev/null and b/books/a-study-in-scarlet/gfx/background.jpg differ diff --git a/books/a-study-in-scarlet/gfx/baker-badge.png b/books/a-study-in-scarlet/gfx/baker-badge.png new file mode 100644 index 0000000..92cc2d4 Binary files /dev/null and b/books/a-study-in-scarlet/gfx/baker-badge.png differ diff --git a/books/a-study-in-scarlet/gfx/baker-framework-badge.png b/books/a-study-in-scarlet/gfx/baker-framework-badge.png new file mode 100644 index 0000000..13be312 Binary files /dev/null and b/books/a-study-in-scarlet/gfx/baker-framework-badge.png differ diff --git a/books/a-study-in-scarlet/gfx/blood-drop.png b/books/a-study-in-scarlet/gfx/blood-drop.png new file mode 100644 index 0000000..aeb6a1a Binary files /dev/null and b/books/a-study-in-scarlet/gfx/blood-drop.png differ diff --git a/books/a-study-in-scarlet/gfx/blood.png b/books/a-study-in-scarlet/gfx/blood.png new file mode 100644 index 0000000..b60454a Binary files /dev/null and b/books/a-study-in-scarlet/gfx/blood.png differ diff --git a/books/a-study-in-scarlet/gfx/cover-image.png b/books/a-study-in-scarlet/gfx/cover-image.png new file mode 100644 index 0000000..60e9860 Binary files /dev/null and b/books/a-study-in-scarlet/gfx/cover-image.png differ diff --git a/books/a-study-in-scarlet/gfx/dots-menu.png b/books/a-study-in-scarlet/gfx/dots-menu.png new file mode 100644 index 0000000..21f1704 Binary files /dev/null and b/books/a-study-in-scarlet/gfx/dots-menu.png differ diff --git a/books/a-study-in-scarlet/images/1-01-a.gif b/books/a-study-in-scarlet/images/1-01-a.gif new file mode 100644 index 0000000..dddf6c1 Binary files /dev/null and b/books/a-study-in-scarlet/images/1-01-a.gif differ diff --git a/books/a-study-in-scarlet/images/1-01-b.gif b/books/a-study-in-scarlet/images/1-01-b.gif new file mode 100644 index 0000000..0273c71 Binary files /dev/null and b/books/a-study-in-scarlet/images/1-01-b.gif differ diff --git a/books/a-study-in-scarlet/images/1-02-a.gif b/books/a-study-in-scarlet/images/1-02-a.gif new file mode 100644 index 0000000..8b66ff6 Binary files /dev/null and b/books/a-study-in-scarlet/images/1-02-a.gif differ diff --git a/books/a-study-in-scarlet/images/1-02-b.gif b/books/a-study-in-scarlet/images/1-02-b.gif new file mode 100644 index 0000000..310727f Binary files /dev/null and b/books/a-study-in-scarlet/images/1-02-b.gif differ diff --git a/books/a-study-in-scarlet/images/1-03-a.gif b/books/a-study-in-scarlet/images/1-03-a.gif new file mode 100644 index 0000000..73dc24c Binary files /dev/null and b/books/a-study-in-scarlet/images/1-03-a.gif differ diff --git a/books/a-study-in-scarlet/images/1-03-b.gif b/books/a-study-in-scarlet/images/1-03-b.gif new file mode 100644 index 0000000..36552d1 Binary files /dev/null and b/books/a-study-in-scarlet/images/1-03-b.gif differ diff --git a/books/a-study-in-scarlet/images/1-04-a.gif b/books/a-study-in-scarlet/images/1-04-a.gif new file mode 100644 index 0000000..bd37b7a Binary files /dev/null and b/books/a-study-in-scarlet/images/1-04-a.gif differ diff --git a/books/a-study-in-scarlet/images/1-04-b.gif b/books/a-study-in-scarlet/images/1-04-b.gif new file mode 100644 index 0000000..bc7d70d Binary files /dev/null and b/books/a-study-in-scarlet/images/1-04-b.gif differ diff --git a/books/a-study-in-scarlet/images/1-05-a.gif b/books/a-study-in-scarlet/images/1-05-a.gif new file mode 100644 index 0000000..cce2c9b Binary files /dev/null and b/books/a-study-in-scarlet/images/1-05-a.gif differ diff --git a/books/a-study-in-scarlet/images/1-06-a.gif b/books/a-study-in-scarlet/images/1-06-a.gif new file mode 100644 index 0000000..965ed93 Binary files /dev/null and b/books/a-study-in-scarlet/images/1-06-a.gif differ diff --git a/books/a-study-in-scarlet/images/1-06-b.gif b/books/a-study-in-scarlet/images/1-06-b.gif new file mode 100644 index 0000000..cc06a76 Binary files /dev/null and b/books/a-study-in-scarlet/images/1-06-b.gif differ diff --git a/books/a-study-in-scarlet/images/1-07-a.gif b/books/a-study-in-scarlet/images/1-07-a.gif new file mode 100644 index 0000000..a07bc47 Binary files /dev/null and b/books/a-study-in-scarlet/images/1-07-a.gif differ diff --git a/books/a-study-in-scarlet/images/1-07-b.gif b/books/a-study-in-scarlet/images/1-07-b.gif new file mode 100644 index 0000000..d1bb59b Binary files /dev/null and b/books/a-study-in-scarlet/images/1-07-b.gif differ diff --git a/books/a-study-in-scarlet/images/2-01-a.gif b/books/a-study-in-scarlet/images/2-01-a.gif new file mode 100644 index 0000000..f8e3e19 Binary files /dev/null and b/books/a-study-in-scarlet/images/2-01-a.gif differ diff --git a/books/a-study-in-scarlet/images/2-01-b.gif b/books/a-study-in-scarlet/images/2-01-b.gif new file mode 100644 index 0000000..6380efd Binary files /dev/null and b/books/a-study-in-scarlet/images/2-01-b.gif differ diff --git a/books/a-study-in-scarlet/images/2-02-a.gif b/books/a-study-in-scarlet/images/2-02-a.gif new file mode 100644 index 0000000..7c17fbd Binary files /dev/null and b/books/a-study-in-scarlet/images/2-02-a.gif differ diff --git a/books/a-study-in-scarlet/images/2-02-b.gif b/books/a-study-in-scarlet/images/2-02-b.gif new file mode 100644 index 0000000..7031d4e Binary files /dev/null and b/books/a-study-in-scarlet/images/2-02-b.gif differ diff --git a/books/a-study-in-scarlet/images/2-03-a.gif b/books/a-study-in-scarlet/images/2-03-a.gif new file mode 100644 index 0000000..9073e2b Binary files /dev/null and b/books/a-study-in-scarlet/images/2-03-a.gif differ diff --git a/books/a-study-in-scarlet/images/2-04-a.gif b/books/a-study-in-scarlet/images/2-04-a.gif new file mode 100644 index 0000000..f424547 Binary files /dev/null and b/books/a-study-in-scarlet/images/2-04-a.gif differ diff --git a/books/a-study-in-scarlet/images/2-04-b.gif b/books/a-study-in-scarlet/images/2-04-b.gif new file mode 100644 index 0000000..76c72c8 Binary files /dev/null and b/books/a-study-in-scarlet/images/2-04-b.gif differ diff --git a/books/a-study-in-scarlet/images/2-05-a.gif b/books/a-study-in-scarlet/images/2-05-a.gif new file mode 100644 index 0000000..adf1227 Binary files /dev/null and b/books/a-study-in-scarlet/images/2-05-a.gif differ diff --git a/books/a-study-in-scarlet/images/2-06-a.gif b/books/a-study-in-scarlet/images/2-06-a.gif new file mode 100644 index 0000000..c0b12f7 Binary files /dev/null and b/books/a-study-in-scarlet/images/2-06-a.gif differ diff --git a/books/a-study-in-scarlet/images/2-06-b.gif b/books/a-study-in-scarlet/images/2-06-b.gif new file mode 100644 index 0000000..8e561e8 Binary files /dev/null and b/books/a-study-in-scarlet/images/2-06-b.gif differ diff --git a/books/a-study-in-scarlet/images/2-07-a.gif b/books/a-study-in-scarlet/images/2-07-a.gif new file mode 100644 index 0000000..6377a24 Binary files /dev/null and b/books/a-study-in-scarlet/images/2-07-a.gif differ diff --git a/books/a-study-in-scarlet/index.html b/books/a-study-in-scarlet/index.html new file mode 100644 index 0000000..ca97181 --- /dev/null +++ b/books/a-study-in-scarlet/index.html @@ -0,0 +1,37 @@ + + + + Index + + + + + + + \ No newline at end of file diff --git a/books/a-study-in-scarlet/js/Hyphenator.js b/books/a-study-in-scarlet/js/Hyphenator.js new file mode 100644 index 0000000..6c8a8c5 --- /dev/null +++ b/books/a-study-in-scarlet/js/Hyphenator.js @@ -0,0 +1,2163 @@ +/** @license Hyphenator 3.3.0 - client side hyphenation for webbrowsers + * Copyright (C) 2011 Mathias Nater, Zürich (mathias at mnn dot ch) + * Project and Source hosted on http://code.google.com/p/hyphenator/ + * + * This JavaScript code is free software: you can redistribute + * it and/or modify it under the terms of the GNU Lesser + * General Public License (GNU LGPL) as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) + * any later version. The code is distributed WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU GPL for more details. + * + * As additional permission under GNU GPL version 3 section 7, you + * may distribute non-source (e.g., minimized or compacted) forms of + * that code without the copy of the GNU GPL normally required by + * section 4, provided you include this license notice and a URL + * through which recipients can access the Corresponding Source. + */ + +/* + * Comments are jsdoctoolkit formatted. See http://code.google.com/p/jsdoc-toolkit/ + */ + +/* The following comment is for JSLint: */ +/*global window, ActiveXObject, unescape */ +/*jslint white: true, browser: true, onevar: true, undef: true, nomen: true, eqeqeq: true, regexp: true, sub: true, newcap: true, immed: true, evil: true, eqeqeq: false */ + + +/** + * @constructor + * @description Provides all functionality to do hyphenation, except the patterns that are loaded + * externally. + * @author Mathias Nater, mathias@mnn.ch + * @version 3.3.0 + * @namespace Holds all methods and properties + * @example + * <script src = "Hyphenator.js" type = "text/javascript"></script> + * <script type = "text/javascript"> + *   Hyphenator.run(); + * </script> + */ +var Hyphenator = (function (window) { + + var + /** + * @name Hyphenator-supportedLang + * @description + * A key-value object that stores supported languages. + * The key is the bcp47 code of the language and the value + * is the (abbreviated) filename of the pattern file. + * @type {Object.} + * @private + * @example + * Check if language lang is supported: + * if (supportedLang.hasOwnProperty(lang)) + */ + supportedLang = { + 'be': 'be.js', + 'ca': 'ca.js', + 'cs': 'cs.js', + 'da': 'da.js', + 'bn': 'bn.js', + 'de': 'de.js', + 'el': 'el-monoton.js', + 'el-monoton': 'el-monoton.js', + 'el-polyton': 'el-polyton.js', + 'en': 'en-us.js', + 'en-gb': 'en-gb.js', + 'en-us': 'en-us.js', + 'es': 'es.js', + 'fi': 'fi.js', + 'fr': 'fr.js', + 'grc': 'grc.js', + 'gu': 'gu.js', + 'hi': 'hi.js', + 'hu': 'hu.js', + 'hy': 'hy.js', + 'it': 'it.js', + 'kn': 'kn.js', + 'la': 'la.js', + 'lt': 'lt.js', + 'lv': 'lv.js', + 'ml': 'ml.js', + 'no': 'no-nb.js', + 'no-nb': 'no-nb.js', + 'nl': 'nl.js', + 'or': 'or.js', + 'pa': 'pa.js', + 'pl': 'pl.js', + 'pt': 'pt.js', + 'ru': 'ru.js', + 'sl': 'sl.js', + 'sv': 'sv.js', + 'ta': 'ta.js', + 'te': 'te.js', + 'tr': 'tr.js', + 'uk': 'uk.js' + }, + + /** + * @name Hyphenator-languageHint + * @description + * An automatically generated string to be displayed in a prompt if the language can't be guessed. + * The string is generated using the supportedLang-object. + * @see Hyphenator-supportedLang + * @type {string} + * @private + * @see Hyphenator-autoSetMainLanguage + */ + + languageHint = (function () { + var k, r = ''; + for (k in supportedLang) { + if (supportedLang.hasOwnProperty(k)) { + r += k + ', '; + } + } + r = r.substring(0, r.length - 2); + return r; + }()), + + /** + * @name Hyphenator-prompterStrings + * @description + * A key-value object holding the strings to be displayed if the language can't be guessed + * If you add hyphenation patterns change this string. + * @type {Object.} + * @private + * @see Hyphenator-autoSetMainLanguage + */ + prompterStrings = { + 'be': 'Мова гэтага сайта не можа быць вызначаны аўтаматычна. Калі ласка пакажыце мову:', + 'cs': 'Jazyk této internetové stránky nebyl automaticky rozpoznán. Určete prosím její jazyk:', + 'da': 'Denne websides sprog kunne ikke bestemmes. Angiv venligst sprog:', + 'de': 'Die Sprache dieser Webseite konnte nicht automatisch bestimmt werden. Bitte Sprache angeben:', + 'en': 'The language of this website could not be determined automatically. Please indicate the main language:', + 'es': 'El idioma del sitio no pudo determinarse autom%E1ticamente. Por favor, indique el idioma principal:', + 'fi': 'Sivun kielt%E4 ei tunnistettu automaattisesti. M%E4%E4rit%E4 sivun p%E4%E4kieli:', + 'fr': 'La langue de ce site n%u2019a pas pu %EAtre d%E9termin%E9e automatiquement. Veuillez indiquer une langue, s.v.p.%A0:', + 'hu': 'A weboldal nyelvét nem sikerült automatikusan megállapítani. Kérem adja meg a nyelvet:', + 'hy': 'Չհաջողվեց հայտնաբերել այս կայքի լեզուն։ Խնդրում ենք նշեք հիմնական լեզուն՝', + 'it': 'Lingua del sito sconosciuta. Indicare una lingua, per favore:', + 'kn': 'ಜಾಲ ತಾಣದ ಭಾಷೆಯನ್ನು ನಿರ್ಧರಿಸಲು ಸಾಧ್ಯವಾಗುತ್ತಿಲ್ಲ. ದಯವಿಟ್ಟು ಮುಖ್ಯ ಭಾಷೆಯನ್ನು ಸೂಚಿಸಿ:', + 'lt': 'Nepavyko automatiškai nustatyti šios svetainės kalbos. Prašome įvesti kalbą:', + 'lv': 'Šīs lapas valodu nevarēja noteikt automātiski. Lūdzu norādiet pamata valodu:', + 'ml': 'ഈ വെ%u0D2C%u0D4D%u200Cസൈറ്റിന്റെ ഭാഷ കണ്ടുപിടിയ്ക്കാ%u0D28%u0D4D%u200D കഴിഞ്ഞില്ല. ഭാഷ ഏതാണെന്നു തിരഞ്ഞെടുക്കുക:', + 'nl': 'De taal van deze website kan niet automatisch worden bepaald. Geef de hoofdtaal op:', + 'no': 'Nettstedets språk kunne ikke finnes automatisk. Vennligst oppgi språk:', + 'pt': 'A língua deste site não pôde ser determinada automaticamente. Por favor indique a língua principal:', + 'ru': 'Язык этого сайта не может быть определен автоматически. Пожалуйста укажите язык:', + 'sl': 'Jezika te spletne strani ni bilo mogoče samodejno določiti. Prosim navedite jezik:', + 'sv': 'Spr%E5ket p%E5 den h%E4r webbplatsen kunde inte avg%F6ras automatiskt. V%E4nligen ange:', + 'tr': 'Bu web sitesinin dili otomatik olarak tespit edilememiştir. Lütfen dökümanın dilini seçiniz%A0:', + 'uk': 'Мова цього веб-сайту не може бути визначена автоматично. Будь ласка, вкажіть головну мову:' + }, + + /** + * @name Hyphenator-basePath + * @description + * A string storing the basepath from where Hyphenator.js was loaded. + * This is used to load the patternfiles. + * The basepath is determined dynamically by searching all script-tags for Hyphenator.js + * If the path cannot be determined http://hyphenator.googlecode.com/svn/trunk/ is used as fallback. + * @type {string} + * @private + * @see Hyphenator-loadPatterns + */ + basePath = (function () { + var s = document.getElementsByTagName('script'), i = 0, p, src, t; + while (!!(t = s[i++])) { + if (!t.src) { + continue; + } + src = t.src; + p = src.indexOf('Hyphenator.js'); + if (p !== -1) { + return src.substring(0, p); + } + } + return 'http://hyphenator.googlecode.com/svn/trunk/'; + }()), + + /** + * @name Hyphenator-isLocal + * @description + * isLocal is true, if Hyphenator is loaded from the same domain, as the webpage, but false, if + * it's loaded from an external source (i.e. directly from google.code) + */ + isLocal = (function () { + var re = false; + if (window.location.href.indexOf(basePath) !== -1) { + re = true; + } + return re; + }()), + + /** + * @name Hyphenator-documentLoaded + * @description + * documentLoaded is true, when the DOM has been loaded. This is set by runOnContentLoaded + */ + documentLoaded = false, + documentCount = 0, + + /** + * @name Hyphenator-persistentConfig + * @description + * if persistentConfig is set to true (defaults to false), config options and the state of the + * toggleBox are stored in DOM-storage (according to the storage-setting). So they haven't to be + * set for each page. + */ + persistentConfig = false, + + /** + * @name Hyphenator-contextWindow + * @description + * contextWindow stores the window for the document to be hyphenated. + * If there are frames this will change. + * So use contextWindow instead of window! + */ + contextWindow = window, + + /** + * @name Hyphenator-doFrames + * @description + * switch to control if frames/iframes should be hyphenated, too + * defaults to false (frames are a bag of hurt!) + */ + doFrames = false, + + /** + * @name Hyphenator-dontHyphenate + * @description + * A key-value object containing all html-tags whose content should not be hyphenated + * @type {Object.} + * @private + * @see Hyphenator-hyphenateElement + */ + dontHyphenate = {'script': true, 'code': true, 'pre': true, 'img': true, 'br': true, 'samp': true, 'kbd': true, 'var': true, 'abbr': true, 'acronym': true, 'sub': true, 'sup': true, 'button': true, 'option': true, 'label': true, 'textarea': true, 'input': true}, + + /** + * @name Hyphenator-enableCache + * @description + * A variable to set if caching is enabled or not + * @type boolean + * @default true + * @private + * @see Hyphenator.config + * @see hyphenateWord + */ + enableCache = true, + + /** + * @name Hyphenator-storageType + * @description + * A variable to define what html5-DOM-Storage-Method is used ('none', 'local' or 'session') + * @type {string} + * @default 'none' + * @private + * @see Hyphenator.config + */ + storageType = 'local', + + /** + * @name Hyphenator-storage + * @description + * An alias to the storage-Method defined in storageType. + * Set by Hyphenator.run() + * @type {Object|undefined} + * @default null + * @private + * @see Hyphenator.run + */ + storage, + + /** + * @name Hyphenator-enableReducedPatternSet + * @description + * A variable to set if storing the used patterns is set + * @type boolean + * @default false + * @private + * @see Hyphenator.config + * @see hyphenateWord + * @see Hyphenator.getRedPatternSet + */ + enableReducedPatternSet = false, + + /** + * @name Hyphenator-enableRemoteLoading + * @description + * A variable to set if pattern files should be loaded remotely or not + * @type boolean + * @default true + * @private + * @see Hyphenator.config + * @see Hyphenator-loadPatterns + */ + enableRemoteLoading = true, + + /** + * @name Hyphenator-displayToggleBox + * @description + * A variable to set if the togglebox should be displayed or not + * @type boolean + * @default false + * @private + * @see Hyphenator.config + * @see Hyphenator-toggleBox + */ + displayToggleBox = false, + + /** + * @name Hyphenator-hyphenateClass + * @description + * A string containing the css-class-name for the hyphenate class + * @type {string} + * @default 'hyphenate' + * @private + * @example + * <p class = "hyphenate">Text</p> + * @see Hyphenator.config + */ + hyphenateClass = 'hyphenate', + + /** + * @name Hyphenator-dontHyphenateClass + * @description + * A string containing the css-class-name for elements that should not be hyphenated + * @type {string} + * @default 'donthyphenate' + * @private + * @example + * <p class = "donthyphenate">Text</p> + * @see Hyphenator.config + */ + dontHyphenateClass = 'donthyphenate', + + /** + * @name Hyphenator-min + * @description + * A number wich indicates the minimal length of words to hyphenate. + * @type {number} + * @default 6 + * @private + * @see Hyphenator.config + */ + min = 6, + + /** + * @name Hyphenator-orphanControl + * @description + * Control how the last words of a line are handled: + * level 1 (default): last word is hyphenated + * level 2: last word is not hyphenated + * level 3: last word is not hyphenated and last space is non breaking + * @type {number} + * @default 1 + * @private + */ + orphanControl = 1, + + /** + * @name Hyphenator-isBookmarklet + * @description + * Indicates if Hyphanetor runs as bookmarklet or not. + * @type boolean + * @default false + * @private + */ + isBookmarklet = (function () { + var loc = null, re = false, jsArray = document.getElementsByTagName('script'), i, l; + for (i = 0, l = jsArray.length; i < l; i++) { + if (!!jsArray[i].getAttribute('src')) { + loc = jsArray[i].getAttribute('src'); + } + if (!loc) { + continue; + } else if (loc.indexOf('Hyphenator.js?bm=true') !== -1) { + re = true; + } + } + return re; + }()), + + /** + * @name Hyphenator-mainLanguage + * @description + * The general language of the document. In contrast to {@link Hyphenator-defaultLanguage}, + * mainLanguage is defined by the client (i.e. by the html or by a prompt). + * @type {string|null} + * @private + * @see Hyphenator-autoSetMainLanguage + */ + mainLanguage = null, + + /** + * @name Hyphenator-defaultLanguage + * @description + * The language defined by the developper. This language setting is defined by a config option. + * It is overwritten by any html-lang-attribute and only taken in count, when no such attribute can + * be found (i.e. just before the prompt). + * @type {string|null} + * @private + * @see Hyphenator-autoSetMainLanguage + */ + defaultLanguage = '', + + /** + * @name Hyphenator-elements + * @description + * An array holding all elements that have to be hyphenated. This var is filled by + * {@link Hyphenator-gatherDocumentInfos} + * @type {Array} + * @private + */ + elements = [], + + /** + * @name Hyphenator-exceptions + * @description + * An object containing exceptions as comma separated strings for each language. + * When the language-objects are loaded, their exceptions are processed, copied here and then deleted. + * @see Hyphenator-prepareLanguagesObj + * @type {Object} + * @private + */ + exceptions = {}, + + countObjProps = function (obj) { + var k, l = 0; + for (k in obj) { + if (obj.hasOwnProperty(k)) { + l++; + } + } + return l; + }, + /** + * @name Hyphenator-docLanguages + * @description + * An object holding all languages used in the document. This is filled by + * {@link Hyphenator-gatherDocumentInfos} + * @type {Object} + * @private + */ + docLanguages = {}, + + + /** + * @name Hyphenator-state + * @description + * A number that inidcates the current state of the script + * 0: not initialized + * 1: loading patterns + * 2: ready + * 3: hyphenation done + * 4: hyphenation removed + * @type {number} + * @private + */ + state = 0, + + /** + * @name Hyphenator-url + * @description + * A string containing a RegularExpression to match URL's + * @type {string} + * @private + */ + url = '(\\w*:\/\/)?((\\w*:)?(\\w*)@)?((([\\d]{1,3}\\.){3}([\\d]{1,3}))|((www\\.|[a-zA-Z]\\.)?[a-zA-Z0-9\\-\\.]+\\.([a-z]{2,4})))(:\\d*)?(\/[\\w#!:\\.?\\+=&%@!\\-]*)*', + // protocoll usr pwd ip or host tld port path + /** + * @name Hyphenator-mail + * @description + * A string containing a RegularExpression to match mail-adresses + * @type {string} + * @private + */ + mail = '[\\w-\\.]+@[\\w\\.]+', + + /** + * @name Hyphenator-urlRE + * @description + * A RegularExpressions-Object for url- and mail adress matching + * @type {RegExp} + * @private + */ + urlOrMailRE = new RegExp('(' + url + ')|(' + mail + ')', 'i'), + + /** + * @name Hyphenator-zeroWidthSpace + * @description + * A string that holds a char. + * Depending on the browser, this is the zero with space or an empty string. + * zeroWidthSpace is used to break URLs + * @type {string} + * @private + */ + zeroWidthSpace = (function () { + var zws, ua = navigator.userAgent.toLowerCase(); + zws = String.fromCharCode(8203); //Unicode zero width space + if (ua.indexOf('msie 6') !== -1) { + zws = ''; //IE6 doesn't support zws + } + if (ua.indexOf('opera') !== -1 && ua.indexOf('version/10.00') !== -1) { + zws = ''; //opera 10 on XP doesn't support zws + } + return zws; + }()), + + /** + * @name Hyphenator-createElem + * @description + * A function alias to document.createElementNS or document.createElement + * @type {function(string, Object)} + * @private + */ + createElem = function (tagname, context) { + context = context || contextWindow; + if (document.createElementNS) { + return context.document.createElementNS('http://www.w3.org/1999/xhtml', tagname); + } else if (document.createElement) { + return context.document.createElement(tagname); + } + }, + + /** + * @name Hyphenator-onHyphenationDone + * @description + * A method to be called, when the last element has been hyphenated or the hyphenation has been + * removed from the last element. + * @see Hyphenator.config + * @type {function()} + * @private + */ + onHyphenationDone = function () {}, + + /** + * @name Hyphenator-onError + * @description + * A function that can be called upon an error. + * @see Hyphenator.config + * @type {function(Object)} + * @private + */ + onError = function (e) { + window.alert("Hyphenator.js says:\n\nAn Error ocurred:\n" + e.message); + }, + + /** + * @name Hyphenator-selectorFunction + * @description + * A function that has to return a HTMLNodeList of Elements to be hyphenated. + * By default it uses the classname ('hyphenate') to select the elements. + * @see Hyphenator.config + * @type {function()} + * @private + */ + selectorFunction = function () { + var tmp, el = [], i, l; + if (document.getElementsByClassName) { + el = contextWindow.document.getElementsByClassName(hyphenateClass); + } else { + tmp = contextWindow.document.getElementsByTagName('*'); + l = tmp.length; + for (i = 0; i < l; i++) + { + if (tmp[i].className.indexOf(hyphenateClass) !== -1 && tmp[i].className.indexOf(dontHyphenateClass) === -1) { + el.push(tmp[i]); + } + } + } + return el; + }, + + /** + * @name Hyphenator-intermediateState + * @description + * The value of style.visibility of the text while it is hyphenated. + * @see Hyphenator.config + * @type {string} + * @private + */ + intermediateState = 'hidden', + + /** + * @name Hyphenator-hyphen + * @description + * A string containing the character for in-word-hyphenation + * @type {string} + * @default the soft hyphen + * @private + * @see Hyphenator.config + */ + hyphen = String.fromCharCode(173), + + /** + * @name Hyphenator-urlhyphen + * @description + * A string containing the character for url/mail-hyphenation + * @type {string} + * @default the zero width space + * @private + * @see Hyphenator.config + * @see Hyphenator-zeroWidthSpace + */ + urlhyphen = zeroWidthSpace, + + /** + * @name Hyphenator-safeCopy + * @description + * Defines wether work-around for copy issues is active or not + * Not supported by Opera (no onCopy handler) + * @type boolean + * @default true + * @private + * @see Hyphenator.config + * @see Hyphenator-registerOnCopy + */ + safeCopy = true, + + /** + * @name Hyphenator-Expando + * @description + * This custom object stores data for elements: storing data directly in elements + * (DomElement.customData = foobar;) isn't a good idea. It would lead to conflicts + * in form elements, when the form has a child with name="foobar". Therefore, this + * solution follows the approach of jQuery: the data is stored in an object and + * referenced by a unique attribute of the element. The attribute has a name that + * is built by the prefix "HyphenatorExpando_" and a random number, so if the very + * very rare case occurs, that there's already an attribute with the same name, a + * simple reload is enough to make it function. + * @private + */ + Expando = (function () { + var container = {}, + name = "HyphenatorExpando_" + Math.random(), + uuid = 0; + return { + getDataForElem : function (elem) { + return container[elem[name].id]; + }, + setDataForElem : function (elem, data) { + var id; + if (elem[name] && elem[name].id !== '') { + id = elem[name].id; + } else { + id = uuid++; + elem[name] = {'id': id}; //object needed, otherways it is reflected in HTML in IE + } + container[id] = data; + }, + appendDataForElem : function (elem, data) { + var k; + for (k in data) { + if (data.hasOwnProperty(k)) { + container[elem[name].id][k] = data[k]; + } + } + }, + delDataOfElem : function (elem) { + delete container[elem[name]]; + } + }; + }()), + + /* + * runOnContentLoaded is based od jQuery.bindReady() + * see + * jQuery JavaScript Library v1.3.2 + * http://jquery.com/ + * + * Copyright (c) 2009 John Resig + * Dual licensed under the MIT and GPL licenses. + * http://docs.jquery.com/License + * + * Date: 2009-02-19 17:34:21 -0500 (Thu, 19 Feb 2009) + * Revision: 6246 + */ + /** + * @name Hyphenator-runOnContentLoaded + * @description + * A crossbrowser solution for the DOMContentLoaded-Event based on jQuery + * 0) { + for (i = 0; i < fl; i++) { + haveAccess = undefined; + //try catch isn't enough for webkit + try { + //opera throws only on document.toString-access + haveAccess = window.frames[i].document.toString(); + } catch (e) { + haveAccess = undefined; + } + if (!!haveAccess) { + init(window.frames[i]); + } + } + contextWindow = window; + f(); + hyphRunForThis[window.location.href] = true; + } else { + init(window); + } + } + + // Cleanup functions for the document ready method + if (document.addEventListener) { + DOMContentLoaded = function () { + document.removeEventListener("DOMContentLoaded", DOMContentLoaded, false); + if (doFrames && window.frames.length > 0) { + //we are in a frameset, so do nothing but wait for onload to fire + return; + } else { + init(window); + } + }; + + } else if (document.attachEvent) { + DOMContentLoaded = function () { + // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). + if (document.readyState === "complete") { + document.detachEvent("onreadystatechange", DOMContentLoaded); + if (doFrames && window.frames.length > 0) { + //we are in a frameset, so do nothing but wait for onload to fire + return; + } else { + init(window); + } + } + }; + } + + // Mozilla, Opera and webkit nightlies currently support this event + if (document.addEventListener) { + // Use the handy event callback + document.addEventListener("DOMContentLoaded", DOMContentLoaded, false); + + // A fallback to window.onload, that will always work + window.addEventListener("load", doOnLoad, false); + + // If IE event model is used + } else if (document.attachEvent) { + // ensure firing before onload, + // maybe late but safe also for iframes + document.attachEvent("onreadystatechange", DOMContentLoaded); + + // A fallback to window.onload, that will always work + window.attachEvent("onload", doOnLoad); + + // If IE and not a frame + // continually check to see if the document is ready + toplevel = false; + try { + toplevel = window.frameElement === null; + } catch (e) {} + + if (document.documentElement.doScroll && toplevel) { + doScrollCheck(); + } + } + + }, + + + + /** + * @name Hyphenator-getLang + * @description + * Gets the language of an element. If no language is set, it may use the {@link Hyphenator-mainLanguage}. + * @param {Object} el The first parameter is an DOM-Element-Object + * @param {boolean} fallback The second parameter is a boolean to tell if the function should return the {@link Hyphenator-mainLanguage} + * if there's no language found for the element. + * @private + */ + getLang = function (el, fallback) { + if (!!el.getAttribute('lang')) { + return el.getAttribute('lang').toLowerCase(); + } + // The following doesn't work in IE due to a bug when getAttribute('xml:lang') in a table + /*if (!!el.getAttribute('xml:lang')) { + return el.getAttribute('xml:lang').substring(0, 2); + }*/ + //instead, we have to do this (thanks to borgzor): + try { + if (!!el.getAttribute('xml:lang')) { + return el.getAttribute('xml:lang').toLowerCase(); + } + } catch (ex) {} + if (el.tagName !== 'HTML') { + return getLang(el.parentNode, true); + } + if (fallback) { + return mainLanguage; + } + return null; + }, + + /** + * @name Hyphenator-autoSetMainLanguage + * @description + * Retrieves the language of the document from the DOM. + * The function looks in the following places: + *
    + *
  • lang-attribute in the html-tag
  • + *
  • <meta http-equiv = "content-language" content = "xy" />
  • + *
  • <meta name = "DC.Language" content = "xy" />
  • + *
  • <meta name = "language" content = "xy" />
  • + * + * If nothing can be found a prompt using {@link Hyphenator-languageHint} and {@link Hyphenator-prompterStrings} is displayed. + * If the retrieved language is in the object {@link Hyphenator-supportedLang} it is copied to {@link Hyphenator-mainLanguage} + * @private + */ + autoSetMainLanguage = function (w) { + w = w || contextWindow; + var el = w.document.getElementsByTagName('html')[0], + m = w.document.getElementsByTagName('meta'), + i, text, e, ul; + mainLanguage = getLang(el, false); + if (!mainLanguage) { + for (i = 0; i < m.length; i++) { + // + if (!!m[i].getAttribute('http-equiv') && (m[i].getAttribute('http-equiv').toLowerCase() === 'content-language')) { + mainLanguage = m[i].getAttribute('content').toLowerCase(); + } + // + if (!!m[i].getAttribute('name') && (m[i].getAttribute('name').toLowerCase() === 'dc.language')) { + mainLanguage = m[i].getAttribute('content').toLowerCase(); + } + // + if (!!m[i].getAttribute('name') && (m[i].getAttribute('name').toLowerCase() === 'language')) { + mainLanguage = m[i].getAttribute('content').toLowerCase(); + } + } + } + //get lang for frame from enclosing document + if (!mainLanguage && doFrames && contextWindow != window.parent) { + autoSetMainLanguage(window.parent); + } + //fallback to defaultLang if set + if (!mainLanguage && defaultLanguage !== '') { + mainLanguage = defaultLanguage; + } + //ask user for lang + if (!mainLanguage) { + text = ''; + ul = navigator.language ? navigator.language : navigator.userLanguage; + ul = ul.substring(0, 2); + if (prompterStrings.hasOwnProperty(ul)) { + text = prompterStrings[ul]; + } else { + text = prompterStrings.en; + } + text += ' (ISO 639-1)\n\n' + languageHint; + mainLanguage = window.prompt(unescape(text), ul).toLowerCase(); + } + if (!supportedLang.hasOwnProperty(mainLanguage)) { + if (supportedLang.hasOwnProperty(mainLanguage.split('-')[0])) { //try subtag + mainLanguage = mainLanguage.split('-')[0]; + } else { + e = new Error('The language "' + mainLanguage + '" is not yet supported.'); + throw e; + } + } + }, + + /** + * @name Hyphenator-gatherDocumentInfos + * @description + * This method runs through the DOM and executes the process()-function on: + * - every node returned by the {@link Hyphenator-selectorFunction}. + * The process()-function copies the element to the elements-variable, sets its visibility + * to intermediateState, retrieves its language and recursivly descends the DOM-tree until + * the child-Nodes aren't of type 1 + * @private + */ + gatherDocumentInfos = function () { + var elToProcess, tmp, i = 0, + process = function (el, hide, lang) { + var n, i = 0, hyphenatorSettings = {}; + if (hide && intermediateState === 'hidden') { + if (!!el.getAttribute('style')) { + hyphenatorSettings.hasOwnStyle = true; + } else { + hyphenatorSettings.hasOwnStyle = false; + } + hyphenatorSettings.isHidden = true; + el.style.visibility = 'hidden'; + } + if (el.lang && typeof(el.lang) === 'string') { + hyphenatorSettings.language = el.lang.toLowerCase(); //copy attribute-lang to internal lang + } else if (lang) { + hyphenatorSettings.language = lang.toLowerCase(); + } else { + hyphenatorSettings.language = getLang(el, true); + } + lang = hyphenatorSettings.language; + if (supportedLang[lang]) { + docLanguages[lang] = true; + } else { + if (supportedLang.hasOwnProperty(lang.split('-')[0])) { //try subtag + lang = lang.split('-')[0]; + hyphenatorSettings.language = lang; + } else if (!isBookmarklet) { + onError(new Error('Language ' + lang + ' is not yet supported.')); + } + } + Expando.setDataForElem(el, hyphenatorSettings); + + elements.push(el); + while (!!(n = el.childNodes[i++])) { + if (n.nodeType === 1 && !dontHyphenate[n.nodeName.toLowerCase()] && + n.className.indexOf(dontHyphenateClass) === -1 && !(n in elToProcess)) { + process(n, false, lang); + } + } + }; + if (isBookmarklet) { + elToProcess = contextWindow.document.getElementsByTagName('body')[0]; + process(elToProcess, false, mainLanguage); + } else { + elToProcess = selectorFunction(); + while (!!(tmp = elToProcess[i++])) + { + process(tmp, true, ''); + } + } + if (!Hyphenator.languages.hasOwnProperty(mainLanguage)) { + docLanguages[mainLanguage] = true; + } else if (!Hyphenator.languages[mainLanguage].prepared) { + docLanguages[mainLanguage] = true; + } + if (elements.length > 0) { + Expando.appendDataForElem(elements[elements.length - 1], {isLast : true}); + } + }, + + /** + * @name Hyphenator-convertPatterns + * @description + * Converts the patterns from string '_a6' to object '_a':'_a6'. + * The result is stored in the {@link Hyphenator-patterns}-object. + * @private + * @param {string} lang the language whose patterns shall be converted + */ + convertPatterns = function (lang) { + var plen, anfang, ende, pats, pat, key, tmp = {}; + pats = Hyphenator.languages[lang].patterns; + for (plen in pats) { + if (pats.hasOwnProperty(plen)) { + plen = parseInt(plen, 10); + anfang = 0; + ende = plen; + while (!!(pat = pats[plen].substring(anfang, ende))) { + key = pat.replace(/\d/g, ''); + tmp[key] = pat; + anfang = ende; + ende += plen; + } + } + } + Hyphenator.languages[lang].patterns = tmp; + Hyphenator.languages[lang].patternsConverted = true; + }, + + /** + * @name Hyphenator-convertExceptionsToObject + * @description + * Converts a list of comma seprated exceptions to an object: + * 'Fortran,Hy-phen-a-tion' -> {'Fortran':'Fortran','Hyphenation':'Hy-phen-a-tion'} + * @private + * @param {string} exc a comma separated string of exceptions (without spaces) + */ + convertExceptionsToObject = function (exc) { + var w = exc.split(', '), + r = {}, + i, l, key; + for (i = 0, l = w.length; i < l; i++) { + key = w[i].replace(/-/g, ''); + if (!r.hasOwnProperty(key)) { + r[key] = w[i]; + } + } + return r; + }, + + /** + * @name Hyphenator-loadPatterns + * @description + * Adds a <script>-Tag to the DOM to load an externeal .js-file containing patterns and settings for the given language. + * If the given language is not in the {@link Hyphenator-supportedLang}-Object it returns. + * One may ask why we are not using AJAX to load the patterns. The XMLHttpRequest-Object + * has a same-origin-policy. This makes the isBookmarklet-functionality impossible. + * @param {string} lang The language to load the patterns for + * @private + * @see Hyphenator-basePath + */ + loadPatterns = function (lang) { + var url, xhr, head, script; + if (supportedLang[lang] && !Hyphenator.languages[lang]) { + url = basePath + 'patterns/' + supportedLang[lang]; + } else { + return; + } + if (isLocal && !isBookmarklet) { + //check if 'url' is available: + xhr = null; + if (typeof XMLHttpRequest !== 'undefined') { + xhr = new XMLHttpRequest(); + } + if (!xhr) { + try { + xhr = new ActiveXObject("Msxml2.XMLHTTP"); + } catch (e) { + xhr = null; + } + } + if (xhr) { + xhr.open('HEAD', url, false); + xhr.setRequestHeader('Cache-Control', 'no-cache'); + xhr.send(null); + if (xhr.status === 404) { + onError(new Error('Could not load\n' + url)); + delete docLanguages[lang]; + return; + } + } + } + if (createElem) { + head = window.document.getElementsByTagName('head').item(0); + script = createElem('script', window); + script.src = url; + script.type = 'text/javascript'; + head.appendChild(script); + } + }, + + /** + * @name Hyphenator-prepareLanguagesObj + * @description + * Adds a cache to each language and converts the exceptions-list to an object. + * If storage is active the object is stored there. + * @private + * @param {string} lang the language ob the lang-obj + */ + prepareLanguagesObj = function (lang) { + var lo = Hyphenator.languages[lang], wrd; + if (!lo.prepared) { + if (enableCache) { + lo.cache = {}; + //Export + lo['cache'] = lo.cache; + } + if (enableReducedPatternSet) { + lo.redPatSet = {}; + } + //add exceptions from the pattern file to the local 'exceptions'-obj + if (lo.hasOwnProperty('exceptions')) { + Hyphenator.addExceptions(lang, lo.exceptions); + delete lo.exceptions; + } + //copy global exceptions to the language specific exceptions + if (exceptions.hasOwnProperty('global')) { + if (exceptions.hasOwnProperty(lang)) { + exceptions[lang] += ', ' + exceptions.global; + } else { + exceptions[lang] = exceptions.global; + } + } + //move exceptions from the the local 'exceptions'-obj to the 'language'-object + if (exceptions.hasOwnProperty(lang)) { + lo.exceptions = convertExceptionsToObject(exceptions[lang]); + delete exceptions[lang]; + } else { + lo.exceptions = {}; + } + convertPatterns(lang); + wrd = '[\\w' + lo.specialChars + '@' + String.fromCharCode(173) + String.fromCharCode(8204) + '-]{' + min + ',}'; + lo.genRegExp = new RegExp('(' + url + ')|(' + mail + ')|(' + wrd + ')', 'gi'); + lo.prepared = true; + } + if (!!storage) { + try { + storage.setItem('Hyphenator_' + lang, window.JSON.stringify(lo)); + } catch (e) { + //onError(e); + } + } + + }, + + /** + * @name Hyphenator-prepare + * @description + * This funtion prepares the Hyphenator-Object: If RemoteLoading is turned off, it assumes + * that the patternfiles are loaded, all conversions are made and the callback is called. + * If storage is active the object is retrieved there. + * If RemoteLoading is on (default), it loads the pattern files and waits until they are loaded, + * by repeatedly checking Hyphenator.languages. If a patterfile is loaded the patterns are + * converted to their object style and the lang-object extended. + * Finally the callback is called. + * @param {function()} callback to call, when all patterns are loaded + * @private + */ + prepare = function (callback) { + var lang, interval, tmp1, tmp2; + if (!enableRemoteLoading) { + for (lang in Hyphenator.languages) { + if (Hyphenator.languages.hasOwnProperty(lang)) { + prepareLanguagesObj(lang); + } + } + state = 2; + callback(); + return; + } + // get all languages that are used and preload the patterns + state = 1; + for (lang in docLanguages) { + if (docLanguages.hasOwnProperty(lang)) { + if (!!storage && storage.getItem('Hyphenator_' + lang)) { + Hyphenator.languages[lang] = window.JSON.parse(storage.getItem('Hyphenator_' + lang)); + if (exceptions.hasOwnProperty('global')) { + tmp1 = convertExceptionsToObject(exceptions.global); + for (tmp2 in tmp1) { + if (tmp1.hasOwnProperty(tmp2)) { + Hyphenator.languages[lang].exceptions[tmp2] = tmp1[tmp2]; + } + } + } + //Replace exceptions since they may have been changed: + if (exceptions.hasOwnProperty(lang)) { + tmp1 = convertExceptionsToObject(exceptions[lang]); + for (tmp2 in tmp1) { + if (tmp1.hasOwnProperty(tmp2)) { + Hyphenator.languages[lang].exceptions[tmp2] = tmp1[tmp2]; + } + } + delete exceptions[lang]; + } + //Replace genRegExp since it may have been changed: + tmp1 = '[\\w' + Hyphenator.languages[lang].specialChars + '@' + String.fromCharCode(173) + String.fromCharCode(8204) + '-]{' + min + ',}'; + Hyphenator.languages[lang].genRegExp = new RegExp('(' + url + ')|(' + mail + ')|(' + tmp1 + ')', 'gi'); + + delete docLanguages[lang]; + continue; + } else { + loadPatterns(lang); + } + } + } + // if all patterns are loaded from storage: callback + if (countObjProps(docLanguages) === 0) { + state = 2; + callback(); + return; + } + // else async wait until patterns are loaded, then callback + interval = window.setInterval(function () { + var finishedLoading = true, lang; + for (lang in docLanguages) { + if (docLanguages.hasOwnProperty(lang)) { + finishedLoading = false; + if (!!Hyphenator.languages[lang]) { + delete docLanguages[lang]; + //do conversion while other patterns are loading: + prepareLanguagesObj(lang); + } + } + } + if (finishedLoading) { + //console.log('callig callback for ' + contextWindow.location.href); + window.clearInterval(interval); + state = 2; + callback(); + } + }, 100); + }, + + /** + * @name Hyphenator-switchToggleBox + * @description + * Creates or hides the toggleBox: a small button to turn off/on hyphenation on a page. + * @see Hyphenator.config + * @private + */ + toggleBox = function () { + var myBox, bdy, myIdAttribute, myTextNode, myClassAttribute, + text = (Hyphenator.doHyphenation ? 'Hy-phen-a-tion' : 'Hyphenation'); + if (!!(myBox = contextWindow.document.getElementById('HyphenatorToggleBox'))) { + myBox.firstChild.data = text; + } else { + bdy = contextWindow.document.getElementsByTagName('body')[0]; + myBox = createElem('div', contextWindow); + myIdAttribute = contextWindow.document.createAttribute('id'); + myIdAttribute.nodeValue = 'HyphenatorToggleBox'; + myClassAttribute = contextWindow.document.createAttribute('class'); + myClassAttribute.nodeValue = dontHyphenateClass; + myTextNode = contextWindow.document.createTextNode(text); + myBox.appendChild(myTextNode); + myBox.setAttributeNode(myIdAttribute); + myBox.setAttributeNode(myClassAttribute); + myBox.onclick = Hyphenator.toggleHyphenation; + myBox.style.position = 'absolute'; + myBox.style.top = '0px'; + myBox.style.right = '0px'; + myBox.style.margin = '0'; + myBox.style.backgroundColor = '#AAAAAA'; + myBox.style.color = '#FFFFFF'; + myBox.style.font = '6pt Arial'; + myBox.style.letterSpacing = '0.2em'; + myBox.style.padding = '3px'; + myBox.style.cursor = 'pointer'; + myBox.style.WebkitBorderBottomLeftRadius = '4px'; + myBox.style.MozBorderRadiusBottomleft = '4px'; + bdy.appendChild(myBox); + } + }, + + /** + * @name Hyphenator-hyphenateWord + * @description + * This function is the heart of Hyphenator.js. It returns a hyphenated word. + * + * If there's already a {@link Hyphenator-hypen} in the word, the word is returned as it is. + * If the word is in the exceptions list or in the cache, it is retrieved from it. + * If there's a '-' put a zeroWidthSpace after the '-' and hyphenate the parts. + * @param {string} lang The language of the word + * @param {string} word The word + * @returns string The hyphenated word + * @public + */ + hyphenateWord = function (lang, word) { + var lo = Hyphenator.languages[lang], + parts, i, l, w, wl, s, hypos, p, maxwins, win, pat = false, patk, c, t, n, numb3rs, inserted, hyphenatedword, val, subst, ZWNJpos = []; + if (word === '') { + return ''; + } + if (word.indexOf(hyphen) !== -1) { + //word already contains shy; -> leave at it is! + return word; + } + if (enableCache && lo.cache.hasOwnProperty(word)) { //the word is in the cache + return lo.cache[word]; + } + if (lo.exceptions.hasOwnProperty(word)) { //the word is in the exceptions list + return lo.exceptions[word].replace(/-/g, hyphen); + } + if (word.indexOf('-') !== -1) { + //word contains '-' -> hyphenate the parts separated with '-' + parts = word.split('-'); + for (i = 0, l = parts.length; i < l; i++) { + parts[i] = hyphenateWord(lang, parts[i]); + } + return parts.join('-'); + } + w = '_' + word + '_'; + if (word.indexOf(String.fromCharCode(8204)) !== -1) { + parts = w.split(String.fromCharCode(8204)); + w = parts.join(''); + for (i = 0, l = parts.length; i < l; i++) { + parts[i] = parts[i].length.toString(); + } + parts.pop(); + ZWNJpos = parts; + } + wl = w.length; + s = w.split(''); + if (!!lo.charSubstitution) { + for (subst in lo.charSubstitution) { + if (lo.charSubstitution.hasOwnProperty(subst)) { + w = w.replace(new RegExp(subst, 'g'), lo.charSubstitution[subst]); + } + } + } + if (word.indexOf("'") !== -1) { + w = w.toLowerCase().replace("'", "’"); //replace APOSTROPHE with RIGHT SINGLE QUOTATION MARK (since the latter is used in the patterns) + } else { + w = w.toLowerCase(); + } + //finally the core hyphenation algorithm + hypos = []; + numb3rs = {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9}; //check for member is faster then isFinite() + n = wl - lo.shortestPattern; + for (p = 0; p <= n; p++) { + maxwins = Math.min((wl - p), lo.longestPattern); + for (win = lo.shortestPattern; win <= maxwins; win++) { + if (lo.patterns.hasOwnProperty(patk = w.substring(p, p + win))) { + pat = lo.patterns[patk]; + if (enableReducedPatternSet && (typeof pat === 'string')) { + lo.redPatSet[patk] = pat; + } + if (typeof pat === 'string') { + //convert from string 'a5b' to array [1,5] (pos,value) + t = 0; + val = []; + for (i = 0; i < pat.length; i++) { + if (!!(c = numb3rs[pat.charAt(i)])) { + val.push(i - t, c); + t++; + } + } + pat = lo.patterns[patk] = val; + } + } else { + continue; + } + for (i = 0; i < pat.length; i++) { + c = p - 1 + pat[i]; + if (!hypos[c] || hypos[c] < pat[i + 1]) { + hypos[c] = pat[i + 1]; + } + i++; + } + } + } + inserted = 0; + for (i = lo.leftmin; i <= (wl - 2 - lo.rightmin); i++) { + if (ZWNJpos.length > 0 && ZWNJpos[0] === i) { + ZWNJpos.shift(); + s.splice(i + inserted - 1, 0, String.fromCharCode(8204)); + inserted++; + } + if (!!(hypos[i] & 1)) { + s.splice(i + inserted + 1, 0, hyphen); + inserted++; + } + } + hyphenatedword = s.slice(1, -1).join(''); + if (enableCache) { + lo.cache[word] = hyphenatedword; + } + return hyphenatedword; + }, + + /** + * @name Hyphenator-hyphenateURL + * @description + * Puts {@link Hyphenator-urlhyphen} after each no-alphanumeric char that my be in a URL. + * @param {string} url to hyphenate + * @returns string the hyphenated URL + * @public + */ + hyphenateURL = function (url) { + return url.replace(/([:\/\.\?#&_,;!@]+)/gi, '$&' + urlhyphen); + }, + + /** + * @name Hyphenator-removeHyphenationFromElement + * @description + * Removes all hyphens from the element. If there are other elements, the function is + * called recursively. + * Removing hyphens is usefull if you like to copy text. Some browsers are buggy when the copy hyphenated texts. + * @param {Object} el The element where to remove hyphenation. + * @public + */ + removeHyphenationFromElement = function (el) { + var h, i = 0, n; + switch (hyphen) { + case '|': + h = '\\|'; + break; + case '+': + h = '\\+'; + break; + case '*': + h = '\\*'; + break; + default: + h = hyphen; + } + while (!!(n = el.childNodes[i++])) { + if (n.nodeType === 3) { + n.data = n.data.replace(new RegExp(h, 'g'), ''); + n.data = n.data.replace(new RegExp(zeroWidthSpace, 'g'), ''); + } else if (n.nodeType === 1) { + removeHyphenationFromElement(n); + } + } + }, + + + /** + * @name Hyphenator-registerOnCopy + * @description + * Huge work-around for browser-inconsistency when it comes to + * copying of hyphenated text. + * The idea behind this code has been provided by http://github.com/aristus/sweet-justice + * sweet-justice is under BSD-License + * @private + */ + registerOnCopy = function (el) { + var body = el.ownerDocument.getElementsByTagName('body')[0], + shadow, + selection, + range, + rangeShadow, + restore, + oncopyHandler = function (e) { + e = e || window.event; + var target = e.target || e.srcElement, + currDoc = target.ownerDocument, + body = currDoc.getElementsByTagName('body')[0], + targetWindow = 'defaultView' in currDoc ? currDoc.defaultView : currDoc.parentWindow; + if (target.tagName && dontHyphenate[target.tagName.toLowerCase()]) { + //Safari needs this + return; + } + //create a hidden shadow element + shadow = currDoc.createElement('div'); + shadow.style.overflow = 'hidden'; + shadow.style.position = 'absolute'; + shadow.style.top = '-5000px'; + shadow.style.height = '1px'; + body.appendChild(shadow); + if (!!window.getSelection) { + //FF3, Webkit + selection = targetWindow.getSelection(); + range = selection.getRangeAt(0); + shadow.appendChild(range.cloneContents()); + removeHyphenationFromElement(shadow); + selection.selectAllChildren(shadow); + restore = function () { + shadow.parentNode.removeChild(shadow); + selection.addRange(range); + }; + } else { + // IE + selection = targetWindow.document.selection; + range = selection.createRange(); + shadow.innerHTML = range.htmlText; + removeHyphenationFromElement(shadow); + rangeShadow = body.createTextRange(); + rangeShadow.moveToElementText(shadow); + rangeShadow.select(); + restore = function () { + shadow.parentNode.removeChild(shadow); + if (range.text !== "") { + range.select(); + } + }; + } + window.setTimeout(restore, 0); + }; + if (!body) { + return; + } + el = el || body; + if (window.addEventListener) { + el.addEventListener("copy", oncopyHandler, false); + } else { + el.attachEvent("oncopy", oncopyHandler); + } + }, + + + /** + * @name Hyphenator-hyphenateElement + * @description + * Takes the content of the given element and - if there's text - replaces the words + * by hyphenated words. If there's another element, the function is called recursively. + * When all words are hyphenated, the visibility of the element is set to 'visible'. + * @param {Object} el The element to hyphenate + * @private + */ + hyphenateElement = function (el) { + var hyphenatorSettings = Expando.getDataForElem(el), + lang = hyphenatorSettings.language, hyphenate, n, i, + controlOrphans = function (part) { + var h, r; + switch (hyphen) { + case '|': + h = '\\|'; + break; + case '+': + h = '\\+'; + break; + case '*': + h = '\\*'; + break; + default: + h = hyphen; + } + if (orphanControl >= 2) { + //remove hyphen points from last word + r = part.split(' '); + r[1] = r[1].replace(new RegExp(h, 'g'), ''); + r[1] = r[1].replace(new RegExp(zeroWidthSpace, 'g'), ''); + r = r.join(' '); + } + if (orphanControl === 3) { + //replace spaces by non breaking spaces + r = r.replace(/[ ]+/g, String.fromCharCode(160)); + } + return r; + }; + if (Hyphenator.languages.hasOwnProperty(lang)) { + hyphenate = function (word) { + if (!Hyphenator.doHyphenation) { + return word; + } else if (urlOrMailRE.test(word)) { + return hyphenateURL(word); + } else { + return hyphenateWord(lang, word); + } + }; + if (safeCopy && (el.tagName.toLowerCase() !== 'body')) { + registerOnCopy(el); + } + i = 0; + while (!!(n = el.childNodes[i++])) { + if (n.nodeType === 3 && n.data.length >= min) { //type 3 = #text -> hyphenate! + n.data = n.data.replace(Hyphenator.languages[lang].genRegExp, hyphenate); + if (orphanControl !== 1) { + n.data = n.data.replace(/[\S]+ [\S]+$/, controlOrphans); + } + } + } + } + if (hyphenatorSettings.isHidden && intermediateState === 'hidden') { + el.style.visibility = 'visible'; + if (!hyphenatorSettings.hasOwnStyle) { + el.setAttribute('style', ''); // without this, removeAttribute doesn't work in Safari (thanks to molily) + el.removeAttribute('style'); + } else { + if (el.style.removeProperty) { + el.style.removeProperty('visibility'); + } else if (el.style.removeAttribute) { // IE + el.style.removeAttribute('visibility'); + } + } + } + if (hyphenatorSettings.isLast) { + state = 3; + documentCount--; + if (documentCount > (-1000) && documentCount <= 0) { + documentCount = (-2000); + onHyphenationDone(); + } + } + }, + + + /** + * @name Hyphenator-hyphenateDocument + * @description + * Calls hyphenateElement() for all members of elements. This is done with a setTimout + * to prevent a "long running Script"-alert when hyphenating large pages. + * Therefore a tricky bind()-function was necessary. + * @private + */ + hyphenateDocument = function () { + function bind(fun, arg) { + return function () { + return fun(arg); + }; + } + var i = 0, el; + while (!!(el = elements[i++])) { + if (el.ownerDocument.location.href === contextWindow.location.href) { + window.setTimeout(bind(hyphenateElement, el), 0); + } + } + }, + + /** + * @name Hyphenator-removeHyphenationFromDocument + * @description + * Does what it says ;-) + * @private + */ + removeHyphenationFromDocument = function () { + var i = 0, el; + while (!!(el = elements[i++])) { + removeHyphenationFromElement(el); + } + state = 4; + }, + + /** + * @name Hyphenator-createStorage + * @description + * inits the private var storage depending of the setting in storageType + * and the supported features of the system. + * @private + */ + createStorage = function () { + try { + if (storageType !== 'none' && + typeof(window.localStorage) !== 'undefined' && + typeof(window.sessionStorage) !== 'undefined' && + typeof(window.JSON.stringify) !== 'undefined' && + typeof(window.JSON.parse) !== 'undefined') { + switch (storageType) { + case 'session': + storage = window.sessionStorage; + break; + case 'local': + storage = window.localStorage; + break; + default: + storage = undefined; + break; + } + } + } catch (f) { + //FF throws an error if DOM.storage.enabled is set to false + } + }, + + /** + * @name Hyphenator-storeConfiguration + * @description + * Stores the current config-options in DOM-Storage + * @private + */ + storeConfiguration = function () { + if (!storage) { + return; + } + var settings = { + 'STORED': true, + 'classname': hyphenateClass, + 'donthyphenateclassname': dontHyphenateClass, + 'minwordlength': min, + 'hyphenchar': hyphen, + 'urlhyphenchar': urlhyphen, + 'togglebox': toggleBox, + 'displaytogglebox': displayToggleBox, + 'remoteloading': enableRemoteLoading, + 'enablecache': enableCache, + 'onhyphenationdonecallback': onHyphenationDone, + 'onerrorhandler': onError, + 'intermediatestate': intermediateState, + 'selectorfunction': selectorFunction, + 'safecopy': safeCopy, + 'doframes': doFrames, + 'storagetype': storageType, + 'orphancontrol': orphanControl, + 'dohyphenation': Hyphenator.doHyphenation, + 'persistentconfig': persistentConfig, + 'defaultlanguage': defaultLanguage + }; + storage.setItem('Hyphenator_config', window.JSON.stringify(settings)); + }, + + /** + * @name Hyphenator-restoreConfiguration + * @description + * Retrieves config-options from DOM-Storage and does configuration accordingly + * @private + */ + restoreConfiguration = function () { + var settings; + if (storage.getItem('Hyphenator_config')) { + settings = window.JSON.parse(storage.getItem('Hyphenator_config')); + Hyphenator.config(settings); + } + }; + + return { + + /** + * @name Hyphenator.version + * @memberOf Hyphenator + * @description + * String containing the actual version of Hyphenator.js + * [major release].[minor releas].[bugfix release] + * major release: new API, new Features, big changes + * minor release: new languages, improvements + * @public + */ + version: '3.3.0', + + /** + * @name Hyphenator.doHyphenation + * @description + * If doHyphenation is set to false (defaults to true), hyphenateDocument() isn't called. + * All other actions are performed. + */ + doHyphenation: true, + + /** + * @name Hyphenator.languages + * @memberOf Hyphenator + * @description + * Objects that holds key-value pairs, where key is the language and the value is the + * language-object loaded from (and set by) the pattern file. + * The language object holds the following members: + * + * + * + * + * + * + * + * + *
    keydesc>
    leftminThe minimum of chars to remain on the old line
    rightminThe minimum of chars to go on the new line
    shortestPatternThe shortes pattern (numbers don't count!)
    longestPatternThe longest pattern (numbers don't count!)
    specialCharsNon-ASCII chars in the alphabet.
    patternsthe patterns
    + * And optionally (or after prepareLanguagesObj() has been called): + * + * + *
    exceptionsExcpetions for the secified language
    + * @public + */ + languages: {}, + + + /** + * @name Hyphenator.config + * @description + * Config function that takes an object as an argument. The object contains key-value-pairs + * containig Hyphenator-settings. This is a shortcut for calling Hyphenator.set...-Methods. + * @param {Object} obj + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    keyvaluesdefault
    classnamestring'hyphenate'
    donthyphenateclassnamestring''
    minwordlengthinteger6
    hyphencharstring'&shy;'
    urlhyphencharstring'zero with space'
    toggleboxfunctionsee code
    displaytoggleboxbooleanfalse
    remoteloadingbooleantrue
    enablecachebooleantrue
    enablereducedpatternsetbooleanfalse
    onhyphenationdonecallbackfunctionempty function
    onerrorhandlerfunctionalert(onError)
    intermediatestatestring'hidden'
    selectorfunctionfunction[…]
    safecopybooleantrue
    doframesbooleanfalse
    storagetypestring'none'
    + * @public + * @example <script src = "Hyphenator.js" type = "text/javascript"></script> +  * <script type = "text/javascript"> +  * Hyphenator.config({'minwordlength':4,'hyphenchar':'|'}); + * Hyphenator.run(); +  * </script> + */ + config: function (obj) { + var assert = function (name, type) { + if (typeof obj[name] === type) { + return true; + } else { + onError(new Error('Config onError: ' + name + ' must be of type ' + type)); + return false; + } + }, + key; + + if (obj.hasOwnProperty('storagetype')) { + if (assert('storagetype', 'string')) { + storageType = obj.storagetype; + } + if (!storage) { + createStorage(); + } + } + if (!obj.hasOwnProperty('STORED') && storage && obj.hasOwnProperty('persistentconfig') && obj.persistentconfig === true) { + restoreConfiguration(); + } + + for (key in obj) { + if (obj.hasOwnProperty(key)) { + switch (key) { + case 'STORED': + break; + case 'classname': + if (assert('classname', 'string')) { + hyphenateClass = obj[key]; + } + break; + case 'donthyphenateclassname': + if (assert('donthyphenateclassname', 'string')) { + dontHyphenateClass = obj[key]; + } + break; + case 'minwordlength': + if (assert('minwordlength', 'number')) { + min = obj[key]; + } + break; + case 'hyphenchar': + if (assert('hyphenchar', 'string')) { + if (obj.hyphenchar === '­') { + obj.hyphenchar = String.fromCharCode(173); + } + hyphen = obj[key]; + } + break; + case 'urlhyphenchar': + if (obj.hasOwnProperty('urlhyphenchar')) { + if (assert('urlhyphenchar', 'string')) { + urlhyphen = obj[key]; + } + } + break; + case 'togglebox': + if (assert('togglebox', 'function')) { + toggleBox = obj[key]; + } + break; + case 'displaytogglebox': + if (assert('displaytogglebox', 'boolean')) { + displayToggleBox = obj[key]; + } + break; + case 'remoteloading': + if (assert('remoteloading', 'boolean')) { + enableRemoteLoading = obj[key]; + } + break; + case 'enablecache': + if (assert('enablecache', 'boolean')) { + enableCache = obj[key]; + } + break; + case 'enablereducedpatternset': + if (assert('enablereducedpatternset', 'boolean')) { + enableReducedPatternSet = obj[key]; + } + break; + case 'onhyphenationdonecallback': + if (assert('onhyphenationdonecallback', 'function')) { + onHyphenationDone = obj[key]; + } + break; + case 'onerrorhandler': + if (assert('onerrorhandler', 'function')) { + onError = obj[key]; + } + break; + case 'intermediatestate': + if (assert('intermediatestate', 'string')) { + intermediateState = obj[key]; + } + break; + case 'selectorfunction': + if (assert('selectorfunction', 'function')) { + selectorFunction = obj[key]; + } + break; + case 'safecopy': + if (assert('safecopy', 'boolean')) { + safeCopy = obj[key]; + } + break; + case 'doframes': + if (assert('doframes', 'boolean')) { + doFrames = obj[key]; + } + break; + case 'storagetype': + if (assert('storagetype', 'string')) { + storageType = obj[key]; + } + break; + case 'orphancontrol': + if (assert('orphancontrol', 'number')) { + orphanControl = obj[key]; + } + break; + case 'dohyphenation': + if (assert('dohyphenation', 'boolean')) { + Hyphenator.doHyphenation = obj[key]; + } + break; + case 'persistentconfig': + if (assert('persistentconfig', 'boolean')) { + persistentConfig = obj[key]; + } + break; + case 'defaultlanguage': + if (assert('defaultlanguage', 'string')) { + defaultLanguage = obj[key]; + } + break; + default: + onError(new Error('Hyphenator.config: property ' + key + ' not known.')); + } + } + } + if (storage && persistentConfig) { + storeConfiguration(); + } + }, + + /** + * @name Hyphenator.run + * @description + * Bootstrap function that starts all hyphenation processes when called. + * @public + * @example <script src = "Hyphenator.js" type = "text/javascript"></script> +  * <script type = "text/javascript"> +  *   Hyphenator.run(); +  * </script> + */ + run: function () { + documentCount = 0; + var process = function () { + try { + if (contextWindow.document.getElementsByTagName('frameset').length > 0) { + return; //we are in a frameset + } + documentCount++; + autoSetMainLanguage(undefined); + gatherDocumentInfos(); + //console.log('preparing for ' + contextWindow.location.href); + prepare(hyphenateDocument); + if (displayToggleBox) { + toggleBox(); + } + } catch (e) { + onError(e); + } + }, i, haveAccess, fl = window.frames.length; + + if (!storage) { + createStorage(); + } + if (!documentLoaded && !isBookmarklet) { + runOnContentLoaded(window, process); + } + if (isBookmarklet || documentLoaded) { + if (doFrames && fl > 0) { + for (i = 0; i < fl; i++) { + haveAccess = undefined; + //try catch isn't enough for webkit + try { + //opera throws only on document.toString-access + haveAccess = window.frames[i].document.toString(); + } catch (e) { + haveAccess = undefined; + } + if (!!haveAccess) { + contextWindow = window.frames[i]; + process(); + } + } + } + contextWindow = window; + process(); + } + }, + + /** + * @name Hyphenator.addExceptions + * @description + * Adds the exceptions from the string to the appropriate language in the + * {@link Hyphenator-languages}-object + * @param {string} lang The language + * @param {string} words A comma separated string of hyphenated words WITH spaces. + * @public + * @example <script src = "Hyphenator.js" type = "text/javascript"></script> +  * <script type = "text/javascript"> +  *   Hyphenator.addExceptions('de','ziem-lich, Wach-stube'); + * Hyphenator.run(); +  * </script> + */ + addExceptions: function (lang, words) { + if (lang === '') { + lang = 'global'; + } + if (exceptions.hasOwnProperty(lang)) { + exceptions[lang] += ", " + words; + } else { + exceptions[lang] = words; + } + }, + + /** + * @name Hyphenator.hyphenate + * @public + * @description + * Hyphenates the target. The language patterns must be loaded. + * If the target is a string, the hyphenated string is returned, + * if it's an object, the values are hyphenated directly. + * @param {string|Object} target the target to be hyphenated + * @param {string} lang the language of the target + * @returns string + * @example <script src = "Hyphenator.js" type = "text/javascript"></script> + * <script src = "patterns/en.js" type = "text/javascript"></script> +  * <script type = "text/javascript"> + * var t = Hyphenator.hyphenate('Hyphenation', 'en'); //Hy|phen|ation + * </script> + */ + hyphenate: function (target, lang) { + var hyphenate, n, i; + if (Hyphenator.languages.hasOwnProperty(lang)) { + if (!Hyphenator.languages[lang].prepared) { + prepareLanguagesObj(lang); + } + hyphenate = function (word) { + if (urlOrMailRE.test(word)) { + return hyphenateURL(word); + } else { + return hyphenateWord(lang, word); + } + }; + if (typeof target === 'string' || target.constructor === String) { + return target.replace(Hyphenator.languages[lang].genRegExp, hyphenate); + } else if (typeof target === 'object') { + i = 0; + while (!!(n = target.childNodes[i++])) { + if (n.nodeType === 3 && n.data.length >= min) { //type 3 = #text -> hyphenate! + n.data = n.data.replace(Hyphenator.languages[lang].genRegExp, hyphenate); + } else if (n.nodeType === 1) { + if (n.lang !== '') { + Hyphenator.hyphenate(n, n.lang); + } else { + Hyphenator.hyphenate(n, lang); + } + } + } + } + } else { + onError(new Error('Language "' + lang + '" is not loaded.')); + } + }, + + /** + * @name Hyphenator.getRedPatternSet + * @description + * Returns {@link Hyphenator-isBookmarklet}. + * @param {string} lang the language patterns are stored for + * @returns object {'patk': pat} + * @public + */ + getRedPatternSet: function (lang) { + return Hyphenator.languages[lang].redPatSet; + }, + + /** + * @name Hyphenator.isBookmarklet + * @description + * Returns {@link Hyphenator-isBookmarklet}. + * @returns boolean + * @public + */ + isBookmarklet: function () { + return isBookmarklet; + }, + + getConfigFromURI: function () { + var loc = null, re = {}, jsArray = document.getElementsByTagName('script'), i, j, l, s, gp, option; + for (i = 0, l = jsArray.length; i < l; i++) { + if (!!jsArray[i].getAttribute('src')) { + loc = jsArray[i].getAttribute('src'); + } + if (!loc) { + continue; + } else { + s = loc.indexOf('Hyphenator.js?'); + if (s === -1) { + continue; + } + gp = loc.substring(s + 14).split('&'); + for (j = 0; j < gp.length; j++) { + option = gp[j].split('='); + if (option[0] === 'bm') { + continue; + } + if (option[1] === 'true') { + re[option[0]] = true; + continue; + } + if (option[1] === 'false') { + re[option[0]] = false; + continue; + } + if (isFinite(option[1])) { + re[option[0]] = parseInt(option[1], 10); + continue; + } + if (option[0] === 'onhyphenationdonecallback') { + re[option[0]] = new Function('', option[1]); + continue; + } + re[option[0]] = option[1]; + } + break; + } + } + return re; + }, + + /** + * @name Hyphenator.toggleHyphenation + * @description + * Checks the current state of the ToggleBox and removes or does hyphenation. + * @public + */ + toggleHyphenation: function () { + if (Hyphenator.doHyphenation) { + removeHyphenationFromDocument(); + Hyphenator.doHyphenation = false; + storeConfiguration(); + toggleBox(); + } else { + hyphenateDocument(); + Hyphenator.doHyphenation = true; + storeConfiguration(); + toggleBox(); + } + } + }; +}(window)); + +//Export properties/methods (for google closure compiler) +Hyphenator['languages'] = Hyphenator.languages; +Hyphenator['config'] = Hyphenator.config; +Hyphenator['run'] = Hyphenator.run; +Hyphenator['addExceptions'] = Hyphenator.addExceptions; +Hyphenator['hyphenate'] = Hyphenator.hyphenate; +Hyphenator['getRedPatternSet'] = Hyphenator.getRedPatternSet; +Hyphenator['isBookmarklet'] = Hyphenator.isBookmarklet; +Hyphenator['getConfigFromURI'] = Hyphenator.getConfigFromURI; +Hyphenator['toggleHyphenation'] = Hyphenator.toggleHyphenation; +window['Hyphenator'] = Hyphenator; + +if (Hyphenator.isBookmarklet()) { + Hyphenator.config({displaytogglebox: true, intermediatestate: 'visible', doframes: true}); + Hyphenator.config(Hyphenator.getConfigFromURI()); + Hyphenator.run(); +} \ No newline at end of file diff --git a/books/a-study-in-scarlet/js/blood.js b/books/a-study-in-scarlet/js/blood.js new file mode 100644 index 0000000..0c6a262 --- /dev/null +++ b/books/a-study-in-scarlet/js/blood.js @@ -0,0 +1,73 @@ +/* + * Baker Ebook Framework - Basic Book + * last update: 2011-07-18 + * + * Copyright (C) 2011 by Davide S. Casali + * + * + * Usage: + * Define the CSS for 'blood-drop'. + * Init with the position of the dripping point: + * + * Blood.init(x, y, timeInterval); + * + */ + +var Blood = { + + sprite: null, + init: { x: 0, y: 0 }, + loopCreate: null, + loop: null, + documentHeight: 0, + accelerometer: { x: 0, y: 0, oldX: null, oldY: null }, + accelerometerSensitivity: 100, + + init: function(x, y, time) { + this.sprite = document.createElement('div'); + this.sprite.setAttribute('class', 'blood-drop'); + this.sprite.style.position = "absolute"; + + // Position + this.init.x = x; + this.init.y = y; + this.positionToCSS(this.init.x, this.init.y, this.sprite); + + // Wait and attach + var self = this; + document.addEventListener("DOMContentLoaded", function() { + self.documentHeight = document.body.offsetHeight; + document.body.appendChild(self.sprite); + }, false); + window.addEventListener("devicemotion", function(e) { + // Process event.acceleration, event.accelerationIncludingGravity, + // event.rotationRate and event.interval + self.accelerometer.x = parseInt(e.accelerationIncludingGravity.x * self.accelerometerSensitivity); + self.accelerometer.y = parseInt(-e.accelerationIncludingGravity.y * self.accelerometerSensitivity); + if (self.accelerometer.oldX != null && self.accelerometer.x != self.accelerometer.oldX && self.accelerometer.y != self.accelerometer.oldY) { + self.drop(); + } + self.accelerometer.oldX = self.accelerometer.x; + self.accelerometer.oldY = self.accelerometer.y; + }, false); + + if (this.loop) clearTimeout(this.loop); // wrong position + this.loopCreate = setInterval("Blood.drop()", time); + }, + + positionToCSS: function(x, y, sprite) { + if (x >= 0) sprite.style.top = x + "px"; + else sprite.style.bottom = -x + "px"; + if (y >= 0) sprite.style.left = y + "px"; + else sprite.style.right = -y + "px"; + }, + + drop: function() { + if ((this.sprite.offsetTop + this.sprite.offsetHeight) < this.documentHeight) { + this.sprite.style.top = (parseInt(this.sprite.style.top) * 1.038) + "px"; + this.loop = setTimeout("Blood.drop()", 1); + } else { + this.positionToCSS(this.init.x, this.init.y, this.sprite); + } + } +} \ No newline at end of file