diff --git a/GMImagePicker/Base.lproj/GMImagePicker.strings b/GMImagePicker/Base.lproj/GMImagePicker.strings new file mode 100644 index 00000000..cd03baa6 Binary files /dev/null and b/GMImagePicker/Base.lproj/GMImagePicker.strings differ diff --git a/GMImagePicker/GMAlbumsViewCell.h b/GMImagePicker/GMAlbumsViewCell.h new file mode 100644 index 00000000..31a2676d --- /dev/null +++ b/GMImagePicker/GMAlbumsViewCell.h @@ -0,0 +1,32 @@ +// +// GMAlbumsViewCell.h +// GMPhotoPicker +// +// Created by Guillermo Muntaner Perelló on 22/09/14. +// Copyright (c) 2014 Guillermo Muntaner Perelló. All rights reserved. +// + +#include +#include + +@interface GMAlbumsViewCell : UITableViewCell + +@property (strong) PHFetchResult *assetsFetchResults; +@property (strong) PHAssetCollection *assetCollection; + +//The labels +@property (nonatomic, strong) UILabel *titleLabel; +@property (nonatomic, strong) UILabel *infoLabel; +//The imageView +@property (nonatomic, strong) UIImageView *imageView1; +@property (nonatomic, strong) UIImageView *imageView2; +@property (nonatomic, strong) UIImageView *imageView3; +//Video additional information +@property (nonatomic, strong) UIImageView *videoIcon; +@property (nonatomic, strong) UIImageView *slowMoIcon; +@property (nonatomic, strong) UIView *gradientView; +@property (nonatomic, strong) CAGradientLayer *gradient; +//Selection overlay + +- (void)setVideoLayout:(BOOL)isVideo; +@end diff --git a/GMImagePicker/GMAlbumsViewCell.m b/GMImagePicker/GMAlbumsViewCell.m new file mode 100644 index 00000000..afe8969d --- /dev/null +++ b/GMImagePicker/GMAlbumsViewCell.m @@ -0,0 +1,131 @@ +// +// GMAlbumsViewCell.m +// GMPhotoPicker +// +// Created by Guillermo Muntaner Perelló on 22/09/14. +// Copyright (c) 2014 Guillermo Muntaner Perelló. All rights reserved. +// + +#import "GMAlbumsViewCell.h" +#import "GMAlbumsViewController.h" +#import "GMImagePickerController.h" +#import + +@implementation GMAlbumsViewCell + +- (void)awakeFromNib +{ + [super awakeFromNib]; +} + +- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier +{ + + if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) + { + // self.isAccessibilityElement = YES; + self.contentView.backgroundColor = [UIColor clearColor]; + self.backgroundColor = [UIColor clearColor]; + self.accessoryType = UITableViewCellAccessoryDisclosureIndicator; + + // Border width of 1 pixel: + float borderWidth = 1.0/[UIScreen mainScreen].scale; + + // ImageView + _imageView3 = [UIImageView new]; + _imageView3.contentMode = UIViewContentModeScaleAspectFill; + _imageView3.frame = CGRectMake(kAlbumLeftToImageSpace+4, 8, kAlbumThumbnailSize3.width, kAlbumThumbnailSize3.height ); + [_imageView3.layer setBorderColor: [[UIColor whiteColor] CGColor]]; + [_imageView3.layer setBorderWidth: borderWidth]; + _imageView3.clipsToBounds = YES; + _imageView3.translatesAutoresizingMaskIntoConstraints = YES; + _imageView3.autoresizingMask = UIViewAutoresizingFlexibleRightMargin; + [self.contentView addSubview:_imageView3]; + + // ImageView + _imageView2 = [UIImageView new]; + _imageView2.contentMode = UIViewContentModeScaleAspectFill; + _imageView2.frame = CGRectMake(kAlbumLeftToImageSpace+2, 8+2, kAlbumThumbnailSize2.width, kAlbumThumbnailSize2.height ); + [_imageView2.layer setBorderColor: [[UIColor whiteColor] CGColor]]; + [_imageView2.layer setBorderWidth: borderWidth]; + _imageView2.clipsToBounds = YES; + _imageView2.translatesAutoresizingMaskIntoConstraints = YES; + _imageView2.autoresizingMask = UIViewAutoresizingFlexibleRightMargin; + [self.contentView addSubview:_imageView2]; + + // ImageView + _imageView1 = [UIImageView new]; + _imageView1.contentMode = UIViewContentModeScaleAspectFill; + _imageView1.frame = CGRectMake(kAlbumLeftToImageSpace, 8+4, kAlbumThumbnailSize1.width, kAlbumThumbnailSize1.height ); + [_imageView1.layer setBorderColor: [[UIColor whiteColor] CGColor]]; + [_imageView1.layer setBorderWidth: borderWidth]; + _imageView1.clipsToBounds = YES; + _imageView1.translatesAutoresizingMaskIntoConstraints = YES; + _imageView1.autoresizingMask = UIViewAutoresizingFlexibleRightMargin; + [self.contentView addSubview:_imageView1]; + + + // The video gradient, label & icon + UIColor *topGradient = [UIColor colorWithRed:0.00 green:0.00 blue:0.00 alpha:0.0]; + UIColor *midGradient = [UIColor colorWithRed:0.00 green:0.00 blue:0.00 alpha:0.33]; + UIColor *botGradient = [UIColor colorWithRed:0.00 green:0.00 blue:0.00 alpha:0.75]; + _gradientView = [[UIView alloc] initWithFrame: CGRectMake(0.0f, kAlbumThumbnailSize1.height-kAlbumGradientHeight, kAlbumThumbnailSize1.width, kAlbumGradientHeight)]; + _gradient = [CAGradientLayer layer]; + _gradient.frame = _gradientView.bounds; + _gradient.colors = [NSArray arrayWithObjects:(id)[topGradient CGColor], (id)[midGradient CGColor], (id)[botGradient CGColor], nil]; + _gradient.locations = @[ @0.0f, @0.5f, @1.0f ]; + [_gradientView.layer insertSublayer:_gradient atIndex:0]; + _gradientView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin; + _gradientView.translatesAutoresizingMaskIntoConstraints = YES; + [self.imageView1 addSubview:_gradientView]; + _gradientView.hidden = YES; + + // VideoIcon + _videoIcon = [UIImageView new]; + _videoIcon.contentMode = UIViewContentModeScaleAspectFill; + _videoIcon.frame = CGRectMake(3,kAlbumThumbnailSize1.height - 4 - 8, 15, 8 ); + _videoIcon.image = [UIImage imageNamed:@"GMVideoIcon" inBundle:[NSBundle bundleForClass:GMAlbumsViewCell.class] compatibleWithTraitCollection:nil]; + _videoIcon.clipsToBounds = YES; + _videoIcon.translatesAutoresizingMaskIntoConstraints = YES; + _videoIcon.autoresizingMask = UIViewAutoresizingFlexibleRightMargin; + [self.imageView1 addSubview:_videoIcon]; + _videoIcon.hidden = NO; + + // TextLabel + self.textLabel.font = [UIFont fontWithName:@"HelveticaNeue" size:17.0]; + self.textLabel.numberOfLines = 1; + + self.detailTextLabel.font = [UIFont fontWithName:@"HelveticaNeue" size:14.0]; + self.detailTextLabel.numberOfLines = 1; + + } + return self; +} + +-(void)layoutSubviews { + [super layoutSubviews]; + self.textLabel.frame = CGRectMake(kAlbumLeftToImageSpace + kAlbumThumbnailSize1.width + kAlbumImageToTextSpace,self.textLabel.frame.origin.y,self.contentView.frame.size.width - kAlbumLeftToImageSpace - kAlbumThumbnailSize1.width - 8, self.textLabel.frame.size.height); + self.detailTextLabel.frame = CGRectMake(kAlbumLeftToImageSpace + kAlbumThumbnailSize1.width + kAlbumImageToTextSpace,self.detailTextLabel.frame.origin.y,self.contentView.frame.size.width - kAlbumLeftToImageSpace - kAlbumThumbnailSize1.width - 8 - kAlbumImageToTextSpace, self.detailTextLabel.frame.size.height); + +} +- (void)setVideoLayout:(BOOL)isVideo +{ + // TODO : Add additional icons for slowmo, burst, etc... + if (isVideo) { + _videoIcon.hidden = NO; + _gradientView.hidden = NO; + } else { + _videoIcon.hidden = YES; + _gradientView.hidden = YES; + } +} + + +- (void)setSelected:(BOOL)selected animated:(BOOL)animated +{ + [super setSelected:selected animated:animated]; + + // Configure the view for the selected state +} + +@end diff --git a/GMImagePicker/GMAlbumsViewController.h b/GMImagePicker/GMAlbumsViewController.h new file mode 100644 index 00000000..d60dc28c --- /dev/null +++ b/GMImagePicker/GMAlbumsViewController.h @@ -0,0 +1,32 @@ +// +// GMAlbumsViewController.h +// GMPhotoPicker +// +// Created by Guillermo Muntaner Perelló on 19/09/14. +// Copyright (c) 2014 Guillermo Muntaner Perelló. All rights reserved. +// + +#import + +// Measuring IOS8 Photos APP at @2x (iPhone5s): +// The rows are 180px/90pts +// Left image border is 21px/10.5pts +// Separation between image and text is 42px/21pts (double the previouse one) +// The bigger image measures 139px/69.5pts including 1px/0.5pts white border. +// The second image measures 131px/65.6pts including 1px/0.5pts white border. Only 3px/1.5pts visible +// The third image measures 123px/61.5pts including 1px/0.5pts white border. Only 3px/1.5pts visible + +static int kAlbumRowHeight = 90; +static int kAlbumLeftToImageSpace = 10; +static int kAlbumImageToTextSpace = 21; +static float const kAlbumGradientHeight = 20.0f; +static CGSize const kAlbumThumbnailSize1 = {70.0f , 70.0f}; +static CGSize const kAlbumThumbnailSize2 = {66.0f , 66.0f}; +static CGSize const kAlbumThumbnailSize3 = {62.0f , 62.0f}; + + +@interface GMAlbumsViewController : UITableViewController + +- (void)selectAllAlbumsCell; + +@end diff --git a/GMImagePicker/GMAlbumsViewController.m b/GMImagePicker/GMAlbumsViewController.m new file mode 100644 index 00000000..26dd151e --- /dev/null +++ b/GMImagePicker/GMAlbumsViewController.m @@ -0,0 +1,416 @@ +// +// GMAlbumsViewController.m +// GMPhotoPicker +// +// Created by Guillermo Muntaner Perelló on 19/09/14. +// Copyright (c) 2014 Guillermo Muntaner Perelló. All rights reserved. +// + +#import "GMImagePickerController.h" +#import "GMAlbumsViewController.h" +#import "GMGridViewCell.h" +#import "GMGridViewController.h" +#import "GMAlbumsViewCell.h" + +#include + +@interface GMAlbumsViewController() + +@property (strong,nonatomic) NSArray *collectionsFetchResults; +@property (strong,nonatomic) NSArray *collectionsLocalizedTitles; +@property (strong,nonatomic) NSArray *collectionsFetchResultsAssets; +@property (strong,nonatomic) NSArray *collectionsFetchResultsTitles; +@property (nonatomic, weak) GMImagePickerController *picker; +@property (strong,nonatomic) PHCachingImageManager *imageManager; + +@end + + +@implementation GMAlbumsViewController + +- (id)init +{ + if (self = [super initWithStyle:UITableViewStylePlain]) { + self.preferredContentSize = kPopoverContentSize; + } + + return self; +} + +static NSString *const AllPhotosReuseIdentifier = @"AllPhotosCell"; +static NSString *const CollectionCellReuseIdentifier = @"CollectionCell"; + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + self.view.backgroundColor = [self.picker pickerBackgroundColor]; + + // Navigation bar customization + if (self.picker.customNavigationBarPrompt) { + self.navigationItem.prompt = self.picker.customNavigationBarPrompt; + } + + self.imageManager = [[PHCachingImageManager alloc] init]; + + // Table view aspect + self.tableView.rowHeight = kAlbumRowHeight; + self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone; + + // Buttons + NSDictionary *barButtonItemAttributes = @{NSFontAttributeName: [UIFont fontWithName:self.picker.pickerFontName size:self.picker.pickerFontHeaderSize]}; + + NSString *cancelTitle = self.picker.customCancelButtonTitle ? self.picker.customCancelButtonTitle : NSLocalizedStringFromTableInBundle(@"picker.navigation.cancel-button", @"GMImagePicker", [NSBundle bundleForClass:GMImagePickerController.class], @"Cancel"); + self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:cancelTitle + style:UIBarButtonItemStylePlain + target:self.picker + action:@selector(dismiss:)]; + if (self.picker.useCustomFontForNavigationBar) { + [self.navigationItem.leftBarButtonItem setTitleTextAttributes:barButtonItemAttributes forState:UIControlStateNormal]; + [self.navigationItem.leftBarButtonItem setTitleTextAttributes:barButtonItemAttributes forState:UIControlStateSelected]; + } + + if (self.picker.allowsMultipleSelection) { + NSString *doneTitle = self.picker.customDoneButtonTitle ? self.picker.customDoneButtonTitle : NSLocalizedStringFromTableInBundle(@"picker.navigation.done-button", @"GMImagePicker", [NSBundle bundleForClass:GMImagePickerController.class], @"Done"); + self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:doneTitle + style:UIBarButtonItemStyleDone + target:self.picker + action:@selector(finishPickingAssets:)]; + if (self.picker.useCustomFontForNavigationBar) { + [self.navigationItem.rightBarButtonItem setTitleTextAttributes:barButtonItemAttributes forState:UIControlStateNormal]; + [self.navigationItem.rightBarButtonItem setTitleTextAttributes:barButtonItemAttributes forState:UIControlStateSelected]; + } + + self.navigationItem.rightBarButtonItem.enabled = (self.picker.autoDisableDoneButton ? self.picker.selectedAssets.count > 0 : TRUE); + } + + // Bottom toolbar + self.toolbarItems = self.picker.toolbarItems; + + // Title + if (!self.picker.title) { + self.title = NSLocalizedStringFromTableInBundle(@"picker.navigation.title", @"GMImagePicker", [NSBundle bundleForClass:GMImagePickerController.class], @"Navigation bar default title"); + } else { + self.title = self.picker.title; + } + + // Fetch PHAssetCollections: + PHFetchResult *topLevelUserCollections = [PHCollectionList fetchTopLevelUserCollectionsWithOptions:nil]; + PHFetchResult *smartAlbums = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeSmartAlbum subtype:PHAssetCollectionSubtypeAlbumRegular options:nil]; + self.collectionsFetchResults = @[topLevelUserCollections, smartAlbums]; + self.collectionsLocalizedTitles = @[NSLocalizedStringFromTableInBundle(@"picker.table.smart-albums-header", @"GMImagePicker", [NSBundle bundleForClass:GMImagePickerController.class], @"Smart Albums"),NSLocalizedStringFromTableInBundle(@"picker.table.user-albums-header", @"GMImagePicker", [NSBundle bundleForClass:GMImagePickerController.class], @"Albums")]; + + [self updateFetchResults]; + + // Register for changes + [[PHPhotoLibrary sharedPhotoLibrary] registerChangeObserver:self]; + + if ([self respondsToSelector:@selector(setEdgesForExtendedLayout:)]) + { + self.edgesForExtendedLayout = UIRectEdgeNone; + } +} + +- (void)dealloc +{ + [[PHPhotoLibrary sharedPhotoLibrary] unregisterChangeObserver:self]; +} + +- (UIStatusBarStyle)preferredStatusBarStyle { + return self.picker.pickerStatusBarStyle; +} + +- (void)selectAllAlbumsCell { + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0]; + [self tableView:self.tableView didSelectRowAtIndexPath:indexPath]; +} + +-(void)updateFetchResults +{ + //What I do here is fetch both the albums list and the assets of each album. + //This way I have acces to the number of items in each album, I can load the 3 + //thumbnails directly and I can pass the fetched result to the gridViewController. + + self.collectionsFetchResultsAssets=nil; + self.collectionsFetchResultsTitles=nil; + + //Fetch PHAssetCollections: + PHFetchResult *topLevelUserCollections = [self.collectionsFetchResults objectAtIndex:0]; + PHFetchResult *smartAlbums = [self.collectionsFetchResults objectAtIndex:1]; + + //All album: Sorted by descending creation date. + NSMutableArray *allFetchResultArray = [[NSMutableArray alloc] init]; + NSMutableArray *allFetchResultLabel = [[NSMutableArray alloc] init]; + { + PHFetchOptions *options = [[PHFetchOptions alloc] init]; + options.predicate = [NSPredicate predicateWithFormat:@"mediaType in %@", self.picker.mediaTypes]; + options.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:NO]]; + PHFetchResult *assetsFetchResult = [PHAsset fetchAssetsWithOptions:options]; + [allFetchResultArray addObject:assetsFetchResult]; + [allFetchResultLabel addObject:NSLocalizedStringFromTableInBundle(@"picker.table.all-photos-label", @"GMImagePicker", [NSBundle bundleForClass:GMImagePickerController.class], @"All photos")]; + } + + //User albums: + NSMutableArray *userFetchResultArray = [[NSMutableArray alloc] init]; + NSMutableArray *userFetchResultLabel = [[NSMutableArray alloc] init]; + for(PHCollection *collection in topLevelUserCollections) + { + if ([collection isKindOfClass:[PHAssetCollection class]]) + { + PHFetchOptions *options = [[PHFetchOptions alloc] init]; + options.predicate = [NSPredicate predicateWithFormat:@"mediaType in %@", self.picker.mediaTypes]; + PHAssetCollection *assetCollection = (PHAssetCollection *)collection; + + //Albums collections are allways PHAssetCollectionType=1 & PHAssetCollectionSubtype=2 + + PHFetchResult *assetsFetchResult = [PHAsset fetchAssetsInAssetCollection:assetCollection options:options]; + [userFetchResultArray addObject:assetsFetchResult]; + [userFetchResultLabel addObject:collection.localizedTitle]; + } + } + + + //Smart albums: Sorted by descending creation date. + NSMutableArray *smartFetchResultArray = [[NSMutableArray alloc] init]; + NSMutableArray *smartFetchResultLabel = [[NSMutableArray alloc] init]; + for(PHCollection *collection in smartAlbums) + { + if ([collection isKindOfClass:[PHAssetCollection class]]) + { + PHAssetCollection *assetCollection = (PHAssetCollection *)collection; + + //Smart collections are PHAssetCollectionType=2; + if(self.picker.customSmartCollections && [self.picker.customSmartCollections containsObject:@(assetCollection.assetCollectionSubtype)]) + { + PHFetchOptions *options = [[PHFetchOptions alloc] init]; + options.predicate = [NSPredicate predicateWithFormat:@"mediaType in %@", self.picker.mediaTypes]; + options.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:NO]]; + + PHFetchResult *assetsFetchResult = [PHAsset fetchAssetsInAssetCollection:assetCollection options:options]; + if(assetsFetchResult.count>0) + { + [smartFetchResultArray addObject:assetsFetchResult]; + [smartFetchResultLabel addObject:collection.localizedTitle]; + } + } + } + } + + self.collectionsFetchResultsAssets= @[allFetchResultArray,smartFetchResultArray,userFetchResultArray]; + self.collectionsFetchResultsTitles= @[allFetchResultLabel,smartFetchResultLabel,userFetchResultLabel]; +} + + +#pragma mark - Accessors + +- (GMImagePickerController *)picker +{ + return (GMImagePickerController *)self.navigationController.parentViewController; +} + + +#pragma mark - Rotation + +- (BOOL)shouldAutorotate +{ + return YES; +} + +- (UIInterfaceOrientationMask)supportedInterfaceOrientations +{ + return UIInterfaceOrientationMaskAllButUpsideDown; +} + + +#pragma mark - UITableViewDataSource + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView +{ + return (NSInteger)self.collectionsFetchResultsAssets.count; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section +{ + PHFetchResult *fetchResult = self.collectionsFetchResultsAssets[(NSUInteger)section]; + return (NSInteger)fetchResult.count; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath +{ + static NSString *CellIdentifier = @"Cell"; + + GMAlbumsViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; + if (cell == nil) { + cell = [[GMAlbumsViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier]; + cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; + } + + // Increment the cell's tag + NSInteger currentTag = cell.tag + 1; + cell.tag = currentTag; + + // Set the label + cell.textLabel.font = [UIFont fontWithName:self.picker.pickerFontName size:self.picker.pickerFontHeaderSize]; + cell.textLabel.text = (self.collectionsFetchResultsTitles[(NSUInteger)indexPath.section])[(NSUInteger)indexPath.row]; + cell.textLabel.textColor = self.picker.pickerTextColor; + + // Retrieve the pre-fetched assets for this album: + PHFetchResult *assetsFetchResult = (self.collectionsFetchResultsAssets[(NSUInteger)indexPath.section])[(NSUInteger)indexPath.row]; + + // Display the number of assets + if (self.picker.displayAlbumsNumberOfAssets) { + cell.detailTextLabel.font = [UIFont fontWithName:self.picker.pickerFontName size:self.picker.pickerFontNormalSize]; + cell.detailTextLabel.text = [self tableCellSubtitle:assetsFetchResult]; + cell.detailTextLabel.textColor = self.picker.pickerTextColor; + } + + // Set the 3 images (if exists): + if ([assetsFetchResult count] > 0) { + CGFloat scale = [UIScreen mainScreen].scale; + + //Compute the thumbnail pixel size: + CGSize tableCellThumbnailSize1 = CGSizeMake(kAlbumThumbnailSize1.width*scale, kAlbumThumbnailSize1.height*scale); + PHAsset *asset = assetsFetchResult[0]; + [cell setVideoLayout:(asset.mediaType==PHAssetMediaTypeVideo)]; + [self.imageManager requestImageForAsset:asset + targetSize:tableCellThumbnailSize1 + contentMode:PHImageContentModeAspectFill + options:nil + resultHandler:^(UIImage *result, NSDictionary *info) { + if (cell.tag == currentTag) { + cell.imageView1.image = result; + } + }]; + + // Second & third images: + // TODO: Only preload the 3pixels height visible frame! + if ([assetsFetchResult count] > 1) { + //Compute the thumbnail pixel size: + CGSize tableCellThumbnailSize2 = CGSizeMake(kAlbumThumbnailSize2.width*scale, kAlbumThumbnailSize2.height*scale); + PHAsset *asset = assetsFetchResult[1]; + [self.imageManager requestImageForAsset:asset + targetSize:tableCellThumbnailSize2 + contentMode:PHImageContentModeAspectFill + options:nil + resultHandler:^(UIImage *result, NSDictionary *info) { + if (cell.tag == currentTag) { + cell.imageView2.image = result; + } + }]; + } else { + cell.imageView2.image = nil; + } + + if ([assetsFetchResult count] > 2) { + CGSize tableCellThumbnailSize3 = CGSizeMake(kAlbumThumbnailSize3.width*scale, kAlbumThumbnailSize3.height*scale); + PHAsset *asset = assetsFetchResult[2]; + [self.imageManager requestImageForAsset:asset + targetSize:tableCellThumbnailSize3 + contentMode:PHImageContentModeAspectFill + options:nil + resultHandler:^(UIImage *result, NSDictionary *info) { + if (cell.tag == currentTag) { + cell.imageView3.image = result; + } + }]; + } else { + cell.imageView3.image = nil; + } + } else { + [cell setVideoLayout:NO]; + cell.imageView3.image = [UIImage imageNamed:@"GMEmptyFolder"]; + cell.imageView2.image = [UIImage imageNamed:@"GMEmptyFolder"]; + cell.imageView1.image = [UIImage imageNamed:@"GMEmptyFolder"]; + } + + return cell; +} + +#pragma mark - UITableViewDelegate + +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath +{ + UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath]; + + // Init the GMGridViewController + GMGridViewController *gridViewController = [[GMGridViewController alloc] initWithPicker:[self picker]]; + // Set the title + gridViewController.title = cell.textLabel.text; + // Use the prefetched assets! + gridViewController.assetsFetchResults = [[_collectionsFetchResultsAssets objectAtIndex:(NSUInteger)indexPath.section] objectAtIndex:(NSUInteger)indexPath.row]; + + // Remove selection so it looks better on slide in + [tableView deselectRowAtIndexPath:indexPath animated:true]; + + // Push GMGridViewController + [self.navigationController pushViewController:gridViewController animated:YES]; +} + +-(void)tableView:(UITableView *)tableView willDisplayHeaderView:(UIView *)view forSection:(NSInteger)section +{ + UITableViewHeaderFooterView *header = (UITableViewHeaderFooterView *)view; + // header.contentView.backgroundColor = [UIColor clearColor]; + // header.backgroundView.backgroundColor = [UIColor clearColor]; + + // Default is a bold font, but keep this styled as a normal font + header.textLabel.font = [UIFont fontWithName:self.picker.pickerFontName size:self.picker.pickerFontNormalSize]; + header.textLabel.textColor = self.picker.pickerTextColor; +} + +- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section +{ + //Tip: Returning nil hides the section header! + + NSString *title = nil; + if (section > 0) { + // Only show title for non-empty sections: + PHFetchResult *fetchResult = self.collectionsFetchResultsAssets[(NSUInteger)section]; + if (fetchResult.count > 0) { + title = self.collectionsLocalizedTitles[(NSUInteger)(section - 1)]; + } + } + return title; +} + + +#pragma mark - PHPhotoLibraryChangeObserver + +- (void)photoLibraryDidChange:(PHChange *)changeInstance +{ + // Call might come on any background queue. Re-dispatch to the main queue to handle it. + dispatch_async(dispatch_get_main_queue(), ^{ + + NSMutableArray *updatedCollectionsFetchResults = nil; + + for (PHFetchResult *collectionsFetchResult in self.collectionsFetchResults) { + PHFetchResultChangeDetails *changeDetails = [changeInstance changeDetailsForFetchResult:collectionsFetchResult]; + if (changeDetails) { + if (!updatedCollectionsFetchResults) { + updatedCollectionsFetchResults = [self.collectionsFetchResults mutableCopy]; + } + [updatedCollectionsFetchResults replaceObjectAtIndex:[self.collectionsFetchResults indexOfObject:collectionsFetchResult] withObject:[changeDetails fetchResultAfterChanges]]; + } + } + + // This only affects to changes in albums level (add/remove/edit album) + if (updatedCollectionsFetchResults) { + self.collectionsFetchResults = updatedCollectionsFetchResults; + } + + // However, we want to update if photos are added, so the counts of items & thumbnails are updated too. + // Maybe some checks could be done here , but for now is OKey. + [self updateFetchResults]; + [self.tableView reloadData]; + + }); +} + +#pragma mark - Cell Subtitle + +- (NSString *)tableCellSubtitle:(PHFetchResult*)assetsFetchResult +{ + // Just return the number of assets. Album app does this: + return [NSString stringWithFormat:@"%ld", (long)[assetsFetchResult count]]; +} + +@end diff --git a/GMImagePicker/GMEmptyFolder@1x.png b/GMImagePicker/GMEmptyFolder@1x.png new file mode 100644 index 00000000..d2789c72 Binary files /dev/null and b/GMImagePicker/GMEmptyFolder@1x.png differ diff --git a/GMImagePicker/GMEmptyFolder@2x.png b/GMImagePicker/GMEmptyFolder@2x.png new file mode 100644 index 00000000..58848ae1 Binary files /dev/null and b/GMImagePicker/GMEmptyFolder@2x.png differ diff --git a/GMImagePicker/GMGridViewCell.h b/GMImagePicker/GMGridViewCell.h new file mode 100644 index 00000000..365cfdb1 --- /dev/null +++ b/GMImagePicker/GMGridViewCell.h @@ -0,0 +1,32 @@ +// +// GMGridViewCell.h +// GMPhotoPicker +// +// Created by Guillermo Muntaner Perelló on 19/09/14. +// Copyright (c) 2014 Guillermo Muntaner Perelló. All rights reserved. +// + +#include +#include + + +@interface GMGridViewCell : UICollectionViewCell + +@property (nonatomic, strong) PHAsset *asset; +//The imageView +@property (nonatomic, strong) UIImageView *imageView; +//Video additional information +@property (nonatomic, strong) UIImageView *videoIcon; +@property (nonatomic, strong) UILabel *videoDuration; +@property (nonatomic, strong) UIView *gradientView; +@property (nonatomic, strong) CAGradientLayer *gradient; +//Selection overlay +@property (nonatomic) BOOL shouldShowSelection; +@property (nonatomic, strong) UIView *coverView; +@property (nonatomic, strong) UIButton *selectedButton; + +@property (nonatomic, assign, getter = isEnabled) BOOL enabled; + +- (void)bind:(PHAsset *)asset; + +@end diff --git a/GMImagePicker/GMGridViewCell.m b/GMImagePicker/GMGridViewCell.m new file mode 100644 index 00000000..9bbc4c08 --- /dev/null +++ b/GMImagePicker/GMGridViewCell.m @@ -0,0 +1,176 @@ +// +// GMGridViewCell.m +// GMPhotoPicker +// +// Created by Guillermo Muntaner Perelló on 19/09/14. +// Copyright (c) 2014 Guillermo Muntaner Perelló. All rights reserved. +// + +#import "GMGridViewCell.h" + + +@interface GMGridViewCell () +@end + + +@implementation GMGridViewCell + +static UIFont *titleFont; +static CGFloat titleHeight; +static UIImage *videoIcon; +static UIColor *titleColor; +static UIImage *checkedIcon; +static UIColor *selectedColor; +static UIColor *disabledColor; + ++ (void)initialize +{ + titleFont = [UIFont systemFontOfSize:12]; + titleHeight = 20.0f; + videoIcon = [UIImage imageNamed:@"GMImagePickerVideo"]; + titleColor = [UIColor whiteColor]; + checkedIcon = [UIImage imageNamed:@"CTAssetsPickerChecked"]; + selectedColor = [UIColor colorWithWhite:1 alpha:0.3]; + disabledColor = [UIColor colorWithWhite:1 alpha:0.9]; +} + +- (void)awakeFromNib +{ + [super awakeFromNib]; + + self.contentView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth; + self.contentView.translatesAutoresizingMaskIntoConstraints = YES; +} + +- (id)initWithFrame:(CGRect)frame +{ + if (self = [super initWithFrame:frame]) { + self.opaque = NO; + self.enabled = YES; + + CGFloat cellSize = self.contentView.bounds.size.width; + + // The image view + _imageView = [UIImageView new]; + _imageView.frame = CGRectMake(0, 0, cellSize, cellSize); + _imageView.contentMode = UIViewContentModeScaleAspectFill; + /*if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) + { + _imageView.contentMode = UIViewContentModeScaleAspectFit; + } + else + { + _imageView.contentMode = UIViewContentModeScaleAspectFill; + }*/ + _imageView.clipsToBounds = YES; + _imageView.translatesAutoresizingMaskIntoConstraints = NO; + _imageView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth; + [self addSubview:_imageView]; + + + // The video gradient, label & icon + float x_offset = 4.0f; + UIColor *topGradient = [UIColor colorWithRed:0.00 green:0.00 blue:0.00 alpha:0.0]; + UIColor *botGradient = [UIColor colorWithRed:0.00 green:0.00 blue:0.00 alpha:0.8]; + _gradientView = [[UIView alloc] initWithFrame: CGRectMake(0.0f, self.bounds.size.height-titleHeight, self.bounds.size.width, titleHeight)]; + _gradient = [CAGradientLayer layer]; + _gradient.frame = _gradientView.bounds; + _gradient.colors = [NSArray arrayWithObjects:(id)[topGradient CGColor], (id)[botGradient CGColor], nil]; + [_gradientView.layer insertSublayer:_gradient atIndex:0]; + _gradientView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin; + _gradientView.translatesAutoresizingMaskIntoConstraints = NO; + [self addSubview:_gradientView]; + _gradientView.hidden = YES; + + _videoIcon = [UIImageView new]; + _videoIcon.frame = CGRectMake(x_offset, self.bounds.size.height-titleHeight, self.bounds.size.width-2*x_offset, titleHeight); + _videoIcon.contentMode = UIViewContentModeLeft; + _videoIcon.image = [UIImage imageNamed:@"GMVideoIcon" inBundle:[NSBundle bundleForClass:GMGridViewCell.class] compatibleWithTraitCollection:nil]; + _videoIcon.translatesAutoresizingMaskIntoConstraints = NO; + _videoIcon.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleWidth; + [self addSubview:_videoIcon]; + _videoIcon.hidden = YES; + + _videoDuration = [UILabel new]; + _videoDuration.font = titleFont; + _videoDuration.textColor = titleColor; + _videoDuration.textAlignment = NSTextAlignmentRight; + _videoDuration.frame = CGRectMake(x_offset, self.bounds.size.height-titleHeight, self.bounds.size.width-2*x_offset, titleHeight); + _videoDuration.contentMode = UIViewContentModeRight; + _videoDuration.translatesAutoresizingMaskIntoConstraints = NO; + _videoDuration.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleWidth; + [self addSubview:_videoDuration]; + _videoDuration.hidden = YES; + + // Selection overlay & icon + _coverView = [[UIView alloc] initWithFrame:self.bounds]; + _coverView.translatesAutoresizingMaskIntoConstraints = NO; + _coverView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth; + _coverView.backgroundColor = [UIColor colorWithRed:0.24 green:0.47 blue:0.85 alpha:0.6]; + [self addSubview:_coverView]; + _coverView.hidden = YES; + + _selectedButton = [UIButton buttonWithType:UIButtonTypeCustom]; + _selectedButton.frame = CGRectMake(2*self.bounds.size.width/3, 0*self.bounds.size.width/3, self.bounds.size.width/3, self.bounds.size.width/3); + _selectedButton.contentMode = UIViewContentModeTopRight; + _selectedButton.adjustsImageWhenHighlighted = NO; + [_selectedButton setImage:nil forState:UIControlStateNormal]; + _selectedButton.translatesAutoresizingMaskIntoConstraints = NO; + _selectedButton.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth; + [_selectedButton setImage:[UIImage imageNamed:@"GMSelected" inBundle:[NSBundle bundleForClass:GMGridViewCell.class] compatibleWithTraitCollection:nil] forState:UIControlStateSelected]; + _selectedButton.hidden = NO; + _selectedButton.userInteractionEnabled = NO; + [self addSubview:_selectedButton]; + } + + // Note: the views above are created in case this is toggled per cell, on the fly, etc.! + self.shouldShowSelection = YES; + + return self; +} + +// Required to resize the CAGradientLayer because it does not support auto resizing. +- (void)layoutSubviews { + [super layoutSubviews]; + _gradient.frame = _gradientView.bounds; +} + +- (void)bind:(PHAsset *)asset +{ + self.asset = asset; + + if (self.asset.mediaType == PHAssetMediaTypeVideo) { + _videoIcon.hidden = NO; + _videoDuration.hidden = NO; + _gradientView.hidden = NO; + _videoDuration.text = [self getDurationWithFormat:self.asset.duration]; + } else { + _videoIcon.hidden = YES; + _videoDuration.hidden = YES; + _gradientView.hidden = YES; + } +} + +// Override setSelected +- (void)setSelected:(BOOL)selected +{ + [super setSelected:selected]; + + if (!self.shouldShowSelection) { + return; + } + + _coverView.hidden = !selected; + _selectedButton.selected = selected; +} + +-(NSString*)getDurationWithFormat:(NSTimeInterval)duration +{ + NSInteger ti = (NSInteger)duration; + NSInteger seconds = ti % 60; + NSInteger minutes = (ti / 60) % 60; + //NSInteger hours = (ti / 3600); + return [NSString stringWithFormat:@"%02ld:%02ld", (long)minutes, (long)seconds]; +} + +@end diff --git a/GMImagePicker/GMGridViewController.h b/GMImagePicker/GMGridViewController.h new file mode 100644 index 00000000..f3941e46 --- /dev/null +++ b/GMImagePicker/GMGridViewController.h @@ -0,0 +1,21 @@ +// +// GMGridViewController.h +// GMPhotoPicker +// +// Created by Guillermo Muntaner Perelló on 19/09/14. +// Copyright (c) 2014 Guillermo Muntaner Perelló. All rights reserved. +// + + +#import "GMImagePickerController.h" +#include +#include + + +@interface GMGridViewController : UICollectionViewController + +@property (strong,nonatomic) PHFetchResult *assetsFetchResults; + +-(id)initWithPicker:(GMImagePickerController *)picker; + +@end diff --git a/GMImagePicker/GMGridViewController.m b/GMImagePicker/GMGridViewController.m new file mode 100644 index 00000000..3b33baa4 --- /dev/null +++ b/GMImagePicker/GMGridViewController.m @@ -0,0 +1,611 @@ +// +// GMGridViewController.m +// GMPhotoPicker +// +// Created by Guillermo Muntaner Perelló on 19/09/14. +// Copyright (c) 2014 Guillermo Muntaner Perelló. All rights reserved. +// + +#import "GMGridViewController.h" +#import "GMImagePickerController.h" +#import "GMAlbumsViewController.h" +#import "GMGridViewCell.h" + +#include + + +//Helper methods +@implementation NSIndexSet (Convenience) +- (NSArray *)aapl_indexPathsFromIndexesWithSection:(NSUInteger)section { + NSMutableArray *indexPaths = [NSMutableArray arrayWithCapacity:self.count]; + [self enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) { + [indexPaths addObject:[NSIndexPath indexPathForItem:(NSInteger)idx inSection:(NSInteger)section]]; + }]; + return indexPaths; +} +@end + +@implementation UICollectionView (Convenience) +- (NSArray *)aapl_indexPathsForElementsInRect:(CGRect)rect { + NSArray *allLayoutAttributes = [self.collectionViewLayout layoutAttributesForElementsInRect:rect]; + if (allLayoutAttributes.count == 0) { return nil; } + NSMutableArray *indexPaths = [NSMutableArray arrayWithCapacity:allLayoutAttributes.count]; + for (UICollectionViewLayoutAttributes *layoutAttributes in allLayoutAttributes) { + NSIndexPath *indexPath = layoutAttributes.indexPath; + [indexPaths addObject:indexPath]; + } + return indexPaths; +} +@end + + + +@interface GMImagePickerController () + +- (void)finishPickingAssets:(id)sender; +- (void)dismiss:(id)sender; +- (NSString *)toolbarTitle; +- (UIView *)noAssetsView; + +@end + + +@interface GMGridViewController () + +@property (nonatomic, weak) GMImagePickerController *picker; +@property (strong,nonatomic) PHCachingImageManager *imageManager; +@property (assign, nonatomic) CGRect previousPreheatRect; + +@end + +static CGSize AssetGridThumbnailSize; +NSString * const GMGridViewCellIdentifier = @"GMGridViewCellIdentifier"; + +@implementation GMGridViewController +{ + CGFloat screenWidth; + CGFloat screenHeight; + UICollectionViewFlowLayout *portraitLayout; + UICollectionViewFlowLayout *landscapeLayout; +} + +-(id)initWithPicker:(GMImagePickerController *)picker +{ + //Custom init. The picker contains custom information to create the FlowLayout + self.picker = picker; + + //Ipad popover is not affected by rotation! + if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) + { + screenWidth = CGRectGetWidth(picker.view.bounds); + screenHeight = CGRectGetHeight(picker.view.bounds); + } + else + { + if(UIInterfaceOrientationIsLandscape([UIApplication sharedApplication].statusBarOrientation)) + { + screenHeight = CGRectGetWidth(picker.view.bounds); + screenWidth = CGRectGetHeight(picker.view.bounds); + } + else + { + screenWidth = CGRectGetWidth(picker.view.bounds); + screenHeight = CGRectGetHeight(picker.view.bounds); + } + } + + + UICollectionViewFlowLayout *layout = [self collectionViewFlowLayoutForOrientation:[UIApplication sharedApplication].statusBarOrientation]; + if (self = [super initWithCollectionViewLayout:layout]) + { + //Compute the thumbnail pixel size: + CGFloat scale = [UIScreen mainScreen].scale; + //NSLog(@"This is @%fx scale device", scale); + if(scale >= 3) + { + scale = 2; + } + + AssetGridThumbnailSize = CGSizeMake(layout.itemSize.width * scale, layout.itemSize.height * scale); + + self.collectionView.allowsMultipleSelection = picker.allowsMultipleSelection; + + [self.collectionView registerClass:GMGridViewCell.class + forCellWithReuseIdentifier:GMGridViewCellIdentifier]; + + self.preferredContentSize = kPopoverContentSize; + } + + return self; +} + + +- (void)viewDidLoad +{ + [super viewDidLoad]; + [self setupViews]; + + // Navigation bar customization + if (self.picker.customNavigationBarPrompt) { + self.navigationItem.prompt = self.picker.customNavigationBarPrompt; + } + + self.imageManager = [[PHCachingImageManager alloc] init]; + [self resetCachedAssets]; + [[PHPhotoLibrary sharedPhotoLibrary] registerChangeObserver:self]; + + if ([self respondsToSelector:@selector(setEdgesForExtendedLayout:)]) + { + self.edgesForExtendedLayout = UIRectEdgeNone; + } +} + +- (void)viewWillAppear:(BOOL)animated +{ + [super viewWillAppear:animated]; + + [self setupButtons]; + [self setupToolbar]; +} + +- (void)viewDidAppear:(BOOL)animated +{ + [super viewDidAppear:animated]; + [self updateCachedAssets]; +} + +- (void)dealloc +{ + [self resetCachedAssets]; + [[PHPhotoLibrary sharedPhotoLibrary] unregisterChangeObserver:self]; +} + +- (UIStatusBarStyle)preferredStatusBarStyle { + return self.picker.pickerStatusBarStyle; +} + + +#pragma mark - Rotation + +- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration +{ + if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) { + return; + } + + UICollectionViewFlowLayout *layout = [self collectionViewFlowLayoutForOrientation:toInterfaceOrientation]; + + //Update the AssetGridThumbnailSize: + CGFloat scale = [UIScreen mainScreen].scale; + AssetGridThumbnailSize = CGSizeMake(layout.itemSize.width * scale, layout.itemSize.height * scale); + + [self resetCachedAssets]; + //This is optional. Reload visible thumbnails: + for (GMGridViewCell *cell in [self.collectionView visibleCells]) { + NSInteger currentTag = cell.tag; + [self.imageManager requestImageForAsset:cell.asset + targetSize:AssetGridThumbnailSize + contentMode:PHImageContentModeAspectFill + options:nil + resultHandler:^(UIImage *result, NSDictionary *info) + { + // Only update the thumbnail if the cell tag hasn't changed. Otherwise, the cell has been re-used. + if (cell.tag == currentTag) { + [cell.imageView setImage:result]; + } + }]; + } + + [self.collectionView setCollectionViewLayout:layout animated:YES]; +} + + +#pragma mark - Setup + +- (void)setupViews +{ + self.collectionView.backgroundColor = [UIColor clearColor]; + self.view.backgroundColor = [self.picker pickerBackgroundColor]; +} + +- (void)setupButtons +{ + if (self.picker.allowsMultipleSelection) { + NSString *doneTitle = self.picker.customDoneButtonTitle ? self.picker.customDoneButtonTitle : NSLocalizedStringFromTableInBundle(@"picker.navigation.done-button", @"GMImagePicker", [NSBundle bundleForClass:GMImagePickerController.class], @"Done"); + self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:doneTitle + style:UIBarButtonItemStyleDone + target:self.picker + action:@selector(finishPickingAssets:)]; + + self.navigationItem.rightBarButtonItem.enabled = (self.picker.autoDisableDoneButton ? self.picker.selectedAssets.count > 0 : TRUE); + } else { + NSString *cancelTitle = self.picker.customCancelButtonTitle ? self.picker.customCancelButtonTitle : NSLocalizedStringFromTableInBundle(@"picker.navigation.cancel-button", @"GMImagePicker", [NSBundle bundleForClass:GMImagePickerController.class], @"Cancel"); + self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:cancelTitle + style:UIBarButtonItemStyleDone + target:self.picker + action:@selector(dismiss:)]; + } + if (self.picker.useCustomFontForNavigationBar) { + if (self.picker.useCustomFontForNavigationBar) { + NSDictionary* barButtonItemAttributes = @{NSFontAttributeName: [UIFont fontWithName:self.picker.pickerFontName size:self.picker.pickerFontHeaderSize]}; + [self.navigationItem.rightBarButtonItem setTitleTextAttributes:barButtonItemAttributes forState:UIControlStateNormal]; + [self.navigationItem.rightBarButtonItem setTitleTextAttributes:barButtonItemAttributes forState:UIControlStateSelected]; + } + } + +} + +- (void)setupToolbar +{ + self.toolbarItems = self.picker.toolbarItems; +} + + +#pragma mark - Collection View Layout + +- (UICollectionViewFlowLayout *)collectionViewFlowLayoutForOrientation:(UIInterfaceOrientation)orientation +{ + if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) + { + if(!portraitLayout) + { + portraitLayout = [[UICollectionViewFlowLayout alloc] init]; + portraitLayout.minimumInteritemSpacing = self.picker.minimumInteritemSpacing; + int cellTotalUsableWidth = (int)(screenWidth - (self.picker.colsInPortrait-1)*self.picker.minimumInteritemSpacing); + portraitLayout.itemSize = CGSizeMake(cellTotalUsableWidth/self.picker.colsInPortrait, cellTotalUsableWidth/self.picker.colsInPortrait); + double cellTotalUsedWidth = (double)portraitLayout.itemSize.width*self.picker.colsInPortrait; + double spaceTotalWidth = (double)screenWidth-cellTotalUsedWidth; + double spaceWidth = spaceTotalWidth/(double)(self.picker.colsInPortrait-1); + portraitLayout.minimumLineSpacing = spaceWidth; + } + return portraitLayout; + } + else + { + if(UIInterfaceOrientationIsLandscape(orientation)) + { + if(!landscapeLayout) + { + landscapeLayout = [[UICollectionViewFlowLayout alloc] init]; + landscapeLayout.minimumInteritemSpacing = self.picker.minimumInteritemSpacing; + int cellTotalUsableWidth = (int)(screenHeight - (self.picker.colsInLandscape-1)*self.picker.minimumInteritemSpacing); + landscapeLayout.itemSize = CGSizeMake(cellTotalUsableWidth/self.picker.colsInLandscape, cellTotalUsableWidth/self.picker.colsInLandscape); + double cellTotalUsedWidth = (double)landscapeLayout.itemSize.width*self.picker.colsInLandscape; + double spaceTotalWidth = (double)screenHeight-cellTotalUsedWidth; + double spaceWidth = spaceTotalWidth/(double)(self.picker.colsInLandscape-1); + landscapeLayout.minimumLineSpacing = spaceWidth; + } + return landscapeLayout; + } + else + { + if(!portraitLayout) + { + portraitLayout = [[UICollectionViewFlowLayout alloc] init]; + portraitLayout.minimumInteritemSpacing = self.picker.minimumInteritemSpacing; + int cellTotalUsableWidth = (int)(screenWidth - (self.picker.colsInPortrait-1) * self.picker.minimumInteritemSpacing); + portraitLayout.itemSize = CGSizeMake(cellTotalUsableWidth/self.picker.colsInPortrait, cellTotalUsableWidth/self.picker.colsInPortrait); + double cellTotalUsedWidth = (double)portraitLayout.itemSize.width*self.picker.colsInPortrait; + double spaceTotalWidth = (double)screenWidth-cellTotalUsedWidth; + double spaceWidth = spaceTotalWidth/(double)(self.picker.colsInPortrait-1); + portraitLayout.minimumLineSpacing = spaceWidth; + } + return portraitLayout; + } + } +} + + +#pragma mark - Collection View Data Source + +- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView +{ + return 1; +} + + +- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath +{ + GMGridViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:GMGridViewCellIdentifier + forIndexPath:indexPath]; + + // Increment the cell's tag + NSInteger currentTag = cell.tag + 1; + cell.tag = currentTag; + + PHAsset *asset = self.assetsFetchResults[(NSUInteger)indexPath.item]; + + /*if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) + { + NSLog(@"Image manager: Requesting FIT image for iPad"); + [self.imageManager requestImageForAsset:asset + targetSize:AssetGridThumbnailSize + contentMode:PHImageContentModeAspectFit + options:nil + resultHandler:^(UIImage *result, NSDictionary *info) { + + // Only update the thumbnail if the cell tag hasn't changed. Otherwise, the cell has been re-used. + if (cell.tag == currentTag) { + [cell.imageView setImage:result]; + } + }]; + } + else*/ + { + //NSLog(@"Image manager: Requesting FILL image for iPhone"); + [self.imageManager requestImageForAsset:asset + targetSize:AssetGridThumbnailSize + contentMode:PHImageContentModeAspectFill + options:nil + resultHandler:^(UIImage *result, NSDictionary *info) { + + // Only update the thumbnail if the cell tag hasn't changed. Otherwise, the cell has been re-used. + if (cell.tag == currentTag) { + [cell.imageView setImage:result]; + } + }]; + } + + + [cell bind:asset]; + + cell.shouldShowSelection = self.picker.allowsMultipleSelection; + + // Optional protocol to determine if some kind of assets can't be selected (pej long videos, etc...) + if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:shouldEnableAsset:)]) { + cell.enabled = [self.picker.delegate assetsPickerController:self.picker shouldEnableAsset:asset]; + } else { + cell.enabled = YES; + } + + // Setting `selected` property blocks further deselection. Have to call selectItemAtIndexPath too. ( ref: http://stackoverflow.com/a/17812116/1648333 ) + if ([self.picker.selectedAssets containsObject:asset]) { + cell.selected = YES; + [collectionView selectItemAtIndexPath:indexPath animated:NO scrollPosition:UICollectionViewScrollPositionNone]; + } else { + cell.selected = NO; + } + + return cell; +} + + +#pragma mark - Collection View Delegate + +- (BOOL)collectionView:(UICollectionView *)collectionView shouldSelectItemAtIndexPath:(NSIndexPath *)indexPath +{ + PHAsset *asset = self.assetsFetchResults[(NSUInteger)indexPath.item]; + + GMGridViewCell *cell = (GMGridViewCell *)[collectionView cellForItemAtIndexPath:indexPath]; + + if (!cell.isEnabled) { + return NO; + } else if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:shouldSelectAsset:)]) { + return [self.picker.delegate assetsPickerController:self.picker shouldSelectAsset:asset]; + } else { + return YES; + } +} + +- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath +{ + PHAsset *asset = self.assetsFetchResults[(NSUInteger)indexPath.item]; + + [self.picker selectAsset:asset]; + + if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:didSelectAsset:)]) { + [self.picker.delegate assetsPickerController:self.picker didSelectAsset:asset]; + } +} + +- (BOOL)collectionView:(UICollectionView *)collectionView shouldDeselectItemAtIndexPath:(NSIndexPath *)indexPath +{ + PHAsset *asset = self.assetsFetchResults[(NSUInteger)indexPath.item]; + + if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:shouldDeselectAsset:)]) { + return [self.picker.delegate assetsPickerController:self.picker shouldDeselectAsset:asset]; + } else { + return YES; + } +} + +- (void)collectionView:(UICollectionView *)collectionView didDeselectItemAtIndexPath:(NSIndexPath *)indexPath +{ + PHAsset *asset = self.assetsFetchResults[(NSUInteger)indexPath.item]; + + [self.picker deselectAsset:asset]; + + if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:didDeselectAsset:)]) { + [self.picker.delegate assetsPickerController:self.picker didDeselectAsset:asset]; + } +} + +- (BOOL)collectionView:(UICollectionView *)collectionView shouldHighlightItemAtIndexPath:(NSIndexPath *)indexPath +{ + PHAsset *asset = self.assetsFetchResults[(NSUInteger)indexPath.item]; + + if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:shouldHighlightAsset:)]) { + return [self.picker.delegate assetsPickerController:self.picker shouldHighlightAsset:asset]; + } else { + return YES; + } +} + +- (void)collectionView:(UICollectionView *)collectionView didHighlightItemAtIndexPath:(NSIndexPath *)indexPath +{ + PHAsset *asset = self.assetsFetchResults[(NSUInteger)indexPath.item]; + + if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:didHighlightAsset:)]) { + [self.picker.delegate assetsPickerController:self.picker didHighlightAsset:asset]; + } +} + +- (void)collectionView:(UICollectionView *)collectionView didUnhighlightItemAtIndexPath:(NSIndexPath *)indexPath +{ + PHAsset *asset = self.assetsFetchResults[(NSUInteger)indexPath.item]; + + if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:didUnhighlightAsset:)]) { + [self.picker.delegate assetsPickerController:self.picker didUnhighlightAsset:asset]; + } +} + + + +#pragma mark - UICollectionViewDataSource + +- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section +{ + NSInteger count = (NSInteger)self.assetsFetchResults.count; + return count; +} + + +#pragma mark - PHPhotoLibraryChangeObserver + +- (void)photoLibraryDidChange:(PHChange *)changeInstance +{ + // Call might come on any background queue. Re-dispatch to the main queue to handle it. + dispatch_async(dispatch_get_main_queue(), ^{ + + // check if there are changes to the assets (insertions, deletions, updates) + PHFetchResultChangeDetails *collectionChanges = [changeInstance changeDetailsForFetchResult:self.assetsFetchResults]; + if (collectionChanges) { + + // get the new fetch result + self.assetsFetchResults = [collectionChanges fetchResultAfterChanges]; + + UICollectionView *collectionView = self.collectionView; + + if (![collectionChanges hasIncrementalChanges] || [collectionChanges hasMoves]) { + // we need to reload all if the incremental diffs are not available + [collectionView reloadData]; + + } else { + // if we have incremental diffs, tell the collection view to animate insertions and deletions + [collectionView performBatchUpdates:^{ + NSIndexSet *removedIndexes = [collectionChanges removedIndexes]; + if ([removedIndexes count]) { + [collectionView deleteItemsAtIndexPaths:[removedIndexes aapl_indexPathsFromIndexesWithSection:0]]; + } + NSIndexSet *insertedIndexes = [collectionChanges insertedIndexes]; + if ([insertedIndexes count]) { + [collectionView insertItemsAtIndexPaths:[insertedIndexes aapl_indexPathsFromIndexesWithSection:0]]; + if (self.picker.showCameraButton && self.picker.autoSelectCameraImages) { + for (NSIndexPath *path in [insertedIndexes aapl_indexPathsFromIndexesWithSection:0]) { + [self collectionView:collectionView didSelectItemAtIndexPath:path]; + } + } + } + NSIndexSet *changedIndexes = [collectionChanges changedIndexes]; + if ([changedIndexes count]) { + [collectionView reloadItemsAtIndexPaths:[changedIndexes aapl_indexPathsFromIndexesWithSection:0]]; + } + } completion:NULL]; + } + + [self resetCachedAssets]; + } + }); +} + + +#pragma mark - UIScrollViewDelegate + +- (void)scrollViewDidScroll:(UIScrollView *)scrollView +{ + [self updateCachedAssets]; +} + + +#pragma mark - Asset Caching + +- (void)resetCachedAssets +{ + [self.imageManager stopCachingImagesForAllAssets]; + self.previousPreheatRect = CGRectZero; +} + +- (void)updateCachedAssets +{ + BOOL isViewVisible = [self isViewLoaded] && [[self view] window] != nil; + if (!isViewVisible) { return; } + + // The preheat window is twice the height of the visible rect + CGRect preheatRect = self.collectionView.bounds; + preheatRect = CGRectInset(preheatRect, 0.0f, -0.5f * CGRectGetHeight(preheatRect)); + + // If scrolled by a "reasonable" amount... + CGFloat delta = ABS(CGRectGetMidY(preheatRect) - CGRectGetMidY(self.previousPreheatRect)); + if (delta > CGRectGetHeight(self.collectionView.bounds) / 3.0f) { + + // Compute the assets to start caching and to stop caching. + NSMutableArray *addedIndexPaths = [NSMutableArray array]; + NSMutableArray *removedIndexPaths = [NSMutableArray array]; + + [self computeDifferenceBetweenRect:self.previousPreheatRect andRect:preheatRect removedHandler:^(CGRect removedRect) { + NSArray *indexPaths = [self.collectionView aapl_indexPathsForElementsInRect:removedRect]; + [removedIndexPaths addObjectsFromArray:indexPaths]; + } addedHandler:^(CGRect addedRect) { + NSArray *indexPaths = [self.collectionView aapl_indexPathsForElementsInRect:addedRect]; + [addedIndexPaths addObjectsFromArray:indexPaths]; + }]; + + NSArray *assetsToStartCaching = [self assetsAtIndexPaths:addedIndexPaths]; + NSArray *assetsToStopCaching = [self assetsAtIndexPaths:removedIndexPaths]; + + [self.imageManager startCachingImagesForAssets:assetsToStartCaching + targetSize:AssetGridThumbnailSize + contentMode:PHImageContentModeAspectFill + options:nil]; + [self.imageManager stopCachingImagesForAssets:assetsToStopCaching + targetSize:AssetGridThumbnailSize + contentMode:PHImageContentModeAspectFill + options:nil]; + + self.previousPreheatRect = preheatRect; + } +} + +- (void)computeDifferenceBetweenRect:(CGRect)oldRect andRect:(CGRect)newRect removedHandler:(void (^)(CGRect removedRect))removedHandler addedHandler:(void (^)(CGRect addedRect))addedHandler +{ + if (CGRectIntersectsRect(newRect, oldRect)) { + CGFloat oldMaxY = CGRectGetMaxY(oldRect); + CGFloat oldMinY = CGRectGetMinY(oldRect); + CGFloat newMaxY = CGRectGetMaxY(newRect); + CGFloat newMinY = CGRectGetMinY(newRect); + if (newMaxY > oldMaxY) { + CGRect rectToAdd = CGRectMake(newRect.origin.x, oldMaxY, newRect.size.width, (newMaxY - oldMaxY)); + addedHandler(rectToAdd); + } + if (oldMinY > newMinY) { + CGRect rectToAdd = CGRectMake(newRect.origin.x, newMinY, newRect.size.width, (oldMinY - newMinY)); + addedHandler(rectToAdd); + } + if (newMaxY < oldMaxY) { + CGRect rectToRemove = CGRectMake(newRect.origin.x, newMaxY, newRect.size.width, (oldMaxY - newMaxY)); + removedHandler(rectToRemove); + } + if (oldMinY < newMinY) { + CGRect rectToRemove = CGRectMake(newRect.origin.x, oldMinY, newRect.size.width, (newMinY - oldMinY)); + removedHandler(rectToRemove); + } + } else { + addedHandler(newRect); + removedHandler(oldRect); + } +} + +- (NSArray *)assetsAtIndexPaths:(NSArray *)indexPaths +{ + if (indexPaths.count == 0) { return nil; } + + NSMutableArray *assets = [NSMutableArray arrayWithCapacity:indexPaths.count]; + for (NSIndexPath *indexPath in indexPaths) { + PHAsset *asset = self.assetsFetchResults[(NSUInteger)indexPath.item]; + [assets addObject:asset]; + } + return assets; +} + + +@end diff --git a/GMImagePicker/GMImagePicker.h b/GMImagePicker/GMImagePicker.h new file mode 100644 index 00000000..976715e6 --- /dev/null +++ b/GMImagePicker/GMImagePicker.h @@ -0,0 +1,24 @@ +// +// GMImagePicker.h +// GMImagePicker +// +// Created by Shadowfacts on 1/14/19. +// Copyright © 2019 Shadowfacts. All rights reserved. +// + +#import + +//! Project version number for GMImagePicker. +FOUNDATION_EXPORT double GMImagePickerVersionNumber; + +//! Project version string for GMImagePicker. +FOUNDATION_EXPORT const unsigned char GMImagePickerVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + +#import +#import +#import +#import +#import diff --git a/GMImagePicker/GMImagePickerController.h b/GMImagePicker/GMImagePickerController.h new file mode 100644 index 00000000..b5c1ae97 --- /dev/null +++ b/GMImagePicker/GMImagePickerController.h @@ -0,0 +1,332 @@ +// +// GMImagePickerController.h +// GMPhotoPicker +// +// Created by Guillermo Muntaner Perelló on 19/09/14. +// Copyright (c) 2014 Guillermo Muntaner Perelló. All rights reserved. +// + +#import +#import + + +//This is the default image picker size! +//static CGSize const kPopoverContentSize = {320, 480}; +//However, the iPad is 1024x768 so it can allow popups up to 768! +static CGSize const kPopoverContentSize = {480, 720}; + + +@protocol GMImagePickerControllerDelegate; + + +/** + * A controller that allows picking multiple photos and videos from user's photo library. + */ +@interface GMImagePickerController : UIViewController + +/** + * The assets picker’s delegate object. + */ +@property (nonatomic, weak) id delegate; + +/** + * It contains the selected `PHAsset` objects. The order of the objects is the selection order. + * + * You can add assets before presenting the picker to show the user some preselected assets. + */ +@property (nonatomic, strong) NSMutableArray *selectedAssets; + + +/** UI Customizations **/ + +/** + * Determines which smart collections are displayed (int array of enum: PHAssetCollectionSubtypeSmartAlbum) + * The default smart collections are: + * - Favorites + * - RecentlyAdded + * - Videos + * - SlomoVideos + * - Timelapses + * - Bursts + * - Panoramas + */ +@property (nonatomic, strong) NSArray* customSmartCollections; + +/** + * Determines which media types are allowed (int array of enum: PHAssetMediaType) + * This defaults to all media types (view, audio and images) + * This can override customSmartCollections behavior (ie, remove video-only smart collections) + */ +@property (nonatomic, strong) NSArray* mediaTypes; + +/** + * If set, it displays a this string instead of the localised default of "Done" on the done button. Note also that this + * is not used when a single selection is active since the selection of the chosen photo closes the VC thus rendering + * the button pointless. + */ +@property (nonatomic) NSString* customDoneButtonTitle; + +/** + * If set, it displays this string instead of the localised default of "Cancel" on the cancel button + */ +@property (nonatomic) NSString* customCancelButtonTitle; + +/** + * If set, it displays a prompt in the navigation bar + */ +@property (nonatomic) NSString* customNavigationBarPrompt; + +/** + * Determines whether or not a toolbar with info about user selection is shown. + * The InfoToolbar is visible by default. + */ +@property (nonatomic) BOOL displaySelectionInfoToolbar; + +/** + * Determines whether or not the number of assets is shown in the Album list. + * The number of assets is visible by default. + */ +@property (nonatomic, assign) BOOL displayAlbumsNumberOfAssets; + +/** + * Automatically disables the "Done" button if nothing is selected. Defaults to YES. + */ +@property (nonatomic, assign) BOOL autoDisableDoneButton; + +/** + * Use the picker either for miltiple image selections, or just a single selection. In the case of a single selection + * the VC is closed on selection so the Done button is neither displayed or used. Default is YES. + */ +@property (nonatomic, assign) BOOL allowsMultipleSelection; + +/** + * In the case where allowsMultipleSelection = NO, set this to YES to have the user confirm their selection. Default is NO. + */ +@property (nonatomic, assign) BOOL confirmSingleSelection; + +/** + * If set, it displays this string (if confirmSingleSelection = YES) instead of the localised default. + */ +@property (nonatomic) NSString *confirmSingleSelectionPrompt; + +/** + * True to always show the toolbar, with a camera button allowing new photos to be taken. False to auto show/hide the + * toolbar, and have no camera button. Default is false. If true, this renders displaySelectionInfoToolbar a no-op. + */ +@property (nonatomic, assign) BOOL showCameraButton; + +/** + * True to auto select the image(s) taken with the camera if showCameraButton = YES. In the case of allowsMultipleSelection = YES, + * this will trigger the selection handler too. + */ +@property (nonatomic, assign) BOOL autoSelectCameraImages; + +/** + * If set, the user is allowed to edit captured still images + */ +@property (nonatomic, assign) BOOL allowsEditingCameraImages; + +/** + * Grid customizations: + * + * - colsInPortrait: Number of columns in portrait (3 by default) + * - colsInLandscape: Number of columns in landscape (5 by default) + * - minimumInteritemSpacing: Horizontal and vertical minimum space between grid cells (2.0 by default) + */ +@property (nonatomic) NSInteger colsInPortrait; +@property (nonatomic) NSInteger colsInLandscape; +@property (nonatomic) double minimumInteritemSpacing; + +/** + * UI customizations: + * + * - pickerBackgroundColor: The colour for all backgrounds; behind the table and cells. Defaults to [UIColor whiteColor] + * - pickerTextColor: The color for text in the views. This needs to work with pickerBackgroundColor! Default of darkTextColor + * - toolbarBackgroundColor: The background color of the toolbar. Defaults to nil. + * - toolbarBarTintColor: The color for the background tint of the toolbar. Defaults to nil. + * - toolbarTextColor: The color of the text on the toolbar + * - toolbarTintColor: The tint colour used for any buttons on the toolbar + * - navigationBarBackgroundColor: The background of the navigation bar. Defaults to nil. + * - navigationBarBarTintColor: The color for the background tint of the navigation bar. Defaults to nil. + * - navigationBarTextColor: The color for the text in the navigation bar. Defaults to [UIColor darkTextColor] + * - navigationBarTintColor: The tint color used for any buttons on the navigation Bar + * - pickerFontName: The font to use everywhere. Defaults to HelveticaNeue. It is advised if you set this to check, and possibly set, appropriately the custom font sizes. For font information, check http://www.iosfonts.com/ + * - pickerFontName: The font to use everywhere. Defaults to HelveticaNeue-Bold. It is advised if you set this to check, and possibly set, appropriately the custom font sizes. + * - pickerFontNormalSize: The size of the custom font used in most places. Defaults to 14.0f + * - pickerFontHeaderSize: The size of the custom font for album names. Defaults to 17.0f + * - pickerStatusBarsStyle: On iPhones this will matter if custom navigation bar colours are being used. Defaults to UIStatusBarStyleDefault + * - useCustomFontForNavigationBar: True to use the custom font (or it's default) in the navigation bar, false to leave to iOS Defaults. + * - arrangeSmartCollectionsFirst: True will put the users smart collections above their albums, false will set it opposite. Default is NO. + */ +@property (nonatomic, strong) UIColor *pickerBackgroundColor; +@property (nonatomic, strong) UIColor *pickerTextColor; +@property (nonatomic, strong) UIColor *toolbarBackgroundColor; +@property (nonatomic, strong) UIColor *toolbarBarTintColor; +@property (nonatomic, strong) UIColor *toolbarTextColor; +@property (nonatomic, strong) UIColor *toolbarTintColor; +@property (nonatomic, strong) UIColor *navigationBarBackgroundColor; +@property (nonatomic, strong) UIColor *navigationBarBarTintColor; +@property (nonatomic, strong) UIColor *navigationBarTextColor; +@property (nonatomic, strong) UIColor *navigationBarTintColor; +@property (nonatomic, strong) NSString *pickerFontName; +@property (nonatomic, strong) NSString *pickerBoldFontName; +@property (nonatomic) CGFloat pickerFontNormalSize; +@property (nonatomic) CGFloat pickerFontHeaderSize; +@property (nonatomic) UIStatusBarStyle pickerStatusBarStyle; +@property (nonatomic) BOOL useCustomFontForNavigationBar; +@property (nonatomic) BOOL arrangeSmartCollectionsFirst; + +/** + * A reference to the navigation controller used to manage the whole picking process + */ +@property (nonatomic, strong) UINavigationController *navigationController; + +/** + * Managing Asset Selection + */ +- (void)selectAsset:(PHAsset *)asset; +- (void)deselectAsset:(PHAsset *)asset; + +/** + * User finish Actions + */ +- (void)dismiss:(id)sender; +- (void)finishPickingAssets:(id)sender; + +@end + + + +@protocol GMImagePickerControllerDelegate + +/** + * @name Closing the Picker + */ + +/** + * Tells the delegate that the user finish picking photos or videos. + * @param picker The controller object managing the assets picker interface. + * @param assets An array containing picked PHAssets objects. + */ + +- (void)assetsPickerController:(GMImagePickerController *)picker didFinishPickingAssets:(NSArray *)assets; + + +@optional + +/** + * Tells the delegate that the user cancelled the pick operation. + * @param picker The controller object managing the assets picker interface. + */ +- (void)assetsPickerControllerDidCancel:(GMImagePickerController *)picker; + + +/** + * @name Enabling Assets + */ + +/** + * Ask the delegate if the specified asset should be shown. + * + * @param picker The controller object managing the assets picker interface. + * @param asset The asset to be shown. + * + * @return `YES` if the asset should be shown or `NO` if it should not. + */ + +- (BOOL)assetsPickerController:(GMImagePickerController *)picker shouldShowAsset:(PHAsset *)asset; + +/** + * Ask the delegate if the specified asset should be enabled for selection. + * + * @param picker The controller object managing the assets picker interface. + * @param asset The asset to be enabled. + * + * @return `YES` if the asset should be enabled or `NO` if it should not. + */ +- (BOOL)assetsPickerController:(GMImagePickerController *)picker shouldEnableAsset:(PHAsset *)asset; + + +/** + * @name Managing the Selected Assets + */ + +/** + * Asks the delegate if the specified asset should be selected. + * + * @param picker The controller object managing the assets picker interface. + * @param asset The asset to be selected. + * + * @return `YES` if the asset should be selected or `NO` if it should not. + * + */ +- (BOOL)assetsPickerController:(GMImagePickerController *)picker shouldSelectAsset:(PHAsset *)asset; + +/** + * Tells the delegate that the asset was selected. + * + * @param picker The controller object managing the assets picker interface. + * @param asset The asset that was selected. + * + */ +- (void)assetsPickerController:(GMImagePickerController *)picker didSelectAsset:(PHAsset *)asset; + +/** + * Asks the delegate if the specified asset should be deselected. + * + * @param picker The controller object managing the assets picker interface. + * @param asset The asset to be deselected. + * + * @return `YES` if the asset should be deselected or `NO` if it should not. + * + */ +- (BOOL)assetsPickerController:(GMImagePickerController *)picker shouldDeselectAsset:(PHAsset *)asset; + +/** + * Tells the delegate that the item at the specified path was deselected. + * + * @param picker The controller object managing the assets picker interface. + * @param asset The asset that was deselected. + * + */ +- (void)assetsPickerController:(GMImagePickerController *)picker didDeselectAsset:(PHAsset *)asset; + + + +/** + * @name Managing Asset Highlighting + */ + +/** + * Asks the delegate if the specified asset should be highlighted. + * + * @param picker The controller object managing the assets picker interface. + * @param asset The asset to be highlighted. + * + * @return `YES` if the asset should be highlighted or `NO` if it should not. + */ +- (BOOL)assetsPickerController:(GMImagePickerController *)picker shouldHighlightAsset:(PHAsset *)asset; + +/** + * Tells the delegate that asset was highlighted. + * + * @param picker The controller object managing the assets picker interface. + * @param asset The asset that was highlighted. + * + */ +- (void)assetsPickerController:(GMImagePickerController *)picker didHighlightAsset:(PHAsset *)asset; + + +/** + * Tells the delegate that the highlight was removed from the asset. + * + * @param picker The controller object managing the assets picker interface. + * @param asset The asset that had its highlight removed. + * + */ +- (void)assetsPickerController:(GMImagePickerController *)picker didUnhighlightAsset:(PHAsset *)asset; + + + + +@end diff --git a/GMImagePicker/GMImagePickerController.m b/GMImagePicker/GMImagePickerController.m new file mode 100644 index 00000000..f1836abf --- /dev/null +++ b/GMImagePicker/GMImagePickerController.m @@ -0,0 +1,388 @@ +// +// GMImagePickerController.m +// GMPhotoPicker +// +// Created by Guillermo Muntaner Perelló on 19/09/14. +// Copyright (c) 2014 Guillermo Muntaner Perelló. All rights reserved. +// + +#import +#import "GMImagePickerController.h" +#import "GMAlbumsViewController.h" +#import + +@interface GMImagePickerController () + +@end + +@implementation GMImagePickerController + +- (id)init +{ + if (self = [super init]) { + _selectedAssets = [[NSMutableArray alloc] init]; + + // Default values: + _displaySelectionInfoToolbar = YES; + _displayAlbumsNumberOfAssets = YES; + _autoDisableDoneButton = YES; + _allowsMultipleSelection = YES; + _confirmSingleSelection = NO; + _showCameraButton = NO; + + // Grid configuration: + _colsInPortrait = 3; + _colsInLandscape = 5; + _minimumInteritemSpacing = 2.0; + + // Sample of how to select the collections you want to display: + _customSmartCollections = @[@(PHAssetCollectionSubtypeSmartAlbumFavorites), + @(PHAssetCollectionSubtypeSmartAlbumRecentlyAdded), + @(PHAssetCollectionSubtypeSmartAlbumVideos), + @(PHAssetCollectionSubtypeSmartAlbumSlomoVideos), + @(PHAssetCollectionSubtypeSmartAlbumTimelapses), + @(PHAssetCollectionSubtypeSmartAlbumBursts), + @(PHAssetCollectionSubtypeSmartAlbumPanoramas)]; + // If you don't want to show smart collections, just put _customSmartCollections to nil; + //_customSmartCollections=nil; + + // Which media types will display + _mediaTypes = @[@(PHAssetMediaTypeAudio), + @(PHAssetMediaTypeVideo), + @(PHAssetMediaTypeImage)]; + + self.preferredContentSize = kPopoverContentSize; + + // UI Customisation + _pickerBackgroundColor = [UIColor whiteColor]; + _pickerTextColor = [UIColor darkTextColor]; + _pickerFontName = @"HelveticaNeue"; + _pickerBoldFontName = @"HelveticaNeue-Bold"; + _pickerFontNormalSize = 14.0f; + _pickerFontHeaderSize = 17.0f; + + _navigationBarBackgroundColor = [UIColor whiteColor]; + _navigationBarTextColor = [UIColor darkTextColor]; + _navigationBarTintColor = [UIColor darkTextColor]; + + _toolbarBarTintColor = [UIColor whiteColor]; + _toolbarTextColor = [UIColor darkTextColor]; + _toolbarTintColor = [UIColor darkTextColor]; + + _pickerStatusBarStyle = UIStatusBarStyleDefault; + + [self setupNavigationController]; + } + return self; +} + +- (void)viewWillAppear:(BOOL)animated +{ + [super viewWillAppear:animated]; + + // Ensure nav and toolbar customisations are set. Defaults are in place, but the user may have changed them + self.view.backgroundColor = _pickerBackgroundColor; + + _navigationController.toolbar.translucent = YES; + _navigationController.toolbar.barTintColor = _toolbarBarTintColor; + _navigationController.toolbar.tintColor = _toolbarTintColor; + + _navigationController.navigationBar.backgroundColor = _navigationBarBackgroundColor; + _navigationController.navigationBar.tintColor = _navigationBarTintColor; + NSDictionary *attributes; + if (_useCustomFontForNavigationBar) { + attributes = @{NSForegroundColorAttributeName : _navigationBarTextColor, + NSFontAttributeName : [UIFont fontWithName:_pickerBoldFontName size:_pickerFontHeaderSize]}; + } else { + attributes = @{NSForegroundColorAttributeName : _navigationBarTextColor}; + } + _navigationController.navigationBar.titleTextAttributes = attributes; + + [self updateToolbar]; +} + +- (UIStatusBarStyle)preferredStatusBarStyle { + return _pickerStatusBarStyle; +} + + +#pragma mark - Setup Navigation Controller + +- (void)setupNavigationController +{ + GMAlbumsViewController *albumsViewController = [[GMAlbumsViewController alloc] init]; + _navigationController = [[UINavigationController alloc] initWithRootViewController:albumsViewController]; + _navigationController.delegate = self; + [_navigationController.navigationBar setTranslucent:NO]; + [_navigationController willMoveToParentViewController:self]; + [_navigationController.view setFrame:self.view.frame]; + [self.view addSubview:_navigationController.view]; + [self addConstraintsToChildViewControllersView:_navigationController.view]; + [self addChildViewController:_navigationController]; + [_navigationController didMoveToParentViewController:self]; +} + +- (void)addConstraintsToChildViewControllersView:(UIView *)view { + view.translatesAutoresizingMaskIntoConstraints = NO; + NSArray * hConstraints = [NSLayoutConstraint constraintsWithVisualFormat:@"H:|-0-[view]-0-|" options:0 metrics:0 views:NSDictionaryOfVariableBindings(view)]; + NSLayoutConstraint * topConstraint = [NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.topLayoutGuide attribute:NSLayoutAttributeBottom multiplier:1 constant:0]; + NSLayoutConstraint * bottomConstraint = [NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:view.superview attribute:NSLayoutAttributeBottom multiplier:1 constant:0]; + [view.superview addConstraints:@[topConstraint,bottomConstraint]]; + [view.superview addConstraints:hConstraints]; +} + + +#pragma mark - UIAlertViewDelegate + +-(void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex +{ + if (buttonIndex == 1) { + // Only if OK was pressed do we want to completge the selection + [self finishPickingAssets:self]; + } +} + + +#pragma mark - Select / Deselect Asset + +- (void)selectAsset:(PHAsset *)asset +{ + [self.selectedAssets insertObject:asset atIndex:self.selectedAssets.count]; + [self updateDoneButton]; + + if (!self.allowsMultipleSelection) { + if (self.confirmSingleSelection) { + NSString *message = self.confirmSingleSelectionPrompt ? self.confirmSingleSelectionPrompt : [NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"picker.confirm.message", @"GMImagePicker", [NSBundle bundleForClass:GMImagePickerController.class], @"Do you want to select the image you tapped on?")]; + + [[[UIAlertView alloc] initWithTitle:[NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"picker.confirm.title", @"GMImagePicker", [NSBundle bundleForClass:GMImagePickerController.class], @"Are You Sure?")] + message:message + delegate:self + cancelButtonTitle:[NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"picker.action.no", @"GMImagePicker", [NSBundle bundleForClass:GMImagePickerController.class], @"No")] + otherButtonTitles:[NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"picker.action.yes", @"GMImagePicker", [NSBundle bundleForClass:GMImagePickerController.class], @"Yes")], nil] show]; + } else { + [self finishPickingAssets:self]; + } + } else if (self.displaySelectionInfoToolbar || self.showCameraButton) { + [self updateToolbar]; + } +} + +- (void)deselectAsset:(PHAsset *)asset +{ + [self.selectedAssets removeObjectAtIndex:[self.selectedAssets indexOfObject:asset]]; + if (self.selectedAssets.count == 0) { + [self updateDoneButton]; + } + + if (self.displaySelectionInfoToolbar || self.showCameraButton) { + [self updateToolbar]; + } +} + +- (void)updateDoneButton +{ + if (!self.allowsMultipleSelection) { + return; + } + + UINavigationController *nav = (UINavigationController *)self.childViewControllers[0]; + for (UIViewController *viewController in nav.viewControllers) { + viewController.navigationItem.rightBarButtonItem.enabled = (self.autoDisableDoneButton ? self.selectedAssets.count > 0 : TRUE); + } +} + +- (void)updateToolbar +{ + if (!self.allowsMultipleSelection && !self.showCameraButton) { + return; + } + + UINavigationController *nav = (UINavigationController *)self.childViewControllers[0]; + for (UIViewController *viewController in nav.viewControllers) { + NSUInteger index = 1; + if (_showCameraButton) { + index++; + } + [[viewController.toolbarItems objectAtIndex:index] setTitleTextAttributes:[self toolbarTitleTextAttributes] forState:UIControlStateNormal]; + [[viewController.toolbarItems objectAtIndex:index] setTitleTextAttributes:[self toolbarTitleTextAttributes] forState:UIControlStateDisabled]; + [[viewController.toolbarItems objectAtIndex:index] setTitle:[self toolbarTitle]]; + [viewController.navigationController setToolbarHidden:(self.selectedAssets.count == 0 && !self.showCameraButton) animated:YES]; + } +} + + +#pragma mark - User finish Actions + +- (void)dismiss:(id)sender +{ + if ([self.delegate respondsToSelector:@selector(assetsPickerControllerDidCancel:)]) { + [self.delegate assetsPickerControllerDidCancel:self]; + } + + [self.presentingViewController dismissViewControllerAnimated:YES completion:nil]; +} + + +- (void)finishPickingAssets:(id)sender +{ + if ([self.delegate respondsToSelector:@selector(assetsPickerController:didFinishPickingAssets:)]) { + [self.delegate assetsPickerController:self didFinishPickingAssets:self.selectedAssets]; + } +} + + +#pragma mark - Toolbar Title + +- (NSPredicate *)predicateOfAssetType:(PHAssetMediaType)type +{ + return [NSPredicate predicateWithBlock:^BOOL(PHAsset *asset, NSDictionary *bindings) { + return (asset.mediaType == type); + }]; +} + +- (NSString *)toolbarTitle +{ + if (self.selectedAssets.count == 0) { + return nil; + } + + NSPredicate *photoPredicate = [self predicateOfAssetType:PHAssetMediaTypeImage]; + NSPredicate *videoPredicate = [self predicateOfAssetType:PHAssetMediaTypeVideo]; + + NSInteger nImages = [self.selectedAssets filteredArrayUsingPredicate:photoPredicate].count; + NSInteger nVideos = [self.selectedAssets filteredArrayUsingPredicate:videoPredicate].count; + + if (nImages > 0 && nVideos > 0) { + return [NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"picker.selection.multiple-items", @"GMImagePicker", [NSBundle bundleForClass:GMImagePickerController.class], @"%@ Items Selected" ), @(nImages + nVideos)]; + } else if (nImages > 1) { + return [NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"picker.selection.multiple-photos", @"GMImagePicker", [NSBundle bundleForClass:GMImagePickerController.class], @"%@ Photos Selected"), @(nImages)]; + } else if (nImages == 1) { + return NSLocalizedStringFromTableInBundle(@"picker.selection.single-photo", @"GMImagePicker", [NSBundle bundleForClass:GMImagePickerController.class], @"1 Photo Selected" ); + } else if (nVideos > 1) { + return [NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"picker.selection.multiple-videos", @"GMImagePicker", [NSBundle bundleForClass:GMImagePickerController.class], @"%@ Videos Selected"), @(nVideos)]; + } else if (nVideos == 1) { + return NSLocalizedStringFromTableInBundle(@"picker.selection.single-video", @"GMImagePicker", [NSBundle bundleForClass:GMImagePickerController.class], @"1 Video Selected"); + } else { + return nil; + } +} + + +#pragma mark - Toolbar Items + +- (void)cameraButtonPressed:(UIBarButtonItem *)button +{ + if (![UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) { + UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"No Camera!" + message:@"Sorry, this device does not have a camera." + delegate:nil + cancelButtonTitle:@"OK" + otherButtonTitles:nil]; + [alert show]; + + return; + } + + // This allows the selection of the image taken to be better seen if the user is not already in that VC + if (self.autoSelectCameraImages && [self.navigationController.topViewController isKindOfClass:[GMAlbumsViewController class]]) { + [((GMAlbumsViewController *)self.navigationController.topViewController) selectAllAlbumsCell]; + } + + UIImagePickerController *picker = [[UIImagePickerController alloc] init]; + picker.sourceType = UIImagePickerControllerSourceTypeCamera; + picker.mediaTypes = @[(NSString *)kUTTypeImage]; + picker.allowsEditing = self.allowsEditingCameraImages; + picker.delegate = self; + picker.modalPresentationStyle = UIModalPresentationPopover; + + UIPopoverPresentationController *popPC = picker.popoverPresentationController; + popPC.permittedArrowDirections = UIPopoverArrowDirectionAny; + popPC.barButtonItem = button; + + [self showViewController:picker sender:button]; +} + +- (NSDictionary *)toolbarTitleTextAttributes { + return @{NSForegroundColorAttributeName : _toolbarTextColor, + NSFontAttributeName : [UIFont fontWithName:_pickerFontName size:_pickerFontHeaderSize]}; +} + +- (UIBarButtonItem *)titleButtonItem +{ + UIBarButtonItem *title = [[UIBarButtonItem alloc] initWithTitle:self.toolbarTitle + style:UIBarButtonItemStylePlain + target:nil + action:nil]; + + NSDictionary *attributes = [self toolbarTitleTextAttributes]; + [title setTitleTextAttributes:attributes forState:UIControlStateNormal]; + [title setTitleTextAttributes:attributes forState:UIControlStateDisabled]; + [title setEnabled:NO]; + + return title; +} + +- (UIBarButtonItem *)spaceButtonItem +{ + return [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil]; +} + +- (UIBarButtonItem *)cameraButtonItem +{ + return [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCamera target:self action:@selector(cameraButtonPressed:)]; +} + +- (NSArray *)toolbarItems +{ + UIBarButtonItem *camera = [self cameraButtonItem]; + UIBarButtonItem *title = [self titleButtonItem]; + UIBarButtonItem *space = [self spaceButtonItem]; + + NSMutableArray *items = [[NSMutableArray alloc] init]; + if (_showCameraButton) { + [items addObject:camera]; + } + [items addObject:space]; + [items addObject:title]; + [items addObject:space]; + + return [NSArray arrayWithArray:items]; +} + + +#pragma mark - Camera Delegate + +- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info +{ + [picker.presentingViewController dismissViewControllerAnimated:YES completion:nil]; + + NSString *mediaType = info[UIImagePickerControllerMediaType]; + if ([mediaType isEqualToString:(NSString *)kUTTypeImage]) { + UIImage *image = info[UIImagePickerControllerEditedImage] ? : info[UIImagePickerControllerOriginalImage]; + UIImageWriteToSavedPhotosAlbum(image, + self, + @selector(image:finishedSavingWithError:contextInfo:), + nil); + } +} + +-(void)imagePickerControllerDidCancel:(UIImagePickerController *)picker +{ + [picker.presentingViewController dismissViewControllerAnimated:YES completion:nil]; +} + +-(void)image:(UIImage *)image finishedSavingWithError:(NSError *)error contextInfo:(void *)contextInfo +{ + if (error) { + UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Image Not Saved" + message:@"Sorry, unable to save the new image!" + delegate:nil + cancelButtonTitle:@"OK" + otherButtonTitles:nil]; + [alert show]; + } + + // Note: The image view will auto refresh as the photo's are being observed in the other VCs +} + +@end diff --git a/GMImagePicker/GMSelected.png b/GMImagePicker/GMSelected.png new file mode 100755 index 00000000..b251b6cd Binary files /dev/null and b/GMImagePicker/GMSelected.png differ diff --git a/GMImagePicker/GMSelected@2x.png b/GMImagePicker/GMSelected@2x.png new file mode 100755 index 00000000..5a8f6f8c Binary files /dev/null and b/GMImagePicker/GMSelected@2x.png differ diff --git a/GMImagePicker/GMVideoIcon.png b/GMImagePicker/GMVideoIcon.png new file mode 100644 index 00000000..865e68d2 Binary files /dev/null and b/GMImagePicker/GMVideoIcon.png differ diff --git a/GMImagePicker/GMVideoIcon@2x.png b/GMImagePicker/GMVideoIcon@2x.png new file mode 100644 index 00000000..4ed325c5 Binary files /dev/null and b/GMImagePicker/GMVideoIcon@2x.png differ diff --git a/GMImagePicker/Info.plist b/GMImagePicker/Info.plist new file mode 100644 index 00000000..e1fe4cfb --- /dev/null +++ b/GMImagePicker/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + + diff --git a/GMImagePicker/ca.lproj/GMImagePicker.strings b/GMImagePicker/ca.lproj/GMImagePicker.strings new file mode 100644 index 00000000..b33a4deb Binary files /dev/null and b/GMImagePicker/ca.lproj/GMImagePicker.strings differ diff --git a/GMImagePicker/de.lproj/GMImagePicker.strings b/GMImagePicker/de.lproj/GMImagePicker.strings new file mode 100644 index 00000000..0e384dce Binary files /dev/null and b/GMImagePicker/de.lproj/GMImagePicker.strings differ diff --git a/GMImagePicker/en.lproj/GMImagePicker.strings b/GMImagePicker/en.lproj/GMImagePicker.strings new file mode 100644 index 00000000..b1836b63 Binary files /dev/null and b/GMImagePicker/en.lproj/GMImagePicker.strings differ diff --git a/GMImagePicker/es.lproj/GMImagePicker.strings b/GMImagePicker/es.lproj/GMImagePicker.strings new file mode 100644 index 00000000..147c276c Binary files /dev/null and b/GMImagePicker/es.lproj/GMImagePicker.strings differ diff --git a/GMImagePicker/fr.lproj/GMImagePicker.strings b/GMImagePicker/fr.lproj/GMImagePicker.strings new file mode 100644 index 00000000..0be5d582 Binary files /dev/null and b/GMImagePicker/fr.lproj/GMImagePicker.strings differ diff --git a/GMImagePicker/it.lproj/GMImagePicker.strings b/GMImagePicker/it.lproj/GMImagePicker.strings new file mode 100644 index 00000000..f6946b32 Binary files /dev/null and b/GMImagePicker/it.lproj/GMImagePicker.strings differ diff --git a/GMImagePicker/pt.lproj/GMImagePicker.strings b/GMImagePicker/pt.lproj/GMImagePicker.strings new file mode 100644 index 00000000..2f3caedc Binary files /dev/null and b/GMImagePicker/pt.lproj/GMImagePicker.strings differ diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 3ca328ef..befce5b8 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -18,6 +18,10 @@ 04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8D212CC7CC009840C4 /* ImageCache.swift */; }; 04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04ED00B021481ED800567C53 /* SteppedProgressView.swift */; }; D6028B9B2150811100F223B9 /* MastodonCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6028B9A2150811100F223B9 /* MastodonCache.swift */; }; + D60A548F21ED515800F1F87C /* GMImagePicker.h in Headers */ = {isa = PBXBuildFile; fileRef = D60A548D21ED515800F1F87C /* GMImagePicker.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D60A549221ED515800F1F87C /* GMImagePicker.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D60A548B21ED515800F1F87C /* GMImagePicker.framework */; }; + D60A549321ED515800F1F87C /* GMImagePicker.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D60A548B21ED515800F1F87C /* GMImagePicker.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + D60C07E421E8176B0057FAA8 /* ComposeMediaView.xib in Resources */ = {isa = PBXBuildFile; fileRef = D60C07E321E8176B0057FAA8 /* ComposeMediaView.xib */; }; D61099B42144B0CC00432DC2 /* Pachyderm.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D61099AB2144B0CC00432DC2 /* Pachyderm.framework */; }; D61099BB2144B0CC00432DC2 /* PachydermTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61099BA2144B0CC00432DC2 /* PachydermTests.swift */; }; D61099BD2144B0CC00432DC2 /* Pachyderm.h in Headers */ = {isa = PBXBuildFile; fileRef = D61099AD2144B0CC00432DC2 /* Pachyderm.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -67,6 +71,9 @@ D627FF7D217E958900CC0648 /* DraftTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D627FF7C217E958900CC0648 /* DraftTableViewCell.xib */; }; D627FF7F217E95E000CC0648 /* DraftTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627FF7E217E95E000CC0648 /* DraftTableViewCell.swift */; }; D627FF81217FE8F400CC0648 /* BehaviorTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627FF80217FE8F400CC0648 /* BehaviorTableViewController.swift */; }; + D6285B4F21EA695800FE4B39 /* StatusContentType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6285B4E21EA695800FE4B39 /* StatusContentType.swift */; }; + D6285B5121EA6E6E00FE4B39 /* AdvancedTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6285B5021EA6E6E00FE4B39 /* AdvancedTableViewController.swift */; }; + D6285B5321EA708700FE4B39 /* StatusFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6285B5221EA708700FE4B39 /* StatusFormat.swift */; }; D6289E84217B795D0003D1D7 /* LargeImageViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6289E83217B795D0003D1D7 /* LargeImageViewController.xib */; }; D62D2422217AA7E1005076CC /* UserActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2421217AA7E1005076CC /* UserActivityManager.swift */; }; D62D2424217ABF3F005076CC /* NSUserActivity+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2423217ABF3F005076CC /* NSUserActivity+Extensions.swift */; }; @@ -110,8 +117,36 @@ D6757A7E2157E02600721E32 /* XCBRequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6757A7D2157E02600721E32 /* XCBRequestSpec.swift */; }; D6757A822157E8FA00721E32 /* XCBSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6757A812157E8FA00721E32 /* XCBSession.swift */; }; D679C09F215850EF00DA27FE /* XCBActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D679C09E215850EF00DA27FE /* XCBActions.swift */; }; + D67C57AD21E265FC00C3118B /* LargeAccountDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57AC21E265FC00C3118B /* LargeAccountDetailView.swift */; }; + D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */; }; + D67C57B221E28FAD00C3118B /* ComposeStatusReplyView.xib in Resources */ = {isa = PBXBuildFile; fileRef = D67C57B121E28FAD00C3118B /* ComposeStatusReplyView.xib */; }; + D67C57B421E2910700C3118B /* ComposeStatusReplyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57B321E2910700C3118B /* ComposeStatusReplyView.swift */; }; D67E0513216438A7000E0927 /* AppearanceTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67E0512216438A7000E0927 /* AppearanceTableViewController.swift */; }; D67E051521643C77000E0927 /* Tab.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67E051421643C77000E0927 /* Tab.swift */; }; + D686329521ED8319008C716E /* GMImagePicker.strings in Resources */ = {isa = PBXBuildFile; fileRef = D686326E21ED8312008C716E /* GMImagePicker.strings */; }; + D686329621ED8319008C716E /* GMImagePicker.strings in Resources */ = {isa = PBXBuildFile; fileRef = D686327121ED8312008C716E /* GMImagePicker.strings */; }; + D686329721ED8319008C716E /* GMGridViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = D686327321ED8312008C716E /* GMGridViewCell.m */; }; + D686329821ED8319008C716E /* GMVideoIcon@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D686327421ED8312008C716E /* GMVideoIcon@2x.png */; }; + D686329921ED8319008C716E /* GMImagePicker.strings in Resources */ = {isa = PBXBuildFile; fileRef = D686327621ED8313008C716E /* GMImagePicker.strings */; }; + D686329A21ED8319008C716E /* GMAlbumsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = D686327821ED8313008C716E /* GMAlbumsViewController.m */; }; + D686329B21ED8319008C716E /* GMVideoIcon.png in Resources */ = {isa = PBXBuildFile; fileRef = D686327921ED8313008C716E /* GMVideoIcon.png */; }; + D686329C21ED8319008C716E /* GMImagePicker.strings in Resources */ = {isa = PBXBuildFile; fileRef = D686327B21ED8313008C716E /* GMImagePicker.strings */; }; + D686329D21ED8319008C716E /* GMEmptyFolder@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D686327D21ED8314008C716E /* GMEmptyFolder@2x.png */; }; + D686329E21ED8319008C716E /* GMAlbumsViewCell.h in Headers */ = {isa = PBXBuildFile; fileRef = D686327E21ED8314008C716E /* GMAlbumsViewCell.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D686329F21ED8319008C716E /* GMGridViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = D686327F21ED8315008C716E /* GMGridViewController.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D68632A021ED8319008C716E /* GMSelected.png in Resources */ = {isa = PBXBuildFile; fileRef = D686328021ED8315008C716E /* GMSelected.png */; }; + D68632A121ED8319008C716E /* GMAlbumsViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = D686328121ED8315008C716E /* GMAlbumsViewCell.m */; }; + D68632A221ED8319008C716E /* GMAlbumsViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = D686328221ED8316008C716E /* GMAlbumsViewController.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D68632A321ED8319008C716E /* GMImagePicker.strings in Resources */ = {isa = PBXBuildFile; fileRef = D686328421ED8316008C716E /* GMImagePicker.strings */; }; + D68632A421ED8319008C716E /* GMImagePicker.strings in Resources */ = {isa = PBXBuildFile; fileRef = D686328721ED8317008C716E /* GMImagePicker.strings */; }; + D68632A521ED8319008C716E /* GMSelected@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D686328921ED8317008C716E /* GMSelected@2x.png */; }; + D68632A621ED8319008C716E /* GMImagePickerController.m in Sources */ = {isa = PBXBuildFile; fileRef = D686328A21ED8317008C716E /* GMImagePickerController.m */; }; + D68632A721ED8319008C716E /* GMGridViewCell.h in Headers */ = {isa = PBXBuildFile; fileRef = D686328B21ED8317008C716E /* GMGridViewCell.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D68632A821ED8319008C716E /* GMImagePicker.strings in Resources */ = {isa = PBXBuildFile; fileRef = D686328D21ED8317008C716E /* GMImagePicker.strings */; }; + D68632A921ED8319008C716E /* GMEmptyFolder@1x.png in Resources */ = {isa = PBXBuildFile; fileRef = D686328F21ED8318008C716E /* GMEmptyFolder@1x.png */; }; + D68632AA21ED8319008C716E /* GMGridViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = D686329021ED8319008C716E /* GMGridViewController.m */; }; + D68632AB21ED8319008C716E /* GMImagePickerController.h in Headers */ = {isa = PBXBuildFile; fileRef = D686329121ED8319008C716E /* GMImagePickerController.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D68632AC21ED8319008C716E /* GMImagePicker.strings in Resources */ = {isa = PBXBuildFile; fileRef = D686329321ED8319008C716E /* GMImagePicker.strings */; }; D6A5FAF1217B7E05003DB2D9 /* ComposeViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6A5FAF0217B7E05003DB2D9 /* ComposeViewController.xib */; }; D6A5FAFB217B86CE003DB2D9 /* OnboardingViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6A5FAFA217B86CE003DB2D9 /* OnboardingViewController.xib */; }; D6B8DB342182A59300424AF7 /* UIAlertController+Visibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */; }; @@ -150,6 +185,13 @@ remoteGlobalIDString = 04496BC7216252E5001F1B23; remoteInfo = TTTAttributedLabel; }; + D60A549021ED515800F1F87C /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D6D4DDC4212518A000E1C4BB /* Project object */; + proxyType = 1; + remoteGlobalIDString = D60A548A21ED515800F1F87C; + remoteInfo = GMImagePicker; + }; D61099B52144B0CC00432DC2 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = D6D4DDC4212518A000E1C4BB /* Project object */; @@ -194,6 +236,7 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( + D60A549321ED515800F1F87C /* GMImagePicker.framework in Embed Frameworks */, 04496BD0216252E5001F1B23 /* TTTAttributedLabel.framework in Embed Frameworks */, D61099C12144B0CC00432DC2 /* Pachyderm.framework in Embed Frameworks */, D6BED170212663DA00F02DA0 /* SwiftSoup.framework in Embed Frameworks */, @@ -216,6 +259,10 @@ 04DACE8D212CC7CC009840C4 /* ImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = ""; }; 04ED00B021481ED800567C53 /* SteppedProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SteppedProgressView.swift; sourceTree = ""; }; D6028B9A2150811100F223B9 /* MastodonCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonCache.swift; sourceTree = ""; }; + D60A548B21ED515800F1F87C /* GMImagePicker.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = GMImagePicker.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D60A548D21ED515800F1F87C /* GMImagePicker.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GMImagePicker.h; sourceTree = ""; }; + D60A548E21ED515800F1F87C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D60C07E321E8176B0057FAA8 /* ComposeMediaView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ComposeMediaView.xib; sourceTree = ""; }; D61099AB2144B0CC00432DC2 /* Pachyderm.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pachyderm.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D61099AD2144B0CC00432DC2 /* Pachyderm.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Pachyderm.h; sourceTree = ""; }; D61099AE2144B0CC00432DC2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -266,6 +313,9 @@ D627FF7C217E958900CC0648 /* DraftTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DraftTableViewCell.xib; sourceTree = ""; }; D627FF7E217E95E000CC0648 /* DraftTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftTableViewCell.swift; sourceTree = ""; }; D627FF80217FE8F400CC0648 /* BehaviorTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BehaviorTableViewController.swift; sourceTree = ""; }; + D6285B4E21EA695800FE4B39 /* StatusContentType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentType.swift; sourceTree = ""; }; + D6285B5021EA6E6E00FE4B39 /* AdvancedTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedTableViewController.swift; sourceTree = ""; }; + D6285B5221EA708700FE4B39 /* StatusFormat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusFormat.swift; sourceTree = ""; }; D6289E83217B795D0003D1D7 /* LargeImageViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = LargeImageViewController.xib; sourceTree = ""; }; D62D2421217AA7E1005076CC /* UserActivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserActivityManager.swift; sourceTree = ""; }; D62D2423217ABF3F005076CC /* NSUserActivity+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSUserActivity+Extensions.swift"; sourceTree = ""; }; @@ -308,8 +358,36 @@ D6757A7D2157E02600721E32 /* XCBRequestSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBRequestSpec.swift; sourceTree = ""; }; D6757A812157E8FA00721E32 /* XCBSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBSession.swift; sourceTree = ""; }; D679C09E215850EF00DA27FE /* XCBActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBActions.swift; sourceTree = ""; }; + D67C57AC21E265FC00C3118B /* LargeAccountDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeAccountDetailView.swift; sourceTree = ""; }; + D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Uniques.swift"; sourceTree = ""; }; + D67C57B121E28FAD00C3118B /* ComposeStatusReplyView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ComposeStatusReplyView.xib; sourceTree = ""; }; + D67C57B321E2910700C3118B /* ComposeStatusReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusReplyView.swift; sourceTree = ""; }; D67E0512216438A7000E0927 /* AppearanceTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceTableViewController.swift; sourceTree = ""; }; D67E051421643C77000E0927 /* Tab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tab.swift; sourceTree = ""; }; + D686326F21ED8312008C716E /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = GMImagePicker.strings; sourceTree = ""; }; + D686327221ED8312008C716E /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ca; path = GMImagePicker.strings; sourceTree = ""; }; + D686327321ED8312008C716E /* GMGridViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GMGridViewCell.m; sourceTree = ""; }; + D686327421ED8312008C716E /* GMVideoIcon@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "GMVideoIcon@2x.png"; sourceTree = ""; }; + D686327721ED8313008C716E /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = GMImagePicker.strings; sourceTree = ""; }; + D686327821ED8313008C716E /* GMAlbumsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GMAlbumsViewController.m; sourceTree = ""; }; + D686327921ED8313008C716E /* GMVideoIcon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = GMVideoIcon.png; sourceTree = ""; }; + D686327C21ED8313008C716E /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = Base; path = GMImagePicker.strings; sourceTree = ""; }; + D686327D21ED8314008C716E /* GMEmptyFolder@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "GMEmptyFolder@2x.png"; sourceTree = ""; }; + D686327E21ED8314008C716E /* GMAlbumsViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GMAlbumsViewCell.h; sourceTree = ""; }; + D686327F21ED8315008C716E /* GMGridViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GMGridViewController.h; sourceTree = ""; }; + D686328021ED8315008C716E /* GMSelected.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = GMSelected.png; sourceTree = ""; }; + D686328121ED8315008C716E /* GMAlbumsViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GMAlbumsViewCell.m; sourceTree = ""; }; + D686328221ED8316008C716E /* GMAlbumsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GMAlbumsViewController.h; sourceTree = ""; }; + D686328521ED8316008C716E /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = GMImagePicker.strings; sourceTree = ""; }; + D686328821ED8317008C716E /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt; path = GMImagePicker.strings; sourceTree = ""; }; + D686328921ED8317008C716E /* GMSelected@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "GMSelected@2x.png"; sourceTree = ""; }; + D686328A21ED8317008C716E /* GMImagePickerController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GMImagePickerController.m; sourceTree = ""; }; + D686328B21ED8317008C716E /* GMGridViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GMGridViewCell.h; sourceTree = ""; }; + D686328E21ED8317008C716E /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = GMImagePicker.strings; sourceTree = ""; }; + D686328F21ED8318008C716E /* GMEmptyFolder@1x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "GMEmptyFolder@1x.png"; sourceTree = ""; }; + D686329021ED8319008C716E /* GMGridViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GMGridViewController.m; sourceTree = ""; }; + D686329121ED8319008C716E /* GMImagePickerController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GMImagePickerController.h; sourceTree = ""; }; + D686329421ED8319008C716E /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = GMImagePicker.strings; sourceTree = ""; }; D6A5FAF0217B7E05003DB2D9 /* ComposeViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ComposeViewController.xib; sourceTree = ""; }; D6A5FAFA217B86CE003DB2D9 /* OnboardingViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = OnboardingViewController.xib; sourceTree = ""; }; D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertController+Visibility.swift"; sourceTree = ""; }; @@ -353,6 +431,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + D60A548821ED515800F1F87C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; D61099A82144B0CC00432DC2 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -372,6 +457,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + D60A549221ED515800F1F87C /* GMImagePicker.framework in Frameworks */, 04496BCF216252E5001F1B23 /* TTTAttributedLabel.framework in Frameworks */, D61099C02144B0CC00432DC2 /* Pachyderm.framework in Frameworks */, D65A37F321472F300087646E /* SwiftSoup.framework in Frameworks */, @@ -407,6 +493,48 @@ path = TTTAttributedLabel; sourceTree = ""; }; + D60A548C21ED515800F1F87C /* GMImagePicker */ = { + isa = PBXGroup; + children = ( + D60A548D21ED515800F1F87C /* GMImagePicker.h */, + D60A548E21ED515800F1F87C /* Info.plist */, + D686327A21ED8313008C716E /* Base.lproj */, + D686327021ED8312008C716E /* ca.lproj */, + D686329221ED8319008C716E /* de.lproj */, + D686328C21ED8317008C716E /* en.lproj */, + D686326D21ED8312008C716E /* es.lproj */, + D686328321ED8316008C716E /* fr.lproj */, + D686327E21ED8314008C716E /* GMAlbumsViewCell.h */, + D686328121ED8315008C716E /* GMAlbumsViewCell.m */, + D686328221ED8316008C716E /* GMAlbumsViewController.h */, + D686327821ED8313008C716E /* GMAlbumsViewController.m */, + D686328F21ED8318008C716E /* GMEmptyFolder@1x.png */, + D686327D21ED8314008C716E /* GMEmptyFolder@2x.png */, + D686328B21ED8317008C716E /* GMGridViewCell.h */, + D686327321ED8312008C716E /* GMGridViewCell.m */, + D686327F21ED8315008C716E /* GMGridViewController.h */, + D686329021ED8319008C716E /* GMGridViewController.m */, + D686329121ED8319008C716E /* GMImagePickerController.h */, + D686328A21ED8317008C716E /* GMImagePickerController.m */, + D686328021ED8315008C716E /* GMSelected.png */, + D686328921ED8317008C716E /* GMSelected@2x.png */, + D686327921ED8313008C716E /* GMVideoIcon.png */, + D686327421ED8312008C716E /* GMVideoIcon@2x.png */, + D686327521ED8313008C716E /* it.lproj */, + D686328621ED8317008C716E /* pt.lproj */, + ); + path = GMImagePicker; + sourceTree = ""; + }; + D60C07E221E817560057FAA8 /* Compose Media */ = { + isa = PBXGroup; + children = ( + D60C07E321E8176B0057FAA8 /* ComposeMediaView.xib */, + D6333B762138D94E00CE884A /* ComposeMediaView.swift */, + ); + path = "Compose Media"; + sourceTree = ""; + }; D61099AC2144B0CC00432DC2 /* Pachyderm */ = { isa = PBXGroup; children = ( @@ -488,6 +616,7 @@ D61099FA214569F600432DC2 /* Report.swift */, D61099FC21456A1D00432DC2 /* SearchResults.swift */, D61099FE21456A4C00432DC2 /* Status.swift */, + D6285B4E21EA695800FE4B39 /* StatusContentType.swift */, D6109A10214607D500432DC2 /* Timeline.swift */, ); path = Model; @@ -596,6 +725,7 @@ D627FF77217E94F200CC0648 /* Drafts */, D6A5FAF0217B7E05003DB2D9 /* ComposeViewController.xib */, D66362702136338600C9CBA2 /* ComposeViewController.swift */, + D6285B5221EA708700FE4B39 /* StatusFormat.swift */, ); path = Compose; sourceTree = ""; @@ -615,6 +745,7 @@ children = ( D663626521360DD700C9CBA2 /* Preferences.storyboard */, D663626721360E2C00C9CBA2 /* PreferencesTableViewController.swift */, + D6285B5021EA6E6E00FE4B39 /* AdvancedTableViewController.swift */, D67E0512216438A7000E0927 /* AppearanceTableViewController.swift */, D627FF80217FE8F400CC0648 /* BehaviorTableViewController.swift */, D6C693C92161253F007D6A6D /* SilentActionPermissionsTableViewController.swift */, @@ -690,6 +821,7 @@ D66362742137068A00C9CBA2 /* Visibility+Helpers.swift */, D6333B362137838300CE884A /* AttributedString+Trim.swift */, D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */, + D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */, ); path = Extensions; sourceTree = ""; @@ -707,15 +839,98 @@ path = XCallbackURL; sourceTree = ""; }; + D67C57A721E2649B00C3118B /* Account Detail */ = { + isa = PBXGroup; + children = ( + D67C57AC21E265FC00C3118B /* LargeAccountDetailView.swift */, + ); + path = "Account Detail"; + sourceTree = ""; + }; + D67C57B021E28F9400C3118B /* Compose Status Reply */ = { + isa = PBXGroup; + children = ( + D67C57B121E28FAD00C3118B /* ComposeStatusReplyView.xib */, + D67C57B321E2910700C3118B /* ComposeStatusReplyView.swift */, + ); + path = "Compose Status Reply"; + sourceTree = ""; + }; + D686326D21ED8312008C716E /* es.lproj */ = { + isa = PBXGroup; + children = ( + D686326E21ED8312008C716E /* GMImagePicker.strings */, + ); + path = es.lproj; + sourceTree = ""; + }; + D686327021ED8312008C716E /* ca.lproj */ = { + isa = PBXGroup; + children = ( + D686327121ED8312008C716E /* GMImagePicker.strings */, + ); + path = ca.lproj; + sourceTree = ""; + }; + D686327521ED8313008C716E /* it.lproj */ = { + isa = PBXGroup; + children = ( + D686327621ED8313008C716E /* GMImagePicker.strings */, + ); + path = it.lproj; + sourceTree = ""; + }; + D686327A21ED8313008C716E /* Base.lproj */ = { + isa = PBXGroup; + children = ( + D686327B21ED8313008C716E /* GMImagePicker.strings */, + ); + path = Base.lproj; + sourceTree = ""; + }; + D686328321ED8316008C716E /* fr.lproj */ = { + isa = PBXGroup; + children = ( + D686328421ED8316008C716E /* GMImagePicker.strings */, + ); + path = fr.lproj; + sourceTree = ""; + }; + D686328621ED8317008C716E /* pt.lproj */ = { + isa = PBXGroup; + children = ( + D686328721ED8317008C716E /* GMImagePicker.strings */, + ); + path = pt.lproj; + sourceTree = ""; + }; + D686328C21ED8317008C716E /* en.lproj */ = { + isa = PBXGroup; + children = ( + D686328D21ED8317008C716E /* GMImagePicker.strings */, + ); + path = en.lproj; + sourceTree = ""; + }; + D686329221ED8319008C716E /* de.lproj */ = { + isa = PBXGroup; + children = ( + D686329321ED8319008C716E /* GMImagePicker.strings */, + ); + path = de.lproj; + sourceTree = ""; + }; D6BED1722126661300F02DA0 /* Views */ = { isa = PBXGroup; children = ( 04496BD621625361001F1B23 /* ContentLabel.swift */, D6C693F82162E4DB007D6A6D /* StatusContentLabel.swift */, D6C94D882139E6EC00CB5196 /* AttachmentView.swift */, - D6333B762138D94E00CE884A /* ComposeMediaView.swift */, D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */, 04ED00B021481ED800567C53 /* SteppedProgressView.swift */, + D67C57A721E2649B00C3118B /* Account Detail */, + D67C57B021E28F9400C3118B /* Compose Status Reply */, + D60C07E221E817560057FAA8 /* Compose Media */, D641C78A213DD926004B4513 /* Status */, D641C78B213DD92F004B4513 /* Profile Header */, D641C78C213DD937004B4513 /* Notifications */, @@ -759,6 +974,7 @@ D6D4DDCE212518A000E1C4BB /* Tusker */, D6D4DDE3212518A200E1C4BB /* TuskerTests */, D6D4DDEE212518A200E1C4BB /* TuskerUITests */, + D60A548C21ED515800F1F87C /* GMImagePicker */, D6D4DDCD212518A000E1C4BB /* Products */, D65A37F221472F300087646E /* Frameworks */, ); @@ -773,6 +989,7 @@ D61099AB2144B0CC00432DC2 /* Pachyderm.framework */, D61099B32144B0CC00432DC2 /* PachydermTests.xctest */, 04496BC8216252E5001F1B23 /* TTTAttributedLabel.framework */, + D60A548B21ED515800F1F87C /* GMImagePicker.framework */, ); name = Products; sourceTree = ""; @@ -847,6 +1064,19 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + D60A548621ED515800F1F87C /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + D686329F21ED8319008C716E /* GMGridViewController.h in Headers */, + D68632A221ED8319008C716E /* GMAlbumsViewController.h in Headers */, + D60A548F21ED515800F1F87C /* GMImagePicker.h in Headers */, + D686329E21ED8319008C716E /* GMAlbumsViewCell.h in Headers */, + D68632AB21ED8319008C716E /* GMImagePickerController.h in Headers */, + D68632A721ED8319008C716E /* GMGridViewCell.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; D61099A62144B0CC00432DC2 /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; @@ -876,6 +1106,24 @@ productReference = 04496BC8216252E5001F1B23 /* TTTAttributedLabel.framework */; productType = "com.apple.product-type.framework"; }; + D60A548A21ED515800F1F87C /* GMImagePicker */ = { + isa = PBXNativeTarget; + buildConfigurationList = D60A549421ED515800F1F87C /* Build configuration list for PBXNativeTarget "GMImagePicker" */; + buildPhases = ( + D60A548621ED515800F1F87C /* Headers */, + D60A548721ED515800F1F87C /* Sources */, + D60A548821ED515800F1F87C /* Frameworks */, + D60A548921ED515800F1F87C /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = GMImagePicker; + productName = GMImagePicker; + productReference = D60A548B21ED515800F1F87C /* GMImagePicker.framework */; + productType = "com.apple.product-type.framework"; + }; D61099AA2144B0CC00432DC2 /* Pachyderm */ = { isa = PBXNativeTarget; buildConfigurationList = D61099C22144B0CC00432DC2 /* Build configuration list for PBXNativeTarget "Pachyderm" */; @@ -927,6 +1175,7 @@ dependencies = ( D61099BF2144B0CC00432DC2 /* PBXTargetDependency */, 04496BCE216252E5001F1B23 /* PBXTargetDependency */, + D60A549121ED515800F1F87C /* PBXTargetDependency */, ); name = Tusker; productName = Tusker; @@ -982,6 +1231,9 @@ 04496BC7216252E5001F1B23 = { CreatedOnToolsVersion = 10.1; }; + D60A548A21ED515800F1F87C = { + CreatedOnToolsVersion = 10.1; + }; D61099AA2144B0CC00432DC2 = { CreatedOnToolsVersion = 10.0; LastSwiftMigration = 1000; @@ -1010,6 +1262,12 @@ knownRegions = ( en, Base, + de, + es, + it, + fr, + pt, + ca, ); mainGroup = D6D4DDC3212518A000E1C4BB; productRefGroup = D6D4DDCD212518A000E1C4BB /* Products */; @@ -1022,6 +1280,7 @@ D61099AA2144B0CC00432DC2 /* Pachyderm */, D61099B22144B0CC00432DC2 /* PachydermTests */, 04496BC7216252E5001F1B23 /* TTTAttributedLabel */, + D60A548A21ED515800F1F87C /* GMImagePicker */, ); }; /* End PBXProject section */ @@ -1034,6 +1293,27 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + D60A548921ED515800F1F87C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D686329C21ED8319008C716E /* GMImagePicker.strings in Resources */, + D686329621ED8319008C716E /* GMImagePicker.strings in Resources */, + D68632A521ED8319008C716E /* GMSelected@2x.png in Resources */, + D686329521ED8319008C716E /* GMImagePicker.strings in Resources */, + D68632A321ED8319008C716E /* GMImagePicker.strings in Resources */, + D686329B21ED8319008C716E /* GMVideoIcon.png in Resources */, + D68632A821ED8319008C716E /* GMImagePicker.strings in Resources */, + D686329921ED8319008C716E /* GMImagePicker.strings in Resources */, + D68632A921ED8319008C716E /* GMEmptyFolder@1x.png in Resources */, + D68632A421ED8319008C716E /* GMImagePicker.strings in Resources */, + D686329821ED8319008C716E /* GMVideoIcon@2x.png in Resources */, + D68632A021ED8319008C716E /* GMSelected.png in Resources */, + D68632AC21ED8319008C716E /* GMImagePicker.strings in Resources */, + D686329D21ED8319008C716E /* GMEmptyFolder@2x.png in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; D61099A92144B0CC00432DC2 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -1064,6 +1344,8 @@ D6289E84217B795D0003D1D7 /* LargeImageViewController.xib in Resources */, D663626621360DD700C9CBA2 /* Preferences.storyboard in Resources */, D627FF79217E950100CC0648 /* DraftsTableViewController.xib in Resources */, + D67C57B221E28FAD00C3118B /* ComposeStatusReplyView.xib in Resources */, + D60C07E421E8176B0057FAA8 /* ComposeMediaView.xib in Resources */, D667E5E12134937B0057A976 /* StatusTableViewCell.xib in Resources */, D6A5FAF1217B7E05003DB2D9 /* ComposeViewController.xib in Resources */, D621544D21682AD90003D87D /* TabTableViewCell.xib in Resources */, @@ -1095,6 +1377,18 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + D60A548721ED515800F1F87C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D686329A21ED8319008C716E /* GMAlbumsViewController.m in Sources */, + D68632A621ED8319008C716E /* GMImagePickerController.m in Sources */, + D686329721ED8319008C716E /* GMGridViewCell.m in Sources */, + D68632AA21ED8319008C716E /* GMGridViewController.m in Sources */, + D68632A121ED8319008C716E /* GMAlbumsViewCell.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; D61099A72144B0CC00432DC2 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -1122,6 +1416,7 @@ D6109A0F21459B6900432DC2 /* Pagination.swift in Sources */, D6109A032145722C00432DC2 /* RegisteredApplication.swift in Sources */, D6109A0921458C4A00432DC2 /* Empty.swift in Sources */, + D6285B4F21EA695800FE4B39 /* StatusContentType.swift in Sources */, D61099DF2144C11400432DC2 /* MastodonError.swift in Sources */, D61099D62144B4B200432DC2 /* FormAttachment.swift in Sources */, D6109A072145756700432DC2 /* LoginSettings.swift in Sources */, @@ -1150,9 +1445,11 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + D6285B5321EA708700FE4B39 /* StatusFormat.swift in Sources */, D6757A822157E8FA00721E32 /* XCBSession.swift in Sources */, 04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */, D6C693F92162E4DB007D6A6D /* StatusContentLabel.swift in Sources */, + D6285B5121EA6E6E00FE4B39 /* AdvancedTableViewController.swift in Sources */, D667E5F52135BCD50057A976 /* ConversationTableViewController.swift in Sources */, D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */, D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */, @@ -1166,6 +1463,7 @@ D646C956213B365700269FB5 /* LargeImageExpandAnimationController.swift in Sources */, D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */, D621544B21682AD30003D87D /* TabTableViewCell.swift in Sources */, + D67C57B421E2910700C3118B /* ComposeStatusReplyView.swift in Sources */, 04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */, D627FF7B217E951500CC0648 /* DraftsTableViewController.swift in Sources */, D6333B772138D94E00CE884A /* ComposeMediaView.swift in Sources */, @@ -1189,6 +1487,7 @@ D6C693CA2161253F007D6A6D /* SilentActionPermissionsTableViewController.swift in Sources */, D6333B372137838300CE884A /* AttributedString+Trim.swift in Sources */, D6B8DB342182A59300424AF7 /* UIAlertController+Visibility.swift in Sources */, + D67C57AD21E265FC00C3118B /* LargeAccountDetailView.swift in Sources */, D641C777213CAA9E004B4513 /* ActionNotificationTableViewCell.swift in Sources */, D627FF74217BBC9700CC0648 /* AppRouter.swift in Sources */, D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */, @@ -1196,6 +1495,7 @@ D627FF76217E923E00CC0648 /* DraftsManager.swift in Sources */, D663626821360E2C00C9CBA2 /* PreferencesTableViewController.swift in Sources */, D64F80E2215875CC00BEF393 /* XCBActionType.swift in Sources */, + D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */, D66362752137068A00C9CBA2 /* Visibility+Helpers.swift in Sources */, D67E0513216438A7000E0927 /* AppearanceTableViewController.swift in Sources */, D6C693FC2162FE6F007D6A6D /* LoadingViewController.swift in Sources */, @@ -1242,6 +1542,11 @@ target = 04496BC7216252E5001F1B23 /* TTTAttributedLabel */; targetProxy = 04496BCD216252E5001F1B23 /* PBXContainerItemProxy */; }; + D60A549121ED515800F1F87C /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D60A548A21ED515800F1F87C /* GMImagePicker */; + targetProxy = D60A549021ED515800F1F87C /* PBXContainerItemProxy */; + }; D61099B62144B0CC00432DC2 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = D61099AA2144B0CC00432DC2 /* Pachyderm */; @@ -1270,6 +1575,70 @@ /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ + D686326E21ED8312008C716E /* GMImagePicker.strings */ = { + isa = PBXVariantGroup; + children = ( + D686326F21ED8312008C716E /* es */, + ); + name = GMImagePicker.strings; + sourceTree = ""; + }; + D686327121ED8312008C716E /* GMImagePicker.strings */ = { + isa = PBXVariantGroup; + children = ( + D686327221ED8312008C716E /* ca */, + ); + name = GMImagePicker.strings; + sourceTree = ""; + }; + D686327621ED8313008C716E /* GMImagePicker.strings */ = { + isa = PBXVariantGroup; + children = ( + D686327721ED8313008C716E /* it */, + ); + name = GMImagePicker.strings; + sourceTree = ""; + }; + D686327B21ED8313008C716E /* GMImagePicker.strings */ = { + isa = PBXVariantGroup; + children = ( + D686327C21ED8313008C716E /* Base */, + ); + name = GMImagePicker.strings; + sourceTree = ""; + }; + D686328421ED8316008C716E /* GMImagePicker.strings */ = { + isa = PBXVariantGroup; + children = ( + D686328521ED8316008C716E /* fr */, + ); + name = GMImagePicker.strings; + sourceTree = ""; + }; + D686328721ED8317008C716E /* GMImagePicker.strings */ = { + isa = PBXVariantGroup; + children = ( + D686328821ED8317008C716E /* pt */, + ); + name = GMImagePicker.strings; + sourceTree = ""; + }; + D686328D21ED8317008C716E /* GMImagePicker.strings */ = { + isa = PBXVariantGroup; + children = ( + D686328E21ED8317008C716E /* en */, + ); + name = GMImagePicker.strings; + sourceTree = ""; + }; + D686329321ED8319008C716E /* GMImagePicker.strings */ = { + isa = PBXVariantGroup; + children = ( + D686329421ED8319008C716E /* de */, + ); + name = GMImagePicker.strings; + sourceTree = ""; + }; D6D4DDD8212518A200E1C4BB /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( @@ -1337,6 +1706,62 @@ }; name = Release; }; + D60A549521ED515800F1F87C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = HGYVAQA9FW; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = GMImagePicker/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 12.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = net.shadowfacts.GMImagePicker; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + D60A549621ED515800F1F87C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = HGYVAQA9FW; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = GMImagePicker/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 12.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = net.shadowfacts.GMImagePicker; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; D61099C32144B0CC00432DC2 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -1690,6 +2115,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + D60A549421ED515800F1F87C /* Build configuration list for PBXNativeTarget "GMImagePicker" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D60A549521ED515800F1F87C /* Debug */, + D60A549621ED515800F1F87C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; D61099C22144B0CC00432DC2 /* Build configuration list for PBXNativeTarget "Pachyderm" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/Tusker/Extensions/Array+Uniques.swift b/Tusker/Extensions/Array+Uniques.swift new file mode 100644 index 00000000..29fdc8ae --- /dev/null +++ b/Tusker/Extensions/Array+Uniques.swift @@ -0,0 +1,23 @@ +// +// Array+Uniques.swift +// Tusker +// +// Created by Shadowfacts on 1/6/19. +// Copyright © 2019 Shadowfacts. All rights reserved. +// + +import Foundation + +extension Array where Element: Hashable { + func uniques() -> [Element] { + var buffer = [Element]() + var added = Set() + for elem in self { + if !added.contains(elem) { + buffer.append(elem) + added.insert(elem) + } + } + return buffer + } +} diff --git a/Tusker/Preferences/Preferences.swift b/Tusker/Preferences/Preferences.swift index 98219067..e6ac38a7 100644 --- a/Tusker/Preferences/Preferences.swift +++ b/Tusker/Preferences/Preferences.swift @@ -45,6 +45,7 @@ class Preferences: Codable { // MARK: - Advanced var silentActions: [String: Permission] = [:] + var statusContentType: StatusContentType = .plain // MARK: - Utility Methods func tabIndex(_ tab: Tab) -> Int { diff --git a/Tusker/Screens/Compose/ComposeViewController.swift b/Tusker/Screens/Compose/ComposeViewController.swift index 71593aa5..8040def2 100644 --- a/Tusker/Screens/Compose/ComposeViewController.swift +++ b/Tusker/Screens/Compose/ComposeViewController.swift @@ -9,67 +9,86 @@ import UIKit import Pachyderm import Intents +import Photos +import GMImagePicker +import MobileCoreServices class ComposeViewController: UIViewController { let router: AppRouter - @IBOutlet weak var scrollView: UIScrollView! - @IBOutlet weak var inReplyToContainerView: UIView! - @IBOutlet weak var inReplyToAvatarImageView: UIImageView! - @IBOutlet weak var inReplyToDisplayNameLabel: UILabel! - @IBOutlet weak var inReplyToUsernameLabel: UILabel! - @IBOutlet weak var inReplyToContentLabel: StatusContentLabel! - @IBOutlet weak var inReplyToLabel: UILabel! - @IBOutlet weak var statusTextView: UITextView! - @IBOutlet weak var placeholderLabel: UILabel! - @IBOutlet weak var charactersRemainingLabel: UILabel! - @IBOutlet weak var visibilityButton: UIButton! - @IBOutlet weak var postButton: UIButton! - @IBOutlet weak var contentWarningTextField: UITextField! - @IBOutlet weak var mediaStackView: UIStackView! - @IBOutlet weak var paddingView: UIView! - @IBOutlet weak var progressView: SteppedProgressView! - - var scrolled = false - var inReplyToID: String? - - // TODO: cleanup this - var mentioningAcct: String? - var text: String? + var accountsToMention: [String] var initialText: String? + var contentWarningEnabled = false { + didSet { + contentWarningStateChanged() + } + } + var visibility: Status.Visibility! { + didSet { + visibilityChanged() + } + } + var selectedAssets: [PHAsset] = [] { + didSet { + updateAttachmentViews() + } + } - var draft: DraftsManager.Draft? - + var currentDraft: DraftsManager.Draft? // Weak so that if a new session is initiated (i.e. XCBManager.currentSession is changed) while the current one is in progress, this one will be released weak var xcbSession: XCBSession? + var postedStatus: Status? - var contentWarning = false { - didSet { - contentWarningTextField.isHidden = !contentWarning - } - } - var visibility = Preferences.shared.defaultPostVisibility { - didSet { - visibilityButton.setTitle(visibility.displayName, for: .normal) - } - } + weak var postBarButtonItem: UIBarButtonItem! + var visibilityBarButtonItem: UIBarButtonItem! + var contentWarningBarButtonItem: UIBarButtonItem! - var status: Status? + @IBOutlet weak var scrollView: UIScrollView! + @IBOutlet weak var contentView: UIView! + @IBOutlet weak var stackView: UIStackView! + + var replyView: ComposeStatusReplyView? + var replyAvatarImageViewTopConstraint: NSLayoutConstraint? + + @IBOutlet weak var selfDetailView: LargeAccountDetailView! + + @IBOutlet weak var charactersRemainingLabel: UILabel! + @IBOutlet weak var statusTextView: UITextView! + @IBOutlet weak var placeholderLabel: UILabel! + + @IBOutlet weak var contentWarningContainerView: UIView! + @IBOutlet weak var contentWarningTextField: UITextField! + + @IBOutlet weak var attachmentsStackView: UIStackView! + @IBOutlet weak var addAttachmentButton: UIButton! + + @IBOutlet weak var postProgressView: SteppedProgressView! init(inReplyTo inReplyToID: String? = nil, mentioningAcct: String? = nil, text: String? = nil, router: AppRouter) { - self.inReplyToID = inReplyToID - self.mentioningAcct = mentioningAcct - self.text = text self.router = router + self.inReplyToID = inReplyToID + if let inReplyToID = inReplyToID, let inReplyTo = MastodonCache.status(for: inReplyToID) { + accountsToMention = [inReplyTo.account.acct] + inReplyTo.mentions.map { $0.acct } + } else if let mentioningAcct = mentioningAcct { + accountsToMention = [mentioningAcct] + } else { + accountsToMention = [] + } + if let ownAccount = MastodonController.account { + accountsToMention.removeAll(where: { acct in ownAccount.acct == acct }) + } + accountsToMention = accountsToMention.uniques() + super.init(nibName: "ComposeViewController", bundle: nil) title = "Compose" - navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelPressed)) - navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Drafts", style: .plain, target: self, action: #selector(draftsPressed)) + navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelButtonPressed)) + navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Post", style: .done, target: self, action: #selector(postButtonPressed)) + postBarButtonItem = navigationItem.rightBarButtonItem } required init?(coder aDecoder: NSCoder) { @@ -79,97 +98,141 @@ class ComposeViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - statusTextView.layer.cornerRadius = 5 - statusTextView.layer.masksToBounds = true + scrollView.delegate = self + statusTextView.delegate = self - visibilityButton.setTitle(visibility.displayName, for: .normal) - contentWarningTextField.delegate = self + statusTextView.becomeFirstResponder() - let toolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: 30)) - let flexSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) - let done = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(keyboardDoneButtonPressed)) - toolbar.setItems([flexSpace, done], animated: false) - toolbar.sizeToFit() + let toolbar = UIToolbar() + contentWarningBarButtonItem = UIBarButtonItem(title: "CW", style: .plain, target: self, action: #selector(contentWarningButtonPressed)) + visibilityBarButtonItem = UIBarButtonItem(barButtonSystemItem: .action, target: self, action: #selector(visbilityButtonPressed)) + toolbar.items = [ + contentWarningBarButtonItem, + visibilityBarButtonItem, + UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) + ] + createFormattingButtons() + [ + UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil), + UIBarButtonItem(title: "Drafts", style: .plain, target: self, action: #selector(draftsButtonPressed)) + ] + toolbar.translatesAutoresizingMaskIntoConstraints = false statusTextView.inputAccessoryView = toolbar + contentWarningTextField.inputAccessoryView = toolbar - if let inReplyToID = inReplyToID, - let inReplyTo = MastodonCache.status(for: inReplyToID) { - inReplyToDisplayNameLabel.text = inReplyTo.account.realDisplayName - inReplyToUsernameLabel.text = "@\(inReplyTo.account.username)" - inReplyToContentLabel.statusID = inReplyToID - inReplyToAvatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: inReplyToAvatarImageView) - inReplyToAvatarImageView.layer.masksToBounds = true - inReplyToAvatarImageView.image = nil - ImageCache.avatars.get(inReplyTo.account.avatar) { (data) in - guard let data = data else { return } - DispatchQueue.main.async { - self.inReplyToAvatarImageView.image = UIImage(data: data) - } + statusTextView.text = accountsToMention.map({ acct in "@\(acct) " }).joined() + initialText = statusTextView.text + + MastodonController.getOwnAccount { (account) in + DispatchQueue.main.async { + self.selfDetailView.update(account: account) } - inReplyToLabel.text = "In reply to \(inReplyTo.account.realDisplayName)" - if inReplyTo.account != MastodonController.account { - statusTextView.text = "@\(inReplyTo.account.acct) " - } - statusTextView.text += inReplyTo.mentions.filter({ $0.id != MastodonController.account.id }).map({ "@\($0.acct) " }).joined() - contentWarning = inReplyTo.sensitive - contentWarningTextField.text = inReplyTo.spoilerText + } + + if let inReplyToID = inReplyToID, let inReplyTo = MastodonCache.status(for: inReplyToID) { visibility = inReplyTo.visibility - } else { - inReplyToLabel.isHidden = true - inReplyToContainerView.isHidden = true + + let replyView = ComposeStatusReplyView.create() + replyView.updateUI(for: inReplyTo) + stackView.insertArrangedSubview(replyView, at: 0) + self.replyView = replyView + + replyAvatarImageViewTopConstraint = replyView.avatarImageView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 8) + replyAvatarImageViewTopConstraint!.isActive = true + + let replyLabelContainer = UIView() + replyLabelContainer.translatesAutoresizingMaskIntoConstraints = false + + let replyLabel = UILabel() + replyLabel.translatesAutoresizingMaskIntoConstraints = false + replyLabel.text = "In reply to \(inReplyTo.account.realDisplayName)" + replyLabel.textColor = .darkGray + replyLabelContainer.addSubview(replyLabel) + + NSLayoutConstraint.activate([ + replyLabel.leadingAnchor.constraint(equalTo: replyLabelContainer.leadingAnchor, constant: 8), + replyLabel.trailingAnchor.constraint(equalTo: replyLabelContainer.trailingAnchor, constant: -8), + replyLabel.topAnchor.constraint(equalTo: replyLabelContainer.topAnchor), + replyLabel.bottomAnchor.constraint(equalTo: replyLabelContainer.bottomAnchor) + ]) + + stackView.insertArrangedSubview(replyLabelContainer, at: 1) } - if let mentioningAcct = mentioningAcct { - statusTextView.text += "@\(mentioningAcct) " - } - if let text = text { - statusTextView.text += text - } - - initialText = statusTextView.text.trimmingCharacters(in: .whitespacesAndNewlines) - updateCharactersRemaining() updatePlaceholder() - progressView.progress = 0 + contentWarningEnabled = false + NotificationCenter.default.addObserver(self, selector: #selector(contentWarningTextFieldDidChange), name: UITextField.textDidChangeNotification, object: contentWarningTextField) - if let mentioningAcct = mentioningAcct { - let req = MastodonController.client.searchForAccount(query: mentioningAcct) - MastodonController.client.run(req) { [weak self] (response) in - if case let .success(accounts, _) = response { - self?.userActivity = UserActivityManager.newPostActivity(mentioning: accounts.first) - } else { - self?.userActivity = UserActivityManager.newPostActivity() - } - } - } else { - self.userActivity = UserActivityManager.newPostActivity() + if inReplyToID == nil { + visibility = Preferences.shared.defaultPostVisibility } + } + + override func viewWillAppear(_ animated: Bool) { + NotificationCenter.default.addObserver(self, selector: #selector(adjustForKeyboard), name: UIResponder.keyboardWillHideNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(adjustForKeyboard), name: UIResponder.keyboardWillChangeFrameNotification, object: nil) + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil) + NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillChangeFrameNotification, object: nil) } override func viewDidLayoutSubviews() { - if inReplyToID != nil && !scrolled { - scrollView.contentOffset = CGPoint(x: 0, y: inReplyToContainerView.bounds.height - 44) - scrolled = true + super.viewDidLayoutSubviews() + +// if inReplyToID != nil { +// scrollView.contentOffset = CGPoint(x: 0, y: stackView.arrangedSubviews.first!.frame.height) +// } + } + + func createFormattingButtons() -> [UIBarButtonItem] { + guard Preferences.shared.statusContentType != .plain else { + return [] + } + + return StatusFormat.allCases.map { (format) in + let (str, attributes) = format.title + let item = UIBarButtonItem(title: str, style: .plain, target: self, action: #selector(formatButtonPressed(_:))) + item.setTitleTextAttributes(attributes, for: .normal) + item.setTitleTextAttributes(attributes, for: .highlighted) + item.tag = StatusFormat.allCases.firstIndex(of: format)! + return item } } + + @objc func adjustForKeyboard(notification: NSNotification) { + let userInfo = notification.userInfo! - func addMedia(for image: UIImage) { - let mediaView = ComposeMediaView(image: image) - mediaView.delegate = self - mediaStackView.addArrangedSubview(mediaView) + let keyboardScreenEndFrame = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as! NSValue).cgRectValue + let keyboardViewEndFrame = view.convert(keyboardScreenEndFrame, from: view.window) + + if notification.name == UIResponder.keyboardWillHideNotification { + scrollView.contentInset = .zero + } else { +// let accessoryFrame = view.convert(statusTextView.inputAccessoryView!.frame, from: view.window) + let offset = keyboardViewEndFrame.height// + accessoryFrame.height + // TODO: radar for incorrect keyboard end frame height (either converted or screen) + // the value returned is somewhere between the height of the keyboard and the height of the keyboard + accessory + // actually maybe not?? + scrollView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: offset, right: 0) + } + scrollView.scrollIndicatorInsets = scrollView.contentInset } func updateCharactersRemaining() { + // TODO: include CW char count let count = CharacterCounter.count(text: statusTextView.text) - let remaining = (MastodonController.instance.maxStatusCharacters ?? 500) - count + let cwCount = contentWarningEnabled ? (contentWarningTextField.text?.count ?? 0) : 0 + let remaining = (MastodonController.instance.maxStatusCharacters ?? 500) - count - cwCount if remaining < 0 { charactersRemainingLabel.textColor = .red - postButton.isEnabled = false + postBarButtonItem.isEnabled = false } else { charactersRemainingLabel.textColor = .darkGray - postButton.isEnabled = true + postBarButtonItem.isEnabled = true } charactersRemainingLabel.text = remaining.description } @@ -178,147 +241,65 @@ class ComposeViewController: UIViewController { placeholderLabel.isHidden = !statusTextView.text.isEmpty } - // MARK: - Navigation - - override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { - statusTextView.resignFirstResponder() - contentWarningTextField.resignFirstResponder() - super.dismiss(animated: flag, completion: completion) + func updateAddAttachmentButton() { + addAttachmentButton.isEnabled = selectedAssets.count < 5 // 4 attachments + 1 button } - // MARK: - Interaction - - @IBAction func visibilityPressed(_ sender: Any) { - let alertController = UIAlertController(currentVisibility: self.visibility) { (visibility) in - guard let visibility = visibility else { return } - UIView.performWithoutAnimation { - self.visibility = visibility - self.visibilityButton.layoutIfNeeded() + func updateAttachmentViews() { + for view in attachmentsStackView.arrangedSubviews { + if view is ComposeMediaView { + view.removeFromSuperview() } } - present(alertController, animated: true) - } - - @IBAction func contentWarningPressed(_ sender: Any) { - contentWarning = !contentWarning - } - - @IBAction func mediaPressed(_ sender: Any) { - let imagePicker = UIImagePickerController() - imagePicker.delegate = self - let alertController = UIAlertController(title: "Choose Image Source", message: nil, preferredStyle: .actionSheet) - if UIImagePickerController.isSourceTypeAvailable(.camera) { - alertController.addAction(UIAlertAction(title: "Camera", style: .default, handler: { _ in - imagePicker.sourceType = .camera - self.present(imagePicker, animated: true) - })) + for asset in selectedAssets { + let mediaView = ComposeMediaView.create() + mediaView.delegate = self + mediaView.update(asset: asset) + attachmentsStackView.insertArrangedSubview(mediaView, at: attachmentsStackView.arrangedSubviews.count - 1) + updateAddAttachmentButton() } - if UIImagePickerController.isSourceTypeAvailable(.photoLibrary) { - alertController.addAction(UIAlertAction(title: "Photo Library", style: .default, handler: { __ in - imagePicker.sourceType = .photoLibrary - self.present(imagePicker, animated: true) - })) - } - alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) - present(alertController, animated: true) } - @IBAction func postPressed(_ sender: Any) { - guard let text = statusTextView.text, - !text.isEmpty else { return } - - postButton.isEnabled = false - - let contentWarning: String? - if self.contentWarning, - let text = contentWarningTextField.text, - !text.isEmpty { - contentWarning = text + func contentWarningStateChanged() { + contentWarningContainerView.isHidden = !contentWarningEnabled + contentWarningBarButtonItem.style = contentWarningEnabled ? .done : .plain + if contentWarningEnabled { + contentWarningTextField.becomeFirstResponder() } else { - contentWarning = nil - } - let sensitive = contentWarning != nil - - let visibility = self.visibility - - var attachments: [Attachment?] = [] - let group = DispatchGroup() - for view in mediaStackView.arrangedSubviews { - guard let mediaView = view as? ComposeMediaView, - let image = mediaView.image, - let data = image.pngData() else { continue } - let index = attachments.count - attachments.append(nil) - progressView.steps += 1 - group.enter() - let request = MastodonController.client.upload(attachment: FormAttachment(pngData: data), description: mediaView.mediaDescription) - MastodonController.client.run(request) { response in - guard case let .success(attachment, _) = response else { fatalError() } - attachments[index] = attachment - self.progressView.step() - group.leave() - } - } - - progressView.steps = 2 + attachments.count - progressView.currentStep = 1 - - group.notify(queue: .main) { - let attachments = attachments.compactMap { $0 } - - let request = MastodonController.client.createStatus(text: text, - inReplyTo: self.inReplyToID, - media: attachments, - sensitive: sensitive, - spoilerText: contentWarning, - visibility: visibility) - MastodonController.client.run(request) { response in - guard case let .success(status, _) = response else { fatalError() } - self.status = status - MastodonCache.add(status: status) - - if let draft = self.draft { - DraftsManager.shared.remove(draft) - } - - DispatchQueue.main.async { - self.progressView.step() - self.dismiss(animated: true) - // TODO: reorganize routing/navigation - (((self.presentingViewController as! MainTabBarViewController).selectedViewController as! UINavigationController).topViewController as! TuskerNavigationDelegate).selected(status: status.id) - - self.xcbSession?.complete(with: .success, additionalData: [ - "statusURL": status.url?.absoluteString, - "statusURI": status.uri - ]) - } - } + statusTextView.becomeFirstResponder() } } - @objc func imagePressed(_ gesture: UITapGestureRecognizer) { - gesture.view!.superview!.removeFromSuperview() - } - - @objc func keyboardDoneButtonPressed() { - statusTextView.endEditing(false) + func visibilityChanged() { + // TODO: update visiblity button image } func saveDraft() { - if let draft = draft { - draft.update(text: statusTextView.text) + // TODO: save attachmenst to drafts + // TODO: save CW to draft + if let currentDraft = currentDraft { + currentDraft.update(text: statusTextView.text) } else { DraftsManager.shared.create(text: statusTextView.text) } } - func close() { + @objc func close() { dismiss(animated: true) xcbSession?.complete(with: .cancel) } - @objc func cancelPressed() { + // MARK: - Navigation + + override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { + statusTextView.resignFirstResponder() + super.dismiss(animated: flag, completion: completion) + } + + // MARK: - Interaction + + @objc func cancelButtonPressed() { guard statusTextView.text.trimmingCharacters(in: .whitespacesAndNewlines) != initialText else { close() return @@ -336,28 +317,177 @@ class ComposeViewController: UIViewController { self.close() })) alert.addAction(UIAlertAction(title: "Delete draft", style: .destructive, handler: { (_) in - if let draft = self.draft { - DraftsManager.shared.remove(draft) + if let currentDraft = self.currentDraft { + DraftsManager.shared.remove(currentDraft) } self.close() })) - alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: { (_) in - })) + alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) router.present(alert, animated: true) } - @objc func draftsPressed() { - let drafts = router.drafts() - drafts.delegate = self - router.present(UINavigationController(rootViewController: drafts), animated: true) + @objc func contentWarningButtonPressed() { + contentWarningEnabled = !contentWarningEnabled + } + + @objc func contentWarningTextFieldDidChange() { + updateCharactersRemaining() + } + + @objc func visbilityButtonPressed() { + let alertController = UIAlertController(currentVisibility: self.visibility) { (visibility) in + guard let visibility = visibility else { return } + self.visibility = visibility + } + present(alertController, animated: true) + } + + @objc func formatButtonPressed(_ button: UIBarButtonItem) { + guard statusTextView.isFirstResponder else { + return + } + + let format = StatusFormat.allCases[button.tag] + guard let insertionResult = format.insertionResult else { + return + } + + let currentSelectedRange = statusTextView.selectedRange + if currentSelectedRange.length == 0 { + statusTextView.insertText(insertionResult.prefix + insertionResult.suffix) + statusTextView.selectedRange = NSRange(location: currentSelectedRange.location + insertionResult.insertionPoint, length: 0) + } else { + let start = statusTextView.text.index(statusTextView.text.startIndex, offsetBy: currentSelectedRange.lowerBound) + let end = statusTextView.text.index(statusTextView.text.startIndex, offsetBy: currentSelectedRange.upperBound) + let selectedText = statusTextView.text[start.. Bool { - textField.endEditing(false) - return true +extension ComposeViewController: UIScrollViewDelegate { + func scrollViewDidScroll(_ scrollView: UIScrollView) { + guard let replyView = replyView else { return } + + var constant: CGFloat = 8 + + if scrollView.contentOffset.y < 0 { + constant -= scrollView.contentOffset.y + replyAvatarImageViewTopConstraint?.constant = 8 - scrollView.contentOffset.y + } else if scrollView.contentOffset.y > replyView.frame.height - replyView.avatarImageView.frame.height - 16 { + constant += replyView.frame.height - replyView.avatarImageView.frame.height - 16 - scrollView.contentOffset.y + } + + replyAvatarImageViewTopConstraint?.constant = constant } } @@ -368,27 +498,23 @@ extension ComposeViewController: UITextViewDelegate { } } -extension ComposeViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate { - func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { - if let selectedImage = info[.originalImage] as? UIImage { - addMedia(for: selectedImage) - dismiss(animated: true) - } +extension ComposeViewController: GMImagePickerControllerDelegate { + func assetsPickerController(_ picker: GMImagePickerController!, didFinishPickingAssets assets: [Any]!) { + let assets = assets as! [PHAsset] + selectedAssets.append(contentsOf: assets) + picker.dismiss(animated: true) + } + + func assetsPickerController(_ picker: GMImagePickerController!, shouldSelect asset: PHAsset!) -> Bool { + return selectedAssets.count + picker.selectedAssets.count < 4 } } extension ComposeViewController: ComposeMediaViewDelegate { - func editDescription(for media: ComposeMediaView) { - let alertController = UIAlertController(title: "Media Description", message: nil, preferredStyle: .alert) - alertController.addTextField { textField in - textField.text = media.mediaDescription - } - alertController.addAction(UIAlertAction(title: "Confirm", style: .default, handler: { _ in - let description = alertController.textFields![0].text - media.mediaDescription = description - })) - alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) - present(alertController, animated: true) + func didRemoveMedia(_ mediaView: ComposeMediaView) { + let index = attachmentsStackView.arrangedSubviews.firstIndex(of: mediaView)! + selectedAssets.remove(at: index) + updateAddAttachmentButton() } } @@ -397,8 +523,9 @@ extension ComposeViewController: DraftsTableViewControllerDelegate { } func draftSelected(_ draft: DraftsManager.Draft) { - self.draft = draft + self.currentDraft = draft statusTextView.text = draft.text updatePlaceholder() + updateCharactersRemaining() } } diff --git a/Tusker/Screens/Compose/ComposeViewController.xib b/Tusker/Screens/Compose/ComposeViewController.xib index 97d35d3a..66afb10d 100644 --- a/Tusker/Screens/Compose/ComposeViewController.xib +++ b/Tusker/Screens/Compose/ComposeViewController.xib @@ -1,33 +1,29 @@ - + - + - - - - - - - - - - - - - - - + + + + + + + + + + + + - @@ -35,166 +31,179 @@ - - + + - - + + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - + + + + + + + + - - - - - - - + + + + - - + + + - - - - - - - - + + + + + + + + + + + diff --git a/Tusker/Screens/Compose/StatusFormat.swift b/Tusker/Screens/Compose/StatusFormat.swift new file mode 100644 index 00000000..85c9ac59 --- /dev/null +++ b/Tusker/Screens/Compose/StatusFormat.swift @@ -0,0 +1,74 @@ +// +// StatusFormat.swift +// Tusker +// +// Created by Shadowfacts on 1/12/19. +// Copyright © 2019 Shadowfacts. All rights reserved. +// + +import Foundation +import Pachyderm + +enum StatusFormat: CaseIterable { + case italics, bold, strikethrough, code + + var insertionResult: FormatInsertionResult? { + switch Preferences.shared.statusContentType { + case .plain: + return nil + case .markdown: + return Markdown.format(self) + case .html: + return HTML.format(self) + } + } + + var title: (String, [NSAttributedString.Key: Any]) { + switch self { + case .italics: + return ("I", [.font: UIFont.italicSystemFont(ofSize: 17)]) + case .bold: + return ("B", [.font: UIFont.boldSystemFont(ofSize: 17)]) + case .strikethrough: + return ("S", [.strikethroughStyle: NSNumber(value: NSUnderlineStyle.single.rawValue)]) + case .code: + return ("", [.font: UIFont(name: "Menlo", size: 17)!]) + } + } +} + +typealias FormatInsertionResult = (prefix: String, suffix: String, insertionPoint: Int) + +protocol FormatType { + static func format(_ format: StatusFormat) -> FormatInsertionResult +} + +extension StatusFormat { + struct Markdown: FormatType { + static var formats: [StatusFormat: String] = [ + .italics: "_", + .bold: "**", + .strikethrough: "~~", + .code: "`" + ] + + static func format(_ format: StatusFormat) -> FormatInsertionResult { + let str = formats[format]! + return (str, str, str.count) + } + } + + struct HTML: FormatType { + static var tags: [StatusFormat: String] = [ + .italics: "em", + .bold: "strong", + .strikethrough: "del", + .code: "code" + ] + + static func format(_ format: StatusFormat) -> FormatInsertionResult { + let tag = tags[format]! + return ("<\(tag)>", "", tag.count + 2) + } + } +} diff --git a/Tusker/Screens/Preferences/AdvancedTableViewController.swift b/Tusker/Screens/Preferences/AdvancedTableViewController.swift new file mode 100644 index 00000000..a8ebdd94 --- /dev/null +++ b/Tusker/Screens/Preferences/AdvancedTableViewController.swift @@ -0,0 +1,56 @@ +// +// AdvancedTableViewController.swift +// Tusker +// +// Created by Shadowfacts on 1/12/19. +// Copyright © 2019 Shadowfacts. All rights reserved. +// + +import UIKit +import Pachyderm + +class AdvancedTableViewController: UITableViewController { + + @IBOutlet weak var postContentTypeLabel: UILabel! + + override func viewDidLoad() { + super.viewDidLoad() + + postContentTypeLabel.text = Preferences.shared.statusContentType.displayName + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + guard indexPath.section == 1 && indexPath.row == 0 else { + return + } + tableView.deselectRow(at: indexPath, animated: true) + + let alertController = UIAlertController(title: "Post Content Type", message: nil, preferredStyle: .actionSheet) + for contentType in StatusContentType.allCases { + let action = UIAlertAction(title: contentType.displayName, style: .default) { (_) in + Preferences.shared.statusContentType = contentType + self.postContentTypeLabel.text = contentType.displayName + } + if contentType == Preferences.shared.statusContentType { + action.setValue(true, forKey: "checked") + } + alertController.addAction(action) + } + alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) + present(alertController, animated: true) + } + +} + +extension StatusContentType { + var displayName: String { + switch self { + case .plain: + return "Plain" + case .markdown: + return "Markdown" + case .html: + return "HTML" + } + } +} diff --git a/Tusker/Screens/Preferences/Preferences.storyboard b/Tusker/Screens/Preferences/Preferences.storyboard index 56e37789..bb6ea324 100644 --- a/Tusker/Screens/Preferences/Preferences.storyboard +++ b/Tusker/Screens/Preferences/Preferences.storyboard @@ -1,10 +1,10 @@ - + - + @@ -311,7 +311,7 @@ - + @@ -344,6 +344,39 @@ + + This option is only supported for Pleroma instances with formatting enabled. On all other instances, formatting symbols will remain in the plain, unformatted text. + + + + + + + + + + + + + + + + + + + + + @@ -351,6 +384,9 @@ + + + diff --git a/Tusker/Views/Account Detail/LargeAccountDetailView.swift b/Tusker/Views/Account Detail/LargeAccountDetailView.swift new file mode 100644 index 00000000..b578179c --- /dev/null +++ b/Tusker/Views/Account Detail/LargeAccountDetailView.swift @@ -0,0 +1,69 @@ +// +// LargeAccountDetailViewController.swift +// Tusker +// +// Created by Shadowfacts on 1/6/19. +// Copyright © 2019 Shadowfacts. All rights reserved. +// + +import UIKit +import Pachyderm + +class LargeAccountDetailView: UIView, PreferencesAdaptive { + + var avatarImageView = UIImageView() + var displayNameLabel = UILabel() + var usernameLabel = UILabel() + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + + avatarImageView.translatesAutoresizingMaskIntoConstraints = false + avatarImageView.layer.masksToBounds = true + addSubview(avatarImageView) + + displayNameLabel.translatesAutoresizingMaskIntoConstraints = false + displayNameLabel.font = .systemFont(ofSize: 20, weight: .semibold) + addSubview(displayNameLabel) + + usernameLabel.translatesAutoresizingMaskIntoConstraints = false + usernameLabel.font = .systemFont(ofSize: 17, weight: .light) + usernameLabel.textColor = .darkGray + addSubview(usernameLabel) + + NSLayoutConstraint.activate([ + avatarImageView.heightAnchor.constraint(equalToConstant: 50), + avatarImageView.widthAnchor.constraint(equalToConstant: 50), + avatarImageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8), + avatarImageView.topAnchor.constraint(equalTo: topAnchor, constant: 8), + avatarImageView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8), + displayNameLabel.leadingAnchor.constraint(equalTo: avatarImageView.trailingAnchor, constant: 8), + displayNameLabel.topAnchor.constraint(equalTo: topAnchor, constant: 8), + usernameLabel.leadingAnchor.constraint(equalTo: avatarImageView.trailingAnchor, constant: 8), + usernameLabel.topAnchor.constraint(equalTo: displayNameLabel.bottomAnchor) + ]) + } + + override func layoutSubviews() { + super.layoutSubviews() + + updateUIForPreferences() + } + + func updateUIForPreferences() { + avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView) + } + + func update(account: Account) { + displayNameLabel.text = account.realDisplayName + usernameLabel.text = "@\(account.acct)" + + ImageCache.avatars.get(account.avatar) { (data) in + guard let data = data else { return } + DispatchQueue.main.async { + self.avatarImageView.image = UIImage(data: data) + } + } + } + +} diff --git a/Tusker/Views/Compose Media/ComposeMediaView.swift b/Tusker/Views/Compose Media/ComposeMediaView.swift new file mode 100644 index 00000000..d3f05329 --- /dev/null +++ b/Tusker/Views/Compose Media/ComposeMediaView.swift @@ -0,0 +1,57 @@ +// +// ComposeAttachmentView.swift +// Tusker +// +// Created by Shadowfacts on 1/10/19. +// Copyright © 2019 Shadowfacts. All rights reserved. +// + +import UIKit +import Photos + +protocol ComposeMediaViewDelegate { + func didRemoveMedia(_ mediaView: ComposeMediaView) +} + +class ComposeMediaView: UIView { + + var delegate: ComposeMediaViewDelegate? + + @IBOutlet weak var imageView: UIImageView! + @IBOutlet weak var descriptionTextView: UITextView! + @IBOutlet weak var placeholderLabel: UILabel! + + static func create() -> ComposeMediaView { + return UINib(nibName: "ComposeMediaView", bundle: nil).instantiate(withOwner: nil, options: nil).first as! ComposeMediaView + } + + override func awakeFromNib() { + super.awakeFromNib() + + imageView.layer.masksToBounds = true + imageView.layer.cornerRadius = 10 // 0.1 * imageView.frame.width + + descriptionTextView.delegate = self + } + + func update(asset: PHAsset) { +// let size = imageView.frame.size // is this initialized yet? + let size = CGSize(width: 80, height: 80) + PHImageManager.default().requestImage(for: asset, targetSize: size, contentMode: .aspectFill, options: nil) { (image, _) in + self.imageView.image = image + } + } + + // MARK: - Interaction + + @IBAction func removePressed(_ sender: Any) { + delegate?.didRemoveMedia(self) + } + +} + +extension ComposeMediaView: UITextViewDelegate { + func textViewDidChange(_ textView: UITextView) { + placeholderLabel.isHidden = !descriptionTextView.text.isEmpty + } +} diff --git a/Tusker/Views/Compose Media/ComposeMediaView.xib b/Tusker/Views/Compose Media/ComposeMediaView.xib new file mode 100644 index 00000000..e6b84d1f --- /dev/null +++ b/Tusker/Views/Compose Media/ComposeMediaView.xib @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tusker/Views/Compose Status Reply/ComposeStatusReplyView.swift b/Tusker/Views/Compose Status Reply/ComposeStatusReplyView.swift new file mode 100644 index 00000000..df557273 --- /dev/null +++ b/Tusker/Views/Compose Status Reply/ComposeStatusReplyView.swift @@ -0,0 +1,46 @@ +// +// ComposeStatusReplyView.swift +// Tusker +// +// Created by Shadowfacts on 1/6/19. +// Copyright © 2019 Shadowfacts. All rights reserved. +// + +import UIKit +import Pachyderm + +class ComposeStatusReplyView: UIView, PreferencesAdaptive { + + @IBOutlet weak var avatarImageView: UIImageView! + @IBOutlet weak var displayNameLabel: UILabel! + @IBOutlet weak var usernameLabel: UILabel! + @IBOutlet weak var contentLabel: StatusContentLabel! + + static func create() -> ComposeStatusReplyView { + return UINib(nibName: "ComposeStatusReplyView", bundle: nil).instantiate(withOwner: nil, options: nil).first as! ComposeStatusReplyView + } + + override func awakeFromNib() { + super.awakeFromNib() + + updateUIForPreferences() + } + + func updateUIForPreferences() { + avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView) + } + + func updateUI(for status: Status) { + displayNameLabel.text = status.account.realDisplayName + usernameLabel.text = "@\(status.account.acct)" + contentLabel.statusID = status.id + + ImageCache.avatars.get(status.account.avatar) { (data) in + guard let data = data else { return } + DispatchQueue.main.async { + self.avatarImageView.image = UIImage(data: data) + } + } + } + +} diff --git a/Tusker/Views/Compose Status Reply/ComposeStatusReplyView.xib b/Tusker/Views/Compose Status Reply/ComposeStatusReplyView.xib new file mode 100644 index 00000000..85a6335f --- /dev/null +++ b/Tusker/Views/Compose Status Reply/ComposeStatusReplyView.xib @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tusker/Views/ComposeMediaView.swift b/Tusker/Views/ComposeMediaView.swift deleted file mode 100644 index 4c6653df..00000000 --- a/Tusker/Views/ComposeMediaView.swift +++ /dev/null @@ -1,57 +0,0 @@ -// -// ComposeAttachmentView.swift -// Tusker -// -// Created by Shadowfacts on 8/30/18. -// Copyright © 2018 Shadowfacts. All rights reserved. -// - -import UIKit - -protocol ComposeMediaViewDelegate { - func editDescription(for media: ComposeMediaView) -} - -class ComposeMediaView: UIImageView { - - var delegate: ComposeMediaViewDelegate? - - var remove: UIImageView - - var mediaDescription: String? - - required init?(coder aDecoder: NSCoder) { - return nil - } - - init(image: UIImage) { - remove = UIImageView(image: UIImage(named: "Remove")) - - super.init(image: image) - - contentMode = .scaleAspectFill - layer.cornerRadius = 5 - layer.masksToBounds = true - isUserInteractionEnabled = true - addGestureRecognizer(UILongPressGestureRecognizer(target: self, action: #selector(longPressed))) - - remove.isUserInteractionEnabled = true - remove.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(removePressed))) - remove.translatesAutoresizingMaskIntoConstraints = false - addSubview(remove) - - remove.widthAnchor.constraint(equalToConstant: 20).isActive = true - remove.heightAnchor.constraint(equalToConstant: 20).isActive = true - remove.topAnchor.constraint(equalTo: topAnchor, constant: 5).isActive = true - remove.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -5).isActive = true - } - - @objc func removePressed() { - removeFromSuperview() - } - - @objc func longPressed() { - delegate?.editDescription(for: self) - } - -}