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
1511a7e0
Commit
1511a7e0
authored
Jan 13, 2026
by
Wei Han
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
code update
image caching
parent
cf0ca12f
Hide whitespace changes
Inline
Side-by-side
Showing
23 changed files
with
392 additions
and
316 deletions
+392
-316
Contents.json
framework/QmsPluginFramework/Images.xcassets/Contents.json
+0
-0
Contents.json
...mework/Images.xcassets/img_overlay.imageset/Contents.json
+1
-1
img_overlay_projdashboard3x.png
...sets/img_overlay.imageset/img_overlay_projdashboard3x.png
+0
-0
Contents.json
...rk/Images.xcassets/img_placeholder.imageset/Contents.json
+21
-0
image_placeholder.png
...s.xcassets/img_placeholder.imageset/image_placeholder.png
+0
-0
project.pbxproj
...ginFramework/QmsPluginFramework.xcodeproj/project.pbxproj
+4
-0
AccessTransactionViewController.mm
...inFramework/AccessItem/AccessTransactionViewController.mm
+13
-28
AppointmentConfirmationViewController.mm
...work/Appointment/AppointmentConfirmationViewController.mm
+5
-13
AppointmentDetailsViewController.mm
...Framework/Appointment/AppointmentDetailsViewController.mm
+3
-13
DashboardViewController.mm
...k/QmsPluginFramework/Dashboard/DashboardViewController.mm
+28
-32
GeneralInfoViewController.mm
...QmsPluginFramework/Dashboard/GeneralInfoViewController.mm
+5
-8
ProjectViewController.mm
...ork/QmsPluginFramework/Dashboard/ProjectViewController.mm
+20
-30
DetailsViewController.mm
...msPluginFramework/Issue/AddIssue/DetailsViewController.mm
+28
-30
PlanViewController.mm
...k/QmsPluginFramework/Issue/AddIssue/PlanViewController.mm
+21
-36
HistoryViewController.mm
...amework/QmsPluginFramework/Issue/HistoryViewController.mm
+8
-11
IssueDetailViewController.mm
...ork/QmsPluginFramework/Issue/IssueDetailViewController.mm
+36
-53
IssuesViewController.mm
...ramework/QmsPluginFramework/Issue/IssuesViewController.mm
+18
-24
WithdrawIssuesViewController.mm
.../QmsPluginFramework/Issue/WithdrawIssuesViewController.mm
+18
-25
SyncScreenViewController.mm
...ework/QmsPluginFramework/Sync/SyncScreenViewController.mm
+1
-1
APIClient.mm
...inFramework/QmsPluginFramework/Utilities/API/APIClient.mm
+0
-8
ImageCacheHelper.h
...PluginFramework/Utilities/ImageCaching/ImageCacheHelper.h
+18
-0
ImageCacheHelper.mm
...luginFramework/Utilities/ImageCaching/ImageCacheHelper.mm
+117
-0
OfflineSyncManager.mm
...uginFramework/Utilities/OfflineSync/OfflineSyncManager.mm
+27
-3
No files found.
framework/QmsPluginFramework/
QmsPluginFramework/QmsPluginAsset
s.xcassets/Contents.json
→
framework/QmsPluginFramework/
Image
s.xcassets/Contents.json
View file @
1511a7e0
File moved
framework/QmsPluginFramework/
QmsPluginFramework/QmsPluginAssets.xcassets/O
verlay.imageset/Contents.json
→
framework/QmsPluginFramework/
Images.xcassets/img_o
verlay.imageset/Contents.json
View file @
1511a7e0
{
"images"
:
[
{
"filename"
:
"
overlay
.png"
,
"filename"
:
"
img_overlay_projdashboard3x
.png"
,
"idiom"
:
"universal"
,
"scale"
:
"1x"
},
...
...
framework/QmsPluginFramework/
QmsPluginFramework/QmsPluginAssets.xcassets/Overlay.imageset/overlay
.png
→
framework/QmsPluginFramework/
Images.xcassets/img_overlay.imageset/img_overlay_projdashboard3x
.png
View file @
1511a7e0
File moved
framework/QmsPluginFramework/Images.xcassets/img_placeholder.imageset/Contents.json
0 → 100644
View file @
1511a7e0
{
"images"
:
[
{
"filename"
:
"image_placeholder.png"
,
"idiom"
:
"universal"
,
"scale"
:
"1x"
},
{
"idiom"
:
"universal"
,
"scale"
:
"2x"
},
{
"idiom"
:
"universal"
,
"scale"
:
"3x"
}
],
"info"
:
{
"author"
:
"xcode"
,
"version"
:
1
}
}
framework/QmsPluginFramework/Images.xcassets/img_placeholder.imageset/image_placeholder.png
0 → 100644
View file @
1511a7e0
1.36 KB
framework/QmsPluginFramework/QmsPluginFramework.xcodeproj/project.pbxproj
View file @
1511a7e0
...
...
@@ -19,6 +19,7 @@
2FC779BD2F0E33410002A1D4
/* MobileCoreServices.framework in Frameworks */
=
{
isa
=
PBXBuildFile
;
fileRef
=
2FC779BC2F0E33410002A1D4
/* MobileCoreServices.framework */
;
};
2FD1557B2ECAC5CC00233981
/* WebKit.framework in Frameworks */
=
{
isa
=
PBXBuildFile
;
fileRef
=
2FD1557A2ECAC5CC00233981
/* WebKit.framework */
;
};
2FD9A1AE2EF0FA7100A7B53E
/* Network.framework in Frameworks */
=
{
isa
=
PBXBuildFile
;
fileRef
=
2FD9A0B72EF0E85B00A7B53E
/* Network.framework */
;
};
2FDE729C2F15C48100C471A0
/* Images.xcassets in Resources */
=
{
isa
=
PBXBuildFile
;
fileRef
=
2FDE729B2F15C48100C471A0
/* Images.xcassets */
;
};
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
...
...
@@ -60,6 +61,7 @@
2FD027B02EEFD25100CF5C33
/* Network.framework */
=
{
isa
=
PBXFileReference
;
lastKnownFileType
=
wrapper.framework
;
name
=
Network.framework
;
path
=
Platforms/MacOSX.platform/Developer/SDKs/MacOSX26.1.sdk/System/Library/Frameworks/Network.framework
;
sourceTree
=
DEVELOPER_DIR
;
};
2FD1557A2ECAC5CC00233981
/* WebKit.framework */
=
{
isa
=
PBXFileReference
;
lastKnownFileType
=
wrapper.framework
;
name
=
WebKit.framework
;
path
=
Platforms/MacOSX.platform/Developer/SDKs/MacOSX26.1.sdk/System/iOSSupport/System/Library/PrivateFrameworks/WebKit.framework
;
sourceTree
=
DEVELOPER_DIR
;
};
2FD9A0B72EF0E85B00A7B53E
/* Network.framework */
=
{
isa
=
PBXFileReference
;
lastKnownFileType
=
wrapper.framework
;
name
=
Network.framework
;
path
=
System/Library/Frameworks/Network.framework
;
sourceTree
=
SDKROOT
;
};
2FDE729B2F15C48100C471A0
/* Images.xcassets */
=
{
isa
=
PBXFileReference
;
lastKnownFileType
=
folder.assetcatalog
;
path
=
Images.xcassets
;
sourceTree
=
"<group>"
;
};
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
...
...
@@ -134,6 +136,7 @@
2F96475B2E86307D002CC7CB
=
{
isa
=
PBXGroup
;
children
=
(
2FDE729B2F15C48100C471A0
/* Images.xcassets */
,
2F9647672E86307D002CC7CB
/* QmsPluginFramework */
,
2FC779AA2F0E1C310002A1D4
/* testApp */
,
2FAEF0982E8B7BC80086EDA3
/* Frameworks */
,
...
...
@@ -271,6 +274,7 @@
isa
=
PBXResourcesBuildPhase
;
buildActionMask
=
2147483647
;
files
=
(
2FDE729C2F15C48100C471A0
/* Images.xcassets in Resources */
,
);
runOnlyForDeploymentPostprocessing
=
0
;
};
...
...
framework/QmsPluginFramework/QmsPluginFramework/AccessItem/AccessTransactionViewController.mm
View file @
1511a7e0
// AccessTransactionViewController.mm
#import "ImageCacheHelper.h"
#import "AccessTransactionViewController.h"
#import
<WebKit
/
WebKit
.
h
>
...
...
@@ -358,24 +358,17 @@
submittedSignature.clipsToBounds = YES;
submittedSignature.layer.borderColor = UIColor.blackColor.CGColor;
submittedSignature.layer.borderWidth = 1.0;
submittedSignature.layer.cornerRadius = 4.0; // optional rounded corners
submittedSignature.clipsToBounds = YES;
submittedSignature.layer.cornerRadius = 4.0;
submittedSignature.userInteractionEnabled = YES;
UITapGestureRecognizer *tapSubmitted = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onTapSignature:)];
[submittedSignature addGestureRecognizer:tapSubmitted];
[self.contentView addSubview:submittedSignature];
NSURL *submittedURL = [NSURL URLWithString:self.transactionData[@"signature_submit"]];
if (submittedURL) {
// Simple async load
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSData *data = [NSData dataWithContentsOfURL:submittedURL];
if (data) {
UIImage *img = [UIImage imageWithData:data];
dispatch_async(dispatch_get_main_queue(), ^{
submittedSignature.image = img;
});
}
});
}
[ImageCacheHelper loadImageWithURL:submittedURL completion:^(UIImage *image) {
submittedSignature.image = image;
}];
[self.contentView addSubview:submittedSignature];
UILabel *submittedName = [self regular16:[NSString stringWithFormat:@"Name: %@", self.transactionData[@"name_submit"] ?: @"--"]];
...
...
@@ -395,24 +388,16 @@
receivedSignature.layer.borderColor = UIColor.blackColor.CGColor;
receivedSignature.layer.borderWidth = 1.0;
receivedSignature.layer.cornerRadius = 4.0;
receivedSignature.clipsToBounds = YES;
receivedSignature.userInteractionEnabled = YES;
UITapGestureRecognizer *tapReceived = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onTapSignature:)];
[receivedSignature addGestureRecognizer:tapReceived];
NSURL *receivedURL = [NSURL URLWithString:self.transactionData[@"signature_receive"]];
if (receivedURL) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSData *data = [NSData dataWithContentsOfURL:receivedURL];
if (data) {
UIImage *img = [UIImage imageWithData:data];
dispatch_async(dispatch_get_main_queue(), ^{
receivedSignature.image = img;
});
}
});
}
[self.contentView addSubview:receivedSignature];
NSURL *receivedURL = [NSURL URLWithString:self.transactionData[@"signature_receive"]];
[ImageCacheHelper loadImageWithURL:receivedURL completion:^(UIImage *image) {
receivedSignature.image = image;
}];
UILabel *receivedName = [self regular16:[NSString stringWithFormat:@"Name: %@", self.transactionData[@"name_receive"] ?: @"--"]];
[self.contentView addSubview:receivedName];
...
...
framework/QmsPluginFramework/QmsPluginFramework/Appointment/AppointmentConfirmationViewController.mm
View file @
1511a7e0
...
...
@@ -3,6 +3,7 @@
#import <WebKit/WebKit.h>
#import <UIKit/UIKit.h>
#import "APIClient.h"
#import "ImageCacheHelper.h"
@interface AppointmentConfirmationViewController () <WKNavigationDelegate>
...
...
@@ -632,22 +633,13 @@
- (void)loadImage:(NSString *)urlString intoImageView:(UIImageView *)imageView {
if (!urlString) return;
NSURL *url = [NSURL URLWithString:urlString];
if (!url) return;
NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:url
completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (data && !error) {
UIImage *image = [UIImage imageWithData:data];
if (image) {
dispatch_async(dispatch_get_main_queue(), ^{
imageView.image = image;
});
}
}
[ImageCacheHelper loadImageWithURL:url completion:^(UIImage *image) {
imageView.image = image;
}];
[task resume];
}
-(void)confirmPressed {
...
...
framework/QmsPluginFramework/QmsPluginFramework/Appointment/AppointmentDetailsViewController.mm
View file @
1511a7e0
...
...
@@ -4,6 +4,7 @@
#import "APIClient.h"
#import
<WebKit
/
WebKit
.
h
>
#import
<EventKit
/
EventKit
.
h
>
#import "ImageCacheHelper.h"
@interface AppointmentDetailsViewController ()
<UIScrollViewDelegate>
...
...
@@ -177,20 +178,9 @@
NSURL *url = [NSURL URLWithString:urlString];
if (!url) return;
NSURLSessionDataTask *task =
[[NSURLSession sharedSession] dataTaskWithURL:url
completionHandler:^(NSData *data, NSURLResponse *res, NSError *err) {
if (err || !data) return;
UIImage *img = [UIImage imageWithData:data];
if (!img) return;
dispatch_async(dispatch_get_main_queue(), ^{
imageView.image = img;
});
[ImageCacheHelper loadImageWithURL:url completion:^(UIImage *image) {
imageView.image = image;
}];
[task resume];
}
- (void)handleFloorPlanTap:(UITapGestureRecognizer *)sender {
...
...
framework/QmsPluginFramework/QmsPluginFramework/Dashboard/DashboardViewController.mm
View file @
1511a7e0
...
...
@@ -14,6 +14,7 @@
#import "OfflineSyncManager.h"
#import "LocalStorage.h"
#import "APIConfig.h"
#import "ImageCacheHelper.h"
@interface DashboardViewController ()
...
...
@@ -68,7 +69,8 @@
[super viewDidLoad];
self.view.backgroundColor = [UIColor colorWithWhite:0.96 alpha:1.0];
_sectionViews = [NSMutableDictionary dictionary];
[self setupHeader];
[self setupScrollContent];
[self setupFooter];
...
...
@@ -212,10 +214,12 @@
// ---------- Overlay ----------
self.overlayImageView = [[UIImageView alloc] init];
self.overlayImageView.translatesAutoresizingMaskIntoConstraints = NO;
self.overlayImageView.contentMode = UIViewContentModeScaleAspectFill;
self.overlayImageView.alpha = 0.4;
self.overlayImageView.contentMode = UIViewContentModeScaleToFill;
self.overlayImageView.userInteractionEnabled = NO;
self.overlayImageView.alpha = 0.5;
[imageContainer addSubview:self.overlayImageView];
[NSLayoutConstraint activateConstraints:@[
[self.overlayImageView.topAnchor constraintEqualToAnchor:imageContainer.topAnchor],
[self.overlayImageView.leadingAnchor constraintEqualToAnchor:imageContainer.leadingAnchor],
...
...
@@ -305,21 +309,27 @@
[self.infoButton.heightAnchor constraintEqualToConstant:24]
]];
// ---------- Load Remote Image ----------
// ---------- Load Remote Image & Overlay ----------
NSBundle *bundle = [NSBundle bundleForClass:[self class]];
UIImage *overlayImage =
[UIImage imageNamed:@"img_overlay"
inBundle:bundle
compatibleWithTraitCollection:nil];
self.overlayImageView.image = overlayImage;
[imageContainer bringSubviewToFront:self.overlayImageView];
NSLog(@"[Overlay] bundle = %@", bundle);
NSLog(@"[Overlay] image = %@", overlayImage);
NSString *remoteHeaderURL = self.projectLogo;
if (remoteHeaderURL.length > 0) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:remoteHeaderURL]];
UIImage *img = data ? [UIImage imageWithData:data] : [UIImage imageNamed:@"header_bg"];
dispatch_async(dispatch_get_main_queue(), ^{
self.backgroundImageView.image = img;
self.overlayImageView.image = [UIImage imageNamed:@"overlay"];
});
});
NSURL *url = [NSURL URLWithString:remoteHeaderURL];
[ImageCacheHelper loadImageWithURL:url completion:^(UIImage *image) {
self.backgroundImageView.image = image ?: [UIImage imageNamed:@"header_bg"];
[imageContainer bringSubviewToFront:self.overlayImageView];
}];
} else {
self.backgroundImageView.image = [UIImage imageNamed:@"header_bg"];
self.overlayImageView.image = [UIImage imageNamed:@"overlay"];
self.overlayImageView.image = [UIImage imageNamed:@"
img_
overlay"];
}
}
...
...
@@ -670,7 +680,6 @@
dispatch_async(dispatch_get_main_queue(), ^{
self.projectNameLabel.text = self.projectName;
self.unitNameLabel.text = self.projectCode;
self.overlayImageView.image = [UIImage imageNamed:@"overlay"];
[self refreshDashboardSectionsWithData:mainData];
});
...
...
@@ -1195,20 +1204,12 @@
issueImage = lastArray[0][@"image"];
}
if (issueImage
&& issueImage
.length > 0) {
if (issueImage.length > 0) {
NSURL *url = [NSURL URLWithString:issueImage];
if (url) {
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
NSData *data = [NSData dataWithContentsOfURL:url];
if (data) {
UIImage *img = [UIImage imageWithData:data];
if (img) {
dispatch_async(dispatch_get_main_queue(), ^{
thumb.image = img;
});
}
}
});
[ImageCacheHelper loadImageWithURL:url completion:^(UIImage *image) {
if (image) thumb.image = image;
}];
}
}
...
...
@@ -1401,11 +1402,6 @@
}
}
- (UIImage *)downloadImageFrom:(NSString *)urlString {
NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:urlString]];
return data ? [UIImage imageWithData:data] : [UIImage imageNamed:@"header_bg"];
}
#pragma mark - Card Factory with Tap Animation
- (UIView *)createCardWithText:(NSString *)text frame:(CGRect)frame {
UIView *card = [[UIView alloc] initWithFrame:frame];
...
...
framework/QmsPluginFramework/QmsPluginFramework/Dashboard/GeneralInfoViewController.mm
View file @
1511a7e0
...
...
@@ -3,6 +3,7 @@
#import <objc/runtime.h>
#import "GeneralInfoViewController.h"
#import "APIClient.h"
#import "ImageCacheHelper.h"
@interface GeneralInfoViewController () <UIDocumentInteractionControllerDelegate, UIScrollViewDelegate>
...
...
@@ -764,15 +765,11 @@ typedef struct {
NSURL *url = [NSURL URLWithString:layoutImageURL];
if (url) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
NSData *data = [NSData dataWithContentsOfURL:url];
if (data) {
UIImage *img = [UIImage imageWithData:data];
dispatch_async(dispatch_get_main_queue(), ^{
imgView.image = img;
});
[ImageCacheHelper loadImageWithURL:url completion:^(UIImage *image) {
if (image) {
imgView.image = image;
}
}
)
;
}
]
;
}
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(showFullImage:)];
...
...
framework/QmsPluginFramework/QmsPluginFramework/Dashboard/ProjectViewController.mm
View file @
1511a7e0
...
...
@@ -3,6 +3,7 @@
#import "APIClient.h"
#import "APIConfig.h"
#import "UnitViewController.h"
#import "ImageCacheHelper.h"
@interface ProjectCardCell : UICollectionViewCell
@property (nonatomic, strong) UIImageView *imageView;
...
...
@@ -160,7 +161,11 @@
forIndexPath:indexPath];
// Default placeholder
cell.imageView.image = [UIImage systemImageNamed:@"photo"];
NSBundle *bundle = [NSBundle bundleForClass:[self class]];
UIImage *placeholderImage = [UIImage imageNamed:@"img_placeholder"
inBundle:bundle
compatibleWithTraitCollection:nil];
cell.imageView.image = placeholderImage;
NSDictionary *project = self.projects[indexPath.item];
...
...
@@ -168,35 +173,20 @@
id projectName = project[@"project_name"];
cell.nameLabel.text = [projectName isKindOfClass:[NSString class]] ? projectName : @"";
// Get logo URL safely
NSString *logo = project[@"logo"];
NSString *thumb = project[@"logo_thumbnail"];
NSString *logoURLString = (logo.length > 0) ? logo :
(thumb.length > 0) ? thumb : nil;
NSURL *url = [NSURL URLWithString:logoURLString ?: @""];
// Async load image safely
if (url) {
NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:url
completionHandler:^(NSData * _Nullable data,
NSURLResponse * _Nullable response,
NSError * _Nullable error) {
UIImage *image = data ? [UIImage imageWithData:data] : nil;
if (!image) return; // leave placeholder
dispatch_async(dispatch_get_main_queue(), ^{
// Only update if the cell is still visible at this indexPath
ProjectCardCell *updateCell = (ProjectCardCell *)[collectionView cellForItemAtIndexPath:indexPath];
if (updateCell) {
updateCell.imageView.image = image;
}
});
}];
[task resume];
}
// Determine logo URL
NSString *logoURLString = project[@"logo"];
if (!logoURLString.length) logoURLString = project[@"logo_thumbnail"];
// Use ImageCacheHelper for async loading & caching
[ImageCacheHelper loadImageWithURL:[NSURL URLWithString:logoURLString ?: @""] completion:^(UIImage *image) {
if (image) {
// Ensure the cell is still visible
ProjectCardCell *updateCell = (ProjectCardCell *)[collectionView cellForItemAtIndexPath:indexPath];
if (updateCell) {
updateCell.imageView.image = image;
}
}
}];
return cell;
}
...
...
framework/QmsPluginFramework/QmsPluginFramework/Issue/AddIssue/DetailsViewController.mm
View file @
1511a7e0
...
...
@@ -4,6 +4,7 @@
#import <Foundation/Foundation.h>
#import "OfflineSyncManager.h"
#import "NetworkManager.h"
#import "ImageCacheHelper.h"
@interface AddIssueDetailsViewController () <UIImagePickerControllerDelegate, UINavigationControllerDelegate>
@property (nonatomic, strong) UIView *headerView;
...
...
@@ -150,37 +151,34 @@
// 🖼 Prefill images under "first"
NSArray *firstImages = self.issueFullData[@"first"];
if ([firstImages isKindOfClass:[NSArray class]] && firstImages.count > 0) {
for (int i = 0; i < MIN(firstImages.count, 3); i++) {
NSDictionary *imgDict = firstImages[i];
NSString *urlStr = imgDict[@"thumb_image"] ?: imgDict[@"image"];
if (![urlStr isKindOfClass:[NSString class]]) continue;
for (int i = 0; i < MIN(firstImages.count, 3); i++) {
NSDictionary *imgDict = firstImages[i];
NSString *urlStr = imgDict[@"thumb_image"] ?: imgDict[@"image"];
if (![urlStr isKindOfClass:[NSString class]]) continue;
[self.existingImages addObject:urlStr];
NSURL *url = [NSURL URLWithString:urlStr];
if (!url) continue;
[ImageCacheHelper loadImageWithURL:url completion:^(UIImage *img) {
if (!img) return;
[self.existingImages addObject:urlStr];
NSURL *url = [NSURL URLWithString:urlStr];
if (!url) continue;
NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (error || !data) return;
UIImage *img = [UIImage imageWithData:data];
if (!img) return;
dispatch_async(dispatch_get_main_queue(), ^{
if (i < self.uploadedImages.count)
self.uploadedImages[i] = img;
else
[self.uploadedImages addObject:img];
UIButton *btn = [self.view viewWithTag:1000 + i];
if ([btn isKindOfClass:[UIButton class]]) {
[btn setImage:img forState:UIControlStateNormal];
btn.imageView.contentMode = UIViewContentModeScaleAspectFill;
btn.clipsToBounds = YES;
[self addRemoveButtonToUpload:btn atIndex:i];
}
});
}];
[task resume];
}
dispatch_async(dispatch_get_main_queue(), ^{
if (i < self.uploadedImages.count)
self.uploadedImages[i] = img;
else
[self.uploadedImages addObject:img];
UIButton *btn = [self.view viewWithTag:1000 + i];
if ([btn isKindOfClass:[UIButton class]]) {
[btn setImage:img forState:UIControlStateNormal];
btn.imageView.contentMode = UIViewContentModeScaleAspectFill;
btn.clipsToBounds = YES;
[self addRemoveButtonToUpload:btn atIndex:i];
}
});
}];
}
}
NSLog(@"✅ Prefilled Edit Mode UI successfully");
}
...
...
framework/QmsPluginFramework/QmsPluginFramework/Issue/AddIssue/PlanViewController.mm
View file @
1511a7e0
...
...
@@ -4,6 +4,7 @@
#import "APIClient.h"
#import <AVFoundation/AVFoundation.h>
#import "IssueTutorialViewController.h"
#import "ImageCacheHelper.h"
@interface PlanViewController ()
...
...
@@ -218,35 +219,41 @@
return;
}
NSLog(@"⬇️ Downloading plan image from %@", self.planImageURL);
NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:url
completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (error || !data) {
NSLog(@"❌ Failed to download plan image: %@", error.localizedDescription);
return;
}
NSLog(@"⬇️ Loading plan image from %@", self.planImageURL);
UIImage *image = [UIImage imageWithData:data];
[ImageCacheHelper loadImageWithURL:url completion:^(UIImage *image) {
if (!image) {
NSLog(@"
⚠️ Could not decode image data
");
NSLog(@"
❌ Failed to load plan image from cache/network
");
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
self.planImageView.image = image;
self.planImageView.userInteractionEnabled = YES;
UITapGestureRecognizer *tapRecognizer =
[[UITapGestureRecognizer alloc] initWithTarget:self
action:@selector(handleTapOnPlan:)];
[self.planImageView addGestureRecognizer:tapRecognizer];
// Add tap recognizer if not already added
BOOL hasTap = NO;
for (UIGestureRecognizer *gr in self.planImageView.gestureRecognizers) {
if ([gr isKindOfClass:[UITapGestureRecognizer class]]) {
hasTap = YES;
break;
}
}
if (!hasTap) {
UITapGestureRecognizer *tapRecognizer =
[[UITapGestureRecognizer alloc] initWithTarget:self
action:@selector(handleTapOnPlan:)];
[self.planImageView addGestureRecognizer:tapRecognizer];
}
[self.scrollView setZoomScale:1.0 animated:NO];
[self.view setNeedsLayout];
[self.view layoutIfNeeded];
[self renderAllLocations:self.locations];
NSLog(@"✅ Plan image displayed successfully");
});
}];
[task resume];
}
- (void)handleTapOnPlan:(UITapGestureRecognizer *)recognizer {
...
...
@@ -426,28 +433,6 @@
]];
}
- (void)loadPlanImageFromURL:(NSString *)urlString {
NSURL *url = [NSURL URLWithString:urlString];
if (!url) return;
NSURLSessionDataTask *task = [[NSURLSession sharedSession]
dataTaskWithURL:url
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (error) {
NSLog(@"❌ Failed to load image: %@", error);
return;
}
UIImage *image = [UIImage imageWithData:data];
if (image) {
dispatch_async(dispatch_get_main_queue(), ^{
self.planImageView.image = image;
NSLog(@"🖼 Plan image loaded successfully");
});
}
}];
[task resume];
}
- (void)showAlertWithTitle:(NSString *)title message:(NSString *)message {
dispatch_async(dispatch_get_main_queue(), ^{
UIAlertController *alert = [UIAlertController alertControllerWithTitle:title
...
...
framework/QmsPluginFramework/QmsPluginFramework/Issue/HistoryViewController.mm
View file @
1511a7e0
...
...
@@ -5,6 +5,7 @@
#import "CalendarView.h"
#import "NSDate+CalendarHelpers.h"
#import "AddInfoViewController.h"
#import "ImageCacheHelper.h"
@interface HistoryViewController () <UITableViewDelegate, UITableViewDataSource>
...
...
@@ -379,6 +380,7 @@
CGFloat imageSize = 56;
CGFloat spacing = 8;
// Inside your thumbnail loop
for (NSDictionary *imgDict in images) {
NSString *thumbURL = imgDict[@"thumb_image"];
if (thumbURL.length == 0) continue;
...
...
@@ -389,22 +391,17 @@
thumbView.contentMode = UIViewContentModeScaleAspectFill;
thumbView.backgroundColor = [UIColor colorWithWhite:0.9 alpha:1];
// Load async
[thumbScroll addSubview:thumbView];
NSURL *url = [NSURL URLWithString:thumbURL];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSData *data = [NSData dataWithContentsOfURL:url];
if (data) {
UIImage *img = [UIImage imageWithData:data];
dispatch_async(dispatch_get_main_queue(), ^{
thumbView.image = img;
});
[ImageCacheHelper loadImageWithURL:url completion:^(UIImage *image) {
if (image) {
thumbView.image = image;
}
}
)
;
}
]
;
[thumbScroll addSubview:thumbView];
x += imageSize + spacing;
}
thumbScroll.contentSize = CGSizeMake(x, imageSize);
return cell;
...
...
framework/QmsPluginFramework/QmsPluginFramework/Issue/IssueDetailViewController.mm
View file @
1511a7e0
...
...
@@ -2,6 +2,7 @@
#import "HistoryViewController.h"
#import "PlanViewController.h"
#import "WithdrawSingleIssueViewController.h"
#import "ImageCacheHelper.h"
@interface IssueDetailViewController () <UIScrollViewDelegate>
...
...
@@ -342,31 +343,19 @@
[imageScroll addSubview:imgView];
NSURL *url = [NSURL URLWithString:urlStr];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSData *data = [NSData dataWithContentsOfURL:url];
if (data) {
UIImage *img = [UIImage imageWithData:data];
dispatch_async(dispatch_get_main_queue(), ^{
imgView.image = img;
});
}
});
[ImageCacheHelper loadImageWithURL:url completion:^(UIImage *image) {
if (image) imgView.image = image;
}];
}
NSString *planThumbURL = self.issueData[@"plan_image_thumb"];
if (planThumbURL.length > 0) {
NSURL *url = [NSURL URLWithString:planThumbURL];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSData *data = [NSData dataWithContentsOfURL:url];
if (data) {
UIImage *img = [UIImage imageWithData:data];
dispatch_async(dispatch_get_main_queue(), ^{
self.floorPlanThumbnail.image = img;
});
}
});
[ImageCacheHelper loadImageWithURL:url completion:^(UIImage *image) {
if (image) self.floorPlanThumbnail.image = image;
}];
}
//thumbnail marker
NSString *x = self.issueData[@"pos_x"];
NSString *y = self.issueData[@"pos_y"];
...
...
@@ -471,41 +460,35 @@
// Load full image asynchronously
NSURL *url = [NSURL URLWithString:planFullURL];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSData *data = [NSData dataWithContentsOfURL:url];
if (!data) return;
UIImage *img = [UIImage imageWithData:data];
[ImageCacheHelper loadImageWithURL:url completion:^(UIImage *img) {
if (!img) return;
dispatch_async(dispatch_get_main_queue(), ^{
fullImageView.image = img;
NSString *xStr = self.issueData[@"pos_x"];
NSString *yStr = self.issueData[@"pos_y"];
if (xStr && yStr) {
CGFloat posX = [xStr floatValue];
CGFloat posY = [yStr floatValue];
// Calculate scale and offsets based on aspect-fit
CGSize imageSize = img.size;
CGSize viewSize = fullImageView.bounds.size;
CGFloat scale = MIN(viewSize.width / imageSize.width, viewSize.height / imageSize.height);
CGFloat offsetX = (viewSize.width - imageSize.width * scale) / 2.0;
CGFloat offsetY = (viewSize.height - imageSize.height * scale) / 2.0;
CGFloat dotX = posX * scale + offsetX;
CGFloat dotY = posY * scale + offsetY;
UIView *marker = [[UIView alloc] initWithFrame:CGRectMake(dotX - 5, dotY - 5, 10, 10)];
marker.backgroundColor = [UIColor systemRedColor];
marker.layer.cornerRadius = 5;
marker.layer.borderColor = [UIColor whiteColor].CGColor;
marker.layer.borderWidth = 1.5;
[fullImageView addSubview:marker];
}
});
});
fullImageView.image = img;
NSString *xStr = self.issueData[@"pos_x"];
NSString *yStr = self.issueData[@"pos_y"];
if (xStr && yStr) {
CGFloat posX = [xStr floatValue];
CGFloat posY = [yStr floatValue];
// Calculate scale and offsets based on aspect-fit
CGSize imageSize = img.size;
CGSize viewSize = fullImageView.bounds.size;
CGFloat scale = MIN(viewSize.width / imageSize.width, viewSize.height / imageSize.height);
CGFloat offsetX = (viewSize.width - imageSize.width * scale) / 2.0;
CGFloat offsetY = (viewSize.height - imageSize.height * scale) / 2.0;
CGFloat dotX = posX * scale + offsetX;
CGFloat dotY = posY * scale + offsetY;
UIView *marker = [[UIView alloc] initWithFrame:CGRectMake(dotX - 5, dotY - 5, 10, 10)];
marker.backgroundColor = [UIColor systemRedColor];
marker.layer.cornerRadius = 5;
marker.layer.borderColor = [UIColor whiteColor].CGColor;
marker.layer.borderWidth = 1.5;
[fullImageView addSubview:marker];
}
}];
}
- (void)closeFullFloorPlan {
...
...
framework/QmsPluginFramework/QmsPluginFramework/Issue/IssuesViewController.mm
View file @
1511a7e0
...
...
@@ -4,6 +4,7 @@
#import "IssueDetailViewController.h"
#import "WithdrawSingleIssueViewController.h"
#import "WithdrawIssuesViewController.h"
#import "ImageCacheHelper.h"
#pragma mark - Internal IssueCell
@interface IssueCell : UITableViewCell
...
...
@@ -605,30 +606,23 @@
// --- Thumbnail Handling ---
if (thumbURL.length > 0) {
NSURL *url = [NSURL URLWithString:thumbURL];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSData *data = [NSData dataWithContentsOfURL:url];
dispatch_async(dispatch_get_main_queue(), ^{
IssueCell *updateCell = [tableView cellForRowAtIndexPath:indexPath];
if (!updateCell) return;
// Clear any "No image" labels
for (UIView *subview in updateCell.thumbView.subviews) {
[subview removeFromSuperview];
}
if (data) {
UIImage *img = [UIImage imageWithData:data];
if (img) {
updateCell.thumbView.image = img;
updateCell.thumbView.backgroundColor = UIColor.clearColor;
} else {
[self setNoImagePlaceholder:updateCell.thumbView];
}
} else {
[self setNoImagePlaceholder:updateCell.thumbView];
}
});
});
[ImageCacheHelper loadImageWithURL:url completion:^(UIImage *img) {
// Make sure cell is still visible
IssueCell *updateCell = [tableView cellForRowAtIndexPath:indexPath];
if (!updateCell) return;
// Clear previous subviews / placeholders
for (UIView *subview in updateCell.thumbView.subviews) {
[subview removeFromSuperview];
}
if (img) {
updateCell.thumbView.image = img;
updateCell.thumbView.backgroundColor = UIColor.clearColor;
} else {
[self setNoImagePlaceholder:updateCell.thumbView];
}
}];
} else {
[self setNoImagePlaceholder:cell.thumbView];
}
...
...
framework/QmsPluginFramework/QmsPluginFramework/Issue/WithdrawIssuesViewController.mm
View file @
1511a7e0
...
...
@@ -3,6 +3,7 @@
#import "APIClient.h"
#import "WithdrawSingleIssueViewController.h"
#import "FilterViewController.h"
#import "ImageCacheHelper.h"
#pragma mark - custom cell structure
@interface WithdrawIssueCell : UITableViewCell
...
...
@@ -631,34 +632,26 @@
// Thumbnail rendering (copied from IssueViewController)
if (thumbURL.length > 0) {
NSURL *url = [NSURL URLWithString:thumbURL];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSData *data = [NSData dataWithContentsOfURL:url];
dispatch_async(dispatch_get_main_queue(), ^{
WithdrawIssueCell *updateCell = [tableView cellForRowAtIndexPath:indexPath];
if (!updateCell) return;
// Clear any "No image" placeholder labels
for (UIView *subview in updateCell.thumbView.subviews) {
[subview removeFromSuperview];
}
if (data) {
UIImage *img = [UIImage imageWithData:data];
if (img) {
updateCell.thumbView.image = img;
updateCell.thumbView.backgroundColor = UIColor.clearColor;
} else {
[self setNoImagePlaceholder:updateCell.thumbView];
}
} else {
[self setNoImagePlaceholder:updateCell.thumbView];
}
});
});
[ImageCacheHelper loadImageWithURL:url completion:^(UIImage *img) {
WithdrawIssueCell *updateCell = [tableView cellForRowAtIndexPath:indexPath];
if (!updateCell) return;
// Clear any "No image" placeholder labels
for (UIView *subview in updateCell.thumbView.subviews) {
[subview removeFromSuperview];
}
if (img) {
updateCell.thumbView.image = img;
updateCell.thumbView.backgroundColor = UIColor.clearColor;
} else {
[self setNoImagePlaceholder:updateCell.thumbView];
}
}];
} else {
[self setNoImagePlaceholder:cell.thumbView];
}
// Text fields
cell.referenceLabel.text = issue[@"reference"] ?: @"";
cell.statusLabel.text = issue[@"status_external"] ?: @"";
...
...
framework/QmsPluginFramework/QmsPluginFramework/Sync/SyncScreenViewController.mm
View file @
1511a7e0
...
...
@@ -620,7 +620,7 @@ static NSString * const kLastOfflineSyncDateKey = @"LAST_OFFLINE_SYNC_DATE";
[APIConfig projectId], [APIConfig drawingPlanId]);
NSString *message = [NSString stringWithFormat:
@"Currently downloading unit \"%@\".\nPlease do not close the app.", sortName];
@"Currently downloading
for
unit \"%@\".\nPlease do not close the app.", sortName];
[[OfflineSyncManager shared] startDownloadUnitImagesWithProgress:^(NSInteger completed, NSInteger total) {
__strong typeof(weakSelf) strongSelf2 = weakSelf;
...
...
framework/QmsPluginFramework/QmsPluginFramework/Utilities/API/APIClient.mm
View file @
1511a7e0
...
...
@@ -1738,10 +1738,6 @@
+ (void)requestArrayImage:(NSArray *)imageArray
completion:(void(^)(BOOL success, NSDictionary * _Nullable response, NSError * _Nullable error))completion {
if ([APIConfig handleOfflineForAPI:@"ArrayImage" completion:completion]) {
return;
}
// ✅ URL
NSURL *url = [APIConfig urlWithPath:@"/owner/offline/syncImageArray"];
...
...
@@ -1795,10 +1791,6 @@
dispatch_async(dispatch_get_main_queue(), ^{
if (completion) {
NSString *cacheKey = [NSString stringWithFormat:@"ArrayImage_%@", [APIConfig projectId]];
NSDictionary *safeJson = [APIConfig dictionaryByReplacingNullsWithBlanks:jsonResponse];
[LocalStorage saveDictionary:safeJson forKey:cacheKey];
NSLog(@"💾 Saved to cache: %@", cacheKey);
completion(YES, jsonResponse, jsonParseError);
}
});
...
...
framework/QmsPluginFramework/QmsPluginFramework/Utilities/ImageCaching/ImageCacheHelper.h
0 → 100644
View file @
1511a7e0
// ImageCacheHelper.h
#import <UIKit/UIKit.h>
@interface
ImageCacheHelper
:
NSObject
/// Load image from cache, download if missing, or fallback to placeholder
+
(
void
)
loadImageWithURL
:(
NSURL
*
)
url
completion
:(
void
(
^
)(
UIImage
*
image
))
completion
;
/// Shared placeholder image
+
(
UIImage
*
)
placeholderImage
;
/// Get the expected cache file path for a URL
+
(
NSString
*
)
cachedFilePathForURL
:(
NSURL
*
)
url
;
@end
framework/QmsPluginFramework/QmsPluginFramework/Utilities/ImageCaching/ImageCacheHelper.mm
0 → 100644
View file @
1511a7e0
// ImageCacheHelper.mm
#import <CommonCrypto/CommonDigest.h>
#import "ImageCacheHelper.h"
#import "NetworkManager.h"
#import "APIClient.h"
@implementation ImageCacheHelper
#pragma mark - Public Methods
+ (void)loadImageWithURL:(NSURL *)url
completion:(void (^)(UIImage *image))completion {
if (!url) {
if (completion) completion([self placeholderImage]);
return;
}
NSString *filePath = [self cachedFilePathForURL:url];
NSFileManager *fm = [NSFileManager defaultManager];
// 1️⃣ Check cache
if ([fm fileExistsAtPath:filePath]) {
UIImage *cachedImage = [UIImage imageWithContentsOfFile:filePath];
if (cachedImage) {
if (completion) completion(cachedImage);
return;
}
}
// 2️⃣ Not cached → check network
if (![self canDownloadImages]) {
// No internet → return placeholder
if (completion) completion([self placeholderImage]);
return;
}
// 3️⃣ Download
[APIClient downloadAndCacheImage:url completion:^(BOOL success, NSString *filePath) {
UIImage *img = nil;
if (success && filePath) {
img = [UIImage imageWithContentsOfFile:filePath];
}
if (!img) img = [self placeholderImage];
dispatch_async(dispatch_get_main_queue(), ^{
if (completion) completion(img);
});
}];
}
+ (UIImage *)placeholderImage {
static UIImage *image = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSBundle *bundle = [NSBundle bundleForClass:self];
image = [UIImage imageNamed:@"img_placeholder"
inBundle:bundle
compatibleWithTraitCollection:nil];
});
return image;
}
#pragma mark - Cache File Helper
+ (NSString *)cachedFilePathForURL:(NSURL *)url {
if (!url) return nil;
NSString *cacheDir = [self offlineImageCacheDirectory]; // You already have this
NSFileManager *fm = [NSFileManager defaultManager];
if (![fm fileExistsAtPath:cacheDir]) {
NSError *dirError = nil;
[fm createDirectoryAtPath:cacheDir
withIntermediateDirectories:YES
attributes:nil
error:&dirError];
if (dirError) {
NSLog(@"❌ Failed to create cache directory: %@", dirError);
}
}
NSString *cleanName = url.lastPathComponent.stringByDeletingPathExtension;
NSString *ext = url.pathExtension;
NSString *fileName = [NSString stringWithFormat:@"%@_%@.%@",
cleanName,
[self md5Hash:url.absoluteString],
ext];
return [cacheDir stringByAppendingPathComponent:fileName];
}
#pragma mark - MD5 Hash Helper
+ (NSString *)md5Hash:(NSString *)input {
if (!input) return @"";
const char *cStr = [input UTF8String];
unsigned char digest[CC_MD5_DIGEST_LENGTH];
CC_MD5(cStr, (CC_LONG)strlen(cStr), digest);
NSMutableString *output = [NSMutableString stringWithCapacity:CC_MD5_DIGEST_LENGTH * 2];
for(int i = 0; i < CC_MD5_DIGEST_LENGTH; i++)
[output appendFormat:@"%02x", digest[i]];
return output;
}
#pragma mark - Offline Cache Directory
+ (NSString *)offlineImageCacheDirectory {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
NSString *cacheDir = [[paths firstObject] stringByAppendingPathComponent:@"OfflineImages"];
return cacheDir;
}
+ (BOOL)canDownloadImages {
return [NetworkManager.sharedManager isConnected];
}
@end
framework/QmsPluginFramework/QmsPluginFramework/Utilities/OfflineSync/OfflineSyncManager.mm
View file @
1511a7e0
...
...
@@ -3,15 +3,16 @@
#import "LocalStorage.h"
#import "NetworkManager.h"
#import "APIClient.h"
#import "APIConfig.h"
static NSString * const kOfflineQueueKey = @"OFFLINE_SYNC_QUEUE";
static NSString * const kSelectedOfflineUnitsKey = @"SELECTED_OFFLINE_UNITS";
static NSString * const kSyncHistoryKey = @"SYNC_HISTORY_QUEUE";
static NSString * const kLastOfflineSyncDateKey = @"LAST_OFFLINE_SYNC_DATE";
static NSString * const kSyncSelectedUnitsKey = @"SYNC_SELECTEDUNITS";
@implementation OfflineSyncManager
#pragma mark - Singleton
...
...
@@ -542,7 +543,7 @@ static NSString * const kLastOfflineSyncDateKey = @"LAST_OFFLINE_SYNC_DATE";
return;
}
NSArray *selectedUnits = [
LocalStorage loadArrayForKey:kSelectedOfflineUnitsKey
] ?: @[];
NSArray *selectedUnits = [
self buildOfflineUnitPayloadForCurrentConfig
] ?: @[];
// ------------------------------
// Build sequential tasks
...
...
@@ -986,10 +987,33 @@ static NSString * const kLastOfflineSyncDateKey = @"LAST_OFFLINE_SYNC_DATE";
if (img[@"link"]) [urls addObject:[NSURL URLWithString:img[@"link"]]];
}
for (NSURL *url in urls) {
NSLog(@"URL for image downloads: %@", url.absoluteString);
}
completion(urls.array, nil);
}];
}
- (NSArray *)buildOfflineUnitPayloadForCurrentConfig {
NSString *projectId = [APIConfig projectId];
NSString *planId = [APIConfig drawingPlanId];
if (!projectId || !planId) {
NSLog(@"⚠️ Missing projectId or planId for image sync");
return @[];
}
NSArray *result = @[
@{
@"project_id": projectId,
@"plan_id": @[planId] // API expects array
}
];
NSLog(@"🔄 Image sync payload (single unit): %@", result);
return result;
}
#pragma mark - Safe Sequential API Helper
- (void)safeSequentialTask:(NSString *)taskKey
...
...
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