forked from organicmaps/organicmaps
[ios] Fixed iPhone portrait PP scroll.
This commit is contained in:
parent
83ebe0e2de
commit
4bd9e204c9
7 changed files with 167 additions and 83 deletions
|
@ -15,7 +15,7 @@
|
|||
@property (nonatomic) MWMSpringAnimation * springAnimation;
|
||||
|
||||
- (void)cancelSpringAnimation;
|
||||
- (void)startAnimatingPlacePage:(MWMPlacePage *)placePage initialVelocity:(CGPoint)velocity;
|
||||
- (void)startAnimatingPlacePage:(MWMPlacePage *)placePage initialVelocity:(CGPoint)velocity completion:(void (^)(void))completion;
|
||||
- (CGPoint)targetPoint;
|
||||
|
||||
@end
|
||||
|
|
|
@ -35,10 +35,10 @@
|
|||
self.springAnimation = nil;
|
||||
}
|
||||
|
||||
- (void)startAnimatingPlacePage:(MWMPlacePage *)placePage initialVelocity:(CGPoint)velocity
|
||||
- (void)startAnimatingPlacePage:(MWMPlacePage *)placePage initialVelocity:(CGPoint)velocity completion:(void (^)(void))completion
|
||||
{
|
||||
[self cancelSpringAnimation];
|
||||
self.springAnimation = [MWMSpringAnimation animationWithView:placePage.extendedPlacePageView target:placePage.targetPoint velocity:velocity];
|
||||
self.springAnimation = [MWMSpringAnimation animationWithView:placePage.extendedPlacePageView target:placePage.targetPoint velocity:velocity completion:completion];
|
||||
[self.manager.ownerViewController.view.animator addAnimation:self.springAnimation];
|
||||
}
|
||||
|
||||
|
|
|
@ -62,6 +62,7 @@ typedef NS_ENUM(NSUInteger, MWMPlacePageManagerState)
|
|||
{
|
||||
self.state = MWMPlacePageManagerStateClosed;
|
||||
[self.placePage dismiss];
|
||||
[[MapsAppDelegate theApp].m_locationManager stop:self];
|
||||
GetFramework().GetBalloonManager().RemovePin();
|
||||
m_userMark = nullptr;
|
||||
self.entity = nil;
|
||||
|
|
|
@ -9,10 +9,14 @@
|
|||
#import <Foundation/Foundation.h>
|
||||
#import "MWMAnimator.h"
|
||||
|
||||
typedef void (^MWMSpringAnimationCompletionBlock) (void);
|
||||
|
||||
@interface MWMSpringAnimation : NSObject <Animation>
|
||||
|
||||
@property (nonatomic, readonly) CGPoint velocity;
|
||||
|
||||
+ (instancetype)animationWithView:(UIView *)view target:(CGPoint)target velocity:(CGPoint)velocity;
|
||||
+ (instancetype)animationWithView:(UIView *)view target:(CGPoint)target velocity:(CGPoint)velocity completion:(MWMSpringAnimationCompletionBlock)completion;
|
||||
|
||||
+ (CGFloat)approxTargetFor:(CGFloat)startValue velocity:(CGFloat)velocity;
|
||||
|
||||
@end
|
||||
|
|
|
@ -14,17 +14,18 @@
|
|||
@property (nonatomic) CGPoint velocity;
|
||||
@property (nonatomic) CGPoint targetPoint;
|
||||
@property (nonatomic) UIView * view;
|
||||
@property (copy, nonatomic) MWMSpringAnimationCompletionBlock completion;
|
||||
|
||||
@end
|
||||
|
||||
@implementation MWMSpringAnimation
|
||||
|
||||
+ (instancetype)animationWithView:(UIView *)view target:(CGPoint)target velocity:(CGPoint)velocity
|
||||
+ (instancetype)animationWithView:(UIView *)view target:(CGPoint)target velocity:(CGPoint)velocity completion:(MWMSpringAnimationCompletionBlock)completion
|
||||
{
|
||||
return [[self alloc] initWithView:view target:target velocity:velocity];
|
||||
return [[self alloc] initWithView:view target:target velocity:velocity completion:completion];
|
||||
}
|
||||
|
||||
- (instancetype)initWithView:(UIView *)view target:(CGPoint)target velocity:(CGPoint)velocity
|
||||
- (instancetype)initWithView:(UIView *)view target:(CGPoint)target velocity:(CGPoint)velocity completion:(MWMSpringAnimationCompletionBlock)completion
|
||||
{
|
||||
self = [super init];
|
||||
if (self)
|
||||
|
@ -32,6 +33,7 @@
|
|||
self.view = view;
|
||||
self.targetPoint = target;
|
||||
self.velocity = velocity;
|
||||
self.completion = completion;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
@ -58,7 +60,15 @@
|
|||
{
|
||||
self.view.center = self.targetPoint;
|
||||
*finished = YES;
|
||||
if (self.completion)
|
||||
self.completion();
|
||||
}
|
||||
}
|
||||
|
||||
+ (CGFloat)approxTargetFor:(CGFloat)startValue velocity:(CGFloat)velocity
|
||||
{
|
||||
CGFloat const decelaration = (velocity > 0 ? -1.0 : 1.0) * 300.0;
|
||||
return startValue - (velocity * velocity) / (2.0 * decelaration);
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
|
@ -29,6 +29,7 @@ typedef NS_ENUM(NSUInteger, MWMiPhoneLandscapePlacePageState)
|
|||
|
||||
@property (nonatomic) MWMiPhoneLandscapePlacePageState state;
|
||||
@property (nonatomic) CGPoint targetPoint;
|
||||
@property (nonatomic) CGFloat panVelocity;
|
||||
|
||||
@end
|
||||
|
||||
|
@ -64,49 +65,56 @@ typedef NS_ENUM(NSUInteger, MWMiPhoneLandscapePlacePageState)
|
|||
self.state = MWMiPhoneLandscapePlacePageStateOpen;
|
||||
}
|
||||
|
||||
- (void)dismiss
|
||||
{
|
||||
self.state = MWMiPhoneLandscapePlacePageStateClosed;
|
||||
}
|
||||
|
||||
- (void)setState:(MWMiPhoneLandscapePlacePageState)state
|
||||
{
|
||||
_state = state;
|
||||
[self updateTargetPoint];
|
||||
}
|
||||
|
||||
- (void)updateTargetPoint
|
||||
{
|
||||
CGSize const size = UIScreen.mainScreen.bounds.size;
|
||||
CGFloat const height = MIN(size.width, size.height);
|
||||
CGFloat const offset = MIN(height, kMaximumPlacePageWidth);
|
||||
|
||||
switch (state)
|
||||
switch (self.state)
|
||||
{
|
||||
case MWMiPhoneLandscapePlacePageStateClosed:
|
||||
GetFramework().GetBalloonManager().RemovePin();
|
||||
[self.actionBar removeFromSuperview];
|
||||
self.targetPoint = CGPointMake(-offset / 2., height / 2.);
|
||||
break;
|
||||
|
||||
case MWMiPhoneLandscapePlacePageStateOpen:
|
||||
self.targetPoint = CGPointMake(offset / 2., height / 2.);
|
||||
break;
|
||||
}
|
||||
_state = state;
|
||||
[self startAnimatingPlacePage:self initialVelocity:self.springAnimation.velocity];
|
||||
}
|
||||
|
||||
#pragma mark - Actions
|
||||
|
||||
- (IBAction)didPan:(UIPanGestureRecognizer *)sender
|
||||
{
|
||||
CGPoint const point = [sender translationInView:self.extendedPlacePageView.superview];
|
||||
UIView * ppv = self.extendedPlacePageView;
|
||||
UIView * ppvSuper = ppv.superview;
|
||||
CGPoint const point = [sender translationInView:ppvSuper];
|
||||
CGSize const size = UIScreen.mainScreen.bounds.size;
|
||||
CGFloat const height = MIN(size.width, size.height);
|
||||
CGFloat const offset = MAX(height, kMaximumPlacePageWidth);
|
||||
CGFloat const x = self.extendedPlacePageView.center.x + point.x;
|
||||
CGFloat const x = ppv.center.x + point.x;
|
||||
|
||||
if (x > offset / 2.)
|
||||
return;
|
||||
|
||||
self.extendedPlacePageView.center = CGPointMake(self.extendedPlacePageView.center.x + point.x, self.extendedPlacePageView.center.y );
|
||||
[sender setTranslation:CGPointZero inView:self.extendedPlacePageView.superview];
|
||||
ppv.center = CGPointMake(ppv.center.x + point.x, ppv.center.y );
|
||||
[sender setTranslation:CGPointZero inView:ppvSuper];
|
||||
if (sender.state == UIGestureRecognizerStateEnded)
|
||||
{
|
||||
CGPoint velocity = [sender velocityInView:self.extendedPlacePageView.superview];
|
||||
velocity.y = 5.;
|
||||
self.state = velocity.x < 0. ? MWMiPhoneLandscapePlacePageStateClosed : MWMiPhoneLandscapePlacePageStateOpen;
|
||||
[self startAnimatingPlacePage:self initialVelocity:velocity];
|
||||
if (self.state == MWMiPhoneLandscapePlacePageStateClosed)
|
||||
self.panVelocity = [sender velocityInView:ppvSuper].x;
|
||||
if (self.panVelocity > 0)
|
||||
self.state = MWMiPhoneLandscapePlacePageStateOpen;
|
||||
else
|
||||
[self.manager dismissPlacePage];
|
||||
}
|
||||
else
|
||||
|
@ -139,4 +147,21 @@ typedef NS_ENUM(NSUInteger, MWMiPhoneLandscapePlacePageState)
|
|||
}];
|
||||
}
|
||||
|
||||
#pragma mark - Properties
|
||||
|
||||
- (void)setTargetPoint:(CGPoint)targetPoint
|
||||
{
|
||||
if (CGPointEqualToPoint(_targetPoint, targetPoint))
|
||||
return;
|
||||
_targetPoint = targetPoint;
|
||||
__weak MWMiPhoneLandscapePlacePage * weakSelf = self;
|
||||
[self startAnimatingPlacePage:self initialVelocity:CGPointMake(self.panVelocity, 0.0) completion:^
|
||||
{
|
||||
__strong MWMiPhoneLandscapePlacePage * self = weakSelf;
|
||||
if (self.state == MWMiPhoneLandscapePlacePageStateClosed)
|
||||
[super dismiss];
|
||||
}];
|
||||
self.panVelocity = 0.0;
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
|
@ -21,12 +21,14 @@
|
|||
#include "Framework.h"
|
||||
|
||||
static NSString * const kPlacePageViewDragKeyPath = @"center";
|
||||
static CGFloat const kPlacePageBottomOffset = 31.;
|
||||
|
||||
typedef NS_ENUM(NSUInteger, MWMiPhonePortraitPlacePageState)
|
||||
{
|
||||
MWMiPhonePortraitPlacePageStateClosed,
|
||||
MWMiPhonePortraitPlacePageStatePreview,
|
||||
MWMiPhonePortraitPlacePageStateOpen
|
||||
MWMiPhonePortraitPlacePageStateOpen,
|
||||
MWMiPhonePortraitPlacePageStateHover
|
||||
};
|
||||
|
||||
@interface MWMiPhonePortraitPlacePage ()
|
||||
|
@ -34,6 +36,7 @@ typedef NS_ENUM(NSUInteger, MWMiPhonePortraitPlacePageState)
|
|||
@property (nonatomic) MWMiPhonePortraitPlacePageState state;
|
||||
@property (nonatomic) CGPoint targetPoint;
|
||||
@property (nonatomic) CGFloat keyboardHeight;
|
||||
@property (nonatomic) CGFloat panVelocity;
|
||||
|
||||
@end
|
||||
|
||||
|
@ -63,6 +66,7 @@ typedef NS_ENUM(NSUInteger, MWMiPhonePortraitPlacePageState)
|
|||
- (void)configure
|
||||
{
|
||||
[super configure];
|
||||
self.basePlacePageView.featureTable.scrollEnabled = NO;
|
||||
UIView const * view = self.manager.ownerViewController.view;
|
||||
if ([view.subviews containsObject:self.extendedPlacePageView])
|
||||
return;
|
||||
|
@ -91,9 +95,6 @@ typedef NS_ENUM(NSUInteger, MWMiPhonePortraitPlacePageState)
|
|||
- (void)dismiss
|
||||
{
|
||||
self.state = MWMiPhonePortraitPlacePageStateClosed;
|
||||
[MWMPlacePageNavigationBar remove];
|
||||
self.keyboardHeight = 0.;
|
||||
[super dismiss];
|
||||
}
|
||||
|
||||
- (void)addBookmark
|
||||
|
@ -112,88 +113,105 @@ typedef NS_ENUM(NSUInteger, MWMiPhonePortraitPlacePageState)
|
|||
- (void)updateMyPositionStatus:(NSString *)status
|
||||
{
|
||||
[super updateMyPositionStatus:status];
|
||||
// Setup current state.
|
||||
MWMiPhonePortraitPlacePageState currentState = self.state;
|
||||
self.state = currentState;
|
||||
[self updateTargetPoint];
|
||||
}
|
||||
|
||||
- (void)setState:(MWMiPhonePortraitPlacePageState)state
|
||||
{
|
||||
CGSize const size = UIScreen.mainScreen.bounds.size;
|
||||
BOOL const isLandscape = size.width > size.height;
|
||||
CGFloat const width = isLandscape ? size.height : size.width;
|
||||
CGFloat const height = isLandscape ? size.width : size.height;
|
||||
static CGFloat const kPlacePageBottomOffset = 31.;
|
||||
CGFloat const width = MIN(size.height, size.width);
|
||||
NSString * anchorImageName;
|
||||
_state = state;
|
||||
[self updateTargetPoint];
|
||||
switch (state)
|
||||
{
|
||||
case MWMiPhonePortraitPlacePageStateClosed:
|
||||
self.targetPoint = CGPointMake(self.extendedPlacePageView.width / 2., self.extendedPlacePageView.height * 2.);
|
||||
[self.actionBar removeFromSuperview];
|
||||
break;
|
||||
|
||||
case MWMiPhonePortraitPlacePageStatePreview:
|
||||
{
|
||||
CGFloat const typeHeight = self.basePlacePageView.typeLabel.text.length > 0 ? self.basePlacePageView.typeLabel.height : self.basePlacePageView.typeDescriptionView.height;
|
||||
CGFloat const h = height - (self.basePlacePageView.titleLabel.height + kPlacePageBottomOffset + typeHeight + self.actionBar.height);
|
||||
self.targetPoint = CGPointMake(width / 2., height + h);
|
||||
|
||||
[MWMPlacePageNavigationBar dismissNavigationBar];
|
||||
anchorImageName = @"bg_placepage_tablet_normal_";
|
||||
break;
|
||||
}
|
||||
|
||||
case MWMiPhonePortraitPlacePageStateOpen:
|
||||
{
|
||||
CGFloat const typeHeight = self.basePlacePageView.typeLabel.text.length > 0 ? self.basePlacePageView.typeLabel.height : self.basePlacePageView.typeDescriptionView.height;
|
||||
CGFloat const h = height - (self.basePlacePageView.titleLabel.height + kPlacePageBottomOffset + typeHeight + [(UITableView *)self.basePlacePageView.featureTable height] + self.actionBar.height + self.keyboardHeight);
|
||||
self.targetPoint = CGPointMake(width / 2., height + h);
|
||||
|
||||
if (self.targetPoint.y <= height)
|
||||
[MWMPlacePageNavigationBar showNavigationBarForPlacePage:self];
|
||||
else
|
||||
[MWMPlacePageNavigationBar dismissNavigationBar];
|
||||
|
||||
case MWMiPhonePortraitPlacePageStateHover:
|
||||
anchorImageName = @"bg_placepage_tablet_open_";
|
||||
break;
|
||||
}
|
||||
}
|
||||
_state = state;
|
||||
[self startAnimatingPlacePage:self initialVelocity:self.springAnimation.velocity];
|
||||
self.anchorImageView.image = [UIImage imageNamed:[anchorImageName stringByAppendingString:@((NSUInteger)width).stringValue]];
|
||||
}
|
||||
|
||||
NSString * anchorImageName;
|
||||
NSNumber const * widthNumber = @((NSUInteger)width);
|
||||
if (_state == MWMiPhonePortraitPlacePageStateOpen)
|
||||
anchorImageName = [@"bg_placepage_tablet_open_" stringByAppendingString:widthNumber.stringValue];
|
||||
else
|
||||
anchorImageName = [@"bg_placepage_tablet_normal_" stringByAppendingString:widthNumber.stringValue];
|
||||
- (void)updateTargetPoint
|
||||
{
|
||||
UIView * ppv = self.extendedPlacePageView;
|
||||
switch (self.state)
|
||||
{
|
||||
case MWMiPhonePortraitPlacePageStateClosed:
|
||||
self.targetPoint = CGPointMake(ppv.width / 2., ppv.height * 2.);
|
||||
break;
|
||||
case MWMiPhonePortraitPlacePageStatePreview:
|
||||
self.targetPoint = [self getPreviewTargetPoint];
|
||||
break;
|
||||
case MWMiPhonePortraitPlacePageStateOpen:
|
||||
self.targetPoint = [self getOpenTargetPoint];
|
||||
break;
|
||||
case MWMiPhonePortraitPlacePageStateHover:
|
||||
self.targetPoint = CGPointMake(ppv.center.x, MAX([MWMSpringAnimation approxTargetFor:ppv.center.y velocity:self.panVelocity], [self getOpenTargetPoint].y));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
self.anchorImageView.image = [UIImage imageNamed:anchorImageName];
|
||||
- (CGPoint)getPreviewTargetPoint
|
||||
{
|
||||
CGSize const size = UIScreen.mainScreen.bounds.size;
|
||||
BOOL const isLandscape = size.width > size.height;
|
||||
CGFloat const width = isLandscape ? size.height : size.width;
|
||||
CGFloat const height = isLandscape ? size.width : size.height;
|
||||
MWMBasePlacePageView * basePPV = self.basePlacePageView;
|
||||
CGFloat const typeHeight = basePPV.typeLabel.text.length > 0 ? basePPV.typeLabel.height : basePPV.typeDescriptionView.height;
|
||||
CGFloat const h = height - (basePPV.titleLabel.height + kPlacePageBottomOffset + typeHeight + self.actionBar.height);
|
||||
return CGPointMake(width / 2., height + h);
|
||||
}
|
||||
|
||||
- (CGPoint)getOpenTargetPoint
|
||||
{
|
||||
CGSize const size = UIScreen.mainScreen.bounds.size;
|
||||
BOOL const isLandscape = size.width > size.height;
|
||||
CGFloat const width = isLandscape ? size.height : size.width;
|
||||
CGFloat const height = isLandscape ? size.width : size.height;
|
||||
MWMBasePlacePageView * basePPV = self.basePlacePageView;
|
||||
CGFloat const typeHeight = basePPV.typeLabel.text.length > 0 ? basePPV.typeLabel.height : basePPV.typeDescriptionView.height;
|
||||
CGFloat const h = height - (basePPV.titleLabel.height + kPlacePageBottomOffset + typeHeight + [(UITableView *)basePPV.featureTable height] + self.actionBar.height + self.keyboardHeight);
|
||||
return CGPointMake(width / 2., height + h);
|
||||
}
|
||||
|
||||
#pragma mark - Actions
|
||||
|
||||
- (IBAction)didPan:(UIPanGestureRecognizer *)sender
|
||||
{
|
||||
CGPoint const point = [sender translationInView:self.extendedPlacePageView.superview];
|
||||
self.extendedPlacePageView.center = CGPointMake(self.extendedPlacePageView.center.x, self.extendedPlacePageView.center.y + point.y);
|
||||
CGSize const size = UIScreen.mainScreen.bounds.size;
|
||||
BOOL const isLandscape = size.width > size.height;
|
||||
CGFloat const height = isLandscape ? size.width : size.height;
|
||||
UIView * ppv = self.extendedPlacePageView;
|
||||
UIView * ppvSuper = ppv.superview;
|
||||
|
||||
if (self.extendedPlacePageView.center.y <= height)
|
||||
ppv.minY += [sender translationInView:ppvSuper].y;
|
||||
ppv.midY = MAX(ppv.midY, [self getOpenTargetPoint].y);
|
||||
if (ppv.minY <= 0.0)
|
||||
[MWMPlacePageNavigationBar showNavigationBarForPlacePage:self];
|
||||
else
|
||||
[MWMPlacePageNavigationBar dismissNavigationBar];
|
||||
|
||||
[sender setTranslation:CGPointZero inView:self.extendedPlacePageView.superview];
|
||||
[sender setTranslation:CGPointZero inView:ppvSuper];
|
||||
if (sender.state == UIGestureRecognizerStateEnded)
|
||||
{
|
||||
CGPoint velocity = [sender velocityInView:self.extendedPlacePageView.superview];
|
||||
velocity.x = 5;
|
||||
self.state = velocity.y >= 0. ? MWMiPhonePortraitPlacePageStateClosed : MWMiPhonePortraitPlacePageStateOpen;
|
||||
[self startAnimatingPlacePage:self initialVelocity:velocity];
|
||||
if (self.state == MWMiPhonePortraitPlacePageStateClosed)
|
||||
{
|
||||
self.actionBar.alpha = 0.;
|
||||
self.panVelocity = [sender velocityInView:ppvSuper].y;
|
||||
CGFloat const estimatedYPosition = [MWMSpringAnimation approxTargetFor:ppv.frame.origin.y velocity:self.panVelocity];
|
||||
CGFloat const bound1 = ppvSuper.height * 0.2;
|
||||
CGFloat const bound2 = ppvSuper.height * 0.5;
|
||||
if (estimatedYPosition < bound1)
|
||||
self.state = MWMiPhonePortraitPlacePageStateHover;
|
||||
else if (self.panVelocity <= 0.0)
|
||||
self.state = MWMiPhonePortraitPlacePageStateOpen;
|
||||
else if (ppv.minY < bound2)
|
||||
self.state = MWMiPhonePortraitPlacePageStatePreview;
|
||||
else
|
||||
[self.manager dismissPlacePage];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -215,11 +233,11 @@ typedef NS_ENUM(NSUInteger, MWMiPhonePortraitPlacePageState)
|
|||
break;
|
||||
|
||||
case MWMiPhonePortraitPlacePageStateOpen:
|
||||
case MWMiPhonePortraitPlacePageStateHover:
|
||||
self.state = MWMiPhonePortraitPlacePageStatePreview;
|
||||
[self.manager.ownerViewController.view endEditing:YES];
|
||||
break;
|
||||
}
|
||||
[self startAnimatingPlacePage:self initialVelocity:self.springAnimation.velocity];
|
||||
}
|
||||
|
||||
- (void)willStartEditingBookmarkTitle:(CGFloat)keyboardHeight
|
||||
|
@ -231,9 +249,35 @@ typedef NS_ENUM(NSUInteger, MWMiPhonePortraitPlacePageState)
|
|||
- (void)willFinishEditingBookmarkTitle:(CGFloat)keyboardHeight
|
||||
{
|
||||
self.keyboardHeight = 0.;
|
||||
//Just setup current state.
|
||||
MWMiPhonePortraitPlacePageState currentState = self.state;
|
||||
self.state = currentState;
|
||||
[self updateTargetPoint];
|
||||
}
|
||||
|
||||
#pragma mark - Properties
|
||||
|
||||
- (void)setTargetPoint:(CGPoint)targetPoint
|
||||
{
|
||||
if (CGPointEqualToPoint(_targetPoint, targetPoint))
|
||||
return;
|
||||
_targetPoint = targetPoint;
|
||||
__weak MWMiPhonePortraitPlacePage * weakSelf = self;
|
||||
[self startAnimatingPlacePage:self initialVelocity:CGPointMake(0.0, self.panVelocity) completion:^
|
||||
{
|
||||
__strong MWMiPhonePortraitPlacePage * self = weakSelf;
|
||||
if (self.state == MWMiPhonePortraitPlacePageStateClosed)
|
||||
{
|
||||
[MWMPlacePageNavigationBar remove];
|
||||
self.keyboardHeight = 0.;
|
||||
[super dismiss];
|
||||
}
|
||||
else
|
||||
{
|
||||
if (self.extendedPlacePageView.minY <= 0.0)
|
||||
[MWMPlacePageNavigationBar showNavigationBarForPlacePage:self];
|
||||
else
|
||||
[MWMPlacePageNavigationBar dismissNavigationBar];
|
||||
}
|
||||
}];
|
||||
self.panVelocity = 0.0;
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
Loading…
Add table
Reference in a new issue