#import #import #import #import "MVFileTransferController.h" #import "MVBuddyListController.h" #import "JVBuddy.h" #import "JVDetailCell.h" static MVFileTransferController *sharedInstance = nil; static NSString *MVToolbarStopItemIdentifier = @"MVToolbarStopItem"; static NSString *MVToolbarRevealItemIdentifier = @"MVToolbarRevealItem"; static NSString *MVToolbarClearItemIdentifier = @"MVToolbarClearItem"; NSString *MVPrettyFileSize( unsigned long long size ) { NSString *ret = nil; if( size == 0. ) ret = NSLocalizedString( @"Zero bytes", "no file size" ); else if( size > 0. && size < 1024. ) ret = [NSString stringWithFormat:NSLocalizedString( @"%lu bytes", "file size measured in bytes" ), size]; else if( size >= 1024. && size < pow( 1024., 2. ) ) ret = [NSString stringWithFormat:NSLocalizedString( @"%.1f KB", "file size measured in kilobytes" ), ( size / 1024. )]; else if( size >= pow( 1024., 2. ) && size < pow( 1024., 3. ) ) ret = [NSString stringWithFormat:NSLocalizedString( @"%.2f MB", "file size measured in megabytes" ), ( size / pow( 1024., 2. ) )]; else if( size >= pow( 1024., 3. ) && size < pow( 1024., 4. ) ) ret = [NSString stringWithFormat:NSLocalizedString( @"%.3f GB", "file size measured in gigabytes" ), ( size / pow( 1024., 3. ) )]; else if( size >= pow( 1024., 4. ) ) ret = [NSString stringWithFormat:NSLocalizedString( @"%.4f TB", "file size measured in terabytes" ), ( size / pow( 1024., 4. ) )]; return [[ret retain] autorelease]; } NSString *MVReadableTime( NSTimeInterval date, BOOL longFormat ) { NSTimeInterval secs = [[NSDate date] timeIntervalSince1970] - date; unsigned int i = 0, stop = 0; NSDictionary *desc = [NSDictionary dictionaryWithObjectsAndKeys:NSLocalizedString( @"second", "singular second" ), [NSNumber numberWithUnsignedInt:1], NSLocalizedString( @"minute", "singular minute" ), [NSNumber numberWithUnsignedInt:60], NSLocalizedString( @"hour", "singular hour" ), [NSNumber numberWithUnsignedInt:3600], NSLocalizedString( @"day", "singular day" ), [NSNumber numberWithUnsignedInt:86400], NSLocalizedString( @"week", "singular week" ), [NSNumber numberWithUnsignedInt:604800], NSLocalizedString( @"month", "singular month" ), [NSNumber numberWithUnsignedInt:2628000], NSLocalizedString( @"year", "singular year" ), [NSNumber numberWithUnsignedInt:31536000], nil]; NSDictionary *plural = [NSDictionary dictionaryWithObjectsAndKeys:NSLocalizedString( @"seconds", "plural seconds" ), [NSNumber numberWithUnsignedInt:1], NSLocalizedString( @"minutes", "plural minutes" ), [NSNumber numberWithUnsignedInt:60], NSLocalizedString( @"hours", "plural hours" ), [NSNumber numberWithUnsignedInt:3600], NSLocalizedString( @"days", "plural days" ), [NSNumber numberWithUnsignedInt:86400], NSLocalizedString( @"weeks", "plural weeks" ), [NSNumber numberWithUnsignedInt:604800], NSLocalizedString( @"months", "plural months" ), [NSNumber numberWithUnsignedInt:2628000], NSLocalizedString( @"years", "plural years" ), [NSNumber numberWithUnsignedInt:31536000], nil]; NSDictionary *use = nil; NSMutableArray *breaks = nil; unsigned int val = 0.; NSString *retval = nil; if( secs < 0 ) secs *= -1; breaks = [[[desc allKeys] mutableCopy] autorelease]; [breaks sortUsingSelector:@selector( compare: )]; while( i < [breaks count] && secs >= (NSTimeInterval) [[breaks objectAtIndex:i] unsignedIntValue] ) i++; if( i > 0 ) i--; stop = [[breaks objectAtIndex:i] unsignedIntValue]; val = (unsigned int) ( secs / stop ); use = ( val > 1 ? plural : desc ); retval = [NSString stringWithFormat:@"%d %@", val, [use objectForKey:[NSNumber numberWithUnsignedInt:stop]]]; if( longFormat && i > 0 ) { unsigned int rest = (unsigned int) ( (unsigned int) secs % stop ); stop = [[breaks objectAtIndex:--i] unsignedIntValue]; rest = (unsigned int) ( rest / stop ); if( rest > 0 ) { use = ( rest > 1 ? plural : desc ); retval = [retval stringByAppendingFormat:@" %d %@", rest, [use objectForKey:[breaks objectAtIndex:i]]]; } } return [[retval retain] autorelease]; } #pragma mark - @interface MVFileTransferController (MVFileTransferControllerPrivate) - (void) _updateProgress:(id) sender; - (void) _incomingFileSheetDidEnd:(NSWindow *) sheet returnCode:(int) returnCode contextInfo:(void *) contextInfo; - (void) _incomingFileSavePanelDidEnd:(NSSavePanel *) sheet returnCode:(int) returnCode contextInfo:(void *) contextInfo; - (void) _downloadFileSavePanelDidEnd:(NSSavePanel *) sheet returnCode:(int) returnCode contextInfo:(void *) contextInfo; - (NSMutableDictionary *) _infoForTransferAtIndex:(unsigned int) index; @end #pragma mark - @implementation MVFileTransferController + (NSString *) userPreferredDownloadFolder { OSStatus err = noErr; ICInstance inst = NULL; ICFileSpec folder; long length = kICFileSpecHeaderSize; FSRef ref; char path[1024]; memset( path, 0, 1024 ); if( ( err = ICStart( &inst, 'coRC' ) ) != noErr ) goto finish; ICGetPref( inst, kICDownloadFolder, NULL, &folder, &length ); ICStop( inst ); if( ( err = FSpMakeFSRef( &folder.fss, &ref ) ) != noErr ) goto finish; if( ( err = FSRefMakePath( &ref, (unsigned char *)path, 1024 ) ) != noErr ) goto finish; finish: if( ! strlen( path ) ) return [@"~/Desktop" stringByExpandingTildeInPath]; return [NSString stringWithUTF8String:path]; } + (void) setUserPreferredDownloadFolder:(NSString *) path { OSStatus err = noErr; ICInstance inst = NULL; ICFileSpec *dir = NULL; FSRef ref; AliasHandle alias; unsigned long length = 0; if( ( err = FSPathMakeRef( (unsigned char *)[path UTF8String], &ref, NULL ) ) != noErr ) return; if( ( err = FSNewAliasMinimal( &ref, &alias ) ) != noErr ) return; length = ( kICFileSpecHeaderSize + GetHandleSize( (Handle) alias ) ); dir = malloc( length ); memset( dir, 0, length ); if( ( err = FSGetCatalogInfo( &ref, kFSCatInfoNone, NULL, NULL, &dir -> fss, NULL ) ) != noErr ) return; memcpy( &dir -> alias, *alias, length - kICFileSpecHeaderSize ); if( ( err = ICStart( &inst, 'coRC' ) ) != noErr ) return; ICSetPref( inst, kICDownloadFolder, kICAttrNoChange, dir, length ); ICStop( inst ); free( dir ); DisposeHandle( (Handle) alias ); } #pragma mark - + (MVFileTransferController *) defaultController { extern MVFileTransferController *sharedInstance; return ( sharedInstance ? sharedInstance : ( sharedInstance = [[self alloc] initWithWindowNibName:nil] ) ); } + (void) _setFileTransferPortRange { NSRange range = NSRangeFromString( [[NSUserDefaults standardUserDefaults] stringForKey:@"JVFileTransferPortRange"] ); [MVFileTransfer setFileTransferPortRange:range]; } #pragma mark - - (id) initWithWindowNibName:(NSString *) windowNibName { if( ( self = [super initWithWindowNibName:@"MVFileTransfer"] ) ) { _transferStorage = [[NSMutableArray array] retain]; _calculationItems = [[NSMutableArray array] retain]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector( _incomingFile: ) name:MVDownloadFileTransferOfferNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector( _fileTransferStarted: ) name:MVFileTransferStartedNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector( _fileTransferFinished: ) name:MVFileTransferFinishedNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector( _fileTransferError: ) name:MVFileTransferErrorOccurredNotification object:nil]; [[self class] performSelector:@selector( _setFileTransferPortRange ) withObject:nil afterDelay:2.]; _safeFileExtentions = [[NSSet setWithObjects:@"jpg",@"jpeg",@"gif",@"png",@"tif",@"tiff",@"psd",@"pdf",@"txt",@"rtf",@"html",@"htm",@"swf",@"mp3",@"wma",@"wmv",@"ogg",@"ogm",@"mov",@"mpg",@"mpeg",@"m1v",@"m2v",@"mp4",@"avi",@"vob",@"avi",@"asx",@"asf",@"pls",@"m3u",@"rmp",@"aif",@"aiff",@"aifc",@"wav",@"wave",@"m4a",@"m4p",@"m4b",@"dmg",@"udif",@"ndif",@"dart",@"sparseimage",@"cdr",@"dvdr",@"iso",@"img",@"toast",@"rar",@"sit",@"sitx",@"bin",@"hqx",@"zip",@"gz",@"tgz",@"tar",@"bz",@"bz2",@"tbz",@"z",@"taz",@"uu",@"uue",@"colloquytranscript",@"torrent",nil] retain]; _updateTimer = [[NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector( _updateProgress: ) userInfo:nil repeats:YES] retain]; } return self; } - (void) release { if( ( [self retainCount] - 1 ) == 1 ) [_updateTimer invalidate]; [super release]; } - (void) dealloc { extern MVFileTransferController *sharedInstance; [[NSNotificationCenter defaultCenter] removeObserver:self]; if( self == sharedInstance ) sharedInstance = nil; [_transferStorage release]; [_safeFileExtentions release]; [_calculationItems release]; [_updateTimer release]; _transferStorage = nil; _safeFileExtentions = nil; _calculationItems = nil; _updateTimer = nil; [super dealloc]; } - (void) windowDidLoad { NSToolbar *toolbar = [[[NSToolbar alloc] initWithIdentifier:@"Transfers"] autorelease]; NSTableColumn *theColumn = nil; [(NSPanel *)[self window] setFloatingPanel:NO]; [[self window] setHidesOnDeactivate:NO]; [[self window] setResizeIncrements:NSMakeSize( 1, [currentFiles rowHeight] + [currentFiles intercellSpacing].height )]; [currentFiles setVerticalMotionCanBeginDrag:NO]; [currentFiles setDoubleAction:@selector( _openFile: )]; [currentFiles setAutosaveName:@"Transfers"]; [currentFiles setAutosaveTableColumns:YES]; theColumn = [currentFiles tableColumnWithIdentifier:@"file"]; JVDetailCell *prototypeCell = [[JVDetailCell new] autorelease]; [prototypeCell setFont:[NSFont systemFontOfSize:11.]]; [prototypeCell setLineBreakMode:NSLineBreakByTruncatingMiddle]; [theColumn setDataCell:prototypeCell]; theColumn = [currentFiles tableColumnWithIdentifier:@"status"]; [[theColumn headerCell] setImage:[NSImage imageNamed:@"statusHeader"]]; [toolbar setDelegate:self]; [toolbar setAllowsUserCustomization:YES]; [toolbar setAutosavesConfiguration:YES]; [[self window] setToolbar:toolbar]; [progressBar setMinValue:0.]; [progressBar setUsesThreadedAnimation:YES]; [self _updateProgress:nil]; [self setWindowFrameAutosaveName:@"Transfers"]; } #pragma mark - - (IBAction) showTransferManager:(id) sender { [[self window] makeKeyAndOrderFront:nil]; } - (IBAction) hideTransferManager:(id) sender { [[self window] orderOut:nil]; } #pragma mark - - (void) downloadFileAtURL:(NSURL *) url toLocalFile:(NSString *) path { WebDownload *download = [[[WebDownload alloc] initWithRequest:[NSURLRequest requestWithURL:url] delegate:self] autorelease]; if( ! download ) { NSBeginAlertSheet( @"Invalid URL", nil, nil, nil, [self window], nil, nil, nil, nil, @"The download URL is either invalid or unsupported." ); return; } [self showTransferManager:nil]; if( path ) [download setDestination:path allowOverwrite:NO]; NSMutableDictionary *info = [NSMutableDictionary dictionary]; [info setObject:[NSNumber numberWithUnsignedLong:0] forKey:@"transfered"]; [info setObject:[NSNumber numberWithUnsignedInt:0] forKey:@"rate"]; [info setObject:[NSNumber numberWithUnsignedLong:0] forKey:@"size"]; [info setObject:download forKey:@"controller"]; [info setObject:url forKey:@"url"]; if( path ) [info setObject:path forKey:@"path"]; else [info setObject:[[url path] lastPathComponent] forKey:@"path"]; [_transferStorage addObject:info]; [currentFiles reloadData]; [self showTransferManager:nil]; } - (void) addFileTransfer:(id) transfer { NSEnumerator *enumerator = nil; NSMutableDictionary *info = nil; NSParameterAssert( transfer != nil ); enumerator = [_transferStorage objectEnumerator]; while( ( info = [enumerator nextObject] ) ) if( [[info objectForKey:@"transfer"] isEqualTo:transfer] ) return; info = [NSMutableDictionary dictionary]; [info setObject:[NSNumber numberWithUnsignedInt:0] forKey:@"rate"]; [info setObject:[NSNumber numberWithUnsignedInt:[transfer status]] forKey:@"status"]; [info setObject:[NSNumber numberWithUnsignedLongLong:[transfer finalSize]] forKey:@"size"]; if( [transfer isDownload] ) [info setObject:[(MVDownloadFileTransfer *)transfer destination] forKey:@"path"]; else if( [transfer isUpload] ) [info setObject:[(MVUploadFileTransfer *)transfer source] forKey:@"path"]; [info setObject:transfer forKey:@"controller"]; [_transferStorage addObject:info]; [currentFiles reloadData]; [self showTransferManager:nil]; } #pragma mark - - (IBAction) stopSelectedTransfer:(id) sender { NSMutableDictionary *info = nil; if( [currentFiles selectedRow] != -1 ) { info = [self _infoForTransferAtIndex:[currentFiles selectedRow]]; [[info objectForKey:@"controller"] cancel]; [info setObject:[NSNumber numberWithUnsignedInt:MVFileTransferStoppedStatus] forKey:@"status"]; } [currentFiles reloadData]; [self _updateProgress:nil]; } - (IBAction) clearFinishedTransfers:(id) sender { unsigned i = 0; NSDictionary *info = nil; if( [currentFiles selectedRow] == -1 ) { for( i = 0; i < [_transferStorage count]; ) { info = [self _infoForTransferAtIndex:i]; unsigned int status = [[info objectForKey:@"status"] unsignedIntValue]; if( status == MVFileTransferDoneStatus || status == MVFileTransferErrorStatus || status == MVFileTransferStoppedStatus ) { [_calculationItems removeObject:info]; [_transferStorage removeObject:info]; [currentFiles reloadData]; } else i++; } } else if( [currentFiles numberOfSelectedRows] == 1 ) { info = [self _infoForTransferAtIndex:[currentFiles selectedRow]]; unsigned int status = [[info objectForKey:@"status"] unsignedIntValue]; if( status == MVFileTransferDoneStatus || status == MVFileTransferErrorStatus || status == MVFileTransferStoppedStatus ) { [_calculationItems removeObject:info]; [_transferStorage removeObject:info]; [currentFiles reloadData]; } } [self _updateProgress:nil]; } - (IBAction) revealSelectedFile:(id) sender { NSDictionary *info = nil; if( [currentFiles numberOfSelectedRows] == 1 ) { info = [self _infoForTransferAtIndex:[currentFiles selectedRow]]; [[NSWorkspace sharedWorkspace] selectFile:[info objectForKey:@"path"] inFileViewerRootedAtPath:@""]; } } #pragma mark - - (IBAction) copy:(id) sender { NSEnumerator *enumerator = [currentFiles selectedRowEnumerator]; NSMutableArray *array = [NSMutableArray array]; NSMutableString *string = [NSMutableString string]; id row = nil; unsigned i = 0; [[NSPasteboard generalPasteboard] declareTypes:[NSArray arrayWithObjects:NSFilenamesPboardType,NSStringPboardType,nil] owner:self]; while( ( row = [enumerator nextObject] ) ) { i = [row unsignedIntValue]; [array addObject:[[self _infoForTransferAtIndex:i] objectForKey:@"path"]]; [string appendString:[[[self _infoForTransferAtIndex:i] objectForKey:@"path"] lastPathComponent]]; if( ! [[[enumerator allObjects] lastObject] isEqual:row] ) [string appendString:@"\n"]; } [[NSPasteboard generalPasteboard] setPropertyList:array forType:NSFilenamesPboardType]; [[NSPasteboard generalPasteboard] setString:string forType:NSStringPboardType]; } @end #pragma mark - @implementation MVFileTransferController (MVFileTransferControllerDelegate) #pragma mark Table View Support - (int) numberOfRowsInTableView:(NSTableView *) view { return [_transferStorage count]; } - (id) tableView:(NSTableView *) view objectValueForTableColumn:(NSTableColumn *) column row:(int) row { if( [[column identifier] isEqual:@"file"] ) { NSString *path = [[self _infoForTransferAtIndex:row] objectForKey:@"path"]; NSImage *fileIcon = [[NSWorkspace sharedWorkspace] iconForFileType:[path pathExtension]]; [fileIcon setScalesWhenResized:YES]; [fileIcon setSize:NSMakeSize( 16., 16. )]; return fileIcon; } else if( [[column identifier] isEqual:@"size"] ) { unsigned long long size = [[[self _infoForTransferAtIndex:row] objectForKey:@"size"] unsignedLongLongValue]; return ( size ? MVPrettyFileSize( size ) : @"--" ); } else if( [[column identifier] isEqual:@"user"] ) { NSString *ret = [[[self _infoForTransferAtIndex:row] objectForKey:@"user"] displayName]; return ( ret ? ret : NSLocalizedString( @"n/a", "not applicable identifier" ) ); } return nil; } - (void) tableView:(NSTableView *) view willDisplayCell:(id) cell forTableColumn:(NSTableColumn *) column row:(int) row { if( [[column identifier] isEqual:@"file"] ) { NSString *path = [[self _infoForTransferAtIndex:row] objectForKey:@"path"]; [cell setMainText:[[NSFileManager defaultManager] displayNameAtPath:path]]; } else if( [[column identifier] isEqual:@"status"] ) { id controller = [[self _infoForTransferAtIndex:row] objectForKey:@"controller"]; MVFileTransferStatus status = (MVFileTransferStatus) [[[self _infoForTransferAtIndex:row] objectForKey:@"status"] unsignedIntValue]; NSString *imageName = @"pending"; if( status == MVFileTransferErrorStatus ) imageName = @"error"; else if( status == MVFileTransferStoppedStatus ) imageName = @"stopped"; else if( status == MVFileTransferDoneStatus ) imageName = @"done"; else if( status == MVFileTransferNormalStatus && [controller isKindOfClass:[MVUploadFileTransfer class]] ) imageName = @"upload"; else if( status == MVFileTransferNormalStatus && ( [controller isKindOfClass:[MVDownloadFileTransfer class]] || [controller isKindOfClass:[WebDownload class]] ) ) imageName = @"download"; [cell setImage:[NSImage imageNamed:imageName]]; } } - (void) tableViewSelectionDidChange:(NSNotification *) notification { NSEnumerator *enumerator = nil; NSToolbarItem *item = nil; BOOL noneSelected = YES; enumerator = [[[[self window] toolbar] visibleItems] objectEnumerator]; if( [currentFiles selectedRow] != -1 ) noneSelected = NO; while( ( item = [enumerator nextObject] ) ) { if( [[item itemIdentifier] isEqual:MVToolbarStopItemIdentifier] ) { if( ! noneSelected && [currentFiles numberOfSelectedRows] == 1 && [[[self _infoForTransferAtIndex:[currentFiles selectedRow]] objectForKey:@"status"] unsignedIntValue] != MVFileTransferDoneStatus ) [item setAction:@selector( stopSelectedTransfer: )]; else [item setAction:NULL]; } else if( [[item itemIdentifier] isEqual:MVToolbarRevealItemIdentifier] ) { if( ! noneSelected && [currentFiles numberOfSelectedRows] == 1 ) [item setAction:@selector( revealSelectedFile: )]; else [item setAction:NULL]; } else if( [[item itemIdentifier] isEqual:MVToolbarClearItemIdentifier] ) { if( ! noneSelected && [currentFiles numberOfSelectedRows] == 1 && [[[self _infoForTransferAtIndex:[currentFiles selectedRow]] objectForKey:@"status"] unsignedIntValue] != MVFileTransferNormalStatus && [[[self _infoForTransferAtIndex:[currentFiles selectedRow]] objectForKey:@"status"] unsignedIntValue] != MVFileTransferHoldingStatus ) [item setAction:@selector( clearFinishedTransfers: )]; else if( noneSelected ) [item setAction:@selector( clearFinishedTransfers: )]; else [item setAction:NULL]; } } enumerator = [currentFiles selectedRowEnumerator]; [_calculationItems removeAllObjects]; id fileItem = nil; while( ( fileItem = [enumerator nextObject] ) ) [_calculationItems addObject:[self _infoForTransferAtIndex:[fileItem unsignedIntValue]]]; } - (BOOL) tableView:(NSTableView *) view writeRows:(NSArray *) rows toPasteboard:(NSPasteboard *) board { NSEnumerator *enumerator = [rows objectEnumerator]; NSMutableArray *array = [NSMutableArray array]; id row = nil; [board declareTypes:[NSArray arrayWithObjects:NSFilenamesPboardType,nil] owner:self]; while( ( row = [enumerator nextObject] ) ) { NSString *path = [[self _infoForTransferAtIndex:[row unsignedIntValue]] objectForKey:@"path"]; if( path ) [array addObject:path]; } [board setPropertyList:array forType:NSFilenamesPboardType]; return YES; } #pragma mark - #pragma mark Toolbar Support - (NSToolbarItem *) toolbar:(NSToolbar *) toolbar itemForItemIdentifier:(NSString *) itemIdent willBeInsertedIntoToolbar:(BOOL) willBeInserted { NSToolbarItem *toolbarItem = [[[NSToolbarItem alloc] initWithItemIdentifier:itemIdent] autorelease]; if( [itemIdent isEqual:MVToolbarStopItemIdentifier] ) { [toolbarItem setLabel:NSLocalizedString( @"Stop", "short toolbar stop button name" )]; [toolbarItem setPaletteLabel:NSLocalizedString( @"Stop Tranfser", "name for stop button in customize palette" )]; [toolbarItem setToolTip:NSLocalizedString( @"Stop File Tranfser", "stop button tooltip" )]; [toolbarItem setImage:[NSImage imageNamed:@"stop"]]; [toolbarItem setTarget:self]; [toolbarItem setAction:NULL]; } else if( [itemIdent isEqual:MVToolbarRevealItemIdentifier] ) { [toolbarItem setLabel:NSLocalizedString( @"Reveal", "show file in Finder toolbar button name" )]; [toolbarItem setPaletteLabel:NSLocalizedString( @"Reveal File", "show file in Finder toolbar customize palette name" )]; [toolbarItem setToolTip:NSLocalizedString( @"Reveal File in Finder", "reveal button tooltip" )]; [toolbarItem setImage:[NSImage imageNamed:@"reveal"]]; [toolbarItem setTarget:self]; [toolbarItem setAction:NULL]; } else if( [itemIdent isEqual:MVToolbarClearItemIdentifier] ) { [toolbarItem setLabel:NSLocalizedString( @"Clear", "clear finished transfers toolbar button name" )]; [toolbarItem setPaletteLabel:NSLocalizedString( @"Clear Finished", "clear finished transfers toolbar customize palette name" )]; [toolbarItem setToolTip:NSLocalizedString( @"Clear Finished Transfers", "clear finished transfers tooltip" )]; [toolbarItem setImage:[NSImage imageNamed:@"clear"]]; [toolbarItem setTarget:self]; [toolbarItem setAction:@selector( clearFinishedTransfers: )]; } else toolbarItem = nil; return toolbarItem; } - (NSArray *) toolbarDefaultItemIdentifiers:(NSToolbar *) toolbar { return [NSArray arrayWithObjects:MVToolbarStopItemIdentifier, MVToolbarClearItemIdentifier, NSToolbarSeparatorItemIdentifier, MVToolbarRevealItemIdentifier, nil]; } - (NSArray *) toolbarAllowedItemIdentifiers:(NSToolbar *) toolbar { return [NSArray arrayWithObjects:MVToolbarStopItemIdentifier, MVToolbarClearItemIdentifier, MVToolbarRevealItemIdentifier, NSToolbarCustomizeToolbarItemIdentifier, NSToolbarFlexibleSpaceItemIdentifier, NSToolbarSpaceItemIdentifier, NSToolbarSeparatorItemIdentifier, nil]; } #pragma mark - #pragma mark Save Panel Trickery - (NSString *) panel:(id) sender userEnteredFilename:(NSString *) filename confirmed:(BOOL) confirmed { return ( confirmed ? [filename stringByAppendingString:@".colloquyFake"] : filename ); } #pragma mark - #pragma mark URL Web Download Support - (void) download:(NSURLDownload *) download decideDestinationWithSuggestedFilename:(NSString *) filename { if( ! [[NSUserDefaults standardUserDefaults] boolForKey:@"JVAskForTransferSaveLocation"] ) { NSString *path = [[[self class] userPreferredDownloadFolder] stringByAppendingPathComponent:filename]; NSEnumerator *enumerator = nil; NSMutableDictionary *info = nil; enumerator = [_transferStorage objectEnumerator]; while( ( info = [enumerator nextObject] ) ) { if( [info objectForKey:@"controller"] == download ) { [info setObject:path forKey:@"path"]; break; } } [self _downloadFileSavePanelDidEnd:nil returnCode:NSOKButton contextInfo:(void *)[download retain]]; } else { NSSavePanel *savePanel = [[NSSavePanel savePanel] retain]; [savePanel beginSheetForDirectory:[[self class] userPreferredDownloadFolder] file:filename modalForWindow:nil modalDelegate:self didEndSelector:@selector( _downloadFileSavePanelDidEnd:returnCode:contextInfo: ) contextInfo:(void *)[download retain]]; } } - (void) download:(NSURLDownload *) download didReceiveResponse:(NSURLResponse *) response { NSEnumerator *enumerator = nil; NSMutableDictionary *info = nil; enumerator = [_transferStorage objectEnumerator]; while( ( info = [enumerator nextObject] ) ) { if( [info objectForKey:@"controller"] == download ) { [info setObject:[NSNumber numberWithUnsignedLong:0] forKey:@"transfered"]; unsigned long size = [response expectedContentLength]; if( (long)size == -1 ) size = 0; [info setObject:[NSNumber numberWithUnsignedLongLong:size] forKey:@"size"]; [currentFiles reloadData]; break; } } } - (NSWindow *) downloadWindowForAuthenticationSheet:(WebDownload *) download { return [self window]; } - (void) download:(NSURLDownload *) download didReceiveDataOfLength:(unsigned) length { NSEnumerator *enumerator = nil; NSMutableDictionary *info = nil; enumerator = [_transferStorage objectEnumerator]; while( ( info = [enumerator nextObject] ) ) { if( [info objectForKey:@"controller"] == download ) { NSTimeInterval timeslice = [[info objectForKey:@"started"] timeIntervalSinceNow] * -1; unsigned long long transfered = [[info objectForKey:@"transfered"] unsignedIntValue] + length; [info setObject:[NSNumber numberWithUnsignedInt:MVFileTransferNormalStatus] forKey:@"status"]; [info setObject:[NSNumber numberWithUnsignedLong:transfered] forKey:@"transfered"]; if( transfered > [[info objectForKey:@"size"] unsignedLongLongValue] ) [info setObject:[NSNumber numberWithUnsignedLong:transfered] forKey:@"size"]; if( transfered != [[info objectForKey:@"size"] unsignedLongLongValue] ) [info setObject:[NSNumber numberWithDouble:( transfered / timeslice )] forKey:@"rate"]; if( ! [info objectForKey:@"started"] ) { [info setObject:[NSDate date] forKey:@"started"]; [currentFiles reloadData]; } break; } } } - (BOOL) download:(NSURLDownload *) download shouldDecodeSourceDataOfMIMEType:(NSString *) encodingType { return NO; } - (void) downloadDidFinish:(NSURLDownload *) download { NSEnumerator *enumerator = nil; NSMutableDictionary *info = nil; enumerator = [[[_transferStorage copy] autorelease] objectEnumerator]; while( ( info = [enumerator nextObject] ) ) { if( [info objectForKey:@"controller"] == download ) { [info setObject:[NSNumber numberWithUnsignedInt:MVFileTransferDoneStatus] forKey:@"status"]; [[NSWorkspace sharedWorkspace] noteFileSystemChanged:[info objectForKey:@"path"]]; if( [[NSUserDefaults standardUserDefaults] boolForKey:@"JVOpenSafeFiles"] && [_safeFileExtentions containsObject:[[[info objectForKey:@"path"] pathExtension] lowercaseString]] ) [[NSWorkspace sharedWorkspace] openFile:[info objectForKey:@"path"] withApplication:nil andDeactivate:NO]; if( [[NSUserDefaults standardUserDefaults] integerForKey:@"JVRemoveTransferedItems"] == 2 ) { [_calculationItems removeObject:info]; [_transferStorage removeObject:info]; } [currentFiles reloadData]; break; } } } - (void) download:(NSURLDownload *) download didFailWithError:(NSError *) error { NSEnumerator *enumerator = nil; NSMutableDictionary *info = nil; enumerator = [_transferStorage objectEnumerator]; while( ( info = [enumerator nextObject] ) ) { if( [info objectForKey:@"controller"] == download ) { [info setObject:[NSNumber numberWithUnsignedInt:MVFileTransferErrorStatus] forKey:@"status"]; [currentFiles reloadData]; break; } } } @end #pragma mark - @implementation MVFileTransferController (MVFileTransferControllerPrivate) #pragma mark ChatCore File Transfer Support - (void) _fileTransferError:(NSNotification *) notification { [currentFiles reloadData]; } - (void) _fileTransferStarted:(NSNotification *) notification { MVDownloadFileTransfer *transfer = [notification object]; NSEnumerator *enumerator = nil; NSMutableDictionary *info = nil; enumerator = [[[_transferStorage copy] autorelease] objectEnumerator]; while( ( info = [enumerator nextObject] ) ) { if( [[info objectForKey:@"controller"] isEqualTo:transfer] ) { if( [transfer startDate] ) [info setObject:[transfer startDate] forKey:@"startDate"]; break; } } [currentFiles reloadData]; } - (void) _fileTransferFinished:(NSNotification *) notification { MVFileTransfer *transfer = [notification object]; NSEnumerator *enumerator = nil; NSMutableDictionary *info = nil; enumerator = [[[_transferStorage copy] autorelease] objectEnumerator]; while( ( info = [enumerator nextObject] ) ) { if( [[info objectForKey:@"controller"] isEqualTo:transfer] ) { if( [transfer isDownload] ) { NSString *path = [(MVDownloadFileTransfer *)transfer destination]; [[NSWorkspace sharedWorkspace] noteFileSystemChanged:path]; if( [[NSUserDefaults standardUserDefaults] boolForKey:@"JVOpenSafeFiles"] && [_safeFileExtentions containsObject:[[path pathExtension] lowercaseString]] ) [[NSWorkspace sharedWorkspace] openFile:path withApplication:nil andDeactivate:NO]; } if( [[NSUserDefaults standardUserDefaults] integerForKey:@"JVRemoveTransferedItems"] == 2 ) { [_calculationItems removeObject:info]; [_transferStorage removeObject:info]; [currentFiles reloadData]; } break; } } [currentFiles reloadData]; } - (void) _incomingFile:(NSNotification *) notification { MVDownloadFileTransfer *transfer = [notification object]; if( [[NSUserDefaults standardUserDefaults] integerForKey:@"JVAutoAcceptFilesFrom"] == 3 ) { [self _incomingFileSheetDidEnd:nil returnCode:NSOKButton contextInfo:(void *)[transfer retain]]; } else if( [[NSUserDefaults standardUserDefaults] integerForKey:@"JVAutoAcceptFilesFrom"] == 2 ) { // JVBuddy *buddy = [[MVBuddyListController sharedBuddyList] buddyForNickname:[transfer user] onServer:[(MVChatConnection *)[transfer connection] server]]; // if( buddy ) [self _incomingFileSheetDidEnd:nil returnCode:NSOKButton contextInfo:(void *)[transfer retain]]; // else NSBeginInformationalAlertSheet( NSLocalizedString( @"Incoming File Transfer", "new file transfer dialog title" ), NSLocalizedString( @"Accept", "accept button name" ), NSLocalizedString( @"Refuse", "refuse button name" ), nil, nil, self, @selector( _incomingFileSheetDidEnd:returnCode:contextInfo: ), NULL, (void *)[transfer retain], NSLocalizedString( @"A file named \"%@\" is being sent to you from %@. This file is %@ in size.", "new file transfer dialog message" ), [transfer originalFileName], [transfer user], MVPrettyFileSize( [transfer finalSize] ) ); } else if( [[NSUserDefaults standardUserDefaults] integerForKey:@"JVAutoAcceptFilesFrom"] == 1 ) { NSBeginInformationalAlertSheet( NSLocalizedString( @"Incoming File Transfer", "new file transfer dialog title" ), NSLocalizedString( @"Accept", "accept button name" ), NSLocalizedString( @"Refuse", "refuse button name" ), nil, nil, self, @selector( _incomingFileSheetDidEnd:returnCode:contextInfo: ), NULL, (void *)[transfer retain], NSLocalizedString( @"A file named \"%@\" is being sent to you from %@. This file is %@ in size.", "new file transfer dialog message" ), [transfer originalFileName], [transfer user], MVPrettyFileSize( [transfer finalSize] ) ); } } - (void) _incomingFileSheetDidEnd:(NSWindow *) sheet returnCode:(int) returnCode contextInfo:(void *) contextInfo { MVDownloadFileTransfer *transfer = [(MVDownloadFileTransfer *)contextInfo autorelease]; if( returnCode == NSOKButton ) { if( ! [[NSUserDefaults standardUserDefaults] boolForKey:@"JVAskForTransferSaveLocation"] ) { NSString *path = [[[self class] userPreferredDownloadFolder] stringByAppendingPathComponent:[transfer originalFileName]]; [sheet close]; [transfer setDestination:path renameIfFileExists:NO]; [self _incomingFileSavePanelDidEnd:nil returnCode:NSOKButton contextInfo:(void *)[transfer retain]]; } else { NSSavePanel *savePanel = [[NSSavePanel savePanel] retain]; [sheet close]; [savePanel setDelegate:self]; [savePanel beginSheetForDirectory:[[self class] userPreferredDownloadFolder] file:[transfer originalFileName] modalForWindow:nil modalDelegate:self didEndSelector:@selector( _incomingFileSavePanelDidEnd:returnCode:contextInfo: ) contextInfo:(void *)[transfer retain]]; } } else [transfer reject]; } - (void) _incomingFileSavePanelDidEnd:(NSSavePanel *) sheet returnCode:(int) returnCode contextInfo:(void *) contextInfo { MVDownloadFileTransfer *transfer = [(MVDownloadFileTransfer *)contextInfo autorelease]; [sheet autorelease]; if( returnCode == NSOKButton ) { NSString *filename = ( [[sheet filename] hasSuffix:@".colloquyFake"] ? [[sheet filename] stringByDeletingPathExtension] : [sheet filename] ); if( ! filename ) filename = [transfer destination]; NSNumber *size = [[[NSFileManager defaultManager] fileAttributesAtPath:filename traverseLink:YES] objectForKey:NSFileSize]; BOOL fileExists = [[NSFileManager defaultManager] fileExistsAtPath:filename]; BOOL resumePossible = ( fileExists && [size unsignedLongLongValue] < [transfer finalSize] ? YES : NO ); int result = NSOKButton; if( resumePossible ) result = NSRunAlertPanel( NSLocalizedString( @"Save", "save dialog title" ), NSLocalizedString( @"The file %@ in %@ already exists. Would you like to resume from where a previous transfer stopped or replace it?", "replace or resume transfer save dialog message" ), NSLocalizedString( @"Resume", "resume button name" ), ( sheet ? @"Cancel" : NSLocalizedString( @"Save As...", "save as button name" ) ), NSLocalizedString( @"Replace", "replace button name" ), [[NSFileManager defaultManager] displayNameAtPath:filename], [filename stringByDeletingLastPathComponent] ); else if( fileExists ) result = NSRunAlertPanel( NSLocalizedString( @"Save", "save dialog title" ), NSLocalizedString( @"The file %@ in %@ already exists and can't be resumed. Replace it?", "replace transfer save dialog message" ), NSLocalizedString( @"Replace", "replace button name" ), ( sheet ? @"Cancel" : NSLocalizedString( @"Save As...", "save as button name" ) ), nil, [[NSFileManager defaultManager] displayNameAtPath:filename], [filename stringByDeletingLastPathComponent] ); if( result == NSCancelButton ) { NSSavePanel *savePanel = [[NSSavePanel savePanel] retain]; [sheet close]; [savePanel setDelegate:self]; [savePanel beginSheetForDirectory:[sheet directory] file:[filename lastPathComponent] modalForWindow:nil modalDelegate:self didEndSelector:@selector( _incomingFileSavePanelDidEnd:returnCode:contextInfo: ) contextInfo:(void *)[transfer retain]]; } else { BOOL resume = ( resumePossible && result == NSOKButton ); [transfer setDestination:filename renameIfFileExists:NO]; [transfer acceptByResumingIfPossible:resume]; [self addFileTransfer:transfer]; } } else [transfer reject]; } #pragma mark - #pragma mark URL Web Download Support - (void) _downloadFileSavePanelDidEnd:(NSSavePanel *) sheet returnCode:(int) returnCode contextInfo:(void *) contextInfo { WebDownload *download = [(WebDownload *) contextInfo autorelease]; // for the previous retain [sheet autorelease]; if( returnCode == NSOKButton ) { NSEnumerator *enumerator = nil; NSMutableDictionary *info = nil; enumerator = [_transferStorage objectEnumerator]; while( ( info = [enumerator nextObject] ) ) { if( [info objectForKey:@"controller"] == download ) { if( sheet ) [info setObject:[sheet filename] forKey:@"path"]; break; } } [download setDestination:[info objectForKey:@"path"] allowOverwrite:YES]; } else { NSEnumerator *enumerator = nil; NSMutableDictionary *info = nil; [download cancel]; enumerator = [_transferStorage objectEnumerator]; while( ( info = [enumerator nextObject] ) ) { if( [info objectForKey:@"controller"] == download ) { [info setObject:[NSNumber numberWithUnsignedInt:MVFileTransferStoppedStatus] forKey:@"status"]; break; } } } } - (void) _openFile:(id) sender { NSDictionary *info = nil; NSEnumerator *enumerator = [currentFiles selectedRowEnumerator]; id item = nil; while( ( item = [enumerator nextObject] ) ) { info = [self _infoForTransferAtIndex:[currentFiles selectedRow]]; [[NSWorkspace sharedWorkspace] openFile:[info objectForKey:@"path"]]; } } - (void) _updateProgress:(id) sender { NSString *str = nil; NSEnumerator *enumerator = nil; NSMutableDictionary *info = nil; unsigned long long totalSizeUp = 0, totalTransferedUp = 0, totalTransfered = 0, totalSize = 0; unsigned long long totalSizeDown = 0, totalTransferedDown = 0; double upRate = 0., downRate = 0., avgRate = 0.; unsigned upCount = 0, downCount = 0; if( sender && ! [[self window] isVisible] ) return; if( [_calculationItems count] ) enumerator = [_calculationItems objectEnumerator]; else enumerator = [_transferStorage objectEnumerator]; while( ( info = [enumerator nextObject] ) ) { id controller = [info objectForKey:@"controller"]; if( [controller isKindOfClass:[MVUploadFileTransfer class]] ) { totalSizeUp += [controller finalSize]; totalTransferedUp += [controller transfered]; NSTimeInterval timeslice = [[controller startDate] timeIntervalSinceNow] * -1; if( [controller status] == MVFileTransferNormalStatus && [controller transfered] != [controller finalSize] ) [info setObject:[NSNumber numberWithDouble:( ( [controller transfered] - [(MVUploadFileTransfer *)controller startOffset] ) / timeslice )] forKey:@"rate"]; [info setObject:[NSNumber numberWithUnsignedInt:[controller status]] forKey:@"status"]; upRate += [[info objectForKey:@"rate"] doubleValue]; upCount++; } else if( [controller isKindOfClass:[MVDownloadFileTransfer class]] ) { totalSizeDown += [controller finalSize]; totalTransferedDown += [controller transfered]; NSTimeInterval timeslice = [[controller startDate] timeIntervalSinceNow] * -1; if( [controller status] == MVFileTransferNormalStatus && [controller transfered] != [controller finalSize] ) [info setObject:[NSNumber numberWithDouble:( ( [controller transfered] - [(MVDownloadFileTransfer *)controller startOffset] ) / timeslice )] forKey:@"rate"]; [info setObject:[NSNumber numberWithUnsignedInt:[controller status]] forKey:@"status"]; downRate += [[info objectForKey:@"rate"] doubleValue]; downCount++; } else if( [controller isKindOfClass:[WebDownload class]] ) { totalSizeDown += [[info objectForKey:@"size"] unsignedLongValue]; totalTransferedDown += [[info objectForKey:@"transfered"] unsignedLongValue]; downRate += [[info objectForKey:@"rate"] doubleValue]; downCount++; } } totalTransfered = totalTransferedDown + totalTransferedUp; totalSize = totalSizeDown + totalSizeUp; if( upCount && downCount ) { upRate = upRate / (float) upCount; if( ! totalTransferedUp || ! totalSizeUp ) { str = NSLocalizedString( @"nothing uploaded yet", "status of pending upload file transfer" ); } else if( totalSizeUp != totalTransferedUp ) { str = [NSString stringWithFormat:NSLocalizedString( @"%@ of %@ uploaded, at %@ per second", "status of current upload file transfer" ), MVPrettyFileSize( totalTransferedUp ), MVPrettyFileSize( totalSizeUp ), MVPrettyFileSize( upRate )]; } else if( totalTransferedUp >= totalSizeUp ) { str = [NSString stringWithFormat:NSLocalizedString( @"total of %@ uploaded, at %@ per second", "results final upload file transfer" ), MVPrettyFileSize( totalSizeUp ), MVPrettyFileSize( upRate )]; } str = [str stringByAppendingString:@"\n"]; downRate = downRate / (float) downCount; if( ! totalTransferedDown || ! totalSizeDown ) { str = [str stringByAppendingString:NSLocalizedString( @"nothing downloaded yet", "status of pending download file transfer" )]; } else if( totalSizeDown != totalTransferedDown ) { str = [str stringByAppendingFormat:NSLocalizedString( @"%@ of %@ downloaded, at %@ per second", "status of current download file transfer" ), MVPrettyFileSize( totalTransferedDown ), MVPrettyFileSize( totalSizeDown ), MVPrettyFileSize( downRate )]; } else if( totalTransferedDown >= totalSizeDown ) { str = [str stringByAppendingFormat:NSLocalizedString( @"total of %@ downloaded, at %@ per second", "results final download file transfer" ), MVPrettyFileSize( totalSizeDown ), MVPrettyFileSize( downRate )]; } } else if( upCount || downCount ) { avgRate = ( upRate + downRate ) / ( (float) upCount + (float) downCount ); if( ! totalTransfered || ! totalSize ) { totalSize = 1; if( downCount ) str = NSLocalizedString( @"nothing downloaded yet", "status of pending download file transfer" ); else if( upCount ) str = NSLocalizedString( @"nothing uploaded yet", "status of pending upload file transfer" ); } else if( totalSize != totalTransfered ) { if( downCount ) str = [NSString stringWithFormat:NSLocalizedString( @"%@ of %@ downloaded, at %@ per second", "status of current download file transfer" ), MVPrettyFileSize( totalTransfered ), MVPrettyFileSize( totalSize ), MVPrettyFileSize( avgRate )]; else if( upCount ) str = [NSString stringWithFormat:NSLocalizedString( @"%@ of %@ uploaded, at %@ per second", "status of current upload file transfer" ), MVPrettyFileSize( totalTransfered ), MVPrettyFileSize( totalSize ), MVPrettyFileSize( avgRate )]; } else if( totalTransfered >= totalSize ) { if( downCount ) str = [NSString stringWithFormat:NSLocalizedString( @"total of %@ downloaded, at %@ per second", "results final download file transfer" ), MVPrettyFileSize( totalSize ), MVPrettyFileSize( avgRate )]; else if( upCount ) str = [NSString stringWithFormat:NSLocalizedString( @"total of %@ uploaded, at %@ per second", "results final upload file transfer" ), MVPrettyFileSize( totalSize ), MVPrettyFileSize( avgRate )]; } if( ( upCount + downCount ) == 1 ) { NSDate *startDate = nil; NSDictionary *info = nil; if( [_calculationItems count] ) info = [_calculationItems lastObject]; else info = [_transferStorage lastObject]; if( [[info objectForKey:@"controller"] isKindOfClass:[MVFileTransfer class]] ) startDate = [[info objectForKey:@"controller"] startDate]; else startDate = [info objectForKey:@"started"]; if( startDate && [[info objectForKey:@"status"] unsignedIntValue] == MVFileTransferNormalStatus ) { str = [str stringByAppendingString:@"\n"]; if( avgRate > 0 ) str = [str stringByAppendingFormat:NSLocalizedString( @"%@ elapsed, %@ remaining", "time that has passed and time that remains on selected transfer" ), MVReadableTime( [startDate timeIntervalSince1970], YES ), MVReadableTime( [[NSDate date] timeIntervalSince1970] + ( ( totalSize - totalTransfered) / avgRate ), NO )]; else if( startDate ) str = [str stringByAppendingFormat:NSLocalizedString( @"%@ elapsed", "time that has passed on selected transfer" ), MVReadableTime( [startDate timeIntervalSince1970], YES )]; } } } else if( ! upCount && ! downCount ) { totalSize = 1; str = NSLocalizedString( @"no recent file transfers", "no files have been transfered or in the process of transfering" ); } [transferStatus setStringValue:str]; [progressBar setDoubleValue:totalTransfered]; [progressBar setMaxValue:totalSize]; [progressBar setNeedsDisplay:YES]; } - (NSMutableDictionary *) _infoForTransferAtIndex:(unsigned int) index { NSMutableDictionary *info = [_transferStorage objectAtIndex:index]; if( [[info objectForKey:@"controller"] isKindOfClass:[MVFileTransfer class]] ) { MVFileTransfer *transfer = [info objectForKey:@"controller"]; [info setObject:[NSNumber numberWithUnsignedLongLong:[transfer transfered]] forKey:@"transfered"]; [info setObject:[NSNumber numberWithUnsignedInt:[transfer status]] forKey:@"status"]; [info setObject:[transfer user] forKey:@"user"]; } return info; } @end