@granite-js/image 0.1.34 → 1.0.1

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 +4 -276
  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,107 @@
1
+ import Foundation
2
+ import React
3
+
4
+ @objc(GraniteImageModule)
5
+ class GraniteImageModule: NSObject {
6
+
7
+ @objc static func requiresMainQueueSetup() -> Bool {
8
+ return false
9
+ }
10
+
11
+ @objc func preload(_ sourcesJson: String,
12
+ resolve: @escaping RCTPromiseResolveBlock,
13
+ reject: @escaping RCTPromiseRejectBlock) {
14
+ NSLog("[GraniteImageModule] preload called with: \(sourcesJson)")
15
+
16
+ guard let jsonData = sourcesJson.data(using: .utf8),
17
+ let sources = try? JSONSerialization.jsonObject(with: jsonData) as? [[String: Any]] else {
18
+ reject("PARSE_ERROR", "Failed to parse sources JSON", nil)
19
+ return
20
+ }
21
+
22
+ guard let provider = GraniteImageRegistry.shared.provider else {
23
+ reject("NO_PROVIDER", "No provider registered, cannot preload", nil)
24
+ return
25
+ }
26
+
27
+ DispatchQueue.global(qos: .default).async {
28
+ let group = DispatchGroup()
29
+ var successCount = 0
30
+ var failCount = 0
31
+ let countLock = NSLock()
32
+
33
+ for source in sources {
34
+ guard let uri = source["uri"] as? String else { continue }
35
+
36
+ let headers = source["headers"] as? [String: String]
37
+ let priorityStr = source["priority"] as? String
38
+ let cacheStr = source["cache"] as? String
39
+
40
+ var priority: GraniteProviderPriority = .normal
41
+ if priorityStr == "high" {
42
+ priority = .high
43
+ } else if priorityStr == "low" {
44
+ priority = .low
45
+ }
46
+
47
+ var cachePolicy: GraniteProviderCachePolicy = .disk
48
+ if cacheStr == "cacheOnly" {
49
+ cachePolicy = .disk
50
+ } else if cacheStr == "web" {
51
+ cachePolicy = .none
52
+ }
53
+
54
+ NSLog("[GraniteImageModule] Preloading: \(uri)")
55
+
56
+ // Use extended loading if available
57
+ group.enter()
58
+ if let loadImageExtended = provider.loadImage(withURL:into:contentMode:headers:priority:cachePolicy:defaultSource:progress:completion:) {
59
+ loadImageExtended(
60
+ uri,
61
+ nil,
62
+ .scaleAspectFill,
63
+ headers,
64
+ priority,
65
+ cachePolicy,
66
+ nil,
67
+ nil,
68
+ { image, error, imageSize in
69
+ countLock.lock()
70
+ if image != nil {
71
+ NSLog("[GraniteImageModule] Preloaded successfully: \(uri) (\(Int(imageSize.width))x\(Int(imageSize.height)))")
72
+ successCount += 1
73
+ } else {
74
+ NSLog("[GraniteImageModule] Preload failed for \(uri): \(error?.localizedDescription ?? "Unknown error")")
75
+ failCount += 1
76
+ }
77
+ countLock.unlock()
78
+ group.leave()
79
+ }
80
+ )
81
+ } else {
82
+ NSLog("[GraniteImageModule] Provider does not support preloading without view")
83
+ group.leave()
84
+ }
85
+ }
86
+
87
+ group.notify(queue: .main) {
88
+ NSLog("[GraniteImageModule] Preload completed: \(successCount) succeeded, \(failCount) failed")
89
+ resolve(nil)
90
+ }
91
+ }
92
+ }
93
+
94
+ @objc func clearMemoryCache(_ resolve: @escaping RCTPromiseResolveBlock,
95
+ reject: @escaping RCTPromiseRejectBlock) {
96
+ NSLog("[GraniteImageModule] clearMemoryCache called")
97
+ URLCache.shared.removeAllCachedResponses()
98
+ resolve(nil)
99
+ }
100
+
101
+ @objc func clearDiskCache(_ resolve: @escaping RCTPromiseResolveBlock,
102
+ reject: @escaping RCTPromiseRejectBlock) {
103
+ NSLog("[GraniteImageModule] clearDiskCache called")
104
+ URLCache.shared.removeAllCachedResponses()
105
+ resolve(nil)
106
+ }
107
+ }
@@ -0,0 +1,15 @@
1
+ #import <React/RCTBridgeModule.h>
2
+
3
+ @interface RCT_EXTERN_MODULE(GraniteImageModule, NSObject)
4
+
5
+ RCT_EXTERN_METHOD(preload:(NSString *)sourcesJson
6
+ resolve:(RCTPromiseResolveBlock)resolve
7
+ reject:(RCTPromiseRejectBlock)reject)
8
+
9
+ RCT_EXTERN_METHOD(clearMemoryCache:(RCTPromiseResolveBlock)resolve
10
+ reject:(RCTPromiseRejectBlock)reject)
11
+
12
+ RCT_EXTERN_METHOD(clearDiskCache:(RCTPromiseResolveBlock)resolve
13
+ reject:(RCTPromiseRejectBlock)reject)
14
+
15
+ @end
@@ -0,0 +1,70 @@
1
+ import UIKit
2
+
3
+ /// Priority levels for image loading
4
+ @objc public enum GraniteProviderPriority: Int {
5
+ case low = 0
6
+ case normal = 1
7
+ case high = 2
8
+ }
9
+
10
+ /// Cache policy for image loading
11
+ @objc public enum GraniteProviderCachePolicy: Int {
12
+ case memory = 0
13
+ case disk = 1
14
+ case none = 2
15
+ }
16
+
17
+ /// Progress callback block
18
+ public typealias GraniteImageProgressBlock = (_ loaded: Int64, _ total: Int64) -> Void
19
+
20
+ /// Completion callback block
21
+ public typealias GraniteImageCompletionBlock = (_ image: UIImage?, _ error: Error?, _ imageSize: CGSize) -> Void
22
+
23
+ /// Protocol that defines the interface for image loading providers.
24
+ /// Implementations can use any image loading library (URLSession, SDWebImage, Kingfisher, etc.)
25
+ @objc public protocol GraniteImageProvidable: AnyObject {
26
+
27
+ /// Creates and returns a new UIView that will be used to display the image.
28
+ /// The returned view should be capable of displaying images (typically UIImageView).
29
+ @objc func createImageView() -> UIView
30
+
31
+ /// Loads an image from the given URL into the provided view.
32
+ /// - Parameters:
33
+ /// - url: The URL string of the image to load
34
+ /// - view: The view returned from createImageView where the image should be displayed
35
+ /// - contentMode: The content mode to use when displaying the image
36
+ @objc func loadImage(withURL url: String, into view: UIView, contentMode: UIView.ContentMode)
37
+
38
+ /// Cancels any ongoing image load for the given view.
39
+ /// - Parameter view: The view whose image load should be cancelled
40
+ @objc func cancelLoad(with view: UIView)
41
+
42
+ /// Loads an image with full options support including headers, priority, cache policy, and callbacks.
43
+ /// - Parameters:
44
+ /// - url: The URL string of the image to load
45
+ /// - view: The view returned from createImageView where the image should be displayed (can be nil for preloading)
46
+ /// - contentMode: The content mode to use when displaying the image
47
+ /// - headers: Optional HTTP headers to include in the request
48
+ /// - priority: The loading priority
49
+ /// - cachePolicy: The cache policy to use
50
+ /// - defaultSource: Optional placeholder image URL or asset name to show while loading
51
+ /// - progress: Optional closure called with loading progress
52
+ /// - completion: Optional closure called when loading completes
53
+ @objc optional func loadImage(
54
+ withURL url: String,
55
+ into view: UIView?,
56
+ contentMode: UIView.ContentMode,
57
+ headers: [String: String]?,
58
+ priority: GraniteProviderPriority,
59
+ cachePolicy: GraniteProviderCachePolicy,
60
+ defaultSource: String?,
61
+ progress: GraniteImageProgressBlock?,
62
+ completion: GraniteImageCompletionBlock?
63
+ )
64
+
65
+ /// Applies a tint color to the image view
66
+ /// - Parameters:
67
+ /// - tintColor: The color to apply
68
+ /// - view: The view to tint
69
+ @objc optional func applyTintColor(_ tintColor: UIColor, to view: UIView)
70
+ }
@@ -0,0 +1,30 @@
1
+ import Foundation
2
+
3
+ /// Singleton registry for managing the current GraniteImageProvidable.
4
+ /// Applications should register their provider implementation at app startup.
5
+ @objc public class GraniteImageRegistry: NSObject {
6
+
7
+ /// Shared singleton instance
8
+ @objc public static let shared = GraniteImageRegistry()
9
+
10
+ /// The currently registered image provider.
11
+ /// If nil, GraniteImage will display an error state.
12
+ @objc public var provider: GraniteImageProvidable?
13
+
14
+ private override init() {
15
+ super.init()
16
+ }
17
+
18
+ /// Registers an image provider to be used by all GraniteImage instances.
19
+ /// - Parameter provider: The provider implementation to register
20
+ @objc public func register(provider: GraniteImageProvidable) {
21
+ self.provider = provider
22
+ NSLog("[GraniteImageRegistry] Provider registered: \(type(of: provider))")
23
+ }
24
+
25
+ /// Clears the currently registered provider.
26
+ @objc public func clearProvider() {
27
+ self.provider = nil
28
+ NSLog("[GraniteImageRegistry] Provider cleared")
29
+ }
30
+ }
@@ -0,0 +1,175 @@
1
+ #if GRANITE_IMAGE_DEFAULT_PROVIDER
2
+
3
+ import UIKit
4
+ import SDWebImage
5
+
6
+ @objc public class SDWebImageProvider: NSObject, GraniteImageProvidable {
7
+
8
+ @objc public func createImageView() -> UIView {
9
+ let imageView = UIImageView()
10
+ imageView.backgroundColor = .lightGray
11
+ return imageView
12
+ }
13
+
14
+ @objc public func loadImage(withURL url: String, into view: UIView, contentMode: UIView.ContentMode) {
15
+ guard let imageView = view as? UIImageView else {
16
+ NSLog("[SDWebImageProvider] View is not UIImageView")
17
+ return
18
+ }
19
+
20
+ imageView.contentMode = contentMode
21
+
22
+ guard let imageURL = URL(string: url) else {
23
+ NSLog("[SDWebImageProvider] Invalid URL: \(url)")
24
+ return
25
+ }
26
+
27
+ imageView.sd_setImage(
28
+ with: imageURL,
29
+ placeholderImage: nil,
30
+ options: .retryFailed
31
+ ) { _, error, cacheType, _ in
32
+ if let error = error {
33
+ NSLog("[SDWebImageProvider] Error loading image: \(error.localizedDescription)")
34
+ } else {
35
+ let cacheTypeStr: String
36
+ switch cacheType {
37
+ case .none: cacheTypeStr = "Network"
38
+ case .disk: cacheTypeStr = "Disk"
39
+ case .memory: cacheTypeStr = "Memory"
40
+ default: cacheTypeStr = "Unknown"
41
+ }
42
+ NSLog("[SDWebImageProvider] Loaded with SDWebImage (\(cacheTypeStr)): \(url)")
43
+ }
44
+ }
45
+ }
46
+
47
+ @objc public func cancelLoad(with view: UIView) {
48
+ guard let imageView = view as? UIImageView else { return }
49
+ imageView.sd_cancelCurrentImageLoad()
50
+ }
51
+
52
+ @objc public func loadImage(
53
+ withURL url: String,
54
+ into view: UIView?,
55
+ contentMode: UIView.ContentMode,
56
+ headers: [String: String]?,
57
+ priority: GraniteProviderPriority,
58
+ cachePolicy: GraniteProviderCachePolicy,
59
+ defaultSource: String?,
60
+ progress progressBlock: GraniteImageProgressBlock?,
61
+ completion completionBlock: GraniteImageCompletionBlock?
62
+ ) {
63
+ // Allow nil view for preloading
64
+ var imageView: UIImageView? = nil
65
+ if let view = view {
66
+ guard let iv = view as? UIImageView else {
67
+ NSLog("[SDWebImageProvider] View is not UIImageView")
68
+ completionBlock?(nil, NSError(domain: "SDWebImageProvider", code: -1, userInfo: [NSLocalizedDescriptionKey: "View is not UIImageView"]), .zero)
69
+ return
70
+ }
71
+ imageView = iv
72
+ imageView?.contentMode = contentMode
73
+ }
74
+
75
+ guard let imageURL = URL(string: url),
76
+ let scheme = imageURL.scheme?.lowercased(),
77
+ scheme == "http" || scheme == "https" else {
78
+ NSLog("[SDWebImageProvider] Invalid URL: \(url)")
79
+ completionBlock?(nil, NSError(domain: "SDWebImageProvider", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid URL"]), .zero)
80
+ return
81
+ }
82
+
83
+ // Load placeholder image if provided
84
+ var placeholderImage: UIImage? = nil
85
+ if let defaultSource = defaultSource, !defaultSource.isEmpty {
86
+ placeholderImage = UIImage(named: defaultSource)
87
+ }
88
+
89
+ // Build options
90
+ var options: SDWebImageOptions = .retryFailed
91
+ switch priority {
92
+ case .low:
93
+ options.insert(.lowPriority)
94
+ case .high:
95
+ options.insert(.highPriority)
96
+ case .normal:
97
+ break
98
+ }
99
+
100
+ // Build context with headers
101
+ var context: [SDWebImageContextOption: Any]? = nil
102
+ if let headers = headers, !headers.isEmpty {
103
+ let modifier = SDWebImageDownloadRequestModifier { request in
104
+ var mutableRequest = request
105
+ for (key, value) in headers {
106
+ mutableRequest.setValue(value, forHTTPHeaderField: key)
107
+ }
108
+ return mutableRequest
109
+ }
110
+ context = [.downloadRequestModifier: modifier]
111
+ }
112
+
113
+ if let imageView = imageView {
114
+ imageView.sd_setImage(
115
+ with: imageURL,
116
+ placeholderImage: placeholderImage,
117
+ options: options,
118
+ context: context,
119
+ progress: { receivedSize, expectedSize, _ in
120
+ progressBlock?(Int64(receivedSize), Int64(expectedSize))
121
+ },
122
+ completed: { image, error, cacheType, _ in
123
+ if let error = error {
124
+ NSLog("[SDWebImageProvider] Error loading image: \(error.localizedDescription)")
125
+ completionBlock?(nil, error, .zero)
126
+ } else {
127
+ let cacheTypeStr: String
128
+ switch cacheType {
129
+ case .none: cacheTypeStr = "Network"
130
+ case .disk: cacheTypeStr = "Disk"
131
+ case .memory: cacheTypeStr = "Memory"
132
+ default: cacheTypeStr = "Unknown"
133
+ }
134
+ NSLog("[SDWebImageProvider] Loaded with SDWebImage (\(cacheTypeStr)): \(url)")
135
+ completionBlock?(image, nil, image?.size ?? .zero)
136
+ }
137
+ }
138
+ )
139
+ } else {
140
+ // Preload without view
141
+ SDWebImageManager.shared.loadImage(
142
+ with: imageURL,
143
+ options: options,
144
+ context: context,
145
+ progress: { receivedSize, expectedSize, _ in
146
+ progressBlock?(Int64(receivedSize), Int64(expectedSize))
147
+ },
148
+ completed: { image, _, error, cacheType, _, _ in
149
+ if let error = error {
150
+ NSLog("[SDWebImageProvider] Error preloading image: \(error.localizedDescription)")
151
+ completionBlock?(nil, error, .zero)
152
+ } else {
153
+ let cacheTypeStr: String
154
+ switch cacheType {
155
+ case .none: cacheTypeStr = "Network"
156
+ case .disk: cacheTypeStr = "Disk"
157
+ case .memory: cacheTypeStr = "Memory"
158
+ default: cacheTypeStr = "Unknown"
159
+ }
160
+ NSLog("[SDWebImageProvider] Preloaded with SDWebImage (\(cacheTypeStr)): \(url)")
161
+ completionBlock?(image, nil, image?.size ?? .zero)
162
+ }
163
+ }
164
+ )
165
+ }
166
+ }
167
+
168
+ @objc public func applyTintColor(_ tintColor: UIColor, to view: UIView) {
169
+ guard let imageView = view as? UIImageView else { return }
170
+ imageView.image = imageView.image?.withRenderingMode(.alwaysTemplate)
171
+ imageView.tintColor = tintColor
172
+ }
173
+ }
174
+
175
+ #endif
package/package.json CHANGED
@@ -1,53 +1,92 @@
1
1
  {
2
2
  "name": "@granite-js/image",
3
- "version": "0.1.34",
3
+ "version": "1.0.1",
4
+ "description": "A pluggable React Native image component that lets you use your existing native image loading infrastructure",
5
+ "type": "module",
6
+ "main": "./dist/module/index.js",
7
+ "types": "./dist/typescript/index.d.ts",
8
+ "react-native": "./src/index.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/typescript/index.d.ts",
12
+ "react-native": "./src/index.ts",
13
+ "default": "./dist/module/index.js"
14
+ },
15
+ "./package.json": "./package.json"
16
+ },
17
+ "files": [
18
+ "src",
19
+ "dist",
20
+ "android",
21
+ "ios",
22
+ "*.podspec",
23
+ "react-native.config.js"
24
+ ],
4
25
  "scripts": {
5
26
  "prepack": "yarn build",
27
+ "example": "yarn workspace @granite-js/image-example",
28
+ "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib",
6
29
  "typecheck": "tsc --noEmit",
7
30
  "lint": "eslint .",
8
- "build": "tsdown"
31
+ "build": "bob build && tsc --project tsconfig.build.json"
9
32
  },
10
- "main": "./dist/index.js",
11
- "module": "./dist/index.mjs",
12
- "types": "./dist/index.d.ts",
33
+ "keywords": [
34
+ "react-native",
35
+ "image",
36
+ "fast-image",
37
+ "ios",
38
+ "android",
39
+ "brownfield",
40
+ "kingfisher",
41
+ "glide",
42
+ "coil",
43
+ "sdwebimage"
44
+ ],
13
45
  "repository": {
14
46
  "type": "git",
15
47
  "url": "git+https://github.com/toss/granite.git",
16
48
  "directory": "packages/image"
17
49
  },
18
- "exports": {
19
- ".": {
20
- "import": {
21
- "types": "./dist/index.d.mts",
22
- "default": "./dist/index.mjs"
23
- },
24
- "require": {
25
- "types": "./dist/index.d.ts",
26
- "default": "./dist/index.js"
27
- }
28
- },
29
- "./package.json": "./package.json"
30
- },
31
- "files": [
32
- "dist"
33
- ],
50
+ "author": "Toss <platform@toss.im>",
51
+ "homepage": "https://github.com/toss/granite/tree/main/packages/image#readme",
52
+ "license": "MIT",
34
53
  "devDependencies": {
35
- "@granite-js/native": "0.1.34",
36
- "@types/react": "18.3.3",
54
+ "@types/react": "19.2.0",
55
+ "del-cli": "^6.0.0",
37
56
  "eslint": "^9.7.0",
38
- "react": "18.2.0",
39
- "react-native": "0.72.6",
40
- "tsdown": "^0.16.5",
41
- "typescript": "5.8.3"
57
+ "react": "19.2.3",
58
+ "react-native": "0.84.0-rc.5",
59
+ "react-native-builder-bob": "0.40.17",
60
+ "typescript": "5.9.3"
42
61
  },
43
62
  "peerDependencies": {
44
- "@granite-js/native": "*",
45
- "@types/react": "*",
46
63
  "react": "*",
47
64
  "react-native": "*"
48
65
  },
49
- "dependencies": {
50
- "react-simplikit": "^0.0.40"
66
+ "codegenConfig": {
67
+ "name": "GraniteImageSpec",
68
+ "type": "components",
69
+ "jsSrcsDir": "src",
70
+ "android": {
71
+ "javaPackageName": "com.facebook.react.viewmanagers"
72
+ },
73
+ "ios": {
74
+ "componentProvider": {
75
+ "GraniteImage": "GraniteImageComponentView"
76
+ }
77
+ }
51
78
  },
52
- "sideEffects": false
79
+ "sideEffects": false,
80
+ "react-native-builder-bob": {
81
+ "source": "src",
82
+ "output": "dist",
83
+ "targets": [
84
+ [
85
+ "module",
86
+ {
87
+ "esm": true
88
+ }
89
+ ]
90
+ ]
91
+ }
53
92
  }