@granite-js/image 0.1.34 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +3 -277
  2. package/GraniteImage.podspec +72 -0
  3. package/android/build.gradle +178 -0
  4. package/android/gradle.properties +5 -0
  5. package/android/src/coil/java/run/granite/image/providers/CoilImageProvider.kt +156 -0
  6. package/android/src/glide/java/run/granite/image/providers/GlideImageProvider.kt +168 -0
  7. package/android/src/main/AndroidManifest.xml +2 -0
  8. package/android/src/main/java/run/granite/image/GraniteImage.kt +277 -0
  9. package/android/src/main/java/run/granite/image/GraniteImageEvents.kt +83 -0
  10. package/android/src/main/java/run/granite/image/GraniteImageManager.kt +100 -0
  11. package/android/src/main/java/run/granite/image/GraniteImageModule.kt +131 -0
  12. package/android/src/main/java/run/granite/image/GraniteImagePackage.kt +59 -0
  13. package/android/src/main/java/run/granite/image/GraniteImageProvider.kt +105 -0
  14. package/android/src/main/java/run/granite/image/GraniteImageRegistry.kt +29 -0
  15. package/android/src/okhttp/java/run/granite/image/providers/OkHttpImageProvider.kt +228 -0
  16. package/dist/module/GraniteImage.js +127 -0
  17. package/dist/module/GraniteImage.js.map +1 -0
  18. package/dist/module/GraniteImageNativeComponent.ts +56 -0
  19. package/dist/module/NativeGraniteImageModule.js +5 -0
  20. package/dist/module/NativeGraniteImageModule.js.map +1 -0
  21. package/dist/module/index.js +6 -0
  22. package/dist/module/index.js.map +1 -0
  23. package/dist/module/package.json +1 -0
  24. package/dist/typescript/GraniteImage.d.ts +35 -0
  25. package/dist/typescript/GraniteImageNativeComponent.d.ts +37 -0
  26. package/dist/typescript/NativeGraniteImageModule.d.ts +16 -0
  27. package/dist/typescript/index.d.ts +4 -0
  28. package/example/react-native.config.js +21 -0
  29. package/ios/GraniteImageComponentView.h +14 -0
  30. package/ios/GraniteImageComponentView.mm +388 -0
  31. package/ios/GraniteImageModule.swift +107 -0
  32. package/ios/GraniteImageModuleBridge.m +15 -0
  33. package/ios/GraniteImageProvider.swift +70 -0
  34. package/ios/GraniteImageRegistry.swift +30 -0
  35. package/ios/Providers/SDWebImageProvider.swift +175 -0
  36. package/package.json +71 -32
  37. package/src/GraniteImage.tsx +215 -0
  38. package/src/GraniteImageNativeComponent.ts +56 -0
  39. package/src/NativeGraniteImageModule.ts +16 -0
  40. package/src/index.ts +21 -0
  41. package/dist/index.d.mts +0 -70
  42. package/dist/index.d.ts +0 -70
  43. package/dist/index.js +0 -204
  44. package/dist/index.mjs +0 -180
@@ -0,0 +1,56 @@
1
+ import { type ViewProps, type ColorValue, codegenNativeComponent } from 'react-native';
2
+ import type { WithDefault, Int32, DirectEventHandler } from 'react-native/Libraries/Types/CodegenTypes';
3
+
4
+ // Event payload types
5
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
6
+ export type OnLoadStartEvent = Readonly<{}>;
7
+
8
+ export type OnProgressEvent = Readonly<{
9
+ loaded: Int32;
10
+ total: Int32;
11
+ }>;
12
+
13
+ export type OnLoadEvent = Readonly<{
14
+ width: Int32;
15
+ height: Int32;
16
+ }>;
17
+
18
+ export type OnErrorEvent = Readonly<{
19
+ error: string;
20
+ }>;
21
+
22
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
23
+ export type OnLoadEndEvent = Readonly<{}>;
24
+
25
+ export type OnGraniteLoadStartEvent = DirectEventHandler<OnLoadStartEvent>;
26
+ export type OnGraniteProgressEvent = DirectEventHandler<OnProgressEvent>;
27
+ export type OnGraniteLoadEvent = DirectEventHandler<OnLoadEvent>;
28
+ export type OnGraniteErrorEvent = DirectEventHandler<OnErrorEvent>;
29
+ export type OnGraniteLoadEndEvent = DirectEventHandler<OnLoadEndEvent>;
30
+
31
+ interface GraniteImageProps extends ViewProps {
32
+ // Source
33
+ uri?: string;
34
+ headers?: string; // JSON string of headers object
35
+
36
+ // Display
37
+ contentMode?: WithDefault<'cover' | 'contain' | 'stretch' | 'center', 'cover'>;
38
+ tintColor?: ColorValue;
39
+
40
+ // Placeholder
41
+ defaultSource?: string; // Local asset name or URI
42
+ fallbackSource?: string; // Fallback image to show on error
43
+
44
+ // Priority & Cache
45
+ priority?: WithDefault<'low' | 'normal' | 'high', 'normal'>;
46
+ cachePolicy?: WithDefault<'memory' | 'disk' | 'none', 'disk'>;
47
+
48
+ // Callbacks
49
+ onGraniteLoadStart?: DirectEventHandler<OnLoadStartEvent>;
50
+ onGraniteProgress?: DirectEventHandler<OnProgressEvent>;
51
+ onGraniteLoad?: DirectEventHandler<OnLoadEvent>;
52
+ onGraniteError?: DirectEventHandler<OnErrorEvent>;
53
+ onGraniteLoadEnd?: DirectEventHandler<OnLoadEndEvent>;
54
+ }
55
+
56
+ export default codegenNativeComponent<GraniteImageProps>('GraniteImage');
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+
3
+ import { TurboModuleRegistry } from 'react-native';
4
+ export default TurboModuleRegistry.getEnforcing('GraniteImageModule');
5
+ //# sourceMappingURL=NativeGraniteImageModule.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["TurboModuleRegistry","getEnforcing"],"sourceRoot":"../../src","sources":["NativeGraniteImageModule.ts"],"mappings":";;AAAA,SAA2BA,mBAAmB,QAAQ,cAAc;AAepE,eAAeA,mBAAmB,CAACC,YAAY,CAAO,oBAAoB,CAAC","ignoreList":[]}
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+
3
+ import GraniteImage from "./GraniteImage.js";
4
+ export { GraniteImage } from "./GraniteImage.js";
5
+ export default GraniteImage;
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["GraniteImage"],"sourceRoot":"../../src","sources":["index.ts"],"mappings":";;AAAA,OAAOA,YAAY,MAAM,mBAAgB;AAEzC,SACEA,YAAY,QAOP,mBAAgB;AAUvB,eAAeA,YAAY","ignoreList":[]}
@@ -0,0 +1 @@
1
+ {"type":"module"}
@@ -0,0 +1,35 @@
1
+ import React from 'react';
2
+ import { type StyleProp, type ViewStyle, type ColorValue, type ImageRequireSource, ViewProps, NativeSyntheticEvent } from 'react-native';
3
+ import { type OnLoadStartEvent, type OnProgressEvent, type OnLoadEvent, type OnErrorEvent, type OnLoadEndEvent } from './GraniteImageNativeComponent';
4
+ export interface GraniteImageSource {
5
+ uri: string;
6
+ headers?: Record<string, string>;
7
+ priority?: 'low' | 'normal' | 'high';
8
+ cache?: 'immutable' | 'web' | 'cacheOnly';
9
+ }
10
+ export type ResizeMode = 'cover' | 'contain' | 'stretch' | 'center';
11
+ export type CachePolicy = 'memory' | 'disk' | 'none';
12
+ export type Priority = 'low' | 'normal' | 'high';
13
+ export interface GraniteImageProps extends ViewProps {
14
+ source: GraniteImageSource | string;
15
+ resizeMode?: ResizeMode;
16
+ tintColor?: ColorValue;
17
+ style?: StyleProp<ViewStyle>;
18
+ defaultSource?: ImageRequireSource | string;
19
+ fallbackSource?: ImageRequireSource | string;
20
+ priority?: Priority;
21
+ cachePolicy?: CachePolicy;
22
+ onLoadStart?: (event: NativeSyntheticEvent<OnLoadStartEvent>) => void;
23
+ onProgress?: (event: NativeSyntheticEvent<OnProgressEvent>) => void;
24
+ onLoad?: (event: NativeSyntheticEvent<OnLoadEvent>) => void;
25
+ onError?: (error: NativeSyntheticEvent<OnErrorEvent>) => void;
26
+ onLoadEnd?: (event: NativeSyntheticEvent<OnLoadEndEvent>) => void;
27
+ }
28
+ export interface GraniteImageStatic {
29
+ clearMemoryCache: () => Promise<void>;
30
+ clearDiskCache: () => Promise<void>;
31
+ preload: (sources: GraniteImageSource[]) => Promise<void>;
32
+ }
33
+ type GraniteImageComponent = React.FC<GraniteImageProps> & GraniteImageStatic;
34
+ export declare const GraniteImage: GraniteImageComponent;
35
+ export default GraniteImage;
@@ -0,0 +1,37 @@
1
+ import { type ViewProps, type ColorValue } from 'react-native';
2
+ import type { WithDefault, Int32, DirectEventHandler } from 'react-native/Libraries/Types/CodegenTypes';
3
+ export type OnLoadStartEvent = Readonly<{}>;
4
+ export type OnProgressEvent = Readonly<{
5
+ loaded: Int32;
6
+ total: Int32;
7
+ }>;
8
+ export type OnLoadEvent = Readonly<{
9
+ width: Int32;
10
+ height: Int32;
11
+ }>;
12
+ export type OnErrorEvent = Readonly<{
13
+ error: string;
14
+ }>;
15
+ export type OnLoadEndEvent = Readonly<{}>;
16
+ export type OnGraniteLoadStartEvent = DirectEventHandler<OnLoadStartEvent>;
17
+ export type OnGraniteProgressEvent = DirectEventHandler<OnProgressEvent>;
18
+ export type OnGraniteLoadEvent = DirectEventHandler<OnLoadEvent>;
19
+ export type OnGraniteErrorEvent = DirectEventHandler<OnErrorEvent>;
20
+ export type OnGraniteLoadEndEvent = DirectEventHandler<OnLoadEndEvent>;
21
+ interface GraniteImageProps extends ViewProps {
22
+ uri?: string;
23
+ headers?: string;
24
+ contentMode?: WithDefault<'cover' | 'contain' | 'stretch' | 'center', 'cover'>;
25
+ tintColor?: ColorValue;
26
+ defaultSource?: string;
27
+ fallbackSource?: string;
28
+ priority?: WithDefault<'low' | 'normal' | 'high', 'normal'>;
29
+ cachePolicy?: WithDefault<'memory' | 'disk' | 'none', 'disk'>;
30
+ onGraniteLoadStart?: DirectEventHandler<OnLoadStartEvent>;
31
+ onGraniteProgress?: DirectEventHandler<OnProgressEvent>;
32
+ onGraniteLoad?: DirectEventHandler<OnLoadEvent>;
33
+ onGraniteError?: DirectEventHandler<OnErrorEvent>;
34
+ onGraniteLoadEnd?: DirectEventHandler<OnLoadEndEvent>;
35
+ }
36
+ declare const _default: import("react-native").HostComponent<GraniteImageProps>;
37
+ export default _default;
@@ -0,0 +1,16 @@
1
+ import { type TurboModule } from 'react-native';
2
+ export interface ImageSource {
3
+ uri: string;
4
+ headers?: {
5
+ [key: string]: string;
6
+ };
7
+ priority?: string;
8
+ cache?: string;
9
+ }
10
+ export interface Spec extends TurboModule {
11
+ preload(sources: string): Promise<void>;
12
+ clearMemoryCache(): Promise<void>;
13
+ clearDiskCache(): Promise<void>;
14
+ }
15
+ declare const _default: Spec;
16
+ export default _default;
@@ -0,0 +1,4 @@
1
+ import GraniteImage from './GraniteImage';
2
+ export { GraniteImage, type GraniteImageProps, type GraniteImageSource, type GraniteImageStatic, type Priority, type ResizeMode, type CachePolicy, } from './GraniteImage';
3
+ export { type OnLoadEvent, type OnProgressEvent, type OnLoadEndEvent, type OnErrorEvent, type OnLoadStartEvent, } from './GraniteImageNativeComponent';
4
+ export default GraniteImage;
@@ -0,0 +1,21 @@
1
+ const path = require('path');
2
+ const pkg = require('../package.json');
3
+
4
+ module.exports = {
5
+ project: {
6
+ ios: {
7
+ automaticPodsInstallation: true,
8
+ },
9
+ },
10
+ dependencies: {
11
+ [pkg.name]: {
12
+ root: path.join(__dirname, '..'),
13
+ platforms: {
14
+ // Codegen script incorrectly fails without this
15
+ // So we explicitly specify the platforms with empty object
16
+ ios: {},
17
+ android: {},
18
+ },
19
+ },
20
+ },
21
+ };
@@ -0,0 +1,14 @@
1
+ //
2
+ // GraniteImageComponentView.h
3
+ // GraniteImagePOC
4
+ //
5
+
6
+ #import <UIKit/UIKit.h>
7
+
8
+ NS_ASSUME_NONNULL_BEGIN
9
+
10
+ // Forward declare to avoid pulling in C++ headers
11
+ @interface GraniteImageComponentView : UIView
12
+ @end
13
+
14
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,388 @@
1
+ #import <UIKit/UIKit.h>
2
+ #import <os/log.h>
3
+
4
+ #import <React/RCTViewComponentView.h>
5
+ #import <React/RCTConversions.h>
6
+ #import <react/renderer/components/GraniteImageSpec/ComponentDescriptors.h>
7
+ #import <react/renderer/components/GraniteImageSpec/EventEmitters.h>
8
+ #import <react/renderer/components/GraniteImageSpec/Props.h>
9
+ #import <react/renderer/components/GraniteImageSpec/RCTComponentViewHelpers.h>
10
+
11
+ // Import Swift module - the header is generated during build
12
+ // For CocoaPods: GraniteImage-Swift.h (based on pod name)
13
+ // For frameworks: <GraniteImage/GraniteImage-Swift.h>
14
+ #if __has_include(<GraniteImage/GraniteImage-Swift.h>)
15
+ #import <GraniteImage/GraniteImage-Swift.h>
16
+ #elif __has_include(<react_native_granite_image/react_native_granite_image-Swift.h>)
17
+ #import <react_native_granite_image/react_native_granite_image-Swift.h>
18
+ #else
19
+ // This will be found in the build directory - CocoaPods generates it there
20
+ #import "GraniteImage-Swift.h"
21
+ #endif
22
+
23
+ using namespace facebook::react;
24
+
25
+ static os_log_t graniteimage_log() {
26
+ static os_log_t log = os_log_create("com.graniteimage", "GraniteImage");
27
+ return log;
28
+ }
29
+
30
+ @interface GraniteImageComponentView : RCTViewComponentView <RCTGraniteImageViewProtocol>
31
+ @end
32
+
33
+ // Constructor to verify the code is being loaded
34
+ __attribute__((constructor)) static void _graniteimage_constructor(void) {
35
+ os_log_error(graniteimage_log(), "Constructor called - class is being loaded");
36
+ }
37
+
38
+ // Force the linker to include this class
39
+ __attribute__((used)) static void _forceIncludeGraniteImageComponentView(void) {
40
+ [GraniteImageComponentView class];
41
+ }
42
+
43
+ @implementation GraniteImageComponentView {
44
+ UIView *_containerView;
45
+ NSString *_currentUri;
46
+ UIViewContentMode _currentContentMode;
47
+ BOOL _needsInitialLoad;
48
+
49
+ // New props
50
+ NSDictionary<NSString *, NSString *> *_currentHeaders;
51
+ GraniteProviderPriority _currentPriority;
52
+ GraniteProviderCachePolicy _currentCachePolicy;
53
+ UIColor *_currentTintColor;
54
+ NSString *_currentDefaultSource;
55
+ NSString *_currentFallbackSource;
56
+ }
57
+
58
+ + (void)load
59
+ {
60
+ NSLog(@"[GraniteImage] +load called - registering component");
61
+ }
62
+
63
+ + (ComponentDescriptorProvider)componentDescriptorProvider
64
+ {
65
+ return concreteComponentDescriptorProvider<GraniteImageComponentDescriptor>();
66
+ }
67
+
68
+ - (instancetype)initWithFrame:(CGRect)frame
69
+ {
70
+ if (self = [super initWithFrame:frame]) {
71
+ static const auto defaultProps = std::make_shared<const GraniteImageProps>();
72
+ _props = defaultProps;
73
+ self.clipsToBounds = YES;
74
+ _currentContentMode = UIViewContentModeScaleAspectFill;
75
+ _currentPriority = GraniteProviderPriorityNormal;
76
+ _currentCachePolicy = GraniteProviderCachePolicyDisk;
77
+ _needsInitialLoad = YES;
78
+ }
79
+ return self;
80
+ }
81
+
82
+ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &)oldProps
83
+ {
84
+ const auto &newViewProps = *std::static_pointer_cast<const GraniteImageProps>(props);
85
+
86
+ // Handle uri changes
87
+ NSString *newUri = [NSString stringWithUTF8String:newViewProps.uri.c_str()];
88
+
89
+ // Handle headers (JSON string)
90
+ NSDictionary<NSString *, NSString *> *newHeaders = nil;
91
+ if (!newViewProps.headers.empty()) {
92
+ NSString *headersJson = [NSString stringWithUTF8String:newViewProps.headers.c_str()];
93
+ NSData *data = [headersJson dataUsingEncoding:NSUTF8StringEncoding];
94
+ if (data) {
95
+ newHeaders = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
96
+ }
97
+ }
98
+
99
+ // Handle contentMode changes
100
+ UIViewContentMode newContentMode = UIViewContentModeScaleAspectFill;
101
+ switch (newViewProps.contentMode) {
102
+ case GraniteImageContentMode::Cover:
103
+ newContentMode = UIViewContentModeScaleAspectFill;
104
+ break;
105
+ case GraniteImageContentMode::Contain:
106
+ newContentMode = UIViewContentModeScaleAspectFit;
107
+ break;
108
+ case GraniteImageContentMode::Stretch:
109
+ newContentMode = UIViewContentModeScaleToFill;
110
+ break;
111
+ case GraniteImageContentMode::Center:
112
+ newContentMode = UIViewContentModeCenter;
113
+ break;
114
+ }
115
+
116
+ // Handle priority
117
+ GraniteProviderPriority newPriority = GraniteProviderPriorityNormal;
118
+ switch (newViewProps.priority) {
119
+ case GraniteImagePriority::Low:
120
+ newPriority = GraniteProviderPriorityLow;
121
+ break;
122
+ case GraniteImagePriority::Normal:
123
+ newPriority = GraniteProviderPriorityNormal;
124
+ break;
125
+ case GraniteImagePriority::High:
126
+ newPriority = GraniteProviderPriorityHigh;
127
+ break;
128
+ }
129
+
130
+ // Handle cache policy
131
+ GraniteProviderCachePolicy newCachePolicy = GraniteProviderCachePolicyDisk;
132
+ switch (newViewProps.cachePolicy) {
133
+ case GraniteImageCachePolicy::Memory:
134
+ newCachePolicy = GraniteProviderCachePolicyMemory;
135
+ break;
136
+ case GraniteImageCachePolicy::Disk:
137
+ newCachePolicy = GraniteProviderCachePolicyDisk;
138
+ break;
139
+ case GraniteImageCachePolicy::None:
140
+ newCachePolicy = GraniteProviderCachePolicyNone;
141
+ break;
142
+ }
143
+
144
+ // Handle tintColor
145
+ UIColor *newTintColor = nil;
146
+ if (newViewProps.tintColor) {
147
+ newTintColor = RCTUIColorFromSharedColor(newViewProps.tintColor);
148
+ }
149
+
150
+ // Handle defaultSource
151
+ NSString *newDefaultSource = nil;
152
+ if (!newViewProps.defaultSource.empty()) {
153
+ newDefaultSource = [NSString stringWithUTF8String:newViewProps.defaultSource.c_str()];
154
+ }
155
+
156
+ // Handle fallbackSource
157
+ NSString *newFallbackSource = nil;
158
+ if (!newViewProps.fallbackSource.empty()) {
159
+ newFallbackSource = [NSString stringWithUTF8String:newViewProps.fallbackSource.c_str()];
160
+ }
161
+
162
+ BOOL shouldReload = _needsInitialLoad;
163
+ _needsInitialLoad = NO;
164
+
165
+ // Only URI change triggers image reload
166
+ if (![newUri isEqualToString:_currentUri ?: @""]) {
167
+ _currentUri = newUri;
168
+ shouldReload = YES;
169
+ }
170
+
171
+ // ContentMode: update in-place without reloading
172
+ if (newContentMode != _currentContentMode) {
173
+ _currentContentMode = newContentMode;
174
+ if (_containerView && [_containerView isKindOfClass:[UIImageView class]]) {
175
+ ((UIImageView *)_containerView).contentMode = newContentMode;
176
+ }
177
+ }
178
+
179
+ // TintColor: update in-place without reloading
180
+ BOOL tintColorChanged = (newTintColor != _currentTintColor) &&
181
+ (newTintColor == nil || ![newTintColor isEqual:_currentTintColor]);
182
+ if (tintColorChanged) {
183
+ _currentTintColor = newTintColor;
184
+ if (_containerView) {
185
+ id<GraniteImageProvidable> provider = [[GraniteImageRegistry shared] provider];
186
+ if (provider && [(NSObject *)provider respondsToSelector:@selector(applyTintColor:to:)]) {
187
+ [provider applyTintColor:newTintColor to:_containerView];
188
+ }
189
+ }
190
+ }
191
+
192
+ // Update stored values (these don't require reload, they affect next load)
193
+ _currentHeaders = newHeaders;
194
+ _currentPriority = newPriority;
195
+ _currentCachePolicy = newCachePolicy;
196
+ _currentDefaultSource = newDefaultSource;
197
+ _currentFallbackSource = newFallbackSource;
198
+
199
+ if (shouldReload) {
200
+ if (_currentUri.length > 0) {
201
+ [self loadImageWithProvider];
202
+ } else {
203
+ // URI가 비어있거나 nil인 경우 에러 발생
204
+ [self showErrorViewWithMessage:@"No URI provided"];
205
+ [self emitOnError:@"No URI provided"];
206
+ [self emitOnLoadEnd];
207
+ }
208
+ }
209
+
210
+ [super updateProps:props oldProps:oldProps];
211
+ }
212
+
213
+ - (void)emitOnLoadStart
214
+ {
215
+ if (_eventEmitter) {
216
+ std::dynamic_pointer_cast<const GraniteImageEventEmitter>(_eventEmitter)->onGraniteLoadStart({});
217
+ }
218
+ }
219
+
220
+ - (void)emitOnProgress:(int64_t)loaded total:(int64_t)total
221
+ {
222
+ if (_eventEmitter) {
223
+ GraniteImageEventEmitter::OnGraniteProgress event;
224
+ event.loaded = (int)loaded;
225
+ event.total = (int)total;
226
+ std::dynamic_pointer_cast<const GraniteImageEventEmitter>(_eventEmitter)->onGraniteProgress(event);
227
+ }
228
+ }
229
+
230
+ - (void)emitOnLoad:(CGSize)imageSize
231
+ {
232
+ if (_eventEmitter) {
233
+ GraniteImageEventEmitter::OnGraniteLoad event;
234
+ event.width = (int)imageSize.width;
235
+ event.height = (int)imageSize.height;
236
+ std::dynamic_pointer_cast<const GraniteImageEventEmitter>(_eventEmitter)->onGraniteLoad(event);
237
+ }
238
+ }
239
+
240
+ - (void)emitOnError:(NSString *)errorMessage
241
+ {
242
+ if (_eventEmitter) {
243
+ GraniteImageEventEmitter::OnGraniteError event;
244
+ event.error = std::string([errorMessage UTF8String]);
245
+ std::dynamic_pointer_cast<const GraniteImageEventEmitter>(_eventEmitter)->onGraniteError(event);
246
+ }
247
+ }
248
+
249
+ - (void)emitOnLoadEnd
250
+ {
251
+ if (_eventEmitter) {
252
+ std::dynamic_pointer_cast<const GraniteImageEventEmitter>(_eventEmitter)->onGraniteLoadEnd({});
253
+ }
254
+ }
255
+
256
+ - (void)loadImageWithProvider
257
+ {
258
+ // Remove existing container view
259
+ [_containerView removeFromSuperview];
260
+ _containerView = nil;
261
+
262
+ id<GraniteImageProvidable> provider = [[GraniteImageRegistry shared] provider];
263
+
264
+ if (!provider) {
265
+ [self showErrorViewWithMessage:@"No GraniteImageProvidable registered"];
266
+ [self emitOnError:@"No GraniteImageProvidable registered"];
267
+ return;
268
+ }
269
+
270
+ if (!_currentUri || _currentUri.length == 0) {
271
+ [self showErrorViewWithMessage:@"No URI provided"];
272
+ [self emitOnError:@"No URI provided"];
273
+ return;
274
+ }
275
+
276
+ // Emit load start event
277
+ [self emitOnLoadStart];
278
+
279
+ // Create new image view from provider
280
+ UIView *imageView = [provider createImageView];
281
+ imageView.frame = self.bounds;
282
+ imageView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
283
+ [self addSubview:imageView];
284
+ _containerView = imageView;
285
+
286
+ // Check if provider supports extended loading with callbacks
287
+ __weak GraniteImageComponentView *weakSelf = self;
288
+ if ([(NSObject *)provider respondsToSelector:@selector(loadImageWithURL:into:contentMode:headers:priority:cachePolicy:defaultSource:progress:completion:)]) {
289
+ [provider loadImageWithURL:_currentUri
290
+ into:imageView
291
+ contentMode:_currentContentMode
292
+ headers:_currentHeaders
293
+ priority:_currentPriority
294
+ cachePolicy:_currentCachePolicy
295
+ defaultSource:_currentDefaultSource
296
+ progress:^(int64_t loaded, int64_t total) {
297
+ dispatch_async(dispatch_get_main_queue(), ^{
298
+ [weakSelf emitOnProgress:loaded total:total];
299
+ });
300
+ }
301
+ completion:^(UIImage *image, NSError *error, CGSize imageSize) {
302
+ dispatch_async(dispatch_get_main_queue(), ^{
303
+ GraniteImageComponentView *strongSelf = weakSelf;
304
+ if (!strongSelf) return;
305
+
306
+ if (error) {
307
+ [strongSelf emitOnError:error.localizedDescription];
308
+
309
+ // Load fallback image if available
310
+ if (strongSelf->_currentFallbackSource.length > 0) {
311
+ [provider loadImageWithURL:strongSelf->_currentFallbackSource
312
+ into:imageView
313
+ contentMode:strongSelf->_currentContentMode
314
+ headers:nil
315
+ priority:GraniteProviderPriorityHigh
316
+ cachePolicy:GraniteProviderCachePolicyDisk
317
+ defaultSource:nil
318
+ progress:nil
319
+ completion:nil];
320
+ }
321
+ } else {
322
+ [strongSelf emitOnLoad:imageSize];
323
+ }
324
+ [strongSelf emitOnLoadEnd];
325
+
326
+ // Apply tint color if set
327
+ if (strongSelf->_currentTintColor && [(NSObject *)provider respondsToSelector:@selector(applyTintColor:to:)]) {
328
+ [provider applyTintColor:strongSelf->_currentTintColor to:imageView];
329
+ }
330
+ });
331
+ }];
332
+ } else {
333
+ // Fall back to simple loading
334
+ [provider loadImageWithURL:_currentUri into:imageView contentMode:_currentContentMode];
335
+
336
+ // For simple providers, emit load success after a short delay (since we don't know when it finishes)
337
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
338
+ GraniteImageComponentView *strongSelf = weakSelf;
339
+ if (strongSelf) {
340
+ // We don't have real image size, so use view bounds
341
+ [strongSelf emitOnLoad:strongSelf.bounds.size];
342
+ [strongSelf emitOnLoadEnd];
343
+ }
344
+ });
345
+ }
346
+ }
347
+
348
+ - (void)showErrorViewWithMessage:(NSString *)message
349
+ {
350
+ UIView *errorView = [[UIView alloc] initWithFrame:self.bounds];
351
+ errorView.backgroundColor = [UIColor redColor];
352
+ errorView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
353
+
354
+ UILabel *label = [[UILabel alloc] init];
355
+ label.text = message;
356
+ label.textColor = [UIColor whiteColor];
357
+ label.textAlignment = NSTextAlignmentCenter;
358
+ label.font = [UIFont systemFontOfSize:12];
359
+ label.numberOfLines = 0;
360
+ label.translatesAutoresizingMaskIntoConstraints = NO;
361
+
362
+ [errorView addSubview:label];
363
+ [NSLayoutConstraint activateConstraints:@[
364
+ [label.centerXAnchor constraintEqualToAnchor:errorView.centerXAnchor],
365
+ [label.centerYAnchor constraintEqualToAnchor:errorView.centerYAnchor],
366
+ [label.leadingAnchor constraintGreaterThanOrEqualToAnchor:errorView.leadingAnchor constant:8],
367
+ [label.trailingAnchor constraintLessThanOrEqualToAnchor:errorView.trailingAnchor constant:-8]
368
+ ]];
369
+
370
+ [self addSubview:errorView];
371
+ _containerView = errorView;
372
+ }
373
+
374
+ - (void)prepareForRecycle
375
+ {
376
+ [super prepareForRecycle];
377
+
378
+ id<GraniteImageProvidable> provider = [[GraniteImageRegistry shared] provider];
379
+ if (_containerView && provider) {
380
+ [provider cancelLoadWith:_containerView];
381
+ }
382
+
383
+ [_containerView removeFromSuperview];
384
+ _containerView = nil;
385
+ _currentUri = nil;
386
+ }
387
+
388
+ @end