Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
W
weihan-plugin
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Wei Han
weihan-plugin
Commits
ae46b699
Commit
ae46b699
authored
Jan 20, 2026
by
Wei Han
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
code update
tutorial page, no proper image yet
parent
4ee212f0
Show whitespace changes
Inline
Side-by-side
Showing
28 changed files
with
1073 additions
and
204 deletions
+1073
-204
Contents.json
...ages.xcassets/img_tutorial_ai01_3x.imageset/Contents.json
+21
-0
img_tutorial_ai01_3x.png
...ts/img_tutorial_ai01_3x.imageset/img_tutorial_ai01_3x.png
+0
-0
Contents.json
...ages.xcassets/img_tutorial_ai02_3x.imageset/Contents.json
+21
-0
img_tutorial_ai02_3x.png
...ts/img_tutorial_ai02_3x.imageset/img_tutorial_ai02_3x.png
+0
-0
Contents.json
...ages.xcassets/img_tutorial_ai03_3x.imageset/Contents.json
+21
-0
img_tutorial_ai03_3x.png
...ts/img_tutorial_ai03_3x.imageset/img_tutorial_ai03_3x.png
+0
-0
Contents.json
...ages.xcassets/img_tutorial_ma01_3x.imageset/Contents.json
+21
-0
img_tutorial_ma01_3x.png
...ts/img_tutorial_ma01_3x.imageset/img_tutorial_ma01_3x.png
+0
-0
Contents.json
...ages.xcassets/img_tutorial_ma02_3x.imageset/Contents.json
+21
-0
img_tutorial_ma02_3x.png
...ts/img_tutorial_ma02_3x.imageset/img_tutorial_ma02_3x.png
+0
-0
Contents.json
...ages.xcassets/img_tutorial_ma03_3x.imageset/Contents.json
+21
-0
img_tutorial_ma03_3x.png
...ts/img_tutorial_ma03_3x.imageset/img_tutorial_ma03_3x.png
+0
-0
AppointmentTutorialViewController.mm
...ramework/Appointment/AppointmentTutorialViewController.mm
+212
-13
AppointmentViewController.mm
...sPluginFramework/Appointment/AppointmentViewController.mm
+28
-49
ProjectViewController.mm
...ork/QmsPluginFramework/Dashboard/ProjectViewController.mm
+23
-23
PlanViewController.mm
...k/QmsPluginFramework/Issue/AddIssue/PlanViewController.mm
+2
-1
IssueTutorialViewController.mm
...k/QmsPluginFramework/Issue/IssueTutorialViewController.mm
+0
-115
IssuesTutorialViewController.h
...k/QmsPluginFramework/Issue/IssuesTutorialViewController.h
+0
-0
IssuesTutorialViewController.mm
.../QmsPluginFramework/Issue/IssuesTutorialViewController.mm
+314
-0
SyncPendingViewController.mm
...work/QmsPluginFramework/Sync/SyncPendingViewController.mm
+0
-3
ThirdDetailsViewController.h
...msPluginFramework/ThirdParty/ThirdDetailsViewController.h
+11
-0
ThirdDetailsViewController.mm
...sPluginFramework/ThirdParty/ThirdDetailsViewController.mm
+10
-0
ThirdPartyViewController.h
.../QmsPluginFramework/ThirdParty/ThirdPartyViewController.h
+12
-0
ThirdPartyViewController.mm
...QmsPluginFramework/ThirdParty/ThirdPartyViewController.mm
+93
-0
TutorialBubbleView.h
...inFramework/Utilities/TutorialButton/TutorialBubbleView.h
+26
-0
TutorialBubbleView.mm
...nFramework/Utilities/TutorialButton/TutorialBubbleView.mm
+202
-0
TutorialStep.h
...msPluginFramework/Utilities/TutorialButton/TutorialStep.h
+9
-0
TutorialStep.mm
...sPluginFramework/Utilities/TutorialButton/TutorialStep.mm
+5
-0
No files found.
framework/QmsPluginFramework/Images.xcassets/img_tutorial_ai01_3x.imageset/Contents.json
0 → 100644
View file @
ae46b699
{
"images"
:
[
{
"filename"
:
"img_tutorial_ai01_3x.png"
,
"idiom"
:
"universal"
,
"scale"
:
"1x"
},
{
"idiom"
:
"universal"
,
"scale"
:
"2x"
},
{
"idiom"
:
"universal"
,
"scale"
:
"3x"
}
],
"info"
:
{
"author"
:
"xcode"
,
"version"
:
1
}
}
framework/QmsPluginFramework/Images.xcassets/img_tutorial_ai01_3x.imageset/img_tutorial_ai01_3x.png
0 → 100644
View file @
ae46b699
315 KB
framework/QmsPluginFramework/Images.xcassets/img_tutorial_ai02_3x.imageset/Contents.json
0 → 100644
View file @
ae46b699
{
"images"
:
[
{
"filename"
:
"img_tutorial_ai02_3x.png"
,
"idiom"
:
"universal"
,
"scale"
:
"1x"
},
{
"idiom"
:
"universal"
,
"scale"
:
"2x"
},
{
"idiom"
:
"universal"
,
"scale"
:
"3x"
}
],
"info"
:
{
"author"
:
"xcode"
,
"version"
:
1
}
}
framework/QmsPluginFramework/Images.xcassets/img_tutorial_ai02_3x.imageset/img_tutorial_ai02_3x.png
0 → 100644
View file @
ae46b699
323 KB
framework/QmsPluginFramework/Images.xcassets/img_tutorial_ai03_3x.imageset/Contents.json
0 → 100644
View file @
ae46b699
{
"images"
:
[
{
"filename"
:
"img_tutorial_ai03_3x.png"
,
"idiom"
:
"universal"
,
"scale"
:
"1x"
},
{
"idiom"
:
"universal"
,
"scale"
:
"2x"
},
{
"idiom"
:
"universal"
,
"scale"
:
"3x"
}
],
"info"
:
{
"author"
:
"xcode"
,
"version"
:
1
}
}
framework/QmsPluginFramework/Images.xcassets/img_tutorial_ai03_3x.imageset/img_tutorial_ai03_3x.png
0 → 100644
View file @
ae46b699
311 KB
framework/QmsPluginFramework/Images.xcassets/img_tutorial_ma01_3x.imageset/Contents.json
0 → 100644
View file @
ae46b699
{
"images"
:
[
{
"filename"
:
"img_tutorial_ma01_3x.png"
,
"idiom"
:
"universal"
,
"scale"
:
"1x"
},
{
"idiom"
:
"universal"
,
"scale"
:
"2x"
},
{
"idiom"
:
"universal"
,
"scale"
:
"3x"
}
],
"info"
:
{
"author"
:
"xcode"
,
"version"
:
1
}
}
framework/QmsPluginFramework/Images.xcassets/img_tutorial_ma01_3x.imageset/img_tutorial_ma01_3x.png
0 → 100644
View file @
ae46b699
314 KB
framework/QmsPluginFramework/Images.xcassets/img_tutorial_ma02_3x.imageset/Contents.json
0 → 100644
View file @
ae46b699
{
"images"
:
[
{
"filename"
:
"img_tutorial_ma02_3x.png"
,
"idiom"
:
"universal"
,
"scale"
:
"1x"
},
{
"idiom"
:
"universal"
,
"scale"
:
"2x"
},
{
"idiom"
:
"universal"
,
"scale"
:
"3x"
}
],
"info"
:
{
"author"
:
"xcode"
,
"version"
:
1
}
}
framework/QmsPluginFramework/Images.xcassets/img_tutorial_ma02_3x.imageset/img_tutorial_ma02_3x.png
0 → 100644
View file @
ae46b699
204 KB
framework/QmsPluginFramework/Images.xcassets/img_tutorial_ma03_3x.imageset/Contents.json
0 → 100644
View file @
ae46b699
{
"images"
:
[
{
"filename"
:
"img_tutorial_ma03_3x.png"
,
"idiom"
:
"universal"
,
"scale"
:
"1x"
},
{
"idiom"
:
"universal"
,
"scale"
:
"2x"
},
{
"idiom"
:
"universal"
,
"scale"
:
"3x"
}
],
"info"
:
{
"author"
:
"xcode"
,
"version"
:
1
}
}
framework/QmsPluginFramework/Images.xcassets/img_tutorial_ma03_3x.imageset/img_tutorial_ma03_3x.png
0 → 100644
View file @
ae46b699
292 KB
framework/QmsPluginFramework/QmsPluginFramework/Appointment/AppointmentTutorialViewController.mm
View file @
ae46b699
// AppointmentTutorialViewController.mm
#import "AppointmentTutorialViewController.h"
#import "TutorialBubbleView.h"
#import "TutorialStep.h"
@interface AppointmentTutorialViewController ()
...
...
@@ -8,6 +10,12 @@
@property (nonatomic, strong) UILabel *titleLabel;
@property (nonatomic, strong) UIButton *skipButton;
@property (nonatomic, assign) NSInteger currentPage;
@property (nonatomic, strong) UIView *contentView;
@property (nonatomic, strong) UIImageView *tutorialImageView;
@property (nonatomic, strong) NSArray<UIImage *> *tutorialImages;
@property (nonatomic, strong) NSArray<TutorialStep *> *steps;
@property (nonatomic, strong) TutorialBubbleView *bubbleView;
@property (nonatomic, assign) BOOL didLayoutInitialPage;
@end
...
...
@@ -16,8 +24,27 @@
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = UIColor.systemBackgroundColor;
self.didLayoutInitialPage = NO;
[self setupHeader];
[self setupContent];
[self setupTutorialData];
[self setupBubble];
[self.view bringSubviewToFront:self.headerView];
}
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
if (!self.didLayoutInitialPage) {
self.didLayoutInitialPage = YES;
// showPage now that layout is ready
[self showPage:self.currentPage];
} else {
// update bubble position for rotation / layout changes
[self updateBubblePosition];
}
}
#pragma mark - Header Setup
...
...
@@ -38,7 +65,7 @@
[self.headerView addSubview:self.backButton];
self.titleLabel = [[UILabel alloc] init];
self.titleLabel.text = @"Add App
o
intment Tutorial";
self.titleLabel.text = @"Add Appintment Tutorial";
self.titleLabel.textColor = UIColor.whiteColor;
self.titleLabel.font = [UIFont boldSystemFontOfSize:18];
self.titleLabel.translatesAutoresizingMaskIntoConstraints = NO;
...
...
@@ -47,6 +74,9 @@
self.skipButton = [UIButton buttonWithType:UIButtonTypeSystem];
[self.skipButton setTitle:@"Skip" forState:UIControlStateNormal];
self.skipButton.tintColor = UIColor.whiteColor;
self.skipButton.layer.borderColor = UIColor.whiteColor.CGColor;
self.skipButton.layer.borderWidth = 0.5;
self.skipButton.layer.cornerRadius = 14;
self.skipButton.titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightMedium];
[self.skipButton addTarget:self
action:@selector(onSkip)
...
...
@@ -61,12 +91,12 @@
[self.headerView.topAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor],
[self.headerView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
[self.headerView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
[self.headerView.heightAnchor constraintEqualToConstant:4
8
],
[self.headerView.heightAnchor constraintEqualToConstant:4
4
],
[self.backButton.leadingAnchor constraintEqualToAnchor:self.headerView.leadingAnchor constant:
8
],
[self.backButton.leadingAnchor constraintEqualToAnchor:self.headerView.leadingAnchor constant:
16
],
[self.backButton.centerYAnchor constraintEqualToAnchor:self.headerView.centerYAnchor],
[self.backButton.widthAnchor constraintEqualToConstant:
32
],
[self.backButton.heightAnchor constraintEqualToConstant:
32
],
[self.backButton.widthAnchor constraintEqualToConstant:
24
],
[self.backButton.heightAnchor constraintEqualToConstant:
24
],
[self.skipButton.trailingAnchor constraintEqualToAnchor:self.headerView.trailingAnchor constant:-16],
[self.skipButton.centerYAnchor constraintEqualToAnchor:self.headerView.centerYAnchor],
...
...
@@ -76,9 +106,97 @@
]];
}
#pragma mark - Content
- (void)setupContent {
self.contentView = [[UIView alloc] init];
self.contentView.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:self.contentView];
self.tutorialImageView = [[UIImageView alloc] init];
self.tutorialImageView.translatesAutoresizingMaskIntoConstraints = NO;
self.tutorialImageView.contentMode = UIViewContentModeScaleAspectFill;
self.tutorialImageView.clipsToBounds = YES;
[self.contentView addSubview:self.tutorialImageView];
[NSLayoutConstraint activateConstraints:@[
[self.contentView.topAnchor constraintEqualToAnchor:self.headerView.bottomAnchor],
[self.contentView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
[self.contentView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
[self.contentView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor],
[self.tutorialImageView.topAnchor constraintEqualToAnchor:self.contentView.topAnchor],
[self.tutorialImageView.leadingAnchor constraintEqualToAnchor:self.contentView.leadingAnchor],
[self.tutorialImageView.trailingAnchor constraintEqualToAnchor:self.contentView.trailingAnchor],
[self.tutorialImageView.bottomAnchor constraintEqualToAnchor:self.contentView.bottomAnchor],
]];
}
#pragma mark - Tutorial Data
- (void)setupTutorialData {
// Using black placeholder images for testing
NSBundle *bundle = [NSBundle bundleForClass:[self class]];
UIImage *img1 = [UIImage imageNamed:@"img_tutorial_ma01_3x"
inBundle:bundle
compatibleWithTraitCollection:nil];
UIImage *img2 = [UIImage imageNamed:@"img_tutorial_ma02_3x"
inBundle:bundle
compatibleWithTraitCollection:nil];
UIImage *img3 = [UIImage imageNamed:@"img_tutorial_ma03_3x"
inBundle:bundle
compatibleWithTraitCollection:nil];
self.tutorialImages = @[img1, img2, img3];
self.steps = @[
({
TutorialStep *s = [TutorialStep new];
s.text = @"Select appointment date.";
s.anchorPoint = CGPointMake(0.63, 0.5);
s.bubbleAbove = NO;
s;
}),
({
TutorialStep *s = [TutorialStep new];
s.text = @"Select appointment time slot.";
s.anchorPoint = CGPointMake(0.4, 0.32);
s.bubbleAbove = NO;
s;
}),
({
TutorialStep *s = [TutorialStep new];
s.text = @"Select \"Confirm\" button to confirm appointment details.";
s.anchorPoint = CGPointMake(0.6, 0.86);
s.bubbleAbove = YES;
s;
})
];
self.currentPage = 0;
}
#pragma mark - Bubble
- (void)setupBubble {
self.bubbleView = [[TutorialBubbleView alloc] init];
self.bubbleView.translatesAutoresizingMaskIntoConstraints = NO;
[self.bubbleView.prevButton addTarget:self
action:@selector(backButtonTapped)
forControlEvents:UIControlEventTouchUpInside];
[self.bubbleView.nextButton addTarget:self
action:@selector(nextButtonTapped)
forControlEvents:UIControlEventTouchUpInside];
[self.contentView addSubview:self.bubbleView];
// Fixed width; height will auto-size
[self.bubbleView.widthAnchor constraintEqualToConstant:260].active = YES;
}
#pragma mark - Helper
- (void)onBack {
[self.navigationController popViewControllerAnimated:YES];
NSLog(@"on back triggered");
[self dismissViewControllerAnimated:YES completion:nil];
}
-(void)onSkip {
...
...
@@ -90,14 +208,25 @@
}
-(void)updateHeaderButtons {
self.skipButton.hidden = (self.currentPage == 2);
self.skipButton.hidden = (self.currentPage == self.steps.count - 1);
}
- (void)showPage:(NSInteger)page {
if (page < 0 || page >= self.tutorialImages.count) return;
self.tutorialImageView.image = self.tutorialImages[page];
[self updateHeaderButtons];
[self updateBubbleContent];
dispatch_async(dispatch_get_main_queue(), ^{
[self updateBubblePosition];
});
}
- (void)nextButtonTapped {
if (self.currentPage <
2
) {
if (self.currentPage <
self.steps.count - 1
) {
self.currentPage++;
// [self showPage:self.currentPage];
[self updateHeaderButtons];
[self showPage:self.currentPage];
} else {
[self completeTutorial];
}
...
...
@@ -106,11 +235,81 @@
- (void)backButtonTapped {
if (self.currentPage > 0) {
self.currentPage--;
// [self showPage:self.currentPage];
[self updateHeaderButtons];
[self showPage:self.currentPage];
}
}
- (CGRect)imageFrameInImageView:(UIImageView *)imageView {
if (!imageView.image) return CGRectZero;
@end
CGSize imageSize = imageView.image.size;
CGSize viewSize = imageView.bounds.size;
CGFloat imageRatio = imageSize.width / imageSize.height;
CGFloat viewRatio = viewSize.width / viewSize.height;
CGSize drawSize;
if (imageRatio > viewRatio) {
// Image is wider → height fills, width overflows
drawSize.height = viewSize.height;
drawSize.width = viewSize.height * imageRatio;
} else {
// Image is taller → width fills, height overflows
drawSize.width = viewSize.width;
drawSize.height = viewSize.width / imageRatio;
}
CGFloat x = (viewSize.width - drawSize.width) / 2.0;
CGFloat y = (viewSize.height - drawSize.height) / 2.0;
return CGRectMake(x, y, drawSize.width, drawSize.height);
}
- (CGPoint)screenPointForCurrentStep {
TutorialStep *step = self.steps[self.currentPage];
CGRect imageFrame = [self imageFrameInImageView:self.tutorialImageView];
CGFloat x = imageFrame.origin.x + step.anchorPoint.x * imageFrame.size.width;
CGFloat y = imageFrame.origin.y + step.anchorPoint.y * imageFrame.size.height;
return CGPointMake(x, y);
}
- (void)updateBubblePosition {
if (!self.bubbleView || !self.tutorialImageView.image) return;
// Make sure bubble layout is up-to-date
[self.bubbleView layoutIfNeeded];
TutorialStep *step = self.steps[self.currentPage];
CGPoint anchor = [self screenPointForCurrentStep];
self.bubbleView.arrowDirection =
step.bubbleAbove ? BubbleArrowDirectionDown : BubbleArrowDirectionUp;
CGFloat bubbleHeight =
[self.bubbleView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
CGFloat bubbleWidth = 260;
CGFloat x = anchor.x - bubbleWidth / 2.0;
CGFloat y = step.bubbleAbove
? anchor.y - bubbleHeight - 12
: anchor.y + 12;
self.bubbleView.frame = CGRectMake(x, y, bubbleWidth, bubbleHeight);
}
- (void)updateBubbleContent {
TutorialStep *step = self.steps[self.currentPage];
[self.bubbleView configureWithText:step.text
step:self.currentPage
total:self.steps.count
isFirst:self.currentPage == 0
isLast:self.currentPage == self.steps.count - 1];
self.bubbleView.prevButton.hidden = (self.currentPage == 0);
}
@end
framework/QmsPluginFramework/QmsPluginFramework/Appointment/AppointmentViewController.mm
View file @
ae46b699
...
...
@@ -8,6 +8,7 @@
#import "NSDate+CalendarHelpers.h"
#import "AppointmentTimeSlotViewController.h"
#import "PullToRefresh.h"
#import "AppointmentTutorialViewController.h"
@interface AppointmentViewController ()
<WKNavigationDelegate
,
WKUIDelegate
,
UIScrollViewDelegate
,
CalendarViewDelegate
>
...
...
@@ -50,11 +51,6 @@
@property (nonatomic, strong) UIButton *backFooterButton;
@property (nonatomic, strong) UIButton *nextFooterButton;
// Tutorial overlay
@property (nonatomic, strong) UIView *tutorialOverlay;
@property (nonatomic, strong) UIImageView *tutorialImageView;
@property (nonatomic, strong) UIButton *skipButton;
@end
@implementation AppointmentViewController
...
...
@@ -122,8 +118,6 @@
[contentStack.bottomAnchor constraintEqualToAnchor:self.contentView.bottomAnchor constant:-16]
]];
// Tutorial overlay added last so it’s on top
[self setupTutorialOverlay];
[self setupTermsOverlay];
// Pull-to-refresh
...
...
@@ -181,6 +175,23 @@
[self.titleLabel.centerXAnchor constraintEqualToAnchor:self.headerBar.centerXAnchor],
[self.titleLabel.centerYAnchor constraintEqualToAnchor:self.headerBar.centerYAnchor],
]];
self.helpButton = [UIButton buttonWithType:UIButtonTypeSystem];
self.helpButton.translatesAutoresizingMaskIntoConstraints = NO;
[self.helpButton setImage:[UIImage systemImageNamed:@"questionmark.circle"] forState:UIControlStateNormal];
self.helpButton.tintColor = UIColor.whiteColor;
[self.helpButton addTarget:self
action:@selector(showTutorial)
forControlEvents:UIControlEventTouchUpInside];
[self.headerBar addSubview:self.helpButton];
[NSLayoutConstraint activateConstraints:@[
[self.helpButton.trailingAnchor constraintEqualToAnchor:self.headerBar.trailingAnchor constant:-16],
[self.helpButton.centerYAnchor constraintEqualToAnchor:self.backButton.centerYAnchor],
[self.helpButton.widthAnchor constraintEqualToConstant:24],
[self.helpButton.heightAnchor constraintEqualToConstant:24],
]];
}
#pragma mark = Overlay t
&
c
...
...
@@ -422,48 +433,6 @@
]];
}
#pragma mark - Tutorial Overlay
- (void)setupTutorialOverlay {
self.tutorialOverlay = [[UIView alloc] init];
self.tutorialOverlay.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.6];
self.tutorialOverlay.hidden = YES;
self.tutorialOverlay.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:self.tutorialOverlay];
[NSLayoutConstraint activateConstraints:@[
[self.tutorialOverlay.topAnchor constraintEqualToAnchor:self.view.topAnchor],
[self.tutorialOverlay.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
[self.tutorialOverlay.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
[self.tutorialOverlay.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor]
]];
self.tutorialImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"tutorial_step1"]];
self.tutorialImageView.contentMode = UIViewContentModeScaleAspectFit;
self.tutorialImageView.translatesAutoresizingMaskIntoConstraints = NO;
[self.tutorialOverlay addSubview:self.tutorialImageView];
self.skipButton = [UIButton buttonWithType:UIButtonTypeSystem];
[self.skipButton setTitle:@"Skip" forState:UIControlStateNormal];
self.skipButton.tintColor = UIColor.whiteColor;
self.skipButton.layer.borderColor = UIColor.whiteColor.CGColor;
self.skipButton.layer.borderWidth = 1.0;
self.skipButton.layer.cornerRadius = 6.0;
self.skipButton.translatesAutoresizingMaskIntoConstraints = NO;
[self.tutorialOverlay addSubview:self.skipButton];
[NSLayoutConstraint activateConstraints:@[
[self.tutorialImageView.centerXAnchor constraintEqualToAnchor:self.tutorialOverlay.centerXAnchor],
[self.tutorialImageView.centerYAnchor constraintEqualToAnchor:self.tutorialOverlay.centerYAnchor],
[self.tutorialImageView.widthAnchor constraintEqualToAnchor:self.tutorialOverlay.widthAnchor multiplier:0.9],
[self.tutorialImageView.heightAnchor constraintEqualToAnchor:self.tutorialOverlay.heightAnchor multiplier:0.7],
[self.skipButton.topAnchor constraintEqualToAnchor:self.tutorialOverlay.safeAreaLayoutGuide.topAnchor constant:16],
[self.skipButton.trailingAnchor constraintEqualToAnchor:self.tutorialOverlay.trailingAnchor constant:-16],
[self.skipButton.widthAnchor constraintEqualToConstant:70],
[self.skipButton.heightAnchor constraintEqualToConstant:32]
]];
}
#pragma mark - api
- (void)requestAppointment {
[APIClient requestAppointment:^(BOOL success, NSDictionary *data, NSError *error) {
...
...
@@ -758,4 +727,14 @@
});
}
- (void)showTutorial {
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Help"
message:@"This would show the tutorial (e.g. step1/step2/step3)."
preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]];
AppointmentTutorialViewController *vc = [[AppointmentTutorialViewController alloc] init];
vc.modalPresentationStyle = UIModalPresentationFullScreen;
[self presentViewController:vc animated:YES completion:nil];
}
@end
framework/QmsPluginFramework/QmsPluginFramework/Dashboard/ProjectViewController.mm
View file @
ae46b699
...
...
@@ -111,29 +111,29 @@
}
- (void)checkAndRefreshAPIs {
NSLog(@"🔍 Checking credentials...");
NSLog(@"ClientID: %@", self.ClientID);
NSLog(@"ClientCode: %@", self.ClientCode);
NSLog(@"user_token: %@", self.user_token);
[self showLoadingOverlay:@"Loading..."];
if (![self isReadyToLoadDashboard]) {
NSLog(@"⏳ Waiting for all credentials to be ready...");
return;
}
if (_isLoadingDashboard) return;
self.isLoadingDashboard = YES;
NSLog(@"✅ All credentials ready. ClientID: %@, ClientCode: %@, user_token: %@",
self.ClientID, self.ClientCode, self.user_token);
[APIConfig setAuthTokem:self.user_token];
[APIConfig setClientId:self.ClientID];
//
self.ClientCode = @"spt";
//
NSLog(@"🔍 Checking credentials...");
//
NSLog(@"ClientID: %@", self.ClientID);
//
NSLog(@"ClientCode: %@", self.ClientCode);
//
NSLog(@"user_token: %@", self.user_token);
//
//
[self showLoadingOverlay:@"Loading..."];
//
//
if (![self isReadyToLoadDashboard]) {
//
NSLog(@"⏳ Waiting for all credentials to be ready...");
//
return;
//
}
//
//
if (_isLoadingDashboard) return;
//
//
self.isLoadingDashboard = YES;
//
//
NSLog(@"✅ All credentials ready. ClientID: %@, ClientCode: %@, user_token: %@",
//
self.ClientID, self.ClientCode, self.user_token);
//
//
//
[APIConfig setAuthTokem:self.user_token];
//
[APIConfig setClientId:self.ClientID];
self.ClientCode = @"spt";
[self requestCompanyCode];
}
...
...
framework/QmsPluginFramework/QmsPluginFramework/Issue/AddIssue/PlanViewController.mm
View file @
ae46b699
...
...
@@ -3,7 +3,7 @@
#import "DetailsViewController.h"
#import "APIClient.h"
#import <AVFoundation/AVFoundation.h>
#import "IssueTutorialViewController.h"
#import "Issue
s
TutorialViewController.h"
#import "ImageCacheHelper.h"
@interface PlanViewController ()
...
...
@@ -375,6 +375,7 @@
forControlEvents:UIControlEventTouchUpInside];
[self.headerView addSubview:self.helpButton];
// 🧩 Title label (project name) — now outside the header
self.titleLabel = [[UILabel alloc] init];
self.titleLabel.translatesAutoresizingMaskIntoConstraints = NO;
...
...
framework/QmsPluginFramework/QmsPluginFramework/Issue/IssueTutorialViewController.mm
deleted
100644 → 0
View file @
4ee212f0
// IssueTutorialViewController.mm
#import "IssueTutorialViewController.h"
@interface IssuesTutorialViewController ()
@property (nonatomic, strong) UIView *headerView;
@property (nonatomic, strong) UIButton *backButton;
@property (nonatomic, strong) UILabel *titleLabel;
@property (nonatomic, strong) UIButton *skipButton;
@property (nonatomic, assign) NSInteger currentPage;
@end
@implementation IssuesTutorialViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = UIColor.systemBackgroundColor;
[self setupHeader];
}
#pragma mark - Header Setup
- (void)setupHeader {
self.headerView = [[UIView alloc] init];
self.headerView.backgroundColor = [UIColor colorWithRed:0.0 green:0.5 blue:0.6 alpha:1.0];
self.headerView.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:self.headerView];
self.backButton = [UIButton buttonWithType:UIButtonTypeCustom];
[self.backButton setImage:[UIImage systemImageNamed:@"chevron.backward"]
forState:UIControlStateNormal];
self.backButton.tintColor = UIColor.whiteColor;
[self.backButton addTarget:self
action:@selector(onBack)
forControlEvents:UIControlEventTouchUpInside];
self.backButton.translatesAutoresizingMaskIntoConstraints = NO;
[self.headerView addSubview:self.backButton];
self.titleLabel = [[UILabel alloc] init];
self.titleLabel.text = @"Add Issue Tutorial";
self.titleLabel.textColor = UIColor.whiteColor;
self.titleLabel.font = [UIFont boldSystemFontOfSize:18];
self.titleLabel.translatesAutoresizingMaskIntoConstraints = NO;
[self.headerView addSubview:self.titleLabel];
self.skipButton = [UIButton buttonWithType:UIButtonTypeSystem];
[self.skipButton setTitle:@"Skip" forState:UIControlStateNormal];
self.skipButton.tintColor = UIColor.whiteColor;
self.skipButton.titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightMedium];
[self.skipButton addTarget:self
action:@selector(onSkip)
forControlEvents:UIControlEventTouchUpInside];
self.skipButton.translatesAutoresizingMaskIntoConstraints = NO;
[self.headerView addSubview:self.skipButton];
// Initially visible on first screen
self.skipButton.hidden = NO;
[NSLayoutConstraint activateConstraints:@[
[self.headerView.topAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor],
[self.headerView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
[self.headerView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
[self.headerView.heightAnchor constraintEqualToConstant:48],
[self.backButton.leadingAnchor constraintEqualToAnchor:self.headerView.leadingAnchor constant:8],
[self.backButton.centerYAnchor constraintEqualToAnchor:self.headerView.centerYAnchor],
[self.backButton.widthAnchor constraintEqualToConstant:32],
[self.backButton.heightAnchor constraintEqualToConstant:32],
[self.skipButton.trailingAnchor constraintEqualToAnchor:self.headerView.trailingAnchor constant:-16],
[self.skipButton.centerYAnchor constraintEqualToAnchor:self.headerView.centerYAnchor],
[self.titleLabel.centerXAnchor constraintEqualToAnchor:self.headerView.centerXAnchor],
[self.titleLabel.centerYAnchor constraintEqualToAnchor:self.headerView.centerYAnchor],
]];
}
#pragma mark - Helper
- (void)onBack {
[self.navigationController popViewControllerAnimated:YES];
}
-(void)onSkip {
[self completeTutorial];
}
-(void)completeTutorial {
[self onBack];
}
-(void)updateHeaderButtons {
self.skipButton.hidden = (self.currentPage == 2);
}
- (void)nextButtonTapped {
if (self.currentPage < 2) {
self.currentPage++;
// [self showPage:self.currentPage];
[self updateHeaderButtons];
} else {
[self completeTutorial];
}
}
- (void)backButtonTapped {
if (self.currentPage > 0) {
self.currentPage--;
// [self showPage:self.currentPage];
[self updateHeaderButtons];
}
}
@end
framework/QmsPluginFramework/QmsPluginFramework/Issue/IssueTutorialViewController.h
→
framework/QmsPluginFramework/QmsPluginFramework/Issue/Issue
s
TutorialViewController.h
View file @
ae46b699
File moved
framework/QmsPluginFramework/QmsPluginFramework/Issue/IssuesTutorialViewController.mm
0 → 100644
View file @
ae46b699
// IssueTutorialViewController.mm
#import "IssuesTutorialViewController.h"
#import "TutorialBubbleView.h"
#import "TutorialStep.h"
@interface IssuesTutorialViewController ()
@property (nonatomic, strong) UIView *headerView;
@property (nonatomic, strong) UIButton *backButton;
@property (nonatomic, strong) UILabel *titleLabel;
@property (nonatomic, strong) UIButton *skipButton;
@property (nonatomic, assign) NSInteger currentPage;
@property (nonatomic, strong) UIView *contentView;
@property (nonatomic, strong) UIImageView *tutorialImageView;
@property (nonatomic, strong) NSArray<UIImage *> *tutorialImages;
@property (nonatomic, strong) NSArray<TutorialStep *> *steps;
@property (nonatomic, strong) TutorialBubbleView *bubbleView;
@property (nonatomic, assign) BOOL didLayoutInitialPage;
@end
@implementation IssuesTutorialViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = UIColor.systemBackgroundColor;
self.didLayoutInitialPage = NO;
[self setupHeader];
[self setupContent];
[self setupTutorialData];
[self setupBubble];
[self.view bringSubviewToFront:self.headerView];
}
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
if (!self.didLayoutInitialPage) {
self.didLayoutInitialPage = YES;
// showPage now that layout is ready
[self showPage:self.currentPage];
} else {
// update bubble position for rotation / layout changes
[self updateBubblePosition];
}
}
#pragma mark - Header Setup
- (void)setupHeader {
self.headerView = [[UIView alloc] init];
self.headerView.backgroundColor = [UIColor colorWithRed:0.0 green:0.5 blue:0.6 alpha:1.0];
self.headerView.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:self.headerView];
self.backButton = [UIButton buttonWithType:UIButtonTypeCustom];
[self.backButton setImage:[UIImage systemImageNamed:@"chevron.backward"]
forState:UIControlStateNormal];
self.backButton.tintColor = UIColor.whiteColor;
[self.backButton addTarget:self
action:@selector(onBack)
forControlEvents:UIControlEventTouchUpInside];
self.backButton.translatesAutoresizingMaskIntoConstraints = NO;
[self.headerView addSubview:self.backButton];
self.titleLabel = [[UILabel alloc] init];
self.titleLabel.text = @"Add Issue Tutorial";
self.titleLabel.textColor = UIColor.whiteColor;
self.titleLabel.font = [UIFont boldSystemFontOfSize:18];
self.titleLabel.translatesAutoresizingMaskIntoConstraints = NO;
[self.headerView addSubview:self.titleLabel];
self.skipButton = [UIButton buttonWithType:UIButtonTypeSystem];
[self.skipButton setTitle:@"Skip" forState:UIControlStateNormal];
self.skipButton.tintColor = UIColor.whiteColor;
self.skipButton.layer.borderColor = UIColor.whiteColor.CGColor;
self.skipButton.layer.borderWidth = 0.5;
self.skipButton.layer.cornerRadius = 14;
self.skipButton.titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightMedium];
[self.skipButton addTarget:self
action:@selector(onSkip)
forControlEvents:UIControlEventTouchUpInside];
self.skipButton.translatesAutoresizingMaskIntoConstraints = NO;
[self.headerView addSubview:self.skipButton];
// Initially visible on first screen
self.skipButton.hidden = NO;
[NSLayoutConstraint activateConstraints:@[
[self.headerView.topAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor],
[self.headerView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
[self.headerView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
[self.headerView.heightAnchor constraintEqualToConstant:44],
[self.backButton.leadingAnchor constraintEqualToAnchor:self.headerView.leadingAnchor constant:16],
[self.backButton.centerYAnchor constraintEqualToAnchor:self.headerView.centerYAnchor],
[self.backButton.widthAnchor constraintEqualToConstant:24],
[self.backButton.heightAnchor constraintEqualToConstant:24],
[self.skipButton.trailingAnchor constraintEqualToAnchor:self.headerView.trailingAnchor constant:-16],
[self.skipButton.centerYAnchor constraintEqualToAnchor:self.headerView.centerYAnchor],
[self.titleLabel.centerXAnchor constraintEqualToAnchor:self.headerView.centerXAnchor],
[self.titleLabel.centerYAnchor constraintEqualToAnchor:self.headerView.centerYAnchor],
]];
}
#pragma mark - Content
- (void)setupContent {
self.contentView = [[UIView alloc] init];
self.contentView.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:self.contentView];
self.tutorialImageView = [[UIImageView alloc] init];
self.tutorialImageView.translatesAutoresizingMaskIntoConstraints = NO;
self.tutorialImageView.contentMode = UIViewContentModeScaleAspectFill;
self.tutorialImageView.clipsToBounds = YES;
[self.contentView addSubview:self.tutorialImageView];
[NSLayoutConstraint activateConstraints:@[
[self.contentView.topAnchor constraintEqualToAnchor:self.headerView.bottomAnchor],
[self.contentView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
[self.contentView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
[self.contentView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor],
[self.tutorialImageView.topAnchor constraintEqualToAnchor:self.contentView.topAnchor],
[self.tutorialImageView.leadingAnchor constraintEqualToAnchor:self.contentView.leadingAnchor],
[self.tutorialImageView.trailingAnchor constraintEqualToAnchor:self.contentView.trailingAnchor],
[self.tutorialImageView.bottomAnchor constraintEqualToAnchor:self.contentView.bottomAnchor],
]];
}
#pragma mark - Tutorial Data
- (void)setupTutorialData {
NSBundle *bundle = [NSBundle bundleForClass:[self class]];
UIImage *img1 = [UIImage imageNamed:@"img_tutorial_ai01_3x"
inBundle:bundle
compatibleWithTraitCollection:nil];
UIImage *img2 = [UIImage imageNamed:@"img_tutorial_ai02_3x"
inBundle:bundle
compatibleWithTraitCollection:nil];
UIImage *img3 = [UIImage imageNamed:@"img_tutorial_ai03_3x"
inBundle:bundle
compatibleWithTraitCollection:nil];
self.tutorialImages = @[img1, img2, img3];
self.steps = @[
({
TutorialStep *s = [TutorialStep new];
s.text = @"Select an area in layout plan to add issue.";
s.anchorPoint = CGPointMake(0.5, 0.49);
s.bubbleAbove = NO;
s;
}),
({
TutorialStep *s = [TutorialStep new];
s.text = @"Move pin marker within location selected.";
s.anchorPoint = CGPointMake(0.5, 0.5);
s.bubbleAbove = NO;
s;
}),
({
TutorialStep *s = [TutorialStep new];
s.text = @"To change location, you can select the \"Reselect\" button.";
s.anchorPoint = CGPointMake(0.4, 0.86);
s.bubbleAbove = YES;
s;
})
];
self.currentPage = 0;
}
#pragma mark - Bubble
- (void)setupBubble {
self.bubbleView = [[TutorialBubbleView alloc] init];
self.bubbleView.translatesAutoresizingMaskIntoConstraints = NO;
[self.bubbleView.prevButton addTarget:self
action:@selector(backButtonTapped)
forControlEvents:UIControlEventTouchUpInside];
[self.bubbleView.nextButton addTarget:self
action:@selector(nextButtonTapped)
forControlEvents:UIControlEventTouchUpInside];
[self.contentView addSubview:self.bubbleView];
// Fixed width; height will auto-size
[self.bubbleView.widthAnchor constraintEqualToConstant:260].active = YES;
}
#pragma mark - Helper
- (void)onBack {
NSLog(@"on back triggered");
[self dismissViewControllerAnimated:YES completion:nil];
}
-(void)onSkip {
[self completeTutorial];
}
-(void)completeTutorial {
[self onBack];
}
-(void)updateHeaderButtons {
self.skipButton.hidden = (self.currentPage == self.steps.count - 1);
}
- (void)showPage:(NSInteger)page {
if (page < 0 || page >= self.tutorialImages.count) return;
self.tutorialImageView.image = self.tutorialImages[page];
[self updateHeaderButtons];
[self updateBubbleContent];
dispatch_async(dispatch_get_main_queue(), ^{
[self updateBubblePosition];
});
}
- (void)nextButtonTapped {
if (self.currentPage < self.steps.count - 1) {
self.currentPage++;
[self showPage:self.currentPage];
} else {
[self completeTutorial];
}
}
- (void)backButtonTapped {
if (self.currentPage > 0) {
self.currentPage--;
[self showPage:self.currentPage];
}
}
- (CGRect)imageFrameInImageView:(UIImageView *)imageView {
if (!imageView.image) return CGRectZero;
CGSize imageSize = imageView.image.size;
CGSize viewSize = imageView.bounds.size;
CGFloat imageRatio = imageSize.width / imageSize.height;
CGFloat viewRatio = viewSize.width / viewSize.height;
CGSize drawSize;
if (imageRatio > viewRatio) {
// Image is wider → height fills, width overflows
drawSize.height = viewSize.height;
drawSize.width = viewSize.height * imageRatio;
} else {
// Image is taller → width fills, height overflows
drawSize.width = viewSize.width;
drawSize.height = viewSize.width / imageRatio;
}
CGFloat x = (viewSize.width - drawSize.width) / 2.0;
CGFloat y = (viewSize.height - drawSize.height) / 2.0;
return CGRectMake(x, y, drawSize.width, drawSize.height);
}
- (CGPoint)screenPointForCurrentStep {
TutorialStep *step = self.steps[self.currentPage];
CGRect imageFrame = [self imageFrameInImageView:self.tutorialImageView];
CGFloat x = imageFrame.origin.x + step.anchorPoint.x * imageFrame.size.width;
CGFloat y = imageFrame.origin.y + step.anchorPoint.y * imageFrame.size.height;
return CGPointMake(x, y);
}
- (void)updateBubblePosition {
if (!self.bubbleView || !self.tutorialImageView.image) return;
// Make sure bubble layout is up-to-date
[self.bubbleView layoutIfNeeded];
TutorialStep *step = self.steps[self.currentPage];
CGPoint anchor = [self screenPointForCurrentStep];
self.bubbleView.arrowDirection =
step.bubbleAbove ? BubbleArrowDirectionDown : BubbleArrowDirectionUp;
CGFloat bubbleHeight =
[self.bubbleView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
CGFloat bubbleWidth = 260;
CGFloat x = anchor.x - bubbleWidth / 2.0;
CGFloat y = step.bubbleAbove
? anchor.y - bubbleHeight - 12
: anchor.y + 12;
self.bubbleView.frame = CGRectMake(x, y, bubbleWidth, bubbleHeight);
}
- (void)updateBubbleContent {
TutorialStep *step = self.steps[self.currentPage];
[self.bubbleView configureWithText:step.text
step:self.currentPage
total:self.steps.count
isFirst:self.currentPage == 0
isLast:self.currentPage == self.steps.count - 1];
self.bubbleView.prevButton.hidden = (self.currentPage == 0);
}
@end
framework/QmsPluginFramework/QmsPluginFramework/Sync/SyncPendingViewController.mm
View file @
ae46b699
...
...
@@ -415,7 +415,4 @@ heightForHeaderInSection:(NSInteger)section {
self.view.userInteractionEnabled = !isLoading;
}
@end
framework/QmsPluginFramework/QmsPluginFramework/ThirdParty/ThirdDetailsViewController.h
0 → 100644
View file @
ae46b699
// ThirdDetailsViewController.h
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface
ThirdPartyDetailsViewController
:
UIViewController
@end
NS_ASSUME_NONNULL_END
framework/QmsPluginFramework/QmsPluginFramework/ThirdParty/ThirdDetailsViewController.mm
0 → 100644
View file @
ae46b699
// ThirdDetailsViewController.mm
#import "ThirdDetailsViewController.h"
@interface ThirdPartyDetailsViewController ()
@end
@implementation ThirdPartyDetailsViewController
@end
framework/QmsPluginFramework/QmsPluginFramework/ThirdParty/ThirdPartyViewController.h
0 → 100644
View file @
ae46b699
// ThirdPartyViewController.h
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface
ThirdPartyViewController
:
UIViewController
@end
NS_ASSUME_NONNULL_END
framework/QmsPluginFramework/QmsPluginFramework/ThirdParty/ThirdPartyViewController.mm
0 → 100644
View file @
ae46b699
// ThirdPartyViewController.mm
#import "ThirdPartyViewController.h"
@interface ThirdPartyViewController ()
@property (nonatomic, strong) UIView *headerBar;
@property (nonatomic, strong) UILabel *titleLabel;
@property (nonatomic, strong) UIButton *backButton;
@property (nonatomic, strong) UIButton *helpButton;
@end
@implementation ThirdPartyViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = UIColor.whiteColor;
[self setupHeader];
}
#pragma mark - Header
- (void)setupHeader {
UILayoutGuide *safe = self.view.safeAreaLayoutGuide;
//Header
self.headerBar = [[UIView alloc] init];
self.headerBar.backgroundColor = [UIColor colorWithRed:0.0 green:0.43 blue:0.55 alpha:1.0]; // #006D8D
[self.view addSubview:self.headerBar];
self.headerBar.translatesAutoresizingMaskIntoConstraints = NO;
[NSLayoutConstraint activateConstraints:@[
[self.headerBar.topAnchor constraintEqualToAnchor:safe.topAnchor],
[self.headerBar.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
[self.headerBar.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
[self.headerBar.heightAnchor constraintEqualToConstant:44],
]];
// Title
self.titleLabel = [[UILabel alloc] init];
self.titleLabel.text = @"Make Appointment";
self.titleLabel.textColor = UIColor.whiteColor;
self.titleLabel.font = [UIFont systemFontOfSize:18 weight:UIFontWeightMedium];
self.titleLabel.textAlignment = NSTextAlignmentCenter;
// Back button
self.backButton = [UIButton buttonWithType:UIButtonTypeSystem];
[self.backButton setImage:[UIImage systemImageNamed:@"xmark"] forState:UIControlStateNormal];
self.backButton.tintColor = UIColor.whiteColor;
[self.backButton addTarget:self
action:@selector(onBack)
forControlEvents:UIControlEventTouchUpInside];
for (UIView *sub in @[self.titleLabel, self.backButton]) {
sub.translatesAutoresizingMaskIntoConstraints = NO;
[self.headerBar addSubview:sub];
}
[NSLayoutConstraint activateConstraints:@[
[self.backButton.leadingAnchor constraintEqualToAnchor:self.headerBar.leadingAnchor constant:16],
[self.backButton.centerYAnchor constraintEqualToAnchor:self.headerBar.centerYAnchor],
[self.backButton.heightAnchor constraintEqualToConstant:24],
[self.backButton.widthAnchor constraintEqualToConstant:24],
[self.titleLabel.centerXAnchor constraintEqualToAnchor:self.headerBar.centerXAnchor],
[self.titleLabel.centerYAnchor constraintEqualToAnchor:self.headerBar.centerYAnchor],
]];
self.helpButton = [UIButton buttonWithType:UIButtonTypeSystem];
self.helpButton.translatesAutoresizingMaskIntoConstraints = NO;
[self.helpButton setImage:[UIImage systemImageNamed:@"questionmark.circle"] forState:UIControlStateNormal];
self.helpButton.tintColor = UIColor.whiteColor;
[self.helpButton addTarget:self
action:@selector(showTutorial)
forControlEvents:UIControlEventTouchUpInside];
[self.headerBar addSubview:self.helpButton];
[NSLayoutConstraint activateConstraints:@[
[self.helpButton.trailingAnchor constraintEqualToAnchor:self.headerBar.trailingAnchor constant:-16],
[self.helpButton.centerYAnchor constraintEqualToAnchor:self.backButton.centerYAnchor],
[self.helpButton.widthAnchor constraintEqualToConstant:24],
[self.helpButton.heightAnchor constraintEqualToConstant:24],
]];
}
#pragma mark - helpers
- (void)onBack {
[self.navigationController popViewControllerAnimated:YES];
}
@end
framework/QmsPluginFramework/QmsPluginFramework/Utilities/TutorialButton/TutorialBubbleView.h
0 → 100644
View file @
ae46b699
// TutorialBubbleView.h
#import <UIKit/UIKit.h>
typedef
NS_ENUM
(
NSInteger
,
BubbleArrowDirection
)
{
BubbleArrowDirectionUp
,
BubbleArrowDirectionDown
};
@interface
TutorialBubbleView
:
UIView
@property
(
nonatomic
,
strong
)
UILabel
*
textLabel
;
@property
(
nonatomic
,
strong
)
UIButton
*
prevButton
;
@property
(
nonatomic
,
strong
)
UIButton
*
nextButton
;
@property
(
nonatomic
,
assign
)
CGFloat
arrowCenterX
;
@property
(
nonatomic
,
assign
)
BubbleArrowDirection
arrowDirection
;
-
(
void
)
configureWithText
:(
NSString
*
)
text
step
:(
NSInteger
)
step
total
:(
NSInteger
)
total
isFirst
:(
BOOL
)
isFirst
isLast
:(
BOOL
)
isLast
;
@end
framework/QmsPluginFramework/QmsPluginFramework/Utilities/TutorialButton/TutorialBubbleView.mm
0 → 100644
View file @
ae46b699
// TutorialBubbleView.mm
#import "TutorialBubbleView.h"
static CGFloat const kArrowHeight = 14.0;
static CGFloat const kArrowPadding = 8.0;
@interface TutorialBubbleView ()
@property (nonatomic, strong) UILabel *stepLabel;
@property (nonatomic, strong) UILabel *messageLabel;
@property (nonatomic, strong) UIStackView *buttonStack;
@property (nonatomic, strong) CAShapeLayer *arrowLayer;
@property (nonatomic, strong) NSLayoutConstraint *topConstraint;
@property (nonatomic, strong) NSLayoutConstraint *bottomConstraint;
@end
@implementation TutorialBubbleView
- (instancetype)init {
self = [super initWithFrame:CGRectZero];
if (!self) return nil;
self.backgroundColor = UIColor.whiteColor;
self.layer.cornerRadius = 16;
self.layer.shadowColor = UIColor.blackColor.CGColor;
self.layer.shadowOpacity = 0.15;
self.layer.shadowRadius = 12;
self.layer.shadowOffset = CGSizeMake(0, 6);
self.layer.masksToBounds = NO;
self.translatesAutoresizingMaskIntoConstraints = NO;
CGFloat padding = 18;
// ───────── Step container (pill) ─────────
UIView *stepContainer = [[UIView alloc] init];
stepContainer.translatesAutoresizingMaskIntoConstraints = NO;
stepContainer.backgroundColor =
[[UIColor systemTealColor] colorWithAlphaComponent:0.15];
stepContainer.layer.cornerRadius = 10;
[self addSubview:stepContainer];
self.stepLabel = [[UILabel alloc] init];
self.stepLabel.font = [UIFont systemFontOfSize:14 weight:UIFontWeightSemibold];
self.stepLabel.textColor = UIColor.secondaryLabelColor;
self.stepLabel.translatesAutoresizingMaskIntoConstraints = NO;
self.stepLabel.textAlignment = NSTextAlignmentCenter;
[stepContainer addSubview:self.stepLabel];
// ───────── Message label ─────────
self.messageLabel = [[UILabel alloc] init];
self.messageLabel.font = [UIFont systemFontOfSize:18 weight:UIFontWeightMedium];
self.messageLabel.textColor = UIColor.labelColor;
self.messageLabel.numberOfLines = 0;
self.messageLabel.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:self.messageLabel];
UIColor *primaryColor = [UIColor colorWithRed:0.0 green:0.5 blue:0.6 alpha:1.0];
// ───────── Prev Button ─────────
self.prevButton = [UIButton buttonWithType:UIButtonTypeSystem];
[self.prevButton setTitle:@"Back" forState:UIControlStateNormal];
self.prevButton.titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightSemibold];
[self.prevButton setTitleColor:primaryColor forState:UIControlStateNormal];
self.prevButton.layer.cornerRadius = 10;
self.prevButton.layer.borderWidth = 0.5;
self.prevButton.layer.borderColor = primaryColor.CGColor;
self.prevButton.contentEdgeInsets = UIEdgeInsetsMake(8, 18, 8, 18);
self.prevButton.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:self.prevButton];
// ───────── Next Button ─────────
self.nextButton = [UIButton buttonWithType:UIButtonTypeSystem];
[self.nextButton setTitle:@"Next" forState:UIControlStateNormal];
self.nextButton.titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightSemibold];
self.nextButton.backgroundColor = primaryColor;
[self.nextButton setTitleColor:UIColor.whiteColor forState:UIControlStateNormal];
self.nextButton.layer.cornerRadius = 10;
self.nextButton.contentEdgeInsets = UIEdgeInsetsMake(8, 18, 8, 18);
self.nextButton.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:self.nextButton];
[NSLayoutConstraint activateConstraints:@[
[self.heightAnchor constraintGreaterThanOrEqualToConstant:120],
// Step pill
[stepContainer.topAnchor constraintEqualToAnchor:self.topAnchor
constant:padding],
[stepContainer.leadingAnchor constraintEqualToAnchor:self.leadingAnchor
constant:padding],
[self.stepLabel.topAnchor constraintEqualToAnchor:stepContainer.topAnchor constant:4],
[self.stepLabel.bottomAnchor constraintEqualToAnchor:stepContainer.bottomAnchor constant:-4],
[self.stepLabel.leadingAnchor constraintEqualToAnchor:stepContainer.leadingAnchor constant:10],
[self.stepLabel.trailingAnchor constraintEqualToAnchor:stepContainer.trailingAnchor constant:-10],
// Message
[self.messageLabel.topAnchor constraintEqualToAnchor:stepContainer.bottomAnchor constant:8],
[self.messageLabel.leadingAnchor constraintEqualToAnchor:self.leadingAnchor constant:padding],
[self.messageLabel.trailingAnchor constraintEqualToAnchor:self.trailingAnchor constant:-padding],
//Prev Button
[self.prevButton.leadingAnchor constraintEqualToAnchor:self.leadingAnchor constant:12],
[self.prevButton.bottomAnchor constraintEqualToAnchor:self.bottomAnchor constant:-12],
[self.prevButton.trailingAnchor constraintEqualToAnchor:self.centerXAnchor constant:-12],
[self.prevButton.topAnchor constraintEqualToAnchor:self.messageLabel.bottomAnchor constant:16],
//Next Button
[self.nextButton.leadingAnchor constraintEqualToAnchor:self.centerXAnchor constant:12],
[self.nextButton.bottomAnchor constraintEqualToAnchor:self.bottomAnchor constant:-12],
[self.nextButton.trailingAnchor constraintEqualToAnchor:self.trailingAnchor constant:-12],
[self.nextButton.topAnchor constraintEqualToAnchor:self.messageLabel.bottomAnchor constant:16],
]];
// ───────── Arrow ─────────
self.arrowLayer = [CAShapeLayer layer];
self.arrowLayer.fillColor = UIColor.whiteColor.CGColor;
[self.layer addSublayer:self.arrowLayer];
return self;
}
- (void)layoutSubviews {
[super layoutSubviews];
CGFloat arrowWidth = 18.0;
CGFloat arrowHeight = kArrowHeight;
CGFloat centerX = (self.arrowCenterX > 0) ? self.arrowCenterX : CGRectGetMidX(self.bounds);
UIBezierPath *path = [UIBezierPath bezierPath];
if (self.arrowDirection == BubbleArrowDirectionUp) {
self.arrowLayer.frame = CGRectMake(0, -arrowHeight, self.bounds.size.width, self.bounds.size.height + arrowHeight);
// Arrow tip at the very top
CGPoint tip = CGPointMake(centerX, 0);
CGPoint baseLeft = CGPointMake(centerX - arrowWidth / 2, arrowHeight);
CGPoint baseRight = CGPointMake(centerX + arrowWidth / 2, arrowHeight);
[path moveToPoint:baseLeft];
[path addLineToPoint:baseRight];
[path addLineToPoint:tip];
} else { // BubbleArrowDirectionDown
self.arrowLayer.frame = CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height - arrowHeight);
CGFloat bubbleHeight = self.bounds.size.height;
// Arrow tip at the very bottom
CGPoint tip = CGPointMake(centerX, bubbleHeight + arrowHeight);
CGPoint baseLeft = CGPointMake(centerX - arrowWidth / 2, bubbleHeight);
CGPoint baseRight = CGPointMake(centerX + arrowWidth / 2, bubbleHeight);
[path moveToPoint:baseLeft];
[path addLineToPoint:baseRight];
[path addLineToPoint:tip];
}
[path closePath];
self.arrowLayer.path = path.CGPath;
}
- (void)configureWithText:(NSString *)text
step:(NSInteger)step
total:(NSInteger)total
isFirst:(BOOL)isFirst
isLast:(BOOL)isLast {
self.stepLabel.text = [NSString stringWithFormat:@"Step %ld",
(long)(step + 1)];
self.messageLabel.text = text;
self.prevButton.hidden = isFirst;
[self.nextButton setTitle:(isLast ? @"Done" : @"Next")
forState:UIControlStateNormal];
}
- (void)setArrowDirection:(BubbleArrowDirection)arrowDirection {
_arrowDirection = arrowDirection;
CGFloat padding = 20;
if (arrowDirection == BubbleArrowDirectionUp) {
self.topConstraint.constant = padding + kArrowHeight;
self.bottomConstraint.constant = -padding;
} else {
self.topConstraint.constant = padding;
self.bottomConstraint.constant = -(padding + kArrowHeight);
}
[self setNeedsLayout];
[self layoutIfNeeded];
}
- (CGSize)intrinsicContentSize {
CGSize size = [super intrinsicContentSize];
return CGSizeMake(size.width, size.height + kArrowHeight);
}
@end
framework/QmsPluginFramework/QmsPluginFramework/Utilities/TutorialButton/TutorialStep.h
0 → 100644
View file @
ae46b699
// TutorialStep.h
#import <Foundation/Foundation.h>
#import <CoreGraphics/CoreGraphics.h>
@interface
TutorialStep
:
NSObject
@property
(
nonatomic
,
copy
)
NSString
*
text
;
@property
(
nonatomic
,
assign
)
CGPoint
anchorPoint
;
@property
(
nonatomic
,
assign
)
BOOL
bubbleAbove
;
@end
framework/QmsPluginFramework/QmsPluginFramework/Utilities/TutorialButton/TutorialStep.mm
0 → 100644
View file @
ae46b699
// TutorialStep.mm
#import "TutorialStep.h"
@implementation TutorialStep
@end
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment