@capgo/capacitor-updater 8.48.0 → 8.49.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.
- package/README.md +186 -8
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +645 -111
- package/android/src/main/java/ee/forgr/capacitor_updater/ShakeMenu.java +203 -30
- package/android/src/main/java/ee/forgr/capacitor_updater/ThreeFingerPinchDetector.java +218 -64
- package/dist/docs.json +528 -17
- package/dist/esm/definitions.d.ts +228 -10
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +7 -1
- package/dist/esm/web.js +24 -0
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +24 -0
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +24 -0
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +538 -77
- package/ios/Sources/CapacitorUpdaterPlugin/ShakeMenu.swift +239 -21
- package/package.json +1 -1
|
@@ -9,7 +9,87 @@ 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.
|
|
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
|
+
var onTrackingStarted: (() -> Void)?
|
|
18
|
+
|
|
19
|
+
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
|
20
|
+
super.touchesBegan(touches, with: event)
|
|
21
|
+
guard let view = self.view, let activeTouches = activeTouches(with: event), activeTouches.count <= 3 else {
|
|
22
|
+
self.state = .failed
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if activeTouches.count == 3 {
|
|
27
|
+
self.initialSpan = span(for: activeTouches, in: view)
|
|
28
|
+
self.scale = 1
|
|
29
|
+
if self.initialSpan > 0 {
|
|
30
|
+
self.onTrackingStarted?()
|
|
31
|
+
} else {
|
|
32
|
+
self.state = .failed
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
|
|
38
|
+
super.touchesMoved(touches, with: event)
|
|
39
|
+
guard let view = self.view,
|
|
40
|
+
let activeTouches = activeTouches(with: event),
|
|
41
|
+
activeTouches.count == 3,
|
|
42
|
+
self.initialSpan > 0 else {
|
|
43
|
+
self.state = .failed
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
self.scale = span(for: activeTouches, in: view) / self.initialSpan
|
|
48
|
+
if abs(self.scale - 1) >= threeFingerPinchScaleDelta {
|
|
49
|
+
self.state = .recognized
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
|
|
54
|
+
super.touchesEnded(touches, with: event)
|
|
55
|
+
if self.state == .possible {
|
|
56
|
+
self.state = .failed
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
|
|
61
|
+
super.touchesCancelled(touches, with: event)
|
|
62
|
+
self.state = .cancelled
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
override func reset() {
|
|
66
|
+
super.reset()
|
|
67
|
+
self.initialSpan = 0
|
|
68
|
+
self.scale = 1
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private func activeTouches(with event: UIEvent) -> [UITouch]? {
|
|
72
|
+
event.touches(for: self)?.filter { touch in
|
|
73
|
+
touch.phase != .ended && touch.phase != .cancelled
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private func span(for touches: [UITouch], in view: UIView) -> CGFloat {
|
|
78
|
+
guard !touches.isEmpty else {
|
|
79
|
+
return 0
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let points = touches.map { $0.location(in: view) }
|
|
83
|
+
let center = points.reduce(CGPoint.zero) { result, point in
|
|
84
|
+
CGPoint(x: result.x + point.x, y: result.y + point.y)
|
|
85
|
+
}
|
|
86
|
+
let centerPoint = CGPoint(x: center.x / CGFloat(points.count), y: center.y / CGFloat(points.count))
|
|
87
|
+
let totalDistance = points.reduce(CGFloat(0)) { result, point in
|
|
88
|
+
result + hypot(point.x - centerPoint.x, point.y - centerPoint.y)
|
|
89
|
+
}
|
|
90
|
+
return totalDistance / CGFloat(points.count)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
13
93
|
|
|
14
94
|
extension UIApplication {
|
|
15
95
|
// swiftlint:disable:next line_length
|
|
@@ -35,7 +115,7 @@ extension CapacitorUpdaterPlugin: UIGestureRecognizerDelegate {
|
|
|
35
115
|
let shouldInstall = self.shakeMenuGesture == Self.shakeMenuGestureThreeFingerPinch &&
|
|
36
116
|
(self.shakeMenuEnabled || self.shakeChannelSelectorEnabled)
|
|
37
117
|
|
|
38
|
-
guard shouldInstall, let targetView = self.bridge?.
|
|
118
|
+
guard shouldInstall, let targetView = self.bridge?.viewController?.view ?? self.bridge?.webView else {
|
|
39
119
|
self.removeShakeMenuGestureRecognizer()
|
|
40
120
|
return
|
|
41
121
|
}
|
|
@@ -46,11 +126,14 @@ extension CapacitorUpdaterPlugin: UIGestureRecognizerDelegate {
|
|
|
46
126
|
|
|
47
127
|
self.removeShakeMenuGestureRecognizer()
|
|
48
128
|
|
|
49
|
-
let recognizer =
|
|
129
|
+
let recognizer = ThreeFingerPinchGestureRecognizer(target: self, action: #selector(self.handleShakeMenuPinch(_:)))
|
|
50
130
|
recognizer.cancelsTouchesInView = false
|
|
51
131
|
recognizer.delaysTouchesBegan = false
|
|
52
132
|
recognizer.delaysTouchesEnded = false
|
|
53
133
|
recognizer.delegate = self
|
|
134
|
+
recognizer.onTrackingStarted = { [weak self] in
|
|
135
|
+
self?.logger.info("Three finger pinch tracking started")
|
|
136
|
+
}
|
|
54
137
|
targetView.addGestureRecognizer(recognizer)
|
|
55
138
|
self.shakeMenuPinchGestureRecognizer = recognizer
|
|
56
139
|
self.logger.info("Three finger pinch menu gesture initialized")
|
|
@@ -66,12 +149,8 @@ extension CapacitorUpdaterPlugin: UIGestureRecognizerDelegate {
|
|
|
66
149
|
}
|
|
67
150
|
}
|
|
68
151
|
|
|
69
|
-
@objc func handleShakeMenuPinch(_ recognizer:
|
|
70
|
-
|
|
71
|
-
self.shakeMenuPinchGestureTriggered = false
|
|
72
|
-
return
|
|
73
|
-
}
|
|
74
|
-
guard recognizer.state == .changed, !self.shakeMenuPinchGestureTriggered, recognizer.numberOfTouches == 3 else {
|
|
152
|
+
@objc func handleShakeMenuPinch(_ recognizer: ThreeFingerPinchGestureRecognizer) {
|
|
153
|
+
guard recognizer.state == .recognized, !self.shakeMenuPinchGestureTriggered else {
|
|
75
154
|
return
|
|
76
155
|
}
|
|
77
156
|
guard abs(recognizer.scale - 1) >= threeFingerPinchScaleDelta else {
|
|
@@ -85,6 +164,7 @@ extension CapacitorUpdaterPlugin: UIGestureRecognizerDelegate {
|
|
|
85
164
|
}
|
|
86
165
|
|
|
87
166
|
self.shakeMenuPinchGestureTriggered = true
|
|
167
|
+
self.logger.info("Three finger pinch detected")
|
|
88
168
|
_ = window.showCapacitorUpdaterMenu(plugin: self, bridge: bridge, gestureName: "Three finger pinch")
|
|
89
169
|
}
|
|
90
170
|
|
|
@@ -165,23 +245,13 @@ extension UIWindow {
|
|
|
165
245
|
|
|
166
246
|
let appName = Bundle.main.infoDictionary?["CFBundleDisplayName"] as? String ?? "App"
|
|
167
247
|
let title = "Preview \(appName) Menu"
|
|
168
|
-
let message = "Reload
|
|
248
|
+
let message = "Reload, switch, or leave the current preview."
|
|
169
249
|
let okButtonTitle = "Leave test app"
|
|
170
|
-
let reloadButtonTitle = "Reload
|
|
250
|
+
let reloadButtonTitle = "Reload preview"
|
|
171
251
|
let cancelButtonTitle = "Close menu"
|
|
172
252
|
|
|
173
253
|
let alertShake = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
|
174
254
|
|
|
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
255
|
alertShake.addAction(UIAlertAction(title: reloadButtonTitle, style: .default) { _ in
|
|
186
256
|
DispatchQueue.global(qos: .userInitiated).async {
|
|
187
257
|
if !plugin.reloadPreviewSessionFromShakeMenu() {
|
|
@@ -192,6 +262,20 @@ extension UIWindow {
|
|
|
192
262
|
}
|
|
193
263
|
})
|
|
194
264
|
|
|
265
|
+
if !plugin.previewMenuPreviews().isEmpty {
|
|
266
|
+
alertShake.addAction(UIAlertAction(title: "Switch preview", style: .default) { _ in
|
|
267
|
+
let showSelector = {
|
|
268
|
+
self.showPreviewSelector(plugin: plugin)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if let presenter = alertShake.presentingViewController {
|
|
272
|
+
presenter.dismiss(animated: true, completion: showSelector)
|
|
273
|
+
} else {
|
|
274
|
+
DispatchQueue.main.async(execute: showSelector)
|
|
275
|
+
}
|
|
276
|
+
})
|
|
277
|
+
}
|
|
278
|
+
|
|
195
279
|
if plugin.shakeChannelSelectorEnabled {
|
|
196
280
|
alertShake.addAction(UIAlertAction(title: "Switch channel", style: .default) { _ in
|
|
197
281
|
let showSelector = {
|
|
@@ -206,6 +290,16 @@ extension UIWindow {
|
|
|
206
290
|
})
|
|
207
291
|
}
|
|
208
292
|
|
|
293
|
+
alertShake.addAction(UIAlertAction(title: okButtonTitle, style: .default) { _ in
|
|
294
|
+
DispatchQueue.global(qos: .userInitiated).async {
|
|
295
|
+
if !plugin.leavePreviewSessionFromShakeMenu() {
|
|
296
|
+
DispatchQueue.main.async {
|
|
297
|
+
self.showError(message: "Could not leave the test app.", plugin: plugin)
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
})
|
|
302
|
+
|
|
209
303
|
alertShake.addAction(UIAlertAction(title: cancelButtonTitle, style: .default))
|
|
210
304
|
|
|
211
305
|
DispatchQueue.main.async {
|
|
@@ -277,6 +371,130 @@ extension UIWindow {
|
|
|
277
371
|
}
|
|
278
372
|
}
|
|
279
373
|
|
|
374
|
+
private func previewLabel(_ preview: [String: Any]) -> String {
|
|
375
|
+
let bundle = preview["bundle"] as? [String: Any]
|
|
376
|
+
let name = preview["name"] as? String
|
|
377
|
+
let version = bundle?["version"] as? String
|
|
378
|
+
var label = [name, version, preview["id"] as? String]
|
|
379
|
+
.compactMap { value in
|
|
380
|
+
guard let value, !value.isEmpty else { return nil }
|
|
381
|
+
return value
|
|
382
|
+
}
|
|
383
|
+
.first ?? "Preview"
|
|
384
|
+
if preview["isActive"] as? Bool == true {
|
|
385
|
+
label += " (current)"
|
|
386
|
+
}
|
|
387
|
+
return label
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
private func showPreviewSelector(plugin: CapacitorUpdaterPlugin) {
|
|
391
|
+
guard let topVC = UIApplication.topViewController() else {
|
|
392
|
+
return
|
|
393
|
+
}
|
|
394
|
+
if topVC.isKind(of: UIAlertController.self) {
|
|
395
|
+
plugin.logger.info("UIAlertController is already presented")
|
|
396
|
+
return
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
let previews = plugin.previewMenuPreviews()
|
|
400
|
+
guard !previews.isEmpty else {
|
|
401
|
+
self.showError(message: "No saved previews available on this device.", plugin: plugin)
|
|
402
|
+
return
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
let alert = UIAlertController(title: "Select Preview", message: "Choose a local preview to open", preferredStyle: .actionSheet)
|
|
406
|
+
let previewsToShow = Array(previews.prefix(5))
|
|
407
|
+
for preview in previewsToShow {
|
|
408
|
+
let title = self.previewLabel(preview)
|
|
409
|
+
let id = preview["id"] as? String ?? ""
|
|
410
|
+
alert.addAction(UIAlertAction(title: title, style: .default) { [weak self] _ in
|
|
411
|
+
self?.selectPreview(id: id, plugin: plugin)
|
|
412
|
+
})
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if previews.count > 5 {
|
|
416
|
+
alert.addAction(UIAlertAction(title: "More previews...", style: .default) { [weak self] _ in
|
|
417
|
+
self?.showSearchablePreviewPicker(previews: previews, plugin: plugin)
|
|
418
|
+
})
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
|
|
422
|
+
|
|
423
|
+
if let popoverController = alert.popoverPresentationController {
|
|
424
|
+
popoverController.sourceView = self
|
|
425
|
+
popoverController.sourceRect = CGRect(x: self.bounds.midX, y: self.bounds.midY, width: 0, height: 0)
|
|
426
|
+
popoverController.permittedArrowDirections = []
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
topVC.present(alert, animated: true)
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
private func showSearchablePreviewPicker(previews: [[String: Any]], plugin: CapacitorUpdaterPlugin) {
|
|
433
|
+
let alert = UIAlertController(title: "Search Previews", message: "Enter preview name or version", preferredStyle: .alert)
|
|
434
|
+
|
|
435
|
+
alert.addTextField { textField in
|
|
436
|
+
textField.placeholder = "Preview name..."
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
alert.addAction(UIAlertAction(title: "Search", style: .default) { [weak self, weak alert] _ in
|
|
440
|
+
guard let self else { return }
|
|
441
|
+
guard let searchText = alert?.textFields?.first?.text?.lowercased(), !searchText.isEmpty else {
|
|
442
|
+
self.showPreviewSelector(plugin: plugin)
|
|
443
|
+
return
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
let filtered = previews.filter { self.previewLabel($0).lowercased().contains(searchText) }
|
|
447
|
+
if filtered.isEmpty {
|
|
448
|
+
self.showError(message: "No previews found matching '\(searchText)'", plugin: plugin)
|
|
449
|
+
} else if filtered.count == 1, let id = filtered[0]["id"] as? String {
|
|
450
|
+
self.selectPreview(id: id, plugin: plugin)
|
|
451
|
+
} else {
|
|
452
|
+
self.presentPreviewPicker(previews: filtered, plugin: plugin)
|
|
453
|
+
}
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
|
|
457
|
+
|
|
458
|
+
DispatchQueue.main.async {
|
|
459
|
+
if let topVC = UIApplication.topViewController() {
|
|
460
|
+
topVC.present(alert, animated: true)
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
private func presentPreviewPicker(previews: [[String: Any]], plugin: CapacitorUpdaterPlugin) {
|
|
466
|
+
let alert = UIAlertController(title: "Select Preview", message: "Choose a local preview to open", preferredStyle: .actionSheet)
|
|
467
|
+
for preview in previews.prefix(5) {
|
|
468
|
+
let id = preview["id"] as? String ?? ""
|
|
469
|
+
alert.addAction(UIAlertAction(title: self.previewLabel(preview), style: .default) { [weak self] _ in
|
|
470
|
+
self?.selectPreview(id: id, plugin: plugin)
|
|
471
|
+
})
|
|
472
|
+
}
|
|
473
|
+
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
|
|
474
|
+
|
|
475
|
+
if let popoverController = alert.popoverPresentationController {
|
|
476
|
+
popoverController.sourceView = self
|
|
477
|
+
popoverController.sourceRect = CGRect(x: self.bounds.midX, y: self.bounds.midY, width: 0, height: 0)
|
|
478
|
+
popoverController.permittedArrowDirections = []
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
DispatchQueue.main.async {
|
|
482
|
+
if let topVC = UIApplication.topViewController() {
|
|
483
|
+
topVC.present(alert, animated: true)
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
private func selectPreview(id: String, plugin: CapacitorUpdaterPlugin) {
|
|
489
|
+
DispatchQueue.global(qos: .userInitiated).async {
|
|
490
|
+
if !plugin.setPreviewFromShakeMenu(id: id) {
|
|
491
|
+
DispatchQueue.main.async {
|
|
492
|
+
self.showError(message: "Could not switch preview.", plugin: plugin)
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
280
498
|
@discardableResult
|
|
281
499
|
private func showChannelSelector(plugin: CapacitorUpdaterPlugin, bridge: CAPBridgeProtocol) -> Bool {
|
|
282
500
|
// Prevent multiple alerts from showing
|