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
Show whitespace changes
Inline
Side-by-side
Showing
23 changed files
with
297 additions
and
221 deletions
+297
-221
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
+2
-10
AppointmentDetailsViewController.mm
...Framework/Appointment/AppointmentDetailsViewController.mm
+3
-13
DashboardViewController.mm
...k/QmsPluginFramework/Dashboard/DashboardViewController.mm
+27
-31
GeneralInfoViewController.mm
...QmsPluginFramework/Dashboard/GeneralInfoViewController.mm
+5
-8
ProjectViewController.mm
...ork/QmsPluginFramework/Dashboard/ProjectViewController.mm
+14
-24
DetailsViewController.mm
...msPluginFramework/Issue/AddIssue/DetailsViewController.mm
+2
-4
PlanViewController.mm
...k/QmsPluginFramework/Issue/AddIssue/PlanViewController.mm
+17
-32
HistoryViewController.mm
...amework/QmsPluginFramework/Issue/HistoryViewController.mm
+8
-11
IssueDetailViewController.mm
...ork/QmsPluginFramework/Issue/IssueDetailViewController.mm
+9
-26
IssuesViewController.mm
...ramework/QmsPluginFramework/Issue/IssuesViewController.mm
+5
-11
WithdrawIssuesViewController.mm
.../QmsPluginFramework/Issue/WithdrawIssuesViewController.mm
+3
-10
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>
...
...
@@ -636,18 +637,9 @@
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(), ^{
[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 ()
...
...
@@ -69,6 +70,7 @@
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"];
//
Determine logo URL
NSString *logo
URLString
= project[@"logo"];
if (!logoURLString.length) logoURLString
= 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
// 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;
}
});
}];
[task resume];
}
}];
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;
...
...
@@ -159,9 +160,7 @@
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];
[ImageCacheHelper loadImageWithURL:url completion:^(UIImage *img) {
if (!img) return;
dispatch_async(dispatch_get_main_queue(), ^{
...
...
@@ -179,7 +178,6 @@
}
});
}];
[task resume];
}
}
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;
// 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,29 +343,17 @@
[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
...
...
@@ -471,14 +460,9 @@
// 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"];
...
...
@@ -504,8 +488,7 @@
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(), ^{
[ImageCacheHelper loadImageWithURL:url completion:^(UIImage *img) {
// Make sure cell is still visible
IssueCell *updateCell = [tableView cellForRowAtIndexPath:indexPath];
if (!updateCell) return;
// Clear any "No image" label
s
// Clear previous subviews / placeholder
s
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];
}
});
});
}];
} 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,9 +632,7 @@
// 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(), ^{
[ImageCacheHelper loadImageWithURL:url completion:^(UIImage *img) {
WithdrawIssueCell *updateCell = [tableView cellForRowAtIndexPath:indexPath];
if (!updateCell) return;
...
...
@@ -642,19 +641,13 @@
[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];
}
});
});
}];
} else {
[self setNoImagePlaceholder:cell.thumbView];
}
...
...
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