diff --git a/.gitignore b/.gitignore index ab5ec3a..a24ab7e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ xcuserdata +auth.json diff --git a/Blog.xcodeproj/project.pbxproj b/Blog.xcodeproj/project.pbxproj index 4807cd0..bf7762d 100644 --- a/Blog.xcodeproj/project.pbxproj +++ b/Blog.xcodeproj/project.pbxproj @@ -7,7 +7,9 @@ objects = { /* Begin PBXBuildFile section */ + 1BCFCC637C63C780248D685E /* PostCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 1BCFCB5C36F301B2B93F8069 /* PostCell.m */; }; 30089596C2F733D451A454E8 /* libPods.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1613DC56A86AFA7E50460A37 /* libPods.a */; }; + 7B4070531AE46BC9000C2E43 /* auth.json in Resources */ = {isa = PBXBuildFile; fileRef = 7B4070521AE46BC9000C2E43 /* auth.json */; }; 7B5C4BDF19F2606900667D48 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 7B5C4BDE19F2606900667D48 /* main.m */; }; 7B5C4BE219F2606900667D48 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7B5C4BE119F2606900667D48 /* AppDelegate.m */; }; 7B5C4BE519F2606900667D48 /* MasterViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 7B5C4BE419F2606900667D48 /* MasterViewController.m */; }; @@ -22,6 +24,7 @@ 7B9E64421A22F3840072FF42 /* BlogService.m in Sources */ = {isa = PBXBuildFile; fileRef = 7B9E64411A22F3840072FF42 /* BlogService.m */; }; 7B9E644C1A230B940072FF42 /* JSONHTTPClient.m in Sources */ = {isa = PBXBuildFile; fileRef = 7B9E644B1A230B940072FF42 /* JSONHTTPClient.m */; }; 7B9E644F1A23129B0072FF42 /* BlogStatus.m in Sources */ = {isa = PBXBuildFile; fileRef = 7B9E644E1A23129B0072FF42 /* BlogStatus.m */; }; + 7BE3A0351AE461E700E45CCB /* PreviewViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 7BE3A0341AE461E700E45CCB /* PreviewViewController.m */; }; 7BF029331A27117200E42EDE /* ModelStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 7BF029321A27117200E42EDE /* ModelStore.m */; }; 7BF029381A280CB200E42EDE /* BlogController.m in Sources */ = {isa = PBXBuildFile; fileRef = 7BF029371A280CB200E42EDE /* BlogController.m */; }; B8B8958B2AA40812EFE04FEF /* libPods-BlogTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9C36BC848680D4F831E4DE23 /* libPods-BlogTests.a */; }; @@ -39,6 +42,9 @@ /* Begin PBXFileReference section */ 1613DC56A86AFA7E50460A37 /* libPods.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libPods.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 1BCFCB5C36F301B2B93F8069 /* PostCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PostCell.m; sourceTree = ""; }; + 1BCFCCF30594E0E2DCC32116 /* PostCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PostCell.h; sourceTree = ""; }; + 7B4070521AE46BC9000C2E43 /* auth.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = auth.json; sourceTree = ""; }; 7B5C4BD919F2606900667D48 /* Blog.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Blog.app; sourceTree = BUILT_PRODUCTS_DIR; }; 7B5C4BDD19F2606900667D48 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 7B5C4BDE19F2606900667D48 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; @@ -66,6 +72,8 @@ 7B9E644B1A230B940072FF42 /* JSONHTTPClient.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JSONHTTPClient.m; sourceTree = ""; }; 7B9E644D1A23129B0072FF42 /* BlogStatus.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BlogStatus.h; sourceTree = ""; }; 7B9E644E1A23129B0072FF42 /* BlogStatus.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BlogStatus.m; sourceTree = ""; }; + 7BE3A0331AE461E700E45CCB /* PreviewViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PreviewViewController.h; sourceTree = ""; }; + 7BE3A0341AE461E700E45CCB /* PreviewViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PreviewViewController.m; sourceTree = ""; }; 7BF029311A27117200E42EDE /* ModelStore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ModelStore.h; sourceTree = ""; }; 7BF029321A27117200E42EDE /* ModelStore.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ModelStore.m; sourceTree = ""; }; 7BF029361A280CB200E42EDE /* BlogController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BlogController.h; sourceTree = ""; }; @@ -140,10 +148,14 @@ 7B5C4BE419F2606900667D48 /* MasterViewController.m */, 7B5C4BE619F2606900667D48 /* DetailViewController.h */, 7B5C4BE719F2606900667D48 /* DetailViewController.m */, + 7BE3A0331AE461E700E45CCB /* PreviewViewController.h */, + 7BE3A0341AE461E700E45CCB /* PreviewViewController.m */, 7B5C4BE919F2606900667D48 /* Main.storyboard */, 7B5C4BEC19F2606900667D48 /* Images.xcassets */, 7B5C4BEE19F2606900667D48 /* LaunchScreen.xib */, 7B5C4BDC19F2606900667D48 /* Supporting Files */, + 1BCFCB5C36F301B2B93F8069 /* PostCell.m */, + 1BCFCCF30594E0E2DCC32116 /* PostCell.h */, ); path = Blog; sourceTree = ""; @@ -151,6 +163,7 @@ 7B5C4BDC19F2606900667D48 /* Supporting Files */ = { isa = PBXGroup; children = ( + 7B4070521AE46BC9000C2E43 /* auth.json */, 7B5C4BDD19F2606900667D48 /* Info.plist */, 7B5C4BDE19F2606900667D48 /* main.m */, ); @@ -273,6 +286,7 @@ TargetAttributes = { 7B5C4BD819F2606900667D48 = { CreatedOnToolsVersion = 6.0.1; + DevelopmentTeam = B2W6993X5Z; }; 7B5C4BF419F2606900667D48 = { CreatedOnToolsVersion = 6.0.1; @@ -307,6 +321,7 @@ 7B5C4BEB19F2606900667D48 /* Main.storyboard in Resources */, 7B5C4BF019F2606900667D48 /* LaunchScreen.xib in Resources */, 7B5C4BED19F2606900667D48 /* Images.xcassets in Resources */, + 7B4070531AE46BC9000C2E43 /* auth.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -398,7 +413,9 @@ 7BF029331A27117200E42EDE /* ModelStore.m in Sources */, 7BF029381A280CB200E42EDE /* BlogController.m in Sources */, 7B5C4BE819F2606900667D48 /* DetailViewController.m in Sources */, + 7BE3A0351AE461E700E45CCB /* PreviewViewController.m in Sources */, 7B9E644F1A23129B0072FF42 /* BlogStatus.m in Sources */, + 1BCFCC637C63C780248D685E /* PostCell.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Blog/AppDelegate.m b/Blog/AppDelegate.m index 731cf81..b1a1f4a 100644 --- a/Blog/AppDelegate.m +++ b/Blog/AppDelegate.m @@ -7,28 +7,78 @@ // #import "AppDelegate.h" +#import "MasterViewController.h" #import "DetailViewController.h" #import "BlogService.h" +#import "YapDatabase.h" +#import "ModelStore.h" +#import "JSONHTTPClient.h" +#import "BlogController.h" @interface AppDelegate () -@property (nonatomic, readonly, strong) BlogService *blogService; - @end @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - _blogService = [BlogService new]; - - // Override point for customization after application launch. UISplitViewController *splitViewController = (UISplitViewController *)self.window.rootViewController; - UINavigationController *navigationController = [splitViewController.viewControllers lastObject]; + UINavigationController *navigationController = splitViewController.viewControllers.lastObject; navigationController.topViewController.navigationItem.leftBarButtonItem = splitViewController.displayModeButtonItem; splitViewController.delegate = self; + [self setupBlogController]; return YES; } +- (MasterViewController *)masterViewController { + UISplitViewController *splitViewController = (UISplitViewController *)self.window.rootViewController; + UINavigationController *navigationController = splitViewController.viewControllers.firstObject; + MasterViewController *masterViewController = (MasterViewController *)navigationController.viewControllers.firstObject; + return masterViewController; +} + +- (void)setupBlogController { + NSString *cachesPath = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).lastObject; + NSString *dbPath = [cachesPath stringByAppendingPathComponent:@"blog.sqlite"]; + ModelStore *store = [self newModelStoreWithPath:dbPath]; + BlogController *blogController = [self newBlogControllerWithModelStore:store rootURL:@"http://ocean.samhuri.net:6706/"]; + + [self masterViewController].blogController = blogController; +} + +- (ModelStore *)newModelStoreWithPath:(NSString *)dbPath { + YapDatabase *database = [[YapDatabase alloc] initWithPath:dbPath]; + YapDatabaseConnection *connection = [database newConnection]; + ModelStore *store = [[ModelStore alloc] initWithConnection:connection]; + return store; +} + +- (BlogController *)newBlogControllerWithModelStore:(ModelStore *)store rootURL:(NSString *)rootURL { + NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; + NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration]; + JSONHTTPClient *client = [[JSONHTTPClient alloc] initWithSession:session]; + client.defaultHeaders = [self defaultBlogHeaders]; + BlogService *service = [[BlogService alloc] initWithRootURL:rootURL client:client]; + BlogController *blogController = [[BlogController alloc] initWithService:service store:store]; + return blogController; +} + +- (NSDictionary *)defaultBlogHeaders { + NSString *authPath = [[NSBundle mainBundle] pathForResource:@"auth.json" ofType:nil]; + if (authPath.length) { + NSData *data = [NSData dataWithContentsOfFile:authPath]; + NSError *error = nil; + NSDictionary *auth = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; + if (auth) { + return @{@"Auth" : [NSString stringWithFormat:@"%@|%@", auth[@"username"], auth[@"password"]]}; + } + NSLog(@"auth.json: %@", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]); + NSLog(@"[ERROR] Failed to parse auth.json: %@ %@", error.localizedDescription, error.userInfo); + } + NSLog(@"[WARNING] No auth.json found. Blog will be read-only."); + return nil; +}; + - (void)applicationWillResignActive:(UIApplication *)application { // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. diff --git a/Blog/Base.lproj/LaunchScreen.xib b/Blog/Base.lproj/LaunchScreen.xib index 2800d22..83ae138 100644 --- a/Blog/Base.lproj/LaunchScreen.xib +++ b/Blog/Base.lproj/LaunchScreen.xib @@ -1,7 +1,7 @@ - + - + @@ -14,17 +14,18 @@ - + + - + diff --git a/Blog/Base.lproj/Main.storyboard b/Blog/Base.lproj/Main.storyboard index 5f4cdfc..9f02e85 100644 --- a/Blog/Base.lproj/Main.storyboard +++ b/Blog/Base.lproj/Main.storyboard @@ -1,15 +1,19 @@ - + - + + - + + + + @@ -28,39 +32,118 @@ - + - + + + + + + + + + + + + + + + Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. + + + + + + + - + + - - - + + + + + + + + + + + + + + + + + + + + + - + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -75,34 +158,49 @@ - + - + + - + - + + + + + + + + + + + + + - + + + @@ -113,12 +211,14 @@ - + + + @@ -142,8 +242,11 @@ - + + + + @@ -155,6 +258,6 @@ - + diff --git a/Blog/BlogController.h b/Blog/BlogController.h index dc19529..ee50696 100644 --- a/Blog/BlogController.h +++ b/Blog/BlogController.h @@ -8,15 +8,15 @@ @import Foundation; -NSString *BlogStatusChangedNotification; -NSString *BlogDraftsChangedNotification; -NSString *BlogDraftAddedNotification; -NSString *BlogDraftRemovedNotification; -NSString *BlogPublishedPostsChangedNotification; -NSString *BlogPublishedPostAddedNotification; -NSString *BlogPublishedPostRemovedNotification; -NSString *BlogPostChangedNotification; -NSString *BlogPostDeletedNotification; +extern NSString *BlogStatusChangedNotification; +extern NSString *BlogDraftsChangedNotification; +extern NSString *BlogDraftAddedNotification; +extern NSString *BlogDraftRemovedNotification; +extern NSString *BlogPublishedPostsChangedNotification; +extern NSString *BlogPublishedPostAddedNotification; +extern NSString *BlogPublishedPostRemovedNotification; +extern NSString *BlogPostChangedNotification; +extern NSString *BlogPostDeletedNotification; @class PMKPromise; @class ModelStore; @@ -26,7 +26,7 @@ NSString *BlogPostDeletedNotification; - (instancetype)initWithService:(BlogService *)service store:(ModelStore *)store; -- (NSURL *)previewURLForPostWithPath:(NSString *)path; +- (NSMutableURLRequest *)previewRequestWithPath:(NSString *)path; - (PMKPromise *)requestBlogStatus; diff --git a/Blog/BlogController.m b/Blog/BlogController.m index 2e3e853..778e498 100644 --- a/Blog/BlogController.m +++ b/Blog/BlogController.m @@ -35,8 +35,11 @@ NSString *BlogPostDeletedNotification = @"BlogPostDeletedNotification"; return self; } -- (NSURL *)previewURLForPostWithPath:(NSString *)path { - return [_service previewURLForPostWithPath:path]; +- (NSMutableURLRequest *)previewRequestWithPath:(NSString *)path; +{ + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[_service urlFor:path]]; + [request addValue:@"text/html" forHTTPHeaderField:@"Accept"]; + return request; } - (PMKPromise *)requestBlogStatus { @@ -55,9 +58,11 @@ NSString *BlogPostDeletedNotification = @"BlogPostDeletedNotification"; - (PMKPromise *)requestDrafts { NSArray *posts = [_store drafts]; if (posts) { + NSLog(@"returning %@ cached drafts", @(posts.count)); return [PMKPromise promiseWithValue:posts]; } else { + NSLog(@"requesting drafts from server"); return [_service requestDrafts].then(^(NSArray *posts) { [_store saveDrafts:posts]; return posts; diff --git a/Blog/BlogService.h b/Blog/BlogService.h index 13b2808..5388060 100644 --- a/Blog/BlogService.h +++ b/Blog/BlogService.h @@ -21,7 +21,7 @@ typedef NS_ENUM(NSUInteger, BlogServiceErrorCode) { - (instancetype)initWithRootURL:(NSString *)rootURL client:(JSONHTTPClient *)client; -- (NSURL *)previewURLForPostWithPath:(NSString *)path; +- (NSURL *)urlFor:(NSString *)path, ...; - (PMKPromise *)requestBlogStatus; - (PMKPromise *)requestPublishEnvironment:(NSString *)environment; diff --git a/Blog/BlogService.m b/Blog/BlogService.m index ea73c96..4ec46b9 100644 --- a/Blog/BlogService.m +++ b/Blog/BlogService.m @@ -33,10 +33,6 @@ NSString * const BlogServiceErrorDomain = @"BlogServiceErrorDomain"; return self; } -- (NSURL *)previewURLForPostWithPath:(NSString *)path { - return [self urlFor:@"%@/preview", path]; -} - - (NSURL *)urlFor:(NSString *)path, ... { va_list args; va_start(args, path); @@ -82,7 +78,7 @@ NSString * const BlogServiceErrorDomain = @"BlogServiceErrorDomain"; } - (PMKPromise *)requestDrafts { - return [self.client get:[self urlFor:@"/drafts"] headers:nil].then([self decodePostsBlock]); + return [self.client get:[self urlFor:@"/posts/drafts"] headers:nil].then([self decodePostsBlock]); } - (PMKPromise *)requestPublishedPosts { @@ -97,9 +93,9 @@ NSString * const BlogServiceErrorDomain = @"BlogServiceErrorDomain"; NSDictionary *fields = @{@"id": draftID, @"title": title, @"body": body, - @"link": link, + @"link": link ?: [NSNull null], }; - return [self.client postJSON:[self urlFor:@"/drafts"] headers:nil fields:fields].then([self decodePostBlock]); + return [self.client postJSON:[self urlFor:@"/posts/drafts"] headers:nil fields:fields].then([self decodePostBlock]); } - (PMKPromise *)requestPublishDraftWithPath:(NSString *)path { @@ -113,7 +109,7 @@ NSString * const BlogServiceErrorDomain = @"BlogServiceErrorDomain"; - (PMKPromise *)requestUpdatePostWithPath:(NSString *)path title:(NSString *)title body:(NSString *)body link:(NSString *)link { NSDictionary *fields = @{@"title": title, @"body": body, - @"link": link, + @"link": link ?: [NSNull null], }; return [self.client putJSON:[self urlFor:path] headers:nil fields:fields]; } diff --git a/Blog/DetailViewController.h b/Blog/DetailViewController.h index 4b324a7..4fe9132 100644 --- a/Blog/DetailViewController.h +++ b/Blog/DetailViewController.h @@ -8,10 +8,12 @@ @import UIKit; +@class BlogController; @class Post; @interface DetailViewController : UIViewController +@property (strong, nonatomic) BlogController *blogController; @property (strong, nonatomic) Post *post; @end diff --git a/Blog/DetailViewController.m b/Blog/DetailViewController.m index 0c36b48..3d21cdd 100644 --- a/Blog/DetailViewController.m +++ b/Blog/DetailViewController.m @@ -6,12 +6,16 @@ // Copyright (c) 2014 Guru Logic Inc. All rights reserved. // +#import #import "DetailViewController.h" +#import "BlogController.h" #import "Post.h" +#import "PreviewViewController.h" -@interface DetailViewController () +@interface DetailViewController () -@property (weak, nonatomic) IBOutlet UILabel *detailDescriptionLabel; +@property (nonatomic, weak) IBOutlet UITextView *textView; +@property (nonatomic, weak) IBOutlet UIToolbar *toolbar; @end @@ -22,30 +26,60 @@ - (void)setPost:(id)newPost { if (_post != newPost) { _post = newPost; - - // Update the view. [self configureView]; } } - (void)configureView { - // Update the user interface for the detail item. if (self.post) { - // FIXME: date, link (edit, open), status (draft, published), delete, preview, publish + // FIXME: date, status (draft, published) self.navigationItem.title = self.post.title ?: @"Untitled"; - self.detailDescriptionLabel.text = self.post.body; + self.textView.text = self.post.body; } } - (void)viewDidLoad { [super viewDidLoad]; - // Do any additional setup after loading the view, typically from a nib. [self configureView]; } -- (void)didReceiveMemoryWarning { - [super didReceiveMemoryWarning]; - // Dispose of any resources that can be recreated. +- (void)viewWillAppear:(BOOL)animated; +{ + [super viewWillAppear:animated]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(savePostBody) name:UIApplicationWillResignActiveNotification object:nil]; } +- (void)viewWillDisappear:(BOOL)animated { + [super viewWillDisappear:animated]; + [self savePostBody]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationWillResignActiveNotification object:nil]; +} + +- (PMKPromise *)savePostBody { + if (!self.post || !self.textView) { return [PMKPromise promiseWithValue:nil]; } + + Post *newPost = [self.post copyWithBody:self.textView.text]; + if (![self.post isEqual:newPost]) + { + self.post = newPost; + return [self.blogController requestUpdatePostWithPath:self.post.path title:self.post.title body:self.post.body link:self.post.url.absoluteString] + .then(^(Post *post) { + NSLog(@"saved post at path %@", self.post.path); + }).catch(^(NSError *error) { + NSLog(@"Error saving post at path %@: %@ %@", self.post.path, error.localizedDescription, error.userInfo); + }); + } + return [PMKPromise promiseWithValue:self.post]; +} + +- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { + [super prepareForSegue:segue sender:sender]; + if ([segue.identifier isEqualToString:@"showPreview"]) { + PreviewViewController *previewViewController = segue.destinationViewController; + previewViewController.promise = [self savePostBody]; + previewViewController.initialRequest = [self.blogController previewRequestWithPath:self.post.path]; + } +} + + @end diff --git a/Blog/Info.plist b/Blog/Info.plist index 11e0c9c..d939436 100644 --- a/Blog/Info.plist +++ b/Blog/Info.plist @@ -30,6 +30,10 @@ armv7 + UIStatusBarStyle + UIStatusBarStyleLightContent + UIViewControllerBasedStatusBarAppearance + UIStatusBarTintParameters UINavigationBar diff --git a/Blog/JSONHTTPClient.m b/Blog/JSONHTTPClient.m index 36a80ff..89e7618 100644 --- a/Blog/JSONHTTPClient.m +++ b/Blog/JSONHTTPClient.m @@ -110,7 +110,7 @@ NSString * const JSONHTTPClientErrorDomain = @"JSONHTTPClientErrorDomain"; [request setValue:headers[key] forHTTPHeaderField:key]; } if (data) { - [request setValue:[NSString stringWithFormat:@"%lu", [data length]] forKey:@"Content-Length"]; + [request setValue:[NSString stringWithFormat:@"%lu", [data length]] forHTTPHeaderField:@"Content-Length"]; [request setHTTPBody:data]; } return request; diff --git a/Blog/MasterViewController.h b/Blog/MasterViewController.h index 997fecd..64b5ad2 100644 --- a/Blog/MasterViewController.h +++ b/Blog/MasterViewController.h @@ -8,7 +8,11 @@ @import UIKit; +@class BlogController; + @interface MasterViewController : UITableViewController +@property (strong, nonatomic) BlogController *blogController; + @end diff --git a/Blog/MasterViewController.m b/Blog/MasterViewController.m index 9e86de2..825df08 100644 --- a/Blog/MasterViewController.m +++ b/Blog/MasterViewController.m @@ -6,15 +6,12 @@ // Copyright (c) 2014 Guru Logic Inc. All rights reserved. // +#import #import "MasterViewController.h" #import "DetailViewController.h" #import "Post.h" #import "BlogController.h" -#import "ModelStore.h" -#import "BlogService.h" -#import "YapDatabaseConnection.h" -#import "YapDatabase.h" -#import "JSONHTTPClient.h" +#import "PostCell.h" @interface MasterViewController () @@ -22,7 +19,6 @@ @property (strong, nonatomic) DetailViewController *detailViewController; @property (strong, nonatomic) IBOutlet UIBarButtonItem *publishButton; @property (strong, nonatomic) IBOutlet UIBarButtonItem *addButton; -@property (strong, nonatomic) BlogController *blogController; @end @@ -34,16 +30,6 @@ self.clearsSelectionOnViewWillAppear = NO; self.preferredContentSize = CGSizeMake(320.0, 600.0); } - NSString *cachesPath = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).lastObject; - NSString *path = [cachesPath stringByAppendingPathComponent:@"blog.sqlite"]; - YapDatabase *database = [[YapDatabase alloc] initWithPath:path]; - YapDatabaseConnection *connection = [database newConnection]; - ModelStore *store = [[ModelStore alloc] initWithConnection:connection]; - NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; - NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration]; - JSONHTTPClient *client = [[JSONHTTPClient alloc] initWithSession:session]; - BlogService *service = [[BlogService alloc] initWithRootURL:@"http://ocean.samhuri.net:6706/" client:client]; - self.blogController = [[BlogController alloc] initWithService:service store:store]; } - (void)viewDidLoad { @@ -65,10 +51,10 @@ // TODO: show a spinner [self.blogController requestDrafts].then(^(NSArray *drafts) { return [self.blogController requestPublishedPosts].then(^(NSArray *posts) { - NSLog(@"drafts = %@", drafts); - NSLog(@"posts = %@", posts); self.posts = [drafts mutableCopy]; - [self.posts addObjectsFromArray:posts]; + for (Post *post in [posts reverseObjectEnumerator]) { + [self.posts addObject:post]; + } [self.tableView reloadData]; }); }); @@ -93,10 +79,11 @@ #pragma mark - Segues - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { - if ([[segue identifier] isEqualToString:@"showDetail"]) { + if ([segue.identifier isEqualToString:@"showDetail"]) { NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow]; Post *post = self.posts[indexPath.row]; DetailViewController *controller = (DetailViewController *)[[segue destinationViewController] topViewController]; + controller.blogController = self.blogController; [controller setPost:post]; controller.navigationItem.leftBarButtonItem = self.splitViewController.displayModeButtonItem; controller.navigationItem.leftItemsSupplementBackButton = YES; @@ -114,12 +101,13 @@ } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { - UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath]; + PostCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath]; Post *post = self.posts[indexPath.row]; // FIXME: unique title - cell.textLabel.text = post.title ?: @"Untitled"; - cell.detailTextLabel.text = post.draft ? @"Draft" : post.formattedDate; + NSString *title = post.title ?: @"Untitled"; + NSString *date = post.draft ? @"Draft" : post.formattedDate; + [cell configureWithTitle:title date:date]; return cell; } diff --git a/Blog/ModelStore.m b/Blog/ModelStore.m index 032f0cc..a194052 100644 --- a/Blog/ModelStore.m +++ b/Blog/ModelStore.m @@ -36,7 +36,12 @@ NSArray *postPaths = [transaction objectForKey:@"drafts" inCollection:@"PostCollection"]; if (postPaths) { [transaction enumerateObjectsForKeys:postPaths inCollection:@"Post" unorderedUsingBlock:^(NSUInteger keyIndex, id object, BOOL *stop) { - [posts addObject:object]; + if (object) { + if (!posts) { + posts = [NSMutableArray new]; + } + [posts addObject:object]; + } }]; } }]; @@ -49,7 +54,12 @@ NSArray *postPaths = [transaction objectForKey:@"published" inCollection:@"PostCollection"]; if (postPaths) { [transaction enumerateObjectsForKeys:postPaths inCollection:@"Post" unorderedUsingBlock:^(NSUInteger keyIndex, id object, BOOL *stop) { - [posts addObject:object]; + if (object) { + if (!posts) { + posts = [NSMutableArray new]; + } + [posts addObject:object]; + } }]; } }]; @@ -90,7 +100,7 @@ NSMutableArray *postIDs = [NSMutableArray array]; for (Post *post in posts) { [transaction setObject:post forKey:post.path inCollection:@"Post"]; - [postIDs addObject:post.objectID]; + [postIDs addObject:post.path]; } [transaction setObject:postIDs forKey:@"drafts" inCollection:@"PostCollection"]; } completionBlock:^{ @@ -105,7 +115,7 @@ NSMutableArray *postIDs = [NSMutableArray array]; for (Post *post in posts) { [transaction setObject:post forKey:post.path inCollection:@"Post"]; - [postIDs addObject:post.objectID]; + [postIDs addObject:post.path]; } [transaction setObject:postIDs forKey:@"published" inCollection:@"PostCollection"]; } completionBlock:^{ diff --git a/Blog/NSDate+marshmallows.h b/Blog/NSDate+marshmallows.h index c10df21..c2bacfd 100644 --- a/Blog/NSDate+marshmallows.h +++ b/Blog/NSDate+marshmallows.h @@ -16,6 +16,7 @@ @property (nonatomic, readonly) NSInteger mm_day; + (NSDate *)mm_dateWithYear:(NSInteger)year month:(NSInteger)month day:(NSInteger)day; ++ (NSDate *)mm_dateWithYear:(NSInteger)year month:(NSInteger)month day:(NSInteger)day hour:(NSInteger)hour minute:(NSInteger)minute second:(NSInteger)second; - (NSString *)mm_relativeToNow; @end diff --git a/Blog/NSDate+marshmallows.m b/Blog/NSDate+marshmallows.m index df53965..cf0c524 100644 --- a/Blog/NSDate+marshmallows.m +++ b/Blog/NSDate+marshmallows.m @@ -18,112 +18,108 @@ @implementation NSDate (Marshmallows) + (NSDate *)mm_dateWithYear:(NSInteger)year month:(NSInteger)month day:(NSInteger)day { - NSCalendar *calendar = [NSCalendar currentCalendar]; - NSDateComponents *components = [NSDateComponents new]; - [components setYear:year]; - [components setMonth:month]; - [components setDay:day]; - return [calendar dateFromComponents:components]; -} + return [self mm_dateWithYear:year month:month day:day hour:0 minute:0 second:0]; +} + ++ (NSDate *)mm_dateWithYear:(NSInteger)year month:(NSInteger)month day:(NSInteger)day hour:(NSInteger)hour minute:(NSInteger)minute second:(NSInteger)second { + return [[NSCalendar currentCalendar] dateWithEra:1 year:year month:month day:day hour:hour minute:minute second:second nanosecond:0]; +} - (NSString *)mm_relativeToNow { double diff = [[NSDate date] timeIntervalSinceDate:self]; - NSString *result = nil; // future if (diff < -2 * YEAR) { - result = [NSString stringWithFormat:@"in %d years", abs(diff / YEAR)]; + return [NSString stringWithFormat:@"in %d years", abs((int)(diff / YEAR))]; } else if (diff < -YEAR) { - result = @"next year"; + return @"next year"; } else if (diff < -8 * WEEK) { - result = [NSString stringWithFormat:@"in %d months", abs(diff / MONTH)]; + return [NSString stringWithFormat:@"in %d months", abs((int)(diff / MONTH))]; } else if (diff < -4 * WEEK) { - result = @"next month"; + return @"next month"; } else if (diff < -2 * WEEK) { - result = [NSString stringWithFormat:@"in %d weeks", abs(diff / WEEK)]; + return [NSString stringWithFormat:@"in %d weeks", abs((int)(diff / WEEK))]; } else if (diff < -WEEK) { - result = @"next week"; + return @"next week"; } else if (diff < -2 * DAY) { - result = [NSString stringWithFormat:@"in %d days", abs(diff / DAY)]; + return [NSString stringWithFormat:@"in %d days", abs((int)(diff / DAY))]; } else if (diff < -DAY) { - result = @"tomorrow"; + return @"tomorrow"; } else if (diff < -2 * HOUR) { - result = [NSString stringWithFormat:@"in %d hours", abs(diff / HOUR)]; + return [NSString stringWithFormat:@"in %d hours", abs((int)(diff / HOUR))]; } else if (diff < -HOUR) { - result = @"in an hour"; + return @"in an hour"; } else if (diff < -2 * MINUTE) { - result = [NSString stringWithFormat:@"in %d minutes", abs(diff / MINUTE)]; + return [NSString stringWithFormat:@"in %d minutes", abs((int)(diff / MINUTE))]; } else if (diff < -MINUTE) { - result = @"in a minute"; + return @"in a minute"; } // present else if (diff < MINUTE) { - result = @"right now"; + return @"right now"; } // past else if (diff < 2 * MINUTE) { - result = @"a minute ago"; + return @"a minute ago"; } else if (diff < HOUR) { - result = [NSString stringWithFormat:@"%d minutes ago", (int)(diff / MINUTE)]; + return [NSString stringWithFormat:@"%d minutes ago", (int)(diff / MINUTE)]; } else if (diff < 2 * HOUR) { - result = @"an hour ago"; + return @"an hour ago"; } else if (diff < DAY) { - result = [NSString stringWithFormat:@"%d hours ago", (int)(diff / HOUR)]; + return [NSString stringWithFormat:@"%d hours ago", (int)(diff / HOUR)]; } else if (diff < 2 * DAY) { - result = @"yesterday"; + return @"yesterday"; } else if (diff < WEEK) { - result = [NSString stringWithFormat:@"%d days ago", (int)(diff / DAY)]; + return [NSString stringWithFormat:@"%d days ago", (int)(diff / DAY)]; } else if (diff < 2 * WEEK) { - result = @"last week"; + return @"last week"; } else if (diff < 4 * WEEK) { - result = [NSString stringWithFormat:@"%d weeks ago", (int)(diff / WEEK)]; + return [NSString stringWithFormat:@"%d weeks ago", (int)(diff / WEEK)]; } else if (diff < 8 * WEEK) { - result = @"last month"; + return @"last month"; } else if (diff < YEAR) { - result = [NSString stringWithFormat:@"%d months ago", (int)(diff / MONTH)]; + return [NSString stringWithFormat:@"%d months ago", (int)(diff / MONTH)]; } else if (diff < 2 * YEAR) { - result = @"last year"; + return @"last year"; } else { - result = [NSString stringWithFormat:@"%d years ago", (int)(diff / YEAR)]; + return [NSString stringWithFormat:@"%d years ago", (int)(diff / YEAR)]; } - - return result; } - (NSInteger)mm_year { - return [[NSCalendar currentCalendar] components:NSCalendarUnitYear fromDate:self].year; + return [[NSCalendar currentCalendar] component:NSCalendarUnitYear fromDate:self]; } - (NSInteger)mm_month { - return [[NSCalendar currentCalendar] components:NSCalendarUnitMonth fromDate:self].month; + return [[NSCalendar currentCalendar] component:NSCalendarUnitMonth fromDate:self]; } - (NSInteger)mm_day { - return [[NSCalendar currentCalendar] components:NSCalendarUnitDay fromDate:self].day; + return [[NSCalendar currentCalendar] component:NSCalendarUnitDay fromDate:self]; } @end diff --git a/Blog/Post.m b/Blog/Post.m index e280141..c3a5970 100644 --- a/Blog/Post.m +++ b/Blog/Post.m @@ -81,7 +81,12 @@ } - (BOOL)isEqualToPost:(Post *)other { - return [self.objectID isEqual:other.objectID]; + return [self.objectID isEqualToString:other.objectID] + && [self.path isEqualToString:other.path] + && [self.title isEqualToString:other.title] + && [self.body isEqualToString:other.body] + && self.draft == other.draft + && ((!self.url && !other.url) || [self.url isEqual:other.url]); } - (BOOL)isEqual:(id)object { @@ -139,11 +144,12 @@ } - (NSString *)path { - if (!_path && self.slug) { + if (!_path) { if (self.draft) { - _path = [NSString stringWithFormat:@"/drafts/%@", self.slug]; + _path = [NSString stringWithFormat:@"/posts/drafts/%@", self.objectID]; } - else if (self.date) { + else { + NSAssert(self.slug && self.date, @"slug and date are required"); NSString *paddedMonth = [self paddedMonthForDate:self.date]; _path = [NSString stringWithFormat:@"/posts/%ld/%@/%@", (long)self.time.mm_year, paddedMonth, self.slug]; } diff --git a/Blog/PostCell.h b/Blog/PostCell.h new file mode 100644 index 0000000..5b5ccca --- /dev/null +++ b/Blog/PostCell.h @@ -0,0 +1,11 @@ +// +// Created by Sami Samhuri on 15-04-19. +// Copyright (c) 2015 Guru Logic Inc. All rights reserved. +// +@import UIKit; + +@interface PostCell : UITableViewCell + +- (void)configureWithTitle:(NSString *)title date:(NSString *)date; + +@end \ No newline at end of file diff --git a/Blog/PostCell.m b/Blog/PostCell.m new file mode 100644 index 0000000..c5aa2c3 --- /dev/null +++ b/Blog/PostCell.m @@ -0,0 +1,21 @@ +// +// Created by Sami Samhuri on 15-04-19. +// Copyright (c) 2015 Guru Logic Inc. All rights reserved. +// +#import "PostCell.h" + +@interface PostCell () + +@property (nonatomic, weak) IBOutlet UILabel *titleLabel; +@property (nonatomic, weak) IBOutlet UILabel *dateLabel; + +@end + +@implementation PostCell + +- (void)configureWithTitle:(NSString *)title date:(NSString *)date { + self.titleLabel.text = title; + self.dateLabel.text = date; +} + +@end \ No newline at end of file diff --git a/Blog/PreviewViewController.h b/Blog/PreviewViewController.h new file mode 100644 index 0000000..f3694fe --- /dev/null +++ b/Blog/PreviewViewController.h @@ -0,0 +1,17 @@ +// +// PreviewViewController.h +// Blog +// +// Created by Sami Samhuri on 2015-04-19. +// Copyright (c) 2015 Guru Logic Inc. All rights reserved. +// + +#import +#import + +@interface PreviewViewController : UIViewController + +@property (nonatomic, strong) NSURLRequest *initialRequest; +@property (nonatomic, strong) PMKPromise *promise; + +@end diff --git a/Blog/PreviewViewController.m b/Blog/PreviewViewController.m new file mode 100644 index 0000000..a2695cf --- /dev/null +++ b/Blog/PreviewViewController.m @@ -0,0 +1,43 @@ +// +// PreviewViewController.m +// Blog +// +// Created by Sami Samhuri on 2015-04-19. +// Copyright (c) 2015 Guru Logic Inc. All rights reserved. +// + +#import "PreviewViewController.h" + +@interface PreviewViewController () + +@property (weak, nonatomic) IBOutlet UIWebView *webView; + +@end + +@implementation PreviewViewController + +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + + if (self.initialRequest) { + if (self.promise) { + self.promise.then(^{ + [self.webView loadRequest:self.initialRequest]; + }).finally(^{ + self.promise = nil; + }); + } + else { + [self.webView loadRequest:self.initialRequest]; + } + } +} + +- (void)setInitialRequest:(NSURLRequest *)initialRequest { + _initialRequest = initialRequest; + [self.webView loadHTMLString:@"" baseURL:nil]; +} + +#pragma mark - UIWebViewDelegate methods + +@end