@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.
- 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 +1 -1
- 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 +225 -16
- package/package.json +1 -1
|
@@ -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.
|
|
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 =
|
|
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:
|
|
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
|
|
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
|
|
239
|
+
let message = "Reload, switch, or leave the current preview."
|
|
169
240
|
let okButtonTitle = "Leave test app"
|
|
170
|
-
let reloadButtonTitle = "Reload
|
|
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
|