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,437 @@
1
+ import AVFoundation
2
+ import UIKit
3
+
4
+ final class NativeImagePreviewVideoPlayerView: UIView {
5
+ private var player: AVPlayer?
6
+ private var playerLayer: AVPlayerLayer?
7
+ private var timeObserver: Any?
8
+ private var endObserver: NSObjectProtocol?
9
+ private var readyForDisplayObservation: NSKeyValueObservation?
10
+ private var itemStatusObservation: NSKeyValueObservation?
11
+ private var likelyToKeepUpObservation: NSKeyValueObservation?
12
+ private var bufferFullObservation: NSKeyValueObservation?
13
+ private var playbackGeneration = 0
14
+ private var isScrubbing = false
15
+ private var wasPlayingBeforeScrub = false
16
+ private var hasReportedFirstFrame = false
17
+ private var isPrimingFirstFrame = false
18
+ private var controlsVisible = false
19
+ private var controlsContainerConstraints: [NSLayoutConstraint] = []
20
+
21
+ private let playButton = UIButton(type: .system)
22
+ private let slider = UISlider()
23
+ private let timeLabel = UILabel()
24
+ private let controlsContainer = UIView()
25
+
26
+ var onControlsInteractStart: (() -> Void)?
27
+ var onControlsInteractEnd: (() -> Void)?
28
+ var onFirstFrameReady: (() -> Void)?
29
+
30
+ var url: URL? {
31
+ didSet {
32
+ setupPlayer()
33
+ }
34
+ }
35
+
36
+ override init(frame: CGRect) {
37
+ super.init(frame: frame)
38
+ setupUI()
39
+ }
40
+
41
+ required init?(coder: NSCoder) {
42
+ fatalError("init(coder:) has not been implemented")
43
+ }
44
+
45
+ override func layoutSubviews() {
46
+ super.layoutSubviews()
47
+ playerLayer?.frame = bounds
48
+ }
49
+
50
+ deinit {
51
+ tearDownPlayer()
52
+ }
53
+
54
+ func toggleControls() {
55
+ setControlsVisible(!controlsVisible, animated: true)
56
+ }
57
+
58
+ func resetToStart() {
59
+ player?.pause()
60
+ player?.seek(to: .zero, toleranceBefore: .zero, toleranceAfter: .zero)
61
+ slider.value = 0
62
+ updateTimeLabel(current: 0, duration: CMTimeGetSeconds(player?.currentItem?.duration ?? .zero))
63
+ updatePlayButton(isPlaying: false)
64
+ }
65
+
66
+ func play() {
67
+ if hasReportedFirstFrame == false {
68
+ primeFirstFrameIfNeeded()
69
+ }
70
+ player?.isMuted = false
71
+ player?.play()
72
+ updatePlayButton(isPlaying: true)
73
+ }
74
+
75
+ func pause() {
76
+ player?.pause()
77
+ if hasReportedFirstFrame == false {
78
+ isPrimingFirstFrame = false
79
+ }
80
+ updatePlayButton(isPlaying: false)
81
+ }
82
+
83
+ func setPlayerVisible(_ visible: Bool) {
84
+ CATransaction.begin()
85
+ CATransaction.setDisableActions(true)
86
+ playerLayer?.frame = bounds
87
+ playerLayer?.isHidden = !visible
88
+ playerLayer?.opacity = visible ? 1 : 0
89
+ CATransaction.commit()
90
+ if visible {
91
+ setNeedsLayout()
92
+ layoutIfNeeded()
93
+ }
94
+ }
95
+
96
+ func attachControls(to hostView: UIView) {
97
+ if controlsContainer.superview === hostView {
98
+ hostView.bringSubviewToFront(controlsContainer)
99
+ return
100
+ }
101
+
102
+ NSLayoutConstraint.deactivate(controlsContainerConstraints)
103
+ controlsContainer.removeFromSuperview()
104
+ hostView.addSubview(controlsContainer)
105
+ controlsContainer.translatesAutoresizingMaskIntoConstraints = false
106
+ controlsContainerConstraints = [
107
+ controlsContainer.bottomAnchor.constraint(equalTo: hostView.safeAreaLayoutGuide.bottomAnchor, constant: -20),
108
+ controlsContainer.leadingAnchor.constraint(equalTo: hostView.leadingAnchor, constant: 12),
109
+ controlsContainer.trailingAnchor.constraint(equalTo: hostView.trailingAnchor, constant: -12),
110
+ controlsContainer.heightAnchor.constraint(equalToConstant: 56),
111
+ ]
112
+ NSLayoutConstraint.activate(controlsContainerConstraints)
113
+ hostView.bringSubviewToFront(controlsContainer)
114
+ }
115
+
116
+ func setControlsVisible(_ visible: Bool, animated: Bool) {
117
+ controlsVisible = visible
118
+ let animations = {
119
+ self.controlsContainer.alpha = visible ? 1 : 0
120
+ }
121
+ let completion: (Bool) -> Void = { _ in
122
+ self.controlsContainer.isUserInteractionEnabled = visible
123
+ }
124
+
125
+ if animated {
126
+ UIView.animate(
127
+ withDuration: 0.22,
128
+ delay: 0,
129
+ options: [.curveEaseInOut, .beginFromCurrentState],
130
+ animations: animations,
131
+ completion: completion
132
+ )
133
+ } else {
134
+ animations()
135
+ completion(true)
136
+ }
137
+ }
138
+
139
+ func isTouchOnInteractiveControls(atWindowPoint point: CGPoint) -> Bool {
140
+ return expandedFrameForInteraction(of: controlsContainer, padding: 20)?.contains(point) == true
141
+ }
142
+
143
+ func isPointInsideInteractiveControls(_ point: CGPoint) -> Bool {
144
+ guard controlsVisible else { return false }
145
+ guard let hostView = controlsContainer.superview else { return false }
146
+ let pointInHost = hostView.convert(point, from: self)
147
+ if controlsContainer.frame.insetBy(dx: -20, dy: -20).contains(pointInHost) {
148
+ return true
149
+ }
150
+ return false
151
+ }
152
+
153
+ func primeFirstFrameIfNeeded() {
154
+ guard let player = player else { return }
155
+ guard hasReportedFirstFrame == false, isPrimingFirstFrame == false else { return }
156
+ isPrimingFirstFrame = true
157
+ player.isMuted = true
158
+ player.play()
159
+ }
160
+
161
+ func resetForReuse() {
162
+ setControlsVisible(false, animated: false)
163
+ url = nil
164
+ }
165
+
166
+ private func setupUI() {
167
+ backgroundColor = .clear
168
+
169
+ controlsContainer.backgroundColor = UIColor.black.withAlphaComponent(0.5)
170
+ controlsContainer.layer.cornerRadius = 12
171
+ controlsContainer.translatesAutoresizingMaskIntoConstraints = false
172
+ controlsContainer.alpha = 0
173
+
174
+ playButton.setImage(UIImage(systemName: "play.fill"), for: .normal)
175
+ playButton.setPreferredSymbolConfiguration(
176
+ UIImage.SymbolConfiguration(pointSize: 20, weight: .bold),
177
+ forImageIn: .normal
178
+ )
179
+ playButton.tintColor = .white
180
+ playButton.translatesAutoresizingMaskIntoConstraints = false
181
+ playButton.addTarget(self, action: #selector(handlePlayButtonTouchDown), for: .touchDown)
182
+ playButton.addTarget(self, action: #selector(togglePlay), for: .touchUpInside)
183
+ playButton.addTarget(
184
+ self,
185
+ action: #selector(handleControlInteractionEnded),
186
+ for: [.touchUpInside, .touchUpOutside, .touchCancel]
187
+ )
188
+ controlsContainer.addSubview(playButton)
189
+
190
+ slider.minimumValue = 0
191
+ slider.maximumValue = 1
192
+ slider.value = 0
193
+ slider.tintColor = .white
194
+ slider.translatesAutoresizingMaskIntoConstraints = false
195
+ slider.addTarget(self, action: #selector(sliderValueChanged), for: .valueChanged)
196
+ slider.addTarget(self, action: #selector(handleSliderTouchDown), for: .touchDown)
197
+ slider.addTarget(self, action: #selector(handleSliderTouchUp), for: [.touchUpInside, .touchUpOutside, .touchCancel])
198
+ controlsContainer.addSubview(slider)
199
+
200
+ timeLabel.textColor = .white
201
+ timeLabel.font = .systemFont(ofSize: 13, weight: .regular)
202
+ timeLabel.text = "00:00 / 00:00"
203
+ timeLabel.translatesAutoresizingMaskIntoConstraints = false
204
+ controlsContainer.addSubview(timeLabel)
205
+
206
+ NSLayoutConstraint.activate([
207
+ playButton.leadingAnchor.constraint(equalTo: controlsContainer.leadingAnchor, constant: 10),
208
+ playButton.centerYAnchor.constraint(equalTo: controlsContainer.centerYAnchor),
209
+ playButton.widthAnchor.constraint(equalToConstant: 44),
210
+ playButton.heightAnchor.constraint(equalToConstant: 44),
211
+
212
+ slider.leadingAnchor.constraint(equalTo: playButton.trailingAnchor, constant: 10),
213
+ slider.trailingAnchor.constraint(equalTo: timeLabel.leadingAnchor, constant: -10),
214
+ slider.centerYAnchor.constraint(equalTo: controlsContainer.centerYAnchor),
215
+
216
+ timeLabel.trailingAnchor.constraint(equalTo: controlsContainer.trailingAnchor, constant: -12),
217
+ timeLabel.centerYAnchor.constraint(equalTo: controlsContainer.centerYAnchor),
218
+ ])
219
+
220
+ attachControls(to: self)
221
+ }
222
+
223
+ private func setupPlayer() {
224
+ playbackGeneration += 1
225
+ let generation = playbackGeneration
226
+ tearDownPlayer()
227
+ guard let url = url else { return }
228
+
229
+ let item = AVPlayerItem(url: url)
230
+ player = AVPlayer(playerItem: item)
231
+ playerLayer = AVPlayerLayer(player: player)
232
+ playerLayer?.videoGravity = .resizeAspect
233
+ if let playerLayer = playerLayer {
234
+ layer.insertSublayer(playerLayer, at: 0)
235
+ }
236
+ playerLayer?.isHidden = true
237
+ player?.pause()
238
+ player?.isMuted = true
239
+ player?.seek(to: .zero, toleranceBefore: .zero, toleranceAfter: .zero)
240
+ hasReportedFirstFrame = false
241
+ isPrimingFirstFrame = false
242
+ slider.value = 0
243
+ updateTimeLabel(current: 0, duration: 0)
244
+ updatePlayButton(isPlaying: false)
245
+
246
+ let notifyFirstFrameReadyIfPossible: () -> Void = { [weak self] in
247
+ guard let self = self, self.playbackGeneration == generation, self.hasReportedFirstFrame == false else { return }
248
+ guard let currentItem = self.player?.currentItem else { return }
249
+ let isReadyToPlay = currentItem.status == .readyToPlay
250
+ let isLikelyToKeepUp = currentItem.isPlaybackLikelyToKeepUp || currentItem.isPlaybackBufferFull
251
+ let isReadyForDisplay = self.playerLayer?.isReadyForDisplay == true
252
+ guard isReadyToPlay, isLikelyToKeepUp, isReadyForDisplay else { return }
253
+ self.hasReportedFirstFrame = true
254
+ self.isPrimingFirstFrame = false
255
+ self.player?.pause()
256
+ self.player?.seek(to: .zero, toleranceBefore: .zero, toleranceAfter: .zero)
257
+ self.player?.isMuted = false
258
+ DispatchQueue.main.async {
259
+ guard self.playbackGeneration == generation else { return }
260
+ DispatchQueue.main.async {
261
+ guard self.playbackGeneration == generation else { return }
262
+ self.onFirstFrameReady?()
263
+ }
264
+ }
265
+ }
266
+
267
+ itemStatusObservation = item.observe(\.status, options: [.new, .initial]) { _, _ in
268
+ notifyFirstFrameReadyIfPossible()
269
+ }
270
+ likelyToKeepUpObservation = item.observe(\.isPlaybackLikelyToKeepUp, options: [.new, .initial]) { _, _ in
271
+ notifyFirstFrameReadyIfPossible()
272
+ }
273
+ bufferFullObservation = item.observe(\.isPlaybackBufferFull, options: [.new, .initial]) { _, _ in
274
+ notifyFirstFrameReadyIfPossible()
275
+ }
276
+
277
+ readyForDisplayObservation = playerLayer?.observe(\.isReadyForDisplay, options: [.new, .initial]) { [weak self] _, change in
278
+ guard let self = self, self.playbackGeneration == generation, change.newValue == true else {
279
+ return
280
+ }
281
+ notifyFirstFrameReadyIfPossible()
282
+ }
283
+
284
+ timeObserver = player?.addPeriodicTimeObserver(
285
+ forInterval: CMTime(seconds: 0.5, preferredTimescale: 600),
286
+ queue: .main
287
+ ) { [weak self] time in
288
+ guard let self = self, self.playbackGeneration == generation, let item = self.player?.currentItem else { return }
289
+ let duration = CMTimeGetSeconds(item.duration)
290
+ let current = CMTimeGetSeconds(time)
291
+ if duration.isFinite && duration > 0, !self.isScrubbing {
292
+ self.slider.value = Float(current / duration)
293
+ }
294
+ self.updateTimeLabel(current: current, duration: duration)
295
+ }
296
+
297
+ endObserver = NotificationCenter.default.addObserver(
298
+ forName: .AVPlayerItemDidPlayToEndTime,
299
+ object: player?.currentItem,
300
+ queue: .main
301
+ ) { [weak self] _ in
302
+ guard let self = self, self.playbackGeneration == generation else { return }
303
+ self.resetToStart()
304
+ }
305
+ }
306
+
307
+ @objc
308
+ private func togglePlay() {
309
+ if hasReportedFirstFrame == false {
310
+ primeFirstFrameIfNeeded()
311
+ }
312
+ guard let player = player else { return }
313
+ if player.timeControlStatus == .playing {
314
+ player.pause()
315
+ updatePlayButton(isPlaying: false)
316
+ } else {
317
+ player.isMuted = false
318
+ player.play()
319
+ updatePlayButton(isPlaying: true)
320
+ }
321
+ }
322
+
323
+ @objc
324
+ private func sliderValueChanged() {
325
+ guard let player = player, let item = player.currentItem else { return }
326
+ let duration = CMTimeGetSeconds(item.duration)
327
+ guard duration.isFinite && duration > 0 else { return }
328
+ let targetTime = Double(slider.value) * duration
329
+ isScrubbing = true
330
+ player.seek(
331
+ to: CMTime(seconds: targetTime, preferredTimescale: 600),
332
+ toleranceBefore: .zero,
333
+ toleranceAfter: .zero
334
+ )
335
+ updateTimeLabel(current: targetTime, duration: duration)
336
+ }
337
+
338
+ private func formatTime(_ seconds: Double) -> String {
339
+ guard seconds.isFinite && seconds >= 0 else { return "--:--" }
340
+ let total = Int(seconds.rounded())
341
+ return String(format: "%02d:%02d", total / 60, total % 60)
342
+ }
343
+
344
+ private func updateTimeLabel(current: Double, duration: Double) {
345
+ timeLabel.text = "\(formatTime(current)) / \(formatTime(duration))"
346
+ }
347
+
348
+ private func updatePlayButton(isPlaying: Bool) {
349
+ playButton.setImage(UIImage(systemName: isPlaying ? "pause.fill" : "play.fill"), for: .normal)
350
+ }
351
+
352
+ private func expandedFrameForInteraction(of view: UIView, padding: CGFloat) -> CGRect? {
353
+ guard controlsVisible else { return nil }
354
+ guard view.alpha > 0.01, !view.bounds.isEmpty else { return nil }
355
+ return view.convert(view.bounds.insetBy(dx: -padding, dy: -padding), to: nil)
356
+ }
357
+
358
+ @objc
359
+ private func handleSliderTouchDown() {
360
+ guard let player = player else { return }
361
+ isScrubbing = true
362
+ wasPlayingBeforeScrub = player.timeControlStatus == .playing
363
+ player.pause()
364
+ onControlsInteractStart?()
365
+ }
366
+
367
+ @objc
368
+ private func handleSliderTouchUp() {
369
+ guard let player = player, let item = player.currentItem else {
370
+ isScrubbing = false
371
+ onControlsInteractEnd?()
372
+ return
373
+ }
374
+ let duration = CMTimeGetSeconds(item.duration)
375
+ guard duration.isFinite && duration > 0 else {
376
+ isScrubbing = false
377
+ onControlsInteractEnd?()
378
+ return
379
+ }
380
+ let targetTime = Double(slider.value) * duration
381
+ player.seek(
382
+ to: CMTime(seconds: targetTime, preferredTimescale: 600),
383
+ toleranceBefore: .zero,
384
+ toleranceAfter: .zero
385
+ ) { [weak self] _ in
386
+ DispatchQueue.main.async {
387
+ guard let self = self else { return }
388
+ self.isScrubbing = false
389
+ if self.wasPlayingBeforeScrub {
390
+ self.play()
391
+ } else {
392
+ self.pause()
393
+ }
394
+ self.onControlsInteractEnd?()
395
+ }
396
+ }
397
+ }
398
+
399
+ @objc
400
+ private func handlePlayButtonTouchDown() {
401
+ onControlsInteractStart?()
402
+ }
403
+
404
+ @objc
405
+ private func handleControlInteractionEnded() {
406
+ onControlsInteractEnd?()
407
+ }
408
+
409
+ private func tearDownPlayer() {
410
+ if let timeObserver = timeObserver, let player = player {
411
+ player.removeTimeObserver(timeObserver)
412
+ }
413
+ if let endObserver = endObserver {
414
+ NotificationCenter.default.removeObserver(endObserver)
415
+ }
416
+ readyForDisplayObservation?.invalidate()
417
+ itemStatusObservation?.invalidate()
418
+ likelyToKeepUpObservation?.invalidate()
419
+ bufferFullObservation?.invalidate()
420
+ readyForDisplayObservation = nil
421
+ itemStatusObservation = nil
422
+ likelyToKeepUpObservation = nil
423
+ bufferFullObservation = nil
424
+ timeObserver = nil
425
+ endObserver = nil
426
+
427
+ player?.pause()
428
+ player = nil
429
+ playerLayer?.removeFromSuperlayer()
430
+ playerLayer = nil
431
+
432
+ isScrubbing = false
433
+ wasPlayingBeforeScrub = false
434
+ hasReportedFirstFrame = false
435
+ isPrimingFirstFrame = false
436
+ }
437
+ }
@@ -0,0 +1,51 @@
1
+ import UIKit
2
+
3
+ public enum NativeImagePreviewViewerTheme {
4
+ case light
5
+ case dark
6
+
7
+ var backgroundColor: UIColor {
8
+ switch self {
9
+ case .light:
10
+ return .white
11
+ case .dark:
12
+ return .black
13
+ }
14
+ }
15
+
16
+ var tintColor: UIColor {
17
+ switch self {
18
+ case .light:
19
+ return .black
20
+ case .dark:
21
+ return .white
22
+ }
23
+ }
24
+ }
25
+
26
+ public enum NativeImagePreviewViewerBarButton {
27
+ case title(String, onTap: ((Int) -> Void)?)
28
+ case icon(UIImage, onTap: ((Int) -> Void)?)
29
+ }
30
+
31
+ public struct NativeImagePreviewViewerConfiguration {
32
+ public var theme: NativeImagePreviewViewerTheme
33
+ public var contentMode: UIView.ContentMode
34
+ public var closeIcon: UIImage?
35
+ public var rightBarButton: NativeImagePreviewViewerBarButton?
36
+ public var onIndexChange: ((Int) -> Void)?
37
+
38
+ public init(
39
+ theme: NativeImagePreviewViewerTheme = .light,
40
+ contentMode: UIView.ContentMode = .scaleAspectFit,
41
+ closeIcon: UIImage? = nil,
42
+ rightBarButton: NativeImagePreviewViewerBarButton? = nil,
43
+ onIndexChange: ((Int) -> Void)? = nil
44
+ ) {
45
+ self.theme = theme
46
+ self.contentMode = contentMode
47
+ self.closeIcon = closeIcon
48
+ self.rightBarButton = rightBarButton
49
+ self.onIndexChange = onIndexChange
50
+ }
51
+ }