// // CLPreferencesViewController.m // Clocker // // Created by Abhishek Banthia on 12/12/15. // // #import "CLPreferencesViewController.h" #import "Panel.h" #import "PanelController.h" #import "ApplicationDelegate.h" #import #import "CommonStrings.h" #import "CLTimezoneData.h" #import #import "CLAPI.h" NSString *const CLSearchPredicateKey = @"SELF CONTAINS[cd]%@"; NSString *const CLPreferencesViewNibIdentifier = @"PreferencesWindow"; NSString *const CLPreferencesTimezoneNameIdentifier = @"formattedAddress"; NSString *const CLPreferencesAbbreviationIdentifier = @"abbreviation"; NSString *const CLPreferencesCustomLabelIdentifier = @"label"; NSString *const CLPreferencesAvailableTimezoneIdentifier = @"availableTimezones"; NSString *const CLNoTimezoneSelectedErrorMessage = @"Please select a timezone!"; NSString *const CLMaxTimezonesErrorMessage = @"Maximum 10 timezones allowed!"; NSString *const CLTimezoneAlreadySelectedError = @"Timezone has already been selected!"; NSString *const CLParseTimezoneSelectionClassIdentifier = @"CLTimezoneSelection"; NSString *const CLParseTimezoneNameProperty = @"areaName"; NSString *const CLMaxCharactersReachedError = @"Only 50 characters allowed!"; NSString *const CLNoInternetConnectivityError = @"You're offline, maybe?"; NSString *const CLLocationSearchURL = @"https://maps.googleapis.com/maps/api/geocode/json?address=%@&key=AIzaSyCyf2knCi6KiKuDJLYDBD3Odq5dt4c-_KI"; NSString *const CLTimezoneSearchURL = @"https://maps.googleapis.com/maps/api/timezone/json?location=%@×tamp=%f&key=AIzaSyCyf2knCi6KiKuDJLYDBD3Odq5dt4c-_KI"; NSString *const CLTryAgainMessage = @"Try again, maybe?"; @interface CLPreferencesViewController () @property (weak) IBOutlet NSTextField *placeholderLabel; @property (assign) BOOL activityInProgress; @property (weak) IBOutlet NSTableView *timezoneTableView; @property (strong) IBOutlet Panel *timezonePanel; @property (weak) IBOutlet NSTableView *availableTimezoneTableView; @property (weak) IBOutlet NSSearchField *searchField; @property (weak) IBOutlet NSTextField *messageLabel; @property (weak) IBOutlet NSSegmentedControl *searchCriteria; @property (weak) IBOutlet NSTableColumn *abbreviation; @end @implementation CLPreferencesViewController - (void)viewDidLoad { [super viewDidLoad]; CALayer *viewLayer = [CALayer layer]; [viewLayer setBackgroundColor:CGColorCreateGenericRGB(255.0, 255.0, 255.0, 0.8)]; //RGB plus Alpha Channel [self.view setWantsLayer:YES]; [self.view setLayer:viewLayer]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(refereshTimezoneTableView) name:CLCustomLabelChangedNotification object:nil]; self.placeholderLabel.hidden = YES; [self refereshTimezoneTableView]; if (!self.filteredArray) { self.timeZoneArray = [[NSMutableArray alloc] initWithArray:[NSTimeZone knownTimeZoneNames]]; self.timeZoneFilteredArray = [NSMutableArray new]; self.filteredArray = [NSMutableArray new]; } self.messageLabel.stringValue = CLEmptyString; [self.availableTimezoneTableView reloadData]; //Register for drag and drop [self.timezoneTableView registerForDraggedTypes: [NSArray arrayWithObject: CLDragSessionKey]]; self.columnName = @"Place(s)"; // Do view setup here. } -(void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self name:CLCustomLabelChangedNotification object:nil]; } -(BOOL)acceptsFirstResponder { return YES; } -(NSInteger)numberOfRowsInTableView:(NSTableView *)tableView { if (tableView == self.timezoneTableView) { return self.selectedTimeZones.count; } else { NSInteger searchCriteria = [self.searchCriteria selectedSegment]; if (searchCriteria == 0) { return self.filteredArray.count; } else { if (self.searchField.stringValue.length > 0) { return self.timeZoneFilteredArray.count; } return self.timeZoneArray.count; } } return 0; } - (nullable id)tableView:(NSTableView *)tableView objectValueForTableColumn:(nullable NSTableColumn *)tableColumn row:(NSInteger)row { CLTimezoneData *dataSource, *selectedDataSource; if (self.filteredArray.count > 0) { dataSource = self.filteredArray[row]; } if (self.selectedTimeZones.count > row) { selectedDataSource = [CLTimezoneData getCustomObject:self.selectedTimeZones[row]]; } if ([[tableColumn identifier] isEqualToString:CLPreferencesTimezoneNameIdentifier]) { if ([selectedDataSource.formattedAddress length] > 0) { return selectedDataSource.formattedAddress; } return selectedDataSource.timezoneID; } else if([[tableColumn identifier] isEqualToString:CLPreferencesAvailableTimezoneIdentifier]) { NSInteger searchCriteria = [self.searchCriteria selectedSegment]; if (searchCriteria == 0) { if (row < self.filteredArray.count) { return dataSource.formattedAddress; } } else { if (self.searchField.stringValue.length > 0) { if (row < self.timeZoneFilteredArray.count) { return self.timeZoneFilteredArray[row]; } } return self.timeZoneArray[row]; } return nil; } else if([[tableColumn identifier] isEqualToString:CLPreferencesCustomLabelIdentifier]) { return selectedDataSource.customLabel; } else if ([tableColumn.identifier isEqualToString:@"favouriteTimezone"]) { return selectedDataSource.isFavourite; } else if ([tableColumn.identifier isEqualToString:@"abbreviation"]) { if (self.searchField.stringValue.length > 0) { if (row < self.timeZoneFilteredArray.count) { return [NSTimeZone timeZoneWithName:self.timeZoneFilteredArray[row]].abbreviation; } } return [NSTimeZone timeZoneWithName:self.timeZoneArray[row]].abbreviation; } return nil; } -(void)tableView:(NSTableView *)tableView setObjectValue:(id)object forTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row { if ([object isKindOfClass:[NSString class]]) { NSString *originalValue = (NSString *)object; NSString *customLabelValue = [originalValue stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceCharacterSet]]; CLTimezoneData *dataObject = [CLTimezoneData getCustomObject:self.selectedTimeZones[row]]; dataObject.customLabel = customLabelValue.length > 0 ? customLabelValue : CLEmptyString; NSData *encodedObject = [NSKeyedArchiver archivedDataWithRootObject:dataObject]; [self.selectedTimeZones replaceObjectAtIndex:row withObject:encodedObject]; [[NSUserDefaults standardUserDefaults] setObject:self.selectedTimeZones forKey:CLDefaultPreferenceKey]; [self refreshMainTableview]; } else { NSMutableArray *newArray = [NSMutableArray new]; for (NSData *object in self.selectedTimeZones) { CLTimezoneData *timezone = [CLTimezoneData getCustomObject:object]; timezone.isFavourite = [NSNumber numberWithInt:0]; NSData *encodedObject = [NSKeyedArchiver archivedDataWithRootObject:timezone]; [newArray addObject:encodedObject]; } CLTimezoneData *dataObject = [CLTimezoneData getCustomObject:newArray[row]]; dataObject.isFavourite = object; NSData *encodedObject = [NSKeyedArchiver archivedDataWithRootObject:dataObject]; [newArray replaceObjectAtIndex:row withObject:encodedObject]; self.selectedTimeZones = [NSMutableArray arrayWithArray:newArray]; [[NSUserDefaults standardUserDefaults] setObject:self.selectedTimeZones forKey:CLDefaultPreferenceKey]; (dataObject.isFavourite.integerValue == 1) ? [[NSUserDefaults standardUserDefaults] setObject:encodedObject forKey:@"favouriteTimezone"]: [[NSUserDefaults standardUserDefaults] setObject:nil forKey:@"favouriteTimezone"]; [self.timezoneTableView reloadData]; } } - (IBAction)addTimeZone:(id)sender { self.abbreviation.hidden = YES; self.filteredArray = [NSMutableArray new]; self.searchCriteria.selectedSegment = 0; self.timeZoneArray = [NSMutableArray new]; [self.view.window beginSheet:self.timezonePanel completionHandler:nil]; } - (IBAction)addToFavorites:(id)sender { self.activityInProgress = YES; if (self.availableTimezoneTableView.selectedRow == -1) { self.messageLabel.stringValue = CLNoTimezoneSelectedErrorMessage; [NSTimer scheduledTimerWithTimeInterval:5 target:self selector:@selector(clearLabel) userInfo:nil repeats:NO]; self.activityInProgress = NO; return; } if (self.selectedTimeZones.count >= 10) { self.messageLabel.stringValue = CLMaxTimezonesErrorMessage; [NSTimer scheduledTimerWithTimeInterval:5 target:self selector:@selector(clearLabel) userInfo:nil repeats:NO]; self.activityInProgress = NO; return; } if (self.searchCriteria.selectedSegment == 0) { CLTimezoneData *dataObject = self.filteredArray[self.availableTimezoneTableView.selectedRow]; for (NSData *encodedData in self.selectedTimeZones) { CLTimezoneData *timezoneObject = [CLTimezoneData getCustomObject:encodedData]; NSString *name = timezoneObject.place_id; NSString *selectedPlaceID = dataObject.place_id; if (self.searchField.stringValue.length > 0) { if ([name isKindOfClass:[NSString class]] && [name isEqualToString:selectedPlaceID]) { self.messageLabel.stringValue = CLTimezoneAlreadySelectedError; [NSTimer scheduledTimerWithTimeInterval:5 target:self selector:@selector(clearLabel) userInfo:nil repeats:NO]; self.activityInProgress = NO; return; } } } self.searchField.stringValue = CLEmptyString; [self getTimeZoneForLatitude:dataObject.latitude andLongitude:dataObject.longitude]; } else { CLTimezoneData *data = [CLTimezoneData new]; data.isFavourite = [NSNumber numberWithInt:NSOffState]; data.customLabel = CLEmptyString; if (self.searchField.stringValue.length > 0) { data.timezoneID = self.timeZoneFilteredArray[self.availableTimezoneTableView.selectedRow]; data.formattedAddress = self.timeZoneFilteredArray[self.availableTimezoneTableView.selectedRow]; } else { data.timezoneID = self.timeZoneArray[self.availableTimezoneTableView.selectedRow]; data.formattedAddress = self.timeZoneArray[self.availableTimezoneTableView.selectedRow]; } NSArray *defaultPreference = [[NSUserDefaults standardUserDefaults] objectForKey:CLDefaultPreferenceKey]; if (defaultPreference == nil) { defaultPreference = [[NSMutableArray alloc] init]; } NSData *encodedObject = [NSKeyedArchiver archivedDataWithRootObject:data]; NSMutableArray *newArray = [[NSMutableArray alloc] initWithArray:defaultPreference]; [newArray addObject:encodedObject]; [[NSUserDefaults standardUserDefaults] setObject:newArray forKey:CLDefaultPreferenceKey]; self.timeZoneArray = [NSMutableArray array]; self.timeZoneFilteredArray = [NSMutableArray array]; [self.availableTimezoneTableView reloadData]; [self refereshTimezoneTableView]; [self refreshMainTableview]; [self.timezonePanel close]; self.placeholderLabel.placeholderString = CLEmptyString; self.searchField.stringValue = CLEmptyString; self.searchField.placeholderString = @"Enter a city, state or country name"; self.availableTimezoneTableView.hidden = NO; self.activityInProgress = NO; } /* PFObject *feedbackObject = [PFObject objectWithClassName:CLParseTimezoneSelectionClassIdentifier]; feedbackObject[CLParseTimezoneNameProperty] = [self.filteredArray[self.availableTimezoneTableView.selectedRow] objectForKey:CLTimezoneName]; [feedbackObject saveEventually];*/ } - (IBAction)closePanel:(id)sender { self.filteredArray = [NSMutableArray array]; self.timeZoneArray = [NSMutableArray new]; self.searchCriteria.selectedSegment = 0; self.columnName = @"Place(s)"; self.placeholderLabel.placeholderString = CLEmptyString; [self.availableTimezoneTableView reloadData]; self.searchField.stringValue = CLEmptyString; self.searchField.placeholderString = @"Enter a city, state or country name"; [self.timezonePanel close]; self.activityInProgress = NO; } - (void)clearLabel { self.messageLabel.stringValue = CLEmptyString; } - (IBAction)removeFromFavourites:(id)sender { NSMutableArray *itemsToRemove = [NSMutableArray array]; if (self.timezoneTableView.selectedRow == -1) { return; } [self.timezoneTableView.selectedRowIndexes enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) { [itemsToRemove addObject:self.selectedTimeZones[idx]]; }]; [self.selectedTimeZones removeObjectsInArray:itemsToRemove]; NSMutableArray *newDefaults = [[NSMutableArray alloc] initWithArray:self.selectedTimeZones]; [[NSUserDefaults standardUserDefaults] setObject:newDefaults forKey:CLDefaultPreferenceKey]; [self.timezoneTableView reloadData]; [self refreshMainTableview]; } - (IBAction)filterTimezoneArray:(id)sender { if (self.searchField.stringValue.length > 0) { NSPredicate *predicate = [NSPredicate predicateWithFormat:CLSearchPredicateKey, self.searchField.stringValue]; self.timeZoneFilteredArray = [NSMutableArray arrayWithArray:[self.timeZoneArray filteredArrayUsingPredicate:predicate]]; [self.availableTimezoneTableView reloadData]; } } - (IBAction)filterArray:(id)sender { if (self.searchCriteria.selectedSegment == 1) { [self filterTimezoneArray:sender]; return; } [self clearLabel]; self.filteredArray = [NSMutableArray array]; if (self.searchField.stringValue.length > 50) { self.activityInProgress = NO; self.messageLabel.stringValue = CLMaxCharactersReachedError; [NSTimer scheduledTimerWithTimeInterval:10 target:self selector:@selector(clearLabel) userInfo:nil repeats:NO]; return; } if (self.searchField.stringValue.length > 0) { [self callGoogleAPiWithSearchString:self.searchField.stringValue]; } else { if (self.dataTask.state == NSURLSessionTaskStateRunning) { [self.dataTask cancel]; } self.activityInProgress = NO; self.placeholderLabel.placeholderString = CLEmptyString; } [self.availableTimezoneTableView reloadData]; } - (void)refereshTimezoneTableView { dispatch_async(dispatch_get_main_queue(), ^{ NSMutableArray *defaultTimeZones = [[NSUserDefaults standardUserDefaults] objectForKey:CLDefaultPreferenceKey]; self.selectedTimeZones = [[NSMutableArray alloc] initWithArray:defaultTimeZones]; [self.timezoneTableView reloadData]; }); } - (void)refreshMainTableview { dispatch_async(dispatch_get_main_queue(), ^{ ApplicationDelegate *appDelegate = [[NSApplication sharedApplication] delegate]; PanelController *panelController = appDelegate.panelController; [panelController updateDefaultPreferences]; [panelController.mainTableview reloadData]; }); } #pragma mark Reordering - (BOOL)tableView:(NSTableView *)tableView writeRowsWithIndexes:(NSIndexSet *)rowIndexes toPasteboard:(NSPasteboard *)pboard { NSData *data = [NSKeyedArchiver archivedDataWithRootObject:rowIndexes]; [pboard declareTypes:[NSArray arrayWithObject:CLDragSessionKey] owner:self]; [pboard setData:data forType:CLDragSessionKey]; return YES; } -(BOOL)tableView:(NSTableView *)tableView acceptDrop:(id)info row:(NSInteger)row dropOperation:(NSTableViewDropOperation)dropOperation { if (row == self.selectedTimeZones.count) { row--; } NSPasteboard *pBoard = [info draggingPasteboard]; NSData *data = [pBoard dataForType:CLDragSessionKey]; NSIndexSet *rowIndexes = [NSKeyedUnarchiver unarchiveObjectWithData:data]; [self.selectedTimeZones exchangeObjectAtIndex:rowIndexes.firstIndex withObjectAtIndex:row]; [[NSUserDefaults standardUserDefaults] setObject:self.selectedTimeZones forKey:CLDefaultPreferenceKey]; [self.timezoneTableView reloadData]; [self refreshMainTableview]; return YES; } -(NSDragOperation)tableView:(NSTableView *)tableView validateDrop:(id)info proposedRow:(NSInteger)row proposedDropOperation:(NSTableViewDropOperation)dropOperation { return NSDragOperationEvery; } - (void)callGoogleAPiWithSearchString:(NSString *)searchString { if (self.dataTask.state == NSURLSessionTaskStateRunning) { [self.dataTask cancel]; } if (self.availableTimezoneTableView.isHidden) { self.availableTimezoneTableView.hidden = NO; } self.placeholderLabel.hidden = NO; if (![CLAPI isUserConnectedToInternet]) { self.placeholderLabel.placeholderString = CLNoInternetConnectivityError; return; } self.activityInProgress = YES; self.placeholderLabel.placeholderString = [NSString stringWithFormat:@"Searching for '%@'", searchString]; NSArray* words = [searchString componentsSeparatedByCharactersInSet :[NSCharacterSet whitespaceAndNewlineCharacterSet]]; searchString = [words componentsJoinedByString:CLEmptyString]; NSString *urlString = [NSString stringWithFormat:CLLocationSearchURL, searchString]; [CLAPI dataTaskWithServicePath:urlString bySender:self withCompletionBlock:^(NSError *error, NSDictionary *json) { dispatch_async(dispatch_get_main_queue(), ^{ if (error) { self.placeholderLabel.placeholderString = [error.localizedDescription isEqualToString:@"The Internet connection appears to be offline."] ? CLNoInternetConnectivityError : CLTryAgainMessage; self.activityInProgress = NO; return; } self.placeholderLabel.placeholderString = CLEmptyString; if ([json[@"status"] isEqualToString:@"ZERO_RESULTS"]) { self.placeholderLabel.placeholderString = @"No results! 😔 Try entering the exact name."; self.activityInProgress = NO; return; } for (NSDictionary *dictionary in json[@"results"]) { NSDictionary *latLang = [[dictionary objectForKey:@"geometry"] objectForKey:@"location"]; NSString *latitude = latLang[@"lat"]; NSString *longitude = latLang[@"lng"]; NSString *formattedAddress = [dictionary objectForKey:@"formatted_address"]; NSDictionary *totalPackage = @{@"latitude":latitude, @"longitude" : longitude, CLTimezoneName:formattedAddress, CLCustomLabel: CLEmptyString, CLTimezoneID : CLEmptyString, CLPlaceIdentifier : dictionary[CLPlaceIdentifier]}; CLTimezoneData *newObject = [[CLTimezoneData alloc] initWithDictionary:totalPackage]; [self.filteredArray addObject:newObject]; } self.activityInProgress = NO; [self.availableTimezoneTableView reloadData]; }); }]; } - (void)getTimeZoneForLatitude:(NSString *)latitude andLongitude:(NSString *)longitude { if (![CLAPI isUserConnectedToInternet]) { dispatch_async(dispatch_get_main_queue(), ^{ self.placeholderLabel.placeholderString = CLNoInternetConnectivityError; self.activityInProgress = NO; self.filteredArray = [NSMutableArray array]; [self.availableTimezoneTableView reloadData]; }); return; } self.searchField.placeholderString = @"Fetching data might take some time!"; self.placeholderLabel.placeholderString = @"Retrieving timezone data"; self.availableTimezoneTableView.hidden = YES; NSString *tuple = [NSString stringWithFormat:@"%@,%@", latitude, longitude]; NSTimeInterval timestamp = [[NSDate date] timeIntervalSince1970]; NSString *urlString = [NSString stringWithFormat:CLTimezoneSearchURL, tuple, timestamp]; [CLAPI dataTaskWithServicePath:urlString bySender:self withCompletionBlock:^(NSError *error, NSDictionary *json) { if (!error) { dispatch_async(dispatch_get_main_queue(), ^{ if (json.count == 0) { self.activityInProgress = NO; self.placeholderLabel.placeholderString = @"No results found! ! 😔 Try Again?"; return; } if ([json[@"status"] isEqualToString:@"ZERO_RESULTS"]) { self.placeholderLabel.placeholderString = @"No results! 😔 Try entering the exact name."; self.activityInProgress = NO; return; } CLTimezoneData *dataObject = self.filteredArray[self.availableTimezoneTableView.selectedRow]; NSString *filteredAddress = dataObject.formattedAddress; NSRange range = [filteredAddress rangeOfString:@","]; if (range.location != NSNotFound) { filteredAddress = [dataObject.formattedAddress substringWithRange:NSMakeRange(0, range.location)]; } NSMutableDictionary *newTimezone = [NSMutableDictionary dictionary]; if (json[@"sunrise"]) { [newTimezone setObject:json[@"sunrise"] forKey:@"sunriseTime"]; } if (json[@"sunset"]) { [newTimezone setObject:json[@"sunset"] forKey:@"sunsetTime"]; } [newTimezone setObject:json[@"timeZoneId"] forKey:CLTimezoneID]; [newTimezone setObject:filteredAddress forKey:CLTimezoneName]; [newTimezone setObject:dataObject.place_id forKey:CLPlaceIdentifier]; [newTimezone setObject:latitude forKey:@"latitude"]; [newTimezone setObject:longitude forKey:@"longitude"]; [newTimezone setObject:CLEmptyString forKey:@"nextUpdate"]; [newTimezone setObject:CLEmptyString forKey:CLCustomLabel]; CLTimezoneData *timezoneObject = [[CLTimezoneData alloc] initWithDictionary:newTimezone]; NSArray *defaultPreference = [[NSUserDefaults standardUserDefaults] objectForKey:CLDefaultPreferenceKey]; if (defaultPreference == nil) { defaultPreference = [[NSMutableArray alloc] init]; } NSData *encodedObject = [NSKeyedArchiver archivedDataWithRootObject:timezoneObject]; NSMutableArray *newArray = [[NSMutableArray alloc] initWithArray:defaultPreference]; [newArray addObject:encodedObject]; [[NSUserDefaults standardUserDefaults] setObject:newArray forKey:CLDefaultPreferenceKey]; self.filteredArray = [NSMutableArray array]; [self.availableTimezoneTableView reloadData]; [self refereshTimezoneTableView]; [self refreshMainTableview]; [self.timezonePanel close]; self.placeholderLabel.placeholderString = CLEmptyString; self.searchField.placeholderString = @"Enter a city, state or country name"; self.availableTimezoneTableView.hidden = NO; self.activityInProgress = NO; }); } else { self.placeholderLabel.placeholderString = [error.localizedDescription isEqualToString:@"The Internet connection appears to be offline."] ? CLNoInternetConnectivityError : CLTryAgainMessage; self.activityInProgress = NO; } }]; } - (IBAction)filterSearch:(id)sender { if (self.searchCriteria.selectedSegment == 1) { if (self.searchField.stringValue.length > 0) { NSPredicate *predicate = [NSPredicate predicateWithFormat:CLSearchPredicateKey, self.searchField.stringValue]; self.timeZoneFilteredArray = [NSMutableArray arrayWithArray:[self.timeZoneArray filteredArrayUsingPredicate:predicate]]; [self.availableTimezoneTableView reloadData]; } } } - (IBAction)searchOptions:(id)sender { if (self.searchCriteria.selectedSegment == 0) { self.searchField.placeholderString = @"Enter a city, state or country name"; self.columnName = @"Place(s)"; self.abbreviation.hidden = YES; } else { self.timeZoneArray = [NSMutableArray arrayWithArray:[NSTimeZone knownTimeZoneNames]]; self.searchField.placeholderString = @"Enter a timezone name"; self.columnName = @"Timezone(s)"; self.abbreviation.hidden = NO; } self.searchField.stringValue = CLEmptyString; [self.availableTimezoneTableView reloadData]; } @end