| 1 | /* |
| 2 | * Flickr.m |
| 3 | * -------- |
| 4 | * Class containing methods to interact with the Flickr web services. |
| 5 | * |
| 6 | * Author: Chris Lee <clee@mg8.org> |
| 7 | * License: GPL v2 <http://www.opensource.org/licenses/gpl-license.php> |
| 8 | */ |
| 9 | |
| 10 | #import <Foundation/Foundation.h> |
| 11 | #import <UIKit/UIAlertSheet.h> |
| 12 | #import "Flickr.h" |
| 13 | #import "MobilePushr.h" |
| 14 | #import "ExtendedAttributes.h" |
| 15 | |
| 16 | #include <unistd.h> |
| 17 | |
| 18 | @class NSXMLNode, NSXMLElement, NSXMLDocument; |
| 19 | |
| 20 | @implementation Flickr |
| 21 | |
| 22 | - (id)initWithPushr: (MobilePushr *)pushr |
| 23 | { |
| 24 | if (![super init]) |
| 25 | return nil; |
| 26 | |
| 27 | _pushr = [pushr retain]; |
| 28 | _settings = [[NSUserDefaults standardUserDefaults] retain]; |
| 29 | |
| 30 | return self; |
| 31 | } |
| 32 | |
| 33 | - (void)dealloc |
| 34 | { |
| 35 | [_pushr release]; |
| 36 | [_settings release]; |
| 37 | [super dealloc]; |
| 38 | } |
| 39 | |
| 40 | #pragma mark UIAlertSheet delegation |
| 41 | - (void)alertSheet: (UIAlertSheet *)sheet buttonClicked: (int)button |
| 42 | { |
| 43 | [sheet dismiss]; |
| 44 | |
| 45 | switch (button) { |
| 46 | case 1: |
| 47 | [_pushr openURL: [self authURL]]; |
| 48 | break; |
| 49 | default: |
| 50 | [_pushr terminate]; |
| 51 | break; |
| 52 | } |
| 53 | } |
| 54 | |
| 55 | #pragma mark XML helper functions |
| 56 | - (BOOL)sanityCheck: (id)responseDocument error: (NSError *)err |
| 57 | { |
| 58 | NSXMLNode *rsp = [[responseDocument children] objectAtIndex: 0]; |
| 59 | if (![[rsp name] isEqualToString: @"rsp"]) { |
| 60 | NSLog(@"This is not an <rsp> tag! Bailing out."); |
| 61 | return FALSE; |
| 62 | } |
| 63 | |
| 64 | id element = [[NSClassFromString(@"NSXMLElement") alloc] initWithXMLString: [rsp XMLString] error: &err]; |
| 65 | if (![[[element attributeForName: @"stat"] stringValue] isEqualToString: @"ok"]) { |
| 66 | NSLog(@"The status is not 'ok', and we have no error recovery."); |
| 67 | NSLog(@"XML: %@", [rsp XMLString]); |
| 68 | [element release]; |
| 69 | return FALSE; |
| 70 | } |
| 71 | |
| 72 | [element release]; |
| 73 | return TRUE; |
| 74 | } |
| 75 | |
| 76 | /* |
| 77 | * Returns an array of XMLNode objects with name matching nodeName. [ken] get is unusual |
| 78 | */ |
| 79 | - (NSArray *)getXMLNodesNamed: (NSString *)nodeName fromResponse: (NSData *)responseData |
| 80 | { |
| 81 | NSError *err = nil; |
| 82 | id responseDoc = [[NSClassFromString(@"NSXMLDocument") alloc] initWithData: responseData options: 0 error: &err]; |
| 83 | if (![self sanityCheck: responseDoc error: err]) { |
| 84 | NSLog(@"Flickr returned an error!"); |
| 85 | [_pushr popupFailureAlertSheet]; |
| 86 | [responseDoc release]; |
| 87 | return nil; |
| 88 | } |
| 89 | |
| 90 | NSMutableArray *matchingNodes = [NSMutableArray array]; |
| 91 | NSArray *nodes = [responseDoc children]; |
| 92 | NSEnumerator *chain = [nodes objectEnumerator]; |
| 93 | NSXMLNode *node = nil; |
| 94 | |
| 95 | while ((node = [chain nextObject])) { |
| 96 | if (![[node name] isEqualToString: nodeName]) { |
| 97 | nodes = [[nodes lastObject] children]; |
| 98 | chain = [nodes objectEnumerator]; |
| 99 | continue; |
| 100 | } |
| 101 | |
| 102 | [matchingNodes addObject: node]; |
| 103 | } |
| 104 | |
| 105 | [responseDoc release]; |
| 106 | |
| 107 | return [NSArray arrayWithArray: matchingNodes]; |
| 108 | } |
| 109 | |
| 110 | /* |
| 111 | * Returns a dictionary filled with the node names, node values, node attribute names, and attribute values. [ken] get is unusual |
| 112 | */ |
| 113 | - (NSDictionary *)getXMLNodesAndAttributesFromResponse: (NSData *)responseData |
| 114 | { |
| 115 | NSError *err = nil; |
| 116 | id responseDoc = [[NSClassFromString(@"NSXMLDocument") alloc] initWithData: responseData options: 0 error: &err]; |
| 117 | if (![self sanityCheck: responseDoc error: err]) { |
| 118 | NSLog(@"Flickr returned an error!"); |
| 119 | [_pushr popupFailureAlertSheet]; |
| 120 | [responseDoc release]; |
| 121 | return nil; |
| 122 | } |
| 123 | |
| 124 | NSMutableDictionary *nodesWithAttributes = [NSMutableDictionary dictionary]; |
| 125 | NSArray *nodes = [responseDoc children]; |
| 126 | NSEnumerator *chain = [nodes objectEnumerator]; |
| 127 | NSXMLNode *node = nil; |
| 128 | |
| 129 | while ((node = [chain nextObject])) { |
| 130 | id element = [[NSClassFromString(@"NSXMLElement") alloc] initWithXMLString: [node XMLString] error: &err]; |
| 131 | if ([[element attributes] count] > 0) { |
| 132 | NSEnumerator *attributeChain = [[element attributes] objectEnumerator]; |
| 133 | id attribute = nil; |
| 134 | while ((attribute = [attributeChain nextObject])) |
| 135 | [nodesWithAttributes setObject: [attribute stringValue] forKey: [NSString stringWithFormat: @"%@%@", [node name], [attribute name]]]; |
| 136 | } |
| 137 | |
| 138 | [nodesWithAttributes setObject: [node stringValue] forKey: [node name]]; |
| 139 | |
| 140 | if ([[node children] count] > 0 && [[[[node children] objectAtIndex: 0] name] length] > 0) { |
| 141 | nodes = [node children]; |
| 142 | chain = [nodes objectEnumerator]; |
| 143 | } |
| 144 | |
| 145 | [element release]; |
| 146 | } |
| 147 | |
| 148 | [responseDoc release]; |
| 149 | |
| 150 | return [NSDictionary dictionaryWithDictionary: nodesWithAttributes]; // [ken] we'd usually just return the mutable dict |
| 151 | } |
| 152 | |
| 153 | #pragma mark internal functions |
| 154 | /* |
| 155 | * Returns a URL with the parameters and values properly appended, including the call signing that Flickr requires from our app. |
| 156 | * |
| 157 | * This method made possible by extending system classes (without having to inherit from them.) Hooray! |
| 158 | */ |
| 159 | - (NSURL *)signedURL: (NSDictionary *)parameters withBase: (NSString *)base |
| 160 | { |
| 161 | NSMutableString *url = [NSMutableString stringWithFormat: @"%@?", base]; |
| 162 | NSMutableString *sig = [NSMutableString stringWithString: PUSHR_SHARED_SECRET]; |
| 163 | |
| 164 | [sig appendString: [[parameters pairsJoinedByString: @""] componentsJoinedByString: @""]]; |
| 165 | [url appendString: [[parameters pairsJoinedByString: @"="] componentsJoinedByString: @"&"]]; |
| 166 | [url appendString: [NSString stringWithFormat: @"&api_sig=%@", [sig md5HexHash]]]; |
| 167 | |
| 168 | return [NSURL URLWithString: url]; |
| 169 | } |
| 170 | |
| 171 | /* |
| 172 | * By default, we want the FLICKR_REST_URL as the base for our calls. |
| 173 | */ |
| 174 | - (NSURL *)signedURL: (NSDictionary *)parameters |
| 175 | { |
| 176 | return [self signedURL: parameters withBase: FLICKR_REST_URL]; |
| 177 | } |
| 178 | |
| 179 | /* |
| 180 | * Returns a one-time-use authorization URL; this URL is a page where the user can tell Flickr to give us permission to upload pictures to their account. |
| 181 | */ |
| 182 | - (NSURL *)authURL |
| 183 | { |
| 184 | NSArray *keys = [NSArray arrayWithObjects: @"api_key", @"perms", @"frob", nil]; |
| 185 | NSArray *vals = [NSArray arrayWithObjects: PUSHR_API_KEY, FLICKR_WRITE_PERMS, [self frob], nil]; |
| 186 | NSDictionary *params = [NSDictionary dictionaryWithObjects: vals forKeys: keys]; |
| 187 | |
| 188 | return [self signedURL: params withBase: FLICKR_AUTH_URL]; |
| 189 | } |
| 190 | |
| 191 | /* |
| 192 | * Get a frob from Flickr, to put in the URL that we send the user to to get their permission to upload pics. |
| 193 | */ |
| 194 | - (NSString *)frob |
| 195 | { |
| 196 | NSArray *keys = [NSArray arrayWithObjects: @"api_key", @"method", nil]; |
| 197 | NSArray *vals = [NSArray arrayWithObjects: PUSHR_API_KEY, FLICKR_GET_FROB, nil]; |
| 198 | NSDictionary *params = [NSDictionary dictionaryWithObjects: vals forKeys: keys]; |
| 199 | |
| 200 | NSURL *url = [self signedURL: params]; |
| 201 | NSData *responseData = [NSData dataWithContentsOfURL: url]; |
| 202 | |
| 203 | NSString *_frob = [[[self getXMLNodesNamed: @"frob" fromResponse: responseData] lastObject] stringValue]; |
| 204 | |
| 205 | [_settings setObject: _frob forKey: @"frob"]; |
| 206 | [_settings synchronize]; |
| 207 | |
| 208 | return [NSString stringWithString: _frob]; |
| 209 | } |
| 210 | |
| 211 | #pragma mark externally-visible interface |
| 212 | /* |
| 213 | * Get the tags the user has already set on their photos. |
| 214 | * TODO: At some point, we should offer a UI to let them tag their future photos with the same tags. |
| 215 | */ |
| 216 | - (NSArray *)tags |
| 217 | { |
| 218 | NSArray *keys = [NSArray arrayWithObjects: @"api_key", @"method", @"user_id", nil]; |
| 219 | NSArray *vals = [NSArray arrayWithObjects: PUSHR_API_KEY, FLICKR_GET_TAGS, [_settings stringForKey: @"nsid"], nil]; |
| 220 | NSDictionary *params = [NSDictionary dictionaryWithObjects: vals forKeys: keys]; |
| 221 | |
| 222 | NSURL *url = [self signedURL: params]; |
| 223 | NSData *responseData = [NSData dataWithContentsOfURL: url]; |
| 224 | NSMutableArray *_tags = [NSMutableArray array]; |
| 225 | |
| 226 | NSEnumerator *iterator = [[self getXMLNodesNamed: @"tag" fromResponse: responseData] objectEnumerator]; |
| 227 | id currentTagNode = nil; |
| 228 | while ((currentTagNode = [iterator nextObject])) |
| 229 | [_tags addObject: [currentTagNode stringValue]]; |
| 230 | |
| 231 | return [NSArray arrayWithArray: _tags]; |
| 232 | } |
| 233 | |
| 234 | /* |
| 235 | * Pop up a dialog so the user can tell Flickr it's cool for us to push pictures to their account. |
| 236 | */ |
| 237 | - (void)sendToGrantPermission |
| 238 | { |
| 239 | UIAlertSheet *alertSheet = [[[UIAlertSheet alloc] initWithFrame: CGRectMake(0.0f, 0.0f, 320.0f, 240.0f)] autorelease]; |
| 240 | [alertSheet setTitle: @"Can't upload to Flickr"]; |
| 241 | [alertSheet setBodyText: @"Pushr needs your permission to upload pictures to Flickr."]; |
| 242 | [alertSheet addButtonWithTitle: @"Proceed"]; |
| 243 | [alertSheet addButtonWithTitle: @"Cancel"]; |
| 244 | [alertSheet setDelegate: self]; |
| 245 | [alertSheet setRunsModal: YES]; |
| 246 | [alertSheet popupAlertAnimated: YES]; |
| 247 | [_settings setBool: TRUE forKey: @"sentToGetToken"]; |
| 248 | } |
| 249 | |
| 250 | /* |
| 251 | * We have a frob that Flickr generated, and we used it in the URL we sent the user to (so that they could give us permission to upload pictures to their account). Now, we assume the user clicked on the 'Okay!' button the page we sent them to go click, and our frob can now be traded for a token. |
| 252 | */ |
| 253 | - (void)tradeFrobForToken |
| 254 | { |
| 255 | NSArray *keys = [NSArray arrayWithObjects: @"api_key", @"method", @"frob", nil]; |
| 256 | NSArray *vals = [NSArray arrayWithObjects: PUSHR_API_KEY, FLICKR_GET_TOKEN, [_settings stringForKey: @"frob"], nil]; |
| 257 | NSDictionary *params = [NSDictionary dictionaryWithObjects: vals forKeys: keys]; |
| 258 | |
| 259 | NSData *responseData = [NSData dataWithContentsOfURL: [self signedURL: params]]; |
| 260 | |
| 261 | NSDictionary *tokenDictionary = [self getXMLNodesAndAttributesFromResponse: responseData]; |
| 262 | NSArray *responseKeys = [tokenDictionary allKeys]; |
| 263 | if (!([responseKeys containsObject: @"token"] && [responseKeys containsObject: @"usernsid"] && [responseKeys containsObject: @"userusername"])) { |
| 264 | NSLog(@"Flickr returned an error!"); |
| 265 | [_settings removeObjectForKey: @"frob"]; |
| 266 | [_settings synchronize]; |
| 267 | [self sendToGrantPermission]; |
| 268 | return; |
| 269 | } |
| 270 | |
| 271 | [_settings setObject: [tokenDictionary objectForKey: @"token"] forKey: @"token"]; |
| 272 | [_settings setObject: [tokenDictionary objectForKey: @"usernsid"] forKey: @"nsid"]; |
| 273 | [_settings setObject: [tokenDictionary objectForKey: @"userusername"] forKey: @"username"]; |
| 274 | [_settings removeObjectForKey: @"frob"]; |
| 275 | [_settings synchronize]; |
| 276 | } |
| 277 | |
| 278 | /* |
| 279 | * We have a token, but is it valid? Maybe the user decided to de-authorize us and we can't push photos to their account anymore. This is how we make sure our token is valid. |
| 280 | */ |
| 281 | - (void)checkToken |
| 282 | { |
| 283 | NSArray *keys = [NSArray arrayWithObjects: @"api_key", @"auth_token", @"method", nil]; |
| 284 | NSArray *vals = [NSArray arrayWithObjects: PUSHR_API_KEY, [_settings stringForKey: @"token"], FLICKR_CHECK_TOKEN, nil]; |
| 285 | NSDictionary *params = [NSDictionary dictionaryWithObjects: vals forKeys: keys]; |
| 286 | NSData *responseData = [NSData dataWithContentsOfURL: [self signedURL: params]]; |
| 287 | NSDictionary *tokenDictionary = [self getXMLNodesAndAttributesFromResponse: responseData]; |
| 288 | NSArray *responseKeys = [tokenDictionary allKeys]; |
| 289 | if (!([responseKeys containsObject: @"token"] && [responseKeys containsObject: @"usernsid"] && [responseKeys containsObject: @"userusername"])) { |
| 290 | NSLog(@"Failed the sanity check when verifying our token. Bailing!"); |
| 291 | [_settings setBool: FALSE forKey: @"sentToGetToken"]; |
| 292 | [self sendToGrantPermission]; |
| 293 | return; |
| 294 | } |
| 295 | |
| 296 | NSLog(@"Well, our token seems good."); |
| 297 | } |
| 298 | |
| 299 | /* |
| 300 | * Takes a JPG file at the specified filesystem path, and uploads it to Flickr using CFNetwork, because there is no way of getting the number of bytes written from an HTTP POST request using the NSHTTP API. |
| 301 | * |
| 302 | * This is, without a doubt, the ugliest code in the entire application. |
| 303 | */ |
| 304 | - (NSString *)pushPhoto: (NSString *)pathToJPG |
| 305 | { |
| 306 | NSString *token = [_settings stringForKey: @"token"]; |
| 307 | NSMutableDictionary *params = [NSMutableDictionary dictionaryWithObjectsAndKeys: PUSHR_API_KEY, @"api_key", token, @"auth_token", nil]; |
| 308 | if ([[ExtendedAttributes allKeysAtPath: pathToJPG] containsObject: NAME_ATTRIBUTE]) |
| 309 | [params setObject: [ExtendedAttributes stringForKey: NAME_ATTRIBUTE atPath: pathToJPG] forKey: @"title"]; |
| 310 | if ([[ExtendedAttributes allKeysAtPath: pathToJPG] containsObject: DESCRIPTION_ATTRIBUTE]) |
| 311 | [params setObject: [ExtendedAttributes stringForKey: DESCRIPTION_ATTRIBUTE atPath: pathToJPG] forKey: @"description"]; |
| 312 | if ([[_settings arrayForKey: @"defaultTags"] count] > 0) |
| 313 | [params setObject: [[_settings arrayForKey: @"defaultTags"] componentsJoinedByString: @" "] forKey: @"tags"]; |
| 314 | if ([[ExtendedAttributes allKeysAtPath: pathToJPG] containsObject: TAGS_ATTRIBUTE]) |
| 315 | [params setObject: [[ExtendedAttributes objectForKey: TAGS_ATTRIBUTE atPath: pathToJPG] componentsJoinedByString: @" "] forKey: @"tags"]; |
| 316 | if ([[_settings arrayForKey: @"defaultPrivacy"] count] > 0 || [[ExtendedAttributes allKeysAtPath: pathToJPG] containsObject: PRIVACY_ATTRIBUTE]) { |
| 317 | NSArray *privacy = [_settings arrayForKey: @"defaultPrivacy"]; |
| 318 | if ([[ExtendedAttributes allKeysAtPath: pathToJPG] containsObject: PRIVACY_ATTRIBUTE]) |
| 319 | privacy = [ExtendedAttributes objectForKey: PRIVACY_ATTRIBUTE atPath: pathToJPG]; |
| 320 | |
| 321 | if ([privacy containsObject: @"Public"]) { |
| 322 | [params setObject: @"1" forKey: @"is_public"]; |
| 323 | } else { |
| 324 | [params setObject: @"0" forKey: @"is_public"]; |
| 325 | if ([privacy containsObject: @"Friends"]) |
| 326 | [params setObject: @"1" forKey: @"is_friend"]; |
| 327 | if ([privacy containsObject: @"Family"]) |
| 328 | [params setObject: @"1" forKey: @"is_family"]; |
| 329 | } |
| 330 | } |
| 331 | NSArray *pairs = [params pairsJoinedByString: @""]; |
| 332 | NSString *api_sig = [NSString stringWithFormat: @"%@%@", PUSHR_SHARED_SECRET, [pairs componentsJoinedByString: @""]]; |
| 333 | [params setObject: [api_sig md5HexHash] forKey: @"api_sig"]; |
| 334 | NSData *jpgData = [NSData dataWithContentsOfFile: pathToJPG]; |
| 335 | [params setObject: jpgData forKey: @"photo"]; |
| 336 | |
| 337 | NSMutableData *body = [[NSMutableData alloc] initWithLength: 0]; |
| 338 | [body appendData: [[[[NSString alloc] initWithFormat: @"--%@\r\n", @MIME_BOUNDARY] autorelease] dataUsingEncoding: NSUTF8StringEncoding]]; |
| 339 | |
| 340 | NSEnumerator *enumerator = [params keyEnumerator]; |
| 341 | id key = nil; |
| 342 | while ((key = [enumerator nextObject])) { |
| 343 | id val = [params objectForKey: key]; |
| 344 | id keyHeader = nil; |
| 345 | if ([key isEqualToString: @"photo"]) { |
| 346 | // If this is the photo... |
| 347 | keyHeader = [[NSString stringWithFormat: @"Content-Disposition: form-data; name=\"photo\"; filename=\"%@\"\r\nContent-Type: image/jpeg\r\n\r\n", pathToJPG] dataUsingEncoding: NSUTF8StringEncoding]; |
| 348 | [body appendData: keyHeader]; |
| 349 | [body appendData: val]; |
| 350 | } else { |
| 351 | // Treat all other values as strings. |
| 352 | keyHeader = [NSString stringWithFormat: @"Content-Disposition: form-data; name=\"%@\"\r\n\r\n", key]; |
| 353 | [body appendData: [keyHeader dataUsingEncoding: NSUTF8StringEncoding]]; |
| 354 | [body appendData: [val dataUsingEncoding: NSUTF8StringEncoding]]; |
| 355 | } |
| 356 | [body appendData: [[NSString stringWithFormat: @"\r\n--%@\r\n", @MIME_BOUNDARY] dataUsingEncoding: NSUTF8StringEncoding]]; |
| 357 | } |
| 358 | |
| 359 | [body appendData: [[NSString stringWithString: @"--\r\n"] dataUsingEncoding: NSUTF8StringEncoding]]; |
| 360 | |
| 361 | CFURLRef _uploadURL = CFURLCreateWithString(kCFAllocatorDefault, (CFStringRef)FLICKR_UPLOAD_URL, NULL); |
| 362 | CFHTTPMessageRef _request = CFHTTPMessageCreateRequest(kCFAllocatorDefault, CFSTR("POST"), _uploadURL, kCFHTTPVersion1_1); |
| 363 | CFRelease(_uploadURL); |
| 364 | _uploadURL = NULL; |
| 365 | |
| 366 | CFHTTPMessageSetHeaderFieldValue(_request, CFSTR("Content-Type"), CFSTR(CONTENT_TYPE)); |
| 367 | CFHTTPMessageSetHeaderFieldValue(_request, CFSTR("Host"), CFSTR("api.flickr.com")); |
| 368 | CFHTTPMessageSetHeaderFieldValue(_request, CFSTR("Content-Length"), (CFStringRef)[NSString stringWithFormat: @"%d", [body length]]); |
| 369 | CFHTTPMessageSetBody(_request, (CFDataRef)body); |
| 370 | |
| 371 | CFReadStreamRef _readStream = CFReadStreamCreateForHTTPRequest(kCFAllocatorDefault, _request); |
| 372 | CFReadStreamOpen(_readStream); |
| 373 | |
| 374 | NSMutableString *responseString = [NSMutableString string]; |
| 375 | CFIndex numBytesRead; |
| 376 | long bytesWritten, previousBytesWritten = 0; |
| 377 | UInt8 buf[1024]; |
| 378 | BOOL doneUploading = NO; |
| 379 | |
| 380 | while (!doneUploading) { |
| 381 | CFNumberRef cfSize = CFReadStreamCopyProperty(_readStream, kCFStreamPropertyHTTPRequestBytesWrittenCount); |
| 382 | CFNumberGetValue(cfSize, kCFNumberLongType, &bytesWritten); |
| 383 | CFRelease(cfSize); |
| 384 | cfSize = NULL; |
| 385 | |
| 386 | if (bytesWritten > previousBytesWritten) { |
| 387 | previousBytesWritten = bytesWritten; |
| 388 | NSNumber *progress = [NSNumber numberWithFloat: ((float)bytesWritten / (float)[body length])]; |
| 389 | [_pushr performSelectorOnMainThread: @selector(updateProgress:) withObject: progress waitUntilDone: YES]; |
| 390 | } |
| 391 | |
| 392 | if (!CFReadStreamHasBytesAvailable(_readStream)) { |
| 393 | usleep(3600); |
| 394 | continue; |
| 395 | } |
| 396 | |
| 397 | numBytesRead = CFReadStreamRead(_readStream, buf, 1024); |
| 398 | if (numBytesRead < 1024) |
| 399 | buf[numBytesRead] = 0; |
| 400 | [responseString appendFormat: @"%s", buf]; |
| 401 | |
| 402 | if (CFReadStreamGetStatus(_readStream) == kCFStreamStatusAtEnd) doneUploading = YES; |
| 403 | } |
| 404 | [body release]; |
| 405 | |
| 406 | CFReadStreamClose(_readStream); |
| 407 | CFRelease(_request); |
| 408 | _request = NULL; |
| 409 | CFRelease(_readStream); |
| 410 | _readStream = NULL; |
| 411 | |
| 412 | return [NSString stringWithString: responseString]; |
| 413 | } |
| 414 | |
| 415 | /* |
| 416 | * When the user clicks on the 'Push to Flickr' button, push the photos that haven't been pushed yet, and pass the XML for the responses back to the main class when finished. |
| 417 | */ |
| 418 | - (void)triggerUpload: (id)photos |
| 419 | { |
| 420 | NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; |
| 421 | NSMutableArray *responses = [NSMutableArray array]; |
| 422 | |
| 423 | NSEnumerator *enumerator = [photos objectEnumerator]; |
| 424 | id photo = nil; |
| 425 | while ((photo = [enumerator nextObject])) { |
| 426 | [_pushr performSelectorOnMainThread: @selector(startingToPush:) withObject: photo waitUntilDone: NO]; |
| 427 | [responses addObject: [self pushPhoto: photo]]; |
| 428 | [ExtendedAttributes setString: @"true" forKey: PUSHED_ATTRIBUTE atPath: photo]; |
| 429 | [_pushr performSelectorOnMainThread: @selector(donePushing:) withObject: photo waitUntilDone: NO]; |
| 430 | } |
| 431 | |
| 432 | [_pushr performSelectorOnMainThread: @selector(allDone:) withObject: responses waitUntilDone: YES]; |
| 433 | [pool release]; |
| 434 | } |
| 435 | |
| 436 | @end |
| 437 | |