zkit-ui 2.0.4 → 2.0.6

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 (86) hide show
  1. package/README.md +56 -0
  2. package/android/build.gradle +100 -0
  3. package/android/src/main/AndroidManifest.xml +1 -0
  4. package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewLayoutSupport.java +450 -0
  5. package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewMediaItem.java +36 -0
  6. package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewModule.kt +43 -0
  7. package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewPagerAdapter.java +140 -0
  8. package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewSharedElementGeometry.java +25 -0
  9. package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewSharedElementNames.java +39 -0
  10. package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewSharedElementState.java +25 -0
  11. package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewSourceViewRegistry.java +115 -0
  12. package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewTransitionSnapshotView.java +60 -0
  13. package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewVideoPlayerView.java +338 -0
  14. package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewView.kt +305 -0
  15. package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewViewerOverlayView.java +636 -0
  16. package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewViewerPageView.java +357 -0
  17. package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewViewerTransitionController.java +223 -0
  18. package/android/src/main/res/layout/native_image_preview_player_view.xml +9 -0
  19. package/dist/all.d.ts +2 -0
  20. package/dist/all.d.ts.map +1 -1
  21. package/dist/all.js +24 -22
  22. package/dist/ui/NativeImagePreview/NativeImagePreview.d.ts +3 -0
  23. package/dist/ui/NativeImagePreview/NativeImagePreview.d.ts.map +1 -0
  24. package/dist/ui/NativeImagePreview/NativeImagePreview.js +99 -0
  25. package/dist/ui/NativeImagePreview/NativeImagePreview.web.d.ts +4 -0
  26. package/dist/ui/NativeImagePreview/NativeImagePreview.web.d.ts.map +1 -0
  27. package/dist/ui/NativeImagePreview/NativeImagePreview.web.js +150 -0
  28. package/dist/ui/NativeImagePreview/context.d.ts +14 -0
  29. package/dist/ui/NativeImagePreview/context.d.ts.map +1 -0
  30. package/dist/ui/NativeImagePreview/context.js +38 -0
  31. package/dist/ui/NativeImagePreview/index.d.ts +3 -0
  32. package/dist/ui/NativeImagePreview/index.d.ts.map +1 -0
  33. package/dist/ui/NativeImagePreview/index.js +5 -0
  34. package/dist/ui/NativeImagePreview/types.d.ts +68 -0
  35. package/dist/ui/NativeImagePreview/types.d.ts.map +1 -0
  36. package/dist/ui/NativeImagePreview/types.js +2 -0
  37. package/dist/ui/NativeImagePreview/utils.d.ts +8 -0
  38. package/dist/ui/NativeImagePreview/utils.d.ts.map +1 -0
  39. package/dist/ui/NativeImagePreview/utils.js +83 -0
  40. package/dist/ui/NativeImagePreview/web/geometry.d.ts +20 -0
  41. package/dist/ui/NativeImagePreview/web/geometry.d.ts.map +1 -0
  42. package/dist/ui/NativeImagePreview/web/geometry.js +121 -0
  43. package/dist/ui/NativeImagePreview/web/media.d.ts +6 -0
  44. package/dist/ui/NativeImagePreview/web/media.d.ts.map +1 -0
  45. package/dist/ui/NativeImagePreview/web/media.js +90 -0
  46. package/dist/ui/NativeImagePreview/web/styles.css +137 -0
  47. package/dist/ui/NativeImagePreview/web/transition.d.ts +15 -0
  48. package/dist/ui/NativeImagePreview/web/transition.d.ts.map +1 -0
  49. package/dist/ui/NativeImagePreview/web/transition.js +77 -0
  50. package/dist/ui/NativeImagePreview/web/types.d.ts +74 -0
  51. package/dist/ui/NativeImagePreview/web/types.d.ts.map +1 -0
  52. package/dist/ui/NativeImagePreview/web/types.js +3 -0
  53. package/dist/ui/NativeImagePreview/web/viewer.d.ts +104 -0
  54. package/dist/ui/NativeImagePreview/web/viewer.d.ts.map +1 -0
  55. package/dist/ui/NativeImagePreview/web/viewer.js +894 -0
  56. package/expo-module.config.json +11 -0
  57. package/ios/NativeImagePreview/NativeImagePreviewImageLoader.swift +73 -0
  58. package/ios/NativeImagePreview/NativeImagePreviewImageViewInteraction.swift +245 -0
  59. package/ios/NativeImagePreview/NativeImagePreviewLayoutSupport.swift +143 -0
  60. package/ios/NativeImagePreview/NativeImagePreviewMedia.swift +28 -0
  61. package/ios/NativeImagePreview/NativeImagePreviewSourceViewRegistry.swift +80 -0
  62. package/ios/NativeImagePreview/NativeImagePreviewVideoPlayerView.swift +437 -0
  63. package/ios/NativeImagePreview/NativeImagePreviewViewerConfiguration.swift +51 -0
  64. package/ios/NativeImagePreview/NativeImagePreviewViewerController.swift +745 -0
  65. package/ios/NativeImagePreview/NativeImagePreviewViewerPageView.swift +623 -0
  66. package/ios/NativeImagePreview/NativeImagePreviewViewerTransitionCoordinator.swift +312 -0
  67. package/ios/NativeImagePreviewModule.swift +46 -0
  68. package/ios/NativeImagePreviewView.swift +252 -0
  69. package/package.json +16 -2
  70. package/src/all.ts +18 -0
  71. package/src/services/ImagePreviewService/README.md +19 -0
  72. package/src/types.d.ts +1 -0
  73. package/src/ui/NativeImagePreview/NativeImagePreview.tsx +146 -0
  74. package/src/ui/NativeImagePreview/NativeImagePreview.web.tsx +201 -0
  75. package/src/ui/NativeImagePreview/README.md +111 -0
  76. package/src/ui/NativeImagePreview/context.tsx +22 -0
  77. package/src/ui/NativeImagePreview/index.tsx +18 -0
  78. package/src/ui/NativeImagePreview/types.ts +94 -0
  79. package/src/ui/NativeImagePreview/utils.ts +108 -0
  80. package/src/ui/NativeImagePreview/web/geometry.ts +134 -0
  81. package/src/ui/NativeImagePreview/web/media.ts +91 -0
  82. package/src/ui/NativeImagePreview/web/styles.css +137 -0
  83. package/src/ui/NativeImagePreview/web/transition.ts +111 -0
  84. package/src/ui/NativeImagePreview/web/types.ts +88 -0
  85. package/src/ui/NativeImagePreview/web/viewer.ts +1024 -0
  86. package/zkit-ui.podspec +13 -1
@@ -0,0 +1,312 @@
1
+ import UIKit
2
+
3
+ private final class NativeImagePreviewTransitionSnapshotView: UIView {
4
+ private let imageView = UIImageView()
5
+
6
+ init(image: UIImage) {
7
+ super.init(frame: .zero)
8
+ backgroundColor = .clear
9
+ clipsToBounds = true
10
+ isUserInteractionEnabled = false
11
+
12
+ imageView.image = image
13
+ imageView.backgroundColor = .clear
14
+ imageView.contentMode = .scaleToFill
15
+ addSubview(imageView)
16
+ }
17
+
18
+ required init?(coder: NSCoder) {
19
+ fatalError("init(coder:) has not been implemented")
20
+ }
21
+
22
+ func updateImage(_ image: UIImage) {
23
+ imageView.image = image
24
+ }
25
+
26
+ func applyContentFrame(_ frame: CGRect) {
27
+ imageView.frame = frame
28
+ }
29
+ }
30
+
31
+ final class NativeImagePreviewViewerTransitionCoordinator {
32
+ private weak var containerView: UIView?
33
+ private weak var hiddenAnchorView: UIView?
34
+ private weak var hiddenPageView: NativeImagePreviewViewerPageView?
35
+ private var activeSnapshotView: UIView?
36
+
37
+ init(containerView: UIView) {
38
+ self.containerView = containerView
39
+ }
40
+
41
+ static func aspectFitRect(for imageSize: CGSize, in bounds: CGRect) -> CGRect {
42
+ guard imageSize.width > 0, imageSize.height > 0 else { return bounds }
43
+
44
+ let widthRatio = bounds.width / imageSize.width
45
+ let heightRatio = bounds.height / imageSize.height
46
+ let scale = min(widthRatio, heightRatio)
47
+ let fittedSize = CGSize(
48
+ width: imageSize.width * scale,
49
+ height: imageSize.height * scale
50
+ )
51
+
52
+ return CGRect(
53
+ x: bounds.midX - fittedSize.width * 0.5,
54
+ y: bounds.midY - fittedSize.height * 0.5,
55
+ width: fittedSize.width,
56
+ height: fittedSize.height
57
+ )
58
+ }
59
+
60
+ func performOpenTransition(
61
+ from sourceView: UIImageView?,
62
+ to pageView: NativeImagePreviewViewerPageView?,
63
+ backgroundView: UIView,
64
+ contentView: UIView,
65
+ completion: @escaping () -> Void
66
+ ) {
67
+ guard let containerView = containerView else {
68
+ completion()
69
+ return
70
+ }
71
+
72
+ restoreHiddenViews()
73
+ activeSnapshotView?.removeFromSuperview()
74
+ activeSnapshotView = nil
75
+
76
+ backgroundView.alpha = 0
77
+ contentView.alpha = 0
78
+
79
+ flushLayoutForSharedElementSampling(of: sourceView)
80
+ guard let sourceView = sourceView, let sourceState = sourceView.nativeImagePreviewSharedElementState() else {
81
+ animateSimpleAppearance(
82
+ backgroundView: backgroundView,
83
+ contentView: contentView,
84
+ completion: completion
85
+ )
86
+ return
87
+ }
88
+
89
+ let snapshotView = makeSnapshot(from: sourceState)
90
+ apply(sourceState.geometry, to: snapshotView)
91
+ containerView.addSubview(snapshotView)
92
+ activeSnapshotView = snapshotView
93
+
94
+ hiddenAnchorView = sourceView
95
+ sourceView.alpha = 0
96
+ pageView?.setMediaHidden(true)
97
+ hiddenPageView = pageView
98
+
99
+ let animateIntoPage: (Bool) -> Void = { [weak self] isReady in
100
+ guard let self = self, let containerView = self.containerView else {
101
+ completion()
102
+ return
103
+ }
104
+
105
+ let windowBounds = containerView.window?.bounds ?? UIScreen.main.bounds
106
+ let targetGeometry: NativeImagePreviewSharedElementGeometry
107
+ if isReady, let pageState = pageView?.sharedElementState() {
108
+ targetGeometry = pageState.geometry
109
+ } else if let pageView = pageView {
110
+ targetGeometry = pageView.defaultTransitionGeometry(in: windowBounds)
111
+ } else {
112
+ targetGeometry = self.defaultGeometry(for: sourceState.image.size, in: windowBounds)
113
+ }
114
+
115
+ UIView.animate(
116
+ withDuration: 0.32,
117
+ delay: 0,
118
+ usingSpringWithDamping: 0.94,
119
+ initialSpringVelocity: 0.08,
120
+ options: [.curveEaseInOut, .allowUserInteraction]
121
+ ) {
122
+ backgroundView.alpha = 1
123
+ self.apply(targetGeometry, to: snapshotView)
124
+ } completion: { _ in
125
+ self.restoreHiddenViews()
126
+ contentView.alpha = 1
127
+ snapshotView.removeFromSuperview()
128
+ self.activeSnapshotView = nil
129
+ completion()
130
+ }
131
+ }
132
+
133
+ guard let pageView = pageView else {
134
+ animateIntoPage(false)
135
+ return
136
+ }
137
+
138
+ waitUntilReady(pageView: pageView, timeout: 0.42) { ready in
139
+ animateIntoPage(ready)
140
+ }
141
+ }
142
+
143
+ func performCloseTransition(
144
+ from pageView: NativeImagePreviewViewerPageView?,
145
+ to anchorView: UIImageView?,
146
+ backgroundView: UIView,
147
+ contentView: UIView,
148
+ completion: @escaping () -> Void
149
+ ) {
150
+ guard let containerView = containerView else {
151
+ completion()
152
+ return
153
+ }
154
+
155
+ restoreHiddenViews()
156
+ activeSnapshotView?.removeFromSuperview()
157
+ activeSnapshotView = nil
158
+
159
+ guard
160
+ let pageView,
161
+ let pageState = pageView.sharedElementState()
162
+ else {
163
+ animateSimpleDismissal(
164
+ backgroundView: backgroundView,
165
+ contentView: contentView,
166
+ completion: completion
167
+ )
168
+ return
169
+ }
170
+
171
+ let snapshotView = makeSnapshot(from: pageState)
172
+ apply(pageState.geometry, to: snapshotView)
173
+ containerView.addSubview(snapshotView)
174
+ activeSnapshotView = snapshotView
175
+
176
+ hiddenPageView = pageView
177
+ pageView.setMediaHidden(true)
178
+
179
+ var targetGeometry: NativeImagePreviewSharedElementGeometry?
180
+ flushLayoutForSharedElementSampling(of: anchorView)
181
+ if let anchorView = anchorView, let anchorState = anchorView.nativeImagePreviewSharedElementState() {
182
+ hiddenAnchorView = anchorView
183
+ anchorView.alpha = 0
184
+ targetGeometry = anchorState.geometry
185
+ }
186
+
187
+ UIView.animate(
188
+ withDuration: 0.24,
189
+ delay: 0,
190
+ options: [.curveEaseInOut, .allowUserInteraction]
191
+ ) {
192
+ backgroundView.alpha = 0
193
+ contentView.alpha = 0
194
+ if let targetGeometry = targetGeometry {
195
+ self.apply(targetGeometry, to: snapshotView)
196
+ } else {
197
+ snapshotView.alpha = 0
198
+ snapshotView.transform = CGAffineTransform(scaleX: 0.92, y: 0.92)
199
+ }
200
+ } completion: { [weak self] _ in
201
+ self?.restoreHiddenViews()
202
+ snapshotView.removeFromSuperview()
203
+ self?.activeSnapshotView = nil
204
+ completion()
205
+ }
206
+ }
207
+
208
+ private func animateSimpleAppearance(
209
+ backgroundView: UIView,
210
+ contentView: UIView,
211
+ completion: @escaping () -> Void
212
+ ) {
213
+ UIView.animate(
214
+ withDuration: 0.18,
215
+ delay: 0,
216
+ options: [.curveEaseOut, .allowUserInteraction]
217
+ ) {
218
+ backgroundView.alpha = 1
219
+ contentView.alpha = 1
220
+ } completion: { _ in
221
+ completion()
222
+ }
223
+ }
224
+
225
+ private func animateSimpleDismissal(
226
+ backgroundView: UIView,
227
+ contentView: UIView,
228
+ completion: @escaping () -> Void
229
+ ) {
230
+ UIView.animate(
231
+ withDuration: 0.18,
232
+ delay: 0,
233
+ options: [.curveEaseIn, .allowUserInteraction]
234
+ ) {
235
+ backgroundView.alpha = 0
236
+ contentView.alpha = 0
237
+ } completion: { _ in
238
+ completion()
239
+ }
240
+ }
241
+
242
+ private func waitUntilReady(
243
+ pageView: NativeImagePreviewViewerPageView,
244
+ timeout: TimeInterval,
245
+ completion: @escaping (Bool) -> Void
246
+ ) {
247
+ let deadline = CACurrentMediaTime() + timeout
248
+ var lastStableState: NativeImagePreviewSharedElementState?
249
+ var stableFrameCount = 0
250
+
251
+ func poll() {
252
+ pageView.layoutIfNeeded()
253
+ if let currentState = pageView.sharedElementState() {
254
+ if let lastStableState = lastStableState, lastStableState.approximatelyEquals(currentState) {
255
+ stableFrameCount += 1
256
+ } else {
257
+ lastStableState = currentState
258
+ stableFrameCount = 1
259
+ }
260
+
261
+ if stableFrameCount >= 2 {
262
+ completion(true)
263
+ return
264
+ }
265
+ }
266
+ if CACurrentMediaTime() >= deadline {
267
+ completion(false)
268
+ return
269
+ }
270
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.016, execute: poll)
271
+ }
272
+
273
+ poll()
274
+ }
275
+
276
+ private func frameInContainer(fromWindowFrame windowFrame: CGRect) -> CGRect {
277
+ guard let containerView = containerView else { return windowFrame }
278
+ return containerView.convert(windowFrame, from: nil)
279
+ }
280
+
281
+ private func flushLayoutForSharedElementSampling(of imageView: UIImageView?) {
282
+ imageView?.superview?.layoutIfNeeded()
283
+ imageView?.layoutIfNeeded()
284
+ containerView?.layoutIfNeeded()
285
+ }
286
+
287
+ private func makeSnapshot(from state: NativeImagePreviewSharedElementState) -> NativeImagePreviewTransitionSnapshotView {
288
+ NativeImagePreviewTransitionSnapshotView(image: state.image)
289
+ }
290
+
291
+ private func apply(_ geometry: NativeImagePreviewSharedElementGeometry, to snapshotView: NativeImagePreviewTransitionSnapshotView) {
292
+ snapshotView.frame = frameInContainer(fromWindowFrame: geometry.visibleFrameInWindow)
293
+ snapshotView.applyContentFrame(geometry.contentFrameInVisibleBounds)
294
+ snapshotView.layer.cornerRadius = geometry.cornerRadius
295
+ }
296
+
297
+ private func defaultGeometry(for imageSize: CGSize, in windowBounds: CGRect) -> NativeImagePreviewSharedElementGeometry {
298
+ let visibleFrame = Self.aspectFitRect(for: imageSize, in: windowBounds)
299
+ return NativeImagePreviewSharedElementGeometry(
300
+ visibleFrameInWindow: visibleFrame,
301
+ contentFrameInVisibleBounds: CGRect(origin: .zero, size: visibleFrame.size),
302
+ cornerRadius: 0
303
+ )
304
+ }
305
+
306
+ private func restoreHiddenViews() {
307
+ hiddenAnchorView?.alpha = 1
308
+ hiddenAnchorView = nil
309
+ hiddenPageView?.setMediaHidden(false)
310
+ hiddenPageView = nil
311
+ }
312
+ }
@@ -0,0 +1,46 @@
1
+ import ExpoModulesCore
2
+
3
+ public class NativeImagePreviewModule: Module {
4
+ public func definition() -> ModuleDefinition {
5
+ Name("ZKitNativeImagePreview")
6
+
7
+ View(NativeImagePreviewView.self) {
8
+ Events("onIndexChange")
9
+
10
+ OnViewDidUpdateProps { (view) in
11
+ view.setupImageView()
12
+ }
13
+
14
+ Prop("urls") { (view, urls: [String]?) in
15
+ view.urls = urls
16
+ }
17
+
18
+ Prop("items") { (view, items: [[String: Any]]?) in
19
+ view.items = items
20
+ }
21
+
22
+ Prop("index") { (view, index: Int?) in
23
+ view.initialIndex = index
24
+ }
25
+
26
+ Prop("previewGroupId") { (view, previewGroupId: String?) in
27
+ view.previewGroupId = previewGroupId
28
+ }
29
+
30
+ Prop("theme") { (view, theme: Theme?) in
31
+ view.theme = theme ?? .dark
32
+ }
33
+ Prop("closeIconName") { (view, closeIconName: String?) in
34
+ view.closeIconName = closeIconName
35
+ }
36
+ Prop("rightNavItemIconName") { (view, rightNavItemIconName: String) in
37
+ view.rightNavItemIconName = rightNavItemIconName
38
+ }
39
+
40
+ }
41
+ }
42
+
43
+ func onIndexChange(index: Int) {
44
+ sendEvent("onIndexChange", ["currentIndex": index])
45
+ }
46
+ }
@@ -0,0 +1,252 @@
1
+ import ExpoModulesCore
2
+ import UIKit
3
+
4
+ class NativeImagePreviewView: ExpoView {
5
+ private var childImageView: UIImageView?
6
+ private weak var registeredAnchorImageView: UIImageView?
7
+ private var registeredPreviewGroupId: String?
8
+ private var registeredIndex: Int?
9
+
10
+ deinit {
11
+ unregisterCurrentAnchor()
12
+ }
13
+
14
+ override func didMoveToWindow() {
15
+ super.didMoveToWindow()
16
+ if window == nil {
17
+ unregisterCurrentAnchor()
18
+ } else {
19
+ registerCurrentAnchorIfNeeded()
20
+ }
21
+ }
22
+
23
+ func getChildImageView() -> UIImageView? {
24
+ var reactSubviews: [UIView]? = nil
25
+ if RCTIsNewArchEnabled() {
26
+ reactSubviews = self.subviews
27
+ } else {
28
+ reactSubviews = self.reactSubviews()
29
+ }
30
+
31
+ guard let reactSubviews = reactSubviews else { return nil }
32
+
33
+ for reactSubview in reactSubviews {
34
+ if let iv = findImageView(in: reactSubview) {
35
+ childImageView = iv
36
+ return iv
37
+ }
38
+ }
39
+
40
+ return nil
41
+ }
42
+
43
+ private func findImageView(in view: UIView) -> UIImageView? {
44
+ if let imageView = view as? UIImageView { return imageView }
45
+ for subview in view.subviews {
46
+ if let imageView = subview as? UIImageView { return imageView }
47
+ if let nested = findImageView(in: subview) { return nested }
48
+ }
49
+ return nil
50
+ }
51
+
52
+ #if !RCT_NEW_ARCH_ENABLED
53
+ override func insertReactSubview(_ subview: UIView!, at atIndex: Int) {
54
+ super.insertReactSubview(subview, at: atIndex)
55
+ setupImageView()
56
+ }
57
+ #endif
58
+
59
+ #if RCT_NEW_ARCH_ENABLED
60
+ // Cleanup gesture recognizers from the image view to work with Fabric view recycling.
61
+ override func unmountChildComponentView(_ childComponentView: UIView, index: Int) {
62
+ unregisterCurrentAnchor()
63
+ childImageView?.removeNativeImagePreviewViewerInteraction()
64
+ childImageView = nil
65
+ super.unmountChildComponentView(childComponentView, index: index)
66
+ }
67
+ #endif
68
+
69
+ var theme: Theme = .dark
70
+ var urls: [String]?
71
+ var items: [[String: Any]]?
72
+ var initialIndex: Int?
73
+ var previewGroupId: String?
74
+ var closeIconName: String?
75
+ var rightNavItemIconName: String?
76
+ let onPressRightNavItemIcon = EventDispatcher()
77
+ let onIndexChange = EventDispatcher()
78
+
79
+ public func setupImageView() {
80
+ registerCurrentAnchorIfNeeded()
81
+ guard let childImage = getChildImageView() else {
82
+ return
83
+ }
84
+
85
+ if let items = self.items, let initialIndex = self.initialIndex {
86
+ setupViewerWithItems(childImage, items: items, initialIndex: initialIndex)
87
+ } else if let urls = self.urls, let initialIndex = self.initialIndex {
88
+ setupViewerWithUrls(childImage, urls: urls, initialIndex: initialIndex)
89
+ } else {
90
+ setupViewerWithSingleImage(childImage)
91
+ }
92
+ }
93
+
94
+ private func setupViewerWithItems(
95
+ _ childImage: UIImageView,
96
+ items: [[String: Any]],
97
+ initialIndex: Int
98
+ ) {
99
+ let configuration = buildViewerConfiguration()
100
+
101
+ let imageItems: [NativeImagePreviewMediaItem] = items.compactMap { item in
102
+ guard let type = item["type"] as? String else { return nil }
103
+
104
+ if type == "video", let urlStr = item["url"] as? String, let url = URL(string: urlStr) {
105
+ var posterUrl: URL? = nil
106
+ if let posterStr = item["poster"] as? String {
107
+ posterUrl = URL(string: posterStr)
108
+ }
109
+ return NativeImagePreviewMediaItem.video(url: url, poster: posterUrl)
110
+ } else if type == "image", let urlStr = item["url"] as? String {
111
+ if urlStr.hasPrefix("http://") || urlStr.hasPrefix("https://") || urlStr.hasPrefix("file://") {
112
+ if let url = URL(string: urlStr) {
113
+ return NativeImagePreviewMediaItem.imageURL(url, placeholder: nil)
114
+ }
115
+ } else {
116
+ return NativeImagePreviewMediaItem.imageURL(URL(fileURLWithPath: urlStr), placeholder: nil)
117
+ }
118
+ }
119
+ return nil
120
+ }
121
+
122
+ let datasource = NativeImagePreviewArrayDataSource(items: imageItems)
123
+ childImage.setupNativeImagePreviewViewer(
124
+ dataSource: datasource,
125
+ initialIndex: initialIndex,
126
+ configuration: configuration,
127
+ previewGroupId: previewGroupId)
128
+ }
129
+
130
+
131
+ private func setupViewerWithUrls(
132
+ _ childImage: UIImageView,
133
+ urls: [String],
134
+ initialIndex: Int
135
+ ) {
136
+ let configuration = buildViewerConfiguration()
137
+
138
+ let urlObjects: [URL] = urls.compactMap { string in
139
+ if string.hasPrefix("http://") || string.hasPrefix("https://") || string.hasPrefix("file://") {
140
+ return URL(string: string)
141
+ }
142
+ return URL(fileURLWithPath: string)
143
+ }
144
+
145
+ childImage.setupNativeImagePreviewViewer(
146
+ urls: urlObjects,
147
+ initialIndex: initialIndex,
148
+ configuration: configuration,
149
+ previewGroupId: previewGroupId)
150
+ }
151
+
152
+ private func setupViewerWithSingleImage(_ childImage: UIImageView) {
153
+ guard let img = childImage.image else {
154
+ print("Missing image in childImage: \(childImage)")
155
+ return
156
+ }
157
+ let configuration = buildViewerConfiguration()
158
+
159
+ childImage.setupNativeImagePreviewViewer(images: [img], configuration: configuration, previewGroupId: previewGroupId)
160
+ }
161
+
162
+ private func buildViewerConfiguration() -> NativeImagePreviewViewerConfiguration {
163
+ var configuration = NativeImagePreviewViewerConfiguration(theme: theme.toViewerTheme())
164
+ let iconColor = theme.iconColor()
165
+
166
+ if let closeIconName = closeIconName,
167
+ let closeIconImage = UIImage(systemName: closeIconName)?.withTintColor(
168
+ iconColor, renderingMode: .alwaysOriginal)
169
+ {
170
+ configuration.closeIcon = closeIconImage
171
+ }
172
+
173
+ if let rightIconName = rightNavItemIconName,
174
+ let rightIconImage = UIImage(systemName: rightIconName)?.withTintColor(
175
+ iconColor, renderingMode: .alwaysOriginal)
176
+ {
177
+ configuration.rightBarButton = .icon(
178
+ rightIconImage,
179
+ onTap: { index in
180
+ self.onPressRightNavItemIcon(["index": index])
181
+ })
182
+ }
183
+
184
+ configuration.onIndexChange = { [weak self] index in
185
+ self?.onIndexChange(["currentIndex": index])
186
+ }
187
+ return configuration
188
+ }
189
+
190
+ private func registerCurrentAnchorIfNeeded() {
191
+ guard
192
+ window != nil,
193
+ let previewGroupId,
194
+ let initialIndex,
195
+ let childImage = getChildImageView()
196
+ else {
197
+ unregisterCurrentAnchor()
198
+ return
199
+ }
200
+
201
+ if
202
+ let registeredAnchorImageView,
203
+ (registeredAnchorImageView !== childImage
204
+ || registeredPreviewGroupId != previewGroupId
205
+ || registeredIndex != initialIndex)
206
+ {
207
+ unregisterCurrentAnchor()
208
+ }
209
+
210
+ NativeImagePreviewSourceViewRegistry.shared.register(
211
+ childImage,
212
+ previewGroupId: previewGroupId,
213
+ index: initialIndex)
214
+ registeredAnchorImageView = childImage
215
+ registeredPreviewGroupId = previewGroupId
216
+ registeredIndex = initialIndex
217
+ }
218
+
219
+ private func unregisterCurrentAnchor() {
220
+ if let registeredAnchorImageView = registeredAnchorImageView {
221
+ NativeImagePreviewSourceViewRegistry.shared.unregister(
222
+ registeredAnchorImageView,
223
+ previewGroupId: registeredPreviewGroupId,
224
+ index: registeredIndex)
225
+ }
226
+ registeredAnchorImageView = nil
227
+ registeredPreviewGroupId = nil
228
+ registeredIndex = nil
229
+ }
230
+ }
231
+
232
+ enum Theme: String, Enumerable {
233
+ case dark
234
+ case light
235
+
236
+ func toViewerTheme() -> NativeImagePreviewViewerTheme {
237
+ switch self {
238
+ case .dark:
239
+ return .dark
240
+ case .light:
241
+ return .light
242
+ }
243
+ }
244
+ func iconColor() -> UIColor {
245
+ switch self {
246
+ case .dark:
247
+ return .white
248
+ case .light:
249
+ return .black
250
+ }
251
+ }
252
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zkit-ui",
3
- "version": "2.0.4",
3
+ "version": "2.0.6",
4
4
  "private": false,
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -87,6 +87,11 @@
87
87
  "react-native": "./dist/ui/LinkedScroll/index.js",
88
88
  "default": "./dist/ui/LinkedScroll/index.js"
89
89
  },
90
+ "./native-image-preview": {
91
+ "types": "./dist/ui/NativeImagePreview/index.d.ts",
92
+ "react-native": "./dist/ui/NativeImagePreview/index.js",
93
+ "default": "./dist/ui/NativeImagePreview/index.js"
94
+ },
90
95
  "./loading-spinner": {
91
96
  "types": "./dist/ui/LoadingSpinner/index.d.ts",
92
97
  "react-native": "./dist/ui/LoadingSpinner/index.js",
@@ -165,7 +170,10 @@
165
170
  "./assets/*": "./dist/assets/*",
166
171
  "./package.json": "./package.json"
167
172
  },
168
- "sideEffects": false,
173
+ "sideEffects": [
174
+ "*.css",
175
+ "**/*.css"
176
+ ],
169
177
  "repository": {
170
178
  "type": "git",
171
179
  "url": "git+https://github.com/Concur-max/zkit.git",
@@ -176,7 +184,10 @@
176
184
  "url": "https://github.com/Concur-max/zkit/issues"
177
185
  },
178
186
  "files": [
187
+ "android/build.gradle",
188
+ "android/src",
179
189
  "dist",
190
+ "expo-module.config.json",
180
191
  "src",
181
192
  "ios",
182
193
  "react-native.config.js",
@@ -197,6 +208,7 @@
197
208
  "dependencies": {
198
209
  "@vant/area-data": "^2.1.0",
199
210
  "dayjs": "^1.11.13",
211
+ "react-native-is-edge-to-edge": "^1.1.6",
200
212
  "react-native-toast-message": "^2.3.3",
201
213
  "zkit-tools": "1.0.1"
202
214
  },
@@ -204,6 +216,7 @@
204
216
  "@expo/vector-icons": "*",
205
217
  "@lodev09/react-native-true-sheet": "*",
206
218
  "@shopify/flash-list": "*",
219
+ "expo": "*",
207
220
  "expo-image": "*",
208
221
  "expo-linear-gradient": "*",
209
222
  "lottie-react-native": "*",
@@ -222,6 +235,7 @@
222
235
  "@shopify/flash-list": "^2.3.1",
223
236
  "@types/react": "~19.1.17",
224
237
  "dayjs": "^1.11.13",
238
+ "expo": "~54.0.31",
225
239
  "expo-image": "~3.0.11",
226
240
  "expo-linear-gradient": "~15.0.8",
227
241
  "lottie-react-native": "7.3.2",
package/src/all.ts CHANGED
@@ -238,6 +238,24 @@ export type {
238
238
  LinkedScrollSectionRenderContext,
239
239
  LinkedScrollValue,
240
240
  } from './ui/LinkedScroll/index';
241
+ export { NativeImagePreview } from './ui/NativeImagePreview/index';
242
+ export type {
243
+ NativeImagePreviewChangeEvent,
244
+ NativeImagePreviewChangeEventPayload,
245
+ NativeImagePreviewChangeMeta,
246
+ NativeImagePreviewChangeReason,
247
+ NativeImagePreviewColorScheme,
248
+ NativeImagePreviewComponent,
249
+ NativeImagePreviewItem,
250
+ NativeImagePreviewItemDescriptor,
251
+ NativeImagePreviewItemProps,
252
+ NativeImagePreviewMediaType,
253
+ NativeImagePreviewNativeProps,
254
+ NativeImagePreviewProps,
255
+ NativeImagePreviewResolvedItem,
256
+ NativeImagePreviewResolvedColorScheme,
257
+ NativeImagePreviewSource,
258
+ } from './ui/NativeImagePreview/index';
241
259
  export { ThemeProvider } from './theme/ThemeProvider';
242
260
  export { useTheme } from './theme/useTheme';
243
261
  export type { Theme, ThemeOverride } from './theme/types';