Merge branch 'refs/heads/feature/feedback_screenshot' into develop
33
Classes/BITActivityIndicatorButton.h
Normal file
@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person
|
||||
* obtaining a copy of this software and associated documentation
|
||||
* files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following
|
||||
* conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface BITActivityIndicatorButton : UIButton
|
||||
|
||||
- (void)setShowsActivityIndicator:(BOOL)showsIndicator;
|
||||
|
||||
@end
|
76
Classes/BITActivityIndicatorButton.m
Normal file
@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person
|
||||
* obtaining a copy of this software and associated documentation
|
||||
* files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following
|
||||
* conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#import "BITActivityIndicatorButton.h"
|
||||
|
||||
@interface BITActivityIndicatorButton()
|
||||
|
||||
@property (nonatomic, strong) UIActivityIndicatorView *indicator;
|
||||
@property (nonatomic) BOOL indicatorVisible;
|
||||
|
||||
@end
|
||||
|
||||
@implementation BITActivityIndicatorButton
|
||||
|
||||
- (void)setShowsActivityIndicator:(BOOL)showsIndicator {
|
||||
if (self.indicatorVisible == showsIndicator){
|
||||
return;
|
||||
}
|
||||
|
||||
if (!self.indicator){
|
||||
self.indicator = [[UIActivityIndicatorView alloc] initWithFrame:self.bounds];
|
||||
[self addSubview:self.indicator];
|
||||
[self.indicator setColor:[UIColor blackColor]];
|
||||
}
|
||||
|
||||
self.indicatorVisible = showsIndicator;
|
||||
|
||||
if (showsIndicator){
|
||||
[self.indicator startAnimating];
|
||||
self.indicator.alpha = 1;
|
||||
self.layer.borderWidth = 1;
|
||||
self.layer.borderColor = [UIColor lightGrayColor].CGColor;
|
||||
self.layer.cornerRadius = 5;
|
||||
self.imageView.image = nil;
|
||||
} else {
|
||||
[self.indicator stopAnimating];
|
||||
self.layer.cornerRadius = 0;
|
||||
self.indicator.alpha = 0;
|
||||
self.layer.borderWidth = 0;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
|
||||
[self.indicator setFrame:self.bounds];
|
||||
|
||||
}
|
||||
|
||||
|
||||
@end
|
33
Classes/BITArrowImageAnnotation.h
Normal file
@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Author: Moritz Haarmann <post@moritzhaarmann.de>
|
||||
*
|
||||
* Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person
|
||||
* obtaining a copy of this software and associated documentation
|
||||
* files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following
|
||||
* conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#import "BITImageAnnotation.h"
|
||||
|
||||
@interface BITArrowImageAnnotation : BITImageAnnotation
|
||||
|
||||
@end
|
205
Classes/BITArrowImageAnnotation.m
Normal file
@ -0,0 +1,205 @@
|
||||
/*
|
||||
* Author: Moritz Haarmann <post@moritzhaarmann.de>
|
||||
*
|
||||
* Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person
|
||||
* obtaining a copy of this software and associated documentation
|
||||
* files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following
|
||||
* conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#import "BITArrowImageAnnotation.h"
|
||||
|
||||
#define kArrowPointCount 7
|
||||
|
||||
|
||||
@interface BITArrowImageAnnotation()
|
||||
|
||||
@property (nonatomic, strong) CAShapeLayer *shapeLayer;
|
||||
@property (nonatomic, strong) CAShapeLayer *strokeLayer;
|
||||
|
||||
@end
|
||||
|
||||
@implementation BITArrowImageAnnotation
|
||||
|
||||
- (id)initWithFrame:(CGRect)frame
|
||||
{
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
self.shapeLayer = [CAShapeLayer layer];
|
||||
self.shapeLayer.strokeColor = [UIColor whiteColor].CGColor;
|
||||
self.shapeLayer.lineWidth = 5;
|
||||
self.shapeLayer.fillColor = [UIColor redColor].CGColor;
|
||||
|
||||
self.strokeLayer = [CAShapeLayer layer];
|
||||
self.strokeLayer.strokeColor = [UIColor redColor].CGColor;
|
||||
self.strokeLayer.lineWidth = 10;
|
||||
self.strokeLayer.fillColor = [UIColor clearColor].CGColor;
|
||||
[self.layer addSublayer:self.strokeLayer];
|
||||
|
||||
[self.layer addSublayer:self.shapeLayer];
|
||||
|
||||
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)buildShape {
|
||||
CGFloat baseWidth = MAX(self.frame.size.width, self.frame.size.height);
|
||||
CGFloat topHeight = MAX(baseWidth / 3.0f,10);
|
||||
|
||||
|
||||
CGFloat lineWidth = MAX(baseWidth / 10.0f,3);
|
||||
CGFloat startX, startY, endX, endY;
|
||||
|
||||
CGRect boundRect = CGRectInset(self.bounds, 0, 0);
|
||||
CGFloat arrowLength= sqrt(pow(CGRectGetWidth(boundRect), 2) + pow(CGRectGetHeight(boundRect), 2));
|
||||
if (arrowLength < 30){
|
||||
|
||||
CGFloat factor = 30.f/arrowLength;
|
||||
|
||||
boundRect = CGRectApplyAffineTransform(boundRect, CGAffineTransformMakeScale(factor,factor));
|
||||
}
|
||||
|
||||
if ( self.movedDelta.width < 0){
|
||||
startX = CGRectGetMinX(boundRect);
|
||||
endX = CGRectGetMaxX(boundRect);
|
||||
} else {
|
||||
startX = CGRectGetMaxX(boundRect);
|
||||
endX = CGRectGetMinX(boundRect);
|
||||
|
||||
}
|
||||
|
||||
if ( self.movedDelta.height < 0){
|
||||
startY = CGRectGetMinY(boundRect);
|
||||
endY = CGRectGetMaxY(boundRect);
|
||||
} else {
|
||||
startY = CGRectGetMaxY(boundRect);
|
||||
endY = CGRectGetMinY(boundRect);
|
||||
|
||||
}
|
||||
|
||||
|
||||
if (abs(CGRectGetWidth(boundRect)) < 30 || abs(CGRectGetHeight(boundRect)) < 30){
|
||||
CGFloat smallerOne = MIN(abs(CGRectGetHeight(boundRect)), abs(CGRectGetWidth(boundRect)));
|
||||
|
||||
CGFloat factor = smallerOne/30.f;
|
||||
|
||||
CGRectApplyAffineTransform(boundRect, CGAffineTransformMakeScale(factor,factor));
|
||||
}
|
||||
|
||||
UIBezierPath *path = [self bezierPathWithArrowFromPoint:CGPointMake(endX, endY) toPoint:CGPointMake(startX, startY) tailWidth:lineWidth headWidth:topHeight headLength:topHeight];
|
||||
|
||||
self.shapeLayer.path = path.CGPath;
|
||||
self.strokeLayer.path = path.CGPath;
|
||||
[CATransaction begin];
|
||||
[CATransaction setAnimationDuration:0];
|
||||
self.strokeLayer.lineWidth = lineWidth/1.5f;
|
||||
self.shapeLayer.lineWidth = lineWidth / 3.0f;
|
||||
|
||||
[CATransaction commit];
|
||||
|
||||
}
|
||||
|
||||
|
||||
- (UIBezierPath *)bezierPathWithArrowFromPoint:(CGPoint)startPoint
|
||||
toPoint:(CGPoint)endPoint
|
||||
tailWidth:(CGFloat)tailWidth
|
||||
headWidth:(CGFloat)headWidth
|
||||
headLength:(CGFloat)headLength {
|
||||
CGFloat length = hypotf(endPoint.x - startPoint.x, endPoint.y - startPoint.y);
|
||||
|
||||
CGPoint points[kArrowPointCount];
|
||||
[self getAxisAlignedArrowPoints:points
|
||||
forLength:length
|
||||
tailWidth:tailWidth
|
||||
headWidth:headWidth
|
||||
headLength:headLength];
|
||||
|
||||
CGAffineTransform transform = [self transformForStartPoint:startPoint
|
||||
endPoint:endPoint
|
||||
length:length];
|
||||
|
||||
CGMutablePathRef cgPath = CGPathCreateMutable();
|
||||
CGPathAddLines(cgPath, &transform, points, sizeof points / sizeof *points);
|
||||
CGPathCloseSubpath(cgPath);
|
||||
|
||||
UIBezierPath *uiPath = [UIBezierPath bezierPathWithCGPath:cgPath];
|
||||
CGPathRelease(cgPath);
|
||||
return uiPath;
|
||||
}
|
||||
|
||||
- (void)getAxisAlignedArrowPoints:(CGPoint[kArrowPointCount])points
|
||||
forLength:(CGFloat)length
|
||||
tailWidth:(CGFloat)tailWidth
|
||||
headWidth:(CGFloat)headWidth
|
||||
headLength:(CGFloat)headLength {
|
||||
CGFloat tailLength = length - headLength;
|
||||
points[0] = CGPointMake(0, tailWidth / 2);
|
||||
points[1] = CGPointMake(tailLength, tailWidth / 2);
|
||||
points[2] = CGPointMake(tailLength, headWidth / 2);
|
||||
points[3] = CGPointMake(length, 0);
|
||||
points[4] = CGPointMake(tailLength, -headWidth / 2);
|
||||
points[5] = CGPointMake(tailLength, -tailWidth / 2);
|
||||
points[6] = CGPointMake(0, -tailWidth / 2);
|
||||
}
|
||||
|
||||
+ (CGAffineTransform)dqd_transformForStartPoint:(CGPoint)startPoint
|
||||
endPoint:(CGPoint)endPoint
|
||||
length:(CGFloat)length {
|
||||
CGFloat cosine = (endPoint.x - startPoint.x) / length;
|
||||
CGFloat sine = (endPoint.y - startPoint.y) / length;
|
||||
return (CGAffineTransform){ cosine, sine, -sine, cosine, startPoint.x, startPoint.y };
|
||||
}
|
||||
|
||||
- (CGAffineTransform)transformForStartPoint:(CGPoint)startPoint
|
||||
endPoint:(CGPoint)endPoint
|
||||
length:(CGFloat)length {
|
||||
CGFloat cosine = (endPoint.x - startPoint.x) / length;
|
||||
CGFloat sine = (endPoint.y - startPoint.y) / length;
|
||||
return (CGAffineTransform){ cosine, sine, -sine, cosine, startPoint.x, startPoint.y };
|
||||
}
|
||||
|
||||
#pragma mark - UIView
|
||||
|
||||
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
|
||||
|
||||
CGPathRef strokePath = CGPathCreateCopyByStrokingPath(self.shapeLayer.path, NULL, fmaxf(90.0f, self.shapeLayer.lineWidth), kCGLineCapRound,kCGLineJoinMiter,0);
|
||||
|
||||
BOOL containsPoint = CGPathContainsPoint(strokePath, NULL, point, NO);
|
||||
|
||||
CGPathRelease(strokePath);
|
||||
|
||||
if (containsPoint){
|
||||
return self;
|
||||
} else {
|
||||
return nil;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-(void)layoutSubviews{
|
||||
[super layoutSubviews];
|
||||
|
||||
[self buildShape];
|
||||
}
|
||||
|
||||
@end
|
33
Classes/BITBlurImageAnnotation.h
Normal file
@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Author: Moritz Haarmann <post@moritzhaarmann.de>
|
||||
*
|
||||
* Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person
|
||||
* obtaining a copy of this software and associated documentation
|
||||
* files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following
|
||||
* conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#import "BITImageAnnotation.h"
|
||||
|
||||
@interface BITBlurImageAnnotation : BITImageAnnotation
|
||||
|
||||
@end
|
102
Classes/BITBlurImageAnnotation.m
Normal file
@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Author: Moritz Haarmann <post@moritzhaarmann.de>
|
||||
*
|
||||
* Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person
|
||||
* obtaining a copy of this software and associated documentation
|
||||
* files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following
|
||||
* conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#import "BITBlurImageAnnotation.h"
|
||||
|
||||
@interface BITBlurImageAnnotation()
|
||||
|
||||
@property (nonatomic, strong) CALayer* imageLayer;
|
||||
@property (nonatomic, strong) UIImage* scaledImage;
|
||||
@property (nonatomic, strong) CALayer* selectedLayer;
|
||||
|
||||
|
||||
@end
|
||||
|
||||
@implementation BITBlurImageAnnotation
|
||||
|
||||
- (id)initWithFrame:(CGRect)frame
|
||||
{
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
self.clipsToBounds = YES;
|
||||
self.imageLayer = [CALayer layer];
|
||||
[self.layer addSublayer:self.imageLayer];
|
||||
|
||||
self.selectedLayer = [CALayer layer];
|
||||
[self.layer insertSublayer:self.selectedLayer above:self.imageLayer];
|
||||
|
||||
self.selectedLayer.backgroundColor = [[UIColor redColor] colorWithAlphaComponent:0.5f].CGColor;
|
||||
self.selectedLayer.opacity = 0.6f;
|
||||
self.clipsToBounds = YES;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
-(void)setSourceImage:(UIImage *)sourceImage {
|
||||
CGSize size = CGSizeMake(sourceImage.size.width/30, sourceImage.size.height/30);
|
||||
|
||||
UIGraphicsBeginImageContext(size);
|
||||
|
||||
[sourceImage drawInRect:CGRectMake(0, 0, size.width, size.height)];
|
||||
self.scaledImage = UIGraphicsGetImageFromCurrentImageContext();
|
||||
self.imageLayer.shouldRasterize = YES;
|
||||
self.imageLayer.rasterizationScale = 1;
|
||||
self.imageLayer.magnificationFilter = kCAFilterNearest;
|
||||
self.imageLayer.contents = (id)self.scaledImage.CGImage;
|
||||
|
||||
UIGraphicsEndImageContext();
|
||||
}
|
||||
|
||||
- (void)setSelected:(BOOL)selected {
|
||||
self->_selected = selected;
|
||||
|
||||
if (selected){
|
||||
self.selectedLayer.opacity = 0.6f;
|
||||
} else {
|
||||
self.selectedLayer.opacity = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
|
||||
[CATransaction begin];
|
||||
[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
|
||||
|
||||
self.imageLayer.frame = self.imageFrame;
|
||||
self.imageLayer.masksToBounds = YES;
|
||||
|
||||
self.selectedLayer.frame= self.bounds;
|
||||
[CATransaction commit];
|
||||
}
|
||||
|
||||
-(BOOL)resizable {
|
||||
return YES;
|
||||
}
|
||||
|
||||
@end
|
@ -850,21 +850,23 @@ static PLCrashReporterCallbacks plCrashCallbacks = {
|
||||
if (_crashManagerStatus == BITCrashManagerStatusDisabled) return NO;
|
||||
|
||||
if ([self.fileManager fileExistsAtPath:_crashesDir]) {
|
||||
NSString *file = nil;
|
||||
NSError *error = NULL;
|
||||
|
||||
NSDirectoryEnumerator *dirEnum = [self.fileManager enumeratorAtPath: _crashesDir];
|
||||
NSArray *dirArray = [self.fileManager contentsOfDirectoryAtPath:_crashesDir error:&error];
|
||||
|
||||
while ((file = [dirEnum nextObject])) {
|
||||
NSDictionary *fileAttributes = [self.fileManager attributesOfItemAtPath:[_crashesDir stringByAppendingPathComponent:file] error:&error];
|
||||
if ([[fileAttributes objectForKey:NSFileSize] intValue] > 0 &&
|
||||
for (NSString *file in dirArray) {
|
||||
NSString *filePath = [_crashesDir stringByAppendingPathComponent:file];
|
||||
|
||||
NSDictionary *fileAttributes = [self.fileManager attributesOfItemAtPath:filePath error:&error];
|
||||
if ([[fileAttributes objectForKey:NSFileType] isEqualToString:NSFileTypeRegular] &&
|
||||
[[fileAttributes objectForKey:NSFileSize] intValue] > 0 &&
|
||||
![file hasSuffix:@".DS_Store"] &&
|
||||
![file hasSuffix:@".analyzer"] &&
|
||||
![file hasSuffix:@".plist"] &&
|
||||
![file hasSuffix:@".data"] &&
|
||||
![file hasSuffix:@".meta"] &&
|
||||
![file hasSuffix:@".desc"]) {
|
||||
[_crashFiles addObject:[_crashesDir stringByAppendingPathComponent: file]];
|
||||
[_crashFiles addObject:filePath];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -70,8 +70,11 @@
|
||||
The follwoing data object classes are currently supported:
|
||||
- NSString
|
||||
- NSURL
|
||||
- UIImage
|
||||
- NSData
|
||||
|
||||
These are automatically concatenated to one text string.
|
||||
These are automatically concatenated to one text string, while any images and NSData
|
||||
objects are added as attachments to the feedback.
|
||||
|
||||
@param items Array of data objects to prefill the feedback text message.
|
||||
*/
|
||||
|
@ -34,6 +34,7 @@
|
||||
#import "HockeySDKPrivate.h"
|
||||
|
||||
#import "BITFeedbackManagerPrivate.h"
|
||||
#import "BITFeedbackMessageAttachment.h"
|
||||
#import "BITFeedbackComposeViewController.h"
|
||||
#import "BITFeedbackUserDataViewController.h"
|
||||
|
||||
@ -41,21 +42,35 @@
|
||||
|
||||
#import "BITHockeyHelper.h"
|
||||
|
||||
#import "BITImageAnnotationViewController.h"
|
||||
|
||||
@interface BITFeedbackComposeViewController () <BITFeedbackUserDataDelegate> {
|
||||
|
||||
@interface BITFeedbackComposeViewController () <BITFeedbackUserDataDelegate, UIImagePickerControllerDelegate, UINavigationControllerDelegate, UIActionSheetDelegate, BITImageAnnotationDelegate> {
|
||||
UIStatusBarStyle _statusBarStyle;
|
||||
}
|
||||
|
||||
@property (nonatomic, weak) BITFeedbackManager *manager;
|
||||
@property (nonatomic, strong) UITextView *textView;
|
||||
@property (nonatomic, strong) UIView *contentViewContainer;
|
||||
@property (nonatomic, strong) UIScrollView *attachmentScrollView;
|
||||
@property (nonatomic, strong) NSMutableArray *attachmentScrollViewImageViews;
|
||||
|
||||
@property (nonatomic, strong) NSString *text;
|
||||
|
||||
@property (nonatomic, strong) NSMutableArray *attachments;
|
||||
@property (nonatomic, strong) NSMutableArray *imageAttachments;
|
||||
|
||||
@property (nonatomic, strong) UIView *textAccessoryView;
|
||||
@property (nonatomic) NSInteger selectedAttachmentIndex;
|
||||
@property (nonatomic, strong) UITapGestureRecognizer *tapRecognizer;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation BITFeedbackComposeViewController {
|
||||
BOOL _blockUserDataScreen;
|
||||
BOOL _blockUserDataScreen;
|
||||
|
||||
BOOL _actionSheetVisible;
|
||||
}
|
||||
|
||||
|
||||
@ -66,8 +81,14 @@
|
||||
if (self) {
|
||||
self.title = BITHockeyLocalizedString(@"HockeyFeedbackComposeTitle");
|
||||
_blockUserDataScreen = NO;
|
||||
_actionSheetVisible = NO;
|
||||
_delegate = nil;
|
||||
_manager = [BITHockeyManager sharedHockeyManager].feedbackManager;
|
||||
_attachments = [NSMutableArray new];
|
||||
_imageAttachments = [NSMutableArray new];
|
||||
_attachmentScrollViewImageViews = [NSMutableArray new];
|
||||
self.tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(scrollViewTapped:)];
|
||||
[self.attachmentScrollView addGestureRecognizer:self.tapRecognizer];
|
||||
|
||||
_text = nil;
|
||||
}
|
||||
@ -84,6 +105,13 @@
|
||||
self.text = [(self.text ? self.text : @"") stringByAppendingFormat:@"%@%@", (self.text ? @" " : @""), item];
|
||||
} else if ([item isKindOfClass:[NSURL class]]) {
|
||||
self.text = [(self.text ? self.text : @"") stringByAppendingFormat:@"%@%@", (self.text ? @" " : @""), [(NSURL *)item absoluteString]];
|
||||
} else if ([item isKindOfClass:[UIImage class]]) {
|
||||
UIImage *image = item;
|
||||
BITFeedbackMessageAttachment *attachment = [BITFeedbackMessageAttachment attachmentWithData:UIImageJPEGRepresentation(image, 0.7f) contentType:@"image/jpeg"];
|
||||
[self.attachments addObject:attachment];
|
||||
[self.imageAttachments addObject:attachment];
|
||||
} else if ([item isKindOfClass:[NSData class]]) {
|
||||
[self.attachments addObject:[BITFeedbackMessageAttachment attachmentWithData:item contentType:@"'application/octet-stream'"]];
|
||||
} else {
|
||||
BITHockeyLog(@"Unknown item type %@", item);
|
||||
}
|
||||
@ -122,12 +150,15 @@
|
||||
frame.size.height = windowSize.width - navBarHeight - modalGap - kbSize.width;
|
||||
}
|
||||
}
|
||||
[self.textView setFrame:frame];
|
||||
[self.contentViewContainer setFrame:frame];
|
||||
|
||||
[self performSelector:@selector(refreshAttachmentScrollview) withObject:nil afterDelay:0.0f];
|
||||
|
||||
}
|
||||
|
||||
- (void)keyboardWillBeHidden:(NSNotification*)aNotification {
|
||||
CGRect frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height);
|
||||
[self.textView setFrame:frame];
|
||||
[self.contentViewContainer setFrame:frame];
|
||||
}
|
||||
|
||||
|
||||
@ -142,20 +173,48 @@
|
||||
self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCancel
|
||||
target:self
|
||||
action:@selector(dismissAction:)];
|
||||
|
||||
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:BITHockeyLocalizedString(@"HockeyFeedbackComposeSend")
|
||||
style:UIBarButtonItemStyleDone
|
||||
target:self
|
||||
action:@selector(sendAction:)];
|
||||
|
||||
// Container that contains both the textfield and eventually the photo scroll view on the right side
|
||||
self.contentViewContainer = [[UIView alloc] initWithFrame:self.view.bounds];
|
||||
self.contentViewContainer.autoresizingMask = UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight;
|
||||
|
||||
[self.view addSubview:self.contentViewContainer];
|
||||
|
||||
// message input textfield
|
||||
self.textView = [[UITextView alloc] initWithFrame:self.view.frame];
|
||||
self.textView = [[UITextView alloc] initWithFrame:self.view.bounds];
|
||||
self.textView.font = [UIFont systemFontOfSize:17];
|
||||
self.textView.delegate = self;
|
||||
self.textView.backgroundColor = [UIColor whiteColor];
|
||||
self.textView.returnKeyType = UIReturnKeyDefault;
|
||||
self.textView.autoresizingMask = UIViewAutoresizingFlexibleWidth;
|
||||
[self.view addSubview:self.textView];
|
||||
self.textView.autoresizingMask = UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight;
|
||||
|
||||
[self.contentViewContainer addSubview:self.textView];
|
||||
|
||||
// Add Photo Button + Container that's displayed above the keyboard.
|
||||
self.textAccessoryView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth(self.view.frame), 44)];
|
||||
self.textAccessoryView.backgroundColor = [UIColor colorWithRed:0.9f green:0.9f blue:0.9f alpha:1.0f];
|
||||
UIButton *addPhotoButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
[addPhotoButton setTitle:BITHockeyLocalizedString(@"HockeyFeedbackComposeAttachmentAddImage") forState:UIControlStateNormal];
|
||||
[addPhotoButton setTitleColor:[UIColor darkGrayColor] forState:UIControlStateNormal];
|
||||
addPhotoButton.frame = CGRectMake(0, 0, CGRectGetWidth(self.view.frame), 44);
|
||||
[addPhotoButton addTarget:self action:@selector(addPhotoAction:) forControlEvents:UIControlEventTouchUpInside];
|
||||
|
||||
[self.textAccessoryView addSubview:addPhotoButton];
|
||||
|
||||
self.textView.inputAccessoryView = self.textAccessoryView;
|
||||
|
||||
// This could be a subclass, yet
|
||||
self.attachmentScrollView = [[UIScrollView alloc] initWithFrame:CGRectZero];
|
||||
self.attachmentScrollView.scrollEnabled = YES;
|
||||
self.attachmentScrollView.bounces = YES;
|
||||
self.attachmentScrollView.autoresizesSubviews = NO;
|
||||
self.attachmentScrollView.autoresizingMask = UIViewAutoresizingFlexibleHeight|UIViewAutoresizingFlexibleRightMargin;
|
||||
|
||||
[self.contentViewContainer addSubview:self.attachmentScrollView];
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated {
|
||||
@ -178,14 +237,13 @@
|
||||
[[UIApplication sharedApplication] setStatusBarStyle:(self.navigationController.navigationBar.barStyle == UIBarStyleDefault) ? UIStatusBarStyleDefault : UIStatusBarStyleBlackOpaque];
|
||||
#endif
|
||||
|
||||
[self.textView setFrame:self.view.frame];
|
||||
// [self.textView setFrame:self.view.frame];
|
||||
|
||||
if (_text) {
|
||||
self.textView.text = _text;
|
||||
self.navigationItem.rightBarButtonItem.enabled = YES;
|
||||
} else {
|
||||
self.navigationItem.rightBarButtonItem.enabled = NO;
|
||||
}
|
||||
|
||||
[self updateBarButtonState];
|
||||
}
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated {
|
||||
@ -210,7 +268,7 @@
|
||||
self.manager.currentFeedbackComposeViewController = nil;
|
||||
|
||||
[super viewWillDisappear:animated];
|
||||
|
||||
|
||||
[[UIApplication sharedApplication] setStatusBarStyle:_statusBarStyle];
|
||||
}
|
||||
|
||||
@ -218,6 +276,89 @@
|
||||
[super viewDidDisappear:animated];
|
||||
}
|
||||
|
||||
-(void)refreshAttachmentScrollview {
|
||||
CGFloat scrollViewWidth = 0;
|
||||
|
||||
if (self.imageAttachments.count){
|
||||
scrollViewWidth = 100;
|
||||
}
|
||||
|
||||
CGRect textViewFrame = self.textView.frame;
|
||||
|
||||
CGRect scrollViewFrame = self.attachmentScrollView.frame;
|
||||
|
||||
BOOL alreadySetup = CGRectGetWidth(scrollViewFrame) > 0;
|
||||
|
||||
if (alreadySetup && self.imageAttachments.count == 0) {
|
||||
textViewFrame.size.width += 100;
|
||||
self.textView.frame = textViewFrame;
|
||||
scrollViewFrame.size.width = 0;
|
||||
self.attachmentScrollView.frame = scrollViewFrame;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!alreadySetup) {
|
||||
textViewFrame.size.width -= scrollViewWidth;
|
||||
scrollViewFrame = CGRectMake(CGRectGetMaxX(textViewFrame), self.view.frame.origin.y, scrollViewWidth, CGRectGetHeight(self.view.bounds));
|
||||
self.textView.frame = textViewFrame;
|
||||
self.attachmentScrollView.frame = scrollViewFrame;
|
||||
self.attachmentScrollView.contentInset = self.textView.contentInset;
|
||||
}
|
||||
|
||||
if (self.imageAttachments.count > self.attachmentScrollViewImageViews.count){
|
||||
NSInteger numberOfViewsToCreate = self.imageAttachments.count - self.attachmentScrollViewImageViews.count;
|
||||
for (int i = 0; i <numberOfViewsToCreate; i++) {
|
||||
UIButton *newImageButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
[newImageButton addTarget:self action:@selector(imageButtonAction:) forControlEvents:UIControlEventTouchUpInside];
|
||||
[self.attachmentScrollViewImageViews addObject:newImageButton];
|
||||
[self.attachmentScrollView addSubview:newImageButton];
|
||||
}
|
||||
}
|
||||
|
||||
int index = 0;
|
||||
|
||||
CGFloat currentYOffset = 0.0f;
|
||||
|
||||
NSEnumerator *reverseAttachments = self.imageAttachments.reverseObjectEnumerator;
|
||||
|
||||
for (BITFeedbackMessageAttachment *attachment in reverseAttachments.allObjects){
|
||||
UIButton *imageButton = self.attachmentScrollViewImageViews[index];
|
||||
|
||||
UIImage *image = [attachment thumbnailWithSize:CGSizeMake(100, 100)];
|
||||
|
||||
// determine the factor by which we scale..
|
||||
CGFloat scaleFactor = CGRectGetWidth(self.attachmentScrollView.frame) / image.size.width;
|
||||
|
||||
CGFloat height = image.size.height * scaleFactor;
|
||||
|
||||
imageButton.frame = CGRectInset(CGRectMake(0, currentYOffset, scaleFactor * image.size.width, height), 10, 10);
|
||||
|
||||
currentYOffset += height;
|
||||
|
||||
[imageButton setImage:image forState:UIControlStateNormal];
|
||||
index++;
|
||||
}
|
||||
|
||||
[self.attachmentScrollView setContentSize:CGSizeMake(CGRectGetWidth(self.attachmentScrollView.frame), currentYOffset)];
|
||||
|
||||
[self updateBarButtonState];
|
||||
}
|
||||
|
||||
- (void)updateBarButtonState {
|
||||
if (self.textView.text.length > 0 ) {
|
||||
self.navigationItem.rightBarButtonItem.enabled = YES;
|
||||
} else {
|
||||
self.navigationItem.rightBarButtonItem.enabled = NO;
|
||||
}
|
||||
|
||||
if (self.imageAttachments.count > 2){
|
||||
self.textView.inputAccessoryView = nil;
|
||||
} else {
|
||||
self.textView.inputAccessoryView = self.textAccessoryView;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - UIViewController Rotation
|
||||
|
||||
@ -241,6 +382,10 @@
|
||||
#pragma mark - Actions
|
||||
|
||||
- (void)dismissAction:(id)sender {
|
||||
for (BITFeedbackMessageAttachment *attachment in self.attachments){
|
||||
[attachment deleteContents];
|
||||
}
|
||||
|
||||
[self dismissWithResult:BITFeedbackComposeResultCancelled];
|
||||
}
|
||||
|
||||
@ -250,7 +395,7 @@
|
||||
|
||||
NSString *text = self.textView.text;
|
||||
|
||||
[self.manager submitMessageWithText:text];
|
||||
[self.manager submitMessageWithText:text andAttachments:self.attachments];
|
||||
|
||||
[self dismissWithResult:BITFeedbackComposeResultSubmitted];
|
||||
}
|
||||
@ -268,6 +413,70 @@
|
||||
}
|
||||
}
|
||||
|
||||
-(void)addPhotoAction:(id)sender {
|
||||
if (_actionSheetVisible) return;
|
||||
|
||||
// add photo.
|
||||
UIImagePickerController *pickerController = [[UIImagePickerController alloc] init];
|
||||
pickerController.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
|
||||
pickerController.delegate = self;
|
||||
pickerController.editing = NO;
|
||||
[self presentViewController:pickerController animated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (void)scrollViewTapped:(id)unused {
|
||||
UIMenuController *menuController = [UIMenuController sharedMenuController];
|
||||
[menuController setTargetRect:CGRectMake([self.tapRecognizer locationInView:self.view].x, [self.tapRecognizer locationInView:self.view].x, 1, 1) inView:self.view];
|
||||
[menuController setMenuVisible:YES animated:YES];
|
||||
}
|
||||
|
||||
- (void)paste:(id)sender {
|
||||
|
||||
}
|
||||
|
||||
#pragma mark - UIImagePickerControllerDelegate
|
||||
|
||||
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info {
|
||||
UIImage *pickedImage = info[UIImagePickerControllerOriginalImage];
|
||||
|
||||
if (pickedImage){
|
||||
NSData *imageData = UIImageJPEGRepresentation(pickedImage, 0.7f);
|
||||
BITFeedbackMessageAttachment *newAttachment = [BITFeedbackMessageAttachment attachmentWithData:imageData contentType:@"image/jpeg"];
|
||||
NSURL *imagePath = [info objectForKey:@"UIImagePickerControllerReferenceURL"];
|
||||
NSString *imageName = [imagePath lastPathComponent];
|
||||
newAttachment.originalFilename = imageName;
|
||||
[self.attachments addObject:newAttachment];
|
||||
[self.imageAttachments addObject:newAttachment];
|
||||
}
|
||||
|
||||
[picker dismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker {
|
||||
[picker dismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (void)imageButtonAction:(UIButton *)sender {
|
||||
// determine the index of the feedback
|
||||
NSInteger index = [self.attachmentScrollViewImageViews indexOfObject:sender];
|
||||
|
||||
self.selectedAttachmentIndex = index;
|
||||
|
||||
UIActionSheet *actionSheet = [[UIActionSheet alloc] initWithTitle: nil
|
||||
delegate: self
|
||||
cancelButtonTitle: BITHockeyLocalizedString(@"HockeyFeedbackComposeAttachmentCancel")
|
||||
destructiveButtonTitle: BITHockeyLocalizedString(@"HockeyFeedbackComposeAttachmentDelete")
|
||||
otherButtonTitles: BITHockeyLocalizedString(@"HockeyFeedbackComposeAttachmentEdit"), nil];
|
||||
|
||||
[actionSheet showFromRect: sender.frame inView: self.attachmentScrollView animated: YES];
|
||||
|
||||
_actionSheetVisible = YES;
|
||||
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
|
||||
[self.textView resignFirstResponder];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - BITFeedbackUserDataDelegate
|
||||
|
||||
- (void)userDataUpdateCancelled {
|
||||
@ -298,15 +507,61 @@
|
||||
#pragma mark - UITextViewDelegate
|
||||
|
||||
- (void)textViewDidChange:(UITextView *)textView {
|
||||
NSUInteger newLength = [textView.text length];
|
||||
if (newLength == 0) {
|
||||
self.navigationItem.rightBarButtonItem.enabled = NO;
|
||||
} else {
|
||||
self.navigationItem.rightBarButtonItem.enabled = YES;
|
||||
}
|
||||
[self updateBarButtonState];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - UIActionSheet Delegate
|
||||
|
||||
- (void)actionSheet:(UIActionSheet *)actionSheet didDismissWithButtonIndex:(NSInteger)buttonIndex {
|
||||
if (buttonIndex == [actionSheet destructiveButtonIndex]) {
|
||||
|
||||
if (self.selectedAttachmentIndex != NSNotFound){
|
||||
BITFeedbackMessageAttachment *attachment = self.imageAttachments[self.selectedAttachmentIndex];
|
||||
[attachment deleteContents]; // mandatory call to delete the files associatd.
|
||||
[self.imageAttachments removeObject:attachment];
|
||||
[self.attachments removeObject:attachment];
|
||||
}
|
||||
self.selectedAttachmentIndex = NSNotFound;
|
||||
|
||||
[self refreshAttachmentScrollview];
|
||||
|
||||
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
|
||||
[self.textView becomeFirstResponder];
|
||||
}
|
||||
} else if (buttonIndex != [actionSheet cancelButtonIndex]) {
|
||||
if (self.selectedAttachmentIndex != NSNotFound){
|
||||
BITFeedbackMessageAttachment *attachment = self.imageAttachments[self.selectedAttachmentIndex];
|
||||
BITImageAnnotationViewController *annotationEditor = [[BITImageAnnotationViewController alloc ] init];
|
||||
annotationEditor.delegate = self;
|
||||
UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:annotationEditor];
|
||||
annotationEditor.image = attachment.imageRepresentation;
|
||||
[self presentViewController:navController animated:YES completion:nil];
|
||||
}
|
||||
} else {
|
||||
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
|
||||
[self.textView becomeFirstResponder];
|
||||
}
|
||||
}
|
||||
_actionSheetVisible = NO;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Image Annotation Delegate
|
||||
|
||||
- (void)annotationController:(BITImageAnnotationViewController *)annotationController didFinishWithImage:(UIImage *)image {
|
||||
if (self.selectedAttachmentIndex != NSNotFound){
|
||||
BITFeedbackMessageAttachment *attachment = self.imageAttachments[self.selectedAttachmentIndex];
|
||||
[attachment replaceData:UIImageJPEGRepresentation(image, 0.7f)];
|
||||
}
|
||||
|
||||
self.selectedAttachmentIndex = NSNotFound;
|
||||
}
|
||||
|
||||
- (void)annotationControllerDidCancel:(BITImageAnnotationViewController *)annotationController {
|
||||
self.selectedAttachmentIndex = NSNotFound;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif /* HOCKEYSDK_FEATURE_FEEDBACK */
|
||||
|
@ -31,6 +31,15 @@
|
||||
#import "BITFeedbackMessage.h"
|
||||
#import "BITAttributedLabel.h"
|
||||
|
||||
@class BITFeedbackMessageAttachment;
|
||||
|
||||
@protocol BITFeedbackListViewCellDelegate <NSObject>
|
||||
|
||||
- (void)listCell:(id)cell didSelectAttachment:(BITFeedbackMessageAttachment *)attachment;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
/**
|
||||
* Cell style depending on the iOS version
|
||||
*/
|
||||
@ -59,6 +68,7 @@ typedef NS_ENUM(NSUInteger, BITFeedbackListViewCellBackgroundStyle) {
|
||||
BITFeedbackListViewCellBackgroundStyleAlternate = 1
|
||||
};
|
||||
|
||||
|
||||
@interface BITFeedbackListViewCell : UITableViewCell
|
||||
|
||||
@property (nonatomic, strong) BITFeedbackMessage *message;
|
||||
@ -69,6 +79,10 @@ typedef NS_ENUM(NSUInteger, BITFeedbackListViewCellBackgroundStyle) {
|
||||
|
||||
@property (nonatomic, strong) BITAttributedLabel *labelText;
|
||||
|
||||
@property (nonatomic, weak) id<BITFeedbackListViewCellDelegate> delegate;
|
||||
|
||||
+ (CGFloat) heightForRowWithMessage:(BITFeedbackMessage *)message tableViewWidth:(CGFloat)width;
|
||||
|
||||
- (void)setAttachments:(NSArray *)attachments;
|
||||
|
||||
@end
|
||||
|
@ -26,9 +26,11 @@
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#import "HockeySDKPrivate.h"
|
||||
|
||||
#import "BITFeedbackListViewCell.h"
|
||||
#import "HockeySDKPrivate.h"
|
||||
#import "BITFeedbackMessageAttachment.h"
|
||||
#import "BITActivityIndicatorButton.h"
|
||||
|
||||
#define BACKGROUNDCOLOR_DEFAULT BIT_RGBCOLOR(245, 245, 245)
|
||||
#define BACKGROUNDCOLOR_ALTERNATE BIT_RGBCOLOR(235, 235, 235)
|
||||
@ -54,6 +56,9 @@
|
||||
|
||||
#define LABEL_TEXT_Y 25
|
||||
|
||||
#define ATTACHMENT_SIZE 45
|
||||
|
||||
|
||||
@interface BITFeedbackListViewCell ()
|
||||
|
||||
@property (nonatomic, strong) NSDateFormatter *dateFormatter;
|
||||
@ -61,6 +66,10 @@
|
||||
|
||||
@property (nonatomic, strong) UILabel *labelTitle;
|
||||
|
||||
@property (nonatomic, strong) NSMutableArray *attachmentViews;
|
||||
|
||||
@property (nonatomic, strong) UIView *accessoryBackgroundView;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@ -79,15 +88,15 @@
|
||||
self.dateFormatter = [[NSDateFormatter alloc] init];
|
||||
[self.dateFormatter setTimeStyle:NSDateFormatterNoStyle];
|
||||
[self.dateFormatter setDateStyle:NSDateFormatterMediumStyle];
|
||||
[self.dateFormatter setLocale:[NSLocale currentLocale]];
|
||||
[self.dateFormatter setLocale:[NSLocale currentLocale]];
|
||||
[self.dateFormatter setDoesRelativeDateFormatting:YES];
|
||||
|
||||
|
||||
self.timeFormatter = [[NSDateFormatter alloc] init];
|
||||
[self.timeFormatter setTimeStyle:NSDateFormatterShortStyle];
|
||||
[self.timeFormatter setDateStyle:NSDateFormatterNoStyle];
|
||||
[self.timeFormatter setLocale:[NSLocale currentLocale]];
|
||||
[self.timeFormatter setDoesRelativeDateFormatting:YES];
|
||||
|
||||
|
||||
self.labelTitle = [[UILabel alloc] init];
|
||||
self.labelTitle.font = [UIFont systemFontOfSize:TITLE_FONTSIZE];
|
||||
|
||||
@ -96,6 +105,8 @@
|
||||
self.labelText.numberOfLines = 0;
|
||||
self.labelText.textAlignment = kBITTextLabelAlignmentLeft;
|
||||
self.labelText.dataDetectorTypes = UIDataDetectorTypeAll;
|
||||
|
||||
self.attachmentViews = [NSMutableArray new];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@ -104,6 +115,7 @@
|
||||
#pragma mark - Private
|
||||
|
||||
- (UIColor *)backgroundColor {
|
||||
|
||||
if (self.backgroundStyle == BITFeedbackListViewCellBackgroundStyleNormal) {
|
||||
if (self.style == BITFeedbackListViewCellPresentatationStyleDefault) {
|
||||
return BACKGROUNDCOLOR_DEFAULT;
|
||||
@ -135,6 +147,19 @@
|
||||
#pragma mark - Layout
|
||||
|
||||
+ (CGFloat) heightForRowWithMessage:(BITFeedbackMessage *)message tableViewWidth:(CGFloat)width {
|
||||
|
||||
CGFloat baseHeight = [self heightForTextInRowWithMessage:message tableViewWidth:width];
|
||||
|
||||
CGFloat attachmentsPerRow = floorf(width / (FRAME_SIDE_BORDER + ATTACHMENT_SIZE));
|
||||
|
||||
CGFloat calculatedHeight = baseHeight + (FRAME_TOP_BORDER + ATTACHMENT_SIZE) * ceil([message previewableAttachments].count / attachmentsPerRow);
|
||||
|
||||
return ceil(calculatedHeight);
|
||||
}
|
||||
|
||||
|
||||
|
||||
+ (CGFloat) heightForTextInRowWithMessage:(BITFeedbackMessage *)message tableViewWidth:(CGFloat)width {
|
||||
CGFloat calculatedHeight;
|
||||
|
||||
#if __IPHONE_OS_VERSION_MAX_ALLOWED > __IPHONE_6_1
|
||||
@ -143,7 +168,11 @@
|
||||
options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading
|
||||
attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:TEXT_FONTSIZE]}
|
||||
context:nil];
|
||||
calculatedHeight = calculatedRect.size.height + FRAME_TOP_BORDER + LABEL_TEXT_Y + FRAME_BOTTOM_BORDER;
|
||||
calculatedHeight = calculatedRect.size.height + FRAME_TOP_BORDER + LABEL_TEXT_Y + FRAME_BOTTOM_BORDER;
|
||||
|
||||
// added to make space for the images.
|
||||
|
||||
|
||||
} else {
|
||||
#endif
|
||||
#pragma clang diagnostic push
|
||||
@ -151,6 +180,7 @@
|
||||
calculatedHeight = [message.text sizeWithFont:[UIFont systemFontOfSize:TEXT_FONTSIZE]
|
||||
constrainedToSize:CGSizeMake(width - (2 * FRAME_SIDE_BORDER), CGFLOAT_MAX)
|
||||
].height + FRAME_TOP_BORDER + LABEL_TEXT_Y + FRAME_BOTTOM_BORDER;
|
||||
|
||||
#pragma clang diagnostic pop
|
||||
#if __IPHONE_OS_VERSION_MAX_ALLOWED > __IPHONE_6_1
|
||||
}
|
||||
@ -159,13 +189,48 @@
|
||||
return ceil(calculatedHeight);
|
||||
}
|
||||
|
||||
- (void)layoutSubviews {
|
||||
UIView *accessoryViewBackground = [[UIView alloc] initWithFrame:CGRectMake(0, 2, self.frame.size.width * 2, self.frame.size.height - 2)];
|
||||
accessoryViewBackground.autoresizingMask = UIViewAutoresizingFlexibleHeight;
|
||||
accessoryViewBackground.clipsToBounds = YES;
|
||||
- (void)setAttachments:(NSArray *)attachments {
|
||||
for (UIView *view in self.attachmentViews){
|
||||
[view removeFromSuperview];
|
||||
}
|
||||
|
||||
// colors
|
||||
accessoryViewBackground.backgroundColor = [self backgroundColor];
|
||||
[self.attachmentViews removeAllObjects];
|
||||
|
||||
for (BITFeedbackMessageAttachment *attachment in attachments){
|
||||
if (attachment.localURL || attachment.sourceURL) {
|
||||
BITActivityIndicatorButton *imageView = [BITActivityIndicatorButton buttonWithType:UIButtonTypeCustom];
|
||||
|
||||
if (attachment.localURL){
|
||||
[imageView setImage:[attachment thumbnailWithSize:CGSizeMake(ATTACHMENT_SIZE, ATTACHMENT_SIZE)] forState:UIControlStateNormal];
|
||||
[imageView setShowsActivityIndicator:NO];
|
||||
} else {
|
||||
[imageView setImage:nil forState:UIControlStateNormal];
|
||||
[imageView setShowsActivityIndicator:YES];
|
||||
}
|
||||
[imageView setContentMode:UIViewContentModeScaleAspectFit];
|
||||
[imageView addTarget:self action:@selector(imageButtonPressed:) forControlEvents:UIControlEventTouchUpInside];
|
||||
|
||||
[self.attachmentViews addObject:imageView];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (void)layoutSubviews {
|
||||
if (!self.accessoryBackgroundView){
|
||||
self.accessoryBackgroundView = [[UIView alloc] initWithFrame:CGRectMake(0, 2, self.frame.size.width * 2, self.frame.size.height - 2)];
|
||||
self.accessoryBackgroundView.autoresizingMask = UIViewAutoresizingFlexibleHeight;
|
||||
self.accessoryBackgroundView.clipsToBounds = YES;
|
||||
|
||||
// colors
|
||||
self.accessoryBackgroundView.backgroundColor = [self backgroundColor];
|
||||
}
|
||||
|
||||
if (self.style == BITFeedbackListViewCellPresentatationStyleDefault) {
|
||||
[self addSubview:self.accessoryBackgroundView];
|
||||
} else if (self.accessoryBackgroundView.superview){
|
||||
[self.accessoryBackgroundView removeFromSuperview];
|
||||
}
|
||||
self.contentView.backgroundColor = [self backgroundColor];
|
||||
self.labelTitle.backgroundColor = [self backgroundColor];
|
||||
self.labelText.backgroundColor = [self backgroundColor];
|
||||
@ -176,11 +241,9 @@
|
||||
} else {
|
||||
[self.labelText setTextColor:TEXTCOLOR_DEFAULT];
|
||||
}
|
||||
|
||||
|
||||
// background for deletion accessory view
|
||||
if (self.style == BITFeedbackListViewCellPresentatationStyleDefault) {
|
||||
[self addSubview:accessoryViewBackground];
|
||||
}
|
||||
|
||||
|
||||
// header
|
||||
NSString *dateString = @"";
|
||||
@ -195,7 +258,7 @@
|
||||
}
|
||||
[self.labelTitle setText:dateString];
|
||||
[self.labelTitle setFrame:CGRectMake(FRAME_SIDE_BORDER, FRAME_TOP_BORDER + LABEL_TITLE_Y, self.frame.size.width - (2 * FRAME_SIDE_BORDER), LABEL_TITLE_HEIGHT)];
|
||||
|
||||
|
||||
if (_message.userMessage) {
|
||||
self.labelTitle.textAlignment = kBITTextLabelAlignmentRight;
|
||||
self.labelText.textAlignment = kBITTextLabelAlignmentRight;
|
||||
@ -203,20 +266,58 @@
|
||||
self.labelTitle.textAlignment = kBITTextLabelAlignmentLeft;
|
||||
self.labelText.textAlignment = kBITTextLabelAlignmentLeft;
|
||||
}
|
||||
|
||||
|
||||
[self addSubview:self.labelTitle];
|
||||
|
||||
|
||||
// text
|
||||
[self.labelText setText:_message.text];
|
||||
CGSize size = CGSizeMake(self.frame.size.width - (2 * FRAME_SIDE_BORDER),
|
||||
[[self class] heightForRowWithMessage:_message tableViewWidth:self.frame.size.width] - LABEL_TEXT_Y - FRAME_BOTTOM_BORDER);
|
||||
CGSize sizeForTextLabel = CGSizeMake(self.frame.size.width - (2 * FRAME_SIDE_BORDER),
|
||||
[[self class] heightForTextInRowWithMessage:_message tableViewWidth:self.frame.size.width] - LABEL_TEXT_Y - FRAME_BOTTOM_BORDER);
|
||||
|
||||
[self.labelText setFrame:CGRectMake(FRAME_SIDE_BORDER, LABEL_TEXT_Y, size.width, size.height)];
|
||||
[self.labelText setFrame:CGRectMake(FRAME_SIDE_BORDER, LABEL_TEXT_Y, sizeForTextLabel.width, sizeForTextLabel.height)];
|
||||
|
||||
[self addSubview:self.labelText];
|
||||
|
||||
CGFloat baseOffsetOfText = CGRectGetMaxY(self.labelText.frame);
|
||||
|
||||
|
||||
int i = 0;
|
||||
|
||||
CGFloat attachmentsPerRow = floorf(self.frame.size.width / (FRAME_SIDE_BORDER + ATTACHMENT_SIZE));
|
||||
|
||||
for (BITActivityIndicatorButton *imageButton in self.attachmentViews) {
|
||||
imageButton.contentMode = UIViewContentModeScaleAspectFit;
|
||||
imageButton.imageView.contentMode = UIViewContentModeScaleAspectFill;
|
||||
|
||||
if (!_message.userMessage) {
|
||||
imageButton.frame = CGRectMake(FRAME_SIDE_BORDER + (FRAME_SIDE_BORDER + ATTACHMENT_SIZE) * (i%(int)attachmentsPerRow) , floor(i/attachmentsPerRow)*(FRAME_SIDE_BORDER + ATTACHMENT_SIZE) + baseOffsetOfText , ATTACHMENT_SIZE, ATTACHMENT_SIZE);
|
||||
} else {
|
||||
imageButton.frame = CGRectMake(self.frame.size.width - FRAME_SIDE_BORDER - ATTACHMENT_SIZE - ((FRAME_SIDE_BORDER + ATTACHMENT_SIZE) * (i%(int)attachmentsPerRow) ), floor(i/attachmentsPerRow)*(FRAME_SIDE_BORDER + ATTACHMENT_SIZE) + baseOffsetOfText , ATTACHMENT_SIZE, ATTACHMENT_SIZE);
|
||||
}
|
||||
|
||||
if (!imageButton.superview) {
|
||||
if (self.accessoryBackgroundView.superview) {
|
||||
[self insertSubview:imageButton aboveSubview:self.accessoryBackgroundView];
|
||||
} else {
|
||||
[self addSubview:imageButton];
|
||||
}
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
[super layoutSubviews];
|
||||
}
|
||||
|
||||
- (void)imageButtonPressed:(id)sender {
|
||||
if ([self.delegate respondsToSelector:@selector(listCell:didSelectAttachment:)]) {
|
||||
NSInteger index = [self.attachmentViews indexOfObject:sender];
|
||||
if (index != NSNotFound && [self.message previewableAttachments].count > index) {
|
||||
BITFeedbackMessageAttachment *attachment = [self.message previewableAttachments][index];
|
||||
[self.delegate listCell:self didSelectAttachment:attachment];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
|
@ -28,6 +28,7 @@
|
||||
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <QuickLook/QuickLook.h>
|
||||
|
||||
#import "BITHockeyBaseViewController.h"
|
||||
|
||||
@ -54,7 +55,7 @@
|
||||
This ensures that the presentation on iOS 6 and iOS 7 will use the corret design on each OS Version.
|
||||
*/
|
||||
|
||||
@interface BITFeedbackListViewController : BITHockeyBaseViewController <UITableViewDelegate, UITableViewDataSource, UIActionSheetDelegate, UIAlertViewDelegate> {
|
||||
@interface BITFeedbackListViewController : BITHockeyBaseViewController <UITableViewDelegate, UITableViewDataSource, UIActionSheetDelegate, UIAlertViewDelegate, QLPreviewControllerDataSource> {
|
||||
}
|
||||
|
||||
@end
|
||||
|
@ -34,17 +34,20 @@
|
||||
#import "HockeySDKPrivate.h"
|
||||
|
||||
#import "BITFeedbackManagerPrivate.h"
|
||||
#import "BITFeedbackManager.h"
|
||||
#import "BITFeedbackListViewController.h"
|
||||
#import "BITFeedbackListViewCell.h"
|
||||
#import "BITFeedbackComposeViewController.h"
|
||||
#import "BITFeedbackUserDataViewController.h"
|
||||
#import "BITFeedbackMessage.h"
|
||||
#import "BITFeedbackMessageAttachment.h"
|
||||
#import "BITAttributedLabel.h"
|
||||
|
||||
#import "BITHockeyBaseManagerPrivate.h"
|
||||
|
||||
#import "BITHockeyHelper.h"
|
||||
#import <QuartzCore/QuartzCore.h>
|
||||
#import <QuickLook/QuickLook.h>
|
||||
|
||||
|
||||
#define DEFAULT_BACKGROUNDCOLOR BIT_RGBCOLOR(245, 245, 245)
|
||||
@ -64,11 +67,13 @@
|
||||
#define BORDER_COLOR BIT_RGBCOLOR(215, 215, 215)
|
||||
|
||||
|
||||
@interface BITFeedbackListViewController () <BITFeedbackUserDataDelegate, BITFeedbackComposeViewControllerDelegate, BITAttributedLabelDelegate>
|
||||
@interface BITFeedbackListViewController () <BITFeedbackUserDataDelegate, BITFeedbackComposeViewControllerDelegate, BITAttributedLabelDelegate, BITFeedbackListViewCellDelegate>
|
||||
|
||||
@property (nonatomic, weak) BITFeedbackManager *manager;
|
||||
@property (nonatomic, strong) NSDateFormatter *lastUpdateDateFormatter;
|
||||
@property (nonatomic) BOOL userDataComposeFlow;
|
||||
@property (nonatomic, strong) NSArray *cachedPreviewItems;
|
||||
@property (nonatomic, strong) NSOperationQueue *thumbnailQueue;
|
||||
|
||||
@end
|
||||
|
||||
@ -90,6 +95,8 @@
|
||||
[self.lastUpdateDateFormatter setDateStyle:NSDateFormatterShortStyle];
|
||||
[self.lastUpdateDateFormatter setTimeStyle:NSDateFormatterShortStyle];
|
||||
self.lastUpdateDateFormatter.locale = [NSLocale currentLocale];
|
||||
|
||||
_thumbnailQueue = [NSOperationQueue new];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@ -98,7 +105,7 @@
|
||||
- (void)dealloc {
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self name:BITHockeyFeedbackMessagesLoadingStarted object:nil];
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self name:BITHockeyFeedbackMessagesLoadingFinished object:nil];
|
||||
|
||||
|
||||
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(showDelayedUserDataViewController) object:nil];
|
||||
}
|
||||
|
||||
@ -107,12 +114,12 @@
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(startLoadingIndicator)
|
||||
name:BITHockeyFeedbackMessagesLoadingStarted
|
||||
object:nil];
|
||||
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(updateList)
|
||||
name:BITHockeyFeedbackMessagesLoadingFinished
|
||||
@ -128,13 +135,13 @@
|
||||
[self.tableView setBackgroundColor:[UIColor colorWithRed:0.82 green:0.84 blue:0.84 alpha:1]];
|
||||
[self.tableView setSeparatorColor:[UIColor colorWithRed:0.79 green:0.79 blue:0.79 alpha:1]];
|
||||
} else {
|
||||
// [self.tableView setBackgroundColor:[UIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:1]];
|
||||
// [self.tableView setBackgroundColor:[UIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:1]];
|
||||
}
|
||||
|
||||
if ([self.manager isPreiOS7Environment]) {
|
||||
self.view.backgroundColor = DEFAULT_BACKGROUNDCOLOR;
|
||||
} else {
|
||||
// self.view.backgroundColor = DEFAULT_BACKGROUNDCOLOR_OS7;
|
||||
// self.view.backgroundColor = DEFAULT_BACKGROUNDCOLOR_OS7;
|
||||
}
|
||||
|
||||
id refreshClass = NSClassFromString(@"UIRefreshControl");
|
||||
@ -143,9 +150,9 @@
|
||||
[self.refreshControl addTarget:self action:@selector(reloadList) forControlEvents:UIControlEventValueChanged];
|
||||
} else {
|
||||
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemRefresh
|
||||
target:self
|
||||
action:@selector(reloadList)];
|
||||
}
|
||||
target:self
|
||||
action:@selector(reloadList)];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)startLoadingIndicator {
|
||||
@ -184,7 +191,9 @@
|
||||
CGSize contentSize = self.tableView.contentSize;
|
||||
CGPoint contentOffset = self.tableView.contentOffset;
|
||||
|
||||
[self refreshPreviewItems];
|
||||
[self.tableView reloadData];
|
||||
|
||||
if (contentSize.height > 0 &&
|
||||
self.tableView.contentSize.height > self.tableView.frame.size.height &&
|
||||
self.tableView.contentSize.height > contentSize.height &&
|
||||
@ -192,7 +201,7 @@
|
||||
[self.tableView setContentOffset:CGPointMake(contentOffset.x, self.tableView.contentSize.height - contentSize.height + contentOffset.y) animated:NO];
|
||||
|
||||
[self stopLoadingIndicator];
|
||||
|
||||
|
||||
[self.tableView flashScrollIndicators];
|
||||
}
|
||||
|
||||
@ -224,7 +233,7 @@
|
||||
} else {
|
||||
[self.tableView reloadData];
|
||||
}
|
||||
|
||||
|
||||
[super viewDidAppear:animated];
|
||||
}
|
||||
|
||||
@ -265,6 +274,8 @@
|
||||
|
||||
- (void)deleteAllMessages {
|
||||
[_manager deleteAllMessages];
|
||||
[self refreshPreviewItems];
|
||||
|
||||
[self.tableView reloadData];
|
||||
}
|
||||
|
||||
@ -282,9 +293,9 @@
|
||||
} else {
|
||||
UIAlertView *deleteAction = [[UIAlertView alloc] initWithTitle:BITHockeyLocalizedString(@"HockeyFeedbackListButonDeleteAllMessages")
|
||||
message:BITHockeyLocalizedString(@"HockeyFeedbackListDeleteAllTitle")
|
||||
delegate:self
|
||||
cancelButtonTitle:BITHockeyLocalizedString(@"HockeyFeedbackListDeleteAllCancel")
|
||||
otherButtonTitles:BITHockeyLocalizedString(@"HockeyFeedbackListDeleteAllDelete"), nil];
|
||||
delegate:self
|
||||
cancelButtonTitle:BITHockeyLocalizedString(@"HockeyFeedbackListDeleteAllCancel")
|
||||
otherButtonTitles:BITHockeyLocalizedString(@"HockeyFeedbackListDeleteAllDelete"), nil];
|
||||
|
||||
[deleteAction setTag:0];
|
||||
[deleteAction show];
|
||||
@ -329,6 +340,7 @@
|
||||
|
||||
-(void)userDataUpdateFinished {
|
||||
[self.manager saveMessages];
|
||||
[self refreshPreviewItems];
|
||||
|
||||
if (self.userDataComposeFlow) {
|
||||
if ([self.manager showFirstRequiredPresentationModal]) {
|
||||
@ -487,7 +499,7 @@
|
||||
|
||||
if (!cell) {
|
||||
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
|
||||
|
||||
|
||||
cell.textLabel.font = [UIFont systemFontOfSize:14];
|
||||
cell.textLabel.numberOfLines = 0;
|
||||
cell.accessoryType = UITableViewCellAccessoryNone;
|
||||
@ -628,7 +640,26 @@
|
||||
cell.message = message;
|
||||
cell.labelText.delegate = self;
|
||||
cell.labelText.userInteractionEnabled = YES;
|
||||
|
||||
cell.delegate = self;
|
||||
[cell setAttachments:message.previewableAttachments];
|
||||
|
||||
for (BITFeedbackMessageAttachment *attachment in message.attachments){
|
||||
if (attachment.needsLoadingFromURL && !attachment.isLoading){
|
||||
attachment.isLoading = YES;
|
||||
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:attachment.sourceURL]];
|
||||
[NSURLConnection sendAsynchronousRequest:request queue:self.thumbnailQueue completionHandler:^(NSURLResponse *response, NSData *responseData, NSError *err) {
|
||||
if (responseData.length) {
|
||||
[attachment replaceData:responseData];
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self.tableView reloadData];
|
||||
});
|
||||
|
||||
[[BITHockeyManager sharedHockeyManager].feedbackManager saveMessages];
|
||||
}
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
[self.manager isPreiOS7Environment] ||
|
||||
(![self.manager isPreiOS7Environment] && indexPath.row != 0)
|
||||
@ -653,12 +684,19 @@
|
||||
|
||||
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
if (editingStyle == UITableViewCellEditingStyleDelete) {
|
||||
BITFeedbackMessage *message = [self.manager messageAtIndex:indexPath.row];
|
||||
BOOL messageHasAttachments = ([message attachments].count > 0);
|
||||
|
||||
if ([_manager deleteMessageAtIndex:indexPath.row]) {
|
||||
if ([_manager numberOfMessages] > 0) {
|
||||
[tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
|
||||
} else {
|
||||
[tableView reloadData];
|
||||
}
|
||||
|
||||
if (messageHasAttachments) {
|
||||
[self refreshPreviewItems];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -753,7 +791,7 @@
|
||||
if (buttonIndex == actionSheet.cancelButtonIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if ([actionSheet tag] == 0) {
|
||||
if (buttonIndex == [actionSheet destructiveButtonIndex]) {
|
||||
[self deleteAllMessages];
|
||||
@ -768,6 +806,69 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - ListViewCellDelegate
|
||||
|
||||
- (void)listCell:(id)cell didSelectAttachment:(BITFeedbackMessageAttachment *)attachment {
|
||||
QLPreviewController *previewController = [[QLPreviewController alloc] init];
|
||||
previewController.dataSource = self;
|
||||
|
||||
[self presentViewController:previewController animated:YES completion:nil];
|
||||
|
||||
if (self.cachedPreviewItems.count > [self.cachedPreviewItems indexOfObject:attachment]) {
|
||||
[previewController setCurrentPreviewItemIndex:[self.cachedPreviewItems indexOfObject:attachment]];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)refreshPreviewItems {
|
||||
self.cachedPreviewItems = nil;
|
||||
NSMutableArray *collectedAttachments = [NSMutableArray new];
|
||||
|
||||
for (int i = 0; i < self.manager.numberOfMessages; i++) {
|
||||
BITFeedbackMessage *message = [self.manager messageAtIndex:i];
|
||||
[collectedAttachments addObjectsFromArray:message.previewableAttachments];
|
||||
}
|
||||
|
||||
self.cachedPreviewItems = collectedAttachments;
|
||||
}
|
||||
|
||||
- (NSInteger)numberOfPreviewItemsInPreviewController:(QLPreviewController *)controller {
|
||||
if (!self.cachedPreviewItems){
|
||||
[self refreshPreviewItems];
|
||||
}
|
||||
|
||||
return self.cachedPreviewItems.count;
|
||||
}
|
||||
|
||||
- (id <QLPreviewItem>)previewController:(QLPreviewController *)controller previewItemAtIndex:(NSInteger)index {
|
||||
if (index >= 0) {
|
||||
__weak QLPreviewController* blockController = controller;
|
||||
BITFeedbackMessageAttachment *attachment = self.cachedPreviewItems[index];
|
||||
|
||||
if (attachment.needsLoadingFromURL && !attachment.isLoading) {
|
||||
attachment.isLoading = YES;
|
||||
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:attachment.sourceURL]];
|
||||
[NSURLConnection sendAsynchronousRequest:request queue:self.thumbnailQueue completionHandler:^(NSURLResponse *response, NSData *responseData, NSError *err) {
|
||||
attachment.isLoading = NO;
|
||||
if (responseData.length) {
|
||||
[attachment replaceData:responseData];
|
||||
[blockController reloadData];
|
||||
|
||||
[[BITHockeyManager sharedHockeyManager].feedbackManager saveMessages];
|
||||
} else {
|
||||
[blockController reloadData];
|
||||
}
|
||||
}];
|
||||
|
||||
return attachment;
|
||||
} else {
|
||||
return self.cachedPreviewItems[index];
|
||||
}
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif /* HOCKEYSDK_FEATURE_FEEDBACK */
|
||||
|
@ -32,6 +32,7 @@
|
||||
#import "BITHockeyBaseManager.h"
|
||||
#import "BITFeedbackListViewController.h"
|
||||
#import "BITFeedbackComposeViewController.h"
|
||||
#import "HockeySDKPrivate.h"
|
||||
|
||||
|
||||
// Notification message which tells that loading messages finished
|
||||
@ -59,6 +60,24 @@ typedef NS_ENUM(NSInteger, BITFeedbackUserDataElement) {
|
||||
BITFeedbackUserDataElementRequired = 2
|
||||
};
|
||||
|
||||
/**
|
||||
* Available modes for opening the feedback compose interface with a screenshot attached
|
||||
*/
|
||||
typedef NS_ENUM(NSInteger, BITFeedbackObservationMode) {
|
||||
/**
|
||||
* No SDK provided trigger is active.
|
||||
*/
|
||||
BITFeedbackObservationNone = 0,
|
||||
/**
|
||||
* Triggeres when the user takes a screenshot. This will grab the latest image from the camera roll. Requires iOS 7 or later!
|
||||
*/
|
||||
BITFeedbackObservationModeOnScreenshot = 1,
|
||||
/**
|
||||
* Triggers when the user tapps with three fingers for three seconds on the screen.
|
||||
*/
|
||||
BITFeedbackObservationModeThreeFingerTap = 2
|
||||
};
|
||||
|
||||
|
||||
@class BITFeedbackMessage;
|
||||
@protocol BITFeedbackManagerDelegate;
|
||||
@ -192,6 +211,26 @@ typedef NS_ENUM(NSInteger, BITFeedbackUserDataElement) {
|
||||
@property (nonatomic, readwrite) BOOL showAlertOnIncomingMessages;
|
||||
|
||||
|
||||
/**
|
||||
Define the trigger that opens the feedback composer and attaches a screenshot
|
||||
|
||||
The following modes are available:
|
||||
|
||||
- `BITFeedbackObservationNone`: No SDK based trigger is active. You can implement your
|
||||
own trigger and then call `[[BITHockeyManager sharedHockeyManager].feedbackManager showFeedbackComposeViewWithGeneratedScreenshot];` to handle your custom events
|
||||
that should trigger this.
|
||||
- `BITFeedbackObservationModeOnScreenshot`: Triggeres when the user takes a screenshot.
|
||||
This will grab the latest image from the camera roll. Requires iOS 7 or later!
|
||||
- `BITFeedbackObservationModeThreeFingerTap`: Triggers when the user tapps with three fingers
|
||||
for three seconds on the screen.
|
||||
|
||||
Default is `BITFeedbackObservationNone`
|
||||
|
||||
@see showFeedbackComposeViewWithGeneratedScreenshot
|
||||
*/
|
||||
@property (nonatomic, readwrite) BITFeedbackObservationMode feedbackObservationMode;
|
||||
|
||||
|
||||
///-----------------------------------------------------------------------------
|
||||
/// @name User Interface
|
||||
///-----------------------------------------------------------------------------
|
||||
@ -241,6 +280,24 @@ typedef NS_ENUM(NSInteger, BITFeedbackUserDataElement) {
|
||||
*/
|
||||
- (void)showFeedbackComposeView;
|
||||
|
||||
/**
|
||||
Present the modal feedback compose message user interface with the items given.
|
||||
All NSString-Content in the array will be concatenated and result in the message,
|
||||
while all UIImage and NSData-instances will be turned into attachments.
|
||||
*/
|
||||
- (void)showFeedbackComposeViewWithPreparedItems:(NSArray *)items;
|
||||
|
||||
/**
|
||||
Presents a modal feedback compose interface with a screenshot attached which is taken at the time of calling this method.
|
||||
|
||||
This should be used when your own trigger fires. The following code should be used:
|
||||
|
||||
[[BITHockeyManager sharedHockeyManager].feedbackManager showFeedbackComposeViewWithGeneratedScreenshot];
|
||||
|
||||
@see feedbackObservationMode
|
||||
*/
|
||||
- (void)showFeedbackComposeViewWithGeneratedScreenshot;
|
||||
|
||||
|
||||
/**
|
||||
Create an feedback compose view
|
||||
|
@ -29,11 +29,14 @@
|
||||
|
||||
#import "HockeySDK.h"
|
||||
|
||||
#import <AssetsLibrary/AssetsLibrary.h>
|
||||
|
||||
#if HOCKEYSDK_FEATURE_FEEDBACK
|
||||
|
||||
#import "HockeySDKPrivate.h"
|
||||
|
||||
#import "BITFeedbackManager.h"
|
||||
#import "BITFeedbackMessageAttachment.h"
|
||||
#import "BITFeedbackManagerPrivate.h"
|
||||
#import "BITHockeyBaseManagerPrivate.h"
|
||||
|
||||
@ -50,6 +53,12 @@
|
||||
#define kBITFeedbackLastMessageID @"HockeyFeedbackLastMessageID"
|
||||
#define kBITFeedbackAppID @"HockeyFeedbackAppID"
|
||||
|
||||
@interface BITFeedbackManager()<UIGestureRecognizerDelegate>
|
||||
|
||||
@property (nonatomic, strong) UITapGestureRecognizer *tapRecognizer;
|
||||
@property (nonatomic) BOOL screenshotNotificationEnabled;
|
||||
|
||||
@end
|
||||
|
||||
@implementation BITFeedbackManager {
|
||||
NSFileManager *_fileManager;
|
||||
@ -62,6 +71,8 @@
|
||||
BOOL _incomingMessagesAlertShowing;
|
||||
BOOL _didEnterBackgroundState;
|
||||
BOOL _networkRequestInProgress;
|
||||
|
||||
BITFeedbackObservationMode _observationMode;
|
||||
}
|
||||
|
||||
#pragma mark - Initialization
|
||||
@ -76,7 +87,7 @@
|
||||
_requireUserEmail = BITFeedbackUserDataElementOptional;
|
||||
_showAlertOnIncomingMessages = YES;
|
||||
_showFirstRequiredPresentationModal = YES;
|
||||
|
||||
|
||||
_disableFeedbackManager = NO;
|
||||
_networkRequestInProgress = NO;
|
||||
_incomingMessagesAlertShowing = NO;
|
||||
@ -85,9 +96,9 @@
|
||||
_lastMessageID = nil;
|
||||
|
||||
self.feedbackList = [NSMutableArray array];
|
||||
|
||||
|
||||
_fileManager = [[NSFileManager alloc] init];
|
||||
|
||||
|
||||
_settingsFile = [bit_settingsDir() stringByAppendingPathComponent:BITHOCKEY_FEEDBACK_SETTINGS];
|
||||
|
||||
_userID = nil;
|
||||
@ -147,12 +158,12 @@
|
||||
}
|
||||
if(nil == _networkDidBecomeReachableObserver) {
|
||||
_networkDidBecomeReachableObserver = [[NSNotificationCenter defaultCenter] addObserverForName:BITHockeyNetworkDidBecomeReachableNotification
|
||||
object:nil
|
||||
queue:NSOperationQueue.mainQueue
|
||||
usingBlock:^(NSNotification *note) {
|
||||
typeof(self) strongSelf = weakSelf;
|
||||
[strongSelf didBecomeActiveActions];
|
||||
}];
|
||||
object:nil
|
||||
queue:NSOperationQueue.mainQueue
|
||||
usingBlock:^(NSNotification *note) {
|
||||
typeof(self) strongSelf = weakSelf;
|
||||
[strongSelf didBecomeActiveActions];
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
@ -213,20 +224,31 @@
|
||||
}
|
||||
|
||||
- (void)showFeedbackComposeView {
|
||||
[self showFeedbackComposeViewWithPreparedItems:nil];
|
||||
}
|
||||
|
||||
- (void)showFeedbackComposeViewWithPreparedItems:(NSArray *)items{
|
||||
if (_currentFeedbackComposeViewController) {
|
||||
BITHockeyLog(@"INFO: update view already visible, aborting");
|
||||
return;
|
||||
}
|
||||
BITFeedbackComposeViewController *composeView = [self feedbackComposeViewController];
|
||||
[composeView prepareWithItems:items];
|
||||
|
||||
[self showView:composeView];
|
||||
|
||||
[self showView:[self feedbackComposeViewController]];
|
||||
}
|
||||
|
||||
- (void)showFeedbackComposeViewWithGeneratedScreenshot {
|
||||
UIImage *screenshot = bit_screenshot();
|
||||
[self showFeedbackComposeViewWithPreparedItems:@[screenshot]];
|
||||
}
|
||||
|
||||
#pragma mark - Manager Control
|
||||
|
||||
- (void)startManager {
|
||||
if ([self isFeedbackManagerDisabled]) return;
|
||||
|
||||
|
||||
[self registerObservers];
|
||||
|
||||
// we are already delayed, so the notification already came in and this won't invoked twice
|
||||
@ -273,12 +295,12 @@
|
||||
userIDForHockeyManager:[BITHockeyManager sharedHockeyManager]
|
||||
componentManager:self];
|
||||
}
|
||||
|
||||
|
||||
if (userID) {
|
||||
availableViaDelegate = YES;
|
||||
self.userID = userID;
|
||||
}
|
||||
|
||||
|
||||
return availableViaDelegate;
|
||||
}
|
||||
|
||||
@ -290,16 +312,16 @@
|
||||
if ([BITHockeyManager sharedHockeyManager].delegate &&
|
||||
[[BITHockeyManager sharedHockeyManager].delegate respondsToSelector:@selector(userNameForHockeyManager:componentManager:)]) {
|
||||
userName = [[BITHockeyManager sharedHockeyManager].delegate
|
||||
userNameForHockeyManager:[BITHockeyManager sharedHockeyManager]
|
||||
componentManager:self];
|
||||
userNameForHockeyManager:[BITHockeyManager sharedHockeyManager]
|
||||
componentManager:self];
|
||||
}
|
||||
|
||||
|
||||
if (userName) {
|
||||
availableViaDelegate = YES;
|
||||
self.userName = userName;
|
||||
self.requireUserName = BITFeedbackUserDataElementDontShow;
|
||||
}
|
||||
|
||||
|
||||
return availableViaDelegate;
|
||||
}
|
||||
|
||||
@ -307,20 +329,20 @@
|
||||
BOOL availableViaDelegate = NO;
|
||||
|
||||
NSString *userEmail = [self stringValueFromKeychainForKey:kBITHockeyMetaUserEmail];
|
||||
|
||||
|
||||
if ([BITHockeyManager sharedHockeyManager].delegate &&
|
||||
[[BITHockeyManager sharedHockeyManager].delegate respondsToSelector:@selector(userEmailForHockeyManager:componentManager:)]) {
|
||||
userEmail = [[BITHockeyManager sharedHockeyManager].delegate
|
||||
userEmailForHockeyManager:[BITHockeyManager sharedHockeyManager]
|
||||
componentManager:self];
|
||||
}
|
||||
|
||||
|
||||
if (userEmail) {
|
||||
availableViaDelegate = YES;
|
||||
self.userEmail = userEmail;
|
||||
self.requireUserEmail = BITFeedbackUserDataElementDontShow;
|
||||
}
|
||||
|
||||
|
||||
return availableViaDelegate;
|
||||
}
|
||||
|
||||
@ -328,7 +350,7 @@
|
||||
[self updateUserIDUsingKeychainAndDelegate];
|
||||
[self updateUserNameUsingKeychainAndDelegate];
|
||||
[self updateUserEmailUsingKeychainAndDelegate];
|
||||
|
||||
|
||||
// if both values are shown via the delegates, we never ever did ask and will never ever ask for user data
|
||||
if (self.requireUserName == BITFeedbackUserDataElementDontShow &&
|
||||
self.requireUserEmail == BITFeedbackUserDataElementDontShow) {
|
||||
@ -345,7 +367,7 @@
|
||||
|
||||
if (![_fileManager fileExistsAtPath:_settingsFile])
|
||||
return;
|
||||
|
||||
|
||||
NSData *codedData = [[NSData alloc] initWithContentsOfFile:_settingsFile];
|
||||
if (codedData == nil) return;
|
||||
|
||||
@ -357,7 +379,7 @@
|
||||
@catch (NSException *exception) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (!userIDViaDelegate) {
|
||||
if ([unarchiver containsValueForKey:kBITFeedbackUserID]) {
|
||||
self.userID = [unarchiver decodeObjectForKey:kBITFeedbackUserID];
|
||||
@ -373,7 +395,7 @@
|
||||
}
|
||||
self.userName = [self stringValueFromKeychainForKey:kBITFeedbackName];
|
||||
}
|
||||
|
||||
|
||||
if (!userEmailViaDelegate) {
|
||||
if ([unarchiver containsValueForKey:kBITFeedbackEmail]) {
|
||||
self.userEmail = [unarchiver decodeObjectForKey:kBITFeedbackEmail];
|
||||
@ -393,7 +415,7 @@
|
||||
|
||||
if ([unarchiver containsValueForKey:kBITFeedbackAppID]) {
|
||||
NSString *appID = [unarchiver decodeObjectForKey:kBITFeedbackAppID];
|
||||
|
||||
|
||||
// the stored thread is from another application identifier, so clear the token
|
||||
// which will cause the new posts to create a new thread on the server for the
|
||||
// current app identifier
|
||||
@ -416,9 +438,9 @@
|
||||
// inform the UI to update its data in case the list is already showing
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:BITHockeyFeedbackMessagesLoadingFinished object:nil];
|
||||
}
|
||||
|
||||
|
||||
[unarchiver finishDecoding];
|
||||
|
||||
|
||||
if (!self.lastCheck) {
|
||||
self.lastCheck = [NSDate distantPast];
|
||||
}
|
||||
@ -430,7 +452,7 @@
|
||||
|
||||
NSMutableData *data = [[NSMutableData alloc] init];
|
||||
NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
|
||||
|
||||
|
||||
if (_didAskUserData)
|
||||
[archiver encodeObject:[NSNumber numberWithBool:YES] forKey:kBITFeedbackUserDataAsked];
|
||||
|
||||
@ -571,6 +593,8 @@
|
||||
|
||||
- (BOOL)deleteMessageAtIndex:(NSUInteger)index {
|
||||
if (_feedbackList && [_feedbackList count] > index && [_feedbackList objectAtIndex:index]) {
|
||||
BITFeedbackMessage *message = _feedbackList[index];
|
||||
[message deleteContents];
|
||||
[_feedbackList removeObjectAtIndex:index];
|
||||
|
||||
[self saveMessages];
|
||||
@ -613,7 +637,7 @@
|
||||
|
||||
- (BOOL)isManualUserDataAvailable {
|
||||
[self updateAppDefinedUserData];
|
||||
|
||||
|
||||
if ((self.requireUserName != BITFeedbackUserDataElementDontShow && self.userName) ||
|
||||
(self.requireUserEmail != BITFeedbackUserDataElementDontShow && self.userEmail))
|
||||
return YES;
|
||||
@ -627,23 +651,23 @@
|
||||
- (void)updateMessageListFromResponse:(NSDictionary *)jsonDictionary {
|
||||
if (!jsonDictionary) {
|
||||
// nil is used when the server returns 404, so we need to mark all existing threads as archives and delete the discussion token
|
||||
|
||||
|
||||
NSArray *messagesSendInProgress = [self messagesWithStatus:BITFeedbackMessageStatusSendInProgress];
|
||||
NSInteger pendingMessagesCount = [messagesSendInProgress count] + [[self messagesWithStatus:BITFeedbackMessageStatusSendPending] count];
|
||||
|
||||
|
||||
[self markSendInProgressMessagesAsPending];
|
||||
|
||||
[_feedbackList enumerateObjectsUsingBlock:^(id objMessage, NSUInteger messagesIdx, BOOL *stop) {
|
||||
if ([(BITFeedbackMessage *)objMessage status] != BITFeedbackMessageStatusSendPending)
|
||||
[(BITFeedbackMessage *)objMessage setStatus:BITFeedbackMessageStatusArchived];
|
||||
}];
|
||||
|
||||
|
||||
if ([self token]) {
|
||||
self.token = nil;
|
||||
}
|
||||
|
||||
NSInteger pendingMessagesCountAfterProcessing = [[self messagesWithStatus:BITFeedbackMessageStatusSendPending] count];
|
||||
|
||||
|
||||
[self saveMessages];
|
||||
|
||||
// check if this request was successful and we have more messages pending and continue if positive
|
||||
@ -692,13 +716,21 @@
|
||||
*stop2 = YES;
|
||||
}
|
||||
}];
|
||||
|
||||
|
||||
if (matchingSendInProgressOrInConflictMessage) {
|
||||
matchingSendInProgressOrInConflictMessage.date = [self parseRFC3339Date:[(NSDictionary *)objMessage objectForKey:@"created_at"]];
|
||||
matchingSendInProgressOrInConflictMessage.id = messageID;
|
||||
matchingSendInProgressOrInConflictMessage.status = BITFeedbackMessageStatusRead;
|
||||
NSArray *feedbackAttachments =[(NSDictionary *)objMessage objectForKey:@"attachments"];
|
||||
if (matchingSendInProgressOrInConflictMessage.attachments.count == feedbackAttachments.count) {
|
||||
int attachmentIndex = 0;
|
||||
for (BITFeedbackMessageAttachment* attachment in matchingSendInProgressOrInConflictMessage.attachments){
|
||||
attachment.id =feedbackAttachments[attachmentIndex][@"id"];
|
||||
attachmentIndex++;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ([(NSDictionary *)objMessage objectForKey:@"clean_text"] || [(NSDictionary *)objMessage objectForKey:@"text"]) {
|
||||
if ([(NSDictionary *)objMessage objectForKey:@"clean_text"] || [(NSDictionary *)objMessage objectForKey:@"text"] || [(NSDictionary *)objMessage objectForKey:@"attachments"]) {
|
||||
BITFeedbackMessage *message = [[BITFeedbackMessage alloc] init];
|
||||
message.text = [(NSDictionary *)objMessage objectForKey:@"clean_text"] ?: [(NSDictionary *)objMessage objectForKey:@"text"] ?: @"";
|
||||
message.name = [(NSDictionary *)objMessage objectForKey:@"name"] ?: @"";
|
||||
@ -708,6 +740,15 @@
|
||||
message.id = [(NSDictionary *)objMessage objectForKey:@"id"];
|
||||
message.status = BITFeedbackMessageStatusUnread;
|
||||
|
||||
for (NSDictionary *attachmentData in objMessage[@"attachments"]) {
|
||||
BITFeedbackMessageAttachment *newAttachment = [BITFeedbackMessageAttachment new];
|
||||
newAttachment.originalFilename = attachmentData[@"file_name"];
|
||||
newAttachment.id = attachmentData[@"id"];
|
||||
newAttachment.sourceURL = attachmentData[@"url"];
|
||||
newAttachment.contentType = attachmentData[@"content_type"];
|
||||
[message addAttachmentsObject:newAttachment];
|
||||
}
|
||||
|
||||
[_feedbackList addObject:message];
|
||||
|
||||
newMessage = YES;
|
||||
@ -724,7 +765,7 @@
|
||||
|
||||
[self sortFeedbackList];
|
||||
[self updateLastMessageID];
|
||||
|
||||
|
||||
// we got a new incoming message, trigger user notification system
|
||||
if (newMessage) {
|
||||
// check if the latest message is from the users own email address, then don't show an alert since he answered using his own email
|
||||
@ -733,12 +774,12 @@
|
||||
BITFeedbackMessage *latestMessage = [self lastMessageHavingID];
|
||||
if (self.userEmail && latestMessage.email && [self.userEmail compare:latestMessage.email] == NSOrderedSame)
|
||||
latestMessageFromUser = YES;
|
||||
|
||||
|
||||
if (!latestMessageFromUser) {
|
||||
if([self.delegate respondsToSelector:@selector(feedbackManagerDidReceiveNewFeedback:)]) {
|
||||
[self.delegate feedbackManagerDidReceiveNewFeedback:self];
|
||||
}
|
||||
|
||||
|
||||
if(self.showAlertOnIncomingMessages && !self.currentFeedbackListViewController && !self.currentFeedbackComposeViewController) {
|
||||
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:BITHockeyLocalizedString(@"HockeyFeedbackNewMessageTitle")
|
||||
message:BITHockeyLocalizedString(@"HockeyFeedbackNewMessageText")
|
||||
@ -754,7 +795,7 @@
|
||||
}
|
||||
|
||||
NSInteger pendingMessagesCountAfterProcessing = [[self messagesWithStatus:BITFeedbackMessageStatusSendPending] count];
|
||||
|
||||
|
||||
// check if this request was successful and we have more messages pending and continue if positive
|
||||
if (pendingMessagesCount > pendingMessagesCountAfterProcessing && pendingMessagesCountAfterProcessing > 0) {
|
||||
[self performSelector:@selector(submitPendingMessages) withObject:nil afterDelay:0.1];
|
||||
@ -765,17 +806,18 @@
|
||||
}
|
||||
|
||||
[self saveMessages];
|
||||
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
- (void)sendNetworkRequestWithHTTPMethod:(NSString *)httpMethod withMessage:(BITFeedbackMessage *)message completionHandler:(void (^)(NSError *err))completionHandler {
|
||||
NSString *boundary = @"----FOO";
|
||||
|
||||
_networkRequestInProgress = YES;
|
||||
// inform the UI to update its data in case the list is already showing
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:BITHockeyFeedbackMessagesLoadingStarted object:nil];
|
||||
|
||||
|
||||
NSString *tokenParameter = @"";
|
||||
if ([self token]) {
|
||||
tokenParameter = [NSString stringWithFormat:@"/%@", [self token]];
|
||||
@ -835,8 +877,26 @@
|
||||
[postBody appendData:[BITHockeyAppClient dataWithPostValue:self.userEmail forKey:@"email" boundary:boundary]];
|
||||
}
|
||||
|
||||
|
||||
NSInteger photoIndex = 0;
|
||||
|
||||
for (BITFeedbackMessageAttachment *attachment in message.attachments){
|
||||
NSString *key = [NSString stringWithFormat:@"attachment%ld", (long)photoIndex];
|
||||
|
||||
NSString *filename = attachment.originalFilename;
|
||||
|
||||
if (!filename) {
|
||||
filename = [NSString stringWithFormat:@"Attachment %ld", (long)photoIndex];
|
||||
}
|
||||
|
||||
[postBody appendData:[BITHockeyAppClient dataWithPostValue:attachment.data forKey:key contentType:attachment.contentType boundary:boundary filename:filename]];
|
||||
|
||||
photoIndex++;
|
||||
}
|
||||
|
||||
[postBody appendData:[[NSString stringWithFormat:@"--%@--\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
|
||||
|
||||
|
||||
[request setHTTPBody:postBody];
|
||||
}
|
||||
|
||||
@ -858,14 +918,14 @@
|
||||
if (!self.token) {
|
||||
// set the token to the first message token, since this is identical
|
||||
__block NSString *token = nil;
|
||||
|
||||
|
||||
[_feedbackList enumerateObjectsUsingBlock:^(id objMessage, NSUInteger messagesIdx, BOOL *stop) {
|
||||
if ([(BITFeedbackMessage *)objMessage status] == BITFeedbackMessageStatusSendInProgress) {
|
||||
token = [(BITFeedbackMessage *)objMessage token];
|
||||
*stop = YES;
|
||||
}
|
||||
}];
|
||||
|
||||
|
||||
if (token) {
|
||||
self.token = token;
|
||||
}
|
||||
@ -938,7 +998,7 @@
|
||||
[self saveMessages];
|
||||
|
||||
NSArray *pendingMessages = [self messagesWithStatus:BITFeedbackMessageStatusSendPending];
|
||||
|
||||
|
||||
if ([pendingMessages count] > 0) {
|
||||
// we send one message at a time
|
||||
BITFeedbackMessage *messageToSend = [pendingMessages objectAtIndex:0];
|
||||
@ -961,7 +1021,6 @@
|
||||
completionHandler:^(NSError *err){
|
||||
if (err) {
|
||||
[self markSendInProgressMessagesAsPending];
|
||||
|
||||
[self saveMessages];
|
||||
}
|
||||
|
||||
@ -971,11 +1030,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
- (void)submitMessageWithText:(NSString *)text {
|
||||
- (void)submitMessageWithText:(NSString *)text andAttachments:(NSArray *)attachments {
|
||||
BITFeedbackMessage *message = [[BITFeedbackMessage alloc] init];
|
||||
message.text = text;
|
||||
[message setStatus:BITFeedbackMessageStatusSendPending];
|
||||
[message setToken:[self uuidAsLowerCaseAndShortened]];
|
||||
[message setToken:[self uuidAsLowerCaseAndShortened]];
|
||||
[message setAttachments:attachments];
|
||||
[message setUserMessage:YES];
|
||||
|
||||
[_feedbackList addObject:message];
|
||||
@ -996,6 +1056,88 @@
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Observation Handling
|
||||
|
||||
- (void)setFeedbackObservationMode:(BITFeedbackObservationMode)feedbackObservationMode {
|
||||
if (feedbackObservationMode != _feedbackObservationMode) {
|
||||
_feedbackObservationMode = feedbackObservationMode;
|
||||
|
||||
if (feedbackObservationMode == BITFeedbackObservationModeOnScreenshot){
|
||||
if (NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_6_1){
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(screenshotNotificationReceived:) name:UIApplicationUserDidTakeScreenshotNotification object:nil];
|
||||
} else {
|
||||
BITHockeyLog("WARNING: BITFeedbackObservationModeOnScreenshot requires iOS 7 or later.");
|
||||
}
|
||||
|
||||
self.screenshotNotificationEnabled = YES;
|
||||
|
||||
if (self.tapRecognizer){
|
||||
[[[UIApplication sharedApplication] keyWindow] removeGestureRecognizer:self.tapRecognizer];
|
||||
self.tapRecognizer = nil;
|
||||
}
|
||||
}
|
||||
|
||||
if (feedbackObservationMode == BITFeedbackObservationModeThreeFingerTap){
|
||||
if (!self.tapRecognizer){
|
||||
self.tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(screenshotTripleTap:)];
|
||||
self.tapRecognizer.numberOfTouchesRequired = 3;
|
||||
self.tapRecognizer.delegate = self;
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[[UIApplication sharedApplication].keyWindow addGestureRecognizer:self.tapRecognizer];
|
||||
});
|
||||
}
|
||||
|
||||
if (self.screenshotNotificationEnabled){
|
||||
if (NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_6_1){
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationUserDidTakeScreenshotNotification object:nil];
|
||||
self.screenshotNotificationEnabled = NO;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-(void)screenshotNotificationReceived:(NSNotification *)notification {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self extractLastPictureFromLibraryAndLaunchFeedback];
|
||||
});
|
||||
}
|
||||
|
||||
-(void)extractLastPictureFromLibraryAndLaunchFeedback {
|
||||
ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];
|
||||
|
||||
[library enumerateGroupsWithTypes:ALAssetsGroupSavedPhotos usingBlock:^(ALAssetsGroup *group, BOOL *stop) {
|
||||
|
||||
[group setAssetsFilter:[ALAssetsFilter allPhotos]];
|
||||
|
||||
[group enumerateAssetsWithOptions:NSEnumerationReverse usingBlock:^(ALAsset *alAsset, NSUInteger index, BOOL *innerStop) {
|
||||
|
||||
if (alAsset) {
|
||||
ALAssetRepresentation *representation = [alAsset defaultRepresentation];
|
||||
UIImage *latestPhoto = [UIImage imageWithCGImage:[representation fullScreenImage]];
|
||||
|
||||
*stop = YES;
|
||||
*innerStop = YES;
|
||||
|
||||
[self showFeedbackComposeViewWithPreparedItems:@[latestPhoto]];
|
||||
}
|
||||
}];
|
||||
} failureBlock: nil];
|
||||
}
|
||||
|
||||
- (void)screenshotTripleTap:(UITapGestureRecognizer *)tapRecognizer {
|
||||
if (tapRecognizer.state == UIGestureRecognizerStateRecognized){
|
||||
[self showFeedbackComposeViewWithGeneratedScreenshot];
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
|
||||
return YES;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
|
||||
#endif /* HOCKEYSDK_FEATURE_FEEDBACK */
|
||||
|
@ -70,7 +70,7 @@
|
||||
- (NSUInteger)numberOfMessages;
|
||||
- (BITFeedbackMessage *)messageAtIndex:(NSUInteger)index;
|
||||
|
||||
- (void)submitMessageWithText:(NSString *)text;
|
||||
- (void)submitMessageWithText:(NSString *)text andAttachments:(NSArray *)photos;
|
||||
- (void)submitPendingMessages;
|
||||
|
||||
// Returns YES if manual user data can be entered, required or optional
|
||||
|
@ -29,6 +29,8 @@
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@class BITFeedbackMessageAttachment;
|
||||
|
||||
/**
|
||||
* Status for each feedback message
|
||||
*/
|
||||
@ -69,7 +71,29 @@ typedef NS_ENUM(NSInteger, BITFeedbackMessageStatus) {
|
||||
@property (nonatomic, copy) NSDate *date;
|
||||
@property (nonatomic, copy) NSNumber *id;
|
||||
@property (nonatomic, copy) NSString *token;
|
||||
@property (nonatomic, strong) NSArray *attachments;
|
||||
@property (nonatomic) BITFeedbackMessageStatus status;
|
||||
@property (nonatomic) BOOL userMessage;
|
||||
|
||||
/**
|
||||
Delete local cached attachment data
|
||||
|
||||
@warning This method must be called before a feedback message is deleted.
|
||||
*/
|
||||
- (void)deleteContents;
|
||||
|
||||
/**
|
||||
Add an attachment to a message
|
||||
|
||||
@param object BITFeedbackMessageAttachment instance representing the attachment that should be added
|
||||
*/
|
||||
-(void)addAttachmentsObject:(BITFeedbackMessageAttachment *)object;
|
||||
|
||||
/**
|
||||
Return the attachments that can be viewed
|
||||
|
||||
@return NSArray containing the attachment objects that can be previewed
|
||||
*/
|
||||
- (NSArray *)previewableAttachments;
|
||||
|
||||
@end
|
||||
|
@ -28,13 +28,14 @@
|
||||
|
||||
|
||||
#import "BITFeedbackMessage.h"
|
||||
#import "BITFeedbackMessageAttachment.h"
|
||||
|
||||
@implementation BITFeedbackMessage
|
||||
|
||||
|
||||
#pragma mark - NSObject
|
||||
|
||||
- (id) init {
|
||||
- (instancetype) init {
|
||||
if ((self = [super init])) {
|
||||
_text = nil;
|
||||
_userID = nil;
|
||||
@ -42,6 +43,7 @@
|
||||
_email = nil;
|
||||
_date = [[NSDate alloc] init];
|
||||
_token = nil;
|
||||
_attachments = nil;
|
||||
_id = [[NSNumber alloc] initWithInteger:0];
|
||||
_status = BITFeedbackMessageStatusSendPending;
|
||||
_userMessage = NO;
|
||||
@ -59,19 +61,21 @@
|
||||
[encoder encodeObject:self.email forKey:@"email"];
|
||||
[encoder encodeObject:self.date forKey:@"date"];
|
||||
[encoder encodeObject:self.id forKey:@"id"];
|
||||
[encoder encodeObject:self.attachments forKey:@"attachments"];
|
||||
[encoder encodeInteger:self.status forKey:@"status"];
|
||||
[encoder encodeBool:self.userMessage forKey:@"userMessage"];
|
||||
[encoder encodeObject:self.token forKey:@"token"];
|
||||
}
|
||||
|
||||
- (id)initWithCoder:(NSCoder *)decoder {
|
||||
if ((self = [super init])) {
|
||||
- (instancetype)initWithCoder:(NSCoder *)decoder {
|
||||
if ((self = [self init])) {
|
||||
self.text = [decoder decodeObjectForKey:@"text"];
|
||||
self.userID = [decoder decodeObjectForKey:@"userID"];
|
||||
self.name = [decoder decodeObjectForKey:@"name"];
|
||||
self.email = [decoder decodeObjectForKey:@"email"];
|
||||
self.date = [decoder decodeObjectForKey:@"date"];
|
||||
self.id = [decoder decodeObjectForKey:@"id"];
|
||||
self.attachments = [decoder decodeObjectForKey:@"attachments"];
|
||||
self.status = (BITFeedbackMessageStatus)[decoder decodeIntegerForKey:@"status"];
|
||||
self.userMessage = [decoder decodeBoolForKey:@"userMessage"];
|
||||
self.token = [decoder decodeObjectForKey:@"token"];
|
||||
@ -79,4 +83,32 @@
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - Deletion
|
||||
|
||||
-(void)deleteContents {
|
||||
for (BITFeedbackMessageAttachment *attachment in self.attachments){
|
||||
[attachment deleteContents];
|
||||
}
|
||||
}
|
||||
|
||||
- (NSArray *)previewableAttachments {
|
||||
NSMutableArray *returnArray = [NSMutableArray new];
|
||||
|
||||
for (BITFeedbackMessageAttachment *attachment in self.attachments){
|
||||
if ([QLPreviewController canPreviewItem:attachment ]){
|
||||
[returnArray addObject:attachment];
|
||||
}
|
||||
}
|
||||
|
||||
return returnArray;
|
||||
}
|
||||
|
||||
-(void)addAttachmentsObject:(BITFeedbackMessageAttachment *)object{
|
||||
if (!self.attachments){
|
||||
self.attachments = [NSArray array];
|
||||
}
|
||||
self.attachments = [self.attachments arrayByAddingObject:object];
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
|
65
Classes/BITFeedbackMessageAttachment.h
Normal file
@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Author: Moritz Haarmann <post@moritzhaarmann.de>
|
||||
*
|
||||
* Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person
|
||||
* obtaining a copy of this software and associated documentation
|
||||
* files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following
|
||||
* conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <QuickLook/QuickLook.h>
|
||||
|
||||
@interface BITFeedbackMessageAttachment : NSObject<NSCoding, QLPreviewItem>
|
||||
|
||||
@property (nonatomic, copy) NSNumber *id;
|
||||
@property (nonatomic, copy) NSString *originalFilename;
|
||||
@property (nonatomic, copy) NSString *contentType;
|
||||
@property (nonatomic, copy) NSString *sourceURL;
|
||||
@property (nonatomic) BOOL isLoading;
|
||||
@property (nonatomic, readonly) NSData *data;
|
||||
|
||||
|
||||
@property (readonly) UIImage *imageRepresentation;
|
||||
|
||||
|
||||
+ (BITFeedbackMessageAttachment *)attachmentWithData:(NSData *)data contentType:(NSString *)contentType;
|
||||
|
||||
- (UIImage *)thumbnailWithSize:(CGSize)size;
|
||||
|
||||
- (void)replaceData:(NSData *)data;
|
||||
|
||||
- (void)deleteContents;
|
||||
|
||||
- (BOOL)needsLoadingFromURL;
|
||||
|
||||
- (BOOL)isImage;
|
||||
|
||||
- (NSURL *)localURL;
|
||||
|
||||
/**
|
||||
Used to determine whether QuickLook can preview this file or not. If not, we don't download it.
|
||||
*/
|
||||
- (NSString*)possibleFilename;
|
||||
|
||||
@end
|
258
Classes/BITFeedbackMessageAttachment.m
Normal file
@ -0,0 +1,258 @@
|
||||
/*
|
||||
* Author: Moritz Haarmann <post@moritzhaarmann.de>
|
||||
*
|
||||
* Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person
|
||||
* obtaining a copy of this software and associated documentation
|
||||
* files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following
|
||||
* conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
|
||||
#import "BITFeedbackMessageAttachment.h"
|
||||
#import "BITHockeyHelper.h"
|
||||
#import "HockeySDKPrivate.h"
|
||||
#import <MobileCoreServices/MobileCoreServices.h>
|
||||
|
||||
#define kCacheFolderName @"attachments"
|
||||
|
||||
@interface BITFeedbackMessageAttachment()
|
||||
|
||||
@property (nonatomic, strong) NSMutableDictionary *thumbnailRepresentations;
|
||||
@property (nonatomic, strong) NSData *internalData;
|
||||
@property (nonatomic, copy) NSString *filename;
|
||||
|
||||
|
||||
@end
|
||||
|
||||
@implementation BITFeedbackMessageAttachment {
|
||||
NSString *_tempFilename;
|
||||
|
||||
NSString *_cachePath;
|
||||
|
||||
NSFileManager *_fm;
|
||||
}
|
||||
|
||||
|
||||
+ (BITFeedbackMessageAttachment *)attachmentWithData:(NSData *)data contentType:(NSString *)contentType {
|
||||
|
||||
static NSDateFormatter *formatter;
|
||||
|
||||
if(!formatter) {
|
||||
formatter = [NSDateFormatter new];
|
||||
formatter.dateStyle = NSDateFormatterShortStyle;
|
||||
formatter.timeStyle = NSDateFormatterShortStyle;
|
||||
}
|
||||
|
||||
BITFeedbackMessageAttachment *newAttachment = [BITFeedbackMessageAttachment new];
|
||||
newAttachment.contentType = contentType;
|
||||
newAttachment.data = data;
|
||||
newAttachment.originalFilename = [NSString stringWithFormat:@"Attachment: %@", [formatter stringFromDate:[NSDate date]]];
|
||||
|
||||
return newAttachment;
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
if ((self = [super init])) {
|
||||
self.isLoading = NO;
|
||||
self.thumbnailRepresentations = [NSMutableDictionary new];
|
||||
|
||||
_fm = [[NSFileManager alloc] init];
|
||||
_cachePath = [bit_settingsDir() stringByAppendingPathComponent:kCacheFolderName];
|
||||
|
||||
BOOL isDirectory;
|
||||
|
||||
if (![_fm fileExistsAtPath:_cachePath isDirectory:&isDirectory]){
|
||||
[_fm createDirectoryAtPath:_cachePath withIntermediateDirectories:YES attributes:nil error:nil];
|
||||
}
|
||||
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setData:(NSData *)data {
|
||||
self->_internalData = data;
|
||||
self.filename = [self possibleFilename];
|
||||
[self->_internalData writeToFile:self.filename atomically:NO];
|
||||
}
|
||||
|
||||
- (NSData *)data {
|
||||
if (!self->_internalData && self.filename) {
|
||||
self.internalData = [NSData dataWithContentsOfFile:self.filename];
|
||||
}
|
||||
|
||||
if (self.internalData) {
|
||||
return self.internalData;
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (void)replaceData:(NSData *)data {
|
||||
self.data = data;
|
||||
self.thumbnailRepresentations = [NSMutableDictionary new];
|
||||
}
|
||||
|
||||
- (BOOL)needsLoadingFromURL {
|
||||
return (self.sourceURL && ![_fm fileExistsAtPath:[self.localURL path]]);
|
||||
}
|
||||
|
||||
- (BOOL)isImage {
|
||||
return ([self.contentType rangeOfString:@"image"].location != NSNotFound);
|
||||
}
|
||||
|
||||
- (NSURL *)localURL {
|
||||
if (self.filename && [_fm fileExistsAtPath:self.filename]) {
|
||||
return [NSURL fileURLWithPath:self.filename];
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark NSCoding
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)aCoder {
|
||||
[aCoder encodeObject:self.contentType forKey:@"contentType"];
|
||||
[aCoder encodeObject:self.filename forKey:@"filename"];
|
||||
[aCoder encodeObject:self.originalFilename forKey:@"originalFilename"];
|
||||
[aCoder encodeObject:self.sourceURL forKey:@"url"];
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
|
||||
if ((self = [self init])) {
|
||||
self.contentType = [aDecoder decodeObjectForKey:@"contentType"];
|
||||
self.filename = [aDecoder decodeObjectForKey:@"filename"];
|
||||
self.thumbnailRepresentations = [NSMutableDictionary new];
|
||||
self.originalFilename = [aDecoder decodeObjectForKey:@"originalFilename"];
|
||||
self.sourceURL = [aDecoder decodeObjectForKey:@"url"];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Thubmnails / Image Representation
|
||||
|
||||
- (UIImage *)imageRepresentation {
|
||||
if ([self.contentType rangeOfString:@"image"].location != NSNotFound && self.filename ) {
|
||||
return [UIImage imageWithData:self.data];
|
||||
} else {
|
||||
// Create a Icon ..
|
||||
UIDocumentInteractionController* docController = [[UIDocumentInteractionController alloc] init];
|
||||
docController.name = self.originalFilename;
|
||||
NSArray* icons = docController.icons;
|
||||
if (icons.count){
|
||||
return icons[0];
|
||||
} else {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (UIImage *)thumbnailWithSize:(CGSize)size {
|
||||
id<NSCopying> cacheKey = [NSValue valueWithCGSize:size];
|
||||
|
||||
if (!self.thumbnailRepresentations[cacheKey]) {
|
||||
UIImage *image = self.imageRepresentation;
|
||||
// consider the scale.
|
||||
if (!image) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
CGFloat scale = [UIScreen mainScreen].scale;
|
||||
|
||||
if (scale != image.scale) {
|
||||
|
||||
CGSize scaledSize = CGSizeApplyAffineTransform(size, CGAffineTransformMakeScale(scale, scale));
|
||||
UIImage *thumbnail = bit_imageToFitSize(image, scaledSize, YES) ;
|
||||
|
||||
UIImage *scaledTumbnail = [UIImage imageWithCGImage:thumbnail.CGImage scale:scale orientation:thumbnail.imageOrientation];
|
||||
if (thumbnail) {
|
||||
[self.thumbnailRepresentations setObject:scaledTumbnail forKey:cacheKey];
|
||||
}
|
||||
|
||||
} else {
|
||||
UIImage *thumbnail = bit_imageToFitSize(image, size, YES) ;
|
||||
|
||||
[self.thumbnailRepresentations setObject:thumbnail forKey:cacheKey];
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return self.thumbnailRepresentations[cacheKey];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Persistence Helpers
|
||||
|
||||
- (NSString *)possibleFilename {
|
||||
if (_tempFilename) {
|
||||
return _tempFilename;
|
||||
}
|
||||
|
||||
NSString *uniqueString = bit_UUID();
|
||||
_tempFilename = [_cachePath stringByAppendingPathComponent:uniqueString];
|
||||
|
||||
// File extension that suits the Content type.
|
||||
|
||||
CFStringRef mimeType = (__bridge CFStringRef)self.contentType;
|
||||
CFStringRef uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, mimeType, NULL);
|
||||
CFStringRef extension = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassFilenameExtension);
|
||||
if (extension) {
|
||||
_tempFilename = [_tempFilename stringByAppendingPathExtension:(__bridge NSString *)(extension)];
|
||||
CFRelease(extension);
|
||||
}
|
||||
|
||||
CFRelease(uti);
|
||||
|
||||
return _tempFilename;
|
||||
}
|
||||
|
||||
- (void)deleteContents {
|
||||
if (self.filename) {
|
||||
[_fm removeItemAtPath:self.filename error:nil];
|
||||
self.filename = nil;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - QLPreviewItem
|
||||
|
||||
- (NSString *)previewItemTitle {
|
||||
return self.originalFilename;
|
||||
}
|
||||
|
||||
- (NSURL *)previewItemURL {
|
||||
if (self.localURL){
|
||||
return self.localURL;
|
||||
} else if (self.sourceURL) {
|
||||
NSString *filename = self.possibleFilename;
|
||||
if (filename) {
|
||||
return [NSURL fileURLWithPath:filename];
|
||||
}
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
@end
|
@ -118,7 +118,7 @@
|
||||
|
||||
#pragma mark - Helpers
|
||||
/**
|
||||
* create a post body from the given value, key and boundary. This is a convenience call to
|
||||
* create a post body from the given value, key and boundary. This is a convenience call to
|
||||
* dataWithPostValue:forKey:contentType:boundary and aimed at NSString-content.
|
||||
*
|
||||
* @param value -
|
||||
|
@ -93,7 +93,7 @@
|
||||
|
||||
[postBody appendData:[[NSString stringWithFormat:@"--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
|
||||
|
||||
// There's certainly a better way to check if we are supposed to send binary data here.
|
||||
// There's certainly a better way to check if we are supposed to send binary data here.
|
||||
if (filename){
|
||||
[postBody appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"; filename=\"%@\"\r\n", key, filename] dataUsingEncoding:NSUTF8StringEncoding]];
|
||||
[postBody appendData:[[NSString stringWithFormat:@"Content-Type: %@\r\n", contentType] dataUsingEncoding:NSUTF8StringEncoding]];
|
||||
|
@ -346,6 +346,11 @@ UIImage *bit_addGlossToImage(UIImage *inputImage) {
|
||||
#pragma mark UIImage helpers
|
||||
|
||||
UIImage *bit_imageToFitSize(UIImage *inputImage, CGSize fitSize, BOOL honorScaleFactor) {
|
||||
|
||||
if (!inputImage){
|
||||
return nil;
|
||||
}
|
||||
|
||||
float imageScaleFactor = 1.0;
|
||||
if (honorScaleFactor) {
|
||||
if ([inputImage respondsToSelector:@selector(scale)]) {
|
||||
|
44
Classes/BITImageAnnotation.h
Normal file
@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Author: Moritz Haarmann <post@moritzhaarmann.de>
|
||||
*
|
||||
* Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person
|
||||
* obtaining a copy of this software and associated documentation
|
||||
* files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following
|
||||
* conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface BITImageAnnotation : UIView {
|
||||
BOOL _selected;
|
||||
}
|
||||
|
||||
@property (nonatomic) CGSize movedDelta;
|
||||
@property (nonatomic, weak) UIImage *sourceImage;
|
||||
@property (nonatomic) CGRect imageFrame;
|
||||
|
||||
-(BOOL)resizable;
|
||||
|
||||
- (void)setSelected:(BOOL)selected;
|
||||
- (BOOL)isSelected;
|
||||
|
||||
@end
|
56
Classes/BITImageAnnotation.m
Normal file
@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Author: Moritz Haarmann <post@moritzhaarmann.de>
|
||||
*
|
||||
* Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person
|
||||
* obtaining a copy of this software and associated documentation
|
||||
* files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following
|
||||
* conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#import "BITImageAnnotation.h"
|
||||
|
||||
@implementation BITImageAnnotation
|
||||
|
||||
- (id)initWithFrame:(CGRect)frame
|
||||
{
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
// Initialization code
|
||||
//self.backgroundColor = [UIColor redColor];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
-(BOOL)resizable {
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void)setSelected:(BOOL)selected {
|
||||
self->_selected = selected;
|
||||
}
|
||||
|
||||
- (BOOL)isSelected {
|
||||
return self->_selected;
|
||||
}
|
||||
|
||||
@end
|
45
Classes/BITImageAnnotationViewController.h
Normal file
@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Author: Moritz Haarmann <post@moritzhaarmann.de>
|
||||
*
|
||||
* Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person
|
||||
* obtaining a copy of this software and associated documentation
|
||||
* files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following
|
||||
* conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@class BITImageAnnotationViewController;
|
||||
|
||||
@protocol BITImageAnnotationDelegate <NSObject>
|
||||
|
||||
- (void)annotationControllerDidCancel:(BITImageAnnotationViewController *)annotationController;
|
||||
- (void)annotationController:(BITImageAnnotationViewController *)annotationController didFinishWithImage:(UIImage *)image;
|
||||
|
||||
@end
|
||||
|
||||
@interface BITImageAnnotationViewController : UIViewController
|
||||
|
||||
@property (nonatomic, strong) UIImage *image;
|
||||
@property (nonatomic, weak) id<BITImageAnnotationDelegate> delegate;
|
||||
|
||||
@end
|
404
Classes/BITImageAnnotationViewController.m
Normal file
@ -0,0 +1,404 @@
|
||||
/*
|
||||
* Author: Moritz Haarmann <post@moritzhaarmann.de>
|
||||
*
|
||||
* Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person
|
||||
* obtaining a copy of this software and associated documentation
|
||||
* files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following
|
||||
* conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#import "BITImageAnnotationViewController.h"
|
||||
#import "BITImageAnnotation.h"
|
||||
#import "BITRectangleImageAnnotation.h"
|
||||
#import "BITArrowImageAnnotation.h"
|
||||
#import "BITBlurImageAnnotation.h"
|
||||
#import "BITHockeyHelper.h"
|
||||
#import "HockeySDKPrivate.h"
|
||||
|
||||
typedef NS_ENUM(NSInteger, BITImageAnnotationViewControllerInteractionMode) {
|
||||
BITImageAnnotationViewControllerInteractionModeNone,
|
||||
BITImageAnnotationViewControllerInteractionModeDraw,
|
||||
BITImageAnnotationViewControllerInteractionModeMove
|
||||
};
|
||||
|
||||
@interface BITImageAnnotationViewController ()
|
||||
|
||||
@property (nonatomic, strong) UIImageView *imageView;
|
||||
@property (nonatomic, strong) UISegmentedControl *editingControls;
|
||||
@property (nonatomic, strong) NSMutableArray *objects;
|
||||
|
||||
@property (nonatomic, strong) UITapGestureRecognizer *tapRecognizer;
|
||||
@property (nonatomic, strong) UIPanGestureRecognizer *panRecognizer;
|
||||
@property (nonatomic, strong) UIPinchGestureRecognizer *pinchRecognizer;
|
||||
|
||||
@property (nonatomic) CGFloat scaleFactor;
|
||||
|
||||
@property (nonatomic) CGPoint panStart;
|
||||
@property (nonatomic,strong) BITImageAnnotation *currentAnnotation;
|
||||
|
||||
@property (nonatomic) BITImageAnnotationViewControllerInteractionMode currentInteraction;
|
||||
|
||||
@property (nonatomic) CGRect pinchStartingFrame;
|
||||
|
||||
@end
|
||||
|
||||
@implementation BITImageAnnotationViewController
|
||||
|
||||
#pragma mark - UIViewController
|
||||
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
[super viewDidLoad];
|
||||
|
||||
self.view.backgroundColor = [UIColor groupTableViewBackgroundColor];
|
||||
|
||||
NSArray *icons = @[@"Arrow.png",@"Rectangle.png", @"Blur.png"];
|
||||
|
||||
self.editingControls = [[UISegmentedControl alloc] initWithItems:@[@"Rectangle", @"Arrow", @"Blur"]];
|
||||
int i=0;
|
||||
for (NSString *imageName in icons){
|
||||
[self.editingControls setImage:bit_imageNamed(imageName, BITHOCKEYSDK_BUNDLE) forSegmentAtIndex:i++];
|
||||
}
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
[self.editingControls setSegmentedControlStyle:UISegmentedControlStyleBar];
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
self.navigationItem.titleView = self.editingControls;
|
||||
|
||||
self.objects = [NSMutableArray new];
|
||||
|
||||
[self.editingControls addTarget:self action:@selector(editingAction:) forControlEvents:UIControlEventTouchUpInside];
|
||||
[self.editingControls setSelectedSegmentIndex:0];
|
||||
|
||||
self.imageView = [[UIImageView alloc] initWithFrame:self.view.bounds];
|
||||
|
||||
self.imageView.clipsToBounds = YES;
|
||||
|
||||
self.imageView.image = self.image;
|
||||
self.imageView.contentMode = UIViewContentModeScaleToFill;
|
||||
|
||||
self.view.frame = UIScreen.mainScreen.applicationFrame;
|
||||
|
||||
[self.view addSubview:self.imageView];
|
||||
// Erm.
|
||||
self.imageView.frame = [UIScreen mainScreen].bounds;
|
||||
|
||||
self.panRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panned:)];
|
||||
self.pinchRecognizer = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(pinched:)];
|
||||
self.tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapped:)];
|
||||
|
||||
[self.imageView addGestureRecognizer:self.pinchRecognizer];
|
||||
[self.imageView addGestureRecognizer:self.panRecognizer];
|
||||
[self.view addGestureRecognizer:self.tapRecognizer];
|
||||
|
||||
self.imageView.userInteractionEnabled = YES;
|
||||
|
||||
self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc ] initWithImage:bit_imageNamed(@"Cancel.png", BITHOCKEYSDK_BUNDLE) landscapeImagePhone:bit_imageNamed(@"Cancel.png", BITHOCKEYSDK_BUNDLE) style:UIBarButtonItemStyleBordered target:self action:@selector(discard:)];
|
||||
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc ] initWithImage:bit_imageNamed(@"Ok.png", BITHOCKEYSDK_BUNDLE) landscapeImagePhone:bit_imageNamed(@"Ok.png", BITHOCKEYSDK_BUNDLE) style:UIBarButtonItemStyleBordered target:self action:@selector(save:)];
|
||||
|
||||
self.view.autoresizesSubviews = NO;
|
||||
}
|
||||
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated {
|
||||
[super viewWillAppear:animated];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(orientationDidChange:) name:UIDeviceOrientationDidChangeNotification object:nil];
|
||||
|
||||
[self fitImageViewFrame];
|
||||
|
||||
}
|
||||
|
||||
- (void)viewWillDisappear:(BOOL)animated {
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIDeviceOrientationDidChangeNotification object:nil];
|
||||
|
||||
}
|
||||
|
||||
- (BOOL)prefersStatusBarHidden {
|
||||
return self.navigationController.navigationBarHidden || self.navigationController.navigationBar.alpha == 0.0f;
|
||||
}
|
||||
|
||||
- (void)orientationDidChange:(NSNotification *)notification {
|
||||
[self fitImageViewFrame];
|
||||
}
|
||||
|
||||
|
||||
- (void)fitImageViewFrame {
|
||||
|
||||
CGSize size = [UIScreen mainScreen].bounds.size;
|
||||
if (UIInterfaceOrientationIsLandscape([UIApplication sharedApplication].statusBarOrientation)){
|
||||
size = CGSizeMake(size.height, size.width);
|
||||
}
|
||||
|
||||
CGFloat heightScaleFactor = size.height / self.image.size.height;
|
||||
CGFloat widthScaleFactor = size.width / self.image.size.width;
|
||||
|
||||
CGFloat factor = MIN(heightScaleFactor, widthScaleFactor);
|
||||
self.scaleFactor = factor;
|
||||
CGSize scaledImageSize = CGSizeMake(self.image.size.width * factor, self.image.size.height * factor);
|
||||
|
||||
CGRect baseFrame = CGRectMake(self.view.frame.size.width/2 - scaledImageSize.width/2, self.view.frame.size.height/2 - scaledImageSize.height/2, scaledImageSize.width, scaledImageSize.height);
|
||||
|
||||
self.imageView.frame = baseFrame;
|
||||
}
|
||||
|
||||
-(void)editingAction:(id)sender {
|
||||
|
||||
}
|
||||
|
||||
- (BITImageAnnotation *)annotationForCurrentMode {
|
||||
if (self.editingControls.selectedSegmentIndex == 0){
|
||||
return [[BITArrowImageAnnotation alloc] initWithFrame:CGRectZero];
|
||||
} else if(self.editingControls.selectedSegmentIndex==1){
|
||||
return [[BITRectangleImageAnnotation alloc] initWithFrame:CGRectZero];
|
||||
} else {
|
||||
return [[BITBlurImageAnnotation alloc] initWithFrame:CGRectZero];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Actions
|
||||
|
||||
- (void)discard:(id)sender {
|
||||
[self.delegate annotationControllerDidCancel:self];
|
||||
[self dismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (void)save:(id)sender {
|
||||
UIImage *image = [self extractImage];
|
||||
[self.delegate annotationController:self didFinishWithImage:image];
|
||||
[self dismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (UIImage *)extractImage {
|
||||
UIGraphicsBeginImageContextWithOptions(self.image.size, YES, 0.0);
|
||||
CGContextRef ctx = UIGraphicsGetCurrentContext();
|
||||
[self.image drawInRect:CGRectMake(0, 0, self.image.size.width, self.image.size.height)];
|
||||
CGContextScaleCTM(ctx,1.0/self.scaleFactor,1.0f/self.scaleFactor);
|
||||
|
||||
// Drawing all the annotations onto the final image.
|
||||
for (BITImageAnnotation *annotation in self.objects){
|
||||
CGContextTranslateCTM(ctx, annotation.frame.origin.x, annotation.frame.origin.y);
|
||||
[annotation.layer renderInContext:ctx];
|
||||
CGContextTranslateCTM(ctx,-1 * annotation.frame.origin.x,-1 * annotation.frame.origin.y);
|
||||
}
|
||||
|
||||
UIImage *renderedImageOfMyself = UIGraphicsGetImageFromCurrentImageContext();
|
||||
UIGraphicsEndImageContext();
|
||||
return renderedImageOfMyself;
|
||||
}
|
||||
|
||||
#pragma mark - UIGestureRecognizers
|
||||
|
||||
- (void)panned:(UIPanGestureRecognizer *)gestureRecognizer {
|
||||
BITImageAnnotation *annotationAtLocation = (BITImageAnnotation *)[self.view hitTest:[gestureRecognizer locationInView:self.view] withEvent:nil];
|
||||
|
||||
if (![annotationAtLocation isKindOfClass:[BITImageAnnotation class]]){
|
||||
annotationAtLocation = nil;
|
||||
}
|
||||
|
||||
// determine the interaction mode if none is set so far.
|
||||
|
||||
if (self.currentInteraction == BITImageAnnotationViewControllerInteractionModeNone){
|
||||
if (annotationAtLocation){
|
||||
self.currentInteraction = BITImageAnnotationViewControllerInteractionModeMove;
|
||||
} else if ([self canDrawNewAnnotation]){
|
||||
self.currentInteraction = BITImageAnnotationViewControllerInteractionModeDraw;
|
||||
}
|
||||
}
|
||||
|
||||
if (self.currentInteraction == BITImageAnnotationViewControllerInteractionModeNone){
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (self.currentInteraction == BITImageAnnotationViewControllerInteractionModeDraw){
|
||||
if (gestureRecognizer.state == UIGestureRecognizerStateBegan){
|
||||
self.currentAnnotation = [self annotationForCurrentMode];
|
||||
[self.objects addObject:self.currentAnnotation];
|
||||
self.currentAnnotation.sourceImage = self.image;
|
||||
|
||||
if (self.imageView.subviews.count > 0 && [self.currentAnnotation isKindOfClass:[BITBlurImageAnnotation class]]){
|
||||
[self.imageView insertSubview:self.currentAnnotation belowSubview:[self firstAnnotationThatIsNotBlur]];
|
||||
} else {
|
||||
[self.imageView addSubview:self.currentAnnotation];
|
||||
}
|
||||
|
||||
self.panStart = [gestureRecognizer locationInView:self.imageView];
|
||||
|
||||
// [self.editingControls setSelectedSegmentIndex:UISegmentedControlNoSegment];
|
||||
|
||||
} else if (gestureRecognizer.state == UIGestureRecognizerStateChanged){
|
||||
CGPoint bla = [gestureRecognizer locationInView:self.imageView];
|
||||
self.currentAnnotation.frame = CGRectMake(self.panStart.x, self.panStart.y, bla.x - self.panStart.x, bla.y - self.panStart.y);
|
||||
self.currentAnnotation.movedDelta = CGSizeMake(bla.x - self.panStart.x, bla.y - self.panStart.y);
|
||||
self.currentAnnotation.imageFrame = [self.view convertRect:self.imageView.frame toView:self.currentAnnotation];
|
||||
[self.currentAnnotation setNeedsLayout];
|
||||
[self.currentAnnotation layoutIfNeeded];
|
||||
} else {
|
||||
[self.currentAnnotation setSelected:NO];
|
||||
self.currentAnnotation = nil;
|
||||
self.currentInteraction = BITImageAnnotationViewControllerInteractionModeNone;
|
||||
}
|
||||
} else if (self.currentInteraction == BITImageAnnotationViewControllerInteractionModeMove){
|
||||
if (gestureRecognizer.state == UIGestureRecognizerStateBegan){
|
||||
// find and possibly move an existing annotation.
|
||||
|
||||
|
||||
if ([self.objects indexOfObject:annotationAtLocation] != NSNotFound){
|
||||
self.currentAnnotation = annotationAtLocation;
|
||||
[annotationAtLocation setSelected:YES];
|
||||
}
|
||||
|
||||
|
||||
} else if (gestureRecognizer.state == UIGestureRecognizerStateChanged && self.currentAnnotation){
|
||||
CGPoint delta = [gestureRecognizer translationInView:self.view];
|
||||
|
||||
CGRect annotationFrame = self.currentAnnotation.frame;
|
||||
annotationFrame.origin.x += delta.x;
|
||||
annotationFrame.origin.y += delta.y;
|
||||
self.currentAnnotation.frame = annotationFrame;
|
||||
self.currentAnnotation.imageFrame = [self.view convertRect:self.imageView.frame toView:self.currentAnnotation];
|
||||
|
||||
[self.currentAnnotation setNeedsLayout];
|
||||
[self.currentAnnotation layoutIfNeeded];
|
||||
|
||||
[gestureRecognizer setTranslation:CGPointZero inView:self.view];
|
||||
|
||||
} else {
|
||||
[self.currentAnnotation setSelected:NO];
|
||||
self.currentAnnotation = nil;
|
||||
self.currentInteraction = BITImageAnnotationViewControllerInteractionModeNone;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-(void)pinched:(UIPinchGestureRecognizer *)gestureRecognizer {
|
||||
if (gestureRecognizer.state == UIGestureRecognizerStateBegan){
|
||||
// try to figure out which view we are talking about.
|
||||
BITImageAnnotation *candidate = nil;
|
||||
BOOL validView = YES;
|
||||
|
||||
for ( int i = 0; i<gestureRecognizer.numberOfTouches; i++){
|
||||
BITImageAnnotation *newCandidate = (BITImageAnnotation *)[self.view hitTest:[gestureRecognizer locationOfTouch:i inView:self.view] withEvent:nil];
|
||||
|
||||
if (![newCandidate isKindOfClass:[BITImageAnnotation class]]){
|
||||
newCandidate = nil;
|
||||
}
|
||||
|
||||
if (candidate == nil){
|
||||
candidate = newCandidate;
|
||||
} else if (candidate != newCandidate){
|
||||
validView = NO;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (validView && [candidate resizable]){
|
||||
self.currentAnnotation = candidate;
|
||||
self.pinchStartingFrame = self.currentAnnotation.frame;
|
||||
[self.currentAnnotation setSelected:YES];
|
||||
}
|
||||
|
||||
} else if (gestureRecognizer.state == UIGestureRecognizerStateChanged && self.currentAnnotation && gestureRecognizer.numberOfTouches>1){
|
||||
CGRect newFrame= (self.pinchStartingFrame);
|
||||
|
||||
// upper point?
|
||||
CGPoint point1 = [gestureRecognizer locationOfTouch:0 inView:self.view];
|
||||
CGPoint point2 = [gestureRecognizer locationOfTouch:1 inView:self.view];
|
||||
|
||||
|
||||
newFrame.origin.x = point1.x;
|
||||
newFrame.origin.y = point1.y;
|
||||
|
||||
newFrame.origin.x = (point1.x > point2.x) ? point2.x : point1.x;
|
||||
newFrame.origin.y = (point1.y > point2.y) ? point2.y : point1.y;
|
||||
|
||||
newFrame.size.width = (point1.x > point2.x) ? point1.x - point2.x : point2.x - point1.x;
|
||||
newFrame.size.height = (point1.y > point2.y) ? point1.y - point2.y : point2.y - point1.y;
|
||||
|
||||
|
||||
self.currentAnnotation.frame = newFrame;
|
||||
self.currentAnnotation.imageFrame = [self.view convertRect:self.imageView.frame toView:self.currentAnnotation];
|
||||
} else {
|
||||
[self.currentAnnotation setSelected:NO];
|
||||
self.currentAnnotation = nil;
|
||||
}
|
||||
}
|
||||
|
||||
-(void)tapped:(UIGestureRecognizer *)tapRecognizer {
|
||||
// This toggles the nav and status bar. Since iOS7 and pre-iOS7 behave weirdly different,
|
||||
// this might look rather hacky, but hiding the navbar under iOS6 leads to some ugly
|
||||
// animation effect which is avoided by simply hiding the navbar setting it's alpha to 0. // moritzh
|
||||
|
||||
if (self.navigationController.navigationBar.alpha == 0 || self.navigationController.navigationBarHidden ){
|
||||
|
||||
[UIView animateWithDuration:0.35f animations:^{
|
||||
|
||||
if (NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_6_1) {
|
||||
[self.navigationController setNavigationBarHidden:NO animated:NO];
|
||||
} else {
|
||||
self.navigationController.navigationBar.alpha = 1.0;
|
||||
}
|
||||
|
||||
[[UIApplication sharedApplication] setStatusBarHidden:NO];
|
||||
|
||||
} completion:^(BOOL finished) {
|
||||
[self fitImageViewFrame];
|
||||
|
||||
}];
|
||||
} else {
|
||||
[UIView animateWithDuration:0.35f animations:^{
|
||||
|
||||
if (NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_6_1) {
|
||||
[self.navigationController setNavigationBarHidden:YES animated:NO];
|
||||
} else {
|
||||
self.navigationController.navigationBar.alpha = 0.0;
|
||||
}
|
||||
|
||||
[[UIApplication sharedApplication] setStatusBarHidden:YES];
|
||||
|
||||
} completion:^(BOOL finished) {
|
||||
[self fitImageViewFrame];
|
||||
|
||||
}];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#pragma mark - Helpers
|
||||
|
||||
-(UIView *)firstAnnotationThatIsNotBlur {
|
||||
for (BITImageAnnotation *annotation in self.imageView.subviews){
|
||||
if (![annotation isKindOfClass:[BITBlurImageAnnotation class]]){
|
||||
return annotation;
|
||||
}
|
||||
}
|
||||
|
||||
return self.imageView;
|
||||
}
|
||||
|
||||
- (BOOL)canDrawNewAnnotation {
|
||||
return [self.editingControls selectedSegmentIndex] != UISegmentedControlNoSegment;
|
||||
}
|
||||
@end
|
33
Classes/BITRectangleImageAnnotation.h
Normal file
@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Author: Moritz Haarmann <post@moritzhaarmann.de>
|
||||
*
|
||||
* Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person
|
||||
* obtaining a copy of this software and associated documentation
|
||||
* files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following
|
||||
* conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#import "BITImageAnnotation.h"
|
||||
|
||||
@interface BITRectangleImageAnnotation : BITImageAnnotation
|
||||
|
||||
@end
|
87
Classes/BITRectangleImageAnnotation.m
Normal file
@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Author: Moritz Haarmann <post@moritzhaarmann.de>
|
||||
*
|
||||
* Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person
|
||||
* obtaining a copy of this software and associated documentation
|
||||
* files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following
|
||||
* conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#import "BITRectangleImageAnnotation.h"
|
||||
|
||||
@interface BITRectangleImageAnnotation()
|
||||
|
||||
@property (nonatomic, strong) CAShapeLayer *shapeLayer;
|
||||
@property (nonatomic, strong) CAShapeLayer *strokeLayer;
|
||||
|
||||
|
||||
@end
|
||||
|
||||
@implementation BITRectangleImageAnnotation
|
||||
|
||||
- (id)initWithFrame:(CGRect)frame
|
||||
{
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
self.shapeLayer = [CAShapeLayer layer];
|
||||
self.shapeLayer.strokeColor = [UIColor redColor].CGColor;
|
||||
self.shapeLayer.lineWidth = 5;
|
||||
self.shapeLayer.fillColor = [UIColor clearColor].CGColor;
|
||||
|
||||
self.strokeLayer = [CAShapeLayer layer];
|
||||
self.strokeLayer.strokeColor = [UIColor whiteColor].CGColor;
|
||||
self.strokeLayer.lineWidth = 10;
|
||||
self.strokeLayer.fillColor = [UIColor clearColor].CGColor;
|
||||
[self.layer addSublayer:self.strokeLayer];
|
||||
|
||||
[self.layer addSublayer:self.shapeLayer];
|
||||
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
|
||||
self.shapeLayer.frame = self.bounds;
|
||||
self.shapeLayer.path = [UIBezierPath bezierPathWithRoundedRect:self.bounds cornerRadius:10].CGPath;
|
||||
|
||||
|
||||
self.strokeLayer.frame = self.bounds;
|
||||
self.strokeLayer.path = [UIBezierPath bezierPathWithRoundedRect:self.bounds cornerRadius:10].CGPath;
|
||||
|
||||
CGFloat lineWidth = MAX(self.frame.size.width / 10.0f,10);
|
||||
|
||||
[CATransaction begin];
|
||||
[CATransaction setAnimationDuration:0];
|
||||
self.strokeLayer.lineWidth = lineWidth/1.5f;
|
||||
self.shapeLayer.lineWidth = lineWidth / 3.0f;
|
||||
|
||||
[CATransaction commit];
|
||||
}
|
||||
|
||||
-(BOOL)resizable {
|
||||
return YES;
|
||||
}
|
||||
|
||||
|
||||
@end
|
BIN
Resources/Arrow.png
Normal file
After Width: | Height: | Size: 394 B |
BIN
Resources/Arrow@2x.png
Normal file
After Width: | Height: | Size: 896 B |
BIN
Resources/Blur.png
Normal file
After Width: | Height: | Size: 169 B |
BIN
Resources/Blur@2x.png
Normal file
After Width: | Height: | Size: 250 B |
BIN
Resources/Cancel.png
Normal file
After Width: | Height: | Size: 179 B |
BIN
Resources/Cancel@2x.png
Normal file
After Width: | Height: | Size: 303 B |
BIN
Resources/Ok.png
Normal file
After Width: | Height: | Size: 245 B |
BIN
Resources/Ok@2x.png
Normal file
After Width: | Height: | Size: 363 B |
BIN
Resources/Rectangle.png
Normal file
After Width: | Height: | Size: 176 B |
BIN
Resources/Rectangle@2x.png
Normal file
After Width: | Height: | Size: 244 B |
@ -212,6 +212,18 @@
|
||||
/* Send button */
|
||||
"HockeyFeedbackComposeSend" = "Senden";
|
||||
|
||||
/* Add Image button for attachment actions */
|
||||
"HockeyFeedbackComposeAttachmentAddImage" = "+ Bild Hinzufügen";
|
||||
|
||||
/* Edit button for attachment actions */
|
||||
"HockeyFeedbackComposeAttachmentEdit" = "Anhang Bearbeiten";
|
||||
|
||||
/* Delete button for attachment actions */
|
||||
"HockeyFeedbackComposeAttachmentDelete" = "Anhang Löschen";
|
||||
|
||||
/* Cancel button for attachment actions */
|
||||
"HockeyFeedbackComposeAttachmentCancel" = "Abbrechen";
|
||||
|
||||
|
||||
/* Set User Data */
|
||||
|
||||
|
@ -212,6 +212,18 @@
|
||||
/* Send button */
|
||||
"HockeyFeedbackComposeSend" = "Send";
|
||||
|
||||
/* Add Image button for attachment actions */
|
||||
"HockeyFeedbackComposeAttachmentAddImage" = "+ Add Image";
|
||||
|
||||
/* Edit button for attachment actions */
|
||||
"HockeyFeedbackComposeAttachmentEdit" = "Edit Attachment";
|
||||
|
||||
/* Delete button for attachment actions */
|
||||
"HockeyFeedbackComposeAttachmentDelete" = "Delete Attachment";
|
||||
|
||||
/* Cancel button for attachment actions */
|
||||
"HockeyFeedbackComposeAttachmentCancel" = "Cancel";
|
||||
|
||||
|
||||
/* Set User Data */
|
||||
|
||||
|
@ -212,6 +212,18 @@
|
||||
/* Send button */
|
||||
"HockeyFeedbackComposeSend" = "Enviar";
|
||||
|
||||
/* Add Image button for attachment actions */
|
||||
"HockeyFeedbackComposeAttachmentAddImage" = "+ Add Image";
|
||||
|
||||
/* Edit button for attachment actions */
|
||||
"HockeyFeedbackComposeAttachmentEdit" = "Edit Attachment";
|
||||
|
||||
/* Delete button for attachment actions */
|
||||
"HockeyFeedbackComposeAttachmentDelete" = "Delete Attachment";
|
||||
|
||||
/* Cancel button for attachment actions */
|
||||
"HockeyFeedbackComposeAttachmentCancel" = "Cancel";
|
||||
|
||||
|
||||
/* Set User Data */
|
||||
|
||||
|
@ -212,6 +212,18 @@
|
||||
/* Send button */
|
||||
"HockeyFeedbackComposeSend" = "Envoyer";
|
||||
|
||||
/* Add Image button for attachment actions */
|
||||
"HockeyFeedbackComposeAttachmentAddImage" = "+ Add Image";
|
||||
|
||||
/* Edit button for attachment actions */
|
||||
"HockeyFeedbackComposeAttachmentEdit" = "Edit Attachment";
|
||||
|
||||
/* Delete button for attachment actions */
|
||||
"HockeyFeedbackComposeAttachmentDelete" = "Delete Attachment";
|
||||
|
||||
/* Cancel button for attachment actions */
|
||||
"HockeyFeedbackComposeAttachmentCancel" = "Cancel";
|
||||
|
||||
|
||||
/* Set User Data */
|
||||
|
||||
|
@ -212,6 +212,18 @@
|
||||
/* Send button */
|
||||
"HockeyFeedbackComposeSend" = "Šalji";
|
||||
|
||||
/* Add Image button for attachment actions */
|
||||
"HockeyFeedbackComposeAttachmentAddImage" = "+ Add Image";
|
||||
|
||||
/* Edit button for attachment actions */
|
||||
"HockeyFeedbackComposeAttachmentEdit" = "Edit Attachment";
|
||||
|
||||
/* Delete button for attachment actions */
|
||||
"HockeyFeedbackComposeAttachmentDelete" = "Delete Attachment";
|
||||
|
||||
/* Cancel button for attachment actions */
|
||||
"HockeyFeedbackComposeAttachmentCancel" = "Cancel";
|
||||
|
||||
|
||||
/* Set User Data */
|
||||
|
||||
|
@ -212,6 +212,18 @@
|
||||
/* Send button */
|
||||
"HockeyFeedbackComposeSend" = "Küldés";
|
||||
|
||||
/* Add Image button for attachment actions */
|
||||
"HockeyFeedbackComposeAttachmentAddImage" = "+ Add Image";
|
||||
|
||||
/* Edit button for attachment actions */
|
||||
"HockeyFeedbackComposeAttachmentEdit" = "Edit Attachment";
|
||||
|
||||
/* Delete button for attachment actions */
|
||||
"HockeyFeedbackComposeAttachmentDelete" = "Delete Attachment";
|
||||
|
||||
/* Cancel button for attachment actions */
|
||||
"HockeyFeedbackComposeAttachmentCancel" = "Cancel";
|
||||
|
||||
|
||||
/* Set User Data */
|
||||
|
||||
|
BIN
Resources/iconCamera.png
Executable file
After Width: | Height: | Size: 346 B |
BIN
Resources/iconCamera@2x.png
Executable file
After Width: | Height: | Size: 642 B |
@ -212,6 +212,18 @@
|
||||
/* Send button */
|
||||
"HockeyFeedbackComposeSend" = "Invia";
|
||||
|
||||
/* Add Image button for attachment actions */
|
||||
"HockeyFeedbackComposeAttachmentAddImage" = "+ Add Image";
|
||||
|
||||
/* Edit button for attachment actions */
|
||||
"HockeyFeedbackComposeAttachmentEdit" = "Edit Attachment";
|
||||
|
||||
/* Delete button for attachment actions */
|
||||
"HockeyFeedbackComposeAttachmentDelete" = "Delete Attachment";
|
||||
|
||||
/* Cancel button for attachment actions */
|
||||
"HockeyFeedbackComposeAttachmentCancel" = "Cancel";
|
||||
|
||||
|
||||
/* Set User Data */
|
||||
|
||||
|
@ -212,6 +212,18 @@
|
||||
/* Send button */
|
||||
"HockeyFeedbackComposeSend" = "送信";
|
||||
|
||||
/* Add Image button for attachment actions */
|
||||
"HockeyFeedbackComposeAttachmentAddImage" = "+ Add Image";
|
||||
|
||||
/* Edit button for attachment actions */
|
||||
"HockeyFeedbackComposeAttachmentEdit" = "Edit Attachment";
|
||||
|
||||
/* Delete button for attachment actions */
|
||||
"HockeyFeedbackComposeAttachmentDelete" = "Delete Attachment";
|
||||
|
||||
/* Cancel button for attachment actions */
|
||||
"HockeyFeedbackComposeAttachmentCancel" = "Cancel";
|
||||
|
||||
|
||||
/* Set User Data */
|
||||
|
||||
|
@ -212,6 +212,18 @@
|
||||
/* Send button */
|
||||
"HockeyFeedbackComposeSend" = "Verstuur";
|
||||
|
||||
/* Add Image button for attachment actions */
|
||||
"HockeyFeedbackComposeAttachmentAddImage" = "+ Add Image";
|
||||
|
||||
/* Edit button for attachment actions */
|
||||
"HockeyFeedbackComposeAttachmentEdit" = "Edit Attachment";
|
||||
|
||||
/* Delete button for attachment actions */
|
||||
"HockeyFeedbackComposeAttachmentDelete" = "Delete Attachment";
|
||||
|
||||
/* Cancel button for attachment actions */
|
||||
"HockeyFeedbackComposeAttachmentCancel" = "Cancel";
|
||||
|
||||
|
||||
/* Set User Data */
|
||||
|
||||
|
@ -212,6 +212,18 @@
|
||||
/* Send button */
|
||||
"HockeyFeedbackComposeSend" = "Enviar";
|
||||
|
||||
/* Add Image button for attachment actions */
|
||||
"HockeyFeedbackComposeAttachmentAddImage" = "+ Add Image";
|
||||
|
||||
/* Edit button for attachment actions */
|
||||
"HockeyFeedbackComposeAttachmentEdit" = "Edit Attachment";
|
||||
|
||||
/* Delete button for attachment actions */
|
||||
"HockeyFeedbackComposeAttachmentDelete" = "Delete Attachment";
|
||||
|
||||
/* Cancel button for attachment actions */
|
||||
"HockeyFeedbackComposeAttachmentCancel" = "Cancel";
|
||||
|
||||
|
||||
/* Set User Data */
|
||||
|
||||
|
@ -212,6 +212,18 @@
|
||||
/* Send button */
|
||||
"HockeyFeedbackComposeSend" = "Enviar";
|
||||
|
||||
/* Add Image button for attachment actions */
|
||||
"HockeyFeedbackComposeAttachmentAddImage" = "+ Add Image";
|
||||
|
||||
/* Edit button for attachment actions */
|
||||
"HockeyFeedbackComposeAttachmentEdit" = "Edit Attachment";
|
||||
|
||||
/* Delete button for attachment actions */
|
||||
"HockeyFeedbackComposeAttachmentDelete" = "Delete Attachment";
|
||||
|
||||
/* Cancel button for attachment actions */
|
||||
"HockeyFeedbackComposeAttachmentCancel" = "Cancel";
|
||||
|
||||
|
||||
/* Set User Data */
|
||||
|
||||
|
@ -212,6 +212,18 @@
|
||||
/* Send button */
|
||||
"HockeyFeedbackComposeSend" = "Trimite";
|
||||
|
||||
/* Add Image button for attachment actions */
|
||||
"HockeyFeedbackComposeAttachmentAddImage" = "+ Add Image";
|
||||
|
||||
/* Edit button for attachment actions */
|
||||
"HockeyFeedbackComposeAttachmentEdit" = "Edit Attachment";
|
||||
|
||||
/* Delete button for attachment actions */
|
||||
"HockeyFeedbackComposeAttachmentDelete" = "Delete Attachment";
|
||||
|
||||
/* Cancel button for attachment actions */
|
||||
"HockeyFeedbackComposeAttachmentCancel" = "Cancel";
|
||||
|
||||
|
||||
/* Set User Data */
|
||||
|
||||
|
@ -212,6 +212,18 @@
|
||||
/* Send button */
|
||||
"HockeyFeedbackComposeSend" = "Отправить";
|
||||
|
||||
/* Add Image button for attachment actions */
|
||||
"HockeyFeedbackComposeAttachmentAddImage" = "+ Add Image";
|
||||
|
||||
/* Edit button for attachment actions */
|
||||
"HockeyFeedbackComposeAttachmentEdit" = "Edit Attachment";
|
||||
|
||||
/* Delete button for attachment actions */
|
||||
"HockeyFeedbackComposeAttachmentDelete" = "Delete Attachment";
|
||||
|
||||
/* Cancel button for attachment actions */
|
||||
"HockeyFeedbackComposeAttachmentCancel" = "Cancel";
|
||||
|
||||
|
||||
/* Set User Data */
|
||||
|
||||
|
@ -212,6 +212,18 @@
|
||||
/* Send button */
|
||||
"HockeyFeedbackComposeSend" = "发送";
|
||||
|
||||
/* Add Image button for attachment actions */
|
||||
"HockeyFeedbackComposeAttachmentAddImage" = "+ Add Image";
|
||||
|
||||
/* Edit button for attachment actions */
|
||||
"HockeyFeedbackComposeAttachmentEdit" = "Edit Attachment";
|
||||
|
||||
/* Delete button for attachment actions */
|
||||
"HockeyFeedbackComposeAttachmentDelete" = "Delete Attachment";
|
||||
|
||||
/* Cancel button for attachment actions */
|
||||
"HockeyFeedbackComposeAttachmentCancel" = "Cancel";
|
||||
|
||||
|
||||
/* Set User Data */
|
||||
|
||||
|
@ -1,3 +1,3 @@
|
||||
OTHER_LDFLAGS=$(inherited) -framework CoreText -framework CoreGraphics -framework Foundation -framework QuartzCore -framework SystemConfiguration -framework UIKit -framework Security
|
||||
OTHER_LDFLAGS=$(inherited) -framework CoreText -framework CoreGraphics -framework Foundation -framework QuartzCore -framework SystemConfiguration -framework UIKit -framework Security -framework AssetsLibrary
|
||||
HOCKEYSDK_DOCSET_NAME=HockeySDK-iOS
|
||||
GCC_PREPROCESSOR_DEFINITIONS=$(inherited) CONFIGURATION_$(CONFIGURATION)
|
||||
|
@ -138,6 +138,35 @@
|
||||
1EF95CA7162CB037000AE3AD /* BITFeedbackActivity.m in Sources */ = {isa = PBXBuildFile; fileRef = 1EF95CA5162CB036000AE3AD /* BITFeedbackActivity.m */; };
|
||||
1EF95CAA162CB314000AE3AD /* BITFeedbackComposeViewControllerDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 1EF95CA9162CB313000AE3AD /* BITFeedbackComposeViewControllerDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
1EFF03E517F2485500A5F13C /* BITCrashManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1EFF03E417F2485500A5F13C /* BITCrashManagerTests.m */; };
|
||||
973EC8B418BCA7BC00DBFFBB /* BITImageAnnotationViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 97F0FA0018AE375E00EF50AA /* BITImageAnnotationViewController.m */; };
|
||||
973EC8B718BCA8A200DBFFBB /* BITRectangleImageAnnotation.h in Headers */ = {isa = PBXBuildFile; fileRef = 973EC8B518BCA8A200DBFFBB /* BITRectangleImageAnnotation.h */; };
|
||||
973EC8B818BCA8A200DBFFBB /* BITRectangleImageAnnotation.m in Sources */ = {isa = PBXBuildFile; fileRef = 973EC8B618BCA8A200DBFFBB /* BITRectangleImageAnnotation.m */; };
|
||||
973EC8BB18BDE29800DBFFBB /* BITArrowImageAnnotation.h in Headers */ = {isa = PBXBuildFile; fileRef = 973EC8B918BDE29800DBFFBB /* BITArrowImageAnnotation.h */; };
|
||||
973EC8BC18BDE29800DBFFBB /* BITArrowImageAnnotation.m in Sources */ = {isa = PBXBuildFile; fileRef = 973EC8BA18BDE29800DBFFBB /* BITArrowImageAnnotation.m */; };
|
||||
973EC8BF18BE2B5B00DBFFBB /* BITBlurImageAnnotation.h in Headers */ = {isa = PBXBuildFile; fileRef = 973EC8BD18BE2B5B00DBFFBB /* BITBlurImageAnnotation.h */; };
|
||||
973EC8C018BE2B5B00DBFFBB /* BITBlurImageAnnotation.m in Sources */ = {isa = PBXBuildFile; fileRef = 973EC8BE18BE2B5B00DBFFBB /* BITBlurImageAnnotation.m */; };
|
||||
9760F6C418BB4D2D00959B93 /* AssetsLibrary.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9760F6C318BB4D2D00959B93 /* AssetsLibrary.framework */; };
|
||||
9760F6CF18BB685600959B93 /* BITImageAnnotation.h in Headers */ = {isa = PBXBuildFile; fileRef = 9760F6CD18BB685600959B93 /* BITImageAnnotation.h */; };
|
||||
9760F6D018BB685600959B93 /* BITImageAnnotation.m in Sources */ = {isa = PBXBuildFile; fileRef = 9760F6CE18BB685600959B93 /* BITImageAnnotation.m */; };
|
||||
9774BCFF192CB20A00085EB5 /* BITActivityIndicatorButton.h in Headers */ = {isa = PBXBuildFile; fileRef = 9774BCFD192CB20A00085EB5 /* BITActivityIndicatorButton.h */; };
|
||||
9774BD00192CB20A00085EB5 /* BITActivityIndicatorButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 9774BCFE192CB20A00085EB5 /* BITActivityIndicatorButton.m */; };
|
||||
9782023218F81BFC00A98D8B /* Arrow.png in Resources */ = {isa = PBXBuildFile; fileRef = 9782022818F81BFC00A98D8B /* Arrow.png */; };
|
||||
9782023318F81BFC00A98D8B /* Arrow@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 9782022918F81BFC00A98D8B /* Arrow@2x.png */; };
|
||||
9782023418F81BFC00A98D8B /* Blur.png in Resources */ = {isa = PBXBuildFile; fileRef = 9782022A18F81BFC00A98D8B /* Blur.png */; };
|
||||
9782023518F81BFC00A98D8B /* Blur@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 9782022B18F81BFC00A98D8B /* Blur@2x.png */; };
|
||||
9782023618F81BFC00A98D8B /* Cancel.png in Resources */ = {isa = PBXBuildFile; fileRef = 9782022C18F81BFC00A98D8B /* Cancel.png */; };
|
||||
9782023718F81BFC00A98D8B /* Cancel@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 9782022D18F81BFC00A98D8B /* Cancel@2x.png */; };
|
||||
9782023818F81BFC00A98D8B /* Ok.png in Resources */ = {isa = PBXBuildFile; fileRef = 9782022E18F81BFC00A98D8B /* Ok.png */; };
|
||||
9782023918F81BFC00A98D8B /* Ok@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 9782022F18F81BFC00A98D8B /* Ok@2x.png */; };
|
||||
9782023A18F81BFC00A98D8B /* Rectangle.png in Resources */ = {isa = PBXBuildFile; fileRef = 9782023018F81BFC00A98D8B /* Rectangle.png */; };
|
||||
9782023B18F81BFC00A98D8B /* Rectangle@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 9782023118F81BFC00A98D8B /* Rectangle@2x.png */; };
|
||||
97BD9BD5191109730043FD59 /* QuickLook.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 97BD9BD4191109730043FD59 /* QuickLook.framework */; };
|
||||
97CC11F71917AB7C0028768F /* QuickLook.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 97BD9BD4191109730043FD59 /* QuickLook.framework */; };
|
||||
97CC11F91917C0310028768F /* MobileCoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 97CC11F81917C0310028768F /* MobileCoreServices.framework */; };
|
||||
97CC11FA1917C0390028768F /* MobileCoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 97CC11F81917C0310028768F /* MobileCoreServices.framework */; };
|
||||
97F0F9FD18ABAECD00EF50AA /* iconCamera.png in Resources */ = {isa = PBXBuildFile; fileRef = 97F0F9FB18ABAECD00EF50AA /* iconCamera.png */; };
|
||||
97F0F9FE18ABAECD00EF50AA /* iconCamera@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 97F0F9FC18ABAECD00EF50AA /* iconCamera@2x.png */; };
|
||||
97F0FA0518B2294D00EF50AA /* BITFeedbackMessageAttachment.m in Sources */ = {isa = PBXBuildFile; fileRef = 97F0FA0318AE5AED00EF50AA /* BITFeedbackMessageAttachment.m */; };
|
||||
E405266217A2AD300096359C /* BITFeedbackManagerDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = E405266117A2AD300096359C /* BITFeedbackManagerDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
E40E0B0917DA19DC005E38C1 /* BITHockeyAppClientTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E40E0B0817DA19DC005E38C1 /* BITHockeyAppClientTests.m */; };
|
||||
E40E0B0C17DA1AFF005E38C1 /* BITHockeyAppClient.h in Headers */ = {isa = PBXBuildFile; fileRef = E40E0B0A17DA1AFF005E38C1 /* BITHockeyAppClient.h */; };
|
||||
@ -303,6 +332,35 @@
|
||||
1EF95CA9162CB313000AE3AD /* BITFeedbackComposeViewControllerDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITFeedbackComposeViewControllerDelegate.h; sourceTree = "<group>"; };
|
||||
1EFF03D717F20F8300A5F13C /* BITCrashManagerPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BITCrashManagerPrivate.h; sourceTree = "<group>"; };
|
||||
1EFF03E417F2485500A5F13C /* BITCrashManagerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITCrashManagerTests.m; sourceTree = "<group>"; };
|
||||
973EC8B518BCA8A200DBFFBB /* BITRectangleImageAnnotation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITRectangleImageAnnotation.h; sourceTree = "<group>"; };
|
||||
973EC8B618BCA8A200DBFFBB /* BITRectangleImageAnnotation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITRectangleImageAnnotation.m; sourceTree = "<group>"; };
|
||||
973EC8B918BDE29800DBFFBB /* BITArrowImageAnnotation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITArrowImageAnnotation.h; sourceTree = "<group>"; };
|
||||
973EC8BA18BDE29800DBFFBB /* BITArrowImageAnnotation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITArrowImageAnnotation.m; sourceTree = "<group>"; };
|
||||
973EC8BD18BE2B5B00DBFFBB /* BITBlurImageAnnotation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITBlurImageAnnotation.h; sourceTree = "<group>"; };
|
||||
973EC8BE18BE2B5B00DBFFBB /* BITBlurImageAnnotation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITBlurImageAnnotation.m; sourceTree = "<group>"; };
|
||||
9760F6C318BB4D2D00959B93 /* AssetsLibrary.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AssetsLibrary.framework; path = System/Library/Frameworks/AssetsLibrary.framework; sourceTree = SDKROOT; };
|
||||
9760F6CD18BB685600959B93 /* BITImageAnnotation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITImageAnnotation.h; sourceTree = "<group>"; };
|
||||
9760F6CE18BB685600959B93 /* BITImageAnnotation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITImageAnnotation.m; sourceTree = "<group>"; };
|
||||
9774BCFD192CB20A00085EB5 /* BITActivityIndicatorButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITActivityIndicatorButton.h; sourceTree = "<group>"; };
|
||||
9774BCFE192CB20A00085EB5 /* BITActivityIndicatorButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITActivityIndicatorButton.m; sourceTree = "<group>"; };
|
||||
9782022818F81BFC00A98D8B /* Arrow.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Arrow.png; sourceTree = "<group>"; };
|
||||
9782022918F81BFC00A98D8B /* Arrow@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Arrow@2x.png"; sourceTree = "<group>"; };
|
||||
9782022A18F81BFC00A98D8B /* Blur.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Blur.png; sourceTree = "<group>"; };
|
||||
9782022B18F81BFC00A98D8B /* Blur@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Blur@2x.png"; sourceTree = "<group>"; };
|
||||
9782022C18F81BFC00A98D8B /* Cancel.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Cancel.png; sourceTree = "<group>"; };
|
||||
9782022D18F81BFC00A98D8B /* Cancel@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Cancel@2x.png"; sourceTree = "<group>"; };
|
||||
9782022E18F81BFC00A98D8B /* Ok.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Ok.png; sourceTree = "<group>"; };
|
||||
9782022F18F81BFC00A98D8B /* Ok@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Ok@2x.png"; sourceTree = "<group>"; };
|
||||
9782023018F81BFC00A98D8B /* Rectangle.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Rectangle.png; sourceTree = "<group>"; };
|
||||
9782023118F81BFC00A98D8B /* Rectangle@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Rectangle@2x.png"; sourceTree = "<group>"; };
|
||||
97BD9BD4191109730043FD59 /* QuickLook.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuickLook.framework; path = System/Library/Frameworks/QuickLook.framework; sourceTree = SDKROOT; };
|
||||
97CC11F81917C0310028768F /* MobileCoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MobileCoreServices.framework; path = System/Library/Frameworks/MobileCoreServices.framework; sourceTree = SDKROOT; };
|
||||
97F0F9FB18ABAECD00EF50AA /* iconCamera.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = iconCamera.png; sourceTree = "<group>"; };
|
||||
97F0F9FC18ABAECD00EF50AA /* iconCamera@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "iconCamera@2x.png"; sourceTree = "<group>"; };
|
||||
97F0F9FF18AE375E00EF50AA /* BITImageAnnotationViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITImageAnnotationViewController.h; sourceTree = "<group>"; };
|
||||
97F0FA0018AE375E00EF50AA /* BITImageAnnotationViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITImageAnnotationViewController.m; sourceTree = "<group>"; };
|
||||
97F0FA0218AE5AED00EF50AA /* BITFeedbackMessageAttachment.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITFeedbackMessageAttachment.h; sourceTree = "<group>"; };
|
||||
97F0FA0318AE5AED00EF50AA /* BITFeedbackMessageAttachment.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITFeedbackMessageAttachment.m; sourceTree = "<group>"; };
|
||||
BEE0207C16C5107E004426EA /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/HockeySDK.strings; sourceTree = "<group>"; };
|
||||
E400561D148D79B500EB22B9 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; };
|
||||
E405266117A2AD300096359C /* BITFeedbackManagerDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITFeedbackManagerDelegate.h; sourceTree = "<group>"; };
|
||||
@ -328,6 +386,9 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
97CC11F91917C0310028768F /* MobileCoreServices.framework in Frameworks */,
|
||||
97BD9BD5191109730043FD59 /* QuickLook.framework in Frameworks */,
|
||||
9760F6C418BB4D2D00959B93 /* AssetsLibrary.framework in Frameworks */,
|
||||
1E5954DC15B6F24A00A03429 /* Foundation.framework in Frameworks */,
|
||||
1E5954DD15B6F24A00A03429 /* CrashReporter.framework in Frameworks */,
|
||||
);
|
||||
@ -344,6 +405,8 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
97CC11FA1917C0390028768F /* MobileCoreServices.framework in Frameworks */,
|
||||
97CC11F71917AB7C0028768F /* QuickLook.framework in Frameworks */,
|
||||
1EA1170016F4D32C001C015C /* libHockeySDK.a in Frameworks */,
|
||||
1E5A459216F0DFC200B55C04 /* SenTestingKit.framework in Frameworks */,
|
||||
1EA1170116F4D354001C015C /* CrashReporter.framework in Frameworks */,
|
||||
@ -359,6 +422,18 @@
|
||||
1E5955A415B71BDC00A03429 /* Images */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9782022818F81BFC00A98D8B /* Arrow.png */,
|
||||
9782022918F81BFC00A98D8B /* Arrow@2x.png */,
|
||||
9782022A18F81BFC00A98D8B /* Blur.png */,
|
||||
9782022B18F81BFC00A98D8B /* Blur@2x.png */,
|
||||
9782022C18F81BFC00A98D8B /* Cancel.png */,
|
||||
9782022D18F81BFC00A98D8B /* Cancel@2x.png */,
|
||||
9782022E18F81BFC00A98D8B /* Ok.png */,
|
||||
9782022F18F81BFC00A98D8B /* Ok@2x.png */,
|
||||
9782023018F81BFC00A98D8B /* Rectangle.png */,
|
||||
9782023118F81BFC00A98D8B /* Rectangle@2x.png */,
|
||||
97F0F9FB18ABAECD00EF50AA /* iconCamera.png */,
|
||||
97F0F9FC18ABAECD00EF50AA /* iconCamera@2x.png */,
|
||||
1E5955BB15B71C8600A03429 /* authorize_denied.png */,
|
||||
1E5955BC15B71C8600A03429 /* authorize_denied@2x.png */,
|
||||
1E5955BF15B71C8600A03429 /* bg.png */,
|
||||
@ -450,8 +525,11 @@
|
||||
1E754E461621FA9A0070AB92 /* Feedback */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9760F6CC18BB684200959B93 /* Image Editor */,
|
||||
1E49A4361612223B00463151 /* BITFeedbackMessage.h */,
|
||||
1E49A4371612223B00463151 /* BITFeedbackMessage.m */,
|
||||
97F0FA0218AE5AED00EF50AA /* BITFeedbackMessageAttachment.h */,
|
||||
97F0FA0318AE5AED00EF50AA /* BITFeedbackMessageAttachment.m */,
|
||||
1E49A42D1612223B00463151 /* BITFeedbackComposeViewController.h */,
|
||||
1E49A42E1612223B00463151 /* BITFeedbackComposeViewController.m */,
|
||||
1EF95CA9162CB313000AE3AD /* BITFeedbackComposeViewControllerDelegate.h */,
|
||||
@ -467,6 +545,8 @@
|
||||
1E49A4341612223B00463151 /* BITFeedbackManager.m */,
|
||||
E405266117A2AD300096359C /* BITFeedbackManagerDelegate.h */,
|
||||
1E49A4351612223B00463151 /* BITFeedbackManagerPrivate.h */,
|
||||
9774BCFD192CB20A00085EB5 /* BITActivityIndicatorButton.h */,
|
||||
9774BCFE192CB20A00085EB5 /* BITActivityIndicatorButton.m */,
|
||||
);
|
||||
name = Feedback;
|
||||
sourceTree = "<group>";
|
||||
@ -550,6 +630,23 @@
|
||||
name = Private;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9760F6CC18BB684200959B93 /* Image Editor */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
97F0F9FF18AE375E00EF50AA /* BITImageAnnotationViewController.h */,
|
||||
97F0FA0018AE375E00EF50AA /* BITImageAnnotationViewController.m */,
|
||||
9760F6CD18BB685600959B93 /* BITImageAnnotation.h */,
|
||||
9760F6CE18BB685600959B93 /* BITImageAnnotation.m */,
|
||||
973EC8B518BCA8A200DBFFBB /* BITRectangleImageAnnotation.h */,
|
||||
973EC8B618BCA8A200DBFFBB /* BITRectangleImageAnnotation.m */,
|
||||
973EC8B918BDE29800DBFFBB /* BITArrowImageAnnotation.h */,
|
||||
973EC8BA18BDE29800DBFFBB /* BITArrowImageAnnotation.m */,
|
||||
973EC8BD18BE2B5B00DBFFBB /* BITBlurImageAnnotation.h */,
|
||||
973EC8BE18BE2B5B00DBFFBB /* BITBlurImageAnnotation.m */,
|
||||
);
|
||||
name = "Image Editor";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E400560F148D79B500EB22B9 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -578,6 +675,9 @@
|
||||
E400561C148D79B500EB22B9 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
97CC11F81917C0310028768F /* MobileCoreServices.framework */,
|
||||
97BD9BD4191109730043FD59 /* QuickLook.framework */,
|
||||
9760F6C318BB4D2D00959B93 /* AssetsLibrary.framework */,
|
||||
E41EB48B148D7C4E0015DEDC /* CrashReporter.framework */,
|
||||
E400561D148D79B500EB22B9 /* Foundation.framework */,
|
||||
1E5A459116F0DFC200B55C04 /* SenTestingKit.framework */,
|
||||
@ -659,6 +759,7 @@
|
||||
1E49A4731612226D00463151 /* BITUpdateManager.h in Headers */,
|
||||
1E49A4791612226D00463151 /* BITUpdateManagerDelegate.h in Headers */,
|
||||
1E49A44E1612223B00463151 /* BITFeedbackManager.h in Headers */,
|
||||
973EC8BF18BE2B5B00DBFFBB /* BITBlurImageAnnotation.h in Headers */,
|
||||
E4B4DB7D17B435550099C67F /* BITAuthenticationViewController.h in Headers */,
|
||||
1E49A4481612223B00463151 /* BITFeedbackListViewController.h in Headers */,
|
||||
1ECA8F4D192B5BD8006B9416 /* BITCrashDetailsPrivate.h in Headers */,
|
||||
@ -681,10 +782,13 @@
|
||||
1E49A4851612226D00463151 /* BITUpdateViewControllerPrivate.h in Headers */,
|
||||
1ECA8F51192B6954006B9416 /* BITCrashMetaData.h in Headers */,
|
||||
1E49A4B5161222B900463151 /* BITHockeyBaseManagerPrivate.h in Headers */,
|
||||
9774BCFF192CB20A00085EB5 /* BITActivityIndicatorButton.h in Headers */,
|
||||
E4933E8017B66CDA00B11ACC /* BITHTTPOperation.h in Headers */,
|
||||
1E49A4BE161222B900463151 /* BITHockeyHelper.h in Headers */,
|
||||
973EC8BB18BDE29800DBFFBB /* BITArrowImageAnnotation.h in Headers */,
|
||||
1E49A4C4161222B900463151 /* BITAppStoreHeader.h in Headers */,
|
||||
1E49A4CA161222B900463151 /* BITStoreButton.h in Headers */,
|
||||
973EC8B718BCA8A200DBFFBB /* BITRectangleImageAnnotation.h in Headers */,
|
||||
E405266217A2AD300096359C /* BITFeedbackManagerDelegate.h in Headers */,
|
||||
1E49A4D0161222B900463151 /* BITWebTableViewCell.h in Headers */,
|
||||
1E49A4D8161222D400463151 /* HockeySDKPrivate.h in Headers */,
|
||||
@ -694,6 +798,7 @@
|
||||
1EACC97B162F041E007578C5 /* BITAttributedLabel.h in Headers */,
|
||||
1E0FEE28173BDB260061331F /* BITKeychainUtils.h in Headers */,
|
||||
1E94F9E416E9136B006570AD /* BITStoreUpdateManagerPrivate.h in Headers */,
|
||||
9760F6CF18BB685600959B93 /* BITImageAnnotation.h in Headers */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@ -807,20 +912,32 @@
|
||||
files = (
|
||||
1E5955C615B71C8600A03429 /* authorize_denied.png in Resources */,
|
||||
1E5955C715B71C8600A03429 /* authorize_denied@2x.png in Resources */,
|
||||
9782023B18F81BFC00A98D8B /* Rectangle@2x.png in Resources */,
|
||||
1E5955CA15B71C8600A03429 /* bg.png in Resources */,
|
||||
97F0F9FD18ABAECD00EF50AA /* iconCamera.png in Resources */,
|
||||
9782023618F81BFC00A98D8B /* Cancel.png in Resources */,
|
||||
9782023318F81BFC00A98D8B /* Arrow@2x.png in Resources */,
|
||||
9782023818F81BFC00A98D8B /* Ok.png in Resources */,
|
||||
1E5955CB15B71C8600A03429 /* buttonHighlight.png in Resources */,
|
||||
1E5955CC15B71C8600A03429 /* buttonHighlight@2x.png in Resources */,
|
||||
1E5955CF15B71C8600A03429 /* IconGradient.png in Resources */,
|
||||
1E5955D015B71C8600A03429 /* IconGradient@2x.png in Resources */,
|
||||
9782023218F81BFC00A98D8B /* Arrow.png in Resources */,
|
||||
9782023A18F81BFC00A98D8B /* Rectangle.png in Resources */,
|
||||
1EAF20A8162DC0F600957B1D /* feedbackActivity@2x~ipad.png in Resources */,
|
||||
1EAF20A9162DC0F600957B1D /* feedbackActivity~ipad.png in Resources */,
|
||||
9782023918F81BFC00A98D8B /* Ok@2x.png in Resources */,
|
||||
9782023418F81BFC00A98D8B /* Blur.png in Resources */,
|
||||
1EAF20AA162DC0F600957B1D /* feedbackActiviy.png in Resources */,
|
||||
1EAF20AB162DC0F600957B1D /* feedbackActiviy@2x.png in Resources */,
|
||||
9782023518F81BFC00A98D8B /* Blur@2x.png in Resources */,
|
||||
1E1127C416580C87007067A2 /* buttonRoundedDelete.png in Resources */,
|
||||
1E1127C516580C87007067A2 /* buttonRoundedDelete@2x.png in Resources */,
|
||||
1E1127C616580C87007067A2 /* buttonRoundedDeleteHighlighted.png in Resources */,
|
||||
9782023718F81BFC00A98D8B /* Cancel@2x.png in Resources */,
|
||||
1E1127C716580C87007067A2 /* buttonRoundedDeleteHighlighted@2x.png in Resources */,
|
||||
1E1127C816580C87007067A2 /* buttonRoundedRegular.png in Resources */,
|
||||
97F0F9FE18ABAECD00EF50AA /* iconCamera@2x.png in Resources */,
|
||||
1E1127C916580C87007067A2 /* buttonRoundedRegular@2x.png in Resources */,
|
||||
1E1127CA16580C87007067A2 /* buttonRoundedRegularHighlighted.png in Resources */,
|
||||
1E1127CB16580C87007067A2 /* buttonRoundedRegularHighlighted@2x.png in Resources */,
|
||||
@ -896,6 +1013,7 @@
|
||||
1E49A43F1612223B00463151 /* BITFeedbackComposeViewController.m in Sources */,
|
||||
E40E0B0D17DA1AFF005E38C1 /* BITHockeyAppClient.m in Sources */,
|
||||
1E49A4451612223B00463151 /* BITFeedbackListViewCell.m in Sources */,
|
||||
973EC8B818BCA8A200DBFFBB /* BITRectangleImageAnnotation.m in Sources */,
|
||||
1E49A44B1612223B00463151 /* BITFeedbackListViewController.m in Sources */,
|
||||
1E49A4511612223B00463151 /* BITFeedbackManager.m in Sources */,
|
||||
1ECA8F52192B6954006B9416 /* BITCrashMetaData.m in Sources */,
|
||||
@ -905,11 +1023,15 @@
|
||||
1E49A4601612223B00463151 /* BITFeedbackUserDataViewController.m in Sources */,
|
||||
1E49A4701612226D00463151 /* BITAppVersionMetaInfo.m in Sources */,
|
||||
1E49A4761612226D00463151 /* BITUpdateManager.m in Sources */,
|
||||
1E49A4C1161222B900463151 /* BITHockeyHelper.m in Sources */,
|
||||
9774BD00192CB20A00085EB5 /* BITActivityIndicatorButton.m in Sources */,
|
||||
1E49A4821612226D00463151 /* BITUpdateViewController.m in Sources */,
|
||||
E4B4DB7E17B435550099C67F /* BITAuthenticationViewController.m in Sources */,
|
||||
1E49A4B2161222B900463151 /* BITHockeyBaseManager.m in Sources */,
|
||||
973EC8C018BE2B5B00DBFFBB /* BITBlurImageAnnotation.m in Sources */,
|
||||
9760F6D018BB685600959B93 /* BITImageAnnotation.m in Sources */,
|
||||
1E49A4BB161222B900463151 /* BITHockeyBaseViewController.m in Sources */,
|
||||
1E49A4C1161222B900463151 /* BITHockeyHelper.m in Sources */,
|
||||
97F0FA0518B2294D00EF50AA /* BITFeedbackMessageAttachment.m in Sources */,
|
||||
1E49A4C7161222B900463151 /* BITAppStoreHeader.m in Sources */,
|
||||
1E49A4CD161222B900463151 /* BITStoreButton.m in Sources */,
|
||||
1E90FD7418EDB86400CF0417 /* BITCrashDetails.m in Sources */,
|
||||
@ -920,6 +1042,8 @@
|
||||
1E754E611621FBB70070AB92 /* BITCrashReportTextFormatter.m in Sources */,
|
||||
1EF95CA7162CB037000AE3AD /* BITFeedbackActivity.m in Sources */,
|
||||
1EACC97C162F041E007578C5 /* BITAttributedLabel.m in Sources */,
|
||||
973EC8BC18BDE29800DBFFBB /* BITArrowImageAnnotation.m in Sources */,
|
||||
973EC8B418BCA7BC00DBFFBB /* BITImageAnnotationViewController.m in Sources */,
|
||||
1E0FEE29173BDB260061331F /* BITKeychainUtils.m in Sources */,
|
||||
1E94F9E216E91330006570AD /* BITStoreUpdateManager.m in Sources */,
|
||||
);
|
||||
|