craft-native 0.0.122 → 0.0.124
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 +3 -0
- package/dist/android/src/index.d.ts +3 -0
- package/dist/android/src/index.js +17 -0
- package/dist/android/templates/AndroidManifest.xml.template +1 -1
- package/dist/android/templates/CraftNativeFlatList.kt.template +113 -6
- package/dist/android/templates/MainActivity.kt.template +17 -0
- package/dist/android/templates/MainActivityNative.kt.template +826 -68
- package/dist/api/mobile.d.ts +251 -0
- package/dist/cli.js +1 -1
- package/dist/index.cjs +22 -1
- package/dist/index.js +22 -1
- package/dist/ios/src/index.d.ts +90 -2
- package/dist/ios/src/index.js +177 -44
- package/dist/ios/templates/CraftApp.swift +1341 -51
- package/dist/ios/templates/CraftNativeActions.swift +97 -9
- package/dist/ios/templates/CraftNativeFlatList.swift +92 -12
- package/dist/ios/templates/CraftNativeScreen.swift +876 -64
- package/dist/ios/templates/CraftNativeUI.swift +562 -0
- package/dist/ios/templates/Info.plist.template +3 -3
- package/dist/mobile.js +187 -0
- package/package.json +1 -1
|
@@ -44,6 +44,10 @@ enum CraftNativeActions {
|
|
|
44
44
|
guard config.enableHaptics else { return failure("CAPABILITY_DISABLED", "Haptics is disabled") }
|
|
45
45
|
triggerHaptic(style: body["style"] as? String ?? "medium")
|
|
46
46
|
return .success(true)
|
|
47
|
+
case "hapticPrepare":
|
|
48
|
+
guard config.enableHaptics else { return failure("CAPABILITY_DISABLED", "Haptics is disabled") }
|
|
49
|
+
CraftHaptics.prepare(body["kind"] as? String)
|
|
50
|
+
return .success(true)
|
|
47
51
|
case "clipboardWrite":
|
|
48
52
|
guard config.enableClipboard else { return failure("CAPABILITY_DISABLED", "Clipboard is disabled") }
|
|
49
53
|
guard let text = body["text"] as? String else {
|
|
@@ -82,6 +86,12 @@ enum CraftNativeActions {
|
|
|
82
86
|
sync = perform(action: "getDeviceInfo", body: [:], config: config)
|
|
83
87
|
case ("Haptics", "impact"):
|
|
84
88
|
sync = perform(action: "haptic", body: ["style": args.first ?? NSNull()], config: config)
|
|
89
|
+
case ("Haptics", "notification"):
|
|
90
|
+
sync = perform(action: "haptic", body: ["style": (args.first as? String) ?? "success"], config: config)
|
|
91
|
+
case ("Haptics", "selection"):
|
|
92
|
+
sync = perform(action: "haptic", body: ["style": "selection"], config: config)
|
|
93
|
+
case ("Haptics", "prepare"):
|
|
94
|
+
sync = perform(action: "hapticPrepare", body: ["kind": args.first ?? NSNull()], config: config)
|
|
85
95
|
case ("Clipboard", "write"):
|
|
86
96
|
sync = perform(action: "clipboardWrite", body: ["text": args.first ?? NSNull()], config: config)
|
|
87
97
|
case ("Clipboard", "read"):
|
|
@@ -460,15 +470,7 @@ enum CraftNativeActions {
|
|
|
460
470
|
}
|
|
461
471
|
|
|
462
472
|
static func triggerHaptic(style: String) {
|
|
463
|
-
|
|
464
|
-
case "light": UIImpactFeedbackGenerator(style: .light).impactOccurred()
|
|
465
|
-
case "heavy": UIImpactFeedbackGenerator(style: .heavy).impactOccurred()
|
|
466
|
-
case "success": UINotificationFeedbackGenerator().notificationOccurred(.success)
|
|
467
|
-
case "warning": UINotificationFeedbackGenerator().notificationOccurred(.warning)
|
|
468
|
-
case "error": UINotificationFeedbackGenerator().notificationOccurred(.error)
|
|
469
|
-
case "selection": UISelectionFeedbackGenerator().selectionChanged()
|
|
470
|
-
default: UIImpactFeedbackGenerator(style: .medium).impactOccurred()
|
|
471
|
-
}
|
|
473
|
+
CraftHaptics.play(style)
|
|
472
474
|
}
|
|
473
475
|
|
|
474
476
|
static func currentAppState() -> String {
|
|
@@ -532,3 +534,89 @@ enum CraftNativeActions {
|
|
|
532
534
|
]
|
|
533
535
|
}
|
|
534
536
|
}
|
|
537
|
+
|
|
538
|
+
/// The Taptic Engine's generators, held for the life of the process.
|
|
539
|
+
///
|
|
540
|
+
/// Each haptic used to make a fresh generator at the moment of the tap. A new
|
|
541
|
+
/// generator has to wake the engine before it can play, which is the tens of
|
|
542
|
+
/// milliseconds between a finger landing and the feel that makes web haptics
|
|
543
|
+
/// read as late. These are made once, prepared again after every use, and
|
|
544
|
+
/// `prepare` lets a page warm one before a gesture it knows is coming (the
|
|
545
|
+
/// start of a drag, a long press about to complete).
|
|
546
|
+
///
|
|
547
|
+
/// The three kinds are the three UIKit has, and they are not interchangeable:
|
|
548
|
+
/// a notification is the success, warning or error pattern, a selection is the
|
|
549
|
+
/// detent a picker plays, and an impact is a single tap at one of five weights.
|
|
550
|
+
/// The page used to get impacts for all of them.
|
|
551
|
+
///
|
|
552
|
+
/// Main thread only, like every UIFeedbackGenerator. Every caller is.
|
|
553
|
+
enum CraftHaptics {
|
|
554
|
+
private static var impacts: [UIImpactFeedbackGenerator.FeedbackStyle: UIImpactFeedbackGenerator] = [:]
|
|
555
|
+
private static var notifier: UINotificationFeedbackGenerator?
|
|
556
|
+
private static var selector: UISelectionFeedbackGenerator?
|
|
557
|
+
|
|
558
|
+
/// An impact weight by name, or nil for a name that is not one.
|
|
559
|
+
static func impactStyle(_ name: String) -> UIImpactFeedbackGenerator.FeedbackStyle? {
|
|
560
|
+
switch name {
|
|
561
|
+
case "light": return .light
|
|
562
|
+
case "medium": return .medium
|
|
563
|
+
case "heavy": return .heavy
|
|
564
|
+
case "soft": return .soft
|
|
565
|
+
case "rigid": return .rigid
|
|
566
|
+
default: return nil
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/// Play one haptic: an impact weight, a notification type, or `selection`.
|
|
571
|
+
/// Anything else plays a medium impact, as an unknown style always has.
|
|
572
|
+
static func play(_ style: String) {
|
|
573
|
+
switch style {
|
|
574
|
+
case "success": notification().notificationOccurred(.success); notification().prepare()
|
|
575
|
+
case "warning": notification().notificationOccurred(.warning); notification().prepare()
|
|
576
|
+
case "error": notification().notificationOccurred(.error); notification().prepare()
|
|
577
|
+
case "selection": selection().selectionChanged(); selection().prepare()
|
|
578
|
+
default:
|
|
579
|
+
let generator = impact(impactStyle(style) ?? .medium)
|
|
580
|
+
generator.impactOccurred()
|
|
581
|
+
generator.prepare()
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/// Warm a generator so the next haptic plays without the engine's wake-up
|
|
586
|
+
/// delay. `impact`, `notification` or `selection` for one kind, an impact
|
|
587
|
+
/// weight or notification type for that one, nothing for all of them.
|
|
588
|
+
/// UIKit keeps a prepared engine awake for a few seconds only.
|
|
589
|
+
static func prepare(_ kind: String?) {
|
|
590
|
+
switch kind {
|
|
591
|
+
case "impact": impact(.medium).prepare()
|
|
592
|
+
case "notification", "success", "warning", "error": notification().prepare()
|
|
593
|
+
case "selection": selection().prepare()
|
|
594
|
+
case let name? where impactStyle(name) != nil: impact(impactStyle(name)!).prepare()
|
|
595
|
+
default:
|
|
596
|
+
impact(.medium).prepare()
|
|
597
|
+
notification().prepare()
|
|
598
|
+
selection().prepare()
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
private static func impact(_ style: UIImpactFeedbackGenerator.FeedbackStyle) -> UIImpactFeedbackGenerator {
|
|
603
|
+
if let generator = impacts[style] { return generator }
|
|
604
|
+
let generator = UIImpactFeedbackGenerator(style: style)
|
|
605
|
+
impacts[style] = generator
|
|
606
|
+
return generator
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
private static func notification() -> UINotificationFeedbackGenerator {
|
|
610
|
+
if let notifier { return notifier }
|
|
611
|
+
let generator = UINotificationFeedbackGenerator()
|
|
612
|
+
notifier = generator
|
|
613
|
+
return generator
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
private static func selection() -> UISelectionFeedbackGenerator {
|
|
617
|
+
if let selector { return selector }
|
|
618
|
+
let generator = UISelectionFeedbackGenerator()
|
|
619
|
+
selector = generator
|
|
620
|
+
return generator
|
|
621
|
+
}
|
|
622
|
+
}
|
|
@@ -56,13 +56,32 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
|
|
|
56
56
|
) -> UICollectionViewLayoutAttributes {
|
|
57
57
|
guard let hostedView = hostedView else { return layoutAttributes }
|
|
58
58
|
let attributes = layoutAttributes.copy() as! UICollectionViewLayoutAttributes
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
59
|
+
// A cell has no collectionView property; the list is its nearest
|
|
60
|
+
// collection view ancestor.
|
|
61
|
+
var ancestor = superview
|
|
62
|
+
while let view = ancestor, !(view is UICollectionView) { ancestor = view.superview }
|
|
63
|
+
if let collectionView = ancestor as? UICollectionView,
|
|
64
|
+
let flowLayout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout,
|
|
65
|
+
flowLayout.scrollDirection == .horizontal {
|
|
66
|
+
let target = CGSize(
|
|
67
|
+
width: UIView.layoutFittingCompressedSize.width,
|
|
68
|
+
height: layoutAttributes.size.height
|
|
69
|
+
)
|
|
70
|
+
let measured = hostedView.systemLayoutSizeFitting(
|
|
71
|
+
target,
|
|
72
|
+
withHorizontalFittingPriority: .fittingSizeLevel,
|
|
73
|
+
verticalFittingPriority: .required
|
|
74
|
+
)
|
|
75
|
+
attributes.size.width = max(1, measured.width)
|
|
76
|
+
} else {
|
|
77
|
+
let target = CGSize(width: layoutAttributes.size.width, height: UIView.layoutFittingCompressedSize.height)
|
|
78
|
+
let measured = hostedView.systemLayoutSizeFitting(
|
|
79
|
+
target,
|
|
80
|
+
withHorizontalFittingPriority: .required,
|
|
81
|
+
verticalFittingPriority: .fittingSizeLevel
|
|
82
|
+
)
|
|
83
|
+
attributes.size.height = max(1, measured.height)
|
|
84
|
+
}
|
|
66
85
|
return attributes
|
|
67
86
|
}
|
|
68
87
|
}
|
|
@@ -86,11 +105,20 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
|
|
|
86
105
|
private var renderItem: RenderItem?
|
|
87
106
|
private var recycleItem: ((String) -> Void)?
|
|
88
107
|
private var endReached: (() -> Void)?
|
|
108
|
+
var onScrollEvent: ((UIScrollView) -> Void)?
|
|
109
|
+
/// Called as the visible rows move, for a screen that lays out around them.
|
|
110
|
+
var onLayoutChanged: (() -> Void)?
|
|
111
|
+
var onScrollBeginDrag: ((UIScrollView) -> Void)?
|
|
112
|
+
var onScrollEndDrag: ((UIScrollView) -> Void)?
|
|
113
|
+
var onMomentumScrollBegin: ((UIScrollView) -> Void)?
|
|
114
|
+
var onMomentumScrollEnd: ((UIScrollView) -> Void)?
|
|
89
115
|
private var endReachedThreshold = 0.1
|
|
90
116
|
private var endReachedSignature: String?
|
|
117
|
+
private var dataContentSignature = ""
|
|
91
118
|
private var columns = 1
|
|
92
119
|
private var isApplyingSnapshot = false
|
|
93
120
|
private var pendingApply: (() -> Void)?
|
|
121
|
+
private var pendingThemeRefresh = false
|
|
94
122
|
|
|
95
123
|
init() {
|
|
96
124
|
let layout = CraftNativeFlatListLayout()
|
|
@@ -220,7 +248,11 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
|
|
|
220
248
|
}
|
|
221
249
|
|
|
222
250
|
let dataItems = next.filter { !$0.isChrome }
|
|
223
|
-
let contentSignature =
|
|
251
|
+
let contentSignature = dataItems.map {
|
|
252
|
+
let encoded = $0.signature.base64EncodedString()
|
|
253
|
+
return "\($0.identity.utf8.count):\($0.identity)\(encoded.utf8.count):\(encoded)"
|
|
254
|
+
}.joined()
|
|
255
|
+
dataContentSignature = contentSignature
|
|
224
256
|
if contentSignature != endReachedSignature { endReachedSignature = nil }
|
|
225
257
|
}
|
|
226
258
|
|
|
@@ -232,9 +264,53 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
|
|
|
232
264
|
renderItem = nil
|
|
233
265
|
recycleItem = nil
|
|
234
266
|
endReached = nil
|
|
267
|
+
dataContentSignature = ""
|
|
268
|
+
onScrollEvent = nil
|
|
269
|
+
onLayoutChanged = nil
|
|
270
|
+
onScrollBeginDrag = nil
|
|
271
|
+
onScrollEndDrag = nil
|
|
272
|
+
onMomentumScrollBegin = nil
|
|
273
|
+
onMomentumScrollEnd = nil
|
|
274
|
+
pendingThemeRefresh = false
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
func refreshThemeDefaults() {
|
|
278
|
+
if isApplyingSnapshot {
|
|
279
|
+
pendingThemeRefresh = true
|
|
280
|
+
return
|
|
281
|
+
}
|
|
282
|
+
var snapshot = diffableDataSource.snapshot()
|
|
283
|
+
let identities = snapshot.itemIdentifiers
|
|
284
|
+
guard !identities.isEmpty else { return }
|
|
285
|
+
pendingThemeRefresh = false
|
|
286
|
+
snapshot.reconfigureItems(identities)
|
|
287
|
+
isApplyingSnapshot = true
|
|
288
|
+
diffableDataSource.apply(snapshot, animatingDifferences: false) { [weak self] in
|
|
289
|
+
self?.finishApplyingSnapshot()
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
|
294
|
+
onScrollEvent?(scrollView)
|
|
295
|
+
onLayoutChanged?()
|
|
296
|
+
evaluateEndReached()
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
|
300
|
+
onScrollBeginDrag?(scrollView)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
|
304
|
+
onScrollEndDrag?(scrollView)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) {
|
|
308
|
+
onMomentumScrollBegin?(scrollView)
|
|
235
309
|
}
|
|
236
310
|
|
|
237
|
-
func
|
|
311
|
+
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
|
312
|
+
onMomentumScrollEnd?(scrollView)
|
|
313
|
+
}
|
|
238
314
|
|
|
239
315
|
func collectionView(
|
|
240
316
|
_ collectionView: UICollectionView,
|
|
@@ -244,7 +320,7 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
|
|
|
244
320
|
guard items.indices.contains(indexPath.item) else { return CGSize(width: bounds.width, height: 44) }
|
|
245
321
|
let item = items[indexPath.item]
|
|
246
322
|
if flowLayout.scrollDirection == .horizontal {
|
|
247
|
-
return CGSize(width:
|
|
323
|
+
return CGSize(width: 44, height: max(1, bounds.height))
|
|
248
324
|
}
|
|
249
325
|
let count = item.isChrome ? 1 : columns
|
|
250
326
|
let spacing = flowLayout.minimumInteritemSpacing * CGFloat(count - 1)
|
|
@@ -261,8 +337,7 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
|
|
|
261
337
|
let remaining = dataIndices.count - lastDataOrdinal - 1
|
|
262
338
|
let thresholdItems = max(1, Int(ceil(Double(dataIndices.count) * endReachedThreshold)))
|
|
263
339
|
guard remaining <= thresholdItems else { return }
|
|
264
|
-
let
|
|
265
|
-
let signature = "\(dataIndices.count):\(lastIdentity)"
|
|
340
|
+
let signature = dataContentSignature
|
|
266
341
|
guard endReachedSignature != signature else { return }
|
|
267
342
|
endReachedSignature = signature
|
|
268
343
|
endReached()
|
|
@@ -277,6 +352,11 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
|
|
|
277
352
|
pendingApply()
|
|
278
353
|
return
|
|
279
354
|
}
|
|
355
|
+
if self.pendingThemeRefresh {
|
|
356
|
+
self.pendingThemeRefresh = false
|
|
357
|
+
self.refreshThemeDefaults()
|
|
358
|
+
return
|
|
359
|
+
}
|
|
280
360
|
self.evaluateEndReached()
|
|
281
361
|
}
|
|
282
362
|
}
|