@capgo/capacitor-updater 8.48.0 → 8.49.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.
@@ -9,7 +9,78 @@ import Capacitor
9
9
 
10
10
  private var lastShakeMenuShownAt: TimeInterval = 0
11
11
  private let shakeMenuCooldownSeconds: TimeInterval = 1.2
12
- private let threeFingerPinchScaleDelta: CGFloat = 0.30
12
+ private let threeFingerPinchScaleDelta: CGFloat = 0.12
13
+
14
+ final class ThreeFingerPinchGestureRecognizer: UIGestureRecognizer {
15
+ private var initialSpan: CGFloat = 0
16
+ private(set) var scale: CGFloat = 1
17
+
18
+ override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
19
+ super.touchesBegan(touches, with: event)
20
+ guard let view = self.view, let activeTouches = activeTouches(in: view, with: event), activeTouches.count <= 3 else {
21
+ self.state = .failed
22
+ return
23
+ }
24
+
25
+ if activeTouches.count == 3 {
26
+ self.initialSpan = span(for: activeTouches, in: view)
27
+ self.scale = 1
28
+ self.state = self.initialSpan > 0 ? .began : .failed
29
+ }
30
+ }
31
+
32
+ override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
33
+ super.touchesMoved(touches, with: event)
34
+ guard let view = self.view,
35
+ let activeTouches = activeTouches(in: view, with: event),
36
+ activeTouches.count == 3,
37
+ self.initialSpan > 0 else {
38
+ self.state = .failed
39
+ return
40
+ }
41
+
42
+ self.scale = span(for: activeTouches, in: view) / self.initialSpan
43
+ self.state = .changed
44
+ }
45
+
46
+ override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
47
+ super.touchesEnded(touches, with: event)
48
+ self.state = self.state == .possible || self.state == .began ? .failed : .ended
49
+ }
50
+
51
+ override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
52
+ super.touchesCancelled(touches, with: event)
53
+ self.state = .cancelled
54
+ }
55
+
56
+ override func reset() {
57
+ super.reset()
58
+ self.initialSpan = 0
59
+ self.scale = 1
60
+ }
61
+
62
+ private func activeTouches(in view: UIView, with event: UIEvent) -> [UITouch]? {
63
+ event.touches(for: view)?.filter { touch in
64
+ touch.phase != .ended && touch.phase != .cancelled
65
+ }
66
+ }
67
+
68
+ private func span(for touches: [UITouch], in view: UIView) -> CGFloat {
69
+ guard !touches.isEmpty else {
70
+ return 0
71
+ }
72
+
73
+ let points = touches.map { $0.location(in: view) }
74
+ let center = points.reduce(CGPoint.zero) { result, point in
75
+ CGPoint(x: result.x + point.x, y: result.y + point.y)
76
+ }
77
+ let centerPoint = CGPoint(x: center.x / CGFloat(points.count), y: center.y / CGFloat(points.count))
78
+ let totalDistance = points.reduce(CGFloat(0)) { result, point in
79
+ result + hypot(point.x - centerPoint.x, point.y - centerPoint.y)
80
+ }
81
+ return totalDistance / CGFloat(points.count)
82
+ }
83
+ }
13
84
 
14
85
  extension UIApplication {
15
86
  // swiftlint:disable:next line_length
@@ -46,7 +117,7 @@ extension CapacitorUpdaterPlugin: UIGestureRecognizerDelegate {
46
117
 
47
118
  self.removeShakeMenuGestureRecognizer()
48
119
 
49
- let recognizer = UIPinchGestureRecognizer(target: self, action: #selector(self.handleShakeMenuPinch(_:)))
120
+ let recognizer = ThreeFingerPinchGestureRecognizer(target: self, action: #selector(self.handleShakeMenuPinch(_:)))
50
121
  recognizer.cancelsTouchesInView = false
51
122
  recognizer.delaysTouchesBegan = false
52
123
  recognizer.delaysTouchesEnded = false
@@ -66,12 +137,12 @@ extension CapacitorUpdaterPlugin: UIGestureRecognizerDelegate {
66
137
  }
67
138
  }
68
139
 
69
- @objc func handleShakeMenuPinch(_ recognizer: UIPinchGestureRecognizer) {
140
+ @objc func handleShakeMenuPinch(_ recognizer: ThreeFingerPinchGestureRecognizer) {
70
141
  if recognizer.state == .ended || recognizer.state == .cancelled || recognizer.state == .failed {
71
142
  self.shakeMenuPinchGestureTriggered = false
72
143
  return
73
144
  }
74
- guard recognizer.state == .changed, !self.shakeMenuPinchGestureTriggered, recognizer.numberOfTouches == 3 else {
145
+ guard recognizer.state == .changed, !self.shakeMenuPinchGestureTriggered else {
75
146
  return
76
147
  }
77
148
  guard abs(recognizer.scale - 1) >= threeFingerPinchScaleDelta else {
@@ -165,23 +236,13 @@ extension UIWindow {
165
236
 
166
237
  let appName = Bundle.main.infoDictionary?["CFBundleDisplayName"] as? String ?? "App"
167
238
  let title = "Preview \(appName) Menu"
168
- let message = "Reload the current preview or leave the test app."
239
+ let message = "Reload, switch, or leave the current preview."
169
240
  let okButtonTitle = "Leave test app"
170
- let reloadButtonTitle = "Reload app"
241
+ let reloadButtonTitle = "Reload preview"
171
242
  let cancelButtonTitle = "Close menu"
172
243
 
173
244
  let alertShake = UIAlertController(title: title, message: message, preferredStyle: .alert)
174
245
 
175
- alertShake.addAction(UIAlertAction(title: okButtonTitle, style: .default) { _ in
176
- DispatchQueue.global(qos: .userInitiated).async {
177
- if !plugin.leavePreviewSessionFromShakeMenu() {
178
- DispatchQueue.main.async {
179
- self.showError(message: "Could not leave the test app.", plugin: plugin)
180
- }
181
- }
182
- }
183
- })
184
-
185
246
  alertShake.addAction(UIAlertAction(title: reloadButtonTitle, style: .default) { _ in
186
247
  DispatchQueue.global(qos: .userInitiated).async {
187
248
  if !plugin.reloadPreviewSessionFromShakeMenu() {
@@ -192,6 +253,20 @@ extension UIWindow {
192
253
  }
193
254
  })
194
255
 
256
+ if !plugin.previewMenuPreviews().isEmpty {
257
+ alertShake.addAction(UIAlertAction(title: "Switch preview", style: .default) { _ in
258
+ let showSelector = {
259
+ self.showPreviewSelector(plugin: plugin)
260
+ }
261
+
262
+ if let presenter = alertShake.presentingViewController {
263
+ presenter.dismiss(animated: true, completion: showSelector)
264
+ } else {
265
+ DispatchQueue.main.async(execute: showSelector)
266
+ }
267
+ })
268
+ }
269
+
195
270
  if plugin.shakeChannelSelectorEnabled {
196
271
  alertShake.addAction(UIAlertAction(title: "Switch channel", style: .default) { _ in
197
272
  let showSelector = {
@@ -206,6 +281,16 @@ extension UIWindow {
206
281
  })
207
282
  }
208
283
 
284
+ alertShake.addAction(UIAlertAction(title: okButtonTitle, style: .default) { _ in
285
+ DispatchQueue.global(qos: .userInitiated).async {
286
+ if !plugin.leavePreviewSessionFromShakeMenu() {
287
+ DispatchQueue.main.async {
288
+ self.showError(message: "Could not leave the test app.", plugin: plugin)
289
+ }
290
+ }
291
+ }
292
+ })
293
+
209
294
  alertShake.addAction(UIAlertAction(title: cancelButtonTitle, style: .default))
210
295
 
211
296
  DispatchQueue.main.async {
@@ -277,6 +362,130 @@ extension UIWindow {
277
362
  }
278
363
  }
279
364
 
365
+ private func previewLabel(_ preview: [String: Any]) -> String {
366
+ let bundle = preview["bundle"] as? [String: Any]
367
+ let name = preview["name"] as? String
368
+ let version = bundle?["version"] as? String
369
+ var label = [name, version, preview["id"] as? String]
370
+ .compactMap { value in
371
+ guard let value, !value.isEmpty else { return nil }
372
+ return value
373
+ }
374
+ .first ?? "Preview"
375
+ if preview["isActive"] as? Bool == true {
376
+ label += " (current)"
377
+ }
378
+ return label
379
+ }
380
+
381
+ private func showPreviewSelector(plugin: CapacitorUpdaterPlugin) {
382
+ guard let topVC = UIApplication.topViewController() else {
383
+ return
384
+ }
385
+ if topVC.isKind(of: UIAlertController.self) {
386
+ plugin.logger.info("UIAlertController is already presented")
387
+ return
388
+ }
389
+
390
+ let previews = plugin.previewMenuPreviews()
391
+ guard !previews.isEmpty else {
392
+ self.showError(message: "No saved previews available on this device.", plugin: plugin)
393
+ return
394
+ }
395
+
396
+ let alert = UIAlertController(title: "Select Preview", message: "Choose a local preview to open", preferredStyle: .actionSheet)
397
+ let previewsToShow = Array(previews.prefix(5))
398
+ for preview in previewsToShow {
399
+ let title = self.previewLabel(preview)
400
+ let id = preview["id"] as? String ?? ""
401
+ alert.addAction(UIAlertAction(title: title, style: .default) { [weak self] _ in
402
+ self?.selectPreview(id: id, plugin: plugin)
403
+ })
404
+ }
405
+
406
+ if previews.count > 5 {
407
+ alert.addAction(UIAlertAction(title: "More previews...", style: .default) { [weak self] _ in
408
+ self?.showSearchablePreviewPicker(previews: previews, plugin: plugin)
409
+ })
410
+ }
411
+
412
+ alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
413
+
414
+ if let popoverController = alert.popoverPresentationController {
415
+ popoverController.sourceView = self
416
+ popoverController.sourceRect = CGRect(x: self.bounds.midX, y: self.bounds.midY, width: 0, height: 0)
417
+ popoverController.permittedArrowDirections = []
418
+ }
419
+
420
+ topVC.present(alert, animated: true)
421
+ }
422
+
423
+ private func showSearchablePreviewPicker(previews: [[String: Any]], plugin: CapacitorUpdaterPlugin) {
424
+ let alert = UIAlertController(title: "Search Previews", message: "Enter preview name or version", preferredStyle: .alert)
425
+
426
+ alert.addTextField { textField in
427
+ textField.placeholder = "Preview name..."
428
+ }
429
+
430
+ alert.addAction(UIAlertAction(title: "Search", style: .default) { [weak self, weak alert] _ in
431
+ guard let self else { return }
432
+ guard let searchText = alert?.textFields?.first?.text?.lowercased(), !searchText.isEmpty else {
433
+ self.showPreviewSelector(plugin: plugin)
434
+ return
435
+ }
436
+
437
+ let filtered = previews.filter { self.previewLabel($0).lowercased().contains(searchText) }
438
+ if filtered.isEmpty {
439
+ self.showError(message: "No previews found matching '\(searchText)'", plugin: plugin)
440
+ } else if filtered.count == 1, let id = filtered[0]["id"] as? String {
441
+ self.selectPreview(id: id, plugin: plugin)
442
+ } else {
443
+ self.presentPreviewPicker(previews: filtered, plugin: plugin)
444
+ }
445
+ })
446
+
447
+ alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
448
+
449
+ DispatchQueue.main.async {
450
+ if let topVC = UIApplication.topViewController() {
451
+ topVC.present(alert, animated: true)
452
+ }
453
+ }
454
+ }
455
+
456
+ private func presentPreviewPicker(previews: [[String: Any]], plugin: CapacitorUpdaterPlugin) {
457
+ let alert = UIAlertController(title: "Select Preview", message: "Choose a local preview to open", preferredStyle: .actionSheet)
458
+ for preview in previews.prefix(5) {
459
+ let id = preview["id"] as? String ?? ""
460
+ alert.addAction(UIAlertAction(title: self.previewLabel(preview), style: .default) { [weak self] _ in
461
+ self?.selectPreview(id: id, plugin: plugin)
462
+ })
463
+ }
464
+ alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
465
+
466
+ if let popoverController = alert.popoverPresentationController {
467
+ popoverController.sourceView = self
468
+ popoverController.sourceRect = CGRect(x: self.bounds.midX, y: self.bounds.midY, width: 0, height: 0)
469
+ popoverController.permittedArrowDirections = []
470
+ }
471
+
472
+ DispatchQueue.main.async {
473
+ if let topVC = UIApplication.topViewController() {
474
+ topVC.present(alert, animated: true)
475
+ }
476
+ }
477
+ }
478
+
479
+ private func selectPreview(id: String, plugin: CapacitorUpdaterPlugin) {
480
+ DispatchQueue.global(qos: .userInitiated).async {
481
+ if !plugin.setPreviewFromShakeMenu(id: id) {
482
+ DispatchQueue.main.async {
483
+ self.showError(message: "Could not switch preview.", plugin: plugin)
484
+ }
485
+ }
486
+ }
487
+ }
488
+
280
489
  @discardableResult
281
490
  private func showChannelSelector(plugin: CapacitorUpdaterPlugin, bridge: CAPBridgeProtocol) -> Bool {
282
491
  // Prevent multiple alerts from showing
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-updater",
3
- "version": "8.48.0",
3
+ "version": "8.49.0",
4
4
  "license": "MPL-2.0",
5
5
  "description": "Live update for capacitor apps",
6
6
  "main": "dist/plugin.cjs.js",